Skip to content

The one-byte bug

The voice assistant on the HA box has three legs: speech-to-text on the GPU box, an LLM brain, and text-to-speech back out. For weeks, a full pipeline run over the websocket died in about 30 milliseconds. The whisper bridge log showed a client connecting and immediately disconnecting with zero audio. Read that as "HA can't reach the bridge" and you'll chase it for days.

It wasn't HA. It wasn't the GPU. It was my own test client, and the whole thing came down to one byte.

The symptom that misled me

A direct REST call to the STT engine transcribed a test clip fine, POST /api/stt/stt.whisper_gpu with the right header, answer in under a second. So the engine was healthy, the bridge was reachable, and the bridge was logging connections. But the full pipeline run produced nothing.

That combination — engine works, bridge connects, no audio arrives — is what sent me down the wrong path.

The root cause

HA's websocket protocol parses every binary frame like this:

# websocket_api/http.py : _async_websocket_command_phase
handler = msg_data[0]     # first byte = binary handler id
payload = msg_data[1:]    # rest = actual audio
async_handle_binary(handler, payload)

So every audio frame must be prefixed with the 1-byte stt_binary_handler_id, the value HA hands you in the run-start event's runner_data.stt_binary_handler_id (usually 1).

My hand-written test harness sent raw PCM with no prefix. HA read the first audio byte as a bogus handler id, silently dropped every chunk, and the run died. The bridge saw connect then drop with no audio, which looks exactly like a network problem if you don't know the framing rule.

The correct send:

for i in range(0, len(pcm), CHUNK):
    await ws.send(bytes([HID]) + pcm[i:i+CHUNK])
await ws.send(bytes([HID]))   # 1-byte terminator = end of audio (NOT b"")

And the second way to get it wrong: an empty terminator b"". len < 1 and HA raises a Disconnect.

Whose side was it

Neither. HA's requirement is correct and by design. The Wyoming bridge is a separate layer and wasn't involved at all. The real satellite always sent the prefix correctly, so the live device was never broken. Only my synthetic test was. The fix was adding the prefix to the client script. No HA change, no GPU change.

What I'd do differently next time is isolate a leg before touching the full harness. The REST STT call answered in 0.3 seconds and that single test should have pointed me at the client, not the bridge. Read the bridge log — "transcribing N bytes" is the proof audio actually arrived, versus a bare "client disconnected".

The wiring, the topology, and the testing playbook are on the Home Assistant voice page.

Comments