feat(dictate): support configurable ASR backends

This commit is contained in:
jb
2026-09-03 00:08:00 -04:00
parent 67877297d0
commit 2c3ee1f014
40 changed files with 7055 additions and 489 deletions
@@ -51,9 +51,9 @@ _REQUEST_FIELDS = {
def _bounded_utf8(value: str, limit: int) -> str:
# Normalize replacement characters even when the original string contains
# lone surrogates. This keeps error output serializable and bounded.
raw = value.encode("utf-8", errors="replace")
if len(raw) <= limit:
return value
return raw[:limit].decode("utf-8", errors="ignore")
@@ -100,7 +100,11 @@ def parse_request(frame: Mapping[str, Any]) -> Request:
raise ProtocolError("invalid_request", "unknown request type", request_id=request_id)
expected_fields = _REQUEST_FIELDS[request_type]
if frozenset(frame) != expected_fields:
try:
actual_fields = frozenset(frame)
except TypeError as error:
raise ProtocolError("invalid_request", "request contains invalid field names", request_id=request_id) from error
if actual_fields != expected_fields:
raise ProtocolError(
"invalid_request",
f"{request_type} requires exactly: {', '.join(sorted(expected_fields))}",
@@ -125,28 +129,38 @@ class FrameDecoder:
def feed(self, data: bytes) -> list[dict[str, Any]]:
if self._failed:
raise ProtocolError("connection_closed", "decoder is closed", close_connection=True)
self._buffer.extend(data)
if not isinstance(data, bytes):
self._fail("malformed_json", "frame data is invalid")
frames: list[dict[str, Any]] = []
remaining = data
while True:
newline = self._buffer.find(b"\n")
# Consume each newline-delimited frame before considering the next one.
# The size limit applies to an individual frame, not to a recv batch:
# clients may pipeline any number of bounded frames in one feed call.
while remaining:
newline = remaining.find(b"\n")
if newline < 0:
if len(self._buffer) > MAX_FRAME_BYTES:
if len(self._buffer) + len(remaining) > MAX_FRAME_BYTES:
self._fail("frame_too_large", f"frame exceeds {MAX_FRAME_BYTES} bytes")
self._buffer.extend(remaining)
return frames
if newline > MAX_FRAME_BYTES:
if len(self._buffer) + newline > MAX_FRAME_BYTES:
self._fail("frame_too_large", f"frame exceeds {MAX_FRAME_BYTES} bytes")
raw = bytes(self._buffer[:newline])
del self._buffer[: newline + 1]
self._buffer.extend(remaining[:newline])
raw = bytes(self._buffer)
self._buffer.clear()
remaining = remaining[newline + 1 :]
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
except (UnicodeDecodeError, ValueError, RecursionError):
self._fail("malformed_json", "frame contains malformed JSON")
if not isinstance(value, dict):
self._fail("malformed_json", "frame must contain a JSON object")
frames.append(value)
return frames
def _fail(self, code: str, detail: str) -> None:
self._failed = True
self._buffer.clear()
@@ -161,8 +175,11 @@ def encode_frame(frame: Mapping[str, Any]) -> bytes:
transcript = payload.get("transcript")
if not isinstance(transcript, str):
raise ProtocolError("invalid_event", "result transcript must be a string")
if len(transcript.encode("utf-8")) > MAX_TRANSCRIPT_BYTES:
raise ProtocolError("transcript_too_large", "transcript exceeds output limit")
try:
if len(transcript.encode("utf-8")) > MAX_TRANSCRIPT_BYTES:
raise ProtocolError("transcript_too_large", "transcript exceeds output limit")
except UnicodeEncodeError as error:
raise ProtocolError("invalid_event", "result transcript contains invalid Unicode") from error
if payload.get("type") == "error":
detail = payload.get("detail", "")
if not isinstance(detail, str):
@@ -175,7 +192,10 @@ def encode_frame(frame: Mapping[str, Any]) -> bytes:
else:
payload.pop("generationId", None)
encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
try:
encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
except (TypeError, ValueError, UnicodeError, RecursionError, OverflowError) as error:
raise ProtocolError("invalid_event", "event contains unsupported values") from error
if len(encoded) > MAX_FRAME_BYTES:
raise ProtocolError("frame_too_large", "encoded frame exceeds output limit")
return encoded + b"\n"
@@ -186,8 +206,8 @@ def error_event(error: ProtocolError) -> dict[str, Any]:
"version": PROTOCOL_VERSION,
"type": "error",
"requestId": _safe_id(error.request_id),
"code": error.code,
"detail": _bounded_utf8(error.detail, MAX_ERROR_DETAIL_BYTES),
"code": _bounded_utf8(error.code if isinstance(error.code, str) else "service_error", MAX_ID_BYTES),
"detail": _bounded_utf8(error.detail if isinstance(error.detail, str) else str(error.detail), MAX_ERROR_DETAIL_BYTES),
}
generation_id = _safe_id(error.generation_id)
if generation_id: