Steven
Steven6 min citire

Am șters 5.000 de linii de cod audio — și transcrierile au început să apară de două ori

GeekBye v1.6 a smuls din rădăcini două transcriere Swift care rulau pe dispozitiv și le-a înlocuit cu un singur pipeline unificat — o ștergere netă de peste 5.000 de linii, făcută în timp ce oamenii erau în plină ședință pe aplicație. Apoi transcrierile au început să apară de două ori, pentru că bug-ul s-a mutat pe singurul strat care încă credea că există două motoare.

Inginerie
Transcriere
Arhitectură
Lansări GeekBye
Am șters 5.000 de linii de cod audio — și transcrierile au început să apară de două ori

Există un tip aparte de teroare în refactorizarea codului care rulează chiar acum, pentru oameni care sunt în plină ședință. Nu poți scoate funcția din uz pentru mentenanță — cineva se bazează pe ea ca să transcrie o conversație pe care n-o poate repeta. GeekBye v1.6 a făcut exact asta celei mai critice căi din aplicație: pipeline-ul audio. Aceasta e povestea a ceea ce am șters, a ceea ce s-a stricat și a celor două lecții de fiabilitate care au ieșit din ea.

Două motoare, unul dintre ele în Swift

Înainte de v1.6, transcrierea voce-în-text din GeekBye rula pe dispozitiv, în binare Swift. Erau două: unul care învelea framework-ul Speech de la Apple și unul mai mare (peste 1.500 de linii) care captura audio dual — microfonul tău plus audio-ul de sistem — prin ScreenCaptureKit și își deschidea propriul WebSocket direct către backend-ul nostru pentru a face stream către Deepgram. Două transcriere, două căi de captură, două moduri de a vorbi cu rețeaua, toate într-un limbaj compilat, ceea ce însemna că fiecare modificare cerea recompilarea unui binar.

v1.6.0 a comasat totul într-un singur pipeline. Commit-ul unic de unificare a șters transcrierea Apple Speech (~1.145 de linii), captatorul Deepgram din Swift (~1.523 de linii), ambele punți TypeScript, vechiul orchestrator și handler-ele IPC pentru audio dual — o ștergere netă de peste 5.200 de linii. În locul lor: renderer-ul captează audio prin Web APIs standard, îl trimite ca PCM peste IPC, iar procesul principal Electron deține o singură cale WebSocket către backend, cu furnizorul ales prin configurația de pe backend. O singură cale de captură, un singur proprietar de socket, un singur loc despre care să raționezi.

Să ștergi cinci mii de linii e o senzație grozavă. Cam o zi.

Bug-ul s-a mutat pe stratul care încă credea că sunt două

Apoi transcrierile au început să apară de două ori. O singură propoziție rostită era salvată ca două intrări identice — uneori chiar mai rău.

Cauza rădăcină e cel mai instructiv lucru din toată această lansare, pentru că e o lege generală a unificării. Unificasem furnizorii în procesul principal — un socket, un handler. Dar stratul React nu primise memo-ul: încă monta două hook-uri de eveniment specifice fiecărui furnizor în același timp, unul construit pentru Deepgram și unul pentru ElevenLabs. Ambele ascultau. Ambele salvau. Fiecare transcriere era persistată de oricare dintre hook-urile care erau active — și amândouă erau.

Și mai era o dublare, mai vicleană, dedesubt: ElevenLabs emite fiecare linie finalizată de două ori — o dată ca eveniment committed și încă o dată ca committed_with_timestamps. Așa că, chiar și cu un singur hook, o propoziție putea sosi ca două evenimente.

Reparația (în seria v1.6) e o soluție îngrijită în două părți, care se mapează exact pe cele două cauze: pune fiecare hook de eveniment în spatele unui flag enabled condus de configurația furnizorului activ de pe backend, ca doar un singur ascultător să fie vreodată activ; și adaugă deduplicarea textului pe sursă (ține minte ultima linie salvată pentru microfon și pentru audio-ul de sistem) ca să înghiți dubla emisie a ElevenLabs.

Lecția e mai mare decât acest bug: când unifici două backend-uri în spatele unei singure interfețe, duplicarea nu dispare — migrează spre stratul care încă presupune că sunt două. Am comasat instalația în procesul principal, iar scurgerea a ieșit la suprafață cu două straturi mai sus, în conectarea evenimentelor din interfață. Unificarea unui pipeline nu e gata când producătorul e unificat; e gata când fiecare consumator a încetat să creadă în vechea formă.

Prima reconectare: „renunță după cinci încercări”

v1.6.0 a livrat și prima reconectare automată WebSocket cu backoff exponențial din GeekBye — și merită arătată tocmai pentru că e strămoșul primitiv al versiunii de neînfrânt pe care o rulăm azi.

Originalul era limitat. Cinci încercări, cu întârzieri de 1s, 2s, 4s, 8s, 16s — și apoi se preda, emițând o eroare fatală: „Conexiune pierdută — te rugăm repornește transcrierea.” La momentul respectiv părea responsabil: nu reîncerca la nesfârșit, spune-i utilizatorului cinstit. În practică era un bug de UX deghizat. O întrerupere de rețea de șaisprezece secunde — o predare Wi-Fi, o reconectare VPN, un tunel — e pasageră, dar o reîncercare limitată o tratează ca definitivă. Utilizatorul n-a greșit cu nimic și i s-a spus să repornească o ședință în desfășurare.

Ăsta e exact eșecul pe care l-a eliminat design-ul actual. Astăzi GeekBye se reconectează cu backoff nelimitat și chiar rotește furnizorii, oprindu-se doar pentru un motiv cu adevărat fatal — autentificare, cotă, facturare. Întregul drum de la „renunță după cinci” la „nu renunța niciodată la o eroare pasageră” e povestit în de ce notetaker-ul tău AI se oprește pe Wi-Fi prost. v1.6 e locul de unde a pornit acel drum, cu versiunea naivă care trebuia să existe mai întâi.

Conexiunile vechi te bântuie dacă nu au nume

Încă o lecție de fiabilitate a aterizat în v1.6.3, reparând o clasă subtilă de bug pe care oricine face reconectare peste un backend cu stare o va întâlni în cele din urmă.

Când te reconectezi — sau schimbi limba de transcriere în plină sesiune, ceea ce se reconectează pe dedesubt — conexiunea veche nu moare mereu în liniște. Mesajele ei de agonie (CONNECTION_LOST, disconnected) pot sosi după ce noua conexiune e deja sănătoasă și pot dărâma înlocuitorul perfect bun. Reparația dă fiecărei încercări de conexiune o identitate — un ID de sesiune per conexiune ștanțat pe URL-ul WebSocket — plus o scurtă fereastră de grație după reconectare, în care mesajele vechi de deconectare de la o încercare anterioară sunt ignorate. Trimite și un mesaj explicit stop înainte de a închide un socket, ca backend-ul să poată închide curat vechea sesiune.

Principiul: orice logică de reconectare peste un backend cu stare trebuie să eticheteze fiecare încercare cu o identitate și să trateze mesajele întârziate de la încercările anterioare ca pe zgomot — altfel o reconectare reușită e ucisă de ultima suflare a predecesorului ei.

Trei lucruri pe care ne-a învățat v1.6

  1. Unificarea relocalizează bug-urile; nu le șterge. Comasezi două motoare într-unul și auditează fiecare consumator care încă crede că sunt două. Al nostru era interfața, montând două hook-uri de eveniment după ce instalația se contopise deja.
  2. Prima ta reconectare va fi limitată, iar limitat e greșit pentru stream-uri live. „Renunță după N încercări” transformă o întrerupere pasageră într-un eșec definitiv de care e învinuit utilizatorul. Distinge fatalul (autentificare, cotă) de pasager (un socket căzut) încă din prima versiune.
  3. Reconectările au nevoie de identitate. Etichetează fiecare încercare; ignoră mesajele întârziate de la încercările moarte. O conexiune fără nume poate fi ucisă de propria fantomă.

Acesta e al doilea capitol al poveștii de fiabilitate care devine GeekBye v2. Pentru primul, vezi saga auto-update-ului care a luat șase lansări în patru zile (v1.5.x); pentru locul unde a ajuns în cele din urmă munca de reconectare, de ce notetaker-ul tău AI se oprește pe Wi-Fi prost; iar pentru întregul arc, anatomia livrării software-ului la perfecțiune.