リアルタイム音声AIはこれまでPush-to-Talk(PTT)方式だった。ボタンを押している間だけ録音し、離すと応答が返る——業務無線のスタイルである。今回、サーバーVAD(音声区間検知)によるハンズフリー通話モードを実装し、UIで切り替えられるようにした。無線機と電話、両方持つことになった。
PTTとVAD、何が変わるのか
| 押して話す(PTT) | ハンズフリー通話(VAD) | |
|---|---|---|
| 発話の区切り | ボタン(手動commit) | サーバーVADが自動検知 |
| 応答の開始 | クライアントが要求 | OpenAIが自動生成 |
| 割り込み | ボタン押下でキャンセル | かぶせて話すと自動バージイン |
| コスト | 話した分だけ | 無音の間も課金 |
実装の中核は、セッション設定の turn_detection を null(手動)から {"type": "server_vad"} に切り替えるだけ——発話の始終検知・自動commit・自動応答・バージインを全部OpenAI側が引き受けてくれる。検証では、commitもresponse.createも一切送らず「音声+2秒の無音」をストリームしただけで、speech_started→speech_stopped→自動文字起こし→自動応答が成立した。クライアント制御ゼロで会話が回る。
ボタンの第二の人生
ハンズフリーでは「押して話す」ボタンが失業する。そこでミュートトグルに転職させた。クリック(またはスペースキー)でマイクのON/OFF。実はこれ、切実な機能である。以前、裏で流していたYouTubeの音声がマイクに回り込み、文字起こしが単語サラダになる事件があった。常時マイクが開くVADモードでは、この手の混入が構造的に起きやすい。即座に黙らせるボタンは保険として必須だった。
細かい作り込みも要った。Web検索の実行中は自動ミュート(検索結果と新規発話の混線防止)。WebSocket回線では speech_started を受けたらローカルの再生キューを止める(バージインの実装)。WebRTC回線ならこれらの大半をプロトコルが勝手にやってくれる。
無線と電話の経済学
このプロジェクトが$5の無料クレジットで長く遊べた最大の理由は、PTT設計だった。押している間しか音声を送らない=無音に課金されない。タクシー無線が少ないチャンネルを大勢で共有できるのと同じ、トランキングの経済性である。
VADモードはその節約装置を自分から手放す。無音の間も音声ストリームが流れ続け、通話1時間で数ドル規模になる。だからUIのヒントに「無音の間も音声を送信・課金されます」と明示した。自然さはタダではない。とはいえ、ボタンから指を離して布団ちゃん(AIペルソナ)と垂れ流しで雑談できる体験は、明確に別物である。会話のテンポにAIの応答が溶け込んでくると、「アプリを操作している」感覚が消える。
これで構成は「回線2種(WebSocket中継/WebRTC直結)×モード2種(PTT/ハンズフリー)」の4通りになった。無線機として使うか、電話として使うか——用途が選べばいい。
コードは 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でハンズフリー通話を実装する(この記事)