Skip to content

dictation_service

dictation_service

The dictation service — the parts wired into one running thing.

Composes the four pieces of the push-to-talk stack:

hotkey (CGEventTap)  →  PTT state machine  →  mic capture  →
transcribe (speech backend)  →  clean paste (clipboard preserved)

The composition — which action leads to capture/transcribe/paste — is a pure method (_run_action) driven by injected callables, so the whole flow is testable with fakes: no microphone, no tap, no transcription model. The live wiring (start/stop) only attaches the real hotkey listener.

Classes

DictationService

DictationService(
    *,
    transcribe: Transcribe,
    paste: Paste,
    hotkey: str = "control",
    capture_factory: CaptureFactory = MicCapture,
    clock: Callable[[], float] | None = None,
    silence_rms: float = 0.15,
    on_status: Callable[[str], None] | None = None,
    history: bool = True,
    model_name: str = "",
    on_transcript: Callable[[str], None] | None = None,
    on_action: Callable[[Action], None] | None = None,
)

Drive dictation from key events, independent of any window.

Source code in src/diapason/desktop/dictation_service.py
def __init__(
    self,
    *,
    transcribe: Transcribe,
    paste: Paste,
    hotkey: str = "control",
    capture_factory: CaptureFactory = MicCapture,
    clock: Callable[[], float] | None = None,
    # Field observation (Carlito's MacBook mic): normal speech lands at
    # level ~1.0 on the 0–100 RMS scale, while true silence sits well
    # under 0.05. The earlier floor of 0.5 left only a 2× margin to real
    # speech — a soft-spoken word could be discarded as silence. 0.15
    # keeps ~7× margin to speech while still rejecting an untouched mic.
    silence_rms: float = 0.15,
    on_status: Callable[[str], None] | None = None,
    history: bool = True,
    model_name: str = "",
    on_transcript: Callable[[str], None] | None = None,
    on_action: Callable[[Action], None] | None = None,
) -> None:
    # Observer for anything that wants the delivered text (menu bar, UI).
    self._on_transcript = on_transcript
    # Fires the instant an action is dispatched, BEFORE any device work.
    # Opening the input stream costs tens of milliseconds, so feedback
    # driven by on_status (which reports after the fact) would lag the
    # keypress by exactly the interval the user is trying to confirm.
    # Anything hooked here must return immediately.
    self._on_action = on_action
    self._history = history
    self._model_name = model_name
    self._transcribe = transcribe
    self._paste = paste
    self._hotkey = hotkey
    self._capture_factory = capture_factory
    # Status callback: every stage of a session reports here. Without it
    # the pipeline is a black box — a mic without permission, a silent
    # buffer, an empty transcript and a failed paste all look identical
    # to the user: "nothing happened".
    self._on_status = on_status
    # Below this RMS (on the 0–100 scale) the buffer is treated as silence
    # and never sent to the transcriber. Skipping silence avoids a model
    # invocation — and, more importantly, the "you. thanks for watching"
    # hallucinations Whisper emits on empty audio.
    self._silence_rms = silence_rms
    self._ptt = PushToTalk()
    self._capture: Optional[MicCapture] = None
    self._listener = None
    if clock is None:
        import time

        clock = time.monotonic
    self._clock = clock

Functions:

float_mono_to_wav

float_mono_to_wav(
    samples: "Any", sample_rate: int = 16000
) -> bytes

Encode a float32 [-1, 1] mono array as 16-bit PCM WAV bytes (stdlib).

Source code in src/diapason/desktop/dictation_service.py
def float_mono_to_wav(samples: "Any", sample_rate: int = 16_000) -> bytes:  # noqa: F821
    """Encode a float32 [-1, 1] mono array as 16-bit PCM WAV bytes (stdlib)."""
    import numpy as np

    arr = np.asarray(samples, dtype="float32").reshape(-1)
    clipped = np.clip(arr, -1.0, 1.0)
    pcm = (clipped * 32767.0).astype("<i2").tobytes()
    buf = io.BytesIO()
    with wave.open(buf, "wb") as wav:
        wav.setnchannels(1)
        wav.setsampwidth(2)
        wav.setframerate(sample_rate)
        wav.writeframes(pcm)
    return buf.getvalue()