👋 はじめに
皆さん 「AIと声で会話したい」 って思ったこと、ありませんか?
テキストのチャットは便利なんですが、やっぱり声で返ってくると没入感が全然違いますよね。
人間同士の会話って、相手の言葉が終わってからだいたい200〜500msくらいで返事が始まりますよね。さすがにそこまでは無理でも、1秒切れれば「会話してる感」は十分出るはず。
で、色々と工夫した結果……694ms。達成しました!🎉
この記事では、1秒未満を実現するためにやった工夫とを共有します。
🏗️ 構成
今回のシステム構成はこんな感じです
| コンポーネント | 技術 |
|---|---|
| LLM |
Gemma4(Ollama経由、ローカル実行、モデル: E2B) |
| TTS | AIVIS Speech(Cloud API ストリーミングモード) |
| フロントエンド | Next.js(ブラウザから直接Ollama・AIVIS APIを呼び出す構成) |
🤖 Gemma4について
Gemma4はGoogleが2025年に公開したオープンウェイトのLLMです。今回使っているのは E2P(Edge-to-Platform)モデル でエッジデバイスからクラウドまで幅広い環境で動作するように設計されています。
ざっくり言うとローカルのMac miniでも十分動くサイズ感なのにチャット品質がかなり高いモデルです。初回トークンの応答が200ms台と速く今回の「1秒未満」という目標に大きく貢献してくれました。
💡 構成のポイント
フロントエンドからOllamaとAIVIS APIに直接リクエストを投げているのがミソです。Lambda とか API Gateway とか挟まないので、余計なネットワークホップがありません。ローカルLLMを使う以上、ここに中継サーバーを置いたら本末転倒ですからね。
🧱 1秒未満を阻む壁 〜ボトルネック分析〜
目標を「1秒未満」に設定したものの、素朴に実装すると各処理が直列に走って、こんな時間配分でした
| 区間 | 時間 | 割合 |
|---|---|---|
| Ollama接続 + 初回トークン | ~250ms | 27% |
| 句点までのトークン蓄積 | ~50ms | 5% |
| TTS音声生成 | ~600ms(ローカル) | 68% |
| 合計 | ~900ms |
900ms……ギリギリ1秒切ってるように見えますがこれは最短ケース。テキストが長くなるとTTSが2秒以上かかって余裕でオーバーします。
安定して1秒未満を実現するにはこのTTSの600msをなんとかしないといけない。
え?LLMじゃなくて音声合成のほうがボトルネックなの!?って感じですよね。
犯人はAIVIS SpeechのローカルDocker(CPU版) でした。具体的にはデコーダーの推論処理がsynthesis全体の90%を占めていてCPUモードだとどうしても600ms以上かかっちゃう。
⚡ ローカル vs クラウド 〜AIVIS Speech速度比較〜
「ローカルが遅いならクラウド使えばいいじゃん」ということで、AIVIS SpeechのクラウドAPIと比較してみました。
| テキスト長 | ローカル(CPU Docker) | クラウドAPI通常 | クラウドAPIストリーミング(TTFB) |
|---|---|---|---|
| 4文字 | 600-856ms | 305ms | 250ms |
| 31文字 | 2,475ms | 335ms | 260ms |
| 49文字 | 2,675ms | 385ms | 280ms |
これ、めちゃめちゃ面白くないですか!?
ローカルだとテキストが長くなるほど遅くなるのに、クラウドAPIのストリーミングモードはテキスト長に関係なくTTFB 250〜300msで安定してるんです。
ローカルのCPU Dockerで色々試したんですが、効果なし。デコーダー推論がボトルネックなのでCPUモードである限り根本的に厳しいという結論になりました。
GPU搭載マシンならローカルでも高速化できると思いますが、今回はMac mini M4(Apple Silicon)環境なので、Docker内でGPUアクセラレーションを使うのはハードルが高いです。
🔧 1秒未満を実現した5つの工夫
🚀 工夫1. Gemma4のシンキングモードをOFFにする
実はGemma4、デフォルトでシンキングモード(thinking mode)がONになっています。シンキングモードは回答精度を上げるためにLLM内部でChain-of-Thoughtの推論を行う機能なんですが音声チャットではこれが致命的。
音声チャットでは「深く考えた正確な回答」より**「素早く自然に返ってくること」**のほうが圧倒的に大事。なので、Ollamaのモデルファイル(Modelfile)でシンキングモードを明示的にOFFにしました。
🔌 工夫2. Ollama直結ストリーミング
フロントエンド(Next.js)からOllamaの /api/chat エンドポイントに直接ストリーミングリクエストを投げます。
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
body: JSON.stringify({
model: 'gemma4-chat',
messages: [{ role: 'user', content: userMessage }],
stream: true
})
});
Lambda や API Gateway を経由しないので、ネットワークホップを最小限に。Gemma4(E2P)のローカル実行という強みを最大限活かす構成です。ここで余計な100msとか足されたら1秒未満が遠のきますからね。
✂️ 工夫3. 句読点ベースのチャンク分割
LLMからトークンが流れてくるのを監視しながら、句読点(。!?、)で文を区切って、最初の句読点が来た瞬間にTTS生成を開始します。
LLMトークンストリーム:
"私" → "は" → "Google" → ... → "た" → "、" ← ここでTTS開始!
- 初回チャンク:句読点で即座にTTSへ送信(応答速度を最優先)
- 2チャンク目以降:50〜200文字でまとめて区切り(チャンク数を抑制して効率化)
この「初回だけ特別扱い」がかなり効きました。最初の一言が速く返ってくるだけで、体感の応答速度がまるで変わります。1秒未満を実現するには、この「初回チャンクをどれだけ早くTTSに渡せるか」が勝負です。
🔥 工夫4. TTS並行生成(Fire-and-Forget)
チャンクが確定したら、awaitしないでTTS生成をPromiseで非同期に開始します。
// チャンク確定時にTTSを非同期開始(LLMストリームをブロックしない)
const ttsPromise = generateTTS(chunk);
ttsPromise.then(audio => playbackQueue.push(audio));
// LLMのストリーム処理は止めずに次のトークンを受信し続ける
- LLMのストリーム受信と、TTSの音声生成が完全に並行動作
- 音声が生成できたらコールバックで再生キューに追加
- 2チャンク目以降は、ユーザーが1チャンク目の音声を聴いている間に裏で生成完了している
初回の1秒未満に直接効くのは工夫1〜3ですが、この並行処理は2チャンク目以降の途切れを防ぐために必須。音声が途中で止まったら「会話してる感」が台無しですからね。
☁️ 工夫5. クラウドAIVIS APIストリーミング採用
最大のボトルネックだったローカルTTSをクラウドAPIに切り替えました。
-
ストリーミングモード(
stream: true)でTTFB 250msに短縮 - 出力形式はMP3(ローカルのWAVより軽量で、ネットワーク転送も速い)
const res = await fetch('https://api.aivis-project.com/v1/tts/synthesize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model_uuid: modelUuid,
text: text,
output_format: 'mp3',
speaking_rate: speedScale ?? 1.0,
leading_silence_seconds: 0.0,
trailing_silence_seconds: 0.1
}),
})
4/10追記.コードに誤りがございましたので修正しております(詳細は公式docをご参照ください)
ローカルCPU Dockerの600msが、クラウドAPIストリーミングで約290msに。これが1秒未満の達成に一番効きました。
📊 実測ログで検証 〜本当に1秒切ってる?〜
「本当に1秒未満なの?」って思いますよね。実際のブラウザコンソールログをお見せします。これがリアルな計測結果です。
⏱️ 実測タイムライン
[0ms] API呼び出し開始
[209ms] Ollama HTTP応答受信(fetch時間: 209ms)
[209ms] 最初のトークン「私は」受信
[402ms] 初回チャンク確定「私はGoogle DeepMindによって開発された、」(27文字)
→ TTS生成開始 #0
[693ms] TTS生成完了 #0 (所要: 291ms, 音声: 59.4KB)
[694ms] 🔊 最初の音声再生開始! ← 1秒未満達成!
[765ms] 2チャンク目 TTS生成開始 #1「オープンウェイトのLLMであるGemma 4です。」(25文字)
[1056ms] TTS生成完了 #1 (所要: 291ms, 音声: 53.3KB)
[3358ms] チャンク #0 再生完了 (再生時間: 2,664ms)
[3358ms] チャンク #1 再生開始(シームレスに繋がる!)
[5685ms] 🎉 全音声再生完了 (合計: 5,685ms)
694ms で音声再生開始。 目標の1秒未満、余裕を持ってクリアです!
📐 694msの内訳
実際のログから初回音声再生までの 694ms を分解するとこうなります:
| 区間 | 実測値 | 割合 | 備考 |
|---|---|---|---|
| Ollama HTTP応答 + 初回トークン | 209ms | 30% | Gemma4 E2Pの軽量さが貢献 |
| 句読点までのトークン蓄積 | 193ms | 28% | 読点「、」で即座に分割 |
| クラウドAIVIS TTS生成 | 291ms | 42% | ストリーミングで安定 |
| 合計 | 694ms | 🎉 1秒未満達成! |
3つの区間がそれぞれ200〜300ms。どれか一つに偏らず、バランスよく時間を使えているのが良い状態だと思います。
🔄 全体のデータフロー
改善後の処理の流れをまとめるとこうなります:
ユーザー発話
│
▼
[フロントエンド] Ollama /api/chat へストリーミングリクエスト
│
▼ 209ms後:最初のトークン受信(Gemma4 E2P)
[チャンク分割] 読点「、」で即座に分割(27文字で確定)
│
├─▶ チャンク#0「私はGoogle DeepMindによって開発された、」
│ → [AIVIS Cloud API] TTS生成(非同期, 291ms)
│ → 🔊 694ms で音声再生開始!
│
├─▶ チャンク#1「オープンウェイトのLLMであるGemma 4です。」
│ → [AIVIS Cloud API] TTS生成(並行, 291ms)
│ → チャンク#0の再生中に生成完了済み → シームレスに再生開始
│
└─▶ 🎉 全音声再生完了(5,685ms)
初回音声再生までの時間:
Ollama応答(209ms) + トークン蓄積(193ms) + TTS生成(291ms) = 694ms < 1秒 ✅
- Ollamaの初回接続のウォームアップ最適化。今回の209msも、事前にダミーリクエストを投げておけばさらに短縮できるかも
締め
694ms。感覚的には居酒屋で「生!」って言った瞬間にジョッキが出てくるレベルです!!感動ですよね。
結局、音声チャットで一番大事なのは正確さじゃなくてテンポ。人間の会話だって、5秒黙られたら「この人大丈夫かな」ってなるじゃないですか。逆にレスポンスが速いだけで「こいつ...わかってるな」感が出る(あくまで感)。そう!速さは正義なのです!
(回答内容はsonnet4.6/opus4.6とかと比べると「う〜ん。。。」というところは否めないですがw)