Skip to content

Formalise the dictation protocol; add Wyoming + OpenAI-compatible listeners - #1

Merged
z33b0t merged 2 commits into
mainfrom
protocol-wyoming-openai
Jul 25, 2026
Merged

Formalise the dictation protocol; add Wyoming + OpenAI-compatible listeners#1
z33b0t merged 2 commits into
mainfrom
protocol-wyoming-openai

Conversation

@z33b0t

Copy link
Copy Markdown
Contributor

Two commits: the protocol work itself, and the HUD change that consumes one of its new signals.

server: formalise the protocol, add Wyoming + OpenAI-compatible listeners

Documents the dictation protocol in docs/protocol.md, adds two optional compatibility surfaces beside it, and backs the lot with a GPU-free regression suite.

Protocol — dictation ids on start/stop echoed by every dictation-scoped event (a late final from a finished dictation could previously land in whatever the user was doing next); declared input formats converted to canonical 16 kHz mono PCM16 by server/pcm.py; an info handshake reporting model + ready/loading state; vad events exposing the server-side Silero VAD; partials split into stable committed and revisable live.

Lifecycle fixes — each of these previously wedged or silently dropped a dictation: a second start now supersedes the one in flight; stop always yields exactly one final; a failed decode clears the session; malformed frames close at most one connection.

Wyoming listener (server/wyoming.py) so Blurt can serve as a Home Assistant STT backend. Off by default — Wyoming has neither auth nor TLS, so an open port would bypass AUTH_TOKEN entirely. WYOMING_PORT=10300 opts in.

OpenAI-compatible transcription API (server/openai_api.py) on the existing TLS port: json/text/srt/vtt/verbose_json + SSE streaming, ffmpeg fallback, size/duration ceilings enforced before the body is read.

clients(HUD): drive meter brightness from the server's VAD

The waveform is fed by the local mic, so it ripples identically whether the server is listening or the socket died mid-sentence. Both desktop clients only spent vad on the pre-transcript placeholder, which stops rendering once the first word lands — exactly when a dead connection starts looking like a quiet room. Meter opacity now follows the same signal (full → 0.4 dimmed, eased ~270 ms so word boundaries can't strobe it).

Testing

scripts/test_protocol.py stubs the model and the VAD, so all five suites run in seconds without a GPU or a model download — native WebSocket protocol, Wyoming (including a round trip driven by the official wyoming client library Home Assistant uses), the OpenAI /v1 API, and PCM conversion. .github/workflows/server.yml runs them across Python 3.11–3.13.

The Mac and Windows client changes are compiled by mac.yml / windows.yml on this PR — they weren't buildable locally (Linux host), so CI is the first check on them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dcx379h8xKRwhmae1zhN5z

z33b0t added 2 commits July 25, 2026 00:01
…ners
Documents the dictation protocol in docs/protocol.md, adds two optional
compatibility surfaces beside it, and backs the lot with a GPU-free
regression suite so none of it drifts.
Protocol:
* start/stop carry a dictation id that every dictation-scoped event echoes,
so a late final from a finished dictation can't land in whatever the user
is doing next — finalisation is async, so this was reachable by stopping
and immediately restarting.
* start may declare its audio format; server/pcm.py converts to the
canonical 16 kHz mono PCM16 (linear resample with cross-frame
interpolation, partial-frame carry) instead of mis-transcribing anything
that isn't already 16 kHz.
* an info handshake reports model, ready/loading state and canonical
format; describe asks for it again.
* vad events expose the server-side Silero VAD, so the HUD can show that
the whole mic -> network -> server path is live, which the local waveform
cannot prove.
* partials split into stable `committed` and revisable `live`. Clients
render live at full strength: text only commits after a pause, so a
single uninterrupted utterance is entirely live, and de-emphasising it
would dim the whole transcript for the common case.
Lifecycle fixes — each of these previously wedged or silently dropped a
dictation: a second start now supersedes the one in flight instead of leaving
the session emitting under an id the client has moved on from; stop always
yields exactly one final, degrading to the committed text rather than losing
a slow decode; a failed decode clears the session so the connection recovers;
and malformed frames close at most one connection.
Wyoming listener (server/wyoming.py) so Blurt can serve as a Home Assistant
STT backend. Off by default: Wyoming has neither auth nor TLS, so an open
port would bypass AUTH_TOKEN entirely. WYOMING_PORT=10300 opts in,
WYOMING_HOST narrows the bind, and a port conflict — likely, since 10300 is
shared with other wyoming-* services — logs a warning instead of aborting
startup.
OpenAI-compatible transcription API (server/openai_api.py) on the existing
TLS port: json, text, srt, vtt, verbose_json and SSE streaming, ffmpeg
fallback for compressed input, and size/duration ceilings enforced before the
request body is read.
Tests: scripts/test_protocol.py stubs the model and the VAD so all five
suites run in seconds without a GPU, covering the malformed-input and
lifecycle cases as well as the happy path, including a round trip driven by
the official wyoming client library that Home Assistant uses.
.github/workflows/server.yml runs them across Python 3.11-3.13.
The dictation pill's waveform is fed by the *local* mic, so it ripples
identically whether the server is listening or the socket died mid-sentence.
The `vad` events added in 27db9fe already carry that answer, but both desktop
clients only spent them on the pre-transcript placeholder ("Hearing you…"
instead of "Listening…"), which stops rendering the moment the first word
lands — exactly when a dead connection starts looking like a quiet room.
Bind the meter's opacity to the same signal instead: full strength while the
server reports speech, dimmed to 0.4 when it doesn't. Dimmed rather than
hidden, because the local meter still has something to say — it's the one
control that shows the whole mic -> network -> server path is alive.
The value is eased at 0.18/tick at 60 Hz (~270 ms to settle) rather than
snapped: VAD toggles on word boundaries, and a hard cut would strobe the pill
through every inter-word gap. Sessions start muted — brightness is earned by
the server actually reporting speech, not assumed — so reset() zeroes it and
startRecording()/StartRecording seeds `hearing(false)` alongside the
placeholder. With no tick running there's nothing to ease the value, so
setHearing snaps in that case.
Mac seeds before show() and Windows after Show(), because the WPF HUD builds
its WaveformView lazily in Show() and has nothing to talk to before that.
docs/protocol.md: spell out what `vad` is *for* under its message entry — the
brightness mapping, the placeholder's narrow window, static/index.html's
status line — and note that ignoring it entirely is fine, you just can't tell
a quiet room from a dead socket.
@z33b0t
z33b0tforce-pushed the protocol-wyoming-openai branch from 93ec7fb to 8f5ef83CompareJuly 25, 2026 09:31
@z33b0t
z33b0t merged commit 7fcdd97 into mainJul 25, 2026
5 checks passed
@z33b0t
z33b0t deleted the protocol-wyoming-openai branch July 25, 2026 09:32
z33b0t added a commit that referenced this pull request Jul 29, 2026
Neither workflow declared a `permissions:` block, so GITHUB_TOKEN was issued
with the repository default scope. Both only ever check out — the www deploy
authenticates to Cloudflare with its own API token — so `contents: read` is
what they need, and stating it means a later change to the repository default
can't silently widen them.
mac.yml and windows.yml already declare `contents: write` at job level for
`gh release create` / `upload`, so they are left alone.
Clears CodeQL actions/missing-workflow-permissions alerts #1 and #2.
Closes#15
Co-authored-by: z33b0t <z33b0t@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@z33b0t