events.py 2.0 KB

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