
The Silence Was Load-Bearing
The last two releases of GeekBye v1 are about the same uncomfortable truth: realtime transcription over a real network is not lossless, and the honest move is to stop pretending it is. v1.8.20 kept a copy of every audio chunk on disk before dropping it during a reconnect, and started marking the gaps in the transcript out loud. v1.9.0 stopped sending silence to save bandwidth — and discovered the silence was the exact signal the transcriber used to know a sentence had ended. Two releases about the cost of throwing things away.
The last two releases of GeekBye v1 — v1.8.20 and v1.9.0, the ones right before the version-2 milestone — share a theme that only becomes obvious when you read them together. Both are about things the app was quietly throwing away, and both arrive at the same conclusion: the thing you drop to be efficient was doing more work than you thought. One release was dropping audio during network reconnects. The other was dropping silence to save bandwidth. In both cases the dropped thing turned out to be load-bearing.
Realtime transcription is not lossless, and v1.8.20 admits it
Here's a fact about streaming speech-to-text over a WebSocket that's easy to look away from: when the connection drops and reconnects — or when the app fails over from one transcription provider to another, Deepgram to ElevenLabs — the client deliberately drops the audio chunks captured during that gap. It has to. A freshly-connected provider that suddenly receives a backlog of stale audio will choke, or worse, latch its voice-activity detector onto the wrong thing. So the client throws those chunks away.
The problem isn't the dropping. The problem is what dropping used to mean: those words simply never appeared in the transcript, and nothing told you they were missing. The recording looked complete. It wasn't.
v1.8.20 attacks that on two fronts, and neither of them is "make reconnects never lose audio" — because you can't, not in realtime. Instead it makes the loss recoverable and honest.
The recoverable half is a durable PCM tee. In the main-process capture handler, every captured chunk is now written to a file on disk before it reaches the reconnect and grace-period drop gates:
every captured chunk is written to
<userData>/sessions/session-<ts>.wavbefore realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass
The mechanism is pleasingly low-tech: teeChunkToDisk opens a WriteStream, reserves a 44-byte WAV header up front, appends raw 16-bit mono samples as they arrive, and patches the header with the final sizes on stop (fs.openSync(..., 'r+'), write 44 bytes at offset 0). The result is a playable WAV of the complete audio — including the parts the realtime transcript never saw. When recording ends it emits a session-audio-saved event with the file path and byte count.
Two honesty notes matter here, because it's easy to overclaim this. First: the tee does not fill the gap in these releases. It preserves the raw audio for a later reconciliation pass — a batch re-transcription that reconciles the file against the live transcript — which is a hook these releases lay down, not a feature they ship. The follow-up commit even annotates session-audio-saved with a TODO pointing at the reconciliation issues so nobody mistakes it for dead code. Second: this costs disk. The team estimated roughly 86 MB per 30-minute recording, so the same release adds a StartupCleanupService that prunes saved sessions older than 7 days. A safety net that grows without bound is a leak; the cleanup is what makes the tee shippable.
The honest half is even simpler. On the first reconnect attempt, the client writes a real, visible entry into the transcript:
[reconnecting — audio may be missing]
That single line is the whole philosophy of the release in miniature. The app can't guarantee it heard everything, so it stops implying that it did. A gap you can see is a gap you can work around; a gap you can't see is a bug report waiting to happen.
v1.9.0: the silence you optimized away was the signal
v1.9.0 is the last v1 release, and it's where the backend handed a set of live-transcript responsibilities down to the client — dropping silence, attributing each transcript line to mic or system audio from a local voice-activity timeline, owning the reconnect loop, enforcing recording caps. Most of that is groundwork for the version-2 architecture and has been told elsewhere. But one thread in it is the best small story in either release, because it's a fix that had to undo the damage of an optimization that was completely correct in isolation.
It starts with an obviously-good change: stop sending silence. The client began filtering out chunks whose dominant source was silence before shipping them over the socket — less bandwidth, less useless audio for the backend to process, and it honored an explicit backend client-contract that said "don't send silence." Every reason to do it was sound.
And it broke utterance finalization completely. The commit that fixes it lays out the causal chain with unusual candor:
After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize.
This is the platonic form of a leaky abstraction biting back. The transcriber's voice-activity detector doesn't have a separate "the speaker is done" input — it infers the end of a sentence from hearing about a second of silence in the audio. Silence wasn't noise on the wire; it was a message. By filtering it out to save bandwidth, the client accidentally stopped sending the one signal that told the transcriber a sentence had ended. So every partial transcript just kept growing, tail trailing off into a ... that never resolved.
You could fix this by putting the silence back — but then you've un-done the optimization. The actual fix is better: send the meaning of the silence without sending the silence. The client now tracks how long the audio has been silent locally, and when that crosses SILENCE_COMMIT_THRESHOLD_MS = 1000 — deliberately matched to the backend's own VAD silence threshold — it fires an explicit commit signal out of band: sendMixedChunk notices the sustained silence, sends a realtime-audio:commit over IPC, the main process forwards a { type: 'commit' } on the socket, and the backend tells ElevenLabs to finalize the partial. Two small pieces of state, silenceStartedAt and silenceCommitFired, keep it from firing repeatedly. The bandwidth win survives; the finalization works again; the trailing ... resolves.
There's a small, honest footnote to this release, too: the unbounded reconnect logic that v1.9.0 introduced was first built in the renderer as a WsReconnectController, then deleted within the same release once the real implementation landed in the main-process handler where it belonged. The reconnect story itself — unbounded backoff, alternating providers every five failures — is told in its own chapter; what v1.9.0 adds is that it's where that architecture was born, and where the client first took ownership of it.
Three things these releases taught
- When you can't prevent loss, make it recoverable and make it visible. v1.8.20 doesn't stop reconnects from dropping audio — that's not physically possible in realtime. It keeps a copy of the raw audio for later, and it labels the gap in the transcript. Neither is glamorous; together they turn silent data loss into a known, bounded, recoverable condition.
- A safety net needs its own bounds. The durable tee would quietly fill the disk if left alone — a fix that becomes a slow-motion bug. The 7-day cleanup shipped in the same release is not an afterthought; it's the thing that makes the tee safe to ship at all. If you add a mechanism that accumulates, add the mechanism that prunes it in the same breath.
- The thing you drop to be efficient may be a signal. The single sharpest lesson in either release: filtering out silence was correct on every axis you'd normally check — bandwidth, load, contract compliance — and it still broke the product, because a downstream system was reading meaning into the silence. When you remove data for efficiency, ask what was inferring something from that data. Sometimes you have to send the meaning explicitly precisely because you stopped sending the thing it used to be inferred from.
And that's the end of the version-1 story — the last hardening before the rewrite. For the previous chapter, the three verbs that keep web audio alive (v1.8.16 + v2.0.2); for where all of this was heading, what a version 2 actually takes: 206 commits (v2.0.0); and for the whole arc, the anatomy of shipping software to perfection.