Home

2026-01-27 OpenAI Realtime 音声対話 plan

OpenAI Realtime 音声対話 実装計画

概要

OpenAI Realtime API を使用したリアルタイム音声対話機能を実装する。既存の Gemini Live 実装と同様のパターンで、独立したページ・コンポーネントとして実装(案C: MVP優先)。

背景: 既存のGemini Live音声対話機能に加えて、OpenAI Realtime APIを使用した音声対話機能を実装することで、ユーザーに選択肢を提供する。

音声フォーマットの違い:

API入力出力
Gemini Live16kHz, 16-bit PCM24kHz, 16-bit PCM
OpenAI Realtime24kHz, 16-bit PCM24kHz, 16-bit PCM

バックエンド計画

1. 概要

OpenAI Realtime API用のエフェメラルキー発行エンドポイントを実装する。

  • エンドポイント: POST /api/openai/realtime/session
  • 目的: フロントエンドがOpenAI Realtime APIへWebSocket接続する際に必要な一時的なエフェメラルキー(ek_xxx形式)を発行
  • 参考実装: 既存の POST /api/gemini/token と同様のパターン

認証フロー

Loading diagram...

2. 懸念点と解決策

懸念点対応方針
OpenAI APIのレスポンス形式公式ドキュメントに従い、client_secret.value を抽出
APIキー未設定時の挙動Geminiと同様に nil を返し、ハンドラーで 503 を返す
HTTPリクエストのタイムアウトhttp.Client{Timeout: 10 * time.Second} を使用
エラーレスポンスの形式OpenAI APIのエラー形式(error.message)をパースし、適切なエラーメッセージを返す
モデル・ボイスのバリデーションOpenAI API にそのまま渡し、エラーハンドリングは OpenAI のレスポンスに委ねる

OpenAI エラーレスポンス形式:

{
  "error": {
    "message": "Invalid API key",
    "type": "invalid_request_error",
    "code": "invalid_api_key"
  }
}

3. 変更ファイル一覧

ファイル変更内容影響度
backend/internal/service/openai.go新規作成: OpenAIServiceインターフェースと実装
backend/internal/service/types.go追加: OpenAISessionResult型
backend/internal/handler/openai.go新規作成: OpenAIHandlerとHandleSession関数
backend/cmd/server/main.go追加: OpenAIService生成とルーティング登録
backend/docs/BACKEND_API.md追加: APIドキュメント

4. 実装ステップ

Step 1: 型定義の追加

対象: backend/internal/service/types.go

追加するもの:

  • OpenAISessionResult: フィールド Token, ExpireTime

注意点:

  • 既存の GeminiTokenResult と命名規則を統一(Token, ExpireTime

Step 2: OpenAIServiceの作成

対象: backend/internal/service/openai.go(新規作成)

追加するもの:

  • インターフェース OpenAIService: メソッド CreateRealtimeSession
  • 構造体 openaiServiceImpl: フィールド apiKey, httpClient
  • 関数 NewOpenAIService: 環境変数 OPENAI_API_KEY を読み取り、未設定時は nil を返す
  • メソッド CreateRealtimeSession: OpenAI /v1/realtime/sessions APIを呼び出しエフェメラルキーを取得

注意点:

  • SDKは使用しない(HTTPリクエストで直接呼び出す)
  • OpenAI APIのエンドポイント: https://api.openai.com/v1/realtime/sessions
  • リクエストボディ: { "model": "gpt-4o-realtime-preview-2024-12-17", "voice": "verse" }
  • 認証ヘッダー: Authorization: Bearer sk-xxx
  • レスポンスから client_secret.value を抽出
  • HTTPクライアント: http.Client{Timeout: 10 * time.Second} を使用

Step 3: OpenAIHandlerの作成

対象: backend/internal/handler/openai.go(新規作成)

追加するもの:

  • OpenAISessionRequest: フィールド Model, Voice(いずれもオプション)
  • OpenAISessionResponse: フィールド Success, Token, ExpireTime, Error
  • 構造体 OpenAIHandler: フィールド openaiService
  • 関数 NewOpenAIHandler
  • メソッド HandleSession: POST /api/openai/realtime/session を処理

注意点:

  • サービスが nil の場合は 503 Service Unavailable を返す
  • リクエストボディは空でも許容(デフォルト値を使用)
  • レスポンスのJSONフィールド名は既存のGemini APIと統一(token, expireTime

Step 4: main.goへの統合

対象: backend/cmd/server/main.go

修正するもの:

  • NewOpenAIService() 呼び出しを追加
  • NewOpenAIHandler(openaiService) 呼び出しを追加
  • ルーティング追加: api.POST("/openai/realtime/session", openaiHandler.HandleSession)

Step 5: ドキュメント更新

対象: backend/docs/BACKEND_API.md

追加するもの:

  • OpenAI API セクション
  • POST /api/openai/realtime/session の仕様

5. API仕様

POST /api/openai/realtime/session

OpenAI Realtime API用のエフェメラルキーを発行する。

リクエスト
{
    "model": "gpt-4o-realtime-preview-2024-12-17",
    "voice": "verse"
}
フィールド必須説明
modelstringNo使用するモデル。デフォルト: gpt-4o-realtime-preview-2024-12-17
voicestringNo音声タイプ。デフォルト: verse
レスポンス(成功)
{
    "success": true,
    "token": "ek_xxx...",
    "expireTime": "2026-01-27T12:00:00Z"
}

注意: フィールド名は既存のGemini API(/api/gemini/token)と統一。

レスポンス(エラー)
{
    "success": false,
    "error": "エラーメッセージ"
}
HTTPステータスコード
コード説明
200正常完了
400リクエスト不正
500セッション作成に失敗(OpenAI API エラー)
503OPENAI_API_KEY 未設定

6. 設定・環境変数

環境変数説明必須
OPENAI_API_KEYOpenAI APIキー(sk-xxx形式)機能利用時のみ

フロントエンド計画

1. 概要

OpenAI Realtime API を使用したリアルタイム音声対話機能を実装する。既存の Gemini Live 実装と同等の機能を持つが、音声フォーマットとWebSocket認証方式が異なる点に注意が必要。

2. 懸念点と解決策

懸念点対応方針
音声入力サンプルレートOpenAI は 24kHz を要求。AudioContext 作成時のサンプルレートを変更
AudioWorklet の共有既存の audio-worklet-processor.js はサンプルレート非依存のため、そのまま流用可能
型定義の重複Gemini と OpenAI で似た構造だが別の型として定義。共通化は過剰設計のため行わない
WebSocket認証方法ブラウザではヘッダー設定不可のため、サブプロトコルでトークン指定

WebSocket接続の認証方式

ブラウザの WebSocket API は直接ヘッダーを設定できないため、OpenAI Realtime API ではサブプロトコルにエフェメラルキーを含める方式を使用する。

const OPENAI_REALTIME_WS_URL = "wss://api.openai.com/v1/realtime";

// 接続時
const url = `${OPENAI_REALTIME_WS_URL}?model=${encodeURIComponent(model)}`;
const ws = new WebSocket(url, [
  "realtime",
  `openai-insecure-api-key.${ephemeralToken}`
]);

参考: OpenAI Realtime API ドキュメント

設計判断

判断選択した方法理由他の選択肢
コンポーネント設計Gemini版をコピーして修正構造が同じで分かりやすい共通コンポーネント抽出(過剰)
API関数配置lib/api.ts に追加既存パターンに従う別ファイル(不要な分割)
型定義配置types/openai.ts を新規作成Gemini と同様の構成types/index.ts に追加(肥大化を避ける)

3. 変更ファイル一覧

ファイル変更内容影響度
frontend/src/types/openai.ts新規作成: OpenAI Realtime API 用型定義
frontend/src/hooks/useOpenAIRealtime.ts新規作成: WebSocket接続・音声処理フック
frontend/src/components/OpenAIRealtimeClient.tsx新規作成: UIコンポーネント
frontend/src/app/openai-realtime/page.tsx新規作成: ページエントリーポイント
frontend/src/lib/api.ts修正: トークン取得関数追加
frontend/docs/screens.md修正: 画面一覧にページ追加

: audioProcessor.ts の修正は不要。既存の関数は汎用的で、サンプルレートは引数で指定可能。

4. 実装ステップ

Step 1: 型定義の作成

対象: frontend/src/types/openai.ts

追加するもの:

接続状態・設定:

  • OpenAIConnectionStatus: "disconnected", "connecting", "connected", "error"
  • OpenAIRealtimeConfig: model?, voice?, instructions?, modalities?

クライアント -> サーバー(送信用):

  • OpenAISessionUpdate: type: "session.update", session
  • OpenAIInputAudioBufferAppend: type: "input_audio_buffer.append", audio

サーバー -> クライアント(受信用):

  • OpenAISessionCreatedEvent: type: "session.created", session
  • OpenAIResponseAudioDeltaEvent: type: "response.audio.delta", delta
  • OpenAIErrorEvent: type: "error", error
  • OpenAIServerEvent: Union型(上記の受信イベント群)

バックエンドAPI連携:

  • OpenAITokenResponse: success, token?, expireTime?, error?

型ガード関数:

  • isSessionCreated, isResponseAudioDelta, isOpenAIError

MVPで処理するイベント:

  • session.created: 接続完了
  • response.audio.delta: 音声データ
  • error: エラー

Step 2: API関数の追加

対象: frontend/src/lib/api.ts

追加するもの:

  • 関数 fetchOpenAIRealtimeToken: エフェメラルトークン取得

仕様:

  • 戻り値: Promise<string>(token を返す)
  • エラー時: Error を throw
  • 既存の fetchGeminiToken と同じパターン

Step 3: カスタムフックの作成

対象: frontend/src/hooks/useOpenAIRealtime.ts

追加するもの:

  • フック useOpenAIRealtime
  • State: connectionStatus, isRecording, error
  • Refs: wsRef, audioContextRef, inputAudioContextRef, streamRef, audioQueueRef
  • 関数: connect, disconnect, startRecording, stopRecording

定数定義:

const OPENAI_REALTIME_WS_URL = "wss://api.openai.com/v1/realtime";
const INPUT_SAMPLE_RATE = 24000;  // OpenAI要件
const OUTPUT_SAMPLE_RATE = 24000; // OpenAI要件

Gemini版との違い:

  • WebSocket URL: wss://api.openai.com/v1/realtime?model=xxx
  • WebSocket認証: サブプロトコル ["realtime", "openai-insecure-api-key.{token}"]
  • 入力サンプルレート: 24kHz(Gemini は 16kHz)
  • setup メッセージ形式: session.update イベント
  • サーバーメッセージ形式: response.audio.delta

session.update メッセージ形式:

{
  type: "session.update",
  session: {
    modalities: ["text", "audio"],
    instructions: "You are a helpful assistant.",
    voice: "verse"
  }
}

エラーケースと対応メッセージ:

  • トークン取得失敗: "Failed to get ephemeral token"
  • WebSocket接続失敗: "Failed to connect to OpenAI Realtime API"
  • マイク権限拒否: "Microphone permission denied"
  • 録音開始失敗: "Failed to start recording: {詳細}"
  • APIエラー: "OpenAI API error: {message}"

Step 4: UIコンポーネントの作成

対象: frontend/src/components/OpenAIRealtimeClient.tsx

追加するもの:

  • コンポーネント OpenAIRealtimeClient
  • ヘルパー関数: getStatusColor, getStatusText

構成:

  • "use client" ディレクティブ(Client Component)
  • useOpenAIRealtime フックを使用
  • Gemini版と同じUI構造(タイトル、接続状態、ボタン、使い方説明)

Step 5: ページの作成

対象: frontend/src/app/openai-realtime/page.tsx

追加するもの:

  • ページコンポーネント OpenAIRealtimePage
  • dynamic インポートで SSR 無効化(ブラウザ API 使用のため)

Step 6: ドキュメント更新

対象: frontend/docs/screens.md

修正するもの:

  • ページ一覧テーブルに /openai-realtime を追加

5. コンポーネント構成

Loading diagram...

責務:

  • page.tsx: SSR無効化、コンポーネントのエントリーポイント
  • OpenAIRealtimeClient: UI表示、ユーザーインタラクション処理
  • useOpenAIRealtime: WebSocket接続、音声入出力、状態管理
  • api.ts: トークン取得のHTTPリクエスト
  • audioProcessor.ts: 音声フォーマット変換(既存関数を使用)
  • types/openai.ts: 型定義と型ガード

6. 型定義

types/openai.ts

型名フィールド用途
OpenAIConnectionStatus"disconnected", "connecting", "connected", "error"接続状態
OpenAIRealtimeConfigmodel?, voice?, instructions?, modalities?設定
OpenAISessionUpdatetype: "session.update", session送信: セッション設定
OpenAIInputAudioBufferAppendtype: "input_audio_buffer.append", audio送信: 音声データ
OpenAISessionCreatedEventtype: "session.created", session受信: 接続完了
OpenAIResponseAudioDeltaEventtype: "response.audio.delta", delta受信: 音声データ
OpenAIErrorEventtype: "error", error受信: エラー
OpenAIServerEventUnion型(受信イベント群)受信メッセージ
OpenAITokenResponsesuccess, token?, expireTime?, error?バックエンドAPI

7. 画面仕様

ページURL

/openai-realtime

UI要素

要素役割
タイトル"OpenAI Realtime" の表示
接続状態インジケーター色付きドット + テキスト
エラー表示エラーメッセージ(エラー時のみ)
接続ボタンConnect / Disconnect
マイクボタンStart Recording / Stop Recording
使い方説明3ステップの操作手順

ユーザー操作フロー

Loading diagram...

次回実装(MVP外)

以下はMVP範囲外とし、次回以降に実装:

  • プロバイダー切り替えUI
  • 抽象化レイヤーの導入(案Bへの移行)
  • 音声のテキスト表示(文字起こし)
  • ツール呼び出し対応(function calling)
  • 音声選択UI(voice パラメータ変更)
  • 会話履歴の保存機能
  • その他のOpenAIイベント対応(session.updated, response.done, input_audio_buffer.speech_started 等)

実装順序

  1. バックエンド: Step 1〜5 を順に実装
  2. フロントエンド: Step 1〜6 を順に実装

バックエンドの /api/openai/realtime/session が完成してからフロントエンドのテストを行う。


参考リンク


バックエンド実装レポート

実装サマリー

  • 実装日: 2026-01-27
  • 実装範囲: バックエンド(backend/ 配下)
  • 変更ファイル数: 7 files

OpenAI Realtime API用のエフェメラルキー発行エンドポイント POST /api/openai/realtime/session を実装した。既存のGemini API実装パターンを踏襲し、一貫したアーキテクチャで設計。

変更ファイル一覧

ファイル変更種別変更内容
backend/internal/service/types.go追記OpenAISessionResult 型を追加(Token, ExpireTime フィールド)
backend/internal/service/openai.go新規作成OpenAIService インターフェースと実装。エフェメラルキー発行ロジック
backend/internal/handler/openai.go新規作成OpenAIHandler。リクエスト/レスポンス型定義とHTTPハンドラー
backend/cmd/server/main.go修正OpenAIService/Handler の初期化とルーティング追加
backend/docs/BACKEND_API.md追記API仕様ドキュメント追加
backend/internal/service/doc.go修正パッケージドキュメントに OpenAIService の説明を追加
backend/internal/handler/doc.go修正パッケージドキュメントに OpenAIHandler の説明を追加

API仕様の要約

POST /api/openai/realtime/session

OpenAI Realtime API用のエフェメラルキー(ek_xxx形式)を発行する。

リクエスト:

{
    "model": "gpt-4o-realtime-preview-2024-12-17",  // オプション
    "voice": "verse"                                // オプション
}

レスポンス(成功):

{
    "success": true,
    "token": "ek_xxx...",
    "expireTime": "2026-01-27T12:00:00Z"
}

HTTPステータス:

コード説明
200正常完了
400リクエスト不正
500OpenAI API エラー
503OPENAI_API_KEY 未設定

計画からの変更点

特になし。計画書通りに実装した。

実装時の課題

特になし。既存のGemini API実装パターンを参考に、スムーズに実装できた。

残存する懸念点

  • API キー管理: OPENAI_API_KEY 環境変数が未設定の場合、サービスが nil を返す設計。フロントエンドで適切なエラーハンドリングが必要
  • トークン有効期限: OpenAI のエフェメラルキーは発行から1分程度で失効する。フロントエンド側で接続前に毎回取得する必要がある

動作確認手順

1. 環境変数の設定

export OPENAI_API_KEY="sk-xxx..."

2. バックエンドの起動

make restart-backend-logs

起動ログに以下が表示されることを確認:

[OpenAIService] Initialized with OpenAI API

3. API疎通確認

# リクエスト送信
curl -X POST http://localhost:8080/api/openai/realtime/session \
  -H "Content-Type: application/json" \
  -d '{}'

# 期待されるレスポンス
{
  "success": true,
  "token": "ek_...",
  "expireTime": "2026-01-27T12:34:56Z"
}

4. パラメータ指定の確認

curl -X POST http://localhost:8080/api/openai/realtime/session \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-4o-realtime-preview-2024-12-17", "voice": "alloy"}'

5. API キー未設定時の確認

# 環境変数を削除してサーバー再起動
unset OPENAI_API_KEY
make restart-backend-logs

# リクエスト送信
curl -X POST http://localhost:8080/api/openai/realtime/session

# 期待されるレスポンス(503 Service Unavailable)
{
  "success": false,
  "error": "OpenAI サービスが利用できません"
}

次のステップ

フロントエンド実装に進む → 完了


フロントエンド実装レポート

実装サマリー

  • 実装日: 2026-01-27
  • 実装範囲: フロントエンド(frontend/ 配下)
  • 変更ファイル数: 6 files

OpenAI Realtime API を使用したリアルタイム音声対話のフロントエンド機能を実装した。既存の Gemini Live 実装と同様のパターンで、型定義・カスタムフック・UIコンポーネント・ページを作成。

変更ファイル一覧

ファイル変更種別変更内容
frontend/src/types/openai.ts新規作成OpenAI Realtime API 用型定義(接続状態、送受信イベント、型ガード関数)
frontend/src/hooks/useOpenAIRealtime.ts新規作成WebSocket 接続・音声入出力・状態管理フック
frontend/src/components/OpenAIRealtimeClient.tsx新規作成音声 AI インターフェースの UI コンポーネント
frontend/src/app/openai-realtime/page.tsx新規作成ページエントリーポイント(SSR 無効化)
frontend/src/lib/api.ts修正fetchOpenAIRealtimeToken 関数を追加
frontend/docs/screens.md修正ページ一覧に /openai-realtime を追加、OpenAI Realtime ページセクションを追加

Gemini Live 版との主な違い

項目Gemini LiveOpenAI Realtime
WebSocket URLGemini API エンドポイントwss://api.openai.com/v1/realtime
認証方式URL にトークンを含めるサブプロトコルにエフェメラルキーを含める
入力サンプルレート16kHz24kHz
出力サンプルレート24kHz24kHz
セットアップメッセージGemini 独自形式session.update イベント
音声データ受信Gemini 独自形式response.audio.delta イベント
VAD(音声区間検出)クライアント側サーバー側(server_vad
音声入力処理AudioWorkletScriptProcessorNode

計画からの変更点

  • AudioWorklet → ScriptProcessorNode: AudioWorklet を使用する計画だったが、24kHz サンプルレートでの互換性を考慮し ScriptProcessorNode を採用
  • 追加イベント型: session.updatedresponse.audio.done イベントの型を追加(MVP範囲で処理に活用)

残存する懸念点

  • ScriptProcessorNode の非推奨: ScriptProcessorNode は Web Audio API で非推奨。将来的に AudioWorklet への移行を検討
  • エフェメラルキーの有効期限: OpenAI のエフェメラルキーは約1分で失効するため、接続前に毎回取得が必要
  • 同時再生の競合: 出力用 AudioContext の遅延作成により、入力用 AudioContext との競合を回避しているが、エッジケースの検証が必要

動作確認手順

  1. バックエンドを起動(OPENAI_API_KEY 環境変数が設定されていること)
  2. フロントエンドを起動
  3. ブラウザで /openai-realtime にアクセス
  4. "Connect" をクリックして接続
  5. "Start Recording" をクリックしてマイク入力開始
  6. 音声で話しかけると AI の音声応答が再生される

GA版移行(2026-01-31)

背景

当初の実装は OpenAI Realtime API Beta 版を使用していたが、GA(General Availability)版がリリースされ、エンドポイントとリクエスト/レスポンス形式が変更された。Beta 版のエフェメラルキーで GA 版に接続しようとすると「API version mismatch」エラーが発生するため、GA 版に移行した。

変更内容

バックエンド(backend/internal/service/openai.go

項目Beta版GA版
エンドポイント/v1/realtime/sessions/v1/realtime/client_secrets
モデル名gpt-4o-realtime-preview-2024-12-17gpt-realtime
リクエスト形式{ model, voice }{ expires_after, session: { type, model, audio } }
レスポンス形式client_secret.valueトップレベルの value

フロントエンド(frontend/src/hooks/useOpenAIRealtime.tsfrontend/src/types/openai.ts

項目Beta版GA版
モデル名gpt-4o-realtime-preview-2024-12-17gpt-realtime
session.updatemodalities フィールドありtype: "realtime" 必須、modalities 非対応
音声応答イベントresponse.audio.deltaresponse.output_audio.delta
音声応答完了イベントresponse.audio.doneresponse.output_audio.done

発生した問題と解決策

1. 音声入力データがゼロ(無音)になる問題

症状: WebSocket で送信される音声データが全て AAAA...(Base64エンコードされたゼロ)になる

原因: AudioContext({ sampleRate: 24000 }) を強制的に指定していたが、ブラウザによっては24kHzがサポートされず、マイク入力が正しく取得できなかった

解決策: デフォルトのサンプルレート(通常48kHz)で AudioContext を作成し、リサンプリングで24kHzに変換するように修正

// 修正前
const inputAudioContext = new AudioContext({ sampleRate: INPUT_SAMPLE_RATE });

// 修正後
const inputAudioContext = new AudioContext();
const nativeSampleRate = inputAudioContext.sampleRate;
// リサンプリングで24kHzに変換

2. 音声応答が再生されない問題

症状: サーバーから音声データは受信しているが、再生されない

原因: GA版では音声応答のイベントタイプが response.audio.delta から response.output_audio.delta に変更されていた。型定義と型ガード関数が古いイベントタイプ名を使っていたため、音声応答データを認識できなかった

解決策: 型定義と型ガード関数を GA 版のイベントタイプに修正

// 修正前
type: "response.audio.delta"
return msg.type === "response.audio.delta";

// 修正後
type: "response.output_audio.delta"
return msg.type === "response.output_audio.delta";

3. 英語で応答される問題

症状: 日本語で話しかけても英語で応答される

原因: instructions のデフォルト値が英語だった

解決策: 日本語の instructions に変更

instructions: instructions || "あなたは親切な音声アシスタントです。日本語で会話してください。フレンドリーで自然な口調で応答してください。"

CORS設定の追加

Tailscale Funnel 経由でのアクセスをサポートするため、バックエンドのCORS設定に *.ts.net ドメインを追加。

// Tailscale Funnel ドメイン (*.ts.net) を許可
if len(origin) > 7 && origin[len(origin)-7:] == ".ts.net" {
    return true
}

関連コミット

  • feat: OpenAI Realtime APIをGA版に移行、エンドポイントとリクエスト形式を更新
  • fix: OpenAI Realtime GA版の音声応答イベント名を修正、リサンプリング処理を追加