Steven
Steven8 min leestijd

De stilte was dragend

De laatste twee releases van GeekBye v1 gaan over dezelfde ongemakkelijke waarheid: real-time transcriptie over een echt netwerk is niet verliesvrij, en de eerlijke zet is te stoppen met doen alsof het dat wel is. v1.8.20 bewaarde een kopie van elke audiochunk op schijf voordat het die tijdens een reconnect weggooide, en begon de gaten in het transcript hardop te markeren. v1.9.0 stopte met het versturen van stilte om bandbreedte te besparen — en ontdekte dat de stilte precies het signaal was dat de transcriber gebruikte om te weten dat een zin was afgelopen. Twee releases over de kosten van dingen weggooien.

Engineering
Audio
Betrouwbaarheid
GeekBye Releases
De stilte was dragend

De laatste twee releases van GeekBye v1 — v1.8.20 en v1.9.0, die vlak voor de mijlpaal van versie 2 — delen een thema dat pas duidelijk wordt als je ze samen leest. Beide gaan over dingen die de app stilletjes weggooide, en beide komen tot dezelfde conclusie: het ding dat je weggooit om efficiënt te zijn, deed meer werk dan je dacht. De ene release gooide audio weg tijdens netwerk-reconnects. De andere gooide stilte weg om bandbreedte te besparen. In beide gevallen bleek het weggegooide ding dragend te zijn.

Real-time transcriptie is niet verliesvrij, en v1.8.20 geeft dat toe

Hier is een feit over streaming spraak-naar-tekst over een WebSocket dat makkelijk weg te kijken is: wanneer de verbinding wegvalt en opnieuw verbindt — of wanneer de app overschakelt van de ene transcriptieprovider naar de andere, Deepgram naar ElevenLabs — gooit de client de audiochunks die tijdens dat gat zijn opgevangen bewust weg. Dat moet wel. Een vers verbonden provider die opeens een achterstand aan verouderde audio ontvangt, verslikt zich, of erger, klemt zijn voice-activity-detector vast op het verkeerde. Dus gooit de client die chunks weg.

Het probleem is niet het weggooien. Het probleem is wat weggooien vroeger betekende: die woorden verschenen simpelweg nooit in het transcript, en niets vertelde je dat ze ontbraken. De opname zag er compleet uit. Dat was ze niet.

v1.8.20 pakt dat op twee fronten aan, en geen van beide is "zorg dat reconnects nooit audio verliezen" — want dat kan niet, niet in real-time. In plaats daarvan maakt het het verlies herstelbaar en eerlijk.

De herstelbare helft is een duurzame PCM-tee. In de capture-handler van het main-proces wordt elke opgevangen chunk nu naar een bestand op schijf geschreven voordat het de drop-gates van reconnect en grace-periode bereikt:

every captured chunk is written to <userData>/sessions/session-<ts>.wav before realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass

Het mechanisme is aangenaam low-tech: teeChunkToDisk opent een WriteStream, reserveert vooraan een 44-byte WAV-header, voegt ruwe 16-bit mono-samples toe zodra ze binnenkomen, en patcht de header bij stop met de definitieve groottes (fs.openSync(..., 'r+'), schrijf 44 bytes op offset 0). Het resultaat is een afspeelbare WAV van de complete audio — inclusief de delen die het real-time transcript nooit zag. Wanneer de opname eindigt, zendt het een session-audio-saved-event uit met het bestandspad en de byte-telling.

Twee eerlijkheidsnoten zijn hier van belang, want het is makkelijk om dit te overdrijven. Ten eerste: de tee vult het gat in deze releases niet. Het bewaart de ruwe audio voor een latere reconciliatieronde — een batch-hertranscriptie die het bestand verzoent met het live transcript — wat een haak is die deze releases neerleggen, geen feature die ze uitbrengen. De vervolgcommit annoteert session-audio-saved zelfs met een TODO die naar de reconciliatie-issues wijst, zodat niemand het aanziet voor dode code. Ten tweede: dit kost schijf. Het team schatte ruwweg 86 MB per opname van 30 minuten, dus dezelfde release voegt een StartupCleanupService toe die opgeslagen sessies ouder dan 7 dagen wegsnoeit. Een vangnet dat onbegrensd groeit is een lek; de opruiming is wat de tee verzendbaar maakt.

De eerlijke helft is nog simpeler. Bij de eerste reconnect-poging schrijft de client een echte, zichtbare regel in het transcript:

[reconnecting — audio may be missing]

Die ene regel is de hele filosofie van de release in het klein. De app kan niet garanderen dat het alles heeft gehoord, dus stopt het met impliceren dat het dat wel deed. Een gat dat je kunt zien is een gat dat je kunt omzeilen; een gat dat je niet kunt zien is een bugrapport dat staat te wachten om te gebeuren.

v1.9.0: de stilte die je wegoptimaliseerde was het signaal

v1.9.0 is de laatste v1-release, en het is waar de backend een reeks live-transcriptverantwoordelijkheden aan de client overdroeg — stilte weggooien, elke transcriptregel toeschrijven aan mic- of systeemaudio vanuit een lokale voice-activity-tijdlijn, de reconnect-lus bezitten, opnamelimieten afdwingen. Het meeste daarvan is voorbereidend werk voor de versie-2-architectuur en is elders verteld. Maar één draad erin is het beste kleine verhaal in beide releases, want het is een fix die de schade moest ongedaan maken van een optimalisatie die op zichzelf volkomen juist was.

Het begint met een overduidelijk goede wijziging: stop met het versturen van stilte. De client begon chunks waarvan de dominante bron stilte was eruit te filteren voordat ze over de socket werden verstuurd — minder bandbreedte, minder nutteloze audio voor de backend om te verwerken, en het eerde een expliciet backend-client-contract dat zei "stuur geen stilte." Elke reden om het te doen was steekhoudend.

En het brak utterance-finalisatie volledig. De commit die het repareert legt de causale keten met ongewone openhartigheid uit:

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.

Dit is de platonische vorm van een lekkende abstractie die terugbijt. De voice-activity-detector van de transcriber heeft geen aparte "de spreker is klaar"-invoer — het leidt het einde van een zin af uit het horen van ongeveer een seconde stilte in de audio. Stilte was geen ruis op de lijn; het was een boodschap. Door het weg te filteren om bandbreedte te besparen, stopte de client per ongeluk met het versturen van het ene signaal dat de transcriber vertelde dat een zin was afgelopen. Dus elk partieel transcript bleef maar groeien, met een staart die wegebde in een ... die nooit oploste.

Je zou dit kunnen repareren door de stilte terug te zetten — maar dan heb je de optimalisatie ongedaan gemaakt. De echte fix is beter: verstuur de betekenis van de stilte zonder de stilte te versturen. De client houdt nu lokaal bij hoe lang de audio stil is geweest, en wanneer dat SILENCE_COMMIT_THRESHOLD_MS = 1000 overschrijdt — bewust afgestemd op de eigen VAD-stiltedrempel van de backend — vuurt het een expliciet commit-signaal out of band af: sendMixedChunk merkt de aanhoudende stilte op, stuurt een realtime-audio:commit over IPC, het main-proces stuurt een { type: 'commit' } door over de socket, en de backend zegt ElevenLabs de partial te finaliseren. Twee kleine stukjes state, silenceStartedAt en silenceCommitFired, houden het ervan af herhaaldelijk te vuren. De bandbreedtewinst blijft behouden; de finalisatie werkt weer; de trailing ... lost op.

Er is ook een kleine, eerlijke voetnoot bij deze release: de onbegrensde reconnect-logica die v1.9.0 introduceerde werd eerst in de renderer gebouwd als een WsReconnectController, en daarna binnen dezelfde release verwijderd zodra de echte implementatie landde in de main-proces-handler waar het thuishoorde. Het reconnect-verhaal zelf — onbegrensde backoff, providers die om de vijf mislukkingen wisselen — wordt in een eigen hoofdstuk verteld; wat v1.9.0 toevoegt is dat het is waar die architectuur werd geboren, en waar de client er voor het eerst eigenaarschap over nam.

Drie dingen die deze releases leerden

  1. Als je verlies niet kunt voorkomen, maak het dan herstelbaar en maak het zichtbaar. v1.8.20 belet reconnects niet audio weg te gooien — dat is fysiek niet mogelijk in real-time. Het bewaart een kopie van de ruwe audio voor later, en het labelt het gat in het transcript. Geen van beide is glamoureus; samen veranderen ze stil dataverlies in een bekende, begrensde, herstelbare toestand.
  2. Een vangnet heeft zijn eigen grenzen nodig. De duurzame tee zou de schijf stilletjes vullen als je het met rust liet — een fix die een bug in slow motion wordt. De opruiming na 7 dagen die in dezelfde release werd uitgebracht is geen bijzaak; het is het ding dat de tee überhaupt veilig verzendbaar maakt. Als je een mechanisme toevoegt dat zich opstapelt, voeg dan in dezelfde adem het mechanisme toe dat het wegsnoeit.
  3. Het ding dat je weggooit om efficiënt te zijn kan een signaal zijn. De allerscherpste les in beide releases: stilte wegfilteren was juist op elke as die je normaal zou controleren — bandbreedte, belasting, contractnaleving — en het brak toch het product, omdat een downstream-systeem betekenis in de stilte las. Wanneer je data verwijdert omwille van efficiëntie, vraag je dan af wat er iets afleidde uit die data. Soms moet je de betekenis expliciet versturen juist omdat je stopte met het versturen van het ding waaruit het vroeger werd afgeleid.

En dat is het einde van het versie-1-verhaal — de laatste verharding voor de herschrijving. Voor het vorige hoofdstuk, de drie werkwoorden die Web Audio in leven houden (v1.8.16 + v2.0.2); voor waar dit alles naartoe ging, wat een versie 2 daadwerkelijk kost: 206 commits (v2.0.0); en voor de hele boog, de anatomie van software tot in de perfectie uitbrengen.

Gerelateerde Artikelen

De drie werkwoorden die Web Audio in leven houden
Steven
Steven9 min leestijd

De drie werkwoorden die Web Audio in leven houden

Twee GeekBye-point-releases, twee maanden uit elkaar en in twee verschillende bestanden, leerden onze audiocode dezelfde les vanuit tegengestelde kanten: behandel de AudioContext van de browser niet langer als wegwerpbaar. De ene release leerde resume() aan te roepen op een context die macOS stilletjes midden in een opname had gesuspend; de andere leerde suspend() te gebruiken in plaats van close() zodat opeenvolgende sessies niet langer tegen Chromiums plafond van ongeveer zes contexten aan knallen. Resume, suspend, close — dat is het hele plot.

Engineering
Audio
Desktop
Een verbroken verbinding hoort niet je hele app te laten crashen — de onze deed dat wel
Steven
Steven6 min leestijd

Een verbroken verbinding hoort niet je hele app te laten crashen — de onze deed dat wel

Toen onze backend midden in een meeting offline ging, pauzeerde niet alleen de transcriptie — de hele app crashte. De oorzaak was één onbehandeld event, en de fix was tien regels. Dit is het releasecluster dat GeekBye ingelogd én verbonden liet blijven door de dingen die dat vroeger de kop kostten.

Engineering
Betrouwbaarheid
Electron
De veiligheidscontrole die onze app onmogelijk te sluiten maakte
Steven
Steven6 min leestijd

De veiligheidscontrole die onze app onmogelijk te sluiten maakte

Automatisch updaten was de moeilijkste feature die we ooit hebben uitgebracht — zes releases in vier dagen om te voorkomen dat het de app onbruikbaar maakte. De ergste bug was er een die we zelf introduceerden terwijl we voorzichtig probeerden te zijn: een "veiligheidscontrole" van 500 milliseconden die een mislukte update veranderde in een proces dat je letterlijk niet kon afsluiten.

Engineering
Electron
Betrouwbaarheid