新しいOpenAI Realtime APIを使用して低遅延のオーディオパイプラインを構築することで、開発者は人間のような自然な会話を行う音声エージェントを本番環境に導入できます。従来、音声インターフェースを構築するには、自動音声認識(ASR)、テキストベースのLLMロジック層、および音声合成(TTS)という3つの個別のモデル層をチェーン化する必要がありました。この多段階のパイプラインは大幅な往復ネットワーク遅延をもたらし、自然な会話を不可能にしていました。常時接続のWebSocket接続を介したネイティブオーディオ処理はこれを一変させ、ネットワーク遅延を300ミリ秒未満に短縮します。このガイドでは、接続状態の確立、生のオーディオバッファのストリーミング、およびセッション構成の最適化について解説します。
[!IMPORTANT] APIセキュリティに関する警告:OpenAIのAPIキーをクライアント側のブラウザスクリプト内に直接公開しないでください。WebSocket接続は必ず、OpenAIにパケットを転送する前に認証ヘッダーを付加する安全なエッジミドルウェア(Cloudflare Workerなど)を経由させてプロキシしてください。
主なポイント:
- WebSocket接続:エッジプロキシを使用して、OpenAIのRealtime WebSocketゲートウェイに直接接続します。
- ネイティブモダリティ:最初のセッション更新構成のペイロードで、
textとaudioの両方を指定します。- オーディオフォーマット:ユーザーの音声を24kHzのbase64エンコードされたモノラルPCM16チャンクとしてストリーミングします。
- 音声割り込み:サーバーの
speech-started信号を監視し、クライアント側の再生を即座に停止します。
ネイティブオーディオLLMのアーキテクチャ
従来の音声スタックは、中央のテキストモデルの外部ラッパーとして音声認識モデルと音声合成モデルの両方を扱います。ネイティブオーディオモデルは、音声を直接処理することでそのオーバーヘッドを排除します。
OpenAIモデルファミリーのリリースにより、ネットワークはオーディオの入出力をネイティブに処理します。モデルは生のオーディオ波形を受信し、声のトーン、抑揚、および内容を分析して、自然な音声を直接生成します。その結果、このパイプラインはASR転記エラーとTTS合成のボトルネックを排除します。
この常時接続を管理するためにWebSocketsが使用されます。通話中接続は開いたまま維持されるため、ユーザーの話し始めを検知した際にエージェントが応答出力を自己中断することができ、実際の電話通話と同じ体験が実現します。
AI統合サービスを利用する前提条件
コードを書く前に、環境が整っていることを確認してください。この構築では、非同期JavaScriptに慣れており、以下が準備されていることを前提としています。
- Realtimeアクセス権を持つOpenAIアカウントと、クレジットがチャージされた有効なAPIキー。
- Node.js 20以降、または
npm create cloudflare@latestで構築されたCloudflare Workersプロジェクト。 - WebSocketクライアント — Node.jsゲートウェイ用の
wsパッケージ、またはWorkers内で利用可能なネイティブグローバルWebSocket。 - ブラウザフロントエンド — Web Audio API(
getUserMediaと再サンプリング用のAudioWorklet)を介してマイクオーディオをキャプチャできるもの。 - base64およびPCMオーディオに関する実務知識 — 送受信するすべてのフレームはbase64エンコードされたPCM16ペイロードです。
アカウントの準備にも少し時間を取ってください。ゲートウェイがセッションを受け入れるには、組織でRealtimeアクセスと課金が有効になっている必要があります。
WebSocket接続の確立
まず、ヘッダーにGPT-realtimeモデルを指定して、OpenAI Realtimeゲートウェイへの接続を開きます。
以下のJavaScriptコードは、接続を初期化し、セッションモダリティを構成し、ストリーミングの入出力バッファを処理する方法を示しています。
1import WebSocket from "ws";
2
3export async function startVoiceAgent(env) {
4 // Connect to the OpenAI Realtime WebSocket gateway
5 const url = "wss://api.openai.com/v1/realtime?model=gpt-realtime";
6 const ws = new WebSocket(url, {
7 headers: {
8 "Authorization": `Bearer ${env.OPENAI_API_KEY}`,
9 "OpenAI-Beta": "realtime=v1"
10 }
11 });
12
13 ws.on("open", () => {
14 console.log("WebSocket connection established with OpenAI Realtime API");
15
16 // Configure session modalities and voice parameters
17 const sessionConfig = {
18 type: "session.update",
19 session: {
20 modalities: ["text", "audio"],
21 instructions: "You are a helpful customer service assistant for Mecanik.",
22 voice: "alloy",
23 input_audio_format: "pcm16",
24 output_audio_format: "pcm16",
25 temperature: 0.7
26 }
27 };
28 ws.send(JSON.stringify(sessionConfig));
29 });
30
31 ws.on("message", (data) => {
32 const event = JSON.parse(data);
33
34 // Handle incoming audio content from the server
35 if (event.type === "response.audio.delta") {
36 const audioBuffer = Buffer.from(event.delta, "base64");
37 // Output buffer to client audio player
38 playAudioChunk(audioBuffer);
39 }
40 });
41}
このハンドラーを構築するときは、APIキーがクライアントブラウザから見えないようにしてください。具体的には、WebSocket接続をプロキシする前に、サーバー上にエッジミドルウェアを確立して認可を処理します。エッジAPI構成の詳細については、Cloudflare Workersを使用したサーバーレスAPIの構築 のガイドを参照してください。
安全なエッジプロキシの構築
上のスニペットは信頼できるサーバーから接続していますが、キーを安全に保つ部分(プロキシ自体)を示していませんでした。Cloudflare Workersでは、new WebSocket()コンストラクタにカスタムヘッダーを添付できないため、代わりにfetchとUpgradeヘッダーを使用してアップストリーム接続を開きます。Workerはブラウザのソケットを受け入れ、シークレットキーを添付してOpenAIにダイヤルし、双方の間でフレームをパイプします。
1export default {
2 async fetch(request, env) {
3 if (request.headers.get("Upgrade") !== "websocket") {
4 return new Response("Expected a WebSocket upgrade", { status: 426 });
5 }
6
7 // 1. Accept the browser <-> Worker socket
8 const [client, server] = Object.values(new WebSocketPair());
9 server.accept();
10
11 // 2. Open the Worker <-> OpenAI socket with the secret key attached
12 const upstreamResponse = await fetch(
13 "https://api.openai.com/v1/realtime?model=gpt-realtime",
14 {
15 headers: {
16 Upgrade: "websocket",
17 Authorization: `Bearer ${env.OPENAI_API_KEY}`,
18 "OpenAI-Beta": "realtime=v1"
19 }
20 }
21 );
22
23 const upstream = upstreamResponse.webSocket;
24 if (!upstream) {
25 return new Response("Upstream refused the upgrade", { status: 502 });
26 }
27 upstream.accept();
28
29 // 3. Pipe frames in both directions
30 server.addEventListener("message", (e) => upstream.send(e.data));
31 upstream.addEventListener("message", (e) => server.send(e.data));
32
33 const close = () => { try { server.close(); upstream.close(); } catch {} };
34 server.addEventListener("close", close);
35 upstream.addEventListener("close", close);
36
37 return new Response(null, { status: 101, webSocket: client });
38 }
39};
キーはwrangler.tomlではなく、wrangler secret put OPENAI_API_KEYを使用して暗号化されたシークレットとして保存し、リポジトリに入らないようにします。ブラウザはwss://your-worker.workers.devに接続し、認証情報を見ることはありません。Cloudflareは、この双方向パターンをWorkers WebSocketsリファレンス
でドキュメント化しています。
ユーザーオーディオストリーミングの処理
セッション更新が受け入れられたら、クライアントはマイク入力をキャプチャし、24kHzモノラルPCM16データに圧縮し、base64セグメントとしてストリーミングする必要があります。接続が維持されるため、ネットワーク切断の処理が不可欠です。ローカルチャンクバッファは、一時的なモバイル回線の切断によって音声パケットの損失やエージェントの応答の途切れが発生しないようにします。クライアントはバッファを保持し、再接続時に即座に再送信します。
1// Example of streaming user microphone data
2function streamMicrophoneChunk(ws, base64AudioChunk) {
3 const audioEvent = {
4 type: "input_audio_buffer.append",
5 audio: base64AudioChunk
6 };
7 ws.send(JSON.stringify(audioEvent));
8}
ユーザーが話し終えると、サーバーは蓄積されたオーディオバッファを自動的に処理し、モデルの応答をトリガーします。完全なAPIイベントリストについては、OpenAI Realtimeガイド を確認してください。
また、フロントエンドの再生プレイヤーにエコーキャンセレーションを実装してください。マイクがスピーカーの出力を拾うと、エージェントは自身の声をユーザーの割り込みと解釈し、セッションループが破損する原因になります。フロントエンドの最適化の詳細については、WordPress対カスタムWeb開発 のガイドを確認してください。
ターン検出と割り込みの管理
デフォルトでは、発話ターンの終了をモデルに通知する必要があります。サーバー側の音声活動検知(VAD)を有効にすると、そのタスクがOpenAIに委ねられます。ゲートウェイは入力バッファを監視し、ユーザーが話し終えたタイミングを判断して、応答を自動的にトリガーします。ハンドシェイク中に送信するセッション更新にturn_detectionブロックを追加します。
1const sessionConfig = {
2 type: "session.update",
3 session: {
4 modalities: ["text", "audio"],
5 voice: "alloy",
6 input_audio_format: "pcm16",
7 output_audio_format: "pcm16",
8 turn_detection: {
9 type: "server_vad",
10 threshold: 0.5,
11 prefix_padding_ms: 300,
12 silence_duration_ms: 500
13 }
14 }
15};
VADが有効な場合、サーバーはエージェントの発話中にユーザーが話し始めた瞬間にinput_audio_buffer.speech_startedを発行します。このイベントが発生したら即座にプレイヤー内のキューにあるオーディオチャンクをすべて破棄し、再生中の発話を停止してください。そうしないと、新しい応答が再生開始された後も前の音声が流れ続けてしまいます。
1let playbackQueue = [];
2
3ws.on("message", (raw) => {
4 const event = JSON.parse(raw);
5 if (event.type === "input_audio_buffer.speech_started") {
6 // User is interrupting: drop everything still buffered
7 playbackQueue = [];
8 stopSpeaker();
9 }
10});
silence_duration_msを増やすと、エージェントは応答する前に長く待つようになり、ゆっくり話すユーザーや迷いながら話すユーザーに適しています。値を下げると会話のテンポは上がりますが、発話の途中でユーザーを遮ってしまうリスクが高まります。
音声エージェントのデプロイ手順
音声エージェントのミドルウェアをデプロイするには、まずサーバーレスエッジノード上に安全なWebSocketルーティングゲートウェイを構成して、OpenAIの資格情報を保護します。これにより、スクレイパーにキーが検出されるのを防ぐことができます。
次に、システムプロンプトとルールを注意深くドラフトします。初期のセッション更新イベント内で、トーン、語彙、および応答の指示を設定します。これにより、会話の流れの明確な範囲が確立されます。
続いて、堅牢な割り込みロジックを実装します。input_audio_buffer.speech_startedイベントをリッスンし、フロントエンドプレイヤーでのオーディオ再生を即座に停止します。最後に、ルーティングホップを減らすために、ユーザーに物理的に近い地域のリージョンにWorkersをデプロイして、グローバルな遅延を最適化します。エッジホスティングのオプションを探索するには、Cloudflare Workers AIチュートリアル
を参照してください。
一般的な問題とトラブルシューティング
最初の実行時の失敗のほとんどは、いくつかの予測可能な間違いから発生します。以下の表は、表示される症状とその一般的な原因および修正方法を示しています。
| 症状 | 考えられる原因 | 修正方法 |
|---|---|---|
接続がすぐに401で閉じる | Authorizationヘッダーの欠落または不正 | プロキシがBearer <key>を添付していること、およびキーにRealtimeアクセス権があることを確認 |
| エージェントに無音または文字化けした音声しか届かない | サンプルレートやビット深度が正しくないオーディオが送信された | base64エンコード前に24kHzモノラルPCM16に再サンプルする |
| ユーザーが話した後にモデルが応答しない | turn_detectionが無効で、手動のコミットが送信されていない | server_vadを有効にするか、input_audio_buffer.commitの後にresponse.createを送信する |
| エージェントがユーザーと重なって話す | speech_startedハンドラーがキューをクリアしていない | そのイベントで再生バッファを空にし、スピーカーを停止する |
| 回答が文の途中で切れる | max_response_output_tokensの設定が低すぎる | 制限を上げるか、infのままにする |
さらに2つのデリケートな問題に注意が必要です。残り残高が少ない状態でrate_limits.updatedイベントが届いた場合は、同時セッションがスロットリングされようとしている初期警告です。再接続を繰り返すのではなく、ログを記録してリクエスト頻度を下げてください。また、エージェントが時々自身の最後の文に応答する場合は、マイクがスピーカー出力を拾っています。エコーキャンセレーションを強化するか、テスターにヘッドセットを使用させてください。
テストと本番環境における考慮事項
双方向に実際のオーディオを流す必要があるため、ローカルテストは手間がかかります。最も迅速な開発サイクルは、Workerをwrangler devで実行し、シンプルなブラウザページを接続し、音質を気にする前にコンソールでイベントストリームを観察することです。開発中はすべてのインバウンドイベントタイプを記録します。session.updated、speech_started、response.audio.delta、およびresponse.doneのシーケンスが正しく表示されれば、通信経路が健全であることがわかります。
出荷する前に、本番環境のいくつかの現実を評価してください。
- コスト:リアルタイムオーディオは、入力および出力オーディオトークンの分数で課金されます。これはプレーンテキストトークンよりもはるかに高価です。セッションの長さを制限し、長い情報提供の回答にはテキストのフォールバックを検討してください。
- 遅延:プロキシをユーザーの近くにデプロイし、処理を軽量に保ちます。ブラウザ、エッジ、OpenAI間の余分なホップはすべて、往復遅延を増加させます。
- 再接続:モバイルネットワークではソケットが切断されがちです。クライアント側に短いローリングバッファを保持し、再接続時にそれを再送信することで、フレームの紛失による会話の破綻を防ぎます。
- オブザーバビリティ:セッションの開始、割り込み数、およびエラーイベントの構造化ログを出力し、ユーザーから苦情が出る前にパフォーマンスの低下を特定できるようにします。
- 安全性とプライバシー:音声は個人データです。データの保持期間を明確にし、エージェントが機密性の高いワークフローを処理する場合は、テキスト書き起こしに対するサーバー側のモデレーションを追加してください。
実際の呼び出し側を使用した短いパイロット運用を行うことで、スクリプト化されたテストではカバーできないアクセント、ノイズ、割り込みなどのエッジケースが明らかになります。そのため、広範な立ち上げの前にテスト運用を実行してください。
主なポイント
- OpenAI Realtime APIは、常時接続のWebSocketを介してオーディオをネイティブに処理し、遅延を300ms未満に抑えます。
- 最初のWebSocketハンドシェイク中に、常にモダリティ、フォーマット、およびプロンプトを構成します。
- ユーザーのオーディオをリアルタイムでモノラルPCM16 base64セグメントとしてストリーミングします。
- 音声開始イベントを監視して、スピーカーの割り込みイベントを処理します。
- 接続の詳細をエッジWorkerルーターでプロキシすることにより、API資格情報のセキュリティを維持します。
よくある質問(FAQ)
OpenAI Realtime APIとは何ですか? OpenAI Realtime APIは、個別のASR/TTSモジュールをバイパスして、生のオーディオをモデルに出し入れしてストリーミングできるようにするWebSocketインターフェースです。オーディオをネイティブに処理することにより、モデルは感情のトーン、アクセントの抑揚、および会話のニュアンスを維持し、300ミリ秒未満の遅延時間を実現します。
音声の割り込みはどのように処理すればよいですか?
サーバー側のinput_audio_buffer.speech_startedイベントを監視します。受信したら、フロントエンドのオーディオバッファをクリアし、スピーカーの再生を即座に停止します。これにより、自然な会話の流れが作成され、ユーザーが話し始めたときにAIエージェントが瞬時に発話を止めることができます。
APIはどのオーディオフォーマットをサポートしていますか? APIは、24kHzモノラルPCM16(生の16ビット符号付き整数)およびG.711(u-lawおよびa-law)オーディオフォーマットをネイティブでサポートしています。開発者はクライアント側でマイクデータをキャプチャしてこれらの特定のフォーマットに変換し、JSON WebSocketフレーム内でbase64エンコードされた文字列としてストリーミングする必要があります。
WebSocketトラフィックを調整するために個別のサーバーが必要ですか? はい。サーバーレスエッジコーディネーター(Cloudflare Workersや軽量のNode.jsゲートウェイなど)を実行することを強くお勧めします。プロキシはクライアントブラウザからマイク入力を受信し、安全な認証ヘッダーを添付して、バイナリストリームをOpenAIのゲートウェイにリダイレクトします。
リアルタイム音声エージェントでエコーループを防ぐにはどうすればよいですか? エコーループは、スピーカーのオーディオがクライアントのマイクに回り込み、誤った割り込みイベントをトリガーするときに発生します。開発者はクライアント側にエコーキャンセレーションアルゴリズムを実装するか、テスト中にヘッドセットを使用して、フィードバックループがセッションを破損するのを防ぐ必要があります。
コメント