WebRTC または WebSocket 経由で Realtime API に接続すると、Realtime モデル(gpt-realtime-2.1 など)を呼び出して、音声変換による会話を行えます。そのためには、 クライアントイベントを送信 してアクションを開始し、 サーバーイベントをリッスン して Realtime API が実行したアクションに対応する必要があります。
このガイドでは、音声やテキストの生成、画像入力、Function Calling などのモデルの機能を使うために必要なイベントの流れと、リアルタイムセッションの状態の捉え方を説明します。
モデルとの会話が不要な場合、つまり応答を期待しない場合は、 Realtime API を 文字起こしモード で使用できます。
リアルタイムの音声変換セッション
リアルタイムセッションは、モデルと接続中のクライアントが状態を保持しながらやり取りする仕組みです。セッションの主な構成要素は次のとおりです。
- 使用するモデルや出力の生成に使う音声など、やり取りのパラメーターを制御する Session オブジェクト
- 現在のセッションで生成されたユーザー入力アイテムとモデル出力アイテムを表す Conversation
- モデルが生成し、Conversation に追加される音声またはテキストのアイテムである Responses
入力音声バッファと WebSockets
WebRTC を使用する場合、モデルと音声を送受信するために必要なメディア処理の多くは WebRTC API が支援します。
音声に WebSockets を使用する場合は、base64 でエンコードした音声を含む JSON イベントをサーバーに送信し、 入力音声バッファ を明示的に操作する必要があります。
これらすべての構成要素が、リアルタイムセッションを構成します。クライアントイベントでセッションの状態を更新し、サーバーイベントを監視してセッション内の状態変化に対応します。
セッションのライフサイクルイベント
WebRTC または WebSockets 経由でセッションを開始すると、サーバーはセッションの準備ができたことを示す session.created イベントを送信します。クライアントでは、session.update イベントで現在のセッション構成を更新できます。ほとんどのセッションプロパティはいつでも更新できます。ただし、モデルが音声出力に使用する voice は、セッション中にモデルが一度でも音声で応答すると変更できなくなります。リアルタイムセッションの最大継続時間は 60 分です。
次の例では、session.update クライアントイベントでセッションを更新します。これらのチャネルでクライアントイベントを送信する方法については、WebRTC または WebSocket のガイドを参照してください。
const event = {
type: "session.update",
session: {
type: "realtime",
model: "gpt-realtime-2.1",
// Lock the output to audio (set to ["text"] if you want text without audio)
output_modalities: ["audio"],
audio: {
input: {
format: {
type: "audio/pcm",
rate: 24000,
},
turn_detection: {
type: "semantic_vad",
},
},
output: {
format: {
type: "audio/pcm",
},
voice: "marin",
},
},
// Use a server-stored prompt by ID. Optionally pin a version and pass variables.
prompt: {
id: "pmpt_123", // your stored prompt ID
version: "89", // optional: pin a specific version
variables: {
city: "Paris", // example variable used by your prompt
},
},
// You can still set direct session fields; these override prompt fields if they overlap:
instructions:
"Speak clearly and briefly. Confirm understanding before taking actions.",
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));セッションが更新されると、サーバーはセッションの新しい状態を含む session.updated イベントを発行します。
| 関連するクライアントイベント | 関連するサーバーイベント |
|---|---|
テキストの入出力
Realtime モデルでテキストを生成するには、現在の会話にテキスト入力を追加し、モデルに応答の生成をリクエストして、その進行状況を示すサーバーイベントを監視します。テキストを生成するには、text モダリティを使用するようにセッションを設定する必要があります(デフォルトで設定されています)。
conversation.item.create クライアントイベントを使って、新しいテキストの会話アイテムを作成します。これは、REST API の Chat Completions でユーザーメッセージ(プロンプト)を送信する操作に相当します。
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "What Prince album sold the most copies?",
},
],
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));会話にユーザーメッセージを追加したら、response.create イベントを送信してモデルの応答を開始します。現在のセッションで音声とテキストの両方が有効になっている場合、モデルは音声とテキストの両方で応答します。テキストのみを生成する場合は、次のように response.create クライアントイベントの送信時に指定できます。
const event = {
type: "response.create",
response: {
output_modalities: ["text"],
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));応答が完全に終了すると、サーバーは response.done イベントを発行します。次に示すように、このイベントにはモデルが生成したテキストの全文が含まれます。
function handleEvent(message) {
const data = "data" in message ? message.data : message.toString();
const serverEvent = JSON.parse(data);
if (serverEvent.type === "response.done") {
console.log(serverEvent.response.output[0]);
}
}
// Listen for server messages (WebRTC)
dataChannel.addEventListener("message", handleEvent);
// Listen for server messages (WebSocket)
// ws.on("message", handleEvent);モデルの応答が生成される間、サーバーは複数のライフサイクルイベントを発行します。response.output_text.delta などのイベントを監視すると、応答の生成中にユーザーへリアルタイムでフィードバックを提供できます。サーバーが発行するイベントの全一覧は、以下の 関連するサーバーイベントに記載しています。おおよその発行順に、テキスト生成に関連するクライアント側のイベントとともに示しています。
| 関連するクライアントイベント | 関連するサーバーイベント |
|---|---|
音声の入出力
Realtime API の特に強力な機能のひとつが、テキストから音声、または音声からテキストへの変換を途中に挟まず、音声でモデルとやり取りできることです。これにより、音声インターフェースのレイテンシを低減でき、入力音声の声の調子や抑揚についても、モデルがより多くの情報を活用できます。
音声の選択肢
リアルタイムセッションでは、音声出力に使う声を、複数の組み込み音声から選んで設定できます。セッションの作成時(または response.create の実行時)に voice を設定すると、モデルの声を指定できます。現在選択できる音声は、alloy、ash、ballad、coral、echo、sage、shimmer、verse、marin、cedar です。セッション内でモデルが一度でも音声を出力すると、そのセッションの voice は変更できません。最良の品質を得るには、marin または cedar をおすすめします。
WebRTC での音声処理
WebRTC を使って Realtime API に接続する場合、Realtime API はクライアントとのピア接続として動作します。モデルの音声出力は、リモートメディアストリームとしてクライアントに配信されます。モデルへの音声入力は、音声デバイスから収集され(getUserMedia)、メディアストリームはトラックとしてピア接続に追加されます。
WebRTC 接続ガイドのサンプルコードでは、ブラウザ API を使ってローカルとリモートの両方の音声を構成する基本的な例を示しています。
// Create a peer connection
const pc = new RTCPeerConnection();
// Set up to play remote audio from the model
const audioEl = document.createElement("audio");
audioEl.autoplay = true;
pc.ontrack = (e) => (audioEl.srcObject = e.streams[0]);
// Add local audio track for microphone input in the browser
const ms = await navigator.mediaDevices.getUserMedia({
audio: true,
});
pc.addTrack(ms.getTracks()[0]);上記のスニペットで Realtime API とやり取りできますが、ほかにもさまざまなことが可能です。多様なユーザーインターフェースの例については、WebRTC サンプルリポジトリを参照してください。これらのサンプルの動作デモもこちらで試せます。
ブラウザでメディアキャプチャとストリームを使用すると、マイクのミュートやミュート解除、入力元デバイスの選択などができます。
WebRTC の音声に関するクライアントイベントとサーバーイベント
デフォルトでは、WebRTC クライアントは音声入力を送信する前に、Realtime API にクライアントイベントを送信する必要はありません。ローカルの音声トラックをピア接続に追加すれば、ユーザーはすぐに話し始められます。
ただし、ピア接続を通じてクライアントとサーバーの間で音声をやり取りしている間も、WebRTC クライアントはサーバーから複数のライフサイクルイベントを受信します。たとえば、次のようなイベントがあります。
- ローカルのメディアトラックで入力が送信されると、サーバーから
input_audio_buffer.speech_startedイベントを受信します。 - ローカルの音声入力が停止すると、
input_audio_buffer.speech_stoppedイベントを受信します。 - 生成中の音声の文字起こしの差分イベントを受信します。
- モデルが文字起こしを行い、応答の送信を完了すると、
response.doneイベントを受信します。
メディアストリーム用の WebRTC API を操作するだけで、必要な制御をすべて実現できる場合があります。ただし、音声の入出力に、より低レベルのインターフェースが必要になることもあります。詳しい説明と、音声入力をきめ細かく処理するために必要なイベントの一覧は、以下の WebSockets のセクションを参照してください。
WebSockets での音声処理
WebSocket で音声を送受信する場合、クライアントからのメディア送信とサーバーからのメディア受信に、もう少し実装が必要になります。以下の表は、WebSocket で音声を送受信するために必要な、WebSocket セッション中のイベントの流れを示しています。
以下のイベントはライフサイクル順に並べていますが、一部のイベント(delta イベントなど)は同時に発生する場合があります。
| ライフサイクルの段階 | クライアントイベント | サーバーイベント |
|---|---|---|
| セッションの初期化 | ||
| ユーザーの音声入力 | (音声メッセージ全体を送信) (音声をチャンク単位でストリーミング) (VAD が無効の場合に使用) (VAD が無効の場合に使用) |
|
| サーバーの音声出力 | (VAD が無効の場合に使用) |
|
サーバーへの音声入力のストリーミング
サーバーに音声入力をストリーミングするには、input_audio_buffer.append クライアントイベントを使用できます。このイベントでは、 Base64 でエンコードされた音声バイト列 をチャンクに分割し、ソケット経由で Realtime API に送信する必要があります。各チャンクのサイズは 15 MB 以下でなければなりません。
入力チャンクの形式は、セッション全体またはレスポンスごとに設定できます。
- セッション:
session.updateのsession.input_audio_format - レスポンス:
response.createのresponse.input_audio_format
import fs from "fs";
import decodeAudio from "audio-decode";
// Converts Float32Array of audio data to PCM16 ArrayBuffer
function floatTo16BitPCM(float32Array) {
const buffer = new ArrayBuffer(float32Array.length * 2);
const view = new DataView(buffer);
let offset = 0;
for (let i = 0; i < float32Array.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, float32Array[i]));
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
return buffer;
}
// Converts a Float32Array to base64-encoded PCM16 data
function base64EncodeAudio(float32Array) {
const arrayBuffer = floatTo16BitPCM(float32Array);
let binary = "";
let bytes = new Uint8Array(arrayBuffer);
const chunkSize = 0x8000; // 32KB chunk size
for (let i = 0; i < bytes.length; i += chunkSize) {
let chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode(...chunk);
}
return btoa(binary);
}
// Fills the audio buffer with the contents of three files,
// then asks the model to generate a response.
const files = [
"fixtures/sample1.wav",
"fixtures/sample2.wav",
"fixtures/sample3.wav",
];
for (const filename of files) {
const audioFile = fs.readFileSync(filename);
const audioBuffer = await decodeAudio(audioFile);
const channelData = audioBuffer.channelData[0];
const base64Chunk = base64EncodeAudio(channelData);
ws.send(
JSON.stringify({
type: "input_audio_buffer.append",
audio: base64Chunk,
})
);
}
ws.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
ws.send(JSON.stringify({ type: "response.create" }));音声メッセージ全体の送信
録音した音声全体を含む会話メッセージを作成することもできます。conversation.item.create クライアントイベントを使用して、input_audio コンテンツを含むメッセージを作成します。
const fullAudio = "<a base64-encoded string of audio bytes>";
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_audio",
audio: fullAudio,
},
],
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));WebSocket からの音声出力の処理
ウェブブラウザなどのクライアントで出力音声を再生する場合は、WebSocket よりも WebRTC の使用を推奨します。WebRTC は、不安定なネットワーク環境でもクライアントデバイスにメディアをより安定して送信できます。
一方、WebSocket を使用するサーバー間アプリケーションで音声出力を扱う場合は、モデルからの音声データを Base64 でエンコードしたチャンクを含む response.output_audio.delta イベントをリッスンする必要があります。これらのチャンクをバッファリングしてファイルに書き出すか、Twilio での通話などに即座にストリーミングする必要があります。
response.output_audio.done イベントと response.done イベントには、音声コンテンツの文字起こしのみが含まれ、音声データそのものは含まれない点に注意してください。実際のバイト列を取得するには、response.output_audio.delta イベントをリッスンする必要があります。
出力チャンクの形式は、セッション全体またはレスポンスごとに設定できます。
- セッション:
session.updateのsession.audio.output.format - レスポンス:
response.createのresponse.audio.output.format
function handleEvent(message) {
const serverEvent = JSON.parse(message.toString());
if (serverEvent.type === "response.output_audio.delta") {
// Access Base64-encoded audio chunks
// console.log(serverEvent.delta);
}
}
// Listen for server messages (WebSocket)
ws.on("message", handleEvent);画像入力
gpt-realtime-2 と gpt-realtime は画像入力にも対応しています。ユーザーメッセージのコンテンツの一部として画像を添付すると、モデルは画像の内容を踏まえて応答できます。
const base64Image = "<a base64-encoded string of image bytes>";
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_image",
image_url: `data:image/{format};base64,${base64Image}`,
},
],
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));音声区間検出
リアルタイムセッションでは、デフォルトで 音声区間検出(VAD) が有効になっています。API はユーザーが話し始めたタイミングと話し終えたタイミングを判断し、自動的に応答します。
VAD の設定方法について詳しくは、音声区間検出ガイドをご覧ください。
VAD の無効化
session.update クライアントイベントで turn_detection を null に設定すると、VAD を無効にできます。これは、プッシュトゥトークのように、音声入力を細かく制御したいインターフェースで便利です。
VAD が無効の場合、音声レスポンスを生成するには、クライアントが追加のクライアントイベントを手動で送信する必要があります。
input_audio_buffer.commitを手動で送信します。これにより、会話に新しいユーザー入力アイテムが作成されます。response.createを手動で送信し、モデルによる音声レスポンスの生成を開始します。- 新しいユーザー入力を開始する前に、
input_audio_buffer.clearを送信します。
VAD を維持したまま自動応答を無効化
VAD モードを有効にしたまま、レスポンスを生成するタイミングを手動で決めたい場合は、session.update クライアントイベントで turn_detection.interrupt_response と turn_detection.create_response を false に設定できます。これにより、VAD の動作はすべて維持されますが、新しいレスポンスは自動的に作成されなくなります。クライアントは response.create イベントでレスポンスの生成を手動で開始できます。
これは、モデレーション、入力検証、RAG パターンなど、入力を制御するために対話のレイテンシーが多少増えてもよい場合に便利です。
デフォルトの会話外でのレスポンスの作成
デフォルトでは、セッション中に生成されたすべてのレスポンスが、セッションの会話の状態(「デフォルトの会話」)に追加されます。ただし、セッションのデフォルトの会話のコンテキスト外でモデルのレスポンスを生成したり、複数のレスポンスを同時に生成したりしたい場合もあります。また、モデルがレスポンスを生成する際に考慮する会話アイテムを、たとえば直近の N ターンのみに限定するなど、より細かく制御したい場合もあります。
response.create クライアントイベントでレスポンスを作成する際に、response.conversation フィールドを文字列 none に設定すると、デフォルトの会話の状態に追加されない「アウトオブバンド」のレスポンスを生成できます。
アウトオブバンドのレスポンスを作成する際は、サーバーから送信されるイベントのうち、どれがそのレスポンスに対応するかを識別する方法も必要になるでしょう。モデルのレスポンスに metadata を指定すると、クライアントから送信したイベントに対して生成されているレスポンスを識別しやすくなります。
const prompt = `
Analyze the conversation so far. If it is related to support, output
"support". If it is related to sales, output "sales".
`;
const event = {
type: "response.create",
response: {
// Setting to "none" indicates the response is out of band
// and will not be added to the default conversation
conversation: "none",
// Set metadata to help identify responses sent back from the model
metadata: { topic: "classification" },
// Set any other available response fields
output_modalities: ["text"],
instructions: prompt,
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));これで、response.done サーバーイベントをリッスンすると、アウトオブバンドのレスポンスの結果を識別できます。
function handleEvent(message) {
const data = "data" in message ? message.data : message.toString();
const serverEvent = JSON.parse(data);
if (
serverEvent.type === "response.done" &&
serverEvent.response.metadata?.topic === "classification"
) {
// this server event pertained to our OOB model response
console.log(serverEvent.response.output[0]);
}
}
// Listen for server messages (WebRTC)
dataChannel.addEventListener("message", handleEvent);
// Listen for server messages (WebSocket)
// ws.on("message", handleEvent);レスポンス用のカスタムコンテキストの作成
デフォルトの会話、つまり現在の会話とは別に、モデルがレスポンスの生成に使用するカスタムコンテキストを構築することもできます。これには、response.create クライアントイベントの input 配列を使用します。新しい入力を使用することも、会話内の既存の入力アイテムを ID で参照することもできます。
const event = {
type: "response.create",
response: {
conversation: "none",
metadata: { topic: "pizza" },
output_modalities: ["text"],
// Create a custom input array for this request with whatever context
// is appropriate
input: [
// potentially include existing conversation items:
{
type: "item_reference",
id: "some_conversation_item_id",
},
{
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "Is it okay to put pineapple on pizza?",
},
],
},
],
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));コンテキストなしでのレスポンスの作成
ほかのすべての指示やコンテキストを無視したレスポンスを、デフォルトの会話に挿入することもできます。そのためには、input を空の配列に設定します。
const prompt = `
Say exactly the following:
I'm a little teapot, short and stout!
This is my handle, this is my spout!
`;
const event = {
type: "response.create",
response: {
// An empty input array removes existing context
input: [],
instructions: prompt,
},
};
// WebRTC data channel and WebSocket both have .send()
dataChannel.send(JSON.stringify(event));Function Calling
Realtime モデルは Function Calling にも対応しており、独自のコードを実行してモデルの機能を拡張できます。大まかな仕組みは次のとおりです。
- セッションを更新するときやレスポンスを作成するときに、モデルが呼び出せる関数の一覧を指定できます。
- モデルは入力を処理する際に関数の呼び出しが必要だと判断すると、関数呼び出しの引数を表すアイテムを会話に追加します。
- クライアントは関数呼び出しの引数を含む会話アイテムを検出すると、その引数を使って独自のコードを実行します。
- 独自のコードの実行が完了すると、クライアントは関数呼び出しの出力を含む新しい会話アイテムを作成し、モデルに応答を求めます。
モデルのユーザーに今日の星占いを提供する呼び出し可能な関数を追加して、実際の動作を見てみましょう。送信する必要があるクライアントイベントオブジェクトの構造と、それに対してサーバーが送信するイベントを示します。
呼び出し可能な関数の設定
まず、ユーザーの入力に応じて呼び出せる関数をモデルに提示する必要があります。利用可能な関数は、セッション単位でも、個々のレスポンス単位でも設定できます。
- セッション:
session.updateのsession.toolsプロパティ - レスポンス:
response.createのresponse.toolsプロパティ
次の例は、星占いを生成する関数を設定する session.update のクライアントイベントペイロードです。この関数は、星占いの対象となる星座を唯一の引数として受け取ります。
{
"type": "session.update",
"session": {
"tools": [
{
"type": "function",
"name": "generate_horoscope",
"description": "Give today's horoscope for an astrological sign.",
"parameters": {
"type": "object",
"properties": {
"sign": {
"type": "string",
"description": "The sign for the horoscope.",
"enum": [
"Aries",
"Taurus",
"Gemini",
"Cancer",
"Leo",
"Virgo",
"Libra",
"Scorpio",
"Sagittarius",
"Capricorn",
"Aquarius",
"Pisces"
]
}
},
"required": ["sign"]
}
}
],
"tool_choice": "auto"
}
}
関数とパラメーターの description フィールドは、関数を呼び出すかどうか、各パラメーターにどのデータを含めるかをモデルが判断するのに役立ちます。ユーザーが自分の星占いを知りたがっているとわかる入力を受け取ると、モデルは sign パラメーターを指定してこの関数を呼び出します。
モデルによる関数呼び出し要求の検出
モデルは入力に基づいて、最適なレスポンスを生成するために関数を呼び出すと判断することがあります。アプリケーションが conversation.item.create イベントで次の会話アイテムを追加し、その後レスポンスを作成する場合を考えてみましょう。
{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What is my horoscope? I am an aquarius."
}
]
}
}
続いて、response.create クライアントイベントでレスポンスを生成します。
{
"type": "response.create"
}
モデルはテキストや音声のレスポンスをすぐに返す代わりに、開発者のアプリケーション内の関数に渡す引数を含むレスポンスを生成します。response.function_call_arguments.delta サーバーイベントをリッスンすると、関数呼び出しの引数の更新をリアルタイムで受け取れますが、response.done にも関数の呼び出しに必要なすべてのデータが含まれます。
{
"type": "response.done",
"event_id": "event_AeqLA8iR6FK20L4XZs2P6",
"response": {
"object": "realtime.response",
"id": "resp_AeqL8XwMUOri9OhcQJIu9",
"status": "completed",
"status_details": null,
"output": [
{
"object": "realtime.item",
"id": "item_AeqL8gmRWDn9bIsUM2T35",
"type": "function_call",
"status": "completed",
"name": "generate_horoscope",
"call_id": "call_sHlR7iaFwQ2YQOqm",
"arguments": "{\"sign\":\"Aquarius\"}"
}
],
...
}
}
サーバーから送信される JSON から、モデルが独自の関数を呼び出そうとしていることを検出できます。
| プロパティ | Function Calling での役割 |
|---|---|
response.output[0].type | function_call に設定されている場合、このレスポンスに名前付きの関数を呼び出すための引数が含まれていることを示します。 |
response.output[0].name | 呼び出すように設定された関数の名前。この例では generate_horoscope |
response.output[0].arguments | 関数の引数を含む JSON 文字列です。この例では "{\"sign\":\"Aquarius\"}" です。 |
response.output[0].call_id | この関数呼び出しに対してシステムが生成した ID です。 関数呼び出しの結果をモデルに返すには、この ID が必要です。 |
この情報を使ってアプリケーション内でコードを実行し、星占いを生成できます。その結果をモデルに返すことで、モデルがレスポンスを生成できるようになります。
関数呼び出しの結果をモデルに提供
関数呼び出しの引数を含むレスポンスをモデルから受け取ると、アプリケーションはその関数呼び出しに応じたコードを実行できます。外部 API との通信やデータベースへのアクセスなど、必要な処理を自由に実装できます。
独自のコードの実行結果をモデルに渡す準備ができたら、conversation.item.create クライアントイベントで、その結果を含む新しい会話アイテムを作成できます。
{
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": "call_sHlR7iaFwQ2YQOqm",
"output": "{\"horoscope\": \"You will soon meet a new friend.\"}"
}
}
- 会話アイテムの型は
function_call_output item.call_idは、上記のresponse.doneイベントで受け取ったものと同じ IDitem.outputは、関数呼び出しの結果を含む JSON 文字列
関数呼び出しの結果を含む会話アイテムを追加したら、クライアントから再び response.create イベントを送信します。これにより、モデルは関数呼び出しから得たデータを使ってレスポンスを生成します。
{
"type": "response.create"
}
エラー処理
セッション中にサーバーでエラーが発生するたびに、サーバーは error イベントを送信します。これらのエラーは、アプリケーションが送信したクライアントイベントに起因する場合があります。
HTTP ではレスポンスがクライアントからのリクエストに暗黙的に対応付けられますが、ここでは、どのクライアントイベントがサーバーでエラーを引き起こしたかを特定するために、クライアントイベントの event_id プロパティを使う必要があります。以下のコードは、クライアントが未対応のイベントタイプを送信しようとする例で、この方法を示しています。
const event = {
event_id: "my_awesome_event",
type: "scooby.dooby.doo",
};
dataChannel.send(JSON.stringify(event));クライアントが送信したこのイベントは失敗し、次のようなエラーイベントが送信されます。
{
"type": "invalid_request_error",
"code": "invalid_value",
"message": "Invalid value: 'scooby.dooby.doo' ...",
"param": "type",
"event_id": "my_awesome_event"
}
割り込みと切り詰め
多くの音声アプリケーションでは、モデルが話している途中でユーザーが割り込めます。VAD が有効な場合、Realtime API はユーザーの発話を検出し、進行中のレスポンスをキャンセルして新しいレスポンスを開始することで、割り込みに対応します。ただし、自然に会話を続けられるようにするには、どこで割り込まれたかをモデルが把握できるようにする必要があります。たとえば、ユーザーが「最後に何と言いましたか?」と尋ねた場合に、この情報が必要です。モデルの最後のレスポンスから未再生の部分を会話内から削除することを、レスポンスの 切り詰め と呼びます。
WebRTC と SIP の接続では、サーバーが出力音声のバッファを管理するため、その時点で音声がどこまで再生されたかを把握しています。ユーザーが割り込むと、サーバーは未再生の音声を自動的に切り詰めます。
WebSocket 接続ではクライアントが音声再生を管理するため、クライアント側で再生を停止し、切り詰めを処理する必要があります。手順は次のとおりです。
- クライアントは、ユーザーが話し始めたことを示す新しい
input_audio_buffer.speech_startedイベントがサーバーから届くのを監視します。サーバーは進行中のモデルのレスポンスを自動的にキャンセルし、response.cancelledイベントを送信します。 - クライアントはこのイベントを検出したら、再生中のモデルの音声を直ちに停止する必要があります。また、割り込まれる前に最後の音声レスポンスがどこまで再生されたかを記録します。
- クライアントは
conversation.item.truncateイベントを送信し、モデルの最後のレスポンスのうち未再生の部分を会話から削除する必要があります。
以下に例を示します。
{
"type": "conversation.item.truncate",
"item_id": "item_1234", # this is the item ID of the model's last response
"content_index": 0,
"audio_end_ms": 1500 # truncate audio after 1.5 seconds
}
文字起こしも同様に切り詰められるでしょうか。Realtime モデルには、文字起こしと音声を正確に対応付けるための十分な情報がありません。そのため、conversation.item.truncate は指定された位置で音声を切り、未再生部分の文字起こしテキストを削除します。これにより未再生の音声を削除するという問題は解決しますが、切り詰めた文字起こしは提供されません。
プッシュツートーク
Realtime API はデフォルトで音声区間検出(VAD)を使用し、音声入力をきっかけにモデルのレスポンスを生成します。VAD を無効にし、モデルに音声入力を送るタイミングをアプリケーション側で制御すれば、プッシュツートーク方式で対話することもできます。たとえば、スペースバーを押している間は音声を収録し、離すとレスポンスの生成を開始します。アプリによっては、この方法が驚くほど効果的です。ユーザーが対話のタイミングを制御でき、VAD の検出ミスを避けられるうえ、VAD のタイムアウトを待たずに済むため、きびきびとした操作感になります。
プッシュツートークの実装は、WebSocket と WebRTC で少し異なります。Realtime API の WebSocket 接続では、すべてのイベントが同じチャネルで順序を保って送信されます。一方、WebRTC 接続では、音声と制御イベントに別々のチャネルを使用します。
WebSockets
WebSocket 接続でプッシュツートークを実装するには、クライアント側で音声再生の停止、割り込みの処理、新しいレスポンスの開始を行います。詳しい手順は次のとおりです。
session.updateイベントで"turn_detection": nullを設定し、VAD を無効にします。- ボタンを押したら、クライアントで音声の録音を開始します。
- モデルのレスポンスが進行中の場合は、
response.cancelイベントを送信してキャンセルします。 - モデルの出力音声を再生中の場合は、直ちに再生を停止し、
conversation.item.truncateイベントを送信して未再生の音声を会話から削除します。
- モデルのレスポンスが進行中の場合は、
- ボタンを離したら、音声を含む
input_audio_buffer.appendメッセージを送信して、新しい音声を入力バッファに追加します。 input_audio_buffer.commitイベントを送信します。これにより、入力バッファに書き込まれた音声がコミットされ、入力の文字起こしが有効な場合は開始されます。- 続いて、
response.createイベントでレスポンスの生成を開始します。
WebRTC と SIP
WebRTC でのプッシュトゥトークの実装も同様ですが、入力音声バッファを明示的にクリアする必要があります。手順は次のとおりです。
session.updateイベントで"turn_detection": nullを設定し、VAD を無効にします。- ボタンを押したら、
input_audio_buffer.clearイベントを送信して、以前の音声入力をクリアします。- モデルのレスポンスが進行中の場合は、
response.cancelイベントを送信してキャンセルします。 - モデルの出力音声を再生中の場合は、
output_audio_buffer.clearイベントを送信して未再生の音声をクリアします。これにより、会話も切り詰められます。
- モデルのレスポンスが進行中の場合は、
- ボタンを離したら、
input_audio_buffer.commitイベントを送信します。これにより、入力バッファに書き込まれた音声がコミットされ、入力音声の文字起こしが有効になっている場合は文字起こしが開始されます。 - 続いて、
response.createイベントで応答の生成を開始します。