
Stillheten var bærende
GeekBye v1s to siste releaser handler om den samme ubehagelige sannheten: sanntidstranskripsjon over et ekte nettverk er ikke tapsfri, og det ærlige trekket er å slutte å late som om den er det. v1.8.20 beholdt en kopi av hver lydbit på disk før den ble kastet under en gjenoppkobling, og begynte å markere transkripsjonens hull høyt. v1.9.0 sluttet å sende stillhet for å spare båndbredde — og oppdaget at stillheten var nøyaktig det signalet transkribereren brukte for å vite at en setning var slutt. To releaser om prisen for å kaste ting.
GeekBye v1s to siste releaser — v1.8.20 og v1.9.0, de like før versjon 2-milepælen — deler et tema som først blir tydelig når du leser dem sammen. Begge handler om ting appen stille kastet bort, og begge lander på den samme konklusjonen: det du kaster for å være effektiv gjorde mer arbeid enn du trodde. Den ene releasen kastet lyd under nettverksgjenoppkoblinger. Den andre kastet stillhet for å spare båndbredde. I begge tilfeller viste det kastede seg å være bærende.
Sanntidstranskripsjon er ikke tapsfri, og v1.8.20 innrømmer det
Her er et faktum om å strømme tale-til-tekst over en WebSocket som er lett å se bort fra: når forbindelsen faller og kobler seg til igjen — eller når appen bytter fra én transkripsjonsleverandør til en annen, Deepgram til ElevenLabs — kaster klienten bevisst lydbitene som ble fanget under det gapet. Den må. En nyoppkoblet leverandør som plutselig mottar en kø av gammel lyd, setter den i halsen, eller enda verre, låser stemmeaktivitetsdetektoren sin på feil ting. Så klienten kaster de bitene.
Problemet er ikke kastingen. Problemet er hva kastingen før betydde: de ordene dukket rett og slett aldri opp i transkripsjonen, og ingenting fortalte deg at de manglet. Opptaket så komplett ut. Det var det ikke.
v1.8.20 angriper det på to fronter, og ingen av dem er "få gjenoppkoblinger til aldri å miste lyd" — for det kan du ikke, ikke i sanntid. I stedet gjør den tapet gjenopprettelig og ærlig.
Den gjenopprettelige halvdelen er et holdbart PCM-tee. I hovedprosessens fangsthåndterer skrives hver fanget bit nå til en fil på disk før den når kasteportene for gjenoppkobling 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 behagelig lavteknologisk: teeChunkToDisk åpner en WriteStream, reserverer en 44-byte WAV-header på forhånd, legger til rå 16-bits mono-samples etter hvert som de kommer, og lapper headeren med de endelige størrelsene ved stopp (fs.openSync(..., 'r+'), skriv 44 byte ved offset 0). Resultatet er en spillbar WAV av den komplette lyden — inkludert delene sanntidstranskriptet aldri så. Når opptaket slutter, sender den ut en session-audio-saved-hendelse med filsti og byteantall.
To ærlighetsmerknader er viktige her, for det er lett å overdrive dette. For det første: teet fyller ikke hullet i disse releasene. Det bevarer rålyden til en senere avstemmingskjøring — en batch-retranskripsjon som avstemmer filen mot livetranskriptet — som er en krok disse releasene legger, ikke en funksjon de leverer. Oppfølgingscommitten annoterer til og med session-audio-saved med en TODO som peker på avstemmingssakene, så ingen forveksler den med død kode. For det andre: dette koster disk. Teamet anslo omtrent 86 MB per 30-minutters opptak, så den samme releasen legger til en StartupCleanupService som rydder lagrede økter eldre enn 7 dager. Et sikkerhetsnett som vokser uten grense er en lekkasje; oppryddingen er det som gjør teet leveringsklart.
Den ærlige halvdelen er enda enklere. Ved det første gjenoppkoblingsforsøket skriver klienten en ekte, synlig oppføring i transkripsjonen:
[reconnecting — audio may be missing]
Den ene linjen er hele releasens filosofi i miniatyr. Appen kan ikke garantere at den hørte alt, så den slutter å antyde at den gjorde det. Et hull du kan se er et hull du kan jobbe rundt; et hull du ikke kan se er en bugrapport som venter på å skje.
v1.9.0: stillheten du optimerte bort var signalet
v1.9.0 er den siste v1-releasen, og det er der backenden overleverte et sett med livetranskript-ansvar til klienten — å kaste stillhet, å tilskrive hver transkriptlinje til mikrofon eller systemlyd ut fra en lokal stemmeaktivitetstidslinje, å eie gjenoppkoblingsløkken, å håndheve opptakstak. Det meste av det er grunnarbeid for versjon 2-arkitekturen og er fortalt annetsteds. Men én tråd i det er den beste lille historien i noen av releasene, fordi det er en fiks som måtte gjøre ugjort skaden fra en optimalisering som var fullstendig korrekt isolert sett.
Det starter med en åpenbart god endring: slutt å sende stillhet. Klienten begynte å filtrere bort biter hvis dominerende kilde var stillhet før de ble sendt over socketen — mindre båndbredde, mindre unyttig lyd for backenden å behandle, og det overholdt en eksplisitt backend-klientkontrakt som sa "ikke send stillhet". Enhver grunn til å gjøre det var god.
Og det brøt fullføringen av ytringer fullstendig. Committen som fikser det legger ut den kausale kjeden med uvanlig åpenhet:
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 platonske formen av en lekk abstraksjon som biter tilbake. Transkribererens stemmeaktivitetsdetektor har ikke et separat "taleren er ferdig"-inndata — den slutter seg til slutten på en setning ved å høre omtrent ett sekunds stillhet i lyden. Stillhet var ikke støy på ledningen; det var en beskjed. Ved å filtrere den bort for å spare båndbredde sluttet klienten ved et uhell å sende det ene signalet som fortalte transkribereren at en setning var slutt. Så hvert delvis transkript bare fortsatte å vokse, halen tonende ut i en ... som aldri ble løst opp.
Du kunne fikse dette ved å legge stillheten tilbake — men da har du gjort optimaliseringen ugjort. Den faktiske fiksen er bedre: send stillhetens betydning uten å sende stillheten. Klienten sporer nå lokalt hvor lenge lyden har vært stille, og når det krysser SILENCE_COMMIT_THRESHOLD_MS = 1000 — bevisst matchet mot backendens egen VAD-stillhetsterskel — avfyrer den et eksplisitt commit-signal utenfor båndet: sendMixedChunk merker den vedvarende stillheten, sender et realtime-audio:commit over IPC, hovedprosessen videresender et { type: 'commit' } på socketen, og backenden ber ElevenLabs om å fullføre det delvise resultatet. To små biter tilstand, silenceStartedAt og silenceCommitFired, hindrer den i å avfyre gjentatte ganger. Båndbreddegevinsten overlever; fullføringen fungerer igjen; det etterslepende ... løses opp.
Det er også en liten, ærlig fotnote til denne releasen: den ubegrensede gjenoppkoblingslogikken som v1.9.0 introduserte, ble først bygget i rendereren som en WsReconnectController, deretter slettet innenfor den samme releasen da den ekte implementasjonen landet i hovedprosesshåndtereren der den hørte hjemme. Selve gjenoppkoblingshistorien — ubegrenset backoff, vekslende leverandører hver femte feil — fortelles i sitt eget kapittel; det v1.9.0 tilfører, er at det er her den arkitekturen ble født, og der klienten først tok eierskap over den.
Tre ting disse releasene lærte oss
- Når du ikke kan forhindre tap, gjør det gjenopprettelig og gjør det synlig. v1.8.20 hindrer ikke gjenoppkoblinger i å kaste lyd — det er ikke fysisk mulig i sanntid. Den beholder en kopi av rålyden til senere, og den merker hullet i transkripsjonen. Ingen av delene er glamorøse; sammen forvandler de stille datatap til en kjent, avgrenset, gjenopprettelig tilstand.
- Et sikkerhetsnett trenger sine egne grenser. Det holdbare teet ville stille fylle disken hvis det ble latt i fred — en fiks som blir en bug i sakte film. Den 7-dagers oppryddingen som ble levert i den samme releasen er ikke en ettertanke; det er det som gjør teet trygt å levere i det hele tatt. Hvis du legger til en mekanisme som hoper seg opp, legg til mekanismen som rydder den i samme åndedrag.
- Det du kaster for å være effektiv kan være et signal. Den skarpeste lærdommen i noen av releasene: å filtrere bort stillhet var korrekt på hver akse du normalt ville sjekke — båndbredde, last, kontraktsoverholdelse — og det brøt likevel produktet, fordi et nedstrøms system leste betydning inn i stillheten. Når du fjerner data av effektivitetshensyn, spør hva som sluttet seg til noe fra de dataene. Noen ganger må du sende betydningen eksplisitt nettopp fordi du sluttet å sende det den før ble sluttet fra.
Og det er slutten på versjon 1-historien — den siste herdingen før omskrivingen. For det forrige kapittelet, de tre verbene som holder Web Audio i live (v1.8.16 + v2.0.2); for hvor alt dette var på vei, hva en versjon 2 faktisk krever: 206 commits (v2.0.0); og for hele buen, anatomien i å shippe programvare til perfeksjon.