ブログ一覧

OpenAI Realtime API の response.cancel が効かない原因

更新: Web開発者向け

OpenAI の Realtime API で音声実況 AI を作っています。場面が変わったら前の話題を打ち切って、新しい場面について喋らせたい。そこで response.cancel を送ったところ、次のエラーが返ってきました。

response_cancel_not_active

「キャンセルすべき応答は存在しない」という意味です。でもスピーカーからはまだ前の話題が流れています。

実測: 12 秒分の発話を約 2 秒で生成し終えている

ログを追うと、モデルは 12 秒分の発話を約 2 秒で生成し終えていました。それ以降は音声バッファが流れているだけで、サーバーから見れば「生成中の応答」はもう存在しません。response.cancel が止めようとしたのは生成であって、私が止めたかったのは再生でした。

この食い違いの結果、次のことが起きていました。

  • 打ち切ったつもりの新しい応答が、再生待ち行列の後ろに積まれる。前の話題が最後まで流れてから新しい話題が始まるので、実況が常に一つ遅れる
  • 音が出る前の応答を次の合図で打ち切ると、出力ゼロの応答が量産される。生成は始まっているのでキャンセルは通るが、ユーザーには何も届かない。これが連鎖すると、合図が来るたびに自分の応答を自分で消し続ける状態になる

機構: 生成・送出・再生は別の状態

Realtime API では、応答のライフサイクルが 3 つの状態に分かれています。

  • 生成: モデルがトークンを作っている。response.cancel が効くのはここだけ
  • 送出: 生成済みの音声がサーバーからクライアントへ流れている
  • 再生: クライアント側のバッファにある音声がスピーカーから出ている

私が使っている WebRTC 接続では、未再生の音声は output_audio_buffer.clear で捨てられます。公式ガイドにも、WebRTC / SIP では output_audio_buffer.clear で未再生の音声を消す、と書かれていました。WebSocket 接続の場合は音声の再生をクライアントが自前で持つので、捨てるのも自前の再生キューです。

「今すぐ黙る」を成立させるには、生成のキャンセルと再生バッファの破棄を両方送る必要がありました。片方だけでは、生成が終わっていれば空振りし、再生が残っていれば喋り続けます。

どう直したか

  • 打ち切りは response.canceloutput_audio_buffer.clear をセットで送る。response_cancel_not_active は「生成はもう終わっている」という情報として扱い、エラーにしない
  • 打ち切りの対象を「音声が出始めた応答」に限定する。まだ音が出ていない応答を次の合図で消すと、出力ゼロの応答が連鎖するので、音が出る前の応答は次の合図を待たせる

どこで止まるか

  • output_audio_buffer.clear は会話履歴も途中で切り詰めるので、モデルから見た「自分が言ったこと」は実際に再生された範囲になります。これは望ましい挙動ですが、履歴を完全に残したい用途には向きません
  • 打ち切りの対象を絞ったぶん、「音が出始める直前の古い応答」は流れてしまうことがあります。遅れを完全にゼロにはできていません
  • 「12 秒を約 2 秒で」は私の構成での一例で、発話の長さやモデルの負荷で変わります。生成が再生より速いという関係だけが一定です

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

  • ストリーミング音声 API では「生成」「送出」「再生」の 3 つを別々に追跡する。止めたいのがどれなのかを先に決める
  • response_cancel_not_active は失敗ではなく「生成はもう終わっている」という状態の通知として読む
  • 打ち切りは「音声が出てから」に限定する。出る前の応答まで消すと体験が沈黙する

response.cancel は発話を止めるコマンドではなく、生成を止めるコマンドでした。モデルが喋り終えるのは、スピーカーが喋り終えるよりずっと早い。

関連

この記事をシェア