Steven
Steven5 мин. чтения

Мы удалили 5 000 строк аудиокода — и транскрипты начали появляться дважды

GeekBye v1.6 выдрал два транскрайбера на устройстве, написанных на Swift, и заменил их одним унифицированным конвейером — чистое удаление более 5 000 строк, сделанное прямо во время того, как люди сидели на встречах в приложении. А потом транскрипты стали появляться дважды, потому что баг переехал в единственный слой, который всё ещё думал, что движков два.

Инженерия
Транскрипция
Архитектура
Релизы GeekBye
Мы удалили 5 000 строк аудиокода — и транскрипты начали появляться дважды

Есть особый вид ужаса в рефакторинге кода, который работает прямо сейчас, для людей, которые прямо сейчас на встрече. Нельзя отключить функцию на техобслуживание — кто-то полагается на неё, чтобы записать разговор, который нельзя повторить. GeekBye v1.6 сделал ровно это с самым критичным путём в приложении: с аудиоконвейером. Это история о том, что мы удалили, что сломалось и какие два урока о надёжности из этого вышли.

Два движка, один из них на Swift

До v1.6 распознавание речи в GeekBye работало на устройстве в Swift-бинарниках. Их было два: один оборачивал фреймворк Apple Speech, а второй, побольше (более 1 500 строк), захватывал двойное аудио — ваш микрофон плюс системный звук — через ScreenCaptureKit и открывал собственное WebSocket-соединение прямо к нашему бэкенду, чтобы стримить в Deepgram. Два транскрайбера, два пути захвата, два способа общаться с сетью — и всё это на компилируемом языке, из-за которого каждое изменение требовало перекомпиляции бинарника.

v1.6.0 свернул всё это в один конвейер. Единственный объединяющий коммит удалил транскрайбер Apple Speech (~1 145 строк), захватчик Deepgram на Swift (~1 523 строк), оба TypeScript-моста, старый оркестратор и IPC-обработчики двойного аудио — чистое удаление более 5 200 строк. На их месте: рендерер захватывает аудио через стандартные Web APIs, отправляет его как PCM через IPC, а главный процесс Electron владеет единственным путём WebSocket к бэкенду, где провайдер выбирается конфигом бэкенда. Один путь захвата, один владелец сокета, одно место, о котором нужно рассуждать.

Удалить пять тысяч строк — прекрасное чувство. Примерно на день.

Баг переехал в слой, который всё ещё думал, что их два

А потом транскрипты начали появляться дважды. Одно произнесённое предложение сохранялось как две одинаковые записи — иногда хуже.

Первопричина — самое поучительное во всём этом релизе, потому что это общий закон объединения. Мы объединили провайдеров в главном процессе — один сокет, один обработчик. Но слой React не получил уведомления: он всё ещё монтировал два хука событий под конкретных провайдеров одновременно, один сделанный под Deepgram, другой под ElevenLabs. Оба слушали. Оба сохраняли. Каждый транскрипт сохранялся тем хуком, который был жив — а живы были оба.

И под этим было второе, ещё более хитрое удвоение: ElevenLabs выдаёт каждую финализированную строку дважды — сначала как событие committed, а затем как committed_with_timestamps. Так что даже с одним хуком одно предложение могло прийти как два события.

Исправление (в линейке v1.6) — аккуратный двусоставный ответ, который точно ложится на две причины: закрыть каждый хук событий за флагом enabled, управляемым конфигом активного провайдера на бэкенде, чтобы жив был только один слушатель; и добавить дедупликацию текста по источнику (запоминать последнюю сохранённую строку для микрофона и для системного звука), чтобы проглотить двойную выдачу ElevenLabs.

Урок больше, чем этот баг: когда вы объединяете два бэкенда за одним интерфейсом, дублирование не исчезает — оно мигрирует в слой, который всё ещё считает, что их два. Мы свернули трубопровод в главном процессе, а протечка всплыла двумя слоями выше, в проводке событий интерфейса. Объединение конвейера не закончено, когда объединён производитель; оно закончено, когда каждый потребитель перестал верить в старую форму.

Первое переподключение: «сдаться после пяти попыток»

v1.6.0 также отгрузил самое первое автоматическое переподключение WebSocket в GeekBye с экспоненциальной задержкой — и его стоит показать именно потому, что это примитивный предок пуленепробиваемой версии, которую мы гоняем сегодня.

Оригинал был ограниченным. Пять попыток, задержки в 1s, 2s, 4s, 8s, 16s — а затем оно капитулировало, выдавая фатальную ошибку: «Соединение потеряно — пожалуйста, перезапустите транскрипцию.» Тогда это казалось ответственным: не повторять вечно, честно сказать пользователю. На практике это был замаскированный UX-баг. Шестнадцатисекундный сетевой сбой — переключение Wi-Fi, переподключение VPN, туннель — кратковременный, но ограниченный повтор трактует его как окончательный. Пользователь ничего не сделал неправильно и получил указание перезапустить живую встречу.

Именно этот сбой устранил текущий дизайн. Сегодня GeekBye переподключается с неограниченной задержкой и даже перебирает провайдеров, а останавливается только по по-настоящему фатальной причине — аутентификация, квота, биллинг. Весь путь от «сдаться после пяти» до «никогда не сдаваться при кратковременной ошибке» рассказан в статье почему ваш ИИ-конспектировщик останавливается на плохом Wi-Fi. v1.6 — это то место, где эта дорога началась, с наивной версией, которая должна была существовать первой.

Устаревшие соединения преследуют вас, если у них нет имён

Ещё один урок о надёжности приземлился в v1.6.3, исправив тонкий класс багов, с которым в итоге столкнётся каждый, кто делает переподключение поверх бэкенда с состоянием.

Когда вы переподключаетесь — или меняете язык транскрипции посреди сессии, что переподключается под капотом, — старое соединение не всегда умирает тихо. Его предсмертные сообщения (CONNECTION_LOST, disconnected) могут прийти после того, как новое соединение уже здорово, и снести совершенно исправную замену. Исправление даёт каждой попытке соединения идентичность — идентификатор сессии на каждое соединение, прописанный в URL WebSocket, — плюс короткое окно снисхождения после переподключения, в течение которого устаревшие сообщения об отключении от предыдущей попытки игнорируются. Оно также отправляет явное сообщение stop перед закрытием сокета, чтобы бэкенд мог чисто свернуть старую сессию.

Принцип: любой логике переподключения поверх бэкенда с состоянием нужно помечать каждую попытку идентичностью и трактовать запоздалые сообщения от предыдущих попыток как шум — иначе успешное переподключение убивает предсмертный вздох его предшественника.

Три вещи, которым научил v1.6

  1. Объединение перемещает баги; оно их не удаляет. Сверните два движка в один и проаудируйте каждого потребителя, который всё ещё думает, что их два. Нашим был интерфейс, монтировавший два хука событий после того, как трубопровод уже слился.
  2. Ваше первое переподключение будет ограниченным, а ограниченное — неправильно для живых потоков. «Сдаться после N попыток» превращает кратковременный сбой в окончательный отказ, в котором винят пользователя. Различайте фатальное (аутентификация, квота) от кратковременного (оборванный сокет) с самой первой версии.
  3. Переподключениям нужна идентичность. Помечайте каждую попытку; игнорируйте запоздалые сообщения от мёртвых попыток. Соединение без имени может быть убито собственным призраком.

Это вторая глава истории о надёжности, которая становится GeekBye v2. О первой смотрите сагу об автообновлении, занявшую шесть релизов за четыре дня (v1.5.x); о том, куда в итоге пришла работа над переподключением, — почему ваш ИИ-конспектировщик останавливается на плохом Wi-Fi; а о всей дуге — анатомию доведения ПО до совершенства.

Похожие статьи

Транскрипция в реальном времени, когда файрвол блокирует WebSocket
Steven
Steven4 мин. чтения

Транскрипция в реальном времени, когда файрвол блокирует WebSocket

Корпоративные сети обожают пропускать HTTPS и втихую убивать upgrade до WebSocket. Это незаметно ломает транскрипцию в реальном времени. GeekBye v2.0.8 автоматически переключается на чисто-HTTPS транспорт — а его выпуск вскрыл баг, который сделал бы всю функцию бесполезной.

Транскрипция
Сети
Инженерия
Почему ИИ-транскрипция перевирает технические термины (и как мы это исправили)
Steven
Steven6 мин. чтения

Почему ИИ-транскрипция перевирает технические термины (и как мы это исправили)

Живая сессия услышала «what is the pointer in C++» как «what is the point in life». Вот путь расследования от того транскрипта до GeekBye v2.0.11 — keyterm biasing, гонка, обрывавшая соединение, и день, когда наш собственный фикс сработал против нас.

Транскрипция
Надёжность
Инженерия
Разрыв соединения не должен ронять всё приложение — а наше ронял
Steven
Steven6 мин. чтения

Разрыв соединения не должен ронять всё приложение — а наше ронял

Когда наш бэкенд отвалился посреди встречи, он не просто приостановил транскрипцию — он уронил всё приложение. Причиной было единственное необработанное событие, а исправление уместилось в десять строк. Это кластер релизов, благодаря которому GeekBye остаётся залогиненным и на связи через то, что раньше его убивало.

Инженерия
Надёжность
Electron