LabHub
はじめる
学ぶ 学習パス コース

音声 AI エージェント — 聞いて、調べて、話すパイプライン

話し終えてから最初の音まで — 直列と重ね合わせを測り予算と照らす

LabHubで続きを見る

目標

聞くことから話すことまでを一本につないで、ユーザーが感じる遅延を測ります。直列と重ね合わせを同じクエリで比較し、分布とバジェットから、減らすべき場所を探します。

なぜ重要なのか

部品ごとに速くても、つなげると秒単位になります。どこを減らすかは、「どうつないだか」と「どの段階がバジェットを超えているか」を分布で見てはじめてわかります。採点ツールは、マシンごとに違う絶対時間の代わりに、記録の中の順序(確定 ≤ 検索 ≤ 最初のトークン ≤ LLM終了)、同じマシンで測った2つの方式の比較(重ね合わせ < 直列)、元データから再計算したパーセンタイル・バジェット超過を見ます。エンドポイント遅延(オーディオ時間)は、採点ツールがVADをもう一度動かして照合します。

ステップ

  1. 12個のクエリ(/opt/lab/fixtures/voice/queries/q01~q12.wav)の、エンドポイント(VADが見た最後の話の終わり + gap 0.5秒)と実際の話の終わり(refs.tsvの4列目)の差を、/root/voice/pipeline/endpoint.jsonに書いてください。
  2. 確定 → 検索 → LLM(全部) → TTS(全部)をつなぐ/root/voice/pipeline/pipe.pyを作り、q01・q03・q05・q07・q09を直列で動かした記録を、/root/voice/pipeline/serial.jsonlに書いてください。
  3. 同じ5つのクエリを、LLMの断片ができるたびにTTSに渡す重ね合わせで動かして、/root/voice/pipeline/overlap.jsonlに書いてください。
  4. 重ねた実行1つを、Chromeのトレース形式で/root/voice/pipeline/trace.jsonに描いてください。
  5. 答えのあるクエリ10個(q01–q10)を重ね合わせで動かして、ユーザー体感遅延(エンドポイント遅延 + 判定後の最初の音)とp50・p95を、/root/voice/pipeline/e2e.jsonに書いてください。
  6. 段階別のバジェットと測定中央値、バジェットを超えた段階を、/root/voice/pipeline/budget.jsonに書いてください。
  7. Pythonプロセスの最大RSSとLLMサーバーのRSS、Podのメモリ上限を、/root/voice/pipeline/memory.jsonに書いてください。
  8. 測定結果をまとめた/root/voice/pipeline/report.jsonを作ってください。

参考

エンドポイント遅延: オーディオ時間で

/opt/lab/fixtures/voice/queries/refs.tsv(名前・原文・発話の開始・発話の終わり)の12個のクエリごとに、silero VAD(threshold 0.5・min_silence 0.1・min_speech 0.1)の最後の区間の終わりをvad_end_sに、それにgap 0.5秒を足した値をendpoint_sに、(endpoint_s − 발화 끝) × 1000をdelay_msに、/root/voice/pipeline/endpoint.json(gap_s・rows)に書いてください(プレースホルダーは発話の終わりです)。

すべての値が音声ファイルの中の時刻です。機械が速くなっても減らない分なので、別に測ります。実際の話の終わりは、合成音声を置いた位置なので正確です。

直列につなぐ

/root/voice/pipeline/pipe.pyにrun(wav, mode)を作ってください。音をストリーミングASRに100 msずつ入れたあと(測りません)、エンドポイントの瞬間を0として、確定(asr_ms) → フィラーを除いたクエリで段落top-3検索(retrieve_ms) → 見つけた段落を付けたLLMストリーム(最初の断片llm_first_ms、終了llm_done_ms、max_tokens 60) → TTSとつなぎ、最初の音(first_audio_ms)と合成の終わり(tts_done_ms)を書いて返します。modeが「serial」なら、答えをすべて受け取ったあとに、全体を一度に合成します。q01・q03・q05・q07・q09を直列で動かし、id・text(確定結果)を加えて、/root/voice/pipeline/serial.jsonlに書いてください。

モデル(ASR・TTS・エンベディング)とKBのエンベディングは、モジュールを読み込むときに1回だけ作ります。TTSをスレッドで動かしておくと、次のステップの重ね合わせでそのまま使えます。直列では、最初の音がそのまま合成の終わりです。

重ねて動かす

run(wav, 'overlap')が、LLMの文がたまるたびに断片(文の終わり・5単語を超えるカンマの前・12単語)を切って、TTSスレッドに渡すようにしてください。同じ5つのクエリを重ね合わせで動かして、/root/voice/pipeline/overlap.jsonlに書いてください。採点: 最初の音の中央値が、直列より速くなければなりません。

直列と重ね合わせの違いが「いつTTSに渡すか」の1点だけであれば、公平に比較できます。同じrun関数で、modeによって分けてください。

バーで描く

overlap.jsonlの最初の記録で、asr_final・retrieve・llm(すべてtid 1)とtts(tid 2、最初のトークンから合成の終わりまで)のバーを"ph": "X"のイベントで、最初の音を"ph": "i"のイベントで作った{"traceEvents": [...]}を、/root/voice/pipeline/trace.jsonに保存してください。ts・durはマイクロ秒です。

ms × 1000 = µsです。chrome://tracingやui.perfetto.devにファイルをドラッグ&ドロップすると、LLMとTTSが重なった様子が見えます。

ユーザーが感じる遅延の分布

q01–q10を重ね合わせで動かし、クエリごとにendpoint_ms(endpoint.jsonのdelay_ms)・first_audio_ms・e2e_ms(2つの合計)をrowsに書き、e2e_msのp50_ms・p95_ms(最近傍順位)を/root/voice/pipeline/e2e.jsonに書いてください。

オーディオ時間(エンドポイント遅延)とウォールクロック時間(判定後の最初の音)を、ここではじめて足します。サンプル10個のp95は、最も大きい値です。

バジェットと突き合わせる

budget_msに、5つの段階(endpoint・asr_final・retrieve・llm_first_token・first_audio_after_token)のバジェット(正の数、合計2500以下)を書き、measured_p50_msに中央値(endpointはendpoint.jsonのdelay_ms、残りはoverlap.jsonlのasr_ms・(retrieve_ms − asr_ms)・(llm_first_ms − retrieve_ms)・(first_audio_ms − llm_first_ms))を、over_budgetに、測定がバジェットを超えた段階の名前を書いた/root/voice/pipeline/budget.jsonを作ってください。

累積時刻の差が、そのまま、その段階の分です。超過した段階ごとに、何で減らせるか、つまりモデル、プロンプト、つなぎ目のどれかを考えてみてください。

メモリもバジェット

pipeでクエリを1つ動かしたあと、このPythonプロセスの/proc/self/statusのVmHWM(最大RSS)をpython_peak_mbに、pgrep -f llama-serverで見つけたサーバーのVmRSSをllm_rss_mbに、合計をtotal_mbに、/sys/fs/cgroup/memory.maxの上限をlimit_mb(数字でなければnull)として、/root/voice/pipeline/memory.jsonに書いてください。

VmHWMとVmRSSはkBで出ます。1024で割ってMBにします。上限はバイトです。2つのプロセスが上限の何%を使っているかが、モデルをもう1つ載せられるかの答えです。

レポート

/root/voice/pipeline/report.jsonに、serial_first_audio_p50_ms・overlap_first_audio_p50_ms(2つのjsonlのfirst_audio_msの中央値)、e2e_p50_ms・e2e_p95_ms、over_budget、total_mbを書いてください。

前のステップのファイルから計算するか、書き写します。