Steven
Steven1 分で読める

沈黙は耐荷重だった

GeekBye v1の最後の二つのリリースは、同じ居心地の悪い真実についてのものです: 現実のネットワーク越しのリアルタイム文字起こしはロスレスではなく、誠実な一手はそのふりをやめることだ、と。v1.8.20は、再接続中にオーディオチャンクをドロップする前に、そのすべてのコピーをディスクに保ち、そして文字起こしのギャップを声に出して印づけ始めました。v1.9.0は帯域幅を節約するために沈黙を送るのをやめ — そして、その沈黙こそが、文の終わりを知るために文字起こしエンジンが使っていたまさにその信号だったと気づきました。物を投げ捨てることの代償についての、二つのリリースです。

エンジニアリング
Audio
信頼性
GeekByeリリース
沈黙は耐荷重だった

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>.wav before realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass

その仕組みは心地よいほど素朴です: teeChunkToDiskWriteStreamを開き、44バイトのWAVヘッダを前もって予約し、生の16ビットモノラルサンプルを到着するそばから追記し、停止時に最終的なサイズでヘッダをパッチします(fs.openSync(..., 'r+')、オフセット0に44バイトを書き込む)。結果は_完全な_オーディオの再生可能なWAVです — リアルタイムの文字起こしが決して見なかった部分も含めて。録画が終わると、ファイルパスとバイト数を伴ったsession-audio-savedイベントを発します。

ここで二つの誠実さのメモが大切です、なぜならこれは大げさに言い過ぎやすいからです。第一に: teeはこれらのリリースではギャップを埋めません。それは生のオーディオを、後の突き合わせパスの_ために_保存します — ファイルをライブの文字起こしと突き合わせるバッチ再文字起こし — であり、それはこれらのリリースが敷く足がかりであって、出荷する機能ではありません。後続のコミットは、誰もそれを死んだコードと取り違えないよう、session-audio-savedに突き合わせのイシューを指すTODOで注記までしています。第二に: これはディスクを食います。チームは30分の録画あたりおよそ86 MBと見積もり、だから同じリリースが、7日より古い保存されたセッションを刈り込むStartupCleanupServiceを加えます。際限なく育つ安全網は漏れです; そのクリーンアップこそが、teeを出荷可能にするものです。

誠実な半分はさらに単純です。最初の再接続の試みで、クライアントは本物の、見える、エントリを文字起こしへ書き込みます:

[reconnecting — audio may be missing]

その一行は、このリリースの哲学のすべてを縮図にしたものです。アプリはすべてを聞いたと保証できないので、聞いたとほのめかすのをやめます。あなたが見えるギャップは、あなたが回避できるギャップです; あなたが見えないギャップは、起きるのを待っているバグ報告です。

v1.9.0: あなたが最適化で消し去った沈黙こそが信号だった

v1.9.0は最後のv1リリースであり、バックエンドがライブ文字起こしの一連の責務をクライアントへ引き渡した場所です — 沈黙をドロップすること、各文字起こし行を局所的な音声活動のタイムラインからマイクかシステムオーディオに帰属させること、再接続ループを所有すること、録画の上限を強制すること。その大半はバージョン2のアーキテクチャのための地ならしであり、別の場所で語られてきました。しかしその中の一本の糸は、どちらのリリースを通しても最良の小さな物語です、なぜならそれは、単独では完全に正しかった最適化がもたらした害を、元に戻さねばならなかった修正だからです。

それは明らかに良い変更から始まります: 沈黙を送るのをやめる。 クライアントは、支配的な発生源が沈黙であるチャンクを、ソケット越しに送り出す前に濾し取り始めました — 帯域幅が減り、バックエンドが処理すべき無用なオーディオが減り、そしてそれは「沈黙を送るな」と言う明示的なバックエンドのクライアント契約を守りました。それをする理由はどれも健全でした。

そしてそれは発話の確定を完全に壊しました。それを直すコミットは、その因果の連鎖を並外れた率直さで並べます:

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に部分を確定するよう告げます。二つの小さな状態、silenceStartedAtsilenceCommitFiredが、それが繰り返し発火するのを防ぎます。帯域幅の勝利は生き延びます; 確定は再び働きます; 末尾の...は解決します。

このリリースにも、小さな、誠実な脚注があります: v1.9.0が導入した際限のない再接続ロジックは、まず_レンダラー_の中でWsReconnectControllerとして作られ、それから本物の実装が、それが属するメインプロセスのハンドラに着地するや、同じリリースの中で削除されました。再接続の物語そのもの — 際限のないバックオフ、五回の失敗ごとに交互するプロバイダ — はそれ自身の章で語られます; v1.9.0が加えるのは、そのアーキテクチャが生まれた場所であり、クライアントが初めてそれを所有した場所だ、ということです。

これらのリリースが教えた三つのこと

  1. 損失を防げないとき、それを回復可能にし、そして見えるようにせよ。 v1.8.20は、再接続がオーディオをドロップするのを止めません — それはリアルタイムでは物理的に不可能です。それは生のオーディオのコピーを後のために保ち、文字起こしの中のギャップにラベルを貼ります。どちらも華やかではありません; 二つ合わせて、それらは静かなデータ損失を、既知の、境界のある、回復可能な状態へと変えます。
  2. 安全網はそれ自身の境界を必要とする。 永続的なteeは、放っておけば静かにディスクを埋め尽くすでしょう — スローモーションのバグになる修正です。同じリリースで出荷された7日間のクリーンアップは、後付けの思いつきではありません; それこそが、teeをそもそも安全に出荷可能にするものです。積み上がる仕組みを加えるなら、それを刈り込む仕組みを同じ息で加えなさい。
  3. 効率のためにあなたが捨てるものは、信号かもしれない。 どちらのリリースを通しても最も鋭い一つの教訓: 沈黙を濾し取ることは、あなたが普通に確かめるあらゆる軸で正しく — 帯域幅、負荷、契約の遵守 — それでもなお製品を壊しました、なぜなら下流のシステムがその沈黙に意味を読み取っていたからです。効率のためにデータを取り除くとき、そのデータから何かを_推論_していたものは何かを問いなさい。ときには、意味を明示的に送らねばならない、まさにあなたがそれが推論されていた元のものを送るのをやめたがゆえに。

そしてそれがバージョン1の物語の終わりです — 書き直しの前の最後の堅牢化です。前の章については、Web Audioを生かし続ける三つの動詞(v1.8.16 + v2.0.2)を; これらすべてが向かっていた先については、バージョン2が実際に要するもの: 206のコミット(v2.0.0)を; そして弧の全体については、ソフトウェアを完璧に出荷することの解剖学をご覧ください。

関連記事

Web Audioを生かし続ける三つの動詞
Steven
Steven2 分で読める

Web Audioを生かし続ける三つの動詞

二ヶ月を隔てて別々のファイルで出た二つのGeekByeポイントリリースが、私たちのオーディオコードに同じ教訓を正反対の端から教えました: ブラウザのAudioContextを使い捨てとして扱うのをやめよ、と。一方のリリースは、macOSが録画中にこっそりsuspendしたコンテキストをresume()することを学びました; もう一方は、連続するセッションがChromiumのおよそ六コンテキストの上限に激突するのをやめさせるべく、close()する代わりにsuspend()することを学びました。resume、suspend、close — それが筋の全部です。

エンジニアリング
Audio
デスクトップ
接続が切れただけでアプリ全体が落ちてはいけない — でも私たちのは落ちた
Steven
Steven1 分で読める

接続が切れただけでアプリ全体が落ちてはいけない — でも私たちのは落ちた

バックエンドが会議の途中でオフラインになったとき、それは文字起こしを止めただけではありませんでした — アプリ全体をクラッシュさせたのです。原因はたった一つの処理されないイベント、修正は十行でした。これは、GeekByeがかつて自分を殺していた事態を通り抜けても、サインインを保ち、接続を保つようにしたリリース群の物語です。

エンジニアリング
信頼性
Electron
アプリを終了不可能にした「安全チェック」
Steven
Steven1 分で読める

アプリを終了不可能にした「安全チェック」

自動アップデートは、私たちが出荷した中で最も難しい機能でした — アプリを文鎮化させないために、4日間で6回のリリース。最悪のバグは、慎重であろうとして自ら仕込んだものでした: 失敗したアップデートを、文字通り終了できないプロセスに変えてしまった、500ミリ秒の「安全」チェックです。

エンジニアリング
Electron
信頼性