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; а о всей дуге — анатомию доведения ПО до совершенства.

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

Убираем backend с пути загрузки
Steven
Steven7 мин. чтения

Убираем backend с пути загрузки

GeekBye записывает твой экран и сохраняет видео в твой Google Drive. Первая версия прогоняла каждую запись через собственные серверы GeekBye по пути туда; релизом позже файл шёл прямо с твоей машины на Drive, а backend был понижен до хранения единственного указателя. Интересная часть в том, как мало кода на самом деле содержит «прямая, возобновляемая» версия — потому что возобновляемость пришла из удаления proxy, а не из его написания.

Инженерия
Архитектура
Десктоп
Одна кодовая база, два приложения: как сделать white-label без форка
Steven
Steven5 мин. чтения

Одна кодовая база, два приложения: как сделать white-label без форка

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

Инженерия
Архитектура
Сборка
Что на самом деле представляет собой релиз из 127 коммитов
Steven
Steven5 мин. чтения

Что на самом деле представляет собой релиз из 127 коммитов

GeekBye v1.7.0 — это 127 коммитов за одиннадцать дней. Снаружи это выглядит как сотня мелочей. Изнутри это были две крупные функции, сплетённые вместе, — и одну из них сначала построили не в том месте, потом вырвали и переделали посреди релиза. Вот анатомия крупного релиза.

Инженерия
Релиз
Архитектура