Steven
Steven8 min di lettura

Il silenzio era portante

Le ultime due release di GeekBye v1 ruotano attorno alla stessa verità scomoda: la trascrizione in tempo reale su una rete vera non è senza perdite, e la mossa onesta è smettere di fingere che lo sia. La v1.8.20 teneva su disco una copia di ogni chunk audio prima di scartarlo durante una riconnessione, e ha iniziato a segnare ad alta voce i buchi nella trascrizione. La v1.9.0 ha smesso di inviare silenzio per risparmiare banda — e ha scoperto che il silenzio era proprio il segnale che il trascrittore usava per sapere che una frase era finita. Due release sul costo di buttare via le cose.

Ingegneria
Audio
Affidabilità
Release di GeekBye
Il silenzio era portante

Le ultime due release di GeekBye v1 — la v1.8.20 e la v1.9.0, quelle appena prima del traguardo della versione 2 — condividono un tema che diventa ovvio solo quando le leggi insieme. Entrambe riguardano cose che l'app buttava via in silenzio, ed entrambe arrivano alla stessa conclusione: ciò che scarti per essere efficiente stava facendo più lavoro di quanto pensassi. Una release scartava audio durante le riconnessioni di rete. L'altra scartava silenzio per risparmiare banda. In entrambi i casi, la cosa scartata si è rivelata portante.

La trascrizione in tempo reale non è senza perdite, e la v1.8.20 lo ammette

Ecco un fatto sullo speech-to-text in streaming su un WebSocket da cui è facile distogliere lo sguardo: quando la connessione cade e si riconnette — o quando l'app fa failover da un provider di trascrizione a un altro, da Deepgram a ElevenLabs — il client scarta di proposito i chunk audio catturati durante quel buco. Deve. Un provider appena connesso che di colpo riceve un arretrato di audio stantio si strozza, o peggio, aggancia il suo rilevatore di attività vocale alla cosa sbagliata. Così il client butta via quei chunk.

Il problema non è lo scarto. Il problema è ciò che lo scarto significava: quelle parole semplicemente non comparivano mai nella trascrizione, e nulla ti avvisava che mancavano. La registrazione sembrava completa. Non lo era.

La v1.8.20 lo affronta su due fronti, e nessuno dei due è «fare in modo che le riconnessioni non perdano mai audio» — perché non puoi, non in tempo reale. Rende invece la perdita recuperabile e onesta.

La metà recuperabile è un tee PCM durevole. Nell'handler di cattura del processo principale, ogni chunk catturato viene ora scritto in un file su disco prima di raggiungere le porte di scarto di riconnessione e di periodo di grazia:

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

Il meccanismo è di una semplicità piacevole: teeChunkToDisk apre un WriteStream, riserva in anticipo un header WAV di 44 byte, accoda i campioni mono grezzi a 16 bit man mano che arrivano, e rattoppa l'header con le dimensioni finali all'arresto (fs.openSync(..., 'r+'), scrivere 44 byte all'offset 0). Il risultato è un WAV riproducibile dell'audio completo — comprese le parti che la trascrizione in tempo reale non ha mai visto. Quando la registrazione finisce, emette un evento session-audio-saved con il percorso del file e il conteggio dei byte.

Qui contano due note di onestà, perché è facile promettere troppo con questo. Primo: il tee non riempie il buco in queste release. Preserva l'audio grezzo per una passata di riconciliazione successiva — una ritrascrizione in batch che riconcilia il file con la trascrizione dal vivo — che è un aggancio che queste release posano, non una funzionalità che consegnano. Il commit di seguito annota perfino session-audio-saved con un TODO che punta agli issue di riconciliazione così che nessuno lo scambi per codice morto. Secondo: questo costa disco. Il team ha stimato circa 86 MB per registrazione da 30 minuti, così la stessa release aggiunge un StartupCleanupService che pota le sessioni salvate più vecchie di 7 giorni. Una rete di sicurezza che cresce senza limite è una perdita; la pulizia è ciò che rende il tee consegnabile.

La metà onesta è ancora più semplice. Al primo tentativo di riconnessione, il client scrive una voce reale e visibile nella trascrizione:

[reconnecting — audio may be missing]

Quella singola riga è tutta la filosofia della release in miniatura. L'app non può garantire di aver sentito tutto, così smette di lasciar intendere di averlo fatto. Un buco che puoi vedere è un buco che puoi aggirare; un buco che non puoi vedere è una segnalazione di bug in attesa di accadere.

v1.9.0: il silenzio che hai ottimizzato fino a farlo sparire era il segnale

La v1.9.0 è l'ultima release della v1, ed è dove il backend ha passato al client un insieme di responsabilità della trascrizione dal vivo — scartare il silenzio, attribuire ogni riga di trascrizione al microfono o all'audio di sistema da una timeline locale di attività vocale, possedere il loop di riconnessione, imporre i limiti di registrazione. La maggior parte di questo è lavoro preparatorio per l'architettura della versione 2 ed è stato raccontato altrove. Ma uno dei suoi fili è la migliore piccola storia di entrambe le release, perché è un fix che ha dovuto disfare il danno di un'ottimizzazione che era completamente corretta presa isolatamente.

Comincia con un cambiamento ovviamente buono: smettere di inviare il silenzio. Il client ha iniziato a filtrare i chunk la cui sorgente dominante era silenzio prima di spedirli sul socket — meno banda, meno audio inutile da elaborare per il backend, e onorava un contratto client esplicito del backend che diceva «non inviare silenzio». Ogni ragione per farlo era solida.

E ha rotto completamente la finalizzazione delle utterance. Il commit che lo corregge espone la catena causale con un candore insolito:

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.

Questa è la forma platonica di un'astrazione che perde e ti morde di rimando. Il rilevatore di attività vocale del trascrittore non ha un ingresso separato per «chi parla ha finito» — deduce la fine di una frase sentendo circa un secondo di silenzio nell'audio. Il silenzio non era rumore sul cavo; era un messaggio. Filtrandolo via per risparmiare banda, il client ha smesso senza volerlo di inviare l'unico segnale che diceva al trascrittore che una frase era finita. Così ogni trascrizione parziale continuava solo a crescere, con la coda che sfumava in dei ... che non si risolvevano mai.

Potresti correggere questo rimettendo il silenzio — ma allora hai disfatto l'ottimizzazione. Il fix vero è migliore: inviare il significato del silenzio senza inviare il silenzio. Il client ora traccia localmente da quanto tempo l'audio è silenzioso, e quando ciò supera SILENCE_COMMIT_THRESHOLD_MS = 1000 — deliberatamente allineato alla soglia di silenzio del VAD del backend stesso — spara un segnale commit esplicito fuori banda: sendMixedChunk nota il silenzio prolungato, invia un realtime-audio:commit via IPC, il processo principale inoltra un { type: 'commit' } sul socket, e il backend dice a ElevenLabs di finalizzare il parziale. Due piccoli pezzi di stato, silenceStartedAt e silenceCommitFired, gli impediscono di sparare ripetutamente. La vittoria di banda sopravvive; la finalizzazione funziona di nuovo; i ... sospesi si risolvono.

C'è anche una piccola, onesta nota a piè di pagina per questa release: la logica di riconnessione senza limite che la v1.9.0 ha introdotto è stata prima costruita nel renderer come un WsReconnectController, poi eliminata all'interno della stessa release una volta che l'implementazione vera è atterrata nell'handler del processo principale, dove le competeva. La storia della riconnessione in sé — backoff senza limite, provider alternati ogni cinque fallimenti — è raccontata in un capitolo a sé; ciò che la v1.9.0 aggiunge è che è lì che quell'architettura è nata, e dove il client ne ha preso possesso per la prima volta.

Tre cose che queste release hanno insegnato

  1. Quando non puoi impedire la perdita, rendila recuperabile e rendila visibile. La v1.8.20 non impedisce alle riconnessioni di scartare audio — non è fisicamente possibile in tempo reale. Tiene una copia dell'audio grezzo per dopo, ed etichetta il buco nella trascrizione. Nessuna delle due è affascinante; insieme trasformano una perdita silenziosa di dati in una condizione nota, limitata e recuperabile.
  2. Una rete di sicurezza ha bisogno dei propri limiti. Il tee durevole riempirebbe il disco in silenzio se lasciato a sé — un fix che diventa un bug al rallentatore. La pulizia a 7 giorni consegnata nella stessa release non è un ripensamento; è ciò che rende il tee sicuro da consegnare, punto. Se aggiungi un meccanismo che accumula, aggiungi nello stesso respiro il meccanismo che lo pota.
  3. Ciò che scarti per essere efficiente potrebbe essere un segnale. La lezione più affilata di entrambe le release: filtrare via il silenzio era corretto su ogni asse che controlleresti di norma — banda, carico, conformità al contratto — e ha comunque rotto il prodotto, perché un sistema a valle stava leggendo un significato nel silenzio. Quando rimuovi dati per efficienza, chiediti cosa stava deducendo qualcosa da quei dati. A volte devi inviare il significato esplicitamente proprio perché hai smesso di inviare la cosa da cui prima veniva dedotto.

E questa è la fine della storia della versione 1 — l'ultimo irrobustimento prima della riscrittura. Per il capitolo precedente, i tre verbi che tengono vivo il Web Audio (v1.8.16 + v2.0.2); per dove tutto questo era diretto, cosa serve davvero per una versione 2: 206 commit (v2.0.0); e per tutto l'arco, l'anatomia del pubblicare software fino alla perfezione.

Articoli Correlati

I tre verbi che tengono vivo il Web Audio
Steven
Steven9 min di lettura

I tre verbi che tengono vivo il Web Audio

Due release puntuali di GeekBye, a due mesi di distanza e in due file diversi, hanno insegnato al nostro codice audio la stessa lezione da estremità opposte: smettere di trattare l'AudioContext del browser come usa e getta. Una release ha imparato a fare resume() di un contesto che macOS aveva silenziosamente sospeso a metà registrazione; l'altra ha imparato a fare suspend() invece di close() così che sessioni consecutive smettano di schiantarsi contro il tetto di circa sei contesti di Chromium. Resume, suspend, close — è tutta qui la trama.

Ingegneria
Audio
Desktop
Una connessione caduta non dovrebbe mandare in crash tutta la tua app — ma la nostra lo faceva
Steven
Steven7 min di lettura

Una connessione caduta non dovrebbe mandare in crash tutta la tua app — ma la nostra lo faceva

Quando il nostro backend è andato offline nel mezzo di una riunione, non ha solo messo in pausa la trascrizione — ha mandato in crash l'intera app. La causa era un singolo evento non gestito, e la correzione erano dieci righe. Questo è il gruppo di release che ha reso GeekBye capace di restare connesso all'account e restare connesso alla rete attraverso le cose che prima lo uccidevano.

Ingegneria
Affidabilità
Electron
Distinguere una chiamata da un'app aperta
Steven
Steven9 min di lettura

Distinguere una chiamata da un'app aperta

GeekBye può accorgersi che ti sei unito a una riunione video e offrirti di registrarla. La rilevazione si rivela la metà facile — un binario Swift che legge i titoli delle finestre ogni dieci secondi. La metà difficile è la precisione: non scattare quando Zoom è semplicemente aperto, non proporti di registrare una riunione che stai già registrando, e non silenziare il microfono nella chiamata in cui sei davvero. Tre release, e ognuna è una salvaguardia che ha dovuto imparare a non sabotare se stessa.

Ingegneria
macOS
Desktop