AIニュース最前線
最新ニュースAI日報Hacker日報週報動画AIツールトレンド企業

AIニュース最前線

世界中のAI最新情報を日本語で毎時更新

最新ニュース日報トレンド企業プレミアムRSS
© 2026 ainew.jp特定商取引法に基づく表記
ニュース一覧元記事を開く
CyberAgent Developers Blog·2026年6月1日 09:50·約12分で読める

Deepgram Flux Multilingual のEnd-of-Turn判定を試す

こんにちは、AI Shift AIチームの大竹です。

今回は Deepgram の最新 STT モデル Flux Multilingual を日本語の電話音声で試した検証ログです。以前のテックブログ VAD・VAPを用いた発話終了検知 では独立した VAD/VAP モデルでの発話終了検知の限界を整理しましたが、Flux は EoT 検出が音声認識モデル本体に統合されている のが大きな違いで、同じ音声・同じ評価軸で検証してみました。

なお、AI Shift Tech Blog では、これまでも音声対話システムにおける発話終了検知・終話判定・ターンテイキング周辺の技術検証をいくつか公開しています。**

本記事では Deepgram Flux Multilingual の EoT 検出に焦点を当てますが、背景技術やこれまでの検証内容に興味がある方は、以下の記事もあわせてご覧ください。

  • inaSpeechSegmenterによる音声区間検出
  • 信号パワーと零交差数を用いた音声区間検出
  • VAD・VAPを用いた発話終了検知
  • Deepgram Fluxを使ったターンテイキング認識の実験
  • ターンテイキングのタイミング予測を簡単に試せるライブラリMaAIを使ってみた

Flux Multilingual とは

Flux Multilingual は Deepgram が 2026 年 4 月末に発表**した会話特化の Streaming STT モデルです。公式ブログ Introducing Flux Multilingual では以下のように紹介されています。

**

One conversational speech model. Ten languages. One API. Single-language accuracy without rebuilding your stack.

英語・スペイン語・フランス語・ドイツ語・ヒンディー語・ロシア語・ポルトガル語・日本語・イタリア語・オランダ語の 10 言語に対応し、言語検出・コードスイッチング・ターン検出・割込み処理を単一のストリーミング接続で同時に提供**する点が特徴です。複数言語で動かしても各言語専用モデル並みの精度を保つことを売りにしています。

中でも最大の特徴は End-of-Turn(EoT)検出が認識モデル本体に組み込まれていることです。従来は VAD(無音検知)で発話終了を判定していましたが、Flux は発話内容そのものから「ここで話者は話し終わった」を確率値で出力します。Deepgram 自身も「無音閾値ではなく会話文脈を理解する学習済み信頼度信号を使用し、競合する実時間 EoT システムより最大 3 倍低いレイテンシを実現」と説明しており、レイテンシと精度のトレードオフを単一モデル内で扱えるのがこのモデルの設計思想です。

Flux では、音声認識結果の transcript に加えて、話者のターン状態を表すイベントもストリーミングで配信されます。

これにより、アプリケーション側は「発話が始まった」「終わりそう」「やはり続いた」「最終的に終わった」といった状態遷移をリアルタイムに受け取り、LLM 推論の開始・取り消し・確定処理に利用できます。

主に配信されるターンイベントは以下の 4 種類です。

イベント

意味

用途

StartOfTurn

発話開始を検知

録音開始

EagerEndOfTurn

「終わりっぽい」推測(confidence ≥ eager_eot_threshold)

LLM の投機実行起動

TurnResumed

EagerEoT 後に発話再開(推測の取り消し)

投機結果の破棄

EndOfTurn

「ここで確定」(confidence ≥ eot_threshold)

最終 transcript 確定

EagerEoT は音声エージェントで「LLM 推論を先に始めて、もし続きが来たら捨てる」という投機的な実行を可能にする仕組みです。

検証セットアップ

入力音声

iiyodomi_phone.wav: 8kHz で録音された電話帯域音声を 16kHz にアップサンプリングしたもの

発話内容:来週の水曜日の午後五時に五人で予約できますか(フィラー・言い淀みあり)

パラメータ

EoT 検出のすべての挙動(StartOfTurn / EagerEoT / TurnResumed / EndOfTurn)が 1 枚の図に収まる設定で記録します。

eot_threshold

eager_eot_threshold

0.7

0.5

コード

実際に使用したコードのコア部分です。80ms ごとに音声チャンクを送り、TurnInfo イベントを受信して記録しています。

検証に使用したコードimport asyncio

import os

from dataclasses import dataclass, field

from pathlib import Path

import numpy as np

import soundfile as sf

from deepgram import AsyncDeepgramClient

from deepgram.core.events import EventType

from dotenv import load_dotenv

load_dotenv()

DEEPGRAM_API_KEY = os.environ["DEEPGRAM_API_KEY"]

サンプルレートは 16000

チャンク長は 80 ミリ秒

チャンクバイト数は、サンプルレートにチャンク長(ミリ秒)を乗じて 1000 で割り、2 を掛けた整数値です。これは linear16(1 サンプルあたり 2 バイト)に対応します。

@dataclass デコレータ付きの TurnEvent クラス:

  • received_at: フローティングポイント型で受信時刻
  • event: イベント種別(StartOfTurn / Update / EagerEndOfTurn / TurnResumed / EndOfTurn のいずれか)
  • audio_window_start: オーディオウィンドウ開始時刻
  • audio_window_end: オーディオウィンドウ終了時刻
  • transcript: 文字起こし結果
  • eot_confidence: 末尾判定の信頼度(浮動小数点数または None)

async def run_flux(audio_path: Path) -> list[TurnEvent]:

audio_data, sr = sf.read(audio_path, dtype="int16")

if audio_data.ndim == 2:

audio_data = audio_data.mean(axis=1).astype(np.int16)

assert sr == SAMPLE_RATE

events: list[TurnEvent] = []

stream_started_at: float | None = None

client = AsyncDeepgramClient(api_key=DEEPGRAM_API_KEY)

async with client.listen.v2.connect(

model="flux-general-multi",

encoding="linear16",

sample_rate=SAMPLE_RATE,

language_hint=["ja"],

eot_threshold=0.7,

eager_eot_threshold=0.5,

) as connection:

def _get(msg, key, default=None):

return msg.get(key, default) if isinstance(msg, dict) else getattr(msg, key, default)

def on_message(message):

# SDK 7.x: TurnInfo は Union 型解決の都合で dict のまま届く場合があるので type で判定

if _get(message, "type") != "TurnInfo" or stream_started_at is None:

return

events.append(TurnEvent(

received_at=asyncio.get_event_loop().time() - stream_started_at,

event=_get(message, "event", ""),

audio_window_start=_get(message, "audio_window_start", 0.0),

audio_window_end=_get(message, "audio_window_end", 0.0),

transcript=_get(message, "transcript", "") or "",

eot_confidence=_get(message, "end_of_turn_confidence"),

))

connection.on(EventType.MESSAGE, on_message)

start_listening() は受信ループなのでバックグラウンドで起動

listen_task = asyncio.create_task(connection.start_listening())

stream_started_at = asyncio.get_event_loop().time()

80ms ごとに音声を送出 (リアルタイム再生をシミュレート)

audio_bytes = audio_data.tobytes()

for i in range(0, len(audio_bytes), CHUNK_BYTES):

await connection.send_media(audio_bytes[i : i + CHUNK_BYTES])

await asyncio.sleep(CHUNK_MS / 1000)

最後の EndOfTurn を待ってからクローズ

await asyncio.sleep(2.0)

await connection.send_close_stream()

await listen_task

return events

if __name__ == "__main__":

events = asyncio.run(run_flux(Path("audio/iiyodomi_phone.wav")))

for e in events:

if e.event in ("StartOfTurn", "EagerEndOfTurn", "TurnResumed", "EndOfTurn"):

print(f"[{e.received_at:6.2f}s] {e.event:18} "

f"win_end={e.audio_window_end:5.2f}s "

f"conf={e.eot_confidence} text={e.transcript!r}")

結果

imageimage**

*Flux Multilingual による EoT(End-of-Turn)判定の信頼度推移*

この 1 枚には、EoT 検出に必要な全要素が凝縮されています。順を追って解説していきましょう。

図の見方

下段の confidence trajectory(信頼度軌跡:灰色の折れ線)は、end_of_turn_confidence** の時系列データです。80ms ごとの Update イベントを繋ぎ合わせたもので、モデルが「ここで話しが終わろうとしている」と判定しているかをリアルタイムで示しています。

2 本の水平線:

  • 緑の破線:eot_threshold=0.7(確定の閾値)
  • 紫の点線:eager_eot_threshold=0.5(投機的な閾値)

マーカーは 4 種類あります:

  • ▲ 青 (StartOfTurn) — 発話開始
  • ▼ 紫 (EagerEndOfTurn) — confidence が 0.5 を超え、「終わりかも」と推測した瞬間
  • ■ 橙 (TurnResumed) — 上記の推測が取り消された瞬間(話者が続けて話すと判定された場合)
  • ★ 緑 (EndOfTurn) — confidence が 0.7 を超え、ターン確定

イベント時系列

時刻

event

confidence

transcript

状態

4.08s

StartOfTurn

0.01

「来週の」

ターン開始

6.40s

EagerEndOfTurn

0.52

「来週の水曜日の」

投機 ①

6.96s

TurnResumed

0.02

「…の午後」

投機 ① 取り消し

9.92s

EagerEndOfTurn

0.51

「…の午後五時に」

投機 ②

10.32s

TurnResumed

0.07

「…五時に五」

投機 ② 取り消し

13.84s

EagerEndOfTurn

0.53

「…五人で予約って」

投機 ③(誤認)

14.16s

TurnResumed

0.04

「…予約ででき」

投機 ③ 取り消し

14.80s

EagerEndOfTurn

0.75

「…予約でできますか。」

投機 ④(最終)

14.80s

EndOfTurn

0.75

同上

確定

最終 transcript: 「来週の水曜日の午後五時に五人で予約でできますか。」

読み取れること

1. EoT 確率は発話途中で何度も「終わりっぽい」山を作る

confidence は単調増加ではなく、6s / 10s / 14s 付近で 3 回の山を作っています。これは話者が文節ごとに小さく区切って話す(「来週の水曜日の」「午後五時に」「五人で予約で」「できますか」)たびに、Flux 内部で「ここが終わりかも?」と判定が揺れている挙動です。

2. eot_threshold=0.7 の閾値設計が誤確定を防いでいる

3 回の山のうち、最初の 2 つは 0.52 / 0.51 と 0.5 をわずかに超える程度で、0.7 のラインには届きません。

今回のログでは、発話途中の文節区切りでも confidence が 0.5 前後まで上がる場面がありました。**

そのため、もし eot_threshold を 0.5 付近に設定していた場合、「来週の水曜日の」や「午後五時に」の時点で EndOfTurn と判定され、ターンが途中で分割されていた可能性があります。

一方で、eot_threshold=0.7 にしておくことで、こうした発話途中の一時的な confidence 上昇は EagerEoT に留め、最終的な確定は「できますか」まで待つことができました。

3. EagerEoT は投機イベント、TurnResumed はそれの取り消し

EagerEoTが出るたびに、その 300〜600ms 後** にTurnResumedが来ています。話者が続けて話したことを Flux が音響的に検知し「さっきの推測は違った」と知らせる挙動です。

4. 投機の transcript には誤りが混ざる

13.84s の EagerEoT で transcript は「…五人で予約って」となっていますが、これは音声コンテキストが不十分な状態での投機的なデコード結果です。後続の TurnResumed で取り消され、最終 EndOfTurn では「…予約でできますか。」に修正されています。LLM 投機実行で 投機的なデコード結果を使う場合は、後から書き換わる前提で設計する必要があります。

考察

Eager EoT は「早期確定」ではなく「投機実行の開始点」

eager モードの実効性は、eager_eot_threshold を eot_threshold よりも低く設定したときに生まれます。**同じ値にしてしまうと、confidence が閾値を超えた瞬間に EagerEoT と EndOfTurn が同時に発火するだけになり、EagerEoT を使って先に LLM 推論を始める余地がほとんどありません。

今回の設定では eager_eot_threshold=0.5、eot_threshold=0.7 と差を持たせたことで、最終的な EndOfTurn より前に複数回 EagerEoT が発火しました。

ただし、EagerEoT は「ここで確定」という意味ではありません。実際に 6.40s、9.92s、13.84s の EagerEoT は、その後の TurnResumed によって取り消されています。

つまり EagerEoT は LLM 推論を投機的に開始するための候補点**であり、TurnResumed が来た場合はその結果を破棄する前提で扱う必要があります。

一方で、TurnResumed が来ずに EndOfTurn まで進めば、EagerEoT から EndOfTurn までの時間を応答生成に使えます。**

たとえば EagerEoT が EndOfTurn より 500ms 早く出れば、その 500ms 分だけ LLM 推論を前倒しできるため、ユーザーへの応答開始を早められます。

今回のログでは最後の EagerEoT と EndOfTurn がどちらも 14.80s に発火しているため、最終応答に直結する大きな投機成功は得られていません。

それでも、Flux が発話途中の区切りごとに投機開始のチャンスを提示し、誤った投機は TurnResumed で安全に取り消せることが確認できました。

transcript フィールドは閾値に依存しない(今回の観察範囲)

EagerEndOfTurn と EndOfTurn の transcript フィールドは、その時点の音声情報から決まる同じ STT デコード結果でした。閾値はあくまで「いつイベントを発火するか」を決めるだけで、少なくとも今回の音声では eager 閾値を 0.3 / 0.5 / 0.6 のどれに振っても最終 EndOfTurn の文字列は同一でした。

ただし eot_threshold 側を下げてターン境界そのものを動かすと話は別で、確定の前段で別ターンとして切られた箇所が独立にデコードされるため、最終 transcript の中身が変わりえます。eager_eot_threshold のチューニングと eot_threshold のチューニングは挙動の性質が違うので、ここは混同しないように注意が必要です。

eager_eot_threshold の存在価値

confidence の値は全ての Update イベントで返されるので、「eot_confidence >= 0.5 で投機実行」というロジックは自前でも書けます。ただし「いつ投機をキャンセルすべきか」を自前判定するのは難しいです。confidence が一時的に下がっただけかもしれないし、話者が少し言い淀んだだけの可能性もあります。TurnResumed は Flux がその時点で受信した音声情報を見て「これは続きの発話だ」と判定した結果なので、自前のヒューリスティックな判定よりも信頼できるフラグとして使えます。

つまり eager_eot_threshold の本質的な役割は閾値判定そのものではなく、EagerEoT / TurnResumed のペアとしてサーバ側で投機ライフサイクル管理を提供してくれること** にあります。

まとめ

今回は Deepgram の最新 STT(Speech-to-Text)モデル Flux Multilingual の End-of-Turn 判定を日本語の電話音声で試してみました。

Flux の真価は EoT 確率の連続的な可視化 と EagerEoT / TurnResumed による投機ライフサイクル管理にあります。これらを組み合わせることで Voice Agent の応答タイミング設計が大幅に楽になります。

特に、EoT 検出が ASR(Automatic Speech Recognition)モデル本体に統合されており、音響情報だけでなく文脈(直前までの書き起こし内容)も考慮して確率が出力される という点に大きな進歩を感じました。

普段、コールセンター向け AI エージェントである AI Worker VoiceAgent を開発している身として、引き続き周辺技術を注視していきたいと思います!

長くなってしまいましたが、最後までお読みいただきありがとうございました!

原文を表示

こんにちは、AI Shift AIチームの大竹です。

今回は Deepgram の最新 STT モデル Flux Multilingual を日本語の電話音声で試した検証ログです。以前のテックブログ VAD・VAPを用いた発話終了検知 では独立した VAD/VAP モデルでの発話終了検知の限界を整理しましたが、Flux は EoT 検出が音声認識モデル本体に統合されている のが大きな違いで、同じ音声・同じ評価軸で検証してみました。

なお、AI Shift Tech Blog では、これまでも音声対話システムにおける発話終了検知・終話判定・ターンテイキング周辺の技術検証をいくつか公開しています。**

本記事では Deepgram Flux Multilingual の EoT 検出に焦点を当てますが、背景技術やこれまでの検証内容に興味がある方は、以下の記事もあわせてご覧ください。

  • inaSpeechSegmenterによる音声区間検出
  • 信号パワーと零交差数を用いた音声区間検出
  • VAD・VAPを用いた発話終了検知
  • Deepgram Fluxを使ったターンテイキング認識の実験
  • ターンテイキングのタイミング予測を簡単に試せるライブラリMaAIを使ってみた

Flux Multilingual とは

Flux Multilingual は Deepgram が 2026 年 4 月末に発表** した会話特化の Streaming STT モデルです。公式ブログ Introducing Flux Multilingual では以下のように紹介されています。

One conversational speech model. Ten languages. One API. Single-language accuracy without rebuilding your stack.

英語・スペイン語・フランス語・ドイツ語・ヒンディー語・ロシア語・ポルトガル語・日本語・イタリア語・オランダ語の 10 言語に対応し、言語検出・コードスイッチング・ターン検出・割込み処理を単一のストリーミング接続で同時に提供 する点が特徴です。複数言語で動かしても各言語専用モデル並みの精度を保つことを売りにしています。

中でも最大の特徴は End-of-Turn(EoT)検出が認識モデル本体に組み込まれている ことです。従来は VAD(無音検知)で発話終了を判定していましたが、Flux は発話内容そのものから「ここで話者は話し終わった」を確率値で出力します。Deepgram 自身も「無音閾値ではなく会話文脈を理解する学習済み信頼度信号を使用し、競合する実時間 EoT システムより最大 3 倍低いレイテンシを実現」と説明しており、レイテンシと精度のトレードオフを単一モデル内で扱えるのがこのモデルの設計思想です。

Flux では、音声認識結果の transcript に加えて、話者のターン状態を表すイベントもストリーミングで配信されます。

これにより、アプリケーション側は「発話が始まった」「終わりそう」「やはり続いた」「最終的に終わった」といった状態遷移をリアルタイムに受け取り、LLM 推論の開始・取り消し・確定処理に利用できます。

主に配信されるターンイベントは以下の 4 種類です。

イベント

意味

用途

StartOfTurn

発話開始を検知

録音開始

EagerEndOfTurn

「終わりっぽい」推測(confidence ≥ eager_eot_threshold)

LLM の投機実行起動

TurnResumed

EagerEoT 後に発話再開(推測の取り消し)

投機結果の破棄

EndOfTurn

「ここで確定」(confidence ≥ eot_threshold)

最終 transcript 確定

EagerEoT は音声エージェントで「LLM 推論を先に始めて、もし続きが来たら捨てる」という投機的な実行を可能にする仕組みです。

検証セットアップ

入力音声

iiyodomi_phone.wav: 8kHz で録音された電話帯域音声を 16kHz にアップサンプリングしたもの

発話内容: 来週の水曜日の午後五時に五人で予約できますか(フィラー・言い淀みあり)

パラメータ

EoT 検出のすべての挙動(StartOfTurn / EagerEoT / TurnResumed / EndOfTurn)が 1 枚の図に収まる設定で記録します。

eot_threshold

eager_eot_threshold

0.7

0.5

コード

実際に使用したコードのコア部分です。80ms ごとに音声チャンクを送り、TurnInfo イベントを受信して記録しています。

検証に使用したコード

code
import asyncio                                                                                                                                                               
import os
from dataclasses import dataclass, field
from pathlib import Path                                                                                                                                                     

import numpy as np                                                                                                                                                           
import soundfile as sf                                    
from deepgram import AsyncDeepgramClient
from deepgram.core.events import EventType
from dotenv import load_dotenv                                                                                                                                               

load_dotenv()                                                                                                                                                                
DEEPGRAM_API_KEY = os.environ["DEEPGRAM_API_KEY"]         

SAMPLE_RATE = 16000
CHUNK_MS = 80                                                                                                                                                                
CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_MS / 1000) * 2  # linear16 = 2 byte/sample
                                                                                                                                                                             
                                                          
@dataclass                                                                                                                                                                   
class TurnEvent:                                          
    received_at: float
    event: str                # StartOfTurn / Update / EagerEndOfTurn / TurnResumed / EndOfTurn
    audio_window_start: float                                                                                                                                                
    audio_window_end: float
    transcript: str                                                                                                                                                          
    eot_confidence: float | None = None                   
                                                                                                                                                                             

async def run_flux(audio_path: Path) -> list[TurnEvent]:                                                                                                                     
    audio_data, sr = sf.read(audio_path, dtype="int16")   
    if audio_data.ndim == 2:
        audio_data = audio_data.mean(axis=1).astype(np.int16)                                                                                                                
    assert sr == SAMPLE_RATE
                                                                                                                                                                             
    events: list[TurnEvent] = []                          
    stream_started_at: float | None = None                                                                                                                                   

    client = AsyncDeepgramClient(api_key=DEEPGRAM_API_KEY)                                                                                                                   
    async with client.listen.v2.connect(                  
        model="flux-general-multi",                                                                                                                                          
        encoding="linear16",
        sample_rate=SAMPLE_RATE,                                                                                                                                             
        language_hint=["ja"],                                                                                                                                                
        eot_threshold=0.7,
        eager_eot_threshold=0.5,                                                                                                                                             
    ) as connection:                                      

        def _get(msg, key, default=None):
            return msg.get(key, default) if isinstance(msg, dict) else getattr(msg, key, default)                                                                            

        def on_message(message):                                                                                                                                             
            # SDK 7.x: TurnInfo は Union 型解決の都合で dict のまま届く場合があるので type で判定
            if _get(message, "type") != "TurnInfo" or stream_started_at is None:                                                                                             
                return
            events.append(TurnEvent(                                                                                                                                         
                received_at=asyncio.get_event_loop().time() - stream_started_at,                                                                                             
                event=_get(message, "event", ""),
                audio_window_start=_get(message, "audio_window_start", 0.0),                                                                                                 
                audio_window_end=_get(message, "audio_window_end", 0.0),                                                                                                     
                transcript=_get(message, "transcript", "") or "",
                eot_confidence=_get(message, "end_of_turn_confidence"),                                                                                                      
            ))                                            
                                                                                                                                                                             
        connection.on(EventType.MESSAGE, on_message)      

        # start_listening() は受信ループなのでバックグラウンドで起動
        listen_task = asyncio.create_task(connection.start_listening())                                                                                                      
        stream_started_at = asyncio.get_event_loop().time()                                                                                                                  
                                                                                                                                                                             
        # 80ms ごとに音声を送出 (リアルタイム再生をシミュレート)                                                                                                             
        audio_bytes = audio_data.tobytes()                                                                                                                                   
        for i in range(0, len(audio_bytes), CHUNK_BYTES):
            await connection.send_media(audio_bytes[i : i + CHUNK_BYTES])                                                                                                    
            await asyncio.sleep(CHUNK_MS / 1000)          
                                                                                                                                                                             
        # 最後の EndOfTurn を待ってからクローズ           
        await asyncio.sleep(2.0)                                                                                                                                             
        await connection.send_close_stream()              
        await listen_task                                                                                                                                                    
                                                          
    return events


if __name__ == "__main__":
    events = asyncio.run(run_flux(Path("audio/iiyodomi_phone.wav")))                                                                                                         
    for e in events:
        if e.event in ("StartOfTurn", "EagerEndOfTurn", "TurnResumed", "EndOfTurn"):                                                                                         
            print(f"[{e.received_at:6.2f}s] {e.event:18} "                                                                                                                   
                  f"win_end={e.audio_window_end:5.2f}s "                                                                                                                     
                  f"conf={e.eot_confidence} text={e.transcript!r}")                                                                                                          

結果

**

*Flux MultilingualによるEoT判定の信頼度推移*

この 1 枚に EoT 検出の全要素が詰まっています。順に読み解いていきます。

図の見方

下段の confidence trajectory(灰色折れ線)が end_of_turn_confidence** の時系列です。80ms ごとの Update イベントを繋いだもので、 モデルが「ここで話し終わりそう」と判定しているかをリアルタイムに示します。

2 本の水平線:

  • 緑の破線: eot_threshold=0.7(確定の閾値)
  • 紫の点線: eager_eot_threshold=0.5(投機の閾値)

マーカーは 4 種類:

  • ▲ 青 (StartOfTurn) — 発話開始
  • ▼ 紫 (EagerEndOfTurn) — confidence が 0.5 を超え、「終わりかも」と推測した瞬間
  • ■ 橙 (TurnResumed) — 上記推測が取り消された瞬間(話者が続けて話したと判定)
  • ★ 緑 (EndOfTurn) — confidence が 0.7 を超え、ターン確定

イベント時系列

時刻

event

confidence

transcript

状態

4.08s

StartOfTurn

0.01

「来週の」

ターン開始

6.40s

EagerEndOfTurn

0.52

「来週の水曜日の」

投機 ①

6.96s

TurnResumed

0.02

「…の午後」

投機 ① 取り消し

9.92s

EagerEndOfTurn

0.51

「…の午後五時に」

投機 ②

10.32s

TurnResumed

0.07

「…五時に五」

投機 ② 取り消し

13.84s

EagerEndOfTurn

0.53

「…五人で予約って」

投機 ③(誤認)

14.16s

TurnResumed

0.04

「…予約ででき」

投機 ③ 取り消し

14.80s

EagerEndOfTurn

0.75

「…予約でできますか。」

投機 ④(最終)

14.80s

EndOfTurn

0.75

同上

確定

最終 transcript: 「来週の水曜日の午後五時に五人で予約でできますか。」

読み取れること

1. EoT 確率は発話途中で何度も「終わりっぽい」山を作る

confidence は単調増加ではなく、6s / 10s / 14s 付近で 3 回の山を作っています。これは話者が文節ごとに小さく区切って話す(「来週の水曜日の」「午後五時に」「五人で予約で」「できますか」)たびに、Flux 内部で「ここが終わりかも?」と判定が揺れている挙動です。

2. eot_threshold=0.7 の閾値設計が誤確定を防いでいる

3 回の山のうち、最初の 2 つは 0.52 / 0.51 と 0.5 をわずかに超える程度で、0.7 のラインには届きません。

今回のログでは、発話途中の文節区切りでも confidence が 0.5 前後まで上がる場面がありました。**

そのため、もし eot_threshold を 0.5 付近に設定していた場合、「来週の水曜日の」や「午後五時に」の時点で EndOfTurn と判定され、ターンが途中で分割されていた可能性があります。

一方で、eot_threshold=0.7 にしておくことで、こうした発話途中の一時的な confidence 上昇は EagerEoT に留め、最終的な確定は「できますか」まで待つことができました。

3. EagerEoT は投機イベント、TurnResumed はそれの取り消し

EagerEoTが出るたびに、その 300〜600ms 後** にTurnResumedが来ています。話者が続けて話したことを Flux が音響的に検知し「さっきの推測は違った」と知らせる挙動です。

4. 投機の transcript には誤りが混ざる

13.84s の EagerEoT で transcript は「…五人で予約って」となっていますが、これは音声コンテキストが不十分な状態での投機的なデコード結果です。後続の TurnResumed で取り消され、最終 EndOfTurn では「…予約でできますか。」に修正されています。LLM 投機実行で 投機的なデコード結果を使う場合は、後から書き換わる前提で設計する必要があります。

考察

Eager EoT は「早期確定」ではなく「投機実行の開始点」

eager mode の実効性は eager_eot_threshold < eot_threshold にしたときに生まれます。**

同じ値にしてしまうと、confidence が閾値を超えた瞬間に EagerEoT と EndOfTurn が同時に発火するだけになり、EagerEoT を使って先に LLM 推論を始める余地がほとんどありません。

今回の設定では eager_eot_threshold=0.5、eot_threshold=0.7 と差を持たせたことで、最終的な EndOfTurn より前に複数回 EagerEoT が発火しました。

ただし、EagerEoT は「ここで確定」という意味ではありません。実際に 6.40s、9.92s、13.84s の EagerEoT は、その後の TurnResumed によって取り消されています。

つまり EagerEoT は LLM 推論を投機的に開始するための候補点**であり、TurnResumed が来た場合はその結果を破棄する前提で扱う必要があります。

一方で、TurnResumed が来ずに EndOfTurn まで進めば、EagerEoT から EndOfTurn までの時間を応答生成に使えます。**

たとえば EagerEoT が EndOfTurn より 500ms 早く出れば、その 500ms 分だけ LLM 推論を前倒しできるため、ユーザーへの応答開始を早められます。

今回のログでは最後の EagerEoT と EndOfTurn がどちらも 14.80s に発火しているため、最終応答に直結する大きな投機成功は得られていません。

それでも、Flux が発話途中の区切りごとに投機開始のチャンスを提示し、誤った投機は TurnResumed で安全に取り消せることが確認できました。

transcript フィールドは閾値に依存しない(今回の観察範囲)

EagerEndOfTurn と EndOfTurn の transcript フィールドは、その時点の音声情報から決まる同じ STT デコード結果でした。閾値はあくまで「いつイベントを発火するか」を決めるだけで、少なくとも今回の音声では eager 閾値を 0.3 / 0.5 / 0.6 のどれに振っても最終 EndOfTurn の文字列は同一でした。

ただし eot_threshold 側を下げてターン境界そのものを動かすと話は別で、確定の前段で別ターンとして切られた箇所が独立にデコードされるため、最終 transcript の中身が変わりえます。eager_eot_threshold のチューニングと eot_threshold のチューニングは挙動の性質が違うので、ここは混同しないように注意が必要です。

eager_eot_threshold の存在価値

confidence の値は全てのUpdateイベントで返されるので、「eot_confidence >= 0.5 で投機実行」というロジックは自前でも書けます。ただし 「いつ投機をキャンセルすべきか」を自前判定するのは難しいです。confidence が一時的に下がっただけかもしれないし、話者が少し言い淀んだだけの可能性もあります。TurnResumed は Flux が その時点で受信した音声情報を見て「これは続きの発話だ」と判定した結果なので、自前のヒューリスティックな判定よりも信頼できるフラグとして使えます。

つまり eager_eot_threshold の本質的な役割は閾値判定そのものではなく、EagerEoT / TurnResumed のペアとしてサーバ側で投機ライフサイクル管理を提供してくれること** にあります。

まとめ

今回はDeepgramの最新STTモデルFlux MultilingualのEnd-of-Turn判定を日本語の電話音声で試してみました。

Flux の真価は EoT 確率の連続的な可視化 と EagerEoT / TurnResumed による投機ライフサイクル管理にあります。これらを組み合わせることでVoiceAgentの応答タイミング設計が大幅に楽になります。

特に、EoT 検出が ASR モデル本体に統合されており、音響情報だけでなく文脈(直前までの書き起こし内容)も考慮して確率が出力される という点に大きな進歩を感じました。

普段、コールセンター向けAIエージェントであるAI Worker VoiceAgentを開発している身として、引き続き周辺技術を注視していきたいと思います!

長くなってしまいましたが、最後までお読みいただきありがとうございました!

この記事をシェア

関連記事

CyberAgent Developers Blog2026年6月2日 13:00

パーサ回帰で Datadog Agent の CPU が急増する問題

CyberAgent Developers Blog2026年6月2日 08:38

Argoワークフロー移行に挑んだ話

CyberAgent Developers Blog2026年5月29日 08:00

配信数の規模・期間の違いに頑健な広告ペーシング制御

ニュース一覧に戻る元記事を読む