0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Voice Live APIで会話の「間」を扱う - 先に話す・言いよどみを待つ・割り込む・待ち時間に話す

0
Last updated at Posted at 2026-11-10

Voice Live APIでエージェントとの会話の「間」を扱う機能を試した話

Voice Live などのエージェントと音声会話するための仕組みを構築するときに一番困ることの1つとして応答性ではないでしょうか。ネットワークの帯域や、エージェントが長い処理を行ったりするときに 今実行中であるのかどうかはかなり重要な関心ごとではないでしょうか。エージェントが処理を行っているタイミングがわからないと以下のような問題が発生することが多いと思います。

  • 接続したのに、こちらが話すまで何も起きない
  • 「えっと……」と考えている途中で、エージェントが話し始めてしまう
  • 長い説明を途中で遮ったのに、遮る前の続きを話し続ける
  • ツールを呼んでいる間、何秒も無言になる

Microsoft FoundryのVoice Live APIでは、こうした「間」を扱う機能もひととおり揃っています。

本記事では、4つの場面ごとに使える機能を、挙動を確認した話について紹介します。

4つの場面と対応する機能

FoundryのVoice Liveでは以下のような場面で利用できる機能が用意されています。

場面 機能 どこで設定するか 状態(07-15) SDK 1.2.0
会話の開始 Proactive messages クライアントの動作(セッション設定ではない) GA 〇
言いよどみ Semantic EOU turn_detectionの中 GA 〇
言いよどみ Smart turn detection turn_detectionの中 preview ×
割り込み Auto-truncation turn_detectionの中 GA 〇
ツールの待ち時間 Interim response interim_response 07-15のリファレンスに定義あり 〇(渡し方に注意)

準備

本記事はVoice Live機能解説シリーズの第1回です。前提となるSDK 1.2.0の話は起点の記事にまとめています。

シリーズの記事一覧(SDK 1.2.0 / API 2026-07-15)

  1. Voice Live SDK 1.2.0の変更点とAPI 2026-07-15 GA
    SDKに型が無い機能の分類
  2. Voice Live APIで会話の「間」を扱う(本記事)
    先に話す・言いよどみを待つ・割り込む・待ち時間に話す
  3. Voice Live の音声入力の品質を上げる
    ノイズ抑制・エコー除去・Live-Reference AEC の選び方
  4. Voice LiveでMCPサーバのツールを使う
    function callingとの違いと並列ツール呼び出し
  5. Voice LiveでFoundryのエージェントをツールとして呼ぶ
    chat-supervisorパターンの使い方と前段モデルの選び方
  6. 30秒の音声で「自分の声」を作って Microsoft Foundry エージェントを喋らせる
    Personal Voice の申請から実装まで
  7. Voice Live APIのアバターを使い分ける
    標準・カスタム写真・カスタムビデオの設定と実装
  8. Voice Liveのアバター映像をWebSocketで受ける
    WebRTCとの違いと映像の形式

番外編:Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ
SDPを1行ずつ突き合わせた記録

リソースの作成から、C#で最初の音声会話を動かすところまでは、公式のクイックスタートに沿って進めると準備できます。対応リージョンは、Voice Liveの概要ページで確認できます。

クイックスタートの環境で本記事の内容を試すときは、次の2点に注意してください。

  • smart turn detectionは、APIバージョンに2026-06-01-previewを指定してAPIを直接呼ぶ必要があります(SDK 1.2.0では選べません)
  • interim responseは、テキストモデルとAzureの音声を組み合わせた構成でしか使えません。gpt-realtimeのような音声をそのまま扱うモデルでは働きません

検証に使ったコードはこちらです。

本記事は2026年10月時点の情報です。対象はVoice Live API 2026-07-15(GA)と.NET SDK Azure.AI.VoiceLive 1.2.0です。設定は言語を問わず読めるよう、session.updateなどのJSONで示します。SDK固有の注意点だけC#で補足します。

会話の開始(Proactive messages)

接続した直後にエージェントのほうから話しかける機能です。名前からsession.updateの設定項目を想像しますが、セッションに設定するものではなく、クライアントがsession.updatedを受け取ったタイミングで、自分でメッセージと応答要求を送る動作です。

公式には2つの方法があります。

方法 何を送るか 向いている用途
Pre-generated response.createに話す文面そのものを入れる 決まった挨拶、ブランドの定型文
LLM-generated systemメッセージで「挨拶して」と指示し、response.create 状況に合わせた挨拶

Pre-generated(文面をそのまま読ませる)

モデルに文章を生成させず、渡したテキストをTTSで読み上げます。読み上げた文面はアシスタントの発話として会話履歴にも入ります。

response.create
{
  "type": "response.create",
  "response": {
    "pre_generated_assistant_message": {
      "role": "assistant",
      "content": [
        { "type": "text", "text": "こんにちは。今日は何をお手伝いしましょうか。" }
      ]
    }
  }
}

contentはテキスト1件だけです。

LLM-generated(挨拶を生成させる)

systemメッセージで指示を入れてから応答を要求します。サンプルではこちらを使いました。

conversation.item.create
{
  "type": "conversation.item.create",
  "item": {
    "type": "message",
    "role": "system",
    "content": [
      { "type": "input_text", "text": "日本語で、ユーザーに手短に温かく挨拶し、続けて何かお手伝いできることを尋ねてください。" }
    ]
  }
}
response.create
{ "type": "response.create" }

どちらもセッションごとに1回だけ送るように、クライアント側でフラグを持ちます。session.updatedは設定を変えるたびに届くので、フラグが無いと設定変更のたびに挨拶し直します。

言いよどみ(Semantic EOU / Smart turn detection)

通常のVAD(音声区間検出)は「一定時間無音だったら話し終わり」と判断します。そのため「えっと……」と考えている間に発話が終わったと判定され、エージェントが話し始めてしまいます。

EOU(End of Utterance)検出は、話した内容から発話が完結したかを判断して、この早すぎる割り込みを抑えます。

Semantic EOU(GA)

GAで使えるのはsemantic系のモデルです。

モデル 対応言語
semantic_detection_v1 英語
semantic_detection_v1_multilingual 英語・日本語を含む10言語(スペイン語・フランス語・イタリア語・ドイツ語・ポルトガル語・中国語・韓国語・ヒンディー語)

日本語なら_multilingualを使います。設定はturn_detectionのtypeではなく、VADの中のend_of_utterance_detectionにネストします。VAD自体も、日本語なら多言語版(azure_semantic_vad_multilingual)を選びます。

session.update
{
  "type": "session.update",
  "session": {
    "turn_detection": {
      "type": "azure_semantic_vad_multilingual",
      "threshold": 0.5,
      "silence_duration_ms": 500,
      "end_of_utterance_detection": {
        "model": "semantic_detection_v1_multilingual",
        "threshold_level": "default",
        "timeout_ms": 1000
      }
    }
  }
}

SDKにはAzureSemanticEouDetection(semantic_detection_v1)、AzureSemanticEouDetectionEn(_en)、AzureSemanticEouDetectionMultilingual(_multilingual)の3つの型があります。APIリファレンスに載っているのはsemantic_detection_v1と_multilingualの2つなので、実際には英語か多言語かを選ぶことになりそうです。

options.TurnDetection = new AzureSemanticVadTurnDetectionMultilingual
{
    EndOfUtteranceDetection = new AzureSemanticEouDetectionMultilingual
    {
        ThresholdLevel = EouThresholdLevel.Default,
        Timeout = TimeSpan.FromMilliseconds(1000)
    }
};

threshold_levelはlow / medium / high / default(medium相当)、timeout_msは続きの発話を待つ最大時間です。

turn_detectionには、間の扱いに効く設定がほかにもあります。

  • remove_filler_words(既定false):「えっと」「あー」のようなつなぎ言葉を、応答中の割り込み判定から除外します。英語ではah / umm / mm / uh / huh / oh / yeah / hmmが対象です。languagesで言語を絞ると精度が上がります
  • eagerness(auto / low / high):割り込みの積極性を変えます。ただしsemantic_vad(OpenAI系)専用で、azure_semantic_vad系では使えません

Smart turn detection(preview)

もう1つ、入力音声を直接処理して発話の終わりを判定するsmart turn detection(smart_end_of_turn_detection)があります。こちらはGAの2026-07-15でもpreviewのままで、SDK 1.2.0にも型がありません。

Smart turn detection and WebRTC remain in preview.
― Voice Live 2026-07-15 API reference

2026-06-01-previewを指定してAPIを直接呼ぶと動作しました。設定の形はsemantic EOUと同じくend_of_utterance_detectionへのネストで、modelだけが違います。

session.update(2026-06-01-preview)
{
  "type": "session.update",
  "session": {
    "turn_detection": {
      "type": "azure_semantic_vad",
      "end_of_utterance_detection": {
        "model": "smart_end_of_turn_detection",
        "threshold_level": "default",
        "timeout_ms": 1000
      }
    }
  }
}

turn_detectionのtypeにend_of_turn_detectionのような値を入れると、session.updateが拒否されて切断されます。VADの中のend_of_utterance_detectionにネストします。

semantic EOUとsmart turn detectionは設定の形がよく似ているので取り違えやすいので注意してください。SDK(GA版)で使えるのはsemantic EOUのほうです。

動作確認

「こんにちは……えっと……明日の天気は?」と間を空けて話したところ、間の部分でエージェントが割り込むことはなく、最後まで聞いてから天気を答えました。

ただし、マイクを止めるタイミングには注意が必要でした。VADのinput_audio_buffer.speech_stoppedを受けた時点でクライアントが録音を止めると、言いよどみの後半がサーバに届きません。これではEOUが判断する材料が無くなり、「間を待つ」挙動を観測できません。サンプルではEOUを試すときだけspeech_stoppedで録音を止めないようにしています。

割り込み(Auto-truncation)

エージェントが話している途中でユーザーが話し始めると(バージイン)、サーバは応答の生成を止めます(interrupt_response、既定で有効)。ここで問題になるのが会話履歴です。

サーバは応答の全文を生成済みなので、何もしないと履歴には「ユーザーが聞いていない後半」まで残ります。LLMは自分がそこまで話したつもりで次の応答を作ってしまいます。

auto_truncateを有効にすると、サーバがこの履歴を実際に聞こえたところまでに切り詰めます。

session.update
{
  "type": "session.update",
  "session": {
    "turn_detection": {
      "type": "azure_semantic_vad_multilingual",
      "threshold": 0.5,
      "silence_duration_ms": 500,
      "interrupt_response": true,
      "auto_truncate": true
    }
  }
}

切り詰めが起きるとconversation.item.truncatedが届きます。auto_truncateはVADの種別を問わず使えます(server_vadでもazure_semantic_vad系でも可)。ここでは日本語向けに多言語版のVADを指定しています。

公式ドキュメントには、切り詰めた履歴の末尾に「ユーザーに遮られた」と書き足すappended_text_after_truncationもあります。こちらはSDKの型に無く、JSONで送る必要があります(本記事では未検証です)。

クライアント側での再生の停止

auto_truncateを有効にしても、クライアントのスピーカーは止まりません。 サーバが止めるのは応答の生成と履歴で、再生を止めるのはクライアントの役割です。

応答の音声は実時間より速く届くので、割り込んだ時点でクライアントには数秒分の未再生の音声が溜まっています。これを捨てないと、履歴は切り詰められたのに古い応答の続きが鳴り続けます。さらに、割り込まれた応答のresponse.audio.deltaは少し遅れて届き続けるので、それも捨てないと次の応答の前に古い音声が混ざります。

クライアントがやることは2つです。

  1. input_audio_buffer.speech_startedを受けたら、再生バッファに溜まっている未再生の音声を捨てる
  2. 割り込まれた応答のresponse_idを覚えておき、そのidのresponse.audio.deltaを捨てる(新しい応答のidが来たら解除)
VoiceLiveAssistant.cs(抜粋)
case SessionUpdateInputAudioBufferSpeechStarted:
    InterruptPlayback();
    break;

// ...

private void InterruptPlayback()
{
    if (playingResponseId == null) return;

    // 割り込まれた応答を覚えておき、遅れて届く音声を捨てる
    interruptedResponseId = playingResponseId;
    playingResponseId = null;

    // 未再生のまま溜まっている音声を捨てる
    TimeSpan discarded = audioHandler.ClearPlayback();
}

private void HandleAudioDelta(SessionUpdateResponseAudioDelta audioDelta)
{
    string? responseId = audioDelta.ResponseId;
    if (responseId != null)
    {
        if (responseId == interruptedResponseId) return; // 古い応答の残り
        playingResponseId = responseId;
        interruptedResponseId = null;                     // 新しい応答が来たら解除
    }
    audioHandler.AddPlaybackData(audioDelta.Delta.ToArray());
}

切り詰め位置の決まり方

公式ドキュメントには次のように書かれています。

The service assumes the response is played at real-time speed.
― Handle voice interruptions in chat history

つまりサーバは「送った音声は実時間で再生されている」と仮定して切り詰める位置を決めていて、クライアントが実際にどこまで鳴らしたかを伝えているわけではありません。再生が遅れがちな環境では、履歴と実際に聞こえた内容がずれる可能性があります。

動作確認

実機で長い説明を途中で遮ったところ、次のようになりました。

  • 割り込んだ時点で7.8秒分の未再生の音声を捨てた
  • 応答はfinish_reasons=["cancelled"]で終わり、conversation.item.truncatedが届いた
  • 次の応答に、遮る前の続きは混ざらなかった
  • スピーカーの音をマイクが拾って誤って割り込む、ということは起きなかった(サンプルはセッション設定でサーバ側のエコー除去server_echo_cancellationを有効にしているため)

一方で、conversation.item.truncatedのaudio_end_msは0 msで届きました。数秒は再生していたので、0 msは実態と合いません。原因はまだわかっていません(SDK側の生の値を読むプロパティがinternalで、wireの値を確かめられていません)。

ツールの待ち時間(Interim response)

ツール呼び出しや応答生成に時間がかかるとき、「確認しますね」のような短いつなぎの発話(フィラー)を話させる機能です。

種類 何を話すか
llm_interim_response 軽量モデル(既定gpt-4.1-mini)が文脈に合わせて生成
static_interim_response 用意した文面のリストからランダムに選ぶ

発火条件(triggers)はlatency(応答が閾値より遅い)とtool(ツール実行中)で、どちらかを満たせば発火します。

session.update
{
  "type": "session.update",
  "session": {
    "interim_response": {
      "type": "llm_interim_response",
      "triggers": ["tool", "latency"],
      "latency_threshold_ms": 2000,
      "model": "gpt-4.1-mini",
      "instructions": "Say a short, natural filler in the user's language while you wait.",
      "max_completion_tokens": 50
    }
  }
}

キーもtypeも、すべてsnake_caseで書きます(interim_response / llm_interim_response / static_interim_response)。ハイフン区切り(interim-responseなど)は受け付けられず、'session.interim-response' unexpected (extra fields not permitted)で拒否されます。

また、使えるのはテキストモデル+Azureの音声によるカスケード構成だけです。gpt-realtimeのような音声をそのまま扱うモデルでは使えません。

SDKでの設定

SDK 1.2.0にはLlmInterimResponseConfigという型がありますが、受け取るVoiceLiveSessionOptions.InterimResponseはBinaryDataです。型で組み立ててから書き出して渡します。

SdkFeatureCatalog.cs(抜粋)
var interim = new LlmInterimResponseConfig
{
    Triggers = { InterimResponseTrigger.Tool, InterimResponseTrigger.Latency },
    LatencyThreshold = TimeSpan.FromMilliseconds(2000),
    Model = "gpt-4.1-mini",
    Instructions = "Say a short, natural filler in the user's language while you wait.",
    MaxCompletionTokens = 50
};
options.InterimResponse = ModelReaderWriter.Write(interim);

ModelReaderWriter.Writeを通せば、実際に送られるJSONのキー名が正しいsnake_caseになります。JSONを手で書くときだけキー名に注意が必要で、型を通せる場面では通したほうが安全です。

interim responseが話されるタイミング

interim responseの発話が入るのは、ツールを呼び出した応答の中です。interim_…というidのアシスタントメッセージ(音声付き)として、その応答のresponse.doneの前に話されます。

そして、function_call_outputがその応答の開いている間に届いたときだけ、interim responseが生成されました。ツールの結果を2秒遅らせると、応答1は関数呼び出しだけで閉じてしまい、interim responseは一度も話されませんでした。

ツールの戻り interim response
すぐ返す 話された
2秒遅らせる 一度も話されなかった

APIバージョン(2026-07-15 / 2026-01-01-preview)、latency_threshold_ms(500 / 2000)、parallel_tool_callsの有無はどれも結果に影響しませんでした。設定自体はsession.updatedにそのまま返ってくるので、受理されているのに何も起きない状態になります。

このため、「時間のかかるツールの裏で何か話させておく」用途には向かないようです。この機能が埋めるのは「ツールは速いが、そのあとの応答生成に時間がかかる」ほうの間だと考えると、挙動と合います。

時間のかかるツールの待ち時間を埋めたい場合は、ツールを呼ぶ前にクライアント側からpre-generatedの発話を差し込むほうがよさそうです(この記事の最初に出てきた方法がそのまま使えます)。

サンプルのSDKコンソールでは、--tool-delay-ms 2000でツールの戻りを遅らせてこの挙動を再現できます。

まとめ

  • Proactive messagesはセッション設定ではなくクライアントの動作で、1回だけ送るフラグを持ちます
  • 日本語のSemantic EOUは_multilingualのモデルを使い、VADの中にネストします
  • EOUを試すときはspeech_stoppedでマイクを止めないようにします
  • Smart turn detectionはまだpreviewで、SDKには型がありません
  • Auto-truncationでサーバが切り詰めるのは履歴だけで、再生の停止と古い音声の破棄はクライアントが行います
  • Interim responseはsnake_caseで書き、遅いツールの待ち時間には話されませんでした

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?