ブログ一覧

Gemini Live API で turnComplete 後に音声が届かない

更新: Web開発者向け

映像を見ながら音声で実況する AI の検証で、Gemini Live API の応答を計測していました。テキストキューを送り、返ってくる音声と転写を記録する計測です。結果は 8 キュー中 8 キューが「転写あり・音声 0ms」。転写が取れているのだから応答は生成されているはずなのに、音声だけが 1 バイトも記録されていません。

API 側の障害を疑いましたが、違いました。`turnComplete` はモデルの生成完了通知であって、音声チャンクの配送完了ではありません。 映像ストリーミングを併用すると turnComplete が音声チャンクより先に届くため、「turnComplete が来たら受信を打ち切る」と書いた計測コードが、そのあとから届く音声を全部捨てていました。

まず実測

2026-08-19〜20 の計測です。v1beta の WebSocket 直結で、モデルは gemini-3.1-flash-live-preview。接続の作り方は以前書いた WebSocket 直叩きの構成と同じです。

  • 映像ストリーミング併用時、turnComplete は最初の音声チャンクより 0.7〜1.3 秒先に届く。音声はその後から流れてくる
  • turnComplete 受信で収集を打ち切る実装 → 8/8 キューが「転写あり・音声 0ms」(全滅に見える)
  • 完了判定を後述の合成判定に変更 → 8/8 キューで音声を正常受信
  • 音声のみ(映像なし)のセッションでも、稀に同じ先着が起きる

「全滅」の正体は API の障害ではなく、こちらの完了判定の誤りでした。

機構: turnComplete は「ターンを終えた」であって「送り終えた」ではない

公式リファレンスの turnComplete の説明は「If true, indicates that the model has completed its turn.」です。モデルがターンを終えたこと、つまり生成側の区切りを示すフラグで、音声チャンク(inlineData)の配送がそれと同期するとはどこにも書かれていません。実測でもそのとおりで、完了通知が先に届き、音声データはその後もストリーミングされ続けます。映像併用時に先着幅が 1 秒前後まで広がる内部事情は外から観測できないので、「そういう届き方をする」という事実を前提に受信側を設計するしかありません。

なお、turnComplete が文の途中で届いて音声の生成自体が切れるという報告も SDK のリポジトリにあります(googleapis/js-genai#707 など)。あちらは生成が途切れる話で、この記事の「生成は完走しているのに受信側が配送を待たない」問題とは別です。症状はどちらも「音声が足りない」なので、どちらに当たっているかを切り分けてから直す必要があります。

どう直したか: 完了判定を合成にする

ターンごとに {firstAudioAt, lastChunkAt, turnCompleteAt} を記録し、発話完了を「turnComplete 受信済み かつ 音声チャンクの到着が 1.2 秒途絶えた」の合成で判定します。音声が一度も来ないターンは 6 秒でタイムアウトさせます。判定部分だけ抜き出すと次の形です。

// turnId -> { firstAudioAt, lastChunkAt, turnCompleteAt }
function isTurnDone(t, now) {
  if (!t.turnCompleteAt) return false; // 生成側の区切りがまだ
  if (t.lastChunkAt) return now - t.lastChunkAt > 1200; // 配送の途絶で完了
  return now - t.turnCompleteAt > 6000; // 本当に音声の無いターン
}

この判定に替えて、同じ 8 キューがすべて正常受信になりました。

計測だけでなく再生側にも同じ罠があります。「turnComplete が来たから次へ進む」と書くと、まだ届いていない音声を取り逃します。次の処理へ進むのは、再生キューの排水(drain)を待ってからです。

遅れて届く音声が次のシーンを汚染する

実運用ではもうひとつ症状が出ました。アプリのフェーズが先へ進んだ後に古いターンの音声が遅れて届き、次のシーンの実況に前のシーンの音声が混ざります。

これは未完了ターンのカウンタで解決しました。キューを注入したら pendingTurns++turnComplete を受けたら pendingTurns--。シーンを切り替えるフラッシュの時点で残数を見て、そのぶんの音声・転写・完了通知を丸ごと破棄予約します。「フラッシュ後に届いたものは捨てる」ではなく「フラッシュ時点で未完了だったターンの分だけ捨てる」にするのが要点で、切り替え直後に送った新しいキューの応答は巻き込まれません。

どこで止まるか

  • 1.2 秒 / 6 秒という閾値はヒューリスティックです。プロトコルに配送完了の通知が無い以上、取り逃しゼロは原理的に保証できません
  • 発話完了の確定が最短でも最終チャンクの 1.2 秒後になります。完了を待って動く後段の処理はそのぶん遅れます
  • 2.5 系(gemini-2.5-flash-native-audio-preview-12-2025)は WebSocket 直結では映像入力が計上されないため、映像併用時の挙動の世代間比較は取れていません
  • preview モデルの挙動は変わり得ます。この記事の数字は 2026-08 時点のものです

この構成で気をつけること

  • turnComplete を「音声も揃った」と読まない。完了判定は turnComplete と音声途絶の合成にする
  • 再生側は再生キューの drain を待ってから次へ進む
  • シーンやフェーズを跨ぐアプリでは、フラッシュ時点の未完了ターン数を数えて遅延到着を破棄する
  • 転写だけ見て「応答は来ている」と判断しない。音声のバイト数まで記録する

「転写はあるのに音声が無い」は API の障害に見えますが、私の場合は受信の打ち切り条件を数行直すだけで済みました。同じ症状を見たら、まず自分の完了判定がどのイベントを信じているかを確認するのが先です。

関連

この記事をシェア