
Tavsheden var bærende
GeekBye v1s to sidste releases handler om den samme ubehagelige sandhed: realtidstransskription over et rigtigt netværk er ikke tabsfri, og det ærlige træk er at holde op med at lade som om den er det. v1.8.20 beholdt en kopi af hver lydbid på disken, før den blev smidt væk under en genforbindelse, og begyndte at markere transskriptionens huller højt. v1.9.0 holdt op med at sende tavshed for at spare båndbredde — og opdagede, at tavsheden var præcis det signal, transskribereren brugte til at vide, at en sætning var slut. To releases om prisen for at smide ting væk.
GeekBye v1s to sidste releases — v1.8.20 og v1.9.0, dem lige før version 2-milepælen — deler et tema, der først bliver tydeligt, når du læser dem sammen. Begge handler om ting, appen tavst smed væk, og begge når frem til den samme konklusion: det, du smider væk for at være effektiv, lavede mere arbejde, end du troede. Den ene release smed lyd væk under netværksgenforbindelser. Den anden smed tavshed væk for at spare båndbredde. I begge tilfælde viste det bortkastede sig at være bærende.
Realtidstransskription er ikke tabsfri, og v1.8.20 indrømmer det
Her er et faktum om at streame tale-til-tekst over en WebSocket, som er let at se væk fra: når forbindelsen falder og genforbinder — eller når appen skifter fra én transskriptionsudbyder til en anden, Deepgram til ElevenLabs — smider klienten bevidst de lydbidder væk, der blev indfanget under det hul. Den er nødt til det. En nyforbundet udbyder, der pludselig modtager en kø af gammel lyd, sætter den i halsen eller, værre endnu, låser sin stemmeaktivitetsdetektor fast på det forkerte. Så klienten smider de bidder væk.
Problemet er ikke det at smide væk. Problemet er, hvad det at smide væk før betød: de ord dukkede simpelthen aldrig op i transskriptionen, og intet fortalte dig, at de manglede. Optagelsen så komplet ud. Det var den ikke.
v1.8.20 angriber det på to fronter, og ingen af dem er "få genforbindelser til aldrig at miste lyd" — for det kan du ikke, ikke i realtid. I stedet gør den tabet genopretteligt og ærligt.
Den genoprettelige halvdel er et holdbart PCM-tee. I hovedprocessens indfangningshåndterer skrives hver indfanget bid nu til en fil på disken, før den når smideportene for genforbindelse og nådeperiode:
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
Mekanismen er behageligt lavteknologisk: teeChunkToDisk åbner en WriteStream, reserverer en 44-byte WAV-header på forhånd, tilføjer rå 16-bit mono-samples, efterhånden som de ankommer, og lapper headeren med de endelige størrelser ved stop (fs.openSync(..., 'r+'), skriv 44 byte ved offset 0). Resultatet er en afspillelig WAV af den komplette lyd — inklusive de dele, realtidstransskriptet aldrig så. Når optagelsen slutter, udsender den en session-audio-saved-hændelse med filsti og byteantal.
To ærlighedsbemærkninger er vigtige her, for det er let at overdrive det her. For det første: teet fylder ikke hullet i disse releases. Det bevarer rålyden til en senere afstemningskørsel — en batch-gentransskription, der afstemmer filen mod livetransskriptet — hvilket er en krog, disse releases lægger, ikke en funktion, de leverer. Opfølgningscommitten annoterer endda session-audio-saved med en TODO, der peger på afstemningssagerne, så ingen forveksler den med død kode. For det andet: det koster disk. Teamet vurderede omkring 86 MB pr. 30-minutters optagelse, så den samme release tilføjer en StartupCleanupService, der rydder gemte sessioner ældre end 7 dage. Et sikkerhedsnet, der vokser uden grænse, er en lækage; oprydningen er det, der gør teet leverbart.
Den ærlige halvdel er endnu enklere. Ved det første genforbindelsesforsøg skriver klienten en rigtig, synlig post i transskriptionen:
[reconnecting — audio may be missing]
Den ene linje er hele releasens filosofi i miniature. Appen kan ikke garantere, at den hørte alt, så den holder op med at antyde, at den gjorde. Et hul, du kan se, er et hul, du kan arbejde uden om; et hul, du ikke kan se, er en bugrapport, der venter på at ske.
v1.9.0: tavsheden, du optimerede væk, var signalet
v1.9.0 er den sidste v1-release, og det er der, backenden overdrog en række livetransskript-ansvar til klienten — at smide tavshed væk, at tilskrive hver transskriptlinje til mikrofon eller systemlyd ud fra en lokal stemmeaktivitetstidslinje, at eje genforbindelsesløkken, at håndhæve optagelseslofter. Det meste af det er grundarbejde til version 2-arkitekturen og er fortalt andetsteds. Men én tråd i det er den bedste lille historie i nogen af releaserne, for det er en fix, der måtte gøre skaden ugjort fra en optimering, der isoleret set var fuldstændig korrekt.
Det starter med en åbenlyst god ændring: hold op med at sende tavshed. Klienten begyndte at filtrere bidder fra, hvis dominerende kilde var tavshed, før de blev sendt over socketen — mindre båndbredde, mindre unyttig lyd for backenden at behandle, og det overholdt en eksplicit backend-klientkontrakt, der sagde "send ikke tavshed". Enhver grund til at gøre det var sund.
Og det brød færdiggørelsen af ytringer fuldstændigt. Committen, der fikser det, lægger den kausale kæde ud med usædvanlig åbenhed:
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.
Dette er den platoniske form af en utæt abstraktion, der bider igen. Transskribererens stemmeaktivitetsdetektor har ikke et separat "taleren er færdig"-input — den udleder enden på en sætning ved at høre cirka et sekunds tavshed i lyden. Tavshed var ikke støj på ledningen; det var en besked. Ved at filtrere den fra for at spare båndbredde holdt klienten ved et uheld op med at sende det ene signal, der fortalte transskribereren, at en sætning var slut. Så hvert delvist transskript blev bare ved med at vokse, halen tonende ud i et ..., der aldrig blev opløst.
Du kunne fikse dette ved at lægge tavsheden tilbage — men så har du gjort optimeringen ugjort. Den egentlige fix er bedre: send tavshedens betydning uden at sende tavsheden. Klienten sporer nu lokalt, hvor længe lyden har været tavs, og når det krydser SILENCE_COMMIT_THRESHOLD_MS = 1000 — bevidst matchet mod backendens egen VAD-tavshedstærskel — affyrer den et eksplicit commit-signal uden for båndet: sendMixedChunk bemærker den vedvarende tavshed, sender et realtime-audio:commit over IPC, hovedprocessen videresender et { type: 'commit' } på socketen, og backenden beder ElevenLabs om at færdiggøre det delvise resultat. To små stykker tilstand, silenceStartedAt og silenceCommitFired, holder den fra at affyre gentagne gange. Båndbreddegevinsten overlever; færdiggørelsen virker igen; det efterslæbende ... opløses.
Der er også en lille, ærlig fodnote til denne release: den ubegrænsede genforbindelseslogik, som v1.9.0 introducerede, blev først bygget i rendereren som en WsReconnectController, og derefter slettet inden for den samme release, da den rigtige implementering landede i hovedproceshåndtereren, hvor den hørte hjemme. Selve genforbindelseshistorien — ubegrænset backoff, skiftende udbydere for hver femte fejl — fortælles i sit eget kapitel; hvad v1.9.0 tilføjer, er, at det er her, den arkitektur blev født, og hvor klienten først tog ejerskab over den.
Tre ting, disse releases lærte os
- Når du ikke kan forhindre tab, gør det genopretteligt, og gør det synligt. v1.8.20 forhindrer ikke genforbindelser i at smide lyd væk — det er ikke fysisk muligt i realtid. Den beholder en kopi af rålyden til senere, og den mærker hullet i transskriptionen. Ingen af delene er glamourøse; sammen forvandler de tavst datatab til en kendt, afgrænset, genoprettelig tilstand.
- Et sikkerhedsnet har brug for sine egne grænser. Det holdbare tee ville tavst fylde disken, hvis det blev ladt alene — en fix, der bliver til en bug i slowmotion. Den 7-dages oprydning, der blev leveret i den samme release, er ikke en eftertanke; det er det, der overhovedet gør teet sikkert at levere. Hvis du tilføjer en mekanisme, der ophober, så tilføj mekanismen, der rydder den, i samme åndedrag.
- Det, du smider væk for at være effektiv, kan være et signal. Den skarpeste lektie i nogen af releaserne: at filtrere tavshed fra var korrekt på enhver akse, du normalt ville tjekke — båndbredde, belastning, kontraktoverholdelse — og det brød alligevel produktet, fordi et nedstrøms system læste betydning ind i tavsheden. Når du fjerner data af effektivitetshensyn, så spørg, hvad der udledte noget fra de data. Nogle gange er du nødt til at sende betydningen eksplicit, netop fordi du holdt op med at sende det, den før blev udledt fra.
Og det er enden på version 1-historien — den sidste hærdning før omskrivningen. For det forrige kapitel, de tre verber, der holder Web Audio i live (v1.8.16 + v2.0.2); for hvor alt dette var på vej hen, hvad en version 2 faktisk kræver: 206 commits (v2.0.0); og for hele buen, anatomien i at shippe software til perfektion.