
Изтрихме 5000 реда аудио код — и транскриптите започнаха да се появяват по два пъти
GeekBye v1.6 изкорени два транскрибера на Swift, работещи на самото устройство, и ги замени с един унифициран пайплайн — нетно изтриване на над 5000 реда, направено докато хора бяха насред срещи в приложението. После транскриптите започнаха да се появяват по два пъти, защото грешката се премести в единствения слой, който още смяташе, че двигателите са два.
Има особен вид ужас в рефакторирането на кода, който работи точно сега, за хора, които са насред среща. Не можеш да свалиш функцията за поддръжка — някой разчита на нея да транскрибира разговор, който не може да повтори. GeekBye v1.6 направи точно това с най-критичния път в приложението: аудио пайплайна. Това е историята на онова, което изтрихме, онова, което се счупи, и двата урока за надеждност, които излязоха от нея.
Два двигателя, единият от тях на Swift
Преди v1.6 реч-към-текст в GeekBye работеше на самото устройство в Swift binaries. Бяха два: единият обвиваше Apple Speech framework, а по-големият (над 1,500 реда) улавяше двойно аудио — микрофона ти плюс системния звук — през ScreenCaptureKit и отваряше свой собствен WebSocket директно към нашия backend, за да стриймва към Deepgram. Два транскрибера, два пътя на улавяне, два начина за комуникация с мрежата, всичко на компилиран език, което означаваше, че всяка промяна изискваше рекомпилиране на binary.
v1.6.0 сви всичко това в един пайплайн. Единственият унифициращ комит изтри транскрибера на Apple Speech (~1,145 реда), Deepgram Swift улавящия компонент (~1,523 реда), двата TypeScript моста, стария оркестратор и IPC handler-ите за двойно аудио — нетно изтриване на над 5,200 реда. На тяхно място: рендерерът улавя аудиото през стандартни Web APIs, изпраща го като PCM през IPC, а главният процес на Electron държи един-единствен WebSocket път към backend-а, като доставчикът се избира от конфигурацията на backend-а. Един път на улавяне, един собственик на сокета, едно място, където да разсъждаваш.
Да изтриеш пет хиляди реда е страхотно усещане. За около ден.
Грешката се премести в слоя, който още смяташе, че двигателите са два
После транскриптите започнаха да се появяват по два пъти. Едно изречено изречение се запазваше като два идентични записа — понякога и по-зле.
Първопричината е най-поучителното нещо в цялото това издание, защото е общ закон на унификацията. Бяхме унифицирали доставчиците в главния процес — един сокет, един handler. Но React слоят не беше получил бележката: той все още монтираше два специфични за доставчик event hook-а едновременно, единият построен за Deepgram, а другият за ElevenLabs. И двата слушаха. И двата запазваха. Всеки транскрипт се персистираше от онзи hook, който беше жив — а те и двата бяха.
И имаше второ, по-коварно удвояване отдолу: ElevenLabs излъчва всеки финализиран ред по два пъти — веднъж като committed събитие и отново като committed_with_timestamps. Така че дори с един hook едно изречение можеше да пристигне като две събития.
Поправката (в линията v1.6) е спретната двусъставна, която се картографира точно върху двете причини: постави всеки event hook зад enabled флаг, управляван от конфигурацията за активен доставчик на backend-а, така че само един слушател да е жив; и добави де-дублиране на текста по източник (запомни последния запазен ред за микрофона и за системния звук), за да погълне двойното излъчване на ElevenLabs.
Урокът е по-голям от тази грешка: когато унифицираш два backend-а зад един интерфейс, дублирането не изчезва — то мигрира към слоя, който все още допуска, че двигателите са два. Свихме водопровода в главния процес и течът изплува два слоя по-нагоре, в event wiring-а на интерфейса. Унифицирането на пайплайн не е свършено, когато продуцентът е унифициран; свършено е, когато всеки консуматор е спрял да вярва в старата форма.
Първото преповързване: „предай се след пет опита"
v1.6.0 достави и първото WebSocket автоматично преповързване на GeekBye с експоненциален backoff — и си струва да го покажем именно защото е примитивният предшественик на непробиваемата версия, която работи днес.
Оригиналът беше ограничен. Пет опита, забавяния от 1s, 2s, 4s, 8s, 16s — и после се предаваше, излъчвайки фатална грешка: „Връзката е изгубена — моля, рестартирайте транскрипцията." Тогава това изглеждаше отговорно: не опитвай вечно, кажи на потребителя честно. На практика беше UX грешка под прикритие. Шестнайсетсекундно мрежово прекъсване — превключване на Wi-Fi, преповързване на VPN, тунел — е преходно, но ограниченият retry го третира като терминално. Потребителят не направи нищо погрешно и му казаха да рестартира среща на живо.
Точно този провал елиминира текущият дизайн. Днес GeekBye се преповързва с неограничен backoff и дори циклично сменя доставчиците, и спира само по истински фатална причина — автентикация, квота, плащане. Цялото пътуване от „предай се след пет" до „никога не се предавай при преходна грешка" е разказано в защо AI бележникът ти спира при лошо Wi-Fi. v1.6 е там, откъдето този път започна, с наивната версия, която трябваше да съществува първа.
Остарелите връзки те преследват, освен ако нямат имена
Още един урок за надеждност пристигна в v1.6.3, поправяйки коварен клас грешки, който всеки, който прави преповързване върху backend със състояние, рано или късно ще срещне.
Когато се преповързваш — или сменяш езика на транскрипцията насред сесия, което под капака преповързва — старата връзка не винаги умира тихо. Нейните предсмъртни съобщения (CONNECTION_LOST, disconnected) могат да пристигнат след като новата връзка вече е здрава, и да съборят напълно добрия заместник. Поправката дава на всеки опит за връзка идентичност — session ID за всяка връзка, отпечатан върху WebSocket URL — плюс кратък гратисен прозорец след преповързване, по време на който остарелите disconnect съобщения от предишен опит се игнорират. Тя изпраща и явно stop съобщение преди затваряне на сокета, за да може backend-ът да разгради старата сесия чисто.
Принципът: всяка логика за преповързване върху backend със състояние трябва да отбелязва всеки опит с идентичност и да третира закъснелите съобщения от предишни опити като шум — иначе успешно преповързване бива убито от предсмъртния дъх на предшественика си.
Три неща, на които ни научи v1.6
- Унификацията премества грешките; не ги изтрива. Свий два двигателя в един и одитирай всеки консуматор, който още смята, че са два. Нашият беше интерфейсът, монтиращ два event hook-а, след като водопроводът вече беше слят.
- Първото ти преповързване ще е ограничено, а ограниченото е грешно за стриймове на живо. „Предай се след N опита" превръща преходно прекъсване в терминален провал, за който обвиняват потребителя. Разграничи фаталното (автентикация, квота) от преходното (изпуснат сокет) още от първата версия.
- Преповързванията се нуждаят от идентичност. Отбелязвай всеки опит; игнорирай закъснелите съобщения от мъртви опити. Връзка без име може да бъде убита от собствения си призрак.
Това е втората глава на историята за надеждността, която се превръща в GeekBye v2. За първата виж сагата за авто-обновяването, която отне шест издания за четири дни (v1.5.x); за това къде работата по преповързването в крайна сметка пристигна, защо AI бележникът ти спира при лошо Wi-Fi; а за цялата дъга, анатомията на доставянето на софтуер до съвършенство.