
Usunęliśmy 5000 linii kodu audio — a transkrypcje zaczęły pojawiać się podwójnie
GeekBye v1.6 wyrwał dwa działające na urządzeniu transkrybery w Swift i zastąpił je jednym, ujednoliconym potokiem — netto skreślenie ponad 5000 linii, wykonane, gdy ludzie byli w trakcie spotkania w aplikacji. Potem transkrypcje zaczęły pojawiać się podwójnie, bo błąd przeniósł się do jedynej warstwy, która wciąż sądziła, że silniki są dwa.
Jest szczególny rodzaj grozy w refaktoryzowaniu kodu, który działa właśnie teraz, dla ludzi, którzy są w trakcie spotkania. Nie da się wyłączyć funkcji na czas konserwacji — ktoś na niej polega, by transkrybować rozmowę, której nie może powtórzyć. GeekBye v1.6 zrobił dokładnie to z najbardziej krytyczną ścieżką w aplikacji: potokiem audio. To opowieść o tym, co usunęliśmy, co się zepsuło i o dwóch lekcjach niezawodności, które z tego wynikły.
Dwa silniki, jeden z nich w Swift
Przed v1.6 zamiana mowy na tekst w GeekBye działała na urządzeniu w binariach Swift. Były dwa: jedno opakowujące framework Apple Speech i większe (ponad 1,500 linii), które przechwytywało podwójne audio — twój mikrofon plus dźwięk systemu — przez ScreenCaptureKit i otwierało własny WebSocket prosto do naszego backendu, by strumieniować do Deepgram. Dwa transkrybery, dwie ścieżki przechwytywania, dwa sposoby rozmowy z siecią, wszystko w skompilowanym języku, co oznaczało, że każda zmiana wymagała rekompilacji binarium.
v1.6.0 zwinął to wszystko w jeden potok. Pojedynczy, ujednolicający commit usunął transkryber Apple Speech (~1,145 linii), przechwytywacz Deepgram w Swift (~1,523 linii), oba mosty w TypeScript, stary orkiestrator i obsługę IPC podwójnego audio — netto skreślenie ponad 5,200 linii. W ich miejsce: renderer przechwytuje audio przez standardowe Web APIs, wysyła je jako PCM przez IPC, a proces główny Electron jest właścicielem jednej ścieżki WebSocket do backendu, z dostawcą wybieranym przez konfigurację backendu. Jedna ścieżka przechwytywania, jeden właściciel gniazda, jedno miejsce do rozumowania.
Usunięcie pięciu tysięcy linii to świetne uczucie. Przez mniej więcej jeden dzień.
Błąd przeniósł się do warstwy, która wciąż sądziła, że są dwa
Potem transkrypcje zaczęły pojawiać się podwójnie. Pojedyncze wypowiedziane zdanie było zapisywane jako dwa identyczne wpisy — czasem gorzej.
Główna przyczyna jest najbardziej pouczającą rzeczą w całym tym wydaniu, bo to ogólne prawo ujednolicania. Ujednoliciliśmy dostawców w procesie głównym — jedno gniazdo, jedna obsługa. Ale warstwa React nie dostała notatki: wciąż montowała dwa haki zdarzeń specyficzne dla dostawców naraz, jeden zbudowany dla Deepgram i jeden dla ElevenLabs. Oba nasłuchiwały. Oba zapisywały. Każda transkrypcja była utrwalana przez to, które haki żyły — a oba żyły.
I był pod tym drugi, bardziej podstępny mechanizm podwajania: ElevenLabs emituje każdą sfinalizowaną linię dwa razy — raz jako zdarzenie committed i ponownie jako committed_with_timestamps. Więc nawet z pojedynczym hakiem jedno zdanie mogło przyjść jako dwa zdarzenia.
Naprawa (w linii v1.6) to schludna dwuczęściówka, która mapuje się dokładnie na dwie przyczyny: bramkuj każdy hak zdarzeń za flagą enabled sterowaną konfiguracją aktywnego dostawcy z backendu, tak by tylko jeden nasłuchiwacz był kiedykolwiek żywy; oraz dodaj deduplikację tekstu per źródło (zapamiętaj ostatnią zapisaną linię dla mikrofonu i dla dźwięku systemu), by przełknąć podwójną emisję ElevenLabs.
Lekcja jest większa niż ten błąd: gdy ujednolicasz dwa backendy za jednym interfejsem, duplikacja nie znika — migruje do warstwy, która wciąż zakłada, że są dwa. Zwinęliśmy hydraulikę w procesie głównym, a wyciek wyszedł na powierzchnię dwie warstwy wyżej, w okablowaniu zdarzeń UI. Ujednolicanie potoku nie jest skończone, gdy producent jest ujednolicony; jest skończone, gdy każdy konsument przestał wierzyć w stary kształt.
Pierwsze ponowne łączenie: „poddaj się po pięciu próbach"
v1.6.0 dostarczył też pierwsze w historii GeekBye automatyczne ponowne łączenie WebSocket z wykładniczym backoffem — i warto je pokazać właśnie dlatego, że to prymitywny przodek kuloodpornej wersji, którą uruchamiamy dziś.
Oryginał był ograniczony. Pięć prób, opóźnienia 1s, 2s, 4s, 8s, 16s — a potem się poddawał, emitując błąd fatalny: „Utracono połączenie — proszę zrestartować transkrypcję." W tamtym czasie wydawało się to odpowiedzialne: nie próbuj w nieskończoność, powiedz użytkownikowi uczciwie. W praktyce był to błąd UX w przebraniu. Szesnastosekundowe zakłócenie sieci — przełączenie Wi-Fi, ponowne połączenie VPN, tunel — jest przejściowe, ale ograniczona liczba prób traktuje je jako terminalne. Użytkownik nie zrobił nic złego, a kazano mu zrestartować spotkanie na żywo.
To dokładnie ta awaria, którą obecny projekt wyeliminował. Dziś GeekBye łączy się ponownie z nieograniczonym backoffem, a nawet cykluje dostawców, i zatrzymuje się jedynie z prawdziwie fatalnego powodu — uwierzytelnianie, limit, rozliczenia. Cała podróż od „poddaj się po pięciu" do „nigdy nie poddawaj się przy błędzie przejściowym" jest opowiedziana w dlaczego twój notatnik AI zatrzymuje się przy słabym Wi-Fi. v1.6 to miejsce, gdzie ta droga się zaczęła, z naiwną wersją, która musiała istnieć najpierw.
Nieaktualne połączenia będą cię prześladować, jeśli nie mają imion
Jeszcze jedna lekcja niezawodności wylądowała w v1.6.3, naprawiając subtelną klasę błędów, którą każdy robiący ponowne łączenie ze stanowym backendem prędzej czy później spotka.
Gdy łączysz się ponownie — lub zmieniasz język transkrypcji w trakcie sesji, co pod maską łączy ponownie — stare połączenie nie zawsze umiera cicho. Jego przedśmiertne komunikaty (CONNECTION_LOST, disconnected) mogą przyjść po tym, jak nowe połączenie jest już zdrowe, i zniszczyć doskonale dobre zastępstwo. Naprawa nadaje każdej próbie połączenia tożsamość — identyfikator sesji per połączenie wtłoczony w URL WebSocket — plus krótkie okno karencji po ponownym połączeniu, podczas którego nieaktualne komunikaty o rozłączeniu z wcześniejszej próby są ignorowane. Wysyła też jawny komunikat stop przed zamknięciem gniazda, by backend mógł czysto zburzyć starą sesję.
Zasada: każda logika ponownego łączenia ze stanowym backendem musi oznaczać każdą próbę tożsamością i traktować spóźnione komunikaty z wcześniejszych prób jako szum — inaczej udane ponowne połączenie zostanie zabite przez przedśmiertny oddech swojego poprzednika.
Trzy rzeczy, których nauczył v1.6
- Ujednolicanie przenosi błędy; nie usuwa ich. Zwiń dwa silniki w jeden i przeaudytuj każdego konsumenta, który wciąż sądzi, że są dwa. Naszym było UI, montujące dwa haki zdarzeń po tym, jak hydraulika już się połączyła.
- Twoje pierwsze ponowne łączenie będzie ograniczone, a ograniczone jest złe dla strumieni na żywo. „Poddaj się po N próbach" zamienia przejściowe zakłócenie w awarię terminalną, o którą obwinia się użytkownika. Odróżniaj fatalne (uwierzytelnianie, limit) od przejściowego (utracone gniazdo) od samej pierwszej wersji.
- Ponowne łączenia potrzebują tożsamości. Oznacz każdą próbę; ignoruj spóźnione komunikaty z martwych prób. Połączenie bez imienia może zostać zamordowane przez własnego ducha.
To drugi rozdział opowieści o niezawodności, która staje się GeekBye v2. Po pierwszy zobacz sagę o automatycznej aktualizacji, która zajęła sześć wydań w cztery dni (v1.5.x); po to, dokąd praca nad ponownym łączeniem ostatecznie dotarła, dlaczego twój notatnik AI zatrzymuje się przy słabym Wi-Fi; a po cały łuk, anatomię dostarczania oprogramowania do perfekcji.