events.py 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. """NDJSON event envelope and codec shared by the STT daemon and its subscribers."""
  2. import json
  3. import time
  4. PROTOCOL_VERSION = 1
  5. TRANSCRIPT = "transcript"
  6. SEGMENT_DONE = "segment_done"
  7. SPEECH_START = "speech_start"
  8. SPEECH_STOP = "speech_stop"
  9. HEARTBEAT = "heartbeat"
  10. STATUS = "status"
  11. RING_END = "ring_end"
  12. SPEAKERS = "speakers"
  13. ALL_TYPES = frozenset(
  14. {
  15. TRANSCRIPT,
  16. SEGMENT_DONE,
  17. SPEECH_START,
  18. SPEECH_STOP,
  19. HEARTBEAT,
  20. STATUS,
  21. RING_END,
  22. SPEAKERS,
  23. }
  24. )
  25. _REQUIRED_FIELDS = ("v", "type", "seq", "ts")
  26. class DecodeError(ValueError):
  27. """Raised when a line cannot be decoded into a valid event envelope."""
  28. def make_event(type_: str, seq: int, ts: float | None = None, **fields) -> dict:
  29. """Build an event envelope, adding any extra keyword arguments as fields."""
  30. event = {
  31. "v": PROTOCOL_VERSION,
  32. "type": type_,
  33. "seq": int(seq),
  34. "ts": time.time() if ts is None else float(ts),
  35. }
  36. event.update(fields)
  37. return event
  38. def encode(event: dict) -> bytes:
  39. """Encode an event as one newline-terminated UTF-8 NDJSON line."""
  40. return (json.dumps(event, ensure_ascii=False) + "\n").encode("utf-8")
  41. def decode(line: bytes | bytearray | str) -> dict:
  42. """Decode one NDJSON line into an event dict, validating the envelope."""
  43. if isinstance(line, (bytes, bytearray)):
  44. try:
  45. text = bytes(line).decode("utf-8")
  46. except UnicodeDecodeError as e:
  47. raise DecodeError(f"invalid utf-8: {e}") from e
  48. elif isinstance(line, str):
  49. text = line
  50. else:
  51. raise DecodeError(f"expected bytes or str, got {type(line).__name__}")
  52. try:
  53. event = json.loads(text)
  54. except json.JSONDecodeError as e:
  55. raise DecodeError(f"invalid json: {e}") from e
  56. if not isinstance(event, dict):
  57. raise DecodeError("event must be a JSON object")
  58. for field in _REQUIRED_FIELDS:
  59. if field not in event:
  60. raise DecodeError(f"missing required field: {field}")
  61. return event