feat(dictate): support configurable ASR backends
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user