Steven
Steven8 min citire

Tăcerea era portantă

Ultimele două lansări ale GeekBye v1 sunt despre același adevăr incomod: transcrierea în timp real peste o rețea reală nu e fără pierderi, iar mișcarea onestă e să încetezi să pretinzi că e. v1.8.20 a păstrat pe disc o copie a fiecărui fragment audio înainte să-l arunce în timpul unei reconectări și a început să marcheze cu voce tare golurile din transcriere. v1.9.0 a încetat să trimită tăcere ca să economisească lățime de bandă — și a descoperit că tăcerea era exact semnalul pe care transcriitorul îl folosea ca să știe că o propoziție s-a încheiat. Două lansări despre costul aruncării lucrurilor.

Inginerie
Audio
Fiabilitate
Lansări GeekBye
Tăcerea era portantă

Ultimele două lansări ale GeekBye v1 — v1.8.20 și v1.9.0, cele de chiar dinaintea reperului versiunii 2 — împărtășesc o temă care devine evidentă abia când le citești împreună. Ambele sunt despre lucruri pe care aplicația le arunca în tăcere, și ambele ajung la aceeași concluzie: lucrul pe care îl arunci ca să fii eficient făcea mai multă muncă decât credeai. O lansare arunca audio în timpul reconectărilor de rețea. Cealaltă arunca tăcere ca să economisească lățime de bandă. În ambele cazuri, lucrul aruncat s-a dovedit a fi portant.

Transcrierea în timp real nu e fără pierderi, iar v1.8.20 o recunoaște

Iată un fapt despre speech-to-text în flux peste un WebSocket de care e ușor să-ți ferești privirea: când conexiunea cade și se reconectează — sau când aplicația trece de la un furnizor de transcriere la altul, de la Deepgram la ElevenLabs — clientul aruncă deliberat fragmentele audio captate în timpul acelui gol. Trebuie s-o facă. Un furnizor proaspăt conectat care primește brusc un teanc de audio învechit se va îneca sau, mai rău, își va agăța detectorul de activitate vocală de lucrul greșit. Așa că clientul aruncă acele fragmente.

Problema nu e aruncarea. Problema e ce însemna aruncarea: acele cuvinte pur și simplu nu apăreau niciodată în transcriere, și nimic nu-ți spunea că lipsesc. Înregistrarea părea completă. Nu era.

v1.8.20 atacă asta pe două fronturi, și niciunul dintre ele nu e „fă reconectările să nu piardă niciodată audio" — pentru că nu poți, nu în timp real. În schimb, face pierderea recuperabilă și onestă.

Jumătatea recuperabilă e un tee PCM durabil. În handler-ul de captură din procesul principal, fiecare fragment captat e acum scris într-un fișier pe disc înainte să ajungă la porțile de aruncare de la reconectare și perioada de grație:

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

Mecanismul e plăcut de rudimentar: teeChunkToDisk deschide un WriteStream, rezervă din start un antet WAV de 44 de octeți, adaugă eșantioane brute mono pe 16 biți pe măsură ce sosesc și corectează antetul cu dimensiunile finale la oprire (fs.openSync(..., 'r+'), scrie 44 de octeți la offset-ul 0). Rezultatul e un WAV redabil al audio-ului complet — inclusiv părțile pe care transcrierea în timp real nu le-a văzut niciodată. Când înregistrarea se încheie, emite un eveniment session-audio-saved cu calea fișierului și numărul de octeți.

Aici contează două note de onestitate, pentru că e ușor să exagerezi asta. Prima: tee-ul nu umple golul în aceste lansări. Păstrează audio-ul brut pentru o trecere ulterioară de reconciliere — o re-transcriere în lot care reconciliază fișierul cu transcrierea live — care e un cârlig pe care aceste lansări îl pun, nu o funcționalitate pe care o livrează. Commit-ul ulterior chiar adnotează session-audio-saved cu un TODO care indică problemele de reconciliere, ca nimeni să nu-l confunde cu cod mort. A doua: asta costă spațiu pe disc. Echipa a estimat aproximativ 86 MB per înregistrare de 30 de minute, așa că aceeași lansare adaugă un StartupCleanupService care curăță sesiunile salvate mai vechi de 7 zile. O plasă de siguranță care crește fără limită e o scurgere; curățarea e ceea ce face tee-ul livrabil.

Jumătatea onestă e și mai simplă. La prima încercare de reconectare, clientul scrie o intrare reală, vizibilă, în transcriere:

[reconnecting — audio may be missing]

Acea singură linie e toată filozofia lansării în miniatură. Aplicația nu poate garanta că a auzit totul, așa că încetează să sugereze că a făcut-o. Un gol pe care îl poți vedea e un gol pe care îl poți ocoli; un gol pe care nu-l poți vedea e un raport de bug care așteaptă să se întâmple.

v1.9.0: tăcerea pe care ai eliminat-o prin optimizare era semnalul

v1.9.0 e ultima lansare v1, și e locul unde backend-ul a predat clientului un set de responsabilități legate de transcrierea live — aruncarea tăcerii, atribuirea fiecărei linii de transcriere microfonului sau audio-ului de sistem dintr-o cronologie locală de activitate vocală, deținerea buclei de reconectare, impunerea plafoanelor de înregistrare. Cea mai mare parte din asta e temelie pentru arhitectura versiunii 2 și a fost povestită în altă parte. Dar un fir din ea e cea mai bună poveste mică din oricare dintre lansări, pentru că e o reparație care a trebuit să anuleze daunele unei optimizări complet corecte izolat.

Începe cu o schimbare evident bună: oprește trimiterea tăcerii. Clientul a început să filtreze fragmentele a căror sursă dominantă era tăcerea înainte să le trimită peste socket — mai puțină lățime de bandă, mai puțin audio inutil de procesat pentru backend, și onora un contract-client explicit al backend-ului care spunea „nu trimite tăcere". Fiecare motiv de a o face era solid.

Și a stricat complet finalizarea enunțurilor. Commit-ul care o repară expune lanțul cauzal cu o candoare neobișnuită:

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.

Aceasta e forma platonică a unei abstracții cu scurgeri care mușcă înapoi. Detectorul de activitate vocală al transcriitorului nu are o intrare separată „vorbitorul a terminat" — el deduce sfârșitul unei propoziții auzind cam o secundă de tăcere în audio. Tăcerea nu era zgomot pe fir; era un mesaj. Filtrând-o ca să economisească lățime de bandă, clientul a încetat din greșeală să trimită singurul semnal care îi spunea transcriitorului că o propoziție s-a încheiat. Așa că fiecare transcriere parțială pur și simplu continua să crească, coada pierzându-se într-un ... care nu se rezolva niciodată.

Ai putea repara asta punând tăcerea la loc — dar atunci ai anulat optimizarea. Reparația reală e mai bună: trimite sensul tăcerii fără să trimiți tăcerea. Clientul urmărește acum de cât timp audio-ul e tăcut local, și când asta depășește SILENCE_COMMIT_THRESHOLD_MS = 1000 — potrivit deliberat cu propriul prag de tăcere al VAD-ului backend-ului — declanșează un semnal commit explicit în afara benzii: sendMixedChunk observă tăcerea susținută, trimite un realtime-audio:commit peste IPC, procesul principal transmite mai departe un { type: 'commit' } pe socket, iar backend-ul îi spune lui ElevenLabs să finalizeze parțiala. Două mici bucăți de stare, silenceStartedAt și silenceCommitFired, îl împiedică să se declanșeze în mod repetat. Victoria de lățime de bandă supraviețuiește; finalizarea funcționează din nou; ...-ul de la coadă se rezolvă.

Există și o mică notă onestă de subsol la această lansare: logica de reconectare nemărginită pe care a introdus-o v1.9.0 a fost construită întâi în renderer ca un WsReconnectController, apoi ștearsă în aceeași lansare odată ce implementarea reală a ajuns în handler-ul din procesul principal, acolo unde îi era locul. Povestea reconectării în sine — backoff nemărginit, alternarea furnizorilor la fiecare cinci eșecuri — e spusă în propriul capitol; ceea ce adaugă v1.9.0 e că aici s-a născut acea arhitectură, și unde clientul a preluat prima dată proprietatea asupra ei.

Trei lucruri pe care ni le-au învățat aceste lansări

  1. Când nu poți preveni pierderea, fă-o recuperabilă și fă-o vizibilă. v1.8.20 nu împiedică reconectările să arunce audio — asta nu e fizic posibil în timp real. Păstrează o copie a audio-ului brut pentru mai târziu și etichetează golul din transcriere. Niciuna nu e spectaculoasă; împreună transformă pierderea silențioasă de date într-o condiție cunoscută, mărginită, recuperabilă.
  2. O plasă de siguranță are nevoie de propriile limite. Tee-ul durabil ar umple în tăcere discul dacă ar fi lăsat în pace — o reparație care devine un bug cu încetinitorul. Curățarea de 7 zile livrată în aceeași lansare nu e o idee de ultim moment; e lucrul care face tee-ul sigur de livrat în general. Dacă adaugi un mecanism care acumulează, adaugă în aceeași suflare mecanismul care îl curăță.
  3. Lucrul pe care îl arunci ca să fii eficient poate fi un semnal. Cea mai ascuțită lecție din oricare dintre lansări: filtrarea tăcerii era corectă pe fiecare axă pe care ai verifica-o în mod normal — lățime de bandă, încărcare, respectarea contractului — și tot a stricat produsul, pentru că un sistem din aval citea sens în tăcere. Când elimini date din motive de eficiență, întreabă-te ce deducea ceva din acele date. Uneori trebuie să trimiți sensul explicit tocmai pentru că ai încetat să trimiți lucrul din care obișnuia să fie dedus.

Și cu asta se încheie povestea versiunii 1 — ultima consolidare înainte de rescriere. Pentru capitolul anterior, cele trei verbe care mențin Web Audio în viață (v1.8.16 + v2.0.2); pentru încotro se îndrepta totul, ce presupune de fapt o versiune 2: 206 commit-uri (v2.0.0); iar pentru întregul arc, anatomia livrării de software până la perfecțiune.

Articole Similare

Cele trei verbe care mențin Web Audio în viață
Steven
Steven9 min citire

Cele trei verbe care mențin Web Audio în viață

Două lansări punctuale GeekBye, la două luni distanță și în două fișiere diferite, au învățat codul nostru audio aceeași lecție din capete opuse: nu mai trata AudioContext-ul browserului ca pe ceva de unică folosință. O lansare a învățat să apeleze resume() pe un context pe care macOS îl suspendase în tăcere în mijlocul înregistrării; cealaltă a învățat să apeleze suspend() în loc de close(), ca sesiunile consecutive să nu mai izbească plafonul Chromium de aproximativ șase contexte. Resume, suspend, close — asta e toată intriga.

Inginerie
Audio
Desktop
O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea
Steven
Steven7 min citire

O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea

Când backend-ul nostru a picat în plină ședință, nu doar că a oprit transcrierea — a prăbușit întreaga aplicație. Cauza era un singur eveniment netratat, iar reparația a fost de zece linii. Acesta e grupul de lansări care a făcut GeekBye să rămână autentificat și conectat prin lucrurile care înainte îl omorau.

Inginerie
Fiabilitate
Electron
Verificarea de siguranță care a făcut aplicația noastră imposibil de închis
Steven
Steven6 min citire

Verificarea de siguranță care a făcut aplicația noastră imposibil de închis

Auto-update a fost cea mai grea funcție pe care am livrat-o vreodată — șase lansări în patru zile ca s-o oprim din a strica aplicația. Cel mai urât bug a fost unul pe care l-am introdus chiar noi încercând să fim precauți: o verificare de „siguranță" de 500 de milisecunde care transforma un update eșuat într-un proces pe care pur și simplu nu-l puteai închide.

Inginerie
Electron
Fiabilitate