WebRTC または WebSocket で Realtime API に接続すると、gpt-realtime-2.1 などの Realtime モデルを呼び出し、音声変換を使って会話できます。処理を開始するにはクライアントイベントを送信し、Realtime API が実行した処理に応じて動作するにはサーバーイベントをリッスンします。
このガイドでは、音声やテキストの生成、画像入力、Function Calling などのモデル機能を使うためのイベントの流れと、Realtime セッションの状態をどう捉えるかを解説します。
モデルと会話する必要がなく、応答を求めない場合は、Realtime API を文字起こしモードで使用できます。
Realtime セッションでは、モデルと接続中のクライアントが状態を保持しながらやり取りします。セッションの主な構成要素は次のとおりです。
- セッションオブジェクトは、使用するモデル、音声出力に使う声など、やり取りに関する設定を管理します。
- 会話は、現在のセッションにおけるユーザーの入力アイテムと、モデルが生成した出力アイテムを表します。
- 応答は、モデルが生成して会話に追加される音声またはテキストのアイテムです。
入力音声バッファと WebSocket
WebRTC を使用する場合、モデルとの音声の送受信に必要なメディア処理の多くは WebRTC API が担います。
WebSocket で音声を扱う場合は、Base64 エンコードした音声を JSON イベントに含めてサーバーに送信し、入力音声バッファを明示的に操作する必要があります。
これらの要素を組み合わせたものが Realtime セッションです。クライアントイベントを使ってセッションの状態を更新し、サーバーイベントをリッスンしてセッション内の状態変化に対応します。

WebRTC または WebSocket でセッションを開始すると、サーバーは session.created イベントを送信し、セッションの準備が整ったことを通知します。クライアントでは、session.update イベントで現在のセッション設定を更新できます。ほとんどのプロパティはいつでも更新できますが、音声出力に使う voice は、そのセッションでモデルが一度でも音声で応答すると変更できなくなります。Realtime セッションの継続時間は最長 60 分です。
次の例では、session.update クライアントイベントでセッションを更新します。各接続でのクライアントイベントの送信方法は、WebRTC または WebSocket のガイドを参照してください。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41const 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",
rate: 24000,
},
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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35event = {
"type": "session.update",
"session": {
"type": "realtime",
"model": "gpt-realtime-2.1",
# Choose ["audio"] or ["text"]; do not combine them.
"output_modalities": ["audio"],
"audio": {
"input": {
"format": {
"type": "audio/pcm",
"rate": 24000,
},
"turn_detection": {"type": "semantic_vad"},
},
"output": {
"format": {
"type": "audio/pcmu",
},
"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.
},
},
# Direct session fields override prompt fields if they overlap.
"instructions": "Speak clearly and briefly. Confirm understanding before taking actions.",
},
}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40// Replace pmpt_123 and 89 with an existing saved prompt and version.
// The saved prompt must define the city variable.
if err := conn.Send(ctx, realtime.RealtimeClientEventParamOfSessionUpdate(realtime.RealtimeSessionCreateRequestParam{
Model: "gpt-realtime-2.1",
Prompt: responses.ResponsePromptParam{
ID: "pmpt_123",
Version: openai.String("89"),
Variables: map[string]responses.ResponsePromptVariableUnionParam{
"city": {
OfString: openai.String("Paris"),
},
},
},
OutputModalities: []string{"audio"},
Instructions: openai.String("Speak clearly and briefly. Confirm before taking action."),
Audio: realtime.RealtimeAudioConfigParam{
Input: realtime.RealtimeAudioConfigInputParam{
Format: realtime.RealtimeAudioFormatsUnionParam{
OfAudioPCM: &realtime.RealtimeAudioFormatsAudioPCMParam{
Rate: 24000,
},
},
TurnDetection: realtime.RealtimeAudioInputTurnDetectionUnionParam{
OfSemanticVad: &realtime.RealtimeAudioInputTurnDetectionSemanticVadParam{},
},
},
Output: realtime.RealtimeAudioConfigOutputParam{
Format: realtime.RealtimeAudioFormatsUnionParam{
OfAudioPCM: &realtime.RealtimeAudioFormatsAudioPCMParam{
Rate: 24000,
},
},
Voice: realtime.RealtimeAudioConfigOutputVoiceUnionParam{
OfString: openai.String("marin"),
},
},
},
})); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.core.JsonValue;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
// Replace pmpt_123 with your stored prompt ID.
var pcm =
RealtimeAudioFormats.AudioPcm.builder()
.type(RealtimeAudioFormats.AudioPcm.Type.AUDIO_PCM)
.rate(RealtimeAudioFormats.AudioPcm.Rate._24000)
.build();
connection.send(
RealtimeClientEvent.ofSessionUpdate(
SessionUpdateEvent.builder()
.session(
RealtimeSessionCreateRequest.builder()
.model("gpt-realtime-2.1")
.addOutputModality(RealtimeSessionCreateRequest.OutputModality.AUDIO)
.audio(
RealtimeAudioConfig.builder()
.input(
RealtimeAudioConfigInput.builder()
.format(pcm)
.turnDetection(
RealtimeAudioInputTurnDetection.SemanticVad.builder()
.build())
.build())
.output(
RealtimeAudioConfigOutput.builder()
.format(pcm)
.voice("marin")
.build())
.build())
.prompt(
com.openai.models.responses.ResponsePrompt.builder()
.id("pmpt_123")
.version("89")
.variables(
com.openai.models.responses.ResponsePrompt.Variables.builder()
.putAdditionalProperty("city", JsonValue.from("Paris"))
.build())
.build())
.instructions(
"Speak clearly and briefly. Confirm understanding before taking actions.")
.build())
.build()));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28# Replace the illustrative IDs and URLs below with your own resource values.
connection.session.update(
type: :realtime,
model: "gpt-realtime-2.1",
output_modalities: [:audio],
audio: {
input: {
format: {
type: :"audio/pcm",
rate: 24_000
},
turn_detection: { type: :semantic_vad }
},
output: {
format: {
type: :"audio/pcm",
rate: 24_000
},
voice: :marin
}
},
prompt: {
id: "pmpt_123",
version: "89",
variables: { city: "Paris" }
},
instructions: "Speak clearly and briefly. Confirm before taking action."
)
セッションの更新が完了すると、サーバーは新しいセッション状態を含む session.updated イベントを送信します。
Realtime モデルでテキストを生成するには、現在の会話にテキスト入力を追加し、モデルに応答の生成を要求します。サーバーから送られるイベントをリッスンすると、応答の生成状況を把握できます。テキストを生成するには、セッションの設定で text モダリティを有効にする必要があります(デフォルトで有効です)。
conversation.item.create クライアントイベントを使い、テキストを含む新しい会話アイテムを作成します。これは、REST API の Chat Completions でユーザーメッセージ(プロンプト)を送信する操作に似ています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const 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()
ws.send(JSON.stringify(event));
1
2
3
4
5
6
7
8
9
10
11
12
13
14event = {
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What Prince album sold the most copies?",
}
],
},
}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9
10if err := conn.Send(ctx, realtime.RealtimeClientEventParamOfConversationItemCreate(realtime.RealtimeConversationItemUserMessageParam{
Content: []realtime.RealtimeConversationItemUserMessageContentParam{
{
Text: openai.String("What Prince album sold the most copies?"),
Type: "input_text",
},
},
})); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.core.JsonMissing;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
connection.send(
RealtimeClientEvent.ofConversationItemCreate(
ConversationItemCreateEvent.builder()
.item(
RealtimeConversationItemUserMessage.builder()
.object_(JsonMissing.of())
.addContent(
RealtimeConversationItemUserMessage.Content.builder()
.type(RealtimeConversationItemUserMessage.Content.Type.INPUT_TEXT)
.text("What Prince album sold the most copies?")
.build())
.build())
.build()));
1
2
3
4
5
6
7using OpenAI.Realtime;
#pragma warning disable OPENAI002
RealtimeMessageItem item = RealtimeItem.CreateUserMessageItem("What Prince album sold the most copies?");
item.Id = Guid.NewGuid().ToString("N");
await session.SendCommandAsync(new RealtimeClientCommandConversationItemCreate(item), timeout.Token);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18connection.conversation.items.create(
type: :message,
role: :user,
content: [
{
type: :input_text,
text: "What is the weather like today?"
}
]
)
receipt = connection.find do |event|
raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)
next unless event.is_a?(OpenAI::Realtime::ConversationItemDone)
event.item.is_a?(OpenAI::Realtime::RealtimeConversationItemUserMessage)
end
raise "Connection closed before the conversation item was acknowledged" unless receipt
ユーザーメッセージを会話に追加したら、response.create イベントを送信してモデルの応答を開始します。出力モダリティは 1 つ選びます。audio は音声とその文字起こしを生成し、text はテキストのみを生成します。テキストのみを生成したい場合は、次の例のように response.create クライアントイベントの送信時に指定できます。
1
2
3
4
5
6
7
8
9const event = {
type: "response.create",
response: {
output_modalities: ["text"],
},
};
// WebRTC data channel and WebSocket both have .send()
ws.send(JSON.stringify(event));
1
2event = {"type": "response.create", "response": {"output_modalities": ["text"]}}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9if err := conn.Send(ctx, realtime.RealtimeClientEventUnionParam{
OfResponseCreate: &realtime.ResponseCreateEventParam{
Response: realtime.RealtimeResponseCreateParams{
OutputModalities: []string{"text"},
},
},
}); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
connection.send(
RealtimeClientEvent.ofResponseCreate(
ResponseCreateEvent.builder()
.response(
RealtimeResponseCreateParams.builder()
.addOutputModality(RealtimeResponseCreateParams.OutputModality.TEXT)
.build())
.build()));
1
2
3
4
5
6
7
8
9
10
11
12
13
14using OpenAI.Realtime;
#pragma warning disable OPENAI002
await session.SendCommandAsync(new RealtimeClientCommandResponseCreate
{
ResponseOptions = new()
{
OutputModalities =
{
RealtimeOutputModality.Text
}
}
}, timeout.Token);
1
2
3
4
5
6
7
8
9
10
11
12
13
14connection.response.create(
output_modalities: [:text],
instructions: "Respond with a concise text message."
)
response_done = connection.find do |event|
raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)
next unless event.is_a?(OpenAI::Realtime::ResponseDoneEvent)
raise "Response failed: #{event.response.status}" unless event.response.status == :completed
true
end
raise "Connection closed before the response completed" unless response_done
応答が完了すると、サーバーは response.done イベントを送信します。次の例のように、このイベントにはモデルが生成したテキストの全文が含まれます。
1
2
3
4
5
6
7
8
9
10
11
12
13function 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);
1
2
3
4def on_message(ws, message):
server_event = json.loads(message)
if server_event["type"] == "response.done":
print(server_event["response"]["output"][0])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31func receiveUntil(ctx context.Context, conn *realtime.RealtimeConnection, terminal string) (realtime.RealtimeServerEventUnion, error) {
for {
event, err := conn.Recv(ctx)
if err != nil {
return event, fmt.Errorf("waiting for %s: %w", terminal, err)
}
if event.Type == "error" {
return event, fmt.Errorf("Realtime API error: %s", event.OfRealtimeErrorEvent.Error.Message)
}
if event.Type == "response.done" && event.OfResponseDoneEvent.Response.Status != "completed" {
return event, fmt.Errorf("response ended with status %s", event.OfResponseDoneEvent.Response.Status)
}
if event.Type == "response.done" {
response := event.OfResponseDoneEvent.Response
fmt.Println("Response status:", response.Status)
for _, output := range response.Output {
item := output.OfRealtimeConversationItemAssistantMessage
if item.Type == "message" {
for _, content := range item.Content {
if content.Type == "output_text" {
fmt.Print(content.Text)
}
}
}
}
}
if event.Type == terminal {
return event, nil
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
static void handleEvent(RealtimeServerEvent event) {
event
.responseDone()
.ifPresent(
done -> {
if (!done.response()
.status()
.filter(RealtimeResponse.Status.COMPLETED::equals)
.isPresent())
throw new IllegalStateException(
"Response ended with status "
+ done.response().status()
+ ": "
+ done.response().statusDetails());
System.out.println(done.response().output());
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14using OpenAI.Realtime;
#pragma warning disable OPENAI002
internal static void HandleEvent(RealtimeServerUpdate update)
{
if (update is RealtimeServerUpdateResponseDone completion && completion.Response.Status != RealtimeResponseStatus.Completed)
throw new InvalidOperationException($"Response ended with status {completion.Response.Status}: {System.ClientModel.Primitives.ModelReaderWriter.Write(completion.Response)}");
if (update is RealtimeServerUpdateResponseDone done)
{
foreach (RealtimeItem item in done.Response.OutputItems)
Console.WriteLine(System.ClientModel.Primitives.ModelReaderWriter.Write(item));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14response_done = connection.find do |event|
next unless event.is_a?(OpenAI::Realtime::ResponseDoneEvent)
puts("Response status: #{event.response.status}")
Array(event.response.output).each do |item|
next unless item.is_a?(OpenAI::Realtime::RealtimeConversationItemAssistantMessage)
item.content.each do |content|
puts(content.text) if content.type == :output_text
end
end
true
end
raise "Connection closed before the response completed" unless response_done
モデルが応答を生成している間、サーバーは複数のライフサイクルイベントを送信します。response.output_text.delta などのイベントをリッスンすると、生成中の応答をリアルタイムでユーザーに提示できます。以下の表の関連するサーバーイベント欄に、サーバーが送信するイベントをおおよその送信順に示します。テキスト生成に関連するクライアントイベントも併記しています。
Realtime API の大きな特長の 1 つは、音声合成や音声認識の中間処理を挟まず、モデルと直接音声でやり取りできることです。これにより音声インターフェースの遅延を抑えられ、モデルは入力音声の口調や抑揚について、より多くの情報を活用できます。
Realtime セッションでは、音声出力に使う声を複数の組み込み音声から選べます。モデルの声は voice で指定します。セッションの作成時、または response.create の送信時に設定できます。現在選べる音声は alloy、ash、ballad、coral、echo、sage、shimmer、verse、marin、cedar です。セッション内でモデルが一度でも音声を出力すると、そのセッションの voice は変更できません。音質を重視する場合は marin または cedar をおすすめします。
WebRTC で Realtime API に接続すると、Realtime API はクライアントのピア接続の相手として動作します。モデルの音声出力はリモートメディアストリームとしてクライアントに届きます。モデルへの音声入力はオーディオデバイスから取得し(getUserMedia)、メディアストリームをトラックとしてピア接続に追加します。
WebRTC 接続ガイドから抜粋した次のコードは、ブラウザの API を使ってローカル音声とリモート音声の両方を設定する基本的な例です。
1
2
3
4
5
6
7
8
9
10
11
12
13// 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 クライアントが音声入力の送信前に Realtime API にクライアントイベントを送る必要はありません。ピア接続にローカル音声トラックを追加すれば、ユーザーはすぐに話し始められます。
一方、ピア接続を介してクライアントとサーバーの間で音声を送受信している間も、WebRTC クライアントにはサーバーからさまざまなライフサイクルイベントが届きます。たとえば、次のようなイベントがあります。
メディアストリーム用の WebRTC API を操作すれば、必要な制御をすべて行える場合があります。ただし、音声の入出力に、より低レベルのインターフェースが必要になることもあります。音声入力を細かく制御する方法と必要なイベントの一覧は、以下の WebSocket のセクションを参照してください。
WebSocket で音声を送受信する場合、クライアントからメディアを送信する処理と、サーバーからメディアを受信する処理をより細かく実装する必要があります。以下の表に、WebSocket セッション中に音声を送受信するために必要なイベントの流れを示します。
イベントはライフサイクルに沿って並べていますが、delta イベントなど、一部のイベントは並行して発生することがあります。
サーバーに音声入力をストリーミングするには、input_audio_buffer.append クライアントイベントを使います。このイベントでは、Base64 エンコードした音声バイト列をチャンクに分け、ソケット経由で Realtime API に送信します。各チャンクのサイズは 15 MB 以下にしてください。
session.audio.input.format は session.update で設定します。PCM の場合は {"type": "audio/pcm", "rate": 24000} を指定し、24 kHz のモノラル音声を送信します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77import 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) {
const sample = Math.max(-1, Math.min(1, float32Array[i]));
view.setInt16(
offset,
Math.round(sample * (sample < 0 ? 0x8000 : 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",
];
// Manual commit and response creation require automatic turn detection off.
ws.send(
JSON.stringify({
type: "session.update",
session: {
type: "realtime",
audio: { input: { turn_detection: null } },
},
})
);
for (const filename of files) {
const audioFile = fs.readFileSync(filename);
const audioBuffer = await decodeAudio(audioFile);
if (
audioBuffer.sampleRate !== 24000 ||
audioBuffer.channelData.length !== 1
) {
throw new Error("Expected 24 kHz mono audio: " + filename);
}
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",
response: { output_modalities: ["audio"] },
})
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import base64
import json
import struct
import soundfile as sf
from websocket import create_connection
# ... create websocket-client named ws ...
def float_to_16bit_pcm(float32_array):
clipped = [max(-1.0, min(1.0, x)) for x in float32_array]
pcm16 = b"".join(struct.pack("<h", min(32767, int(x * 32768))) for x in clipped)
return pcm16
def base64_encode_audio(float32_array):
pcm_bytes = float_to_16bit_pcm(float32_array)
encoded = base64.b64encode(pcm_bytes).decode("ascii")
return encoded
files = ["./path/to/sample1.wav", "./path/to/sample2.wav", "./path/to/sample3.wav"]
for filename in files:
data, samplerate = sf.read(filename, dtype="float32")
if samplerate != 24000 or data.ndim != 1:
raise ValueError(f"Expected 24 kHz mono audio: {filename}")
base64_chunk = base64_encode_audio(data)
# Send the client event
event = {"type": "input_audio_buffer.append", "audio": base64_chunk}
ws.send(json.dumps(event))
1
2
3
4
5
6
7// audio contains mono PCM16 samples at 24 kHz.
for start := 0; start < len(audio); start += 9600 {
end := min(start+9600, len(audio))
if err := conn.Send(ctx, realtime.RealtimeClientEventParamOfInputAudioBufferAppend(base64.StdEncoding.EncodeToString(audio[start:end]))); err != nil {
return err
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
import java.util.Base64;
// Call for each PCM16, 24 kHz, mono chunk supplied by your audio source.
static void appendAudio(RealtimeConnection connection, byte[] pcmAudio) throws Exception {
String base64Audio = Base64.getEncoder().encodeToString(pcmAudio);
send(
connection,
RealtimeClientEvent.ofInputAudioBufferAppend(
InputAudioBufferAppendEvent.builder().audio(base64Audio).build()));
}
// Retry only explicit admission rejections; these guarantee nothing was sent.
private static void send(RealtimeConnection connection, RealtimeClientEvent event)
throws Exception {
long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(5);
while (true) {
try {
connection.send(event);
return;
} catch (com.openai.core.http.WebSocketWriteNotAttempted.Busy busy) {
if (System.nanoTime() >= deadline) throw busy;
Thread.sleep(10);
}
}
}
1
2
3
4
5
6
7
8
9using OpenAI.Realtime;
#pragma warning disable OPENAI002
// Call for each PCM16, 24 kHz, mono chunk supplied by your audio source.
internal static async Task AppendAudioAsync(RealtimeSessionClient session, BinaryData pcmAudio, CancellationToken cancellationToken)
{
await session.SendCommandAsync(new RealtimeClientCommandInputAudioBufferAppend(pcmAudio), cancellationToken);
}
1
2
3
4
5File.open("speech.pcm", "rb") do |audio|
while (chunk = audio.read(9_600))
connection.input_audio_buffer.append_bytes(chunk)
end
end
録音した音声全体を 1 つの会話メッセージとして送信することもできます。conversation.item.create クライアントイベントを使い、input_audio コンテンツを含むメッセージを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const 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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fullAudio = "<a base64-encoded string of audio bytes>"
event = {
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_audio",
"audio": fullAudio,
}
],
},
}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9
10if err := conn.Send(ctx, realtime.RealtimeClientEventParamOfConversationItemCreate(realtime.RealtimeConversationItemUserMessageParam{
Content: []realtime.RealtimeConversationItemUserMessageContentParam{
{
Audio: openai.String(base64.StdEncoding.EncodeToString(audio)),
Type: "input_audio",
},
},
})); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.core.JsonMissing;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
import java.util.Base64;
// Pass the complete PCM16, 24 kHz, mono recording from your application.
static void sendAudioItem(RealtimeConnection connection, byte[] pcmAudio) throws Exception {
String fullAudio = Base64.getEncoder().encodeToString(pcmAudio);
var event =
RealtimeClientEvent.ofConversationItemCreate(
ConversationItemCreateEvent.builder()
.item(
RealtimeConversationItemUserMessage.builder()
.object_(JsonMissing.of())
.addContent(
RealtimeConversationItemUserMessage.Content.builder()
.type(RealtimeConversationItemUserMessage.Content.Type.INPUT_AUDIO)
.audio(fullAudio)
.build())
.build())
.build());
// Retry only explicit admission rejections; these guarantee nothing was sent.
long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(5);
while (true) {
try {
connection.send(event);
return;
} catch (com.openai.core.http.WebSocketWriteNotAttempted.Busy busy) {
if (System.nanoTime() >= deadline) throw busy;
Thread.sleep(10);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13using OpenAI.Realtime;
#pragma warning disable OPENAI002
// Pass the complete PCM16, 24 kHz, mono recording from your application.
internal static async Task<string> SendAudioItemAsync(RealtimeSessionClient session, BinaryData pcmAudio, CancellationToken cancellationToken)
{
RealtimeMessageItem item = RealtimeItem.CreateUserMessageItem([new RealtimeInputAudioMessageContentPart(pcmAudio)]);
string itemId = Guid.NewGuid().ToString("N");
item.Id = itemId;
await session.SendCommandAsync(new RealtimeClientCommandConversationItemCreate(item), cancellationToken);
return itemId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20audio = Base64.strict_encode64(File.binread("speech.pcm"))
connection.conversation.items.create(
type: :message,
role: :user,
content: [
{
type: :input_audio,
audio: audio
}
]
)
receipt = connection.find do |event|
raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)
event.is_a?(OpenAI::Realtime::ConversationItemDone) &&
event.item.is_a?(OpenAI::Realtime::RealtimeConversationItemUserMessage)
end
raise "Connection closed before the audio item was acknowledged" unless receipt
ウェブブラウザなどのクライアント側で出力音声を再生する場合は、WebSocket よりも WebRTC をおすすめします。ネットワークの状態が不安定でも、WebRTC の方が安定してクライアントデバイスにメディアを送信できます。
サーバー間で通信するアプリケーションで WebSocket を使って音声出力を扱うには、response.output_audio.delta イベントをリッスンします。このイベントには、モデルが出力した音声データのチャンクが Base64 エンコードされて含まれています。チャンクはバッファに蓄積してファイルに書き出すか、Twilio を使った通話などの送信先にそのままストリーミングします。
response.output_audio.done と response.done の各イベントに含まれるのは音声の文字起こしのみで、音声データ自体は含まれません。音声のバイト列を取得するには、response.output_audio.delta イベントをリッスンしてください。
出力チャンクの形式は、セッション全体に対して設定することも、応答ごとに設定することもできます。
1
2
3
4
5
6
7
8
9
10function handleEvent(message) {
const serverEvent = JSON.parse(message.toString());
if (serverEvent.type === "response.output_audio.delta") {
const audio = Buffer.from(serverEvent.delta, "base64");
console.log(`Received ${audio.length} audio bytes`);
}
}
// Listen for server messages (WebSocket)
ws.on("message", handleEvent);
1
2
3
4
5def on_message(ws, message):
server_event = json.loads(message)
if server_event["type"] == "response.output_audio.delta":
# Access Base64-encoded audio chunks:
print(server_event["delta"])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24func receiveUntil(ctx context.Context, conn *realtime.RealtimeConnection, terminal string) (realtime.RealtimeServerEventUnion, error) {
for {
event, err := conn.Recv(ctx)
if err != nil {
return event, fmt.Errorf("waiting for %s: %w", terminal, err)
}
if event.Type == "response.output_audio.delta" {
audio, err := base64.StdEncoding.DecodeString(event.OfResponseAudioDeltaEvent.Delta)
if err != nil {
return event, err
}
fmt.Printf("Received %d audio bytes\n", len(audio))
}
if event.Type == "error" {
return event, fmt.Errorf("Realtime API error: %s", event.OfRealtimeErrorEvent.Error.Message)
}
if event.Type == "response.done" && event.OfResponseDoneEvent.Response.Status != "completed" {
return event, fmt.Errorf("response ended with status %s", event.OfResponseDoneEvent.Response.Status)
}
if event.Type == terminal {
return event, nil
}
}
}
1
2
3
4
5
6
7
8
9import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
static void handleEvent(RealtimeServerEvent event) {
event.responseOutputAudioDelta().ifPresent(delta -> System.out.println(delta.delta()));
}
1
2
3
4
5
6
7
8
9using OpenAI.Realtime;
#pragma warning disable OPENAI002
internal static void HandleEvent(RealtimeServerUpdate update)
{
if (update is RealtimeServerUpdateResponseOutputAudioDelta delta)
Console.WriteLine(Convert.ToBase64String(delta.Delta.ToArray()));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14response_done = connection.find do |event|
case event
when OpenAI::Realtime::ResponseAudioDeltaEvent
audio_bytes = Base64.strict_decode64(event.delta)
puts("Received #{audio_bytes.bytesize} audio bytes")
when OpenAI::Realtime::RealtimeErrorEvent
raise event.error.message
when OpenAI::Realtime::ResponseDoneEvent
raise "Response failed: #{event.response.status}" unless event.response.status == :completed
true
end
end
raise "Connection closed before the response completed" unless response_done
gpt-realtime-2 と gpt-realtime は画像入力にも対応しています。ユーザーメッセージにコンテンツの一部として画像を添付すると、モデルは画像の内容を踏まえて応答できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const 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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24if err := conn.Send(ctx, realtime.RealtimeClientEventParamOfConversationItemCreate(realtime.RealtimeConversationItemUserMessageParam{
Content: []realtime.RealtimeConversationItemUserMessageContentParam{
{
ImageURL: openai.String("data:image/png;base64," + base64.StdEncoding.EncodeToString(image)),
Type: "input_image",
},
{
Text: openai.String("Describe this image."),
Type: "input_text",
},
},
})); err != nil {
return err
}
if err := conn.Send(ctx, realtime.RealtimeClientEventUnionParam{
OfResponseCreate: &realtime.ResponseCreateEventParam{
Response: realtime.RealtimeResponseCreateParams{
OutputModalities: []string{"text"},
},
},
}); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26encoded_image = Base64.strict_encode64(File.binread("image.png"))
connection.conversation.items.create(
type: :message,
role: :user,
content: [
{
type: :input_image,
image_url: "data:image/png;base64,#{encoded_image}"
},
{
type: :input_text,
text: "Describe this image."
}
]
)
connection.response.create(output_modalities: [:text])
completion = connection.find do |event|
raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)
event.is_a?(OpenAI::Realtime::ResponseDoneEvent)
end
unless completion.is_a?(OpenAI::Realtime::ResponseDoneEvent) && completion.response.status == :completed
raise "The Realtime response did not complete"
end
Realtime セッションでは、デフォルトで 音声区間検出(VAD) が有効です。API がユーザーの発話の開始と終了を検出し、自動的に応答します。
VAD の設定方法については、音声区間検出のガイドを参照してください。
turn_detection を null に設定する session.update クライアントイベントを送ると、VAD を無効にできます。プッシュトゥトークのように、音声入力を細かく制御したいインターフェースで役立ちます。
VAD が無効の場合、音声応答を生成するには、クライアント側で次のイベントを明示的に送信する必要があります。
VAD を使いつつ応答を生成するタイミングを自分で決めたい場合は、turn_detection.interrupt_response と turn_detection.create_response を false に設定する session.update クライアントイベントを送信します。VAD の動作は維持されますが、新しい応答は自動生成されません。クライアントから response.create イベントを送ることで、明示的に応答を開始できます。
この設定は、モデレーションや入力検証、RAG を実装する際に、やり取りの遅延が多少増えても入力を制御したい場合に役立ちます。
デフォルトでは、セッション中に生成されたすべての応答が、そのセッションの会話状態(「デフォルトの会話」)に追加されます。ただし、デフォルトの会話のコンテキストから切り離してモデルの応答を生成したい場合や、複数の応答を並行して生成したい場合もあります。また、直近の N ターンだけを対象にするなど、応答の生成時にモデルが参照する会話アイテムを細かく制御したいこともあります。
デフォルトの会話状態に追加されない「帯域外」の応答を生成するには、response.conversation フィールドに文字列 none を設定し、response.create クライアントイベントで応答を作成します。
帯域外の応答を作成する際は、その応答に関連するサーバーイベントを識別できるようにしておくと便利です。モデルの応答に metadata を指定すると、クライアントから送ったイベントに対してどの応答が生成されているかを識別できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const 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()
ws.send(JSON.stringify(event));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20prompt = """
Analyze the conversation so far. If it is related to support, output
"support". If it is related to sales, output "sales".
"""
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,
},
}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9
10
11
12
13
14if err := conn.Send(ctx, realtime.RealtimeClientEventUnionParam{
OfResponseCreate: &realtime.ResponseCreateEventParam{
Response: realtime.RealtimeResponseCreateParams{
Instructions: openai.String("\nAnalyze the conversation so far. If it is related to support, output\n\"support\". If it is related to sales, output \"sales\".\n"),
Metadata: shared.Metadata{
"topic": "classification",
},
Conversation: "none",
OutputModalities: []string{"text"},
},
},
}); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.core.JsonValue;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
connection.send(
RealtimeClientEvent.ofResponseCreate(
ResponseCreateEvent.builder()
.response(
RealtimeResponseCreateParams.builder()
.conversation("none")
.metadata(
RealtimeResponseCreateParams.Metadata.builder()
.putAdditionalProperty("topic", JsonValue.from("classification"))
.build())
.addOutputModality(RealtimeResponseCreateParams.OutputModality.TEXT)
.instructions(
"Analyze the conversation so far. If it is related to support, output \"support\". If it is related to sales, output \"sales\".")
.build())
.build()));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20using OpenAI.Realtime;
#pragma warning disable OPENAI002
await session.SendCommandAsync(new RealtimeClientCommandResponseCreate
{
ResponseOptions = new()
{
DefaultConversationConfiguration = RealtimeResponseDefaultConversationConfiguration.None,
Metadata = new Dictionary<string, BinaryData>
{
["topic"] = BinaryData.FromObjectAsJson("classification")
},
OutputModalities =
{
RealtimeOutputModality.Text
},
Instructions = "Analyze the conversation so far. If it is related to support, output \"support\". If it is related to sales, output \"sales\"."
}
}, timeout.Token);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16connection.response.create(
conversation: :none,
metadata: { topic: "classification" },
output_modalities: [:text],
instructions: "Classify the conversation as support or sales."
)
response_done = connection.find do |event|
raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)
next unless event.is_a?(OpenAI::Realtime::ResponseDoneEvent)
raise "Response failed: #{event.response.status}" unless event.response.status == :completed
true
end
raise "Connection closed before the response completed" unless response_done
これで、response.done サーバーイベントをリッスンしたときに、帯域外応答の結果を識別できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function 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"
) {
const { response } = serverEvent;
if (response.status !== "completed" || response.output.length === 0) {
throw new Error(
"Classification response ended with status " + response.status
);
}
console.log(response.output[0]);
}
}
// Listen for server messages (WebRTC)
// dataChannel.addEventListener("message", handleEvent);
// Listen for server messages (WebSocket)
ws.on("message", handleEvent);
1
2
3
4
5
6
7
8
9
10
11
12def on_message(ws, message):
server_event = json.loads(message)
if server_event["type"] != "response.done":
return
response = server_event["response"]
if (response.get("metadata") or {}).get("topic") != "classification":
return
if response["status"] != "completed" or not response["output"]:
raise RuntimeError(
f"Classification response ended with status {response['status']}"
)
print(response["output"][0])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34func receiveUntil(ctx context.Context, conn *realtime.RealtimeConnection, terminal string) (realtime.RealtimeServerEventUnion, error) {
for {
event, err := conn.Recv(ctx)
if err != nil {
return event, fmt.Errorf("waiting for %s: %w", terminal, err)
}
if event.Type == "error" {
return event, fmt.Errorf("Realtime API error: %s", event.OfRealtimeErrorEvent.Error.Message)
}
if event.Type == "response.done" && event.OfResponseDoneEvent.Response.Metadata["topic"] != "classification" {
continue
}
if event.Type == "response.done" && event.OfResponseDoneEvent.Response.Status != "completed" {
return event, fmt.Errorf("response ended with status %s", event.OfResponseDoneEvent.Response.Status)
}
if event.Type == "response.done" {
response := event.OfResponseDoneEvent.Response
fmt.Println("Response status:", response.Status)
for _, output := range response.Output {
item := output.OfRealtimeConversationItemAssistantMessage
if item.Type == "message" {
for _, content := range item.Content {
if content.Type == "output_text" {
fmt.Print(content.Text)
}
}
}
}
}
if event.Type == terminal {
return event, nil
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.core.JsonValue;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
static boolean handleEvent(RealtimeServerEvent event) {
if (event.responseDone().isEmpty()) return false;
var response = event.responseDone().orElseThrow().response();
if (!response
.metadata()
.map(m -> m._additionalProperties().get("topic"))
.flatMap(JsonValue::asString)
.filter("classification"::equals)
.isPresent()) return false;
if (!response.status().filter(RealtimeResponse.Status.COMPLETED::equals).isPresent())
throw new IllegalStateException(
"Response ended with status " + response.status() + ": " + response.statusDetails());
System.out.println(response.output());
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16using OpenAI.Realtime;
#pragma warning disable OPENAI002
internal static bool HandleEvent(RealtimeServerUpdate update)
{
if (update is not RealtimeServerUpdateResponseDone done ||
!done.Response.Metadata.TryGetValue("topic", out BinaryData? topic) ||
topic.ToObjectFromJson<string>() != "classification")
return false;
if (done.Response.Status != RealtimeResponseStatus.Completed)
throw new InvalidOperationException($"Response ended with status {done.Response.Status}: {System.ClientModel.Primitives.ModelReaderWriter.Write(done.Response)}");
foreach (RealtimeItem item in done.Response.OutputItems)
Console.WriteLine(System.ClientModel.Primitives.ModelReaderWriter.Write(item));
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15response_done = connection.find do |event|
next unless event.is_a?(OpenAI::Realtime::ResponseDoneEvent)
next unless event.response.metadata&.fetch(:topic, nil) == "classification"
puts("Classification response completed: #{event.response.status}")
Array(event.response.output).each do |item|
next unless item.is_a?(OpenAI::Realtime::RealtimeConversationItemAssistantMessage)
item.content.each do |content|
puts("Classification: #{content.text}") if content.type == :output_text
end
end
true
end
raise "Connection closed before the response completed" unless response_done
現在のデフォルトの会話とは別に、モデルが応答の生成に使うコンテキストを組み立てることもできます。input 配列を response.create クライアントイベントに指定してください。新しい入力を指定することも、会話内の既存の入力アイテムを ID で参照することもできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45const existingItemId = "item_pizza";
ws.send(
JSON.stringify({
type: "conversation.item.create",
item: {
id: existingItemId,
type: "message",
role: "user",
content: [
{ type: "input_text", text: "We are ordering pizza for dinner." },
],
},
})
);
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: existingItemId,
},
{
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()
ws.send(JSON.stringify(event));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import json
existing_item_id = "item_pizza"
ws.send(
json.dumps(
{
"type": "conversation.item.create",
"item": {
"id": existing_item_id,
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "We are ordering pizza for dinner."}
],
},
}
)
)
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": existing_item_id},
# include new content as well
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Is it okay to put pineapple on pizza?",
}
],
},
],
},
}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34existing_item_id = "item_order"
connection.conversation.items.create(
id: existing_item_id,
type: :message,
role: :user,
content: [
{
type: :input_text,
text: "Order ORD-123 has not arrived."
}
]
)
connection.response.create(
conversation: :none,
metadata: { topic: "classification" },
output_modalities: [:text],
input: [
{
type: :item_reference,
id: existing_item_id
},
{
type: :message,
role: :user,
content: [
{
type: :input_text,
text: "Classify this issue: my order is late."
}
]
}
]
)
他のすべての指示とコンテキストを無視して生成した応答を、デフォルトの会話に追加することもできます。その場合は input に空の配列を設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const 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: [],
output_modalities: ["text"],
instructions: prompt,
},
};
// WebRTC data channel and WebSocket both have .send()
ws.send(JSON.stringify(event));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17prompt = """
Say exactly the following:
I'm a little teapot, short and stout!
This is my handle, this is my spout!
"""
event = {
"type": "response.create",
"response": {
"output_modalities": ["text"],
# An empty input array removes all prior context
"input": [],
"instructions": prompt,
},
}
ws.send(json.dumps(event))
1
2
3
4
5
6
7
8
9
10
11if err := conn.Send(ctx, realtime.RealtimeClientEventUnionParam{
OfResponseCreate: &realtime.ResponseCreateEventParam{
Response: realtime.RealtimeResponseCreateParams{
Instructions: openai.String("\nSay exactly the following:\nI'm a little teapot, short and stout!\nThis is my handle, this is my spout!\n"),
Input: []realtime.ConversationItemUnionParam{},
OutputModalities: []string{"text"},
},
},
}); err != nil {
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;
import java.util.List;
connection.send(
RealtimeClientEvent.ofResponseCreate(
ResponseCreateEvent.builder()
.response(
RealtimeResponseCreateParams.builder()
.addOutputModality(RealtimeResponseCreateParams.OutputModality.TEXT)
.input(List.of())
.instructions(
"Say exactly the following: I'm a little teapot, short and stout! This is my handle, this is my spout!")
.build())
.build()));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17using OpenAI.Realtime;
#pragma warning disable OPENAI002
RealtimeResponseOptions options = new()
{
OutputModalities = { RealtimeOutputModality.Text },
Instructions = "Say exactly the following: I'm a little teapot, short and stout! This is my handle, this is my spout!"
};
options.InputItems.Clear();
// An explicitly empty list excludes conversation context.
await session.SendCommandAsync(new RealtimeClientCommandResponseCreate
{
ResponseOptions = options
}, timeout.Token);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15connection.response.create(
input: [],
output_modalities: [:text],
instructions: "Generate a concise greeting without conversation context."
)
response_done = connection.find do |event|
raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)
next unless event.is_a?(OpenAI::Realtime::ResponseDoneEvent)
raise "Response failed: #{event.response.status}" unless event.response.status == :completed
true
end
raise "Connection closed before the response completed" unless response_done
Realtime モデルは Function Calling にも対応しています。独自のコードを実行することで、モデルの機能を拡張できます。処理の流れは次のとおりです。
- セッションの更新時または応答の作成時に、モデルが呼び出せる関数の一覧を指定できます。
- 入力を処理する際、関数を呼び出す必要があるとモデルが判断すると、その呼び出しに使う引数を表すアイテムを会話に追加します。
- クライアントは、関数呼び出しの引数を含む会話アイテムを検出し、その引数を使って独自のコードを実行します。
- コードの実行が完了したら、クライアントは関数の出力を含む新しい会話アイテムを作成し、モデルに応答を要求します。
ユーザーに今日の星占いを返す関数を追加する例で、実際の流れを見てみましょう。クライアントが送信するイベントオブジェクトの形式と、それに対してサーバーが送信するイベントを示します。
まず、ユーザー入力に応じてモデルが呼び出せる関数を指定します。使用できる関数はセッション単位でも、個々の応答単位でも設定できます。
以下は、session.update クライアントイベントのペイロードの例です。占いたい星座を唯一の引数として受け取り、その星座の運勢を生成する関数を設定します。
session.update
12345678910111213141516171819202122232425262728293031323334353637{
"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 イベントで次の会話アイテムを追加し、応答を作成するとします。
12345678910111213{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What is my horoscope? I am an aquarius."
}
]
}
}
続いて、response.create クライアントイベントを送信して応答を生成します。
123{
"type": "response.create"
}
この場合、モデルはすぐにテキストや音声で答える代わりに、開発者のアプリケーション内の関数に渡す引数を含む応答を生成します。response.function_call_arguments.delta サーバーイベントをリッスンすると、引数の更新をリアルタイムで受け取れます。response.done にも関数の呼び出しに必要なデータがすべて含まれています。
response.done
123456789101112131415161718192021{
"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 クライアントイベントで、その結果を含む新しい会話アイテムを作成します。
12345678{
"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 イベントで受け取った ID と同じ値を指定します。
item.output は、関数の実行結果を含む JSON 文字列です。
関数の実行結果を含む会話アイテムを追加したら、クライアントから再び response.create イベントを送信します。これにより、モデルは関数の実行結果を使って応答します。
123{
"type": "response.create"
}
error イベントは、セッション中にサーバーでエラーが発生するたびに送信されます。エラーの原因が、アプリケーションから送信したクライアントイベントにある場合もあります。
HTTP ではレスポンスがクライアントのリクエストに自動的に対応付けられますが、Realtime API では、どのクライアントイベントがサーバーのエラーを引き起こしたかを特定するために、クライアントイベントに event_id プロパティを指定する必要があります。次のコードは、クライアントがサポートされていない種類のイベントを送信しようとする例です。
1
2
3
4
5
6const event = {
event_id: "my_awesome_event",
type: "scooby.dooby.doo",
};
dataChannel.send(JSON.stringify(event));
このクライアントイベントの処理に失敗すると、サーバーは次のようなエラーイベントを送信します。
1234567891011{
"type": "error",
"event_id": "event_error_123",
"error": {
"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 を受信したら、モデルの音声の再生を直ちに停止し、そのアイテムの再生待ちの音声をすべて破棄します。ユーザーがそのアイテムの音声を実際に聞いた時間をミリ秒単位で記録します。
conversation.item.truncate イベントに、中断されたアイテムの ID と音声コンテンツのインデックスを指定し、audio_end_ms に記録した再生時間を設定して送信します。これで、未再生の部分が会話から削除されます。
- VAD によって進行中の応答が中断されると、サーバーはその応答をキャンセルし、
response.done を送信します。このとき response.status は cancelled になります。このイベントは応答の終了を確認するためのものです。再生の停止や切り詰めイベントの送信は、このイベントを待たずに行ってください。
切り詰めイベントの例を示します。
123456{
"type": "conversation.item.truncate",
"item_id": "item_1234",
"content_index": 0,
"audio_end_ms": 1500
}
この例では、再生開始から 1.5 秒の位置で音声を切り詰めます。
文字起こしも同じ位置で切り詰められるでしょうか。Realtime モデルには、文字起こしと音声の位置を正確に対応付けるだけの情報がありません。そのため、conversation.item.truncate は指定した位置で音声を切り詰め、未再生部分の文字起こしを削除します。未再生の音声は会話から除かれますが、再生済みの部分だけに切り詰めた文字起こしは得られません。
Realtime API はデフォルトで音声区間検出(VAD)を使用し、音声入力をきっかけにモデルが応答します。VAD を無効にし、モデルに音声入力を送るタイミングをアプリケーション側で制御すると、プッシュトゥトークも実装できます。たとえば、スペースキーを押している間だけ音声を取り込み、キーを離したら応答を開始します。この方式が特に有効なアプリもあります。ユーザー自身がやり取りのタイミングを決められ、VAD の検出ミスを避けられるうえ、VAD のタイムアウトを待たずに済むので軽快に感じられます。
プッシュトゥトークの実装方法は、WebSocket と WebRTC で少し異なります。Realtime API の WebSocket 接続では、すべてのイベントが同じチャネルを通り、送信順が保たれます。一方、WebRTC 接続では音声と制御イベントが別々のチャネルを通ります。
WebSocket 接続でプッシュトゥトークを実装する場合、クライアント側で音声の再生を停止し、割り込みを処理して、新しい応答を開始します。具体的な手順は次のとおりです。
"turn_detection": null を設定した session.update イベントを送信し、VAD を無効にします。
- ボタンが押されたら、クライアントで音声の録音を開始します。
- モデルが応答を生成中の場合は、
response.cancel イベントを送信してキャンセルします。
- モデルの出力音声を再生中の場合は、直ちに再生を停止し、
conversation.item.truncate イベントを送信して、未再生の音声を会話から削除します。
- ボタンが離されたら、録音した音声を含む
input_audio_buffer.append メッセージを送信し、新しい音声を入力バッファに追加します。
input_audio_buffer.commit イベントを送信します。これで入力バッファに書き込んだ音声が確定し、入力の文字起こしが有効であれば開始されます。
- 続いて
response.create イベントを送信し、応答を開始します。
WebRTC でプッシュトゥトークを実装する手順も同様ですが、入力音声バッファを明示的にクリアする必要があります。手順は次のとおりです。
"turn_detection": null を設定した session.update イベントを送信し、VAD を無効にします。
- ボタンが押されたら、
input_audio_buffer.clear イベントを送信し、それまでの音声入力をクリアします。
- モデルが応答を生成中の場合は、
response.cancel イベントを送信してキャンセルします。
- モデルの出力音声を再生中の場合は、
output_audio_buffer.clear イベントを送信して未再生の音声をクリアします。これにより、会話内の応答も切り詰められます。
- ボタンが離されたら、
input_audio_buffer.commit イベントを送信します。これで入力バッファに書き込んだ音声が確定し、入力の文字起こしが有効であれば開始されます。
- 続いて
response.create イベントを送信し、応答を開始します。