
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.
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
- 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.
- 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.
- 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.