
El silenci era portant
Les dues últimes releases de GeekBye v1 tracten de la mateixa veritat incòmoda: la transcripció en temps real sobre una xarxa real no és sense pèrdues, i el moviment honest és deixar de fer veure que ho és. v1.8.20 va guardar una còpia de cada chunk d'àudio al disc abans de descartar-lo durant una reconnexió, i va començar a marcar en veu alta els buits de la transcripció. v1.9.0 va deixar d'enviar silenci per estalviar amplada de banda — i va descobrir que el silenci era exactament el senyal que el transcriptor feia servir per saber que una frase havia acabat. Dues releases sobre el cost de llençar coses.
Les dues últimes releases de GeekBye v1 — v1.8.20 i v1.9.0, les que hi ha just abans de la fita de la versió 2 — comparteixen un tema que només es fa evident quan les llegeixes juntes. Totes dues tracten de coses que l'app llençava silenciosament, i totes dues arriben a la mateixa conclusió: la cosa que descartes per ser eficient feia més feina del que pensaves. Una release estava descartant àudio durant les reconnexions de xarxa. L'altra estava descartant silenci per estalviar amplada de banda. En tots dos casos, la cosa descartada va resultar ser portant.
La transcripció en temps real no és sense pèrdues, i v1.8.20 ho admet
Heus aquí un fet sobre la transcripció de veu a text en streaming sobre un WebSocket que és fàcil d'evitar mirar: quan la connexió cau i es torna a connectar — o quan l'app fa un relleu d'un proveïdor de transcripció a un altre, de Deepgram a ElevenLabs — el client descarta deliberadament els chunks d'àudio capturats durant aquell buit. Ho ha de fer. Un proveïdor acabat de connectar que de sobte rep una acumulació d'àudio ranci s'ennuegarà, o pitjor, enganxarà el seu detector d'activitat de veu a la cosa equivocada. Així que el client llença aquells chunks.
El problema no és el descart. El problema és què significava descartar abans: aquelles paraules simplement no apareixien mai a la transcripció, i res no et deia que faltaven. La gravació semblava completa. No ho era.
v1.8.20 ataca això en dos fronts, i cap d'ells és "fer que les reconnexions no perdin mai àudio" — perquè no pots, no en temps real. En comptes d'això, fa que la pèrdua sigui recuperable i honesta.
La meitat recuperable és un tee de PCM durador. Al gestor de captura del procés principal, cada chunk capturat ara s'escriu a un fitxer al disc abans que arribi a les portes de descart de reconnexió i de període de gràcia:
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
El mecanisme és agradablement de baixa tecnologia: teeChunkToDisk obre un WriteStream, reserva per endavant una capçalera WAV de 44 bytes, hi afegeix mostres mono de 16 bits en brut a mesura que arriben, i esmena la capçalera amb les mides finals en aturar (fs.openSync(..., 'r+'), escriu 44 bytes a l'offset 0). El resultat és un WAV reproduïble de l'àudio complet — incloent-hi les parts que la transcripció en temps real no va veure mai. Quan la gravació acaba, emet un esdeveniment session-audio-saved amb la ruta del fitxer i el recompte de bytes.
Dues notes d'honestedat importen aquí, perquè és fàcil sobredimensionar això. Primera: el tee no omple el buit en aquestes releases. Preserva l'àudio en brut per a una passada de reconciliació posterior — una re-transcripció per lots que reconcilia el fitxer amb la transcripció en directe — que és un ganxo que aquestes releases deixen posat, no una funció que publiquen. El commit de seguiment fins i tot anota session-audio-saved amb un TODO que apunta als issues de reconciliació perquè ningú ho confongui amb codi mort. Segona: això costa disc. L'equip va estimar aproximadament 86 MB per gravació de 30 minuts, així que la mateixa release afegeix un StartupCleanupService que poda les sessions desades de més de 7 dies. Una xarxa de seguretat que creix sense límit és una fuita; la neteja és el que fa que el tee es pugui publicar.
La meitat honesta és encara més simple. Al primer intent de reconnexió, el client escriu una entrada real i visible a la transcripció:
[reconnecting — audio may be missing]
Aquesta única línia és tota la filosofia de la release en miniatura. L'app no pot garantir que ho hagi sentit tot, així que deixa d'insinuar que ho va fer. Un buit que pots veure és un buit que pots evitar; un buit que no pots veure és un informe d'error esperant a passar.
v1.9.0: el silenci que vas optimitzar era el senyal
v1.9.0 és l'última release de v1, i és on el backend va delegar al client un conjunt de responsabilitats de transcripció en directe — descartar silenci, atribuir cada línia de transcripció a l'àudio del micròfon o del sistema des d'una línia de temps local d'activitat de veu, ser propietari del bucle de reconnexió, aplicar límits de gravació. La major part d'això és feina preliminar per a l'arquitectura de la versió 2 i s'ha explicat en un altre lloc. Però un fil d'aquí és la millor petita història de qualsevol de les dues releases, perquè és una solució que va haver de desfer el dany d'una optimització que era completament correcta aïlladament.
Comença amb un canvi òbviament bo: deixa d'enviar silenci. El client va començar a filtrar els chunks la font dominant dels quals era silenci abans d'enviar-los per la socket — menys amplada de banda, menys àudio inútil perquè el backend el processi, i respectava un contracte de client de backend explícit que deia "no enviïs silenci." Tota raó per fer-ho era sòlida.
I va trencar completament la finalització d'utterances. El commit que ho arregla exposa la cadena causal amb una franquesa inusual:
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.
Aquesta és la forma platònica d'una abstracció amb fuites que et torna la mossegada. El detector d'activitat de veu del transcriptor no té una entrada separada de "l'orador ha acabat" — infereix el final d'una frase en sentir aproximadament un segon de silenci en l'àudio. El silenci no era soroll a la línia; era un missatge. En filtrar-lo per estalviar amplada de banda, el client va deixar accidentalment d'enviar l'únic senyal que li deia al transcriptor que una frase havia acabat. Així que cada transcripció parcial simplement seguia creixent, amb la cua esvaint-se en un ... que mai no es resolia.
Podries arreglar això tornant a posar el silenci — però llavors has desfet l'optimització. La solució real és millor: envia el significat del silenci sense enviar el silenci. El client ara rastreja quant temps ha estat en silenci l'àudio localment, i quan això traspassa SILENCE_COMMIT_THRESHOLD_MS = 1000 — deliberadament fet coincidir amb el propi llindar de silenci de la VAD del backend — dispara un senyal commit explícit fora de banda: sendMixedChunk s'adona del silenci sostingut, envia un realtime-audio:commit per IPC, el procés principal reenvia un { type: 'commit' } per la socket, i el backend li diu a ElevenLabs que finalitzi el parcial. Dos petits fragments d'estat, silenceStartedAt i silenceCommitFired, eviten que es dispari repetidament. La victòria d'amplada de banda sobreviu; la finalització torna a funcionar; el ... final es resol.
També hi ha una petita nota a peu de pàgina honesta d'aquesta release: la lògica de reconnexió sense límit que va introduir v1.9.0 es va construir primer al renderer com un WsReconnectController, i després es va eliminar dins de la mateixa release un cop la implementació real va aterrar al gestor del procés principal on pertanyia. La història de la reconnexió en si — retrocés sense límit, proveïdors alternant cada cinc fallades — s'explica en el seu propi capítol; el que hi afegeix v1.9.0 és que és on va néixer aquella arquitectura, i on el client en va prendre possessió per primer cop.
Tres coses que van ensenyar aquestes releases
- Quan no pots evitar la pèrdua, fes-la recuperable i fes-la visible. v1.8.20 no impedeix que les reconnexions descartin àudio — això no és físicament possible en temps real. Guarda una còpia de l'àudio en brut per a més tard, i etiqueta el buit a la transcripció. Cap de les dues coses és glamurosa; juntes converteixen la pèrdua silenciosa de dades en una condició coneguda, limitada i recuperable.
- Una xarxa de seguretat necessita els seus propis límits. El tee durador ompliria silenciosament el disc si es deixés sol — una solució que es converteix en un bug a càmera lenta. La neteja de 7 dies publicada a la mateixa release no és una idea de darrera hora; és la cosa que fa que el tee sigui segur de publicar. Si afegeixes un mecanisme que s'acumula, afegeix el mecanisme que el poda en el mateix alè.
- La cosa que descartes per ser eficient pot ser un senyal. La lliçó més esmolada de qualsevol de les dues releases: filtrar el silenci era correcte en cada eix que normalment comprovaries — amplada de banda, càrrega, compliment del contracte — i tot i així va trencar el producte, perquè un sistema aigües avall estava llegint significat en el silenci. Quan elimines dades per eficiència, pregunta't què estava inferint alguna cosa d'aquelles dades. De vegades has d'enviar el significat explícitament precisament perquè vas deixar d'enviar la cosa de la qual s'inferia abans.
I aquest és el final de la història de la versió 1 — l'últim enduriment abans de la reescriptura. Per al capítol anterior, els tres verbs que mantenen viu el Web Audio (v1.8.16 + v2.0.2); per a on es dirigia tot això, què costa realment una versió 2: 206 commits (v2.0.0); i per a tot l'arc, l'anatomia de publicar programari fins a la perfecció.