
Тишината беше носеща
Последните две издания на GeekBye v1 са за една и съща неудобна истина: транскрипцията в реално време през реална мрежа не е без загуби, а честният ход е да спреш да се преструваш, че е. v1.8.20 запази на диска копие на всеки аудио фрагмент, преди да го изхвърли по време на повторно свързване, и започна да маркира на глас пропуските в транскрипцията. v1.9.0 спря да изпраща тишина, за да пести честотна лента — и откри, че тишината беше точно сигналът, който транскрайбърът използваше, за да знае, че изречението е свършило. Две издания за цената на изхвърлянето на неща.
Последните две издания на GeekBye v1 — v1.8.20 и v1.9.0, тези точно преди етапа на версия 2 — споделят тема, която става очевидна едва когато ги четеш заедно. И двете са за неща, които приложението тихо изхвърляше, и двете стигат до едно и също заключение: нещото, което изхвърляш, за да си ефективен, вършеше повече работа, отколкото си мислел. Едното издание изхвърляше звук по време на мрежови повторни свързвания. Другото изхвърляше тишина, за да пести честотна лента. И в двата случая изхвърленото нещо се оказа носещо.
Транскрипцията в реално време не е без загуби, а v1.8.20 го признава
Ето един факт за стрийминг на реч към текст през WebSocket, от който е лесно да отвърнеш поглед: когато връзката падне и се свърже отново — или когато приложението превключи от един доставчик на транскрипция към друг, от Deepgram към ElevenLabs — клиентът нарочно изхвърля аудио фрагментите, заснети по време на онзи пропуск. Няма как иначе. Прясно свързан доставчик, който внезапно получи натрупване от стар звук, ще се задави или, по-лошо, ще закачи детектора си за гласова активност за грешното нещо. Затова клиентът изхвърля онези фрагменти.
Проблемът не е изхвърлянето. Проблемът е какво означаваше изхвърлянето: онези думи просто никога не се появяваха в транскрипцията, и нищо не ти казваше, че липсват. Записът изглеждаше пълен. Не беше.
v1.8.20 атакува това на два фронта, и нито един от тях не е „направи повторните свързвания никога да не губят звук" — защото не можеш, не в реално време. Вместо това прави загубата възстановима и честна.
Възстановимата половина е трайно PCM tee. В хендлъра за заснемане на главния процес всеки заснет фрагмент вече се записва във файл на диска преди да достигне портите за изхвърляне при повторно свързване и гратисния период:
every captured chunk is written to
<userData>/sessions/session-<ts>.wavbefore realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass
Механизмът е приятно семпъл: teeChunkToDisk отваря WriteStream, резервира предварително 44-байтов WAV header, добавя сурови 16-битови моно семпли при пристигането им и коригира header-а с крайните размери при спиране (fs.openSync(..., 'r+'), записва 44 байта на отместване 0). Резултатът е възпроизводим WAV на пълния звук — включително частите, които транскрипцията в реално време никога не видя. Когато записът приключи, излъчва събитие session-audio-saved с пътя до файла и броя байтове.
Тук са важни две бележки за честност, защото е лесно да преувеличиш това. Първо: tee-то не запълва пропуска в тези издания. То запазва суровия звук за по-късно преминаване за съгласуване — пакетна повторна транскрипция, която съгласува файла с живата транскрипция — което е кука, която тези издания поставят, а не функция, която доставят. Последващият commit дори анотира session-audio-saved с TODO, сочещ към проблемите за съгласуване, така че никой да не го обърка с мъртъв код. Второ: това струва диск. Екипът оцени приблизително 86 MB на 30-минутен запис, така че същото издание добавя StartupCleanupService, който изчиства запазените сесии, по-стари от 7 дни. Предпазна мрежа, която расте без граница, е теч; изчистването е това, което прави tee-то доставимо.
Честната половина е още по-проста. При първия опит за повторно свързване клиентът записва истински, видим запис в транскрипцията:
[reconnecting — audio may be missing]
Онзи единствен ред е цялата философия на изданието в миниатюра. Приложението не може да гарантира, че е чуло всичко, затова спира да намеква, че го е направило. Пропуск, който можеш да видиш, е пропуск, който можеш да заобиколиш; пропуск, който не можеш да видиш, е доклад за бъг, който чака да се случи.
v1.9.0: тишината, която премахна чрез оптимизация, беше сигналът
v1.9.0 е последното v1 издание, и е мястото, където бекендът предаде на клиента набор от отговорности за живата транскрипция — изхвърляне на тишина, приписване на всеки ред от транскрипцията на микрофона или системния звук от локална времева линия на гласова активност, притежаване на цикъла за повторно свързване, налагане на лимити за запис. По-голямата част от това е основа за архитектурата на версия 2 и е разказана другаде. Но една нишка в него е най-добрата малка история в което и да е от изданията, защото е поправка, която трябваше да отмени щетите от оптимизация, която беше напълно правилна изолирано.
Започва с очевидно добра промяна: спри да изпращаш тишина. Клиентът започна да филтрира фрагментите, чийто доминиращ източник беше тишина, преди да ги изпрати през сокета — по-малко честотна лента, по-малко безполезен звук за обработка от бекенда, и това спазваше изричен клиентски договор на бекенда, който казваше „не изпращай тишина". Всяка причина да го направи беше основателна.
И счупи финализирането на изказванията напълно. Commit-ът, който го поправя, излага причинно-следствената верига с необичайна откровеност:
After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize.
Това е платоновата форма на пропусклива абстракция, която хапе обратно. Детекторът за гласова активност на транскрайбъра няма отделен вход „говорещият приключи" — той извежда края на изречението от това, че чува около секунда тишина в звука. Тишината не беше шум по жицата; беше съобщение. Филтрирайки я, за да пести честотна лента, клиентът случайно спря да изпраща единствения сигнал, който казваше на транскрайбъра, че изречението е свършило. Затова всяка частична транскрипция просто продължаваше да расте, опашката ѝ заглъхваше в ..., който никога не се разрешаваше.
Можеш да поправиш това, като върнеш тишината — но тогава си отменил оптимизацията. Истинската поправка е по-добра: изпрати смисъла на тишината, без да изпращаш тишината. Клиентът вече следи колко дълго звукът е бил тих локално, и когато това премине SILENCE_COMMIT_THRESHOLD_MS = 1000 — нарочно съгласувано със собствения праг за тишина на VAD на бекенда — задейства изричен commit сигнал извън лентата: sendMixedChunk забелязва устойчивата тишина, изпраща realtime-audio:commit през IPC, главният процес препраща { type: 'commit' } по сокета, а бекендът казва на ElevenLabs да финализира частичното. Две малки части състояние, silenceStartedAt и silenceCommitFired, го пазят от многократно задействане. Победата за честотната лента оцелява; финализирането работи отново; ... в края се разрешава.
Има и малка, честна бележка под линия към това издание: неограничената логика за повторно свързване, която v1.9.0 въведе, беше първо изградена в renderer-а като WsReconnectController, после изтрита в рамките на същото издание, щом истинската имплементация кацна в хендлъра на главния процес, където ѝ беше мястото. Самата история за повторното свързване — неограничен backoff, редуване на доставчици на всеки пет неуспеха — е разказана в собствена глава; това, което v1.9.0 добавя, е, че тук се роди онази архитектура, и където клиентът за пръв път пое собственост над нея.
Три неща, на които ни научиха тези издания
- Когато не можеш да предотвратиш загубата, направи я възстановима и направи я видима. v1.8.20 не спира повторните свързвания да изхвърлят звук — това не е физически възможно в реално време. То пази копие на суровия звук за по-късно и етикетира пропуска в транскрипцията. Нито едното не е бляскаво; заедно превръщат тихата загуба на данни в познато, ограничено, възстановимо състояние.
- Предпазната мрежа се нуждае от собствени граници. Трайното tee тихо би запълнило диска, ако бъде оставено само — поправка, която се превръща в бавно разгръщащ се бъг. Изчистването на 7 дни, доставено в същото издание, не е допълнителна мисъл; то е това, което прави tee-то безопасно за доставяне изобщо. Ако добавиш механизъм, който натрупва, добави в същия дъх и механизма, който го изчиства.
- Нещото, което изхвърляш, за да си ефективен, може да е сигнал. Най-острата единична поука в което и да е от изданията: филтрирането на тишината беше правилно по всяка ос, която обикновено би проверил — честотна лента, натоварване, спазване на договора — и въпреки това счупи продукта, защото система надолу по веригата четеше смисъл в тишината. Когато премахваш данни заради ефективност, попитай какво извеждаше нещо от тези данни. Понякога трябва да изпратиш смисъла изрично именно защото си спрял да изпращаш нещото, от което той преди се извеждаше.
И това е краят на историята за версия 1 — последното укрепване преди пренаписването. За предишната глава, трите глагола, които поддържат Web Audio жив (v1.8.16 + v2.0.2); за това накъде водеше всичко, какво всъщност изисква една версия 2: 206 комита (v2.0.0); а за цялата дъга, анатомията на доставянето на софтуер до съвършенство.