Steven
Steven7 min läsning

Tystnaden var bärande

GeekBye v1:s två sista releaser handlar om samma obekväma sanning: realtidstranskribering över ett riktigt nätverk är inte förlustfri, och det ärliga draget är att sluta låtsas att den är det. v1.8.20 behöll en kopia av varje ljudbit på disk innan den slängdes under en återanslutning, och började markera transkriptets luckor högt. v1.9.0 slutade skicka tystnad för att spara bandbredd — och upptäckte att tystnaden var exakt den signal transkriberaren använde för att veta att en mening hade tagit slut. Två releaser om priset för att slänga saker.

Utveckling
Audio
Tillförlitlighet
GeekBye-releaser
Tystnaden var bärande

GeekBye v1:s två sista releaser — v1.8.20 och v1.9.0, de precis före version 2-milstolpen — delar ett tema som blir uppenbart först när du läser dem tillsammans. Båda handlar om saker appen tyst slängde bort, och båda landar i samma slutsats: det du slänger för att vara effektiv gjorde mer arbete än du trodde. Den ena releasen slängde ljud under nätverksåteranslutningar. Den andra slängde tystnad för att spara bandbredd. I båda fallen visade sig det slängda vara bärande.

Realtidstranskribering är inte förlustfri, och v1.8.20 erkänner det

Här är ett faktum om att strömma tal-till-text över en WebSocket som är lätt att titta bort från: när anslutningen faller och återansluter — eller när appen växlar över från en transkriberingsleverantör till en annan, Deepgram till ElevenLabs — slänger klienten avsiktligt de ljudbitar som fångades under det gapet. Den måste. En nyansluten leverantör som plötsligt tar emot en kö av gammalt ljud sätter i halsen, eller värre, låser sin röstaktivitetsdetektor på fel sak. Så klienten slänger de bitarna.

Problemet är inte slängandet. Problemet är vad slängandet förr betydde: de orden dök helt enkelt aldrig upp i transkriptet, och ingenting berättade för dig att de saknades. Inspelningen såg komplett ut. Det var den inte.

v1.8.20 angriper det på två fronter, och ingen av dem är ”få återanslutningar att aldrig förlora ljud” — för det går inte, inte i realtid. I stället gör den förlusten återvinningsbar och ärlig.

Den återvinningsbara halvan är ett hållbart PCM-tee. I huvudprocessens infångningshanterare skrivs varje infångad bit nu till en fil på disk innan den når slängportarna för återanslutning och nådperiod:

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

Mekanismen är angenämt lågteknologisk: teeChunkToDisk öppnar en WriteStream, reserverar en 44-byte WAV-header i förväg, lägger till råa 16-bitars mono-sampel allteftersom de kommer, och lappar headern med de slutliga storlekarna vid stopp (fs.openSync(..., 'r+'), skriv 44 byte vid offset 0). Resultatet är en spelbar WAV av det kompletta ljudet — inklusive de delar realtidstranskriptet aldrig såg. När inspelningen slutar sänder den en session-audio-saved-händelse med filsökväg och byteantal.

Två ärlighetsnoteringar spelar roll här, för det är lätt att överdriva det här. För det första: teet fyller inte luckan i dessa releaser. Det bevarar råljudet för en senare avstämningsomgång — en batch-omtranskribering som stämmer av filen mot livetranskriptet — vilket är en krok dessa releaser lägger, inte en funktion de skeppar. Uppföljningscommitten annoterar till och med session-audio-saved med en TODO som pekar på avstämningsärendena så att ingen misstar den för död kod. För det andra: det kostar disk. Teamet uppskattade ungefär 86 MB per 30-minuters inspelning, så samma release lägger till en StartupCleanupService som rensar sparade sessioner äldre än 7 dagar. Ett skyddsnät som växer obegränsat är en läcka; rensningen är det som gör teet skeppbart.

Den ärliga halvan är ännu enklare. Vid det första återanslutningsförsöket skriver klienten en riktig, synlig post i transkriptet:

[reconnecting — audio may be missing]

Den enda raden är hela releasens filosofi i miniatyr. Appen kan inte garantera att den hörde allt, så den slutar antyda att den gjorde det. En lucka du kan se är en lucka du kan jobba runt; en lucka du inte kan se är en buggrapport som väntar på att hända.

v1.9.0: tystnaden du optimerade bort var signalen

v1.9.0 är den sista v1-releasen, och det är där backenden lämnade ner en uppsättning livetranskript-ansvar till klienten — att slänga tystnad, att tillskriva varje transkriptrad till mikrofon eller systemljud utifrån en lokal röstaktivitetstidslinje, att äga återanslutningsloopen, att upprätthålla inspelningstak. Det mesta av det är grundarbete för version 2-arkitekturen och har berättats på annat håll. Men en tråd i det är den bästa lilla historien i någon av releaserna, för det är en fix som var tvungen att göra ogjort skadan av en optimering som var helt korrekt isolerat sett.

Det börjar med en uppenbart bra ändring: sluta skicka tystnad. Klienten började filtrera bort bitar vars dominerande källa var tystnad innan de skickades över socketen — mindre bandbredd, mindre onyttigt ljud för backenden att bearbeta, och det hedrade ett explicit backend-klientkontrakt som sa ”skicka inte tystnad”. Varje skäl att göra det var sunt.

Och det bröt slutförandet av yttranden fullständigt. Committen som fixar det lägger ut den kausala kedjan med ovanlig uppriktighet:

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.

Det här är den platonska formen av en läckande abstraktion som biter tillbaka. Transkriberarens röstaktivitetsdetektor har ingen separat ”talaren är klar”-ingång — den sluter sig till slutet på en mening genom att höra ungefär en sekunds tystnad i ljudet. Tystnad var inte brus på ledningen; det var ett meddelande. Genom att filtrera bort den för att spara bandbredd slutade klienten av misstag skicka den enda signal som talade om för transkriberaren att en mening hade tagit slut. Så varje partiellt transkript fortsatte bara växa, svansen tonande ut i ett ... som aldrig löstes upp.

Du skulle kunna fixa det här genom att lägga tillbaka tystnaden — men då har du gjort ogjort optimeringen. Den faktiska fixen är bättre: skicka tystnadens betydelse utan att skicka tystnaden. Klienten spårar nu lokalt hur länge ljudet har varit tyst, och när det passerar SILENCE_COMMIT_THRESHOLD_MS = 1000 — avsiktligt matchat mot backendens egen VAD-tystnadströskel — avfyrar den en explicit commit-signal utanför bandet: sendMixedChunk märker den ihållande tystnaden, skickar en realtime-audio:commit över IPC, huvudprocessen vidarebefordrar en { type: 'commit' } på socketen, och backenden säger åt ElevenLabs att slutföra det partiella resultatet. Två små bitar tillstånd, silenceStartedAt och silenceCommitFired, hindrar den från att avfyra upprepade gånger. Bandbreddsvinsten överlever; slutförandet fungerar igen; det eftersläpande ... löses upp.

Det finns en liten, ärlig fotnot till den här releasen också: den obegränsade återanslutningslogiken som v1.9.0 införde byggdes först i renderaren som en WsReconnectController, och raderades sedan inom samma release när den verkliga implementationen landade i huvudprocesshanteraren där den hörde hemma. Själva återanslutningshistorien — obegränsad backoff, växlande leverantörer var femte misslyckande — berättas i sitt eget kapitel; vad v1.9.0 tillför är att det är här den arkitekturen föddes, och där klienten först tog ägarskap över den.

Tre saker dessa releaser lärde ut

  1. När du inte kan förhindra förlust, gör den återvinningsbar och gör den synlig. v1.8.20 hindrar inte återanslutningar från att slänga ljud — det är inte fysiskt möjligt i realtid. Den behåller en kopia av råljudet till senare, och den märker luckan i transkriptet. Ingetdera är glamoröst; tillsammans förvandlar de tyst dataförlust till ett känt, avgränsat, återvinningsbart tillstånd.
  2. Ett skyddsnät behöver sina egna gränser. Det hållbara teet skulle tyst fylla disken om det lämnades i fred — en fix som blir en bugg i ultrarapid. Den 7-dagars rensning som skeppades i samma release är ingen eftertanke; det är det som gör teet säkert att skeppa över huvud taget. Om du lägger till en mekanism som ackumulerar, lägg till mekanismen som rensar den i samma andetag.
  3. Det du slänger för att vara effektiv kan vara en signal. Den skarpaste läxan i någon av releaserna: att filtrera bort tystnad var korrekt på varje axel du normalt skulle kontrollera — bandbredd, last, kontraktsefterlevnad — och det bröt ändå produkten, eftersom ett nedströms system läste in betydelse i tystnaden. När du tar bort data för effektivitet, fråga vad som slöt sig till något från den datan. Ibland måste du skicka betydelsen explicit just för att du slutade skicka det som den förr slöts till utifrån.

Och där är slutet på version 1-historien — den sista härdningen före omskrivningen. För det föregående kapitlet, de tre verben som håller Web Audio vid liv (v1.8.16 + v2.0.2); för dit allt detta var på väg, vad en version 2 faktiskt kräver: 206 commits (v2.0.0); och för hela bågen, anatomin i att skeppa mjukvara till perfektion.

Relaterade artiklar

De tre verben som håller Web Audio vid liv
Steven
Steven8 min läsning

De tre verben som håller Web Audio vid liv

Två GeekBye-punktreleaser, två månader isär och i två olika filer, lärde vår ljudkod samma läxa från motsatta håll: sluta behandla webbläsarens AudioContext som slit-och-släng. Den ena releasen lärde sig att anropa resume() på en kontext som macOS tyst hade suspenderat mitt i inspelningen; den andra lärde sig att anropa suspend() i stället för close() så att inspelningar i följd slutar dundra in i Chromiums tak på ungefär sex kontexter. Resume, suspend, close — det är hela handlingen.

Utveckling
Audio
Desktop
En tappad anslutning borde inte krascha hela din app — men vår gjorde det
Steven
Steven6 min läsning

En tappad anslutning borde inte krascha hela din app — men vår gjorde det

När vår backend gick offline mitt i ett möte pausade den inte bara transkriberingen — den kraschade hela appen. Orsaken var en enda ohanterad händelse, och fixen var tio rader. Det här är releaseklustret som fick GeekBye att förbli inloggad och förbli ansluten genom de saker som förr tog livet av den.

Utveckling
Tillförlitlighet
Electron
Säkerhetskontrollen som gjorde vår app omöjlig att avsluta
Steven
Steven5 min läsning

Säkerhetskontrollen som gjorde vår app omöjlig att avsluta

Automatisk uppdatering var den svåraste funktionen vi någonsin skeppat — sex releaser på fyra dagar för att hindra den från att förstöra appen. Den värsta buggen var en vi själva införde när vi försökte vara försiktiga: en 500-millisekunders "säkerhetskontroll" som förvandlade en misslyckad uppdatering till en process du bokstavligen inte kunde avsluta.

Utveckling
Electron
Tillförlitlighet