feat(dictate): use local socket-activated Parakeet

This commit is contained in:
jb
2026-08-26 12:38:45 -04:00
parent ea74dbc6a7
commit e234468a73
34 changed files with 6702 additions and 527 deletions
@@ -0,0 +1,5 @@
"""GPU-isolated local dictate service."""
from .protocol import PROTOCOL_VERSION
__all__ = ["PROTOCOL_VERSION"]
@@ -0,0 +1,22 @@
"""In-memory PCM validation and conversion."""
from __future__ import annotations
from array import array
import struct
MAX_PCM_BYTES = 5 * 60 * 16_000 * 2
class AudioError(ValueError):
pass
def pcm_s16le_to_floats(pcm: bytes, *, max_bytes: int = MAX_PCM_BYTES) -> array[float]:
"""Convert mono little-endian signed-16 PCM to a bounded float32 array."""
if len(pcm) > max_bytes:
raise AudioError("PCM exceeds the five-minute duration limit")
if len(pcm) % 2:
raise AudioError("PCM byte length must be even")
return array("f", (sample[0] / 32768.0 for sample in struct.iter_unpack("<h", pcm)))
@@ -0,0 +1,138 @@
"""Lazy, offline NeMo adapter for an explicit local Parakeet archive."""
from __future__ import annotations
from copy import deepcopy
import hashlib
import os
from pathlib import Path
from typing import Protocol, Sequence
MAX_TERMS_FILE_BYTES = 64 * 1024
MAX_CODING_TERMS = 512
MAX_TERM_BYTES = 128
PINNED_MODEL_SHA256 = "3cbdc85877e668ca7b82d0d56770eb1fac76691f55d6b97545e8d61ca588d10d"
class TranscriptionModel(Protocol):
def load(self) -> None: ...
def transcribe(self, samples: Sequence[float]) -> str: ...
def load_coding_terms(path: str | os.PathLike[str]) -> list[str]:
"""Load a bounded host-owned phrase list, ignoring comments and duplicates."""
terms_path = Path(path).expanduser()
if not terms_path.is_file():
raise ValueError("PI_DICTATE_TERMS_PATH must name an existing local dictionary")
if terms_path.stat().st_size > MAX_TERMS_FILE_BYTES:
raise ValueError(f"coding dictionary exceeds {MAX_TERMS_FILE_BYTES} bytes")
terms: list[str] = []
seen: set[str] = set()
for raw_line in terms_path.read_text(encoding="utf-8").splitlines():
term = raw_line.strip()
if not term or term.startswith("#"):
continue
if len(term.encode("utf-8")) > MAX_TERM_BYTES:
raise ValueError(f"coding dictionary terms may not exceed {MAX_TERM_BYTES} UTF-8 bytes")
if term not in seen:
terms.append(term)
seen.add(term)
if len(terms) > MAX_CODING_TERMS:
raise ValueError(f"coding dictionary may not exceed {MAX_CODING_TERMS} terms")
return terms
class NemoParakeetModel:
"""Load NeMo only when requested so core tests require neither CUDA nor NeMo."""
def __init__(
self,
model_path: str | os.PathLike[str],
*,
terms_path: str | os.PathLike[str] | None = None,
boost_alpha: float = 1.0,
expected_sha256: str = PINNED_MODEL_SHA256,
) -> None:
path = Path(model_path).expanduser()
if path.suffix != ".nemo" or not path.is_file():
raise ValueError("PI_DICTATE_MODEL_PATH must name an existing local .nemo file")
if boost_alpha <= 0:
raise ValueError("PI_DICTATE_BOOST_ALPHA must be positive")
self._model_path = path.resolve()
self._terms = load_coding_terms(terms_path) if terms_path is not None else []
self._boost_alpha = boost_alpha
self._expected_sha256 = expected_sha256
self._model: object | None = None
self._numpy: object | None = None
@property
def loaded(self) -> bool:
return self._model is not None
def load(self) -> None:
if self._model is not None:
return
with self._model_path.open("rb") as model_file:
actual_sha256 = hashlib.file_digest(model_file, "sha256").hexdigest()
if actual_sha256 != self._expected_sha256:
raise ValueError(
"model checksum mismatch: expected the pinned Parakeet revision"
)
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["TRANSFORMERS_OFFLINE"] = "1"
os.environ["HF_DATASETS_OFFLINE"] = "1"
os.environ["NEMO_OFFLINE"] = "1"
import numpy
from nemo.collections.asr.models import ASRModel
from omegaconf import open_dict
model = ASRModel.restore_from(
restore_path=str(self._model_path),
map_location="cuda",
)
model.to("cuda")
model.eval()
if self._terms:
decoding = deepcopy(model.cfg.decoding)
with open_dict(decoding):
greedy = decoding["greedy"]
greedy["boosting_tree"] = {
"key_phrases_list": list(self._terms),
"context_score": 1.0,
"depth_scaling": 2.0,
"unk_score": 0.0,
"final_eos_score": 1.0,
"score_per_phrase": 0.0,
"source_lang": "en",
"use_triton": True,
"uniform_weights": False,
"use_bpe_dropout": False,
}
greedy["boosting_tree_alpha"] = self._boost_alpha
model.change_decoding_strategy(decoding, verbose=False)
self._numpy = numpy
self._model = model
def transcribe(self, samples: Sequence[float]) -> str:
if self._model is None or self._numpy is None:
raise RuntimeError("model is not loaded")
numpy = self._numpy
audio = numpy.asarray(samples, dtype=numpy.float32) # type: ignore[attr-defined]
output: Sequence[object] = self._model.transcribe( # type: ignore[attr-defined]
audio=[audio],
batch_size=1,
num_workers=0,
verbose=False,
)
if not output:
return ""
hypothesis = output[0]
if isinstance(hypothesis, str):
return hypothesis
text = getattr(hypothesis, "text", None)
if isinstance(text, str):
return text
raise RuntimeError("NeMo returned an unsupported transcription result")
@@ -0,0 +1,195 @@
"""Bounded newline-delimited JSON protocol primitives for dictate protocol v1."""
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import Any, Mapping
PROTOCOL_VERSION = 1
MAX_FRAME_BYTES = 64 * 1024
MAX_TRANSCRIPT_BYTES = 32 * 1024
MAX_ERROR_DETAIL_BYTES = 2 * 1024
MAX_ID_BYTES = 128
class ProtocolError(ValueError):
"""A stable protocol error suitable for returning to a client."""
def __init__(
self,
code: str,
detail: str,
*,
close_connection: bool = False,
request_id: str = "",
generation_id: str | None = None,
) -> None:
super().__init__(detail)
self.code = code
self.detail = detail
self.close_connection = close_connection
self.request_id = request_id
self.generation_id = generation_id
@dataclass(frozen=True)
class Request:
version: int
type: str
request_id: str
generation_id: str | None = None
_REQUEST_FIELDS = {
"hello": frozenset({"version", "type", "requestId"}),
"ping": frozenset({"version", "type", "requestId"}),
"start": frozenset({"version", "type", "requestId", "generationId"}),
"stop": frozenset({"version", "type", "requestId", "generationId"}),
"cancel": frozenset({"version", "type", "requestId", "generationId"}),
}
def _bounded_utf8(value: str, limit: int) -> str:
raw = value.encode("utf-8", errors="replace")
if len(raw) <= limit:
return value
return raw[:limit].decode("utf-8", errors="ignore")
def _validate_id(value: Any, field: str) -> str:
if not isinstance(value, str) or not value:
raise ProtocolError("invalid_request", f"{field} must be a non-empty string")
try:
encoded = value.encode("utf-8")
except UnicodeEncodeError as error:
raise ProtocolError("invalid_request", f"{field} must contain valid Unicode") from error
if len(encoded) > MAX_ID_BYTES:
raise ProtocolError("invalid_request", f"{field} exceeds {MAX_ID_BYTES} UTF-8 bytes")
return value
def _safe_id(value: Any) -> str:
try:
return _validate_id(value, "identifier")
except ProtocolError:
return ""
def parse_request(frame: Mapping[str, Any]) -> Request:
"""Validate a decoded request and reject all fields outside its exact schema."""
if not isinstance(frame, Mapping):
raise ProtocolError("invalid_request", "request must be a JSON object")
request_id_value = frame.get("requestId")
request_id = _safe_id(request_id_value)
version = frame.get("version")
if not isinstance(version, int) or isinstance(version, bool):
raise ProtocolError("invalid_request", "version must be an integer", request_id=request_id)
if version != PROTOCOL_VERSION:
raise ProtocolError(
"unsupported_version",
f"protocol version {version} is unsupported",
close_connection=True,
request_id=request_id,
)
request_type = frame.get("type")
if not isinstance(request_type, str) or request_type not in _REQUEST_FIELDS:
raise ProtocolError("invalid_request", "unknown request type", request_id=request_id)
expected_fields = _REQUEST_FIELDS[request_type]
if frozenset(frame) != expected_fields:
raise ProtocolError(
"invalid_request",
f"{request_type} requires exactly: {', '.join(sorted(expected_fields))}",
request_id=request_id,
)
request_id = _validate_id(request_id_value, "requestId")
generation_id = None
if "generationId" in expected_fields:
generation_id = _validate_id(frame.get("generationId"), "generationId")
return Request(version, request_type, request_id, generation_id)
class FrameDecoder:
"""Incrementally decode bounded newline-delimited JSON objects."""
def __init__(self) -> None:
self._buffer = bytearray()
self._failed = False
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)
frames: list[dict[str, Any]] = []
while True:
newline = self._buffer.find(b"\n")
if newline < 0:
if len(self._buffer) > MAX_FRAME_BYTES:
self._fail("frame_too_large", f"frame exceeds {MAX_FRAME_BYTES} bytes")
return frames
if 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]
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
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)
def _fail(self, code: str, detail: str) -> None:
self._failed = True
self._buffer.clear()
raise ProtocolError(code, detail, close_connection=True)
def encode_frame(frame: Mapping[str, Any]) -> bytes:
"""Encode one bounded service frame, applying the error-detail output cap."""
payload = dict(frame)
if payload.get("type") == "result":
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")
if payload.get("type") == "error":
detail = payload.get("detail", "")
if not isinstance(detail, str):
detail = str(detail)
payload["detail"] = _bounded_utf8(detail, MAX_ERROR_DETAIL_BYTES)
payload["requestId"] = _safe_id(payload.get("requestId"))
generation_id = _safe_id(payload.get("generationId"))
if generation_id:
payload["generationId"] = generation_id
else:
payload.pop("generationId", None)
encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
if len(encoded) > MAX_FRAME_BYTES:
raise ProtocolError("frame_too_large", "encoded frame exceeds output limit")
return encoded + b"\n"
def error_event(error: ProtocolError) -> dict[str, Any]:
event: 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),
}
generation_id = _safe_id(error.generation_id)
if generation_id:
event["generationId"] = generation_id
return event
@@ -0,0 +1,297 @@
"""PipeWire recorder adapter with bounded in-memory output."""
from __future__ import annotations
from dataclasses import dataclass
import os
import signal
import subprocess
import threading
import time
from typing import BinaryIO, Callable, Protocol
from .audio import MAX_PCM_BYTES
PW_RECORD_COMMAND = [
"pw-record",
"--raw",
"--format",
"s16",
"--rate",
"16000",
"--channels",
"1",
"-",
]
MAX_STDERR_BYTES = 64 * 1024
class Process(Protocol):
pid: int
stdout: BinaryIO
stderr: BinaryIO
returncode: int | None
def poll(self) -> int | None: ...
def terminate(self) -> None: ...
def kill(self) -> None: ...
def wait(self, timeout: float | None = None) -> int: ...
ProcessFactory = Callable[..., Process]
ProcessGroupSignal = Callable[[int, int], None]
@dataclass(frozen=True)
class Capture:
pcm: bytes
returncode: int
signal: int | None
stderr_tail: bytes
class RecorderError(RuntimeError):
def __init__(
self,
code: str,
detail: str,
*,
returncode: int | None = None,
signal_number: int | None = None,
) -> None:
super().__init__(detail)
self.code = code
self.detail = detail
self.returncode = returncode
self.signal_number = signal_number
class PwRecorder:
"""Capture raw PCM while continuously draining both child pipes."""
def __init__(
self,
*,
process_factory: ProcessFactory = subprocess.Popen,
max_bytes: int = MAX_PCM_BYTES,
stderr_limit: int = MAX_STDERR_BYTES,
stop_timeout: float = 3.0,
clock: Callable[[], float] = time.monotonic,
process_group_signal: ProcessGroupSignal = os.killpg,
) -> None:
self._process_factory = process_factory
self._max_bytes = max_bytes
self._stderr_limit = stderr_limit
self._stop_timeout = stop_timeout
self._clock = clock
self._process_group_signal = process_group_signal
self._process: Process | None = None
self._stdout_thread: threading.Thread | None = None
self._stderr_thread: threading.Thread | None = None
self._pcm = bytearray()
self._stderr = bytearray()
self._lock = threading.Lock()
self._intentional = False
self._duration_limited = False
self._duration_stop_at: float | None = None
self._reported = False
@property
def buffered_bytes(self) -> int:
with self._lock:
return len(self._pcm)
def start(self) -> None:
if self._process is not None:
raise RecorderError("invalid_state", "recorder is already started")
try:
process = self._process_factory(
list(PW_RECORD_COMMAND),
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
except OSError as error:
raise RecorderError("recorder_spawn", f"could not start pw-record: {error}") from error
if process.stdout is None or process.stderr is None:
raise RecorderError("recorder_spawn", "pw-record pipes were not created")
self._process = process
self._stdout_thread = threading.Thread(target=self._drain_stdout, daemon=True)
self._stderr_thread = threading.Thread(target=self._drain_stderr, daemon=True)
self._stdout_thread.start()
self._stderr_thread.start()
def stop(self) -> Capture:
return self._finish(discard=False)
def cancel(self) -> None:
try:
self._finish(discard=True)
finally:
with self._lock:
self._pcm.clear()
def poll_unexpected(self) -> RecorderError | None:
process = self._require_process()
returncode = process.poll()
if returncode is None:
with self._lock:
duration_stop_at = self._duration_stop_at
if (
not self._intentional
and not self._reported
and duration_stop_at is not None
and self._clock() - duration_stop_at >= self._stop_timeout
):
self._reported = True
self._signal_group(process, signal.SIGKILL)
process.wait(timeout=self._stop_timeout)
self._join_drainers()
with self._lock:
self._pcm.clear()
return RecorderError(
"duration_limit",
"recording exceeded the five-minute duration limit; pw-record was killed after ignoring SIGTERM",
returncode=process.returncode,
signal_number=signal.SIGKILL,
)
return None
if self._intentional or self._reported:
return None
self._reported = True
self._join_drainers()
with self._lock:
self._pcm.clear()
if self._duration_limited:
return RecorderError(
"duration_limit",
"recording exceeded the five-minute duration limit",
returncode=returncode,
signal_number=-returncode if returncode < 0 else None,
)
return self._exit_error(returncode, "pw-record exited unexpectedly")
def _finish(self, *, discard: bool) -> Capture:
process = self._require_process()
self._intentional = True
terminated_running_process = process.poll() is None
if terminated_running_process:
self._signal_group(process, signal.SIGTERM)
try:
returncode = process.wait(timeout=self._stop_timeout)
except subprocess.TimeoutExpired as error:
self._signal_group(process, signal.SIGKILL)
process.wait(timeout=self._stop_timeout)
self._join_drainers()
with self._lock:
self._pcm.clear()
raise RecorderError(
"recorder_timeout",
"pw-record did not stop after SIGTERM and was killed",
returncode=process.returncode,
signal_number=signal.SIGKILL,
) from error
self._join_drainers()
stderr_tail = self._stderr_bytes()
if self._duration_limited:
with self._lock:
self._pcm.clear()
raise RecorderError(
"duration_limit",
"recording exceeded the five-minute duration limit",
returncode=returncode,
signal_number=-returncode if returncode < 0 else None,
)
clean_pipewire_sigterm = (
terminated_running_process and returncode == 1 and not stderr_tail
)
if returncode not in (0, -signal.SIGTERM) and not clean_pipewire_sigterm:
with self._lock:
self._pcm.clear()
raise self._exit_error(returncode, "pw-record failed while stopping")
with self._lock:
pcm = b"" if discard else bytes(self._pcm)
self._pcm.clear()
return Capture(
pcm=pcm,
returncode=returncode,
signal=-returncode if returncode < 0 else None,
stderr_tail=stderr_tail,
)
def _drain_stdout(self) -> None:
process = self._require_process()
while True:
chunk = process.stdout.read(64 * 1024)
if not chunk:
return
with self._lock:
if self._duration_limited:
continue
if len(self._pcm) + len(chunk) > self._max_bytes:
self._duration_limited = True
self._duration_stop_at = self._clock()
self._pcm.clear()
else:
self._pcm.extend(chunk)
continue
if process.poll() is None:
self._signal_group(process, signal.SIGTERM)
def _drain_stderr(self) -> None:
process = self._require_process()
while True:
chunk = process.stderr.read(16 * 1024)
if not chunk:
return
with self._lock:
self._stderr.extend(chunk)
overflow = len(self._stderr) - self._stderr_limit
if overflow > 0:
del self._stderr[:overflow]
def _join_drainers(self) -> None:
for thread in (self._stdout_thread, self._stderr_thread):
if thread is None:
continue
thread.join(timeout=self._stop_timeout)
if thread.is_alive():
raise RecorderError(
"recorder_timeout",
"pw-record pipe did not close after the process group stopped",
)
def _signal_group(self, process: Process, signal_number: int) -> None:
try:
self._process_group_signal(process.pid, signal_number)
except ProcessLookupError:
pass
def _stderr_bytes(self) -> bytes:
with self._lock:
return bytes(self._stderr)
def _exit_error(self, returncode: int, prefix: str) -> RecorderError:
if returncode < 0:
classification = f"signal {-returncode}"
signal_number = -returncode
else:
classification = f"code {returncode}"
signal_number = None
stderr = self._stderr_bytes().decode("utf-8", errors="replace").strip()
detail = f"{prefix} with {classification}"
if stderr:
detail = f"{detail}: {stderr}"
return RecorderError(
"recorder_exit",
detail,
returncode=returncode,
signal_number=signal_number,
)
def _require_process(self) -> Process:
if self._process is None:
raise RecorderError("invalid_state", "recorder has not started")
return self._process
@@ -0,0 +1,570 @@
"""Dictate service state machine and Unix-domain socket loop."""
from __future__ import annotations
from collections.abc import Sequence
from concurrent.futures import Executor, Future, ThreadPoolExecutor
from dataclasses import dataclass
import os
from queue import Empty, SimpleQueue
import selectors
import socket
import time
from typing import Callable, Protocol
from .audio import AudioError, pcm_s16le_to_floats
from .model import NemoParakeetModel, TranscriptionModel
from .protocol import (
MAX_FRAME_BYTES,
MAX_TRANSCRIPT_BYTES,
PROTOCOL_VERSION,
FrameDecoder,
ProtocolError,
Request,
encode_frame,
error_event,
parse_request,
)
from .recorder import Capture, PwRecorder, RecorderError
DEFAULT_IDLE_SECONDS = 15 * 60
MAX_CLIENT_CONNECTIONS = 16
MAX_CONNECTION_OUTPUT_BYTES = MAX_FRAME_BYTES
class Recorder(Protocol):
def start(self) -> None: ...
def stop(self) -> Capture: ...
def cancel(self) -> None: ...
def poll_unexpected(self) -> RecorderError | None: ...
@dataclass(frozen=True)
class Outbound:
client_id: str
frame: dict[str, object]
@dataclass
class _Generation:
owner: str
generation_id: str
request_id: str
state: str
recorder: Recorder | None = None
tombstoned: bool = False
@dataclass(frozen=True)
class _Completion:
kind: str
generation_id: str
future: Future[object]
class DictateServiceCore:
"""Single-generation protocol state machine with injected host boundaries."""
def __init__(
self,
*,
model: TranscriptionModel,
recorder_factory: Callable[[], Recorder] = PwRecorder,
executor: Executor | None = None,
clock: Callable[[], float] = time.monotonic,
idle_seconds: float = DEFAULT_IDLE_SECONDS,
) -> None:
self._model = model
self._recorder_factory = recorder_factory
self._executor = executor or ThreadPoolExecutor(max_workers=1, thread_name_prefix="dictate")
self._owns_executor = executor is None
self._clock = clock
self._idle_seconds = idle_seconds
self._last_activity = clock()
self._clients: dict[str, bool] = {}
self._active: _Generation | None = None
self._model_loaded = bool(getattr(model, "loaded", False))
self._worker_kind: str | None = None
self._completions: SimpleQueue[_Completion] = SimpleQueue()
self._events: list[Outbound] = []
@property
def active_generation_id(self) -> str | None:
return self._active.generation_id if self._active else None
def accepted(self, client_id: str) -> None:
"""Track a socket without treating acceptance as protocol activity."""
self._clients.setdefault(client_id, False)
def handle_request(self, client_id: str, request: Request) -> None:
"""Handle one already parsed request and queue its observable events."""
self._last_activity = self._clock()
self._clients.setdefault(client_id, False)
if request.type == "hello":
self._clients[client_id] = True
self._emit(
client_id,
{
"version": PROTOCOL_VERSION,
"type": "pong",
"requestId": request.request_id,
"service": "pi-dictate",
"ready": self._model_loaded,
},
)
return
if not self._clients[client_id]:
self._request_error(client_id, request, "hello_required", "hello is required first")
return
if request.type == "ping":
self._emit(
client_id,
{
"version": PROTOCOL_VERSION,
"type": "pong",
"requestId": request.request_id,
"service": "pi-dictate",
"ready": self._model_loaded,
"busy": self._active is not None,
},
)
elif request.type == "start":
self._start(client_id, request)
elif request.type == "stop":
self._stop(client_id, request)
elif request.type == "cancel":
self._cancel(client_id, request)
def poll(self) -> None:
"""Apply completed workers and recorder exits without blocking the socket loop."""
while True:
try:
completion = self._completions.get_nowait()
except Empty:
break
self._apply_completion(completion)
active = self._active
if active and active.state == "listening" and active.recorder and self._worker_kind is None:
try:
failure = active.recorder.poll_unexpected()
except RecorderError as error:
failure = error
if failure is not None:
self._async_error(active, failure.code, failure.detail)
self._finish_generation(active, notify_idle=True)
def drain_events(self) -> list[Outbound]:
events, self._events = self._events, []
return events
def disconnect(self, client_id: str) -> None:
self._clients.pop(client_id, None)
active = self._active
if active is None or active.owner != client_id:
return
active.tombstoned = True
if active.state == "listening" and active.recorder is not None:
active.state = "stopping"
self._submit("cancel", active, active.recorder.cancel)
def should_exit(self) -> bool:
if self._active is not None or self._worker_kind is not None:
return False
return self._clock() - self._last_activity >= self._idle_seconds
def close(self) -> None:
active = self._active
if active and active.recorder and active.state == "listening":
try:
active.recorder.cancel()
except RecorderError:
pass
self._active = None
if self._owns_executor:
self._executor.shutdown(wait=True, cancel_futures=False)
def _start(self, client_id: str, request: Request) -> None:
assert request.generation_id is not None
if self._active is not None or self._worker_kind is not None:
self._request_error(client_id, request, "busy", "another generation is active")
return
active = _Generation(client_id, request.generation_id, request.request_id, "loading")
self._active = active
if not self._model_loaded:
self._state(active, "loading")
self._submit("load", active, self._model.load)
return
self._begin_recording(active)
def _begin_recording(self, active: _Generation) -> None:
if active.tombstoned:
self._finish_generation(active, notify_idle=False)
return
try:
recorder = self._recorder_factory()
recorder.start()
except RecorderError as error:
self._async_error(active, error.code, error.detail)
self._finish_generation(active, notify_idle=True)
return
except OSError as error:
self._async_error(active, "recorder_spawn", str(error))
self._finish_generation(active, notify_idle=True)
return
active.recorder = recorder
self._state(active, "listening")
def _stop(self, client_id: str, request: Request) -> None:
active = self._owned_generation(client_id, request)
if active is None:
return
if active.state != "listening" or active.recorder is None:
self._request_error(client_id, request, "invalid_state", f"cannot stop while {active.state}")
return
active.request_id = request.request_id
try:
failure = active.recorder.poll_unexpected()
except RecorderError as error:
failure = error
if failure is not None:
self._async_error(active, failure.code, failure.detail)
self._finish_generation(active, notify_idle=True)
return
active.state = "stopping"
self._state(active, "stopping", assign=False)
self._submit("stop", active, self._stop_and_convert, active.recorder)
def _cancel(self, client_id: str, request: Request) -> None:
active = self._owned_generation(client_id, request)
if active is None:
return
if active.tombstoned:
self._request_error(client_id, request, "invalid_state", "generation is already cancelled")
return
active.tombstoned = True
active.request_id = request.request_id
self._emit(
client_id,
{
"version": PROTOCOL_VERSION,
"type": "cancelled",
"requestId": active.request_id,
"generationId": active.generation_id,
},
)
if active.state == "listening" and active.recorder is not None:
active.state = "stopping"
self._submit("cancel", active, active.recorder.cancel)
def _owned_generation(self, client_id: str, request: Request) -> _Generation | None:
assert request.generation_id is not None
active = self._active
if active is None:
self._request_error(client_id, request, "invalid_state", "no generation is active")
return None
if active.owner != client_id:
self._request_error(client_id, request, "not_owner", "generation belongs to another client")
return None
if active.generation_id != request.generation_id:
self._request_error(
client_id,
request,
"generation_mismatch",
"generation ID does not match the active generation",
)
return None
return active
@staticmethod
def _stop_and_convert(recorder: Recorder) -> Sequence[float]:
capture = recorder.stop()
return pcm_s16le_to_floats(capture.pcm)
def _submit(
self,
kind: str,
active: _Generation,
function: Callable[..., object],
*args: object,
) -> None:
self._worker_kind = kind
future = self._executor.submit(function, *args)
future.add_done_callback(
lambda completed, kind=kind, generation_id=active.generation_id: self._completions.put(
_Completion(kind, generation_id, completed)
)
)
def _apply_completion(self, completion: _Completion) -> None:
if self._worker_kind == completion.kind:
self._worker_kind = None
active = self._active
if active is None or active.generation_id != completion.generation_id:
return
try:
result = completion.future.result()
except RecorderError as error:
if not active.tombstoned:
self._async_error(active, error.code, error.detail)
self._finish_generation(active, notify_idle=not active.tombstoned)
return
except AudioError as error:
if not active.tombstoned:
self._async_error(active, "invalid_audio", str(error))
self._finish_generation(active, notify_idle=not active.tombstoned)
return
except BaseException as error:
if not active.tombstoned:
code = "model_failure" if completion.kind in ("load", "infer") else "recorder_failure"
self._async_error(active, code, str(error))
self._finish_generation(active, notify_idle=not active.tombstoned)
return
if completion.kind == "load":
self._model_loaded = True
if active.tombstoned:
self._finish_generation(active, notify_idle=False)
else:
self._begin_recording(active)
elif completion.kind == "stop":
if active.tombstoned:
self._finish_generation(active, notify_idle=False)
return
samples = result
if not isinstance(samples, Sequence) or isinstance(samples, (str, bytes, bytearray)):
self._async_error(active, "invalid_audio", "audio conversion returned invalid samples")
self._finish_generation(active, notify_idle=True)
return
self._state(active, "transcribing")
self._submit("infer", active, self._model.transcribe, samples)
elif completion.kind == "infer":
if active.tombstoned:
self._finish_generation(active, notify_idle=False)
return
if not isinstance(result, str):
self._async_error(active, "model_failure", "model returned a non-string transcript")
elif len(result.encode("utf-8")) > MAX_TRANSCRIPT_BYTES:
self._async_error(active, "transcript_too_large", "transcript exceeds output limit")
else:
self._emit(
active.owner,
{
"version": PROTOCOL_VERSION,
"type": "result",
"requestId": active.request_id,
"generationId": active.generation_id,
"transcript": result,
},
)
self._finish_generation(active, notify_idle=True)
elif completion.kind == "cancel":
self._finish_generation(active, notify_idle=False)
def _finish_generation(self, active: _Generation, *, notify_idle: bool) -> None:
if self._active is not active:
return
self._active = None
self._last_activity = self._clock()
if notify_idle and active.owner in self._clients:
self._state(active, "idle", assign=False)
def _state(self, active: _Generation, state: str, *, assign: bool = True) -> None:
if assign:
active.state = state
self._emit(
active.owner,
{
"version": PROTOCOL_VERSION,
"type": "state",
"requestId": active.request_id,
"generationId": active.generation_id,
"state": state,
},
)
def _request_error(self, client_id: str, request: Request, code: str, detail: str) -> None:
error = ProtocolError(
code,
detail,
request_id=request.request_id,
generation_id=request.generation_id,
)
self._emit(client_id, error_event(error))
def _async_error(self, active: _Generation, code: str, detail: str) -> None:
error = ProtocolError(
code,
detail,
request_id=active.request_id,
generation_id=active.generation_id,
)
self._emit(active.owner, error_event(error))
def _emit(self, client_id: str, frame: dict[str, object]) -> None:
if client_id in self._clients:
self._events.append(Outbound(client_id, frame))
@dataclass
class _Connection:
socket: socket.socket
decoder: FrameDecoder
output: bytearray
close_after_write: bool = False
def inherited_listener(environ: dict[str, str] | None = None) -> socket.socket:
"""Return systemd's single inherited listener at fd 3."""
environment = os.environ if environ is None else environ
try:
listen_pid = int(environment.get("LISTEN_PID", "0"))
listen_fds = int(environment.get("LISTEN_FDS", "0"))
except ValueError as error:
raise RuntimeError("invalid systemd socket activation environment") from error
if listen_pid != os.getpid() or listen_fds != 1:
raise RuntimeError("expected exactly one systemd socket-activated listener")
listener = socket.socket(fileno=3)
listener.setblocking(False)
return listener
def serve(listener: socket.socket, core: DictateServiceCore) -> None:
"""Serve protocol v1 on an explicit listener until the core becomes idle."""
selector = selectors.DefaultSelector()
connections: dict[str, _Connection] = {}
sequence = 0
listener.setblocking(False)
selector.register(listener, selectors.EVENT_READ, None)
try:
while not core.should_exit():
core.poll()
for outbound in core.drain_events():
connection = connections.get(outbound.client_id)
if connection is None:
continue
try:
encoded = encode_frame(outbound.frame)
except ProtocolError:
_close_connection(selector, connections, core, outbound.client_id)
continue
if len(connection.output) + len(encoded) > MAX_CONNECTION_OUTPUT_BYTES:
_close_connection(selector, connections, core, outbound.client_id)
continue
connection.output.extend(encoded)
selector.modify(
connection.socket,
selectors.EVENT_READ | selectors.EVENT_WRITE,
outbound.client_id,
)
for key, mask in selector.select(timeout=0.1):
if key.data is None:
try:
client, _address = listener.accept()
except BlockingIOError:
continue
if len(connections) >= MAX_CLIENT_CONNECTIONS:
client.close()
continue
client.setblocking(False)
sequence += 1
client_id = f"client-{sequence}"
connections[client_id] = _Connection(client, FrameDecoder(), bytearray())
core.accepted(client_id)
selector.register(client, selectors.EVENT_READ, client_id)
continue
client_id = key.data
connection = connections.get(client_id)
if connection is None:
continue
if mask & selectors.EVENT_READ:
try:
data = connection.socket.recv(64 * 1024)
except BlockingIOError:
data = None
except (BrokenPipeError, ConnectionResetError):
_close_connection(selector, connections, core, client_id)
continue
if data == b"":
_close_connection(selector, connections, core, client_id)
continue
if data:
try:
for frame in connection.decoder.feed(data):
core.handle_request(client_id, parse_request(frame))
except ProtocolError as error:
connection.output.extend(encode_frame(error_event(error)))
if error.close_connection:
connection.close_after_write = True
if mask & selectors.EVENT_WRITE and connection.output:
try:
sent = connection.socket.send(connection.output)
except BlockingIOError:
sent = 0
except (BrokenPipeError, ConnectionResetError):
_close_connection(selector, connections, core, client_id)
continue
del connection.output[:sent]
if connection.close_after_write and not connection.output:
_close_connection(selector, connections, core, client_id)
elif client_id in connections:
events = selectors.EVENT_READ
if connection.output:
events |= selectors.EVENT_WRITE
selector.modify(connection.socket, events, client_id)
finally:
for client_id in list(connections):
_close_connection(selector, connections, core, client_id)
selector.unregister(listener)
listener.close()
selector.close()
core.close()
def _close_connection(
selector: selectors.BaseSelector,
connections: dict[str, _Connection],
core: DictateServiceCore,
client_id: str,
) -> None:
connection = connections.pop(client_id, None)
if connection is None:
return
core.disconnect(client_id)
try:
selector.unregister(connection.socket)
except KeyError:
pass
connection.socket.close()
def main() -> None:
model_path = os.environ.get("PI_DICTATE_MODEL_PATH")
if not model_path:
raise SystemExit("PI_DICTATE_MODEL_PATH must name an explicit local .nemo file")
terms_path = os.environ.get("PI_DICTATE_TERMS_PATH")
if not terms_path:
raise SystemExit("PI_DICTATE_TERMS_PATH must name the host coding dictionary")
idle_value = os.environ.get("PI_DICTATE_IDLE_SECONDS", str(DEFAULT_IDLE_SECONDS))
boost_value = os.environ.get("PI_DICTATE_BOOST_ALPHA", "1.0")
try:
idle_seconds = float(idle_value)
boost_alpha = float(boost_value)
except ValueError as error:
raise SystemExit("PI_DICTATE_IDLE_SECONDS and PI_DICTATE_BOOST_ALPHA must be numeric") from error
if idle_seconds <= 0:
raise SystemExit("PI_DICTATE_IDLE_SECONDS must be positive")
if boost_alpha <= 0:
raise SystemExit("PI_DICTATE_BOOST_ALPHA must be positive")
model = NemoParakeetModel(model_path, terms_path=terms_path, boost_alpha=boost_alpha)
core = DictateServiceCore(model=model, idle_seconds=idle_seconds)
listener = inherited_listener()
serve(listener, core)
if __name__ == "__main__":
main()