Steven
Steven6 min di lettura

Abbiamo cancellato 5,000 righe di codice audio — e le trascrizioni hanno iniziato ad apparire due volte

GeekBye v1.6 ha eliminato due trascrittori Swift on-device e li ha sostituiti con un'unica pipeline — una riduzione netta di oltre 5,000 righe, eseguita mentre le persone erano a metà riunione sull'app. Poi le trascrizioni hanno iniziato ad apparire due volte, perché il bug si è spostato nell'unico livello che credeva ancora di avere due motori.

Ingegneria
Trascrizione
Architettura
Release di GeekBye
Abbiamo cancellato 5,000 righe di codice audio — e le trascrizioni hanno iniziato ad apparire due volte

C'è un tipo particolare di terrore nel fare refactoring del codice che sta girando adesso, per persone che sono nel bel mezzo di una riunione. Non puoi mettere offline la funzionalità per manutenzione — qualcuno ci sta contando per trascrivere una conversazione che non può ripetere. GeekBye v1.6 ha fatto esattamente questo al percorso più critico dell'app: la pipeline audio. Questa è la storia di ciò che abbiamo eliminato, di cosa si è rotto, e delle due lezioni di affidabilità che ne sono emerse.

Due motori, uno dei quali in Swift

Prima di v1.6, il riconoscimento vocale di GeekBye girava on-device in binari Swift. Ce n'erano due: uno che avvolgeva il framework Apple Speech, e uno più grande (oltre 1,500 righe) che acquisiva audio duale — il tuo microfono più l'audio di sistema — tramite ScreenCaptureKit e apriva il proprio WebSocket direttamente al nostro backend per lo streaming verso Deepgram. Due trascrittori, due percorsi di acquisizione, due modi di parlare con la rete, tutto in un linguaggio compilato che significava che ogni modifica richiedeva la ricompilazione di un binario.

v1.6.0 ha condensato tutto in un'unica pipeline. Il singolo commit unificante ha eliminato il trascrittore Apple Speech (~1,145 righe), il capturer Swift di Deepgram (~1,523 righe), entrambi i bridge TypeScript, il vecchio orchestratore e gli handler IPC per l'audio duale — una riduzione netta di oltre 5,200 righe. Al loro posto: il renderer acquisisce l'audio tramite Web APIs standard, lo invia come PCM via IPC, e il processo principale di Electron possiede un singolo percorso WebSocket verso il backend, con il provider scelto dalla configurazione del backend. Un percorso di acquisizione, un proprietario del socket, un solo posto dove ragionare.

Eliminare cinquemila righe fa un gran bel effetto. Per circa un giorno.

Il bug si è spostato nel livello che credeva ancora di averne due

Poi le trascrizioni hanno iniziato ad apparire due volte. Una singola frase pronunciata veniva salvata come due voci identiche — a volte peggio.

La causa principale è la cosa più istruttiva di tutta questa release, perché è una legge generale dell'unificazione. Avevamo unificato i provider nel processo principale — un socket, un handler. Ma il livello React non ne era al corrente: montava ancora due hook di evento specifici per provider contemporaneamente, uno costruito per Deepgram e uno per ElevenLabs. Entrambi ascoltavano. Entrambi salvavano. Ogni trascrizione veniva resa persistente da qualunque hook fosse attivo — ed erano attivi entrambi.

E c'era un secondo raddoppiamento più subdolo al di sotto: ElevenLabs emette ogni riga finalizzata due volte — una volta come evento committed e di nuovo come committed_with_timestamps. Quindi anche con un singolo hook, una frase poteva arrivare come due eventi.

Il fix (nella linea v1.6) è un elegante doppietto che si mappa esattamente sulle due cause: mettere ogni hook di evento dietro un flag enabled guidato dalla configurazione del provider attivo del backend, così solo un listener è mai attivo; e aggiungere la deduplicazione del testo per sorgente (ricordare l'ultima riga salvata per il microfono e per l'audio di sistema) per assorbire il doppio-emit di ElevenLabs.

La lezione è più grande di questo bug: quando unifichi due backend dietro un'unica interfaccia, la duplicazione non scompare — migra nel livello che presuppone ancora ce ne siano due. Abbiamo compresso l'idraulica nel processo principale e la perdita è emersa due livelli più su, nel cablaggio degli eventi dell'interfaccia. Unificare una pipeline non è finito quando il producer è unificato; è finito quando ogni consumer ha smesso di credere nella forma vecchia.

La prima riconnessione: "arrendersi dopo cinque tentativi"

v1.6.0 ha anche spedito la primissima riconnessione automatica WebSocket di GeekBye con backoff esponenziale — e vale la pena mostrarla esattamente perché è l'antenato primitivo della versione a prova di proiettile che usiamo oggi.

L'originale era limitato. Cinque tentativi, ritardi di 1s, 2s, 4s, 8s, 16s — e poi si arrendeva, emettendo un errore fatale: "Connessione persa — per favore riavvia la trascrizione." All'epoca sembrava responsabile: non ritentare all'infinito, dire all'utente onestamente. In pratica era un bug UX mascherato. Un calo di rete di sedici secondi — un passaggio di Wi-Fi, una riconnessione VPN, un tunnel — è transitorio, ma un retry limitato lo tratta come terminale. L'utente non ha fatto nulla di sbagliato e gli è stato detto di riavviare una riunione in corso.

Questo è esattamente il fallimento che il design attuale ha eliminato. Oggi GeekBye si riconnette con backoff illimitato e alterna persino i provider, e si ferma solo per una ragione genuinamente fatale — auth, quota, billing. L'intero percorso da "arrendersi dopo cinque" a "non arrendersi mai su un errore transitorio" è raccontato in perché il tuo notetaker con IA si ferma con un cattivo Wi-Fi. v1.6 è dove quella strada è iniziata, con la versione ingenua che doveva esistere prima.

Le connessioni stantie ti perseguitano se non hanno un nome

Un'altra lezione di affidabilità è arrivata in v1.6.3, risolvendo una sottile classe di bug che chiunque faccia riconnessione su un backend stateful incontrerà prima o poi.

Quando ti riconnetti — o cambi lingua di trascrizione a metà sessione, il che riconnette sotto il cofano — la vecchia connessione non sempre muore in silenzio. I suoi messaggi d'agonia (CONNECTION_LOST, disconnected) possono arrivare dopo che la nuova connessione è già sana, e abbattere il perfettamente buon rimpiazzo. Il fix dà a ogni tentativo di connessione un'identità — un ID di sessione per connessione impresso nell'URL del WebSocket — più una breve finestra di tolleranza dopo la riconnessione durante la quale i messaggi di disconnessione stantii di un tentativo precedente vengono ignorati. Invia anche un messaggio esplicito stop prima di chiudere un socket così il backend può abbattere la vecchia sessione in modo pulito.

Il principio: qualsiasi logica di riconnessione su un backend stateful deve etichettare ogni tentativo con un'identità e trattare i messaggi tardivi di tentativi precedenti come rumore — altrimenti una riconnessione riuscita viene uccisa dall'ultimo respiro del suo predecessore.

Tre cose che la v1.6 ci ha insegnato

  1. L'unificazione ri-localizza i bug; non li elimina. Comprimi due motori in uno e controlla ogni consumer che crede ancora ce ne siano due. Il nostro era l'interfaccia, che montava due hook di evento dopo che l'idraulica si era già unificata.
  2. La tua prima riconnessione sarà limitata, e il limitato è sbagliato per gli stream in diretta. "Arrendersi dopo N tentativi" trasforma un calo transitorio in un fallimento terminale di cui l'utente viene incolpato. Distingui fatale (auth, quota) da transitorio (un socket caduto) fin dalla prima versione.
  3. Le riconnessioni hanno bisogno di un'identità. Etichetta ogni tentativo; ignora i messaggi tardivi di tentativi morti. Una connessione senza nome può essere uccisa dal proprio fantasma.

Questo è il secondo capitolo della storia di affidabilità che diventa GeekBye v2. Per il primo, vedi la saga dell'aggiornamento automatico che ha richiesto sei release in quattro giorni (v1.5.x); per dove il lavoro di riconnessione è eventualmente arrivato, perché il tuo notetaker con IA si ferma con un cattivo Wi-Fi; e per l'intero arco, l'anatomia del rilascio software alla perfezione.

Articoli Correlati

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
Una variabile CSS, cinque round di revisione e una toolchain Swift che ha mentito
Steven
Steven8 min di lettura

Una variabile CSS, cinque round di revisione e una toolchain Swift che ha mentito

La v2.0.7 di GeekBye ha reso l'intero overlay traslucido in modo regolabile — cosa che suona come una modifica CSS di una riga e non lo era affatto. La storia vera è ciò che ha intercettato la revisione del codice: due superfici che si rifiutavano di sfumare, una barra di registrazione che sembrava sbagliata alla stessa opacità e un binario compilato che oscillava di 672 byte perché la nostra stessa documentazione ha indicato a un revisore la versione sbagliata di Swift.

Ingegneria
Design
Build
Software sereno: eliminare un interruttore che tremola e insegnare alla chat a dire in che modalità si trova
Steven
Steven6 min di lettura

Software sereno: eliminare un interruttore che tremola e insegnare alla chat a dire in che modalità si trova

Due piccole release di GeekBye, senza funzionalità di punta — solo un interruttore delle impostazioni che ha smesso di tremolare e un assistente di chat che finalmente ti dice se ha risposto in modalità riunione o in modalità programmazione. È questo che costa davvero il "software sereno", un dettaglio alla volta.

Prodotto
Design
Ingegneria