リアルタイム音声AIの設計初日、「ブラウザとOpenAI Realtime APIをどう繋ぐか」で2つのアーキテクチャを比較した。全トラフィックが自サーバーを通るWebSocket中継と、ブラウザがOpenAIへ直接繋ぐWebRTC。当時は中継方式を選んだが、今回両方実装してUIで切り替えられるようにした。机上の比較表を、実測で答え合わせできる状態にしたわけである。
WebRTC直結モードの設計
WebRTCでは音声(メディアトラック)もイベント(データチャネル oai-events)もブラウザ⇄OpenAI直結になる。ではサーバーは失業するかというと、そうはならない。
- 一時キーの発行 — サーバーがAPIキーで
client_secretsを叩き、ペルソナ設定を埋め込んだ数分で失効する一時キー(ek_…)を発行。APIキー本体は今まで通りブラウザに渡さない - 検索の実行代行 — function callingのイベントはブラウザに直接届くので、ブラウザが検知して
/api/searchにお願いし、結果をデータチャネルでOpenAIへ返す - 履歴の受け取り — 中継が会話を見られないため、ブラウザが文字起こしを自己申告で送ってくる
つまり「音声の通り道」は手放しても、「頭脳の置き場所」は選べる。
検証には小技を使った。開発環境のブラウザにはマイク権限がないので、getUserMedia を AudioContext の合成ストリームに差し替え、macOSの say コマンドで作った音声を流し込む。偽マイクでWebRTCの文字起こしまでE2Eテストできる。
実測: 固有名詞で殴り合わせる
同じマイク・同じ部屋で、両回線に同じ質問をぶつけた。題材はブルワーズの剛腕ルーキー「ジェイコブ・ミジオロウスキー」——音声認識には地獄のような子音の連続である。
- WebRTC: 「ジェイコブ・ミジオロウスキーとどっちが強いの、大谷翔平は」→ ほぼ完璧に認識
- WebSocket: 「ヘイグーに上手きと大谷将暉は大勢成績を調べてくれる」→ 単語サラダ。しかもAIは佐藤健と神木隆之介の共演作品を調べ始めた(誰も頼んでいない)
精度はWebRTCの圧勝だった。理由は構造的で、Realtime APIのWebSocket入力は24kHz PCM固定なのに対し、WebRTCはOpus 48kHzのまま届く。倍の帯域と、Googleが10年磨いた音声パイプライン(AEC・ノイズ抑制一体型)の差である。
バグ1: WS側だけ劣化する「鮮度」問題
ただしWSの惨敗には、もう一つ原因が潜んでいた。WebRTCは接続のたびにマイクを取り直すのに、WSは録音チェーンをページを閉じるまで使い回す設計だった。回線をA/B切替すると、WS側だけ何セッションも前のデバイス選択・OS音声処理状態のまま話し続ける。「接続ごとに録音チェーンを作り直す」修正を入れた——「電話は常に1本」の哲学をマイクにも適用した形だ。
バグ2: デグローム融合事件
精度が上がった後、今度は面白いハルシネーションが出た。「ミジオロウスキーと大谷はどっちが投手として優れている?」と聞いたら、AIが「ジェイコブ・デグローム(本名ジェイコブ・ミジオロウスキー)」と答えたのである。実在する2人の投手を融合し、架空の本名関係まで発明した。
原因を分析すると、質問が「どっちが優れている?」という意見の形をしていたため、「事実を聞かれたら検索する」というルールをすり抜けていた。モデルは知らない名前を、一番似ている既知の投手に勝手に名寄せした——エンティティ融合と呼ばれる型で、厄介なのはモデルが「知らないことを知らない」点だ。
対策として共通ルールに「確信のない固有名詞は、意見や比較を求められた場合でも答える前に検索する」「裏付けのない本名・別名の断定を禁止」を追加した。同じ質問で再検証したところ、今度は両選手それぞれに検索が発火し、実際の成績に基づく比較が返ってきた。デグロームは二度と現れなかった。
結論
精度のWebRTC、監査のWebSocket。 個人利用ならWebRTCを既定にし(実際デフォルトを変えた)、全会話の記録・介入・監査が要る用途では中継を選ぶ。両方持っておけば、用途が決めてくれる。
コードは yoshiharu-ishii/realtime_voice で公開している。
リアルタイム音声AI 開発シリーズ
- Claude Codeと1日で作るリアルタイム音声AI ― $5から始めて、RAG・ペルソナ・Cognito認証・Terraformまで
- 「コードは読まない」開発スタイル ― Mermaid図解とモジュール分割で人間とAIの分業を作る
- AIが空耳する日 — 音声通話アプリの誤検知対策、文字起こし崩壊の犯人探し、そしてコンテナ化
- terraform applyで音声AIサービスが立つまで — User Pool全消し事故と、死なないAPIキーの謎
- User Poolはインフラではなくデータである — Terraformスタック分割の2軸
- WebSocket中継 vs WebRTC直結 ― 両方実装して固有名詞で殴り合わせたら、精度の勝敗とバグが2匹出てきた(この記事)
- 無線機から電話へ ― サーバーVADでハンズフリー通話を実装する