OpenAI の Realtime API の使い方
以下の記事が面白かったので、簡単にまとめました。
1. Realtime API
「Realtime API」は、低遅延なマルチモーダル会話エクスペリエンスを構築するためのAPIです。現在、入出力の両方でテキスト・音声がサポートされており、Function Calling を利用することもできます。
特徴は次のとおりです。
2. クイックスタート
「Realtime API」は、「WebSocket」を介して通信するステートフルなイベントベースAPIです。
機能を紹介するデモアプリ「openai-realtime-console」が提供されています。このアプリを本番環境で使用することは推奨されませんが、「Realtime API」におけるイベントのフローを視覚化して理解するのに役立ちます。
3. WebSocket の接続
「WebSocket」の接続には、次のパラメータが必要です。
以下は、「WebSocket」を接続し、クライアント・サーバ間でイベントを送受信する例です。
import WebSocket from "ws";
// WebSocketの接続
const url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01";
const ws = new WebSocket(url, {
headers: {
"Authorization": "Bearer " + process.env.OPENAI_API_KEY,
"OpenAI-Beta": "realtime=v1",
},
});
// 接続完了時に呼ばれる
ws.on("open", function open() {
console.log("サーバに接続しました。");
// クライアントイベントを送信
ws.send(JSON.stringify({
type: "response.create",
response: {
modalities: ["text"],
instructions: "Please assist the user.",
}
}));
});
// サーバイベントを受信
ws.on("message", function incoming(message) {
console.log(JSON.parse(message.toString()));
});
クライアントイベントの送信例は、次のとおりです。
・ユーザーのテキストの送信
const event = {
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: 'Hello!'
}
]
}
};
ws.send(JSON.stringify(event));
ws.send(JSON.stringify({type: 'response.create'}));
・ユーザーの音声の送信
import fs from 'fs';
import decodeAudio from 'audio-decode';
// オーディオデータのFloat32Arrayを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;
}
// Float32Arrayをbase64エンコードされたPCM16データに変換
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.apply(null, chunk);
}
return btoa(binary);
}
// audio-decode を使用して生のオーディオバイトを取得
const myAudio = fs.readFileSync('./path/to/audio.wav');
const audioBuffer = await decodeAudio(myAudio);
const channelData = audioBuffer.getChannelData(0); // only accepts mono
const base64AudioData = base64EncodeAudio(channelData);
const event = {
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [
{
type: 'input_audio',
audio: base64AudioData
}
]
}
};
ws.send(JSON.stringify(event));
ws.send(JSON.stringify({type: 'response.create'}));
・ユーザーの音声のストリーミング
import fs from 'fs';
import decodeAudio from 'audio-decode';
// オーディオデータのFloat32Arrayを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;
}
// Float32Arrayをbase64エンコードされたPCM16データに変換
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.apply(null, chunk);
}
return btoa(binary);
}
// オーディオ バッファに3つのファイルの内容を入力し、
// モデルに応答を生成するように要求
const files = [
'./path/to/sample1.wav',
'./path/to/sample2.wav',
'./path/to/sample3.wav'
];
for (const filename of files) {
const audioFile = fs.readFileSync(filename);
const audioBuffer = await decodeAudio(audioFile);
const channelData = audioBuffer.getChannelData(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'}));
4. ステート
「Realtime API」は、「WebSocket」接続中に「ステート」を保持します。
「ステート」の構成要素は、次のとおりです。
4-1. セッション (Session)
「セッション」(Session) は、クライアント・サーバ間の接続です。
クライアントは「セッション」を作成すると、テキストとオーディオチャンクを含むJSON形式のイベントを送信します。サーバは音声と文字起こし Function Calling で応答します
セッションの設定はいつでも更新でき、応答ごとに更新することも可能です。セッションの主な設定は次のとおりです。
・Sessionオブジェクトの例
{
id: "sess_001",
object: "realtime.session",
...
model: "gpt-4o",
voice: "alloy",
...
}
4-2. 会話履歴 (Conversation) ・ 会話アイテム (Item)
「会話履歴」(Conversation) は「会話アイテム」(Item) のリストです。「セッション」作成時に1つ自動的に作成されます。
・Conversationオブジェクトの例
{
id: "conv_001",
object: "realtime.conversation",
}
「会話アイテム」(Item) の種類は、次の3つです。
クライアントは、「conversation.item.create」「onversation.item.delete」を使用して、「message」「function_call_output」を追加・削除できます。
・Itemオブジェクトの例
{
id: "msg_001",
object: "realtime.item",
type: "message",
status: "completed",
role: "user",
content: [{
type: "input_text",
text: "Hello, how's it going?"
}]
}
4-3. 入力オーディオバッファ (Input Audio Buffer)
サーバは、「入力オーディオバッファ」(Input Audio Buffer) でコミット前の入力オーディオバイトを保持します。
クライアントは、「input_audio_buffer.append」を使用して「入力オーディオバッファ」に「入力オーディオバイト」を追加できます。
「Server VAD mode」では、VADが発話終了を検出した時、保留中のオーディオは会話履歴に追加され、応答生成に使用されます。これが発生すると、「input_audio_buffer.speech_started」「input_audio_buffer.speech_stopped」「input_audio_buffer.committed」「 conversation.item.created」という一連のイベントが発行されます。
クライアントは、「input_audio_buffer.commit」を使用して、手動でコミットすることもできます。
4-4. 応答 (Response)
サーバからの「応答」(Response) のタイミングは、「turn_detection」の設定によって異なります。
4-5. 関数コール (Function Call)
クライアントは、「session.update」でサーバのデフォルト関数を設定するか、「response.create」で応答ごとの関数を設定できます。
サーバは「function_call」で応答します。
セッションへの関数の設定例は、次のとおりです。
{
tools: [
{
name: "get_weather",
description: "Get the weather at a given location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "Location to get the weather from",
},
scale: {
type: "string",
enum: ['celsius', 'farenheit']
},
},
required: ["location", "scale"],
},
},
...
]
}
5. 統合ガイド
5-1. オーディオフォーマット
「Realtime API」は現在、2つのフォーマットをサポートしています。
今後、さらに多くのフォーマットをサポートする予定です。
オーディオは、オーディオフレームのbase64エンコードされたチャンクである必要があります。
以下のPythonコードは「pydub」を使用して、入力オーディオバイトから会話アイテムを作成します。生のバイトにヘッダー情報が含まれていることを前提としています。Node.jsの場合、「audio-decode」には異なるファイル時間から生のオーディオトラックを読み込むためのユーティリティがあります。
import io
import json
from pydub import AudioSegment
def audio_to_item_create_event(audio_bytes: bytes) -> str:
# Load the audio file from the byte stream
audio = AudioSegment.from_file(io.BytesIO(audio_bytes))
# Resample to 24kHz mono pcm16
pcm_audio = audio.set_frame_rate(24000).set_channels(1).set_sample_width(2).raw_data
# Encode to base64 string
pcm_base64 = base64.b64encode(pcm_audio).decode()
event = {
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{
"type": "input_audio",
"audio": encoded_chunk
}]
}
}
return json.dumps(event)
5-2. インストラクション
「instructions」は、会話の先頭に追加されるシステムメッセージです。セッションまたはレスポンス毎回に設定することで、サーバの応答を制御できます。安全なデフォルトとして次の指示を推奨しますが、ユースケースに合った任意の指示を使用できます。
5-3. イベント送信
イベントを送信するには、イベントペイロードデータを含むJSON文字列を送信する必要があります。
// Make sure we are connected
ws.on('open', () => {
// Send an event
const event = {
type: 'conversation.item.create',
item: {
type: 'message',
role: 'user',
content: [
{
type: 'input_text',
text: 'Hello!'
}
]
}
};
ws.send(JSON.stringify(event));
});
5-4. イベント受信
イベントを受信するには、WebSocketのmessageイベントを受信し、結果をJSONとして解析します。
ws.on('message', data => {
try {
const event = JSON.parse(data);
console.log(event);
} catch (e) {
console.error(e);
}
});
5-5. 中断処理
サーバが音声で応答しているときに中断すると、モデルの推論が停止しますが、会話履歴に切り詰められた応答が保持されます。「server_vad」では、サーバ側のVADが再び入力音声を検出したときにこれが発生します。どちらのモードでも、クライアントは「response.cancel」を送信して、モデルを明示的に中断できます。
サーバはリアルタイムよりも速く音声を生成するため、サーバの中断ポイントはクライアント側の音声再生のポイントから外れます。つまり、サーバは、クライアントがユーザーに対して再生するよりも長い応答を生成している可能性があります。クライアントは、「conversation.item.truncate」を使用して、モデルの応答を中断前にクライアントが再生したものに切り詰めることができます。
5-6. Function Calling
クライアントは、「session.update」でサーバのデフォルト関数を設定するか、「response.create」で応答ごとの関数を設定できます。サーバは、適切な場合、「function_call」で応答します。関数は、「Chat Completions API」の形式で渡されます。
5-7. モデレーション
「Instructions」の一部としてガードレールを含める必要がありますが、堅牢な使用のためにはモデルの出力を検査することを推奨します。
「Realtime API」はテキストとオーディオを返すので、テキストを使用して、オーディオ出力を完全に再生するか、不要な出力が検出された場合に停止してデフォルトのメッセージに置き換えるかを確認できます。
5-8. エラー処理
すべてのエラーは、エラーイベント (「Server event "error"」参照) とともにサーバからクライアントに渡されます。これらのエラーは、クライアント イベントの形状が無効な場合に発生します。
これらのエラーは次のように処理できます。
const errorHandler = (error) => {
console.log('type', error.type);
console.log('code', error.code);
console.log('message', error.message);
console.log('param', error.param);
console.log('event_id', error.event_id);
};
ws.on('message', data => {
try {
const event = JSON.parse(data);
if (event.type === 'error') {
const { error } = event;
errorHandler(error);
}
} catch (e) {
console.error(e);
}
});
5-9. 履歴の追加
「Realtime API」を使用すると、クライアントは会話履歴を入力し、リアルタイムの音声セッションを開始できます。
唯一の制限は、クライアントがオーディオを含むアシスタントメッセージを作成できないことです。これを実行できるのはサーバのみです。
クライアントはテキストメッセージまたは「Function Calling」を追加できます。クライアントは、「conversation.item.create」を使用して会話履歴を作成できます。
5-10. 会話の再開
「Realtime API」は接続が終了した後、「セッション」と「会話履歴」はサーバ上に保存されません。ネットワーク状態が悪いなどの理由でクライアントが切断された場合は、新しい「セッション」を作成し、「会話履歴」に「会話アイテム」を挿入することで、会話を再開できます。
// Session 1
// [server] session.created
// [server] conversation.created
// ... various back and forth
//
// [connection ends due to client disconnect]
// Session 2
// [server] session.created
// [server] conversation.created
// Populate the conversation from memory:
{
type: "conversation.item.create",
item: {
type: "message"
role: "user",
content: [{
type: "audio",
audio: AudioBase64Bytes
}]
}
}
{
type: "conversation.item.create",
item: {
type: "message"
role: "assistant",
content: [
// Audio responses from a previous session cannot be populated
// in a new session. We suggest converting the previous message's
// transcript into a new "text" message so that similar content is
// exposed to the model.
{
type: "text",
text: "Sure, how can I help you?"
}
]
}
}
// Continue the conversation:
//
// [client] input_audio_buffer.append
// ... various back and forth
5-11. 会話履歴の自動切り捨て
会話が長時間続くと、入力トークン数がモデルの入力コンテキスト制限 (GPT-4oの場合は128kトークン) を超える可能性があります。この時点で、「Realtime API」は、コンテキストの最も重要な部分 (インストラクション、最新のメッセージなど) を保持するヒューリスティックベースのアルゴリズムに基づいて、会話を自動的に切り捨てます。これにより、会話は中断されることなく継続されます。
将来的には、この切り捨て動作をさらに制御できるようにする予定です。
6. イベント
6-1. クライアントイベント
WebSocketでクライアントからサーバに送信するイベントです。
6-2. サーバイベント
WebSocketでサーバからクライアントに送信するイベントです。
完全な仕様はAPIリファレンスで確認できます。