
Ticho bylo nosné
Poslední dvě vydání GeekBye v1 jsou o téže nepříjemné pravdě: přepis v reálném čase přes skutečnou síť není bezeztrátový a poctivý krok je přestat předstírat, že je. v1.8.20 uchovávalo kopii každého audio chunku na disku, než ho během reconnectu zahodilo, a začalo mezery v přepisu označovat nahlas. v1.9.0 přestalo posílat ticho kvůli úspoře šířky pásma — a zjistilo, že ticho bylo přesně ten signál, podle kterého přepisovač poznal, že věta skončila. Dvě vydání o ceně zahazování věcí.
Poslední dvě vydání GeekBye v1 — v1.8.20 a v1.9.0, ta těsně před milníkem verze 2 — sdílejí téma, které je zřejmé teprve, když je čteš spolu. Obě jsou o věcech, které aplikace tiše zahazovala, a obě docházejí ke stejnému závěru: věc, kterou zahazuješ kvůli efektivitě, odváděla víc práce, než sis myslel. Jedno vydání zahazovalo audio během síťových reconnectů. Druhé zahazovalo ticho kvůli úspoře šířky pásma. V obou případech se zahazovaná věc ukázala být nosná.
Přepis v reálném čase není bezeztrátový a v1.8.20 to přiznává
Tady je fakt o streamovaném speech-to-textu přes WebSocket, od kterého se snadno odvrací pohled: když spojení spadne a znovu se připojí — nebo když aplikace přepne z jednoho poskytovatele přepisu na druhého, z Deepgramu na ElevenLabs — klient záměrně zahodí audio chunky zachycené během té mezery. Musí. Čerstvě připojený poskytovatel, který náhle dostane nahromaděnou dávku zastaralého audia, se zakucká, nebo hůř, zachytí svůj detektor hlasové aktivity na špatnou věc. Takže klient ty chunky zahodí.
Problém není v zahazování. Problém je v tom, co zahazování dřív znamenalo: ta slova se prostě nikdy v přepisu neobjevila a nic ti neřeklo, že chybí. Nahrávka vypadala úplná. Nebyla.
v1.8.20 to řeší na dvou frontách a ani jedna z nich není „ať reconnecty nikdy neztratí audio" — protože to nejde, ne v reálném čase. Místo toho dělá ztrátu obnovitelnou a poctivou.
Obnovitelná polovina je trvalá PCM odbočka. V capture handleru hlavního procesu se teď každý zachycený chunk zapíše do souboru na disku předtím, než dorazí k zahazovacím bránám reconnectu a ochranné lhůty:
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
Mechanismus je příjemně nízkotechnologický: teeChunkToDisk otevře WriteStream, dopředu rezervuje 44bajtovou WAV hlavičku, přidává surové 16bitové mono vzorky, jak přicházejí, a při zastavení hlavičku doplní o finální velikosti (fs.openSync(..., 'r+'), zápis 44 bajtů na offset 0). Výsledkem je přehratelný WAV kompletního audia — včetně částí, které živý přepis nikdy neviděl. Když nahrávání skončí, vyšle událost session-audio-saved s cestou k souboru a počtem bajtů.
Tady jsou důležité dvě poznámky v duchu poctivosti, protože je snadné to přehnat. Zaprvé: odbočka mezeru v těchto vydáních nevyplní. Zachovává surové audio pro pozdější rekonciliační průchod — dávkový přepis, který soubor srovná s živým přepisem — což je háček, který tato vydání pokládají, ne funkce, kterou dodávají. Následný commit dokonce session-audio-saved opatří TODO poznámkou odkazující na rekonciliační issues, aby si to nikdo nespletl s mrtvým kódem. Zadruhé: tohle stojí místo na disku. Tým odhadl zhruba 86 MB na 30minutové nahrávání, takže totéž vydání přidává StartupCleanupService, který promazává uložené sesiony starší než 7 dní. Záchranná síť, která roste bez omezení, je únik; úklid je to, co dělá odbočku dodatelnou.
Poctivá polovina je ještě jednodušší. Při prvním pokusu o reconnect klient zapíše do přepisu skutečný, viditelný záznam:
[reconnecting — audio may be missing]
Ten jediný řádek je celá filozofie vydání v miniatuře. Aplikace nemůže zaručit, že slyšela všechno, tak přestane naznačovat, že ano. Mezera, kterou vidíš, je mezera, kterou umíš obejít; mezera, kterou nevidíš, je bug report čekající, až se stane.
v1.9.0: ticho, které jsi odoptimalizoval, byl signál
v1.9.0 je poslední vydání v1 a je to místo, kde backend předal klientovi sadu zodpovědností za živý přepis — zahazování ticha, přiřazování každého řádku přepisu k mikrofonu nebo systémovému audiu z lokální časové osy hlasové aktivity, vlastnictví reconnect smyčky, vynucování limitů nahrávání. Většina toho je základ pro architekturu verze 2 a byla vyprávěna jinde. Ale jedno vlákno v tom je nejlepší malý příběh z obou vydání, protože je to oprava, která musela napravit škodu způsobenou optimalizací, jež byla sama o sobě naprosto správná.
Začíná to očividně dobrou změnou: přestaň posílat ticho. Klient začal odfiltrovávat chunky, jejichž dominantním zdrojem bylo ticho, ještě než je poslal přes socket — méně šířky pásma, méně zbytečného audia ke zpracování na backendu, a ctilo to explicitní klientský kontrakt backendu, který říkal „neposílej ticho". Každý důvod to udělat byl rozumný.
A úplně to rozbilo finalizaci výpovědí. Commit, který to opravuje, rozkládá kauzální řetězec s neobvyklou otevřeností:
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.
Tohle je platónská podoba děravé abstrakce, která se mstí. Detektor hlasové aktivity přepisovače nemá separátní vstup „mluvčí domluvil" — odvozuje konec věty z toho, že v audiu zaslechne zhruba sekundu ticha. Ticho nebylo šum na drátě; byla to zpráva. Tím, že ho klient odfiltroval kvůli úspoře šířky pásma, náhodně přestal posílat ten jediný signál, který přepisovači řekl, že věta skončila. Takže každý částečný přepis prostě dál rostl, ocas se táhl do ..., které se nikdy nevyřešilo.
Mohl bys to opravit tím, že ticho vrátíš zpět — ale pak jsi odoptimalizoval optimalizaci. Skutečná oprava je lepší: pošli význam ticha, aniž bys posílal ticho. Klient teď lokálně sleduje, jak dlouho je audio tiché, a když to překročí SILENCE_COMMIT_THRESHOLD_MS = 1000 — záměrně sladěné s vlastním prahem ticha VAD na backendu — vystřelí explicitní signál commit mimo pásmo: sendMixedChunk si všimne trvalého ticha, pošle realtime-audio:commit přes IPC, hlavní proces přepošle { type: 'commit' } po socketu a backend řekne ElevenLabs, ať částečný přepis finalizuje. Dva malé kousky stavu, silenceStartedAt a silenceCommitFired, brání opakovanému vystřelení. Výhra v šířce pásma přežívá; finalizace zase funguje; koncové ... se vyřeší.
I k tomuto vydání patří malá, poctivá poznámka pod čarou: neomezená reconnect logika, kterou v1.9.0 zavedlo, byla nejdřív postavena v rendereru jako WsReconnectController, a pak během téhož vydání smazána, jakmile skutečná implementace přistála v handleru hlavního procesu, kam patřila. Samotný příběh reconnectu — neomezený backoff, střídání poskytovatelů každých pět selhání — je vyprávěn ve vlastní kapitole; co v1.9.0 přidává, je to, že je to místo, kde se ta architektura zrodila a kde ji klient poprvé převzal do vlastnictví.
Tři věci, které nás tato vydání naučila
- Když nemůžeš zabránit ztrátě, udělej ji obnovitelnou a udělej ji viditelnou. v1.8.20 nebrání reconnectům v zahazování audia — to v reálném čase fyzicky nejde. Uchovává kopii surového audia na později a označí mezeru v přepisu. Ani jedno není okázalé; společně mění tichou ztrátu dat na známý, ohraničený, obnovitelný stav.
- Záchranná síť potřebuje vlastní meze. Trvalá odbočka by potichu zaplnila disk, kdyby ji člověk nechal být — oprava, která se stane bugem ve zpomaleném záběru. Sedmidenní úklid dodaný v témže vydání není dodatečný nápad; je to to, co vůbec dělá odbočku bezpečnou k dodání. Když přidáš mechanismus, který se hromadí, přidej jedním dechem i mechanismus, který ho promazává.
- Věc, kterou zahodíš kvůli efektivitě, může být signál. Ta nejostřejší lekce z obou vydání: odfiltrování ticha bylo správné na každé ose, kterou bys normálně kontroloval — šířka pásma, zátěž, dodržení kontraktu — a přesto to rozbilo produkt, protože navazující systém do ticha vkládal význam. Když odstraňuješ data kvůli efektivitě, ptej se, co si z těch dat něco odvozovalo. Někdy musíš poslat význam explicitně právě proto, že jsi přestal posílat věc, ze které se dřív odvozoval.
A to je konec příběhu verze 1 — poslední zpevňování před přepisem. Předchozí kapitolu najdeš v tři slovesa, která udržují web audio naživu (v1.8.16 + v2.0.2); kam to všechno směřovalo, v co doopravdy stojí verze 2: 206 commitů (v2.0.0); a celý oblouk v anatomii dodávání softwaru k dokonalosti.