※本記事は個人の自己研鑽として取り組んだ内容であり、所属組織の公式見解や業務事例ではありません。
初めに
- RTX4060ローカル環境の前提で、双方向な音声会話をGemma 4 + llama.cppで、Docker Composeを利用し疑似的に実現する設計を検討しました
- 現状RTX4060 8GBのローカル環境では、後述のTML-Interaction-Smallのようなリアルタイム相互作用モデルの動作は現実的ではないと認識しています
- そこで、この記事ではGemma 4 E2B + llama.cpp + FastAPI + VOICEVOXを前提に、音声入力・音声出力・チャット出力・バックグラウンド推論をキューで分離することで、別アプローチで疑似的な双方向音声会話ができる機構として設計を検討しました
- 実装はまだ行っていないため、この記事では着想、アーキテクチャと設計判断、フローを整理しました
きっかけ
- Thinking Machines LabのYouTube公式動画を見て、いいなと思いました
- 日ごろから、音声会話をベースに裏で推論してくれたり、ツールコールしてくれたり、必要に応じて画面に情報を表示したり、チャットも併用してくれるようになるといいなと思っていました
なぜ「双方向」で「疑似」なのか
- 現状のローカルでのLLMでの音声会話は、ターン式の設計に寄りやすいと理解しています。ターン式には後述の課題があります
- 双方向にすることでターン式の課題に対処ができ、より実益のある会話体験にできる余地が残されていると認識したためです
ターン式の課題
以下3点と認識しています
- ユーザーが話している間、LLM側からさえぎる形での発話が実装しづらい
- 入力に対応する応答が1:1で返ってくる前提なので、「LLMが自発的に黙る(話を聞く)」ことの実現がしづらい
- 推論があったとしても、入力 -> 推論 -> 出力と同期的なため、LLM側で考えながら言葉を発するということが実現しづらい
双方向化で期待できること
前述課題に対し、それぞれ以下が期待できると理解しました。
- LLM側が、ユーザーの発話と独立して発話可能になることで、さえぎりや、相槌、意向の表明など表現の幅がより広がりそう
- ユーザーが発話し終わっても、LLM側があえて発話しないという選択をとれるようになり、間に意味を持たせられそう
- ユーザーの発話を受けながら、LLM側にて発話をしながらも、裏では思考をつづけながら会話してくれるため、議論やロールプレイなど、ある程度複雑な状況にもより効果的に使えそう
ローカル環境でのスペック制約による疑似
- Thinking Machines LabのTML-Interaction-Smallのように、ユーザーの音声入力を数百ミリ秒単位に切り出して順次LLMに処理してもらうことは、RTX4060では現状現実的ではないと認識しています
- TML-Interaction-Smallのモデルのパラメタ数は276B MoE (12B active) で、容量的にRTX4060 8GBに載りきらない現状です
- ただし、疑似的になら別のアプローチで実現できる余地があると考えました
実現したいUX
以下の3点と想定しました。
- AIと音声で会話しながら、必要なタイミングでチャット欄でも同時並行で別途情報をやりとりできるような会話体験
- ユーザーが話している途中でも、AI側が「聞く」「話す」「黙る」「割り込む」「非音声で情報を出す」を自律的に判断し切り替わる体験
- AI側が発言中に、ユーザーが話し始めた場合、AI側が画一的に黙るのではなく、「黙る」「続ける」「非音声に切り替え情報を出す」を自律的に判断し切り替わる体験
ユースケース (理想)
想定した使い道は、ロールプレイや雑談、ブレインストーミングです。ただ、ローカルでここまでできるかは未知数です。
- ロールプレイ(商談、面接、接客など)
- 一問一答のようなものに加え、会話の流れを保ちながら、質問や話題転換、その場でストーリ構築や展開を並行しながら進められる
- 音声では会話を続けながら、必要に応じてチャット欄には改善点、観点、メモなどをやり取りできる
- AI側の発話のタイミングや、受け答え、提示する質問や発言の質が上がるので、トレーニングの効果向上が期待できる
- 雑談
- 会話を続けながら、裏で推論をし続けてくれるので、複雑な話にもある程度対応が期待できる
- AI側が、考えながら必要なタイミングで音声発言・チャットを切り替えることで、会話の幅が広がる
- チャットで応じた場合、会話で応じたい場合と状況や気分によって、基盤とするLLMを共有しながらもAIとの会話方法が選べるようになる
- ブレインストーミング
- 会話を続けながら、裏で推論をし続けてくれているので、意図を組んで質問を投げかけてくれる
- 必要に応じて、チャットにこれまでの経過を随時まとめながら進めてくれる
- 思いつくものを話しつつ、一方チャットも同時に使えるので音声会話しながら整理することもできる
要件
- RTX4060 8GBのローカル環境で動かせる
- Docker Composeで環境の構築・再現が可能なようにする
- LLMとの対話は、ブラウザでのUIを介して行う (実装ハーネスDockerコンテナのエンドポイントへブラウザでアクセス)
- ユーザーは、音声入力とテキスト入力を同時に組み合わせて扱える
- システム側は、文字出力とTTS音声出力を非同期に独立して返せる
- システム側は、ユーザー発話中でも、発話・待機・チャット発言を切り替えて行える
- 疑似的な双方向を実現する
- 音声は、連続処理を想定しマルチモーダルには扱わず、テキスト化して処理する
技術スタック
- Docker Compose: 各コンテナの再現可能な起動
- UI (HTML + CSS + JS): 一般のブラウザでハーネス側のエンドポイントへアクセスし使用する。マイク音源取得、音声再生、タイムライン・UI表示を行う
- LLMハーネス側 (FastAPI (Python)): ブラウザへのUI配信、WebSocket、各種情報の保持・ハンドリングを行う
- VAD (Silero VAD): 発話区間の検出、切り出し情報を提供する
- ASR (whisper.cpp): 音声->テキスト化を行う
- TTS (VOICEVOX): 現行の自然な合成音声の音声ファイルを提供する
- LLM (Gemma 4 E2B + llama.cpp): 各種トリアージ、会話テキスト生成や推論結果を提供する
判断の理由
音声処理
- 今回はGemma 4の音声マルチモーダルではなく、VAD + ASRとしました
- Gemma 4は、音声のマルチモーダル処理も可能と認識しています
- https://ai.google.dev/gemma/docs/core?hl=ja
- 理由として、双方向にするため連続処理させる必要があり、負荷を抑えたいと考えたためです
出力制御
- 今回は、処理を一連の同期処理ではなく、音声出力 / チャット出力 / バックグラウンドでの推論 それぞれがキュー・Workerを持つことで、分離しました
- ターン式ではなく疑似的な双方向を実現するために、音声発話、チャット、推論を独立させる意図となります
TTS
- 今回はローカルで動作する、VOICEVOXを一例として想定しました
- 疑似とはいえ双方向的になると、システム側の音声出力量が増えそうです。従量課金の発生しないローカル合成音声エンジンを想定しました
- VOICEVOXは、ソフトウェア本体は商用・非商用問わず使える模様ですが、使用する音声ライブラリの規約に別途従う必要があると理解しました
- https://voicevox.hiroshiba.jp/term/
アーキテクチャ
全体構成
- コンテナは、LLMハーネス、TTS、LLMの3つを立てます
- ユーザーはブラウザで、LLMハーネス側の専用エンドポイントにあるUIを表示・経由して操作します
- マイク音声取得・テキスト入力・TTS音声再生はブラウザ側で担います
- LLMハーネスは、音声チャンクを受け取った場合はVAD・ASRを経てテキスト化し処理します。
- LLMハーネスは、後述のキューにある情報を順次TTSコンテナにリクエストし、音声データを得て、ブラウザへ流しつづけます
アプリ内構成: FastAPI (Python)
処理の流れ
イメージ
モジュール・.py構成
ユーザー入力 -> 音声テキスト化
- LLMハーネスは、ブラウザからマイク入力音声チャンク、またはユーザー入力テキストを順次受け取ります
- マイク音声入力チャンクだった場合は、VADを通し、発話単位を検出し、複数部位に切り出し細分化します
- 細分化した音声チャンクを、ASRに順次通し、テキストを得ます
- ユーザー入力テキストは特に加工しないです
- 得たテキストを、input_queueにエンキューします
input_queueのWorker
キューに入ったものを以下の通りに順次処理します
-
LlmClient経由で、トリアージを行います。トリアージでは以下を得ます
- 発話すべきか黙るか
- チャットすべきか
- 緊急か (さえぎるレベルか否か)
- ユーザー意図
- 主題 (何についての内容か)
- いま発話すべき場合に使うTTS用の文言
- 発話文言のtts用パラメタ
- チャットすべき場合に使う応答テキスト
-
トリアージ結果を各キューへエンキューします
- 発話すべきだった場合、tts_queueにエンキューします
- チャットすべきだった場合、text_output_queueにエンキューします
- 発話すべきか・チャットすべきか否かにかかわらず、reasoning_queueにエンキューします
- 緊急だった場合、各エンキュー時に優先度を変更し優先して処理されるようにします
tts_queueのWorker
- TtsClient経由で、順次、音声データ生成をリクエストし、受け取った音声データをブラウザ側へ送り続けます
text_output_queueのWorker
- ブラウザへ、テキストを順次送り続けます
reasoning_queueのWorker
- LlmClient経由で、順次、推論を行います。推論の結果を前述と同様にトリアージし、トリアージ結果に基づき前述の各キューへエンキューします
生成テキスト・TTS音声データ -> ブラウザ
- ブラウザは、順次受け取ったテキストデータを表示し、TTS音声データを再生し続けます
要検討
E2Bモデルを使用する想定ですが、遅延や実行品質がどの水準かが未知数です。特に以下の観点となります
- トリアージ部分と推論を同じコンテナに対してリクエストをするので、許容できる遅延に収まるか
- ユーザーの音声入力からTTS音声再生までのタイムラグが、音声会話として不自然にならないか
- 古い会話の文脈に基づく推論結果出力への対処
- キューが詰まってきたとき、古いものを破棄するか、間引くか、優先度で調整するか
むすび
- まだ机上での検討なため、実装を進めてみたいと考えています
- ご意見や、誤りなどありましたら、コメント欄にて教えていただけますと幸いです
- ここまでお読みくださりありがとうございました