Speech-to-Speechとカスケード型の前提整理
今回のテーマは、音声で話しかけたら音声で返ってくる、いわゆる Speech-to-Speech音声を入力し、テキストへの変換を経由せず、音声のまま応答を生成する対話方式。声のトーンや感情などの情報が保たれやすい。 の対話AIです。きっかけは Liquid AI から公開された LFM2.5-Audio-1.5B-JPLiquid AI が公開した小型の音声言語モデル。1.5B(15億パラメータ)という小ささで日本語に対応し、手元の Mac でも動かせる点が特徴。 でした。
これまで音声対話システムは大きく三段構えが基本だった、と戸田さんは整理します。まず音声を文字にする ASR(音声認識)、次に文字を LLM に渡して返事を考える部分、最後に返事を音声に変える TTS(音声合成)です。この三つをつなぐ形式はカスケード型ASR→LLM→TTS のように処理を段階的につなぐ方式。パイプライン型とも呼ばれる。制御しやすい一方、各段で遅延が乗る。と呼ばれます。
ASR(音声認識)
音声を文字に変換する
LLM
文字を受け取り返事を考える(近年はここをLLMに集約)
TTS(音声合成)
生成された返事を音声に変える
この方式はよくできていますが弱点もあります。各段でちょっとずつ遅延が乗りますし、音声を一度テキストに潰すため、声のトーンや言い淀み、感情といった情報が落ちてしまうのです。
そこで出てきたのが End-to-End の音声モデル。音声を入れて音声をそのまま出す、間にテキスト変換を挟まない方式です。GPT のリアルタイム系や、ローカルで動く Qwen Omni、Moshi などが例に挙げられました。ただし、これらはモデルが大きく GPU 前提だったところに、1.5B でローカル動作するモデルが登場したことが今回の出発点です。
検証のお題とデモアプリの構成
検証のお題は、タスク指向対話の代表例であるレストラン予約に設定されました。ボットがヒアリングする情報は「日にち」「時間」「人数」の三つだけ。名前や電話番号を聞く例もありますが、あえて絞ってシンプル化しています。
戸田さんの当初の目論見は「これなら簡単にできて、そこから徐々に難しくしていける」というものでした。ところが、このシンプルな三項目のヒアリングだけでも全然うまくいかなかった、というのが今回の話の中心です。
また、今回こだわったのは「できるだけ Speech-to-Speech のまま成立させる」ことでした。作ったのはブラウザで試せるデモアプリです。ブラウザでマイクの音を拾い、WebSocket でローカルのサーバーに送り、サーバー側で処理してモデルに返し、音声を再生する構成になっています。
最初の壁──ローカル実行の重さ
最初に立ちはだかった壁は、ローカル実行が思ったより重かったことです。LFM2.5-Audio はテキストのトークンと音声のトークンが交互に生成される仕組みで、音声トークンを順々にデコードすればストリーミングで喋れるようになっています。
しかし戸田さんの環境(MacBook Pro / M3 Max)では、RTFReal Time Factor。ざっくり「実時間の何倍かかるか」を示す指標。1を切ればリアルタイム、1を超えると再生が生成に追いつかない。 が概ね1.6〜1.8でした。1を切らないとリアルタイムにならないため、6秒ぐらいの音声を作るのに11〜12秒ほどかかってしまう計算です。
つまり喋りが再生に追いついていきません。最初は細かいチャンクで小刻みに再生していましたが、生成が追いつかず音がブツブツ切れてしまいました。バッファリングも試したものの、根本的に RTF が1を切らないと自然な発話にはなりません。結局、小刻み再生をやめて、一回の返事がまるごと生成されてから返す仕組みに妥協しました。ターンテーキングのテンポは悪くなるものの、ブツブツ切れるよりはマシという判断です。
話速というのはモデルの賢さではどうしようもない。ハードウェアと量子化の話になる。
ここでの教訓は、話速はモデルの賢さではどうにもならず、ハードウェアと量子化の問題だということ。ローカルで Speech-to-Speech の対話システムを作るなら、スタートラインの時点で考えなければならない問題がある、とわかった段階です。
タスク状態の制御に苦戦した試行錯誤
速度はハードの問題として、次は発話の賢さ、つまりタスクの状態管理の検証に移ります。まずはシステムプロンプトに素直に入れる方法です。「店側の予約受付として話す」「聞くのは日にち・時間・人数の三つ」「まだ聞いてない項目を一つずつ聞く」「三つ揃ったら復唱して完了」といった内容です。
テキストで読むと簡単で、フロンティアモデルなら余裕の内容に思えます。しかし、うまくいきませんでした。
おそらくLLMが「予約タスクなら名前や電話番号を聞くはず」と勝手に予測して、指示していない項目を聞き始めたのだろう、と戸田さんは見ています。シンプルにシステムプロンプトに入れるだけではできない、という結果でした。ここから試行錯誤が続きます。
試行1:各ターンで内部状態の要約を渡す
ヒアリングが一つ進むごとに、次のターンで「現在の予約状態は、日にちが12月3日、時間は未確認、人数は未確認。未確認の項目を聞いてください」といったメモを渡す方法です。2023年頃のテキストLLMでは王道の手法でした。ただ、今回はこのメモ自体を読み上げてしまったため、この案はボツになりました。
試行2:フューショット(成功例を見せる)
成功した会話例をプロンプトに入れるフューショットfew-shot。少数の具体例をプロンプト内で提示し、モデルにパターンを学習させる手法。ルールを言葉で伝えるより例を見せる方が効く場合がある。は結構効きました。小規模な1.5B程度だと、ルールを渡すより例を見せる方が効くという感覚を得たそうです。ただ副作用があり、例に「金曜日の18時2名」と書くと、ユーザーが何も言っていないのにその値で進めようとしたり、「お客様:」「店員:」のような台本形式にすると続きを書き始め、一人二役で勝手に話し出すことがありました。
試行3:対話履歴を偽装する
画面上は一人目のお客さんに見えても、モデルの中では既に何人もさばいた後の新規客に見せる方法です。チャット履歴に隠しフューショットを仕込むイメージで、これも効きました。ただし、急に韓国語や中国語のような日本語でない文言が生成されることがありました。
本気でやるなら合成音声をエンコードして履歴に入れると安定するかもしれませんが、今回はそこまで試せませんでした。
また、スロット抽出でも工夫がありました。LFM2.5-Audio は入力音声の書き起こしがないため、ボットが話した内容からスロットを取る必要があります。そこで指示にヒアリング内容の復唱を入れ、正規表現で日時や人数を取得しました。
カスケード型に戻したら一気に安定
LFM2.5-Audio は ASR だけ単体で使うといったモジュール分割もできます。そこで、カスケード型のシステムも作ってみました。ASR でテキスト化し、外部のルールでスロットを更新し、次に聞くことを決め、その文をモデルに読ませる流れです。
今までの苦労は何だったんだってくらい、安定して対話ができました。
ここで戸田さんは全体を俯瞰します。現実の世界では今でもカスケード型が非常に強いと言われています。End-to-End は自然さやレイテンシーに理想がありますが、カスケード型は制御しやすく、デバッグも楽で、状態を外に持てるため連携もしやすいという現実的な強さがあるのです。
予約・注文など、タスクの正しさがシビアで絶対に間違えられない案件。制御・デバッグ・状態連携がしやすい。
雑談や自然な掛け合いそのものが価値になる用途。自然さとレイテンシーで優位。
Gemini Liveとの比較で見えた足回りの差
せっかくなので、フロンティア側との比較として Gemini LiveGoogle の Gemini によるリアルタイム音声対話機能。function calling などの足回りが揃っており、音声で話しながら構造化された状態を裏で更新できる。 でも同じレストラン予約デモを作ってみたところ、あっさりできて「悔しいくらい」だったそうです。
特に大きかったのが function callingモデルが応答の中で外部の関数を呼び出せる仕組み。予約状態を更新する関数などをモデル自身に呼ばせ、対話と構造化データの更新を連動できる。 の部分でした。LFM ではモデルの復唱を正規表現で拾う遠回りをしていましたが、Gemini Live ではモデル自身に予約状態を更新する関数を呼ばせられます。音声で自然に喋りながら、裏で構造化された状態が更新される仕組みが実現できました。
ここから見えてきたのは、ローカルモデルに足りないのは音声の賢さそのものではなく、対話の制御性や tool calling(function calling)といった「足回り」だということ。その周辺が揃っていないのが、ローカル開発時に効いてくるのではないかと戸田さんは指摘します。
Speech-to-Speech対話の3つの層
一連の検証を通じて、戸田さんは Speech-to-Speech のタスク対話には大きく3つの層があると整理しました。
LFM2.5 の最大の魅力は、1つ目の「音声として自然に対話できること」をローカルで試せる、最初のラインを突破できる点にあります。一方、ローカル環境では2つ目のリアルタイム性が厳しく、1.5B という賢さでは3つ目のタスク進行も不安定になりやすい、というのが現状です。
これに対し Gemini Live は2つ目と3つ目を一気にクリアできている感触があるといいます。ローカルに限らず API や GPU リソースを使った対話では、こうした End-to-End のものがだんだん増えてくるかもしれない、と締めくくられました。
まとめ
今回の正直な結論は、「GPU なしのローカル完結で、実用的な Speech-to-Speech のタスク対話を作るのはまだ難しい」というものでした。あまり面白みのない結果、と戸田さん自身も言いますが、音声対話を自然にやっていくラインの境界線が見えたという意味では収穫があったと振り返ります。
何より、音声対話のモデルが手元に来たこと自体が大きな一歩だと評価しています。今回は CUDA の GPU を使っていないため、Qwen 系や Moshi などのより強いローカルモデルで試せば、また違う景色が見えるかもしれない、と次の可能性にも触れられました。詳しい数字・設定・コードは Zenn の記事にまとめられています。
- LFM2.5-Audio-1.5B-JPは、1.5Bという小ささでローカル(Mac)で動くSpeech-to-Speechモデル。「音声として自然に会話できる」ラインをローカルで突破できるのが最大の魅力。
- ローカル実行ではRTFが1.6〜1.8で、6秒の音声生成に11〜12秒かかりリアルタイム性が確保できず、返事をまるごと生成してから返す妥協が必要だった。
- レストラン予約(日にち・時間・人数の3項目)というシンプルなタスクでも、プロンプト直入れでは状態管理が破綻。フューショットや履歴偽装で改善したが副作用も出た。
- ASR単体を使ったカスケード型に戻すと一気に安定。タスクの正しさがシビアな用途では、まだカスケード型が現実的。
- Gemini Liveはfunction callingで対話と状態更新を自然に連動でき、ローカルモデルに足りないのは音声の賢さより「足回り」だと見えてきた。
- 結論として、GPUなしのローカル完結で実用的なタスク対話を作るのはまだ難しいが、音声対話モデルが手元に来た意義は大きい。
