Steven
Steven6 perc olvasás

5000 Sor Hangkódot Töröltünk — és az Átiratok Elkezdtek Kétszer Megjelenni

A GeekBye v1.6 kitépte a két eszközön futó Swift átíró motort, és egyetlen egységes pipeline-ra cserélte őket — nettó több mint 5000 sor törlésével, miközben emberek épp meeting közben használták az alkalmazást. Aztán az átiratok elkezdtek kétszer megjelenni, mert a hiba átvándorolt az egyetlen rétegbe, amelyik még mindig azt hitte, két motor van.

Mérnökség
Átírás
Architektúra
GeekBye Kiadások
5000 Sor Hangkódot Töröltünk — és az Átiratok Elkezdtek Kétszer Megjelenni

Van egy sajátos fajta rettegés abban, amikor olyan kódot alakítasz át, ami épp most fut, olyan emberek számára, akik meeting közben vannak. Nem tudod karbantartásra leállítani a funkciót — valaki egy olyan beszélgetés átírásában bízik benne, amit nem tud megismételni. A GeekBye v1.6 pontosan ezt tette az alkalmazás legkritikusabb útvonalával: a hang-pipeline-nal. Ez a története annak, mit töröltünk, mi tört el, és a két megbízhatósági tanulságnak, ami ebből kijött.

Két motor, az egyik Swiftben

A v1.6 előtt a GeekBye beszéd-szöveg átírása az eszközön futott, Swift binárisokban. Kettő volt belőlük: az egyik az Apple Speech keretrendszert csomagolta be, a másik pedig egy nagyobb (több mint 1,500 soros), amely kettős hangot rögzített — a mikrofonodat plusz a rendszerhangot — ScreenCaptureKit segítségével, és a saját WebSocketjét nyitotta közvetlenül a hátterünkhöz, hogy a Deepgramhez streameljen. Két átíró, két rögzítési útvonal, kétféleképpen beszélni a hálózattal, mind egy fordított nyelvben, ami azt jelentette, hogy minden változtatás egy bináris újrafordítását igényelte.

A v1.6.0 mindezt egyetlen pipeline-ba omlasztotta. Az egyetlen egyesítő commit törölte az Apple Speech átírót (~1,145 sor), a Deepgram Swift rögzítőt (~1,523 sor), mindkét TypeScript hidat, a régi orchestratort és a kettős hangú IPC handlereket — nettó több mint 5,200 sor törlése. Helyükön: a renderer szabványos Web APIs-on keresztül rögzíti a hangot, PCM-ként küldi tovább IPC-n, az Electron fő folyamata pedig egyetlen WebSocket-útvonalat birtokol a háttér felé, a szolgáltatót a háttér konfigurációja választja. Egy rögzítési útvonal, egy socket-tulajdonos, egy hely, ahol átgondolni.

Ötezer sort törölni nagyszerű érzés. Nagyjából egy napig.

A hiba átvándorolt a rétegbe, amelyik még mindig azt hitte, kettő van

Aztán az átiratok elkezdtek kétszer megjelenni. Egyetlen kimondott mondat két azonos bejegyzésként mentődött el — néha rosszabbul.

Az alapvető ok a legtanulságosabb dolog ebben az egész kiadásban, mert ez az egységesítés egy általános törvénye. A szolgáltatókat a fő folyamatban egyesítettük — egy socket, egy handler. De a React réteg nem kapta meg az üzenetet: még mindig két szolgáltató-specifikus eseménykampót csatolt fel egyszerre, egyet a Deepgramhez, egyet az ElevenLabshoz építve. Mindkettő figyelt. Mindkettő mentett. Minden átiratot elmentett bármelyik kampó, amelyik élt — és mindkettő élt.

És volt egy második, alattomosabb duplázás alatta: az ElevenLabs minden lezárt sort kétszer bocsát ki — egyszer committed eseményként, majd újra committed_with_timestamps-ként. Így még egyetlen kampóval is egy mondat két eseményként érkezhetett.

A javítás (a v1.6-os vonalban) egy takaros kétrészes megoldás, ami pontosan a két okra képeződik le: minden eseménykampót egy enabled zászló mögé zárunk, amelyet a háttér aktív-szolgáltató konfigurációja vezérel, így egyszerre csak egy figyelő él; és hozzáadunk egy forrásonkénti szöveg-deduplikációt (megjegyezzük az utoljára mentett sort a mikrofonhoz és a rendszerhanghoz), hogy elnyeljük az ElevenLabs kettős kibocsátását.

A tanulság nagyobb ennél a hibánál: amikor két hátteret egyesítesz egyetlen felület mögött, a duplikáció nem tűnik el — átvándorol abba a rétegbe, amelyik még mindig feltételezi, hogy kettő van. A vízvezetéket a fő folyamatban omlasztottuk össze, és a szivárgás két réteggel feljebb bukkant fel, a felület eseménykapcsolásában. Egy pipeline egységesítése nincs kész, amikor a producer egységes; akkor van kész, amikor minden fogyasztó abbahagyta a régi alakban való hitet.

Az első újracsatlakozás: „add fel öt próba után"

A v1.6.0 szállította a GeekBye legelső WebSocket automatikus újracsatlakozását exponenciális backoffal is — és érdemes megmutatni, épp azért, mert a primitív őse annak a golyóálló verziónak, amit ma futtatunk.

Az eredeti korlátozott volt. Öt próbálkozás, 1s, 2s, 4s, 8s, 16s késleltetéssel — aztán megadta magát, egy végzetes hibát bocsátva ki: „Kapcsolat elveszett — kérjük, indítsa újra az átírást." Akkoriban ez felelősségteljesnek tűnt: ne próbálkozz örökké, mondd meg őszintén a felhasználónak. A gyakorlatban álruhás UX-hiba volt. Egy tizenhat másodperces hálózati kihagyás — egy Wi-Fi-átadás, egy VPN-újracsatlakozás, egy alagút — átmeneti, de egy korlátozott újrapróbálkozás végzetesként kezeli. A felhasználó semmi rosszat nem tett, és azt mondták neki, hogy indítson újra egy élő meetinget.

Pontosan ez az a hiba, amit a jelenlegi kialakítás megszüntetett. Ma a GeekBye korlátlan backoffal csatlakozik újra, sőt szolgáltatókat is váltogat, és csak egy valóban végzetes okért áll le — hitelesítés, kvóta, számlázás. Az egész út a „add fel öt után"-tól a „soha ne add fel egy átmeneti hibánál"-ig itt van elmesélve: miért áll le az AI jegyzetelőd rossz Wi-Fin. A v1.6 az, ahol ez az út elindult, a naiv verzióval, aminek először léteznie kellett.

Az elavult kapcsolatok kísértenek, hacsak nincs nevük

Még egy megbízhatósági tanulság landolt a v1.6.3-ban, egy finom hibaosztályt javítva, amivel bárki, aki egy állapottal rendelkező háttér felett újracsatlakozást csinál, előbb-utóbb találkozni fog.

Amikor újracsatlakozol — vagy menet közben váltasz átírási nyelvet, ami a háttérben újracsatlakozik —, a régi kapcsolat nem mindig hal meg csendben. Halálhörgés-üzenetei (CONNECTION_LOST, disconnected) megérkezhetnek azután, hogy az új kapcsolat már egészséges, és lerombolhatják a tökéletesen jó utódot. A javítás minden kapcsolódási próbálkozásnak identitást ad — egy kapcsolatonkénti session-azonosítót a WebSocket URL-jére bélyegezve — plusz egy rövid türelmi ablakot az újracsatlakozás után, amely alatt egy korábbi próbálkozásból származó elavult disconnect-üzeneteket figyelmen kívül hagyunk. Egy explicit stop üzenetet is küld egy socket bezárása előtt, hogy a háttér tisztán lebonthassa a régi session-t.

Az elv: bármely újracsatlakozási logikának egy állapottal rendelkező háttér felett minden próbálkozást identitással kell megjelölnie, és a korábbi próbálkozásokból érkező kései üzeneteket zajként kell kezelnie — különben egy sikeres újracsatlakozást megöl az elődje utolsó lehelete.

Három dolog, amire a v1.6 megtanított

  1. Az egységesítés áthelyezi a hibákat; nem törli őket. Omlassz össze két motort eggyé, és auditáld minden fogyasztót, amelyik még mindig azt hiszi, kettő van. A miénk a felület volt, két eseménykampót felcsatolva azután, hogy a vízvezetéket már összevontuk.
  2. Az első újracsatlakozásod korlátozott lesz, és a korlátozott rossz az élő streamekhez. Az „add fel N próba után" egy átmeneti kihagyást egy végzetes hibává változtat, amiért a felhasználót hibáztatják. Különböztesd meg a végzetest (hitelesítés, kvóta) az átmenetitől (egy leesett socket) a legelső verziótól kezdve.
  3. Az újracsatlakozásoknak identitás kell. Jelölj meg minden próbálkozást; hagyd figyelmen kívül a halott próbálkozások kései üzeneteit. Egy név nélküli kapcsolatot megölhet a saját szelleme.

Ez a második fejezete a megbízhatósági történetnek, ami GeekBye v2-vé válik. Az elsőért lásd az automatikus frissítés sagáját, ami hat kiadást vett igénybe négy nap alatt (v1.5.x); ahová az újracsatlakozási munka végül megérkezett, miért áll le az AI jegyzetelőd rossz Wi-Fin; és az egész ívért a szoftver tökéletességig szállításának anatómiája.