2025年1月14日火曜日

OpenAIのRealtime APIをWebRTCで呼び出す

OpenAIのRealtime APIを、Oracle APEXのアプリケーションからWebRTCを使って呼び出してみます。参照するOpenAIのドキュメントは以下です。

Realtime API with WebRTC

WebRTCによるOpenAI Realtime APIの呼び出しは、すべてWebブラウザで動作するJavaScriptによって実装します。上記のOpenAIのドキュメントに記載されているコードを、ほぼそのまま流用します。WebRTCでRealtime APIを呼び出すには、OpenAIのサーバーよりephemeral token(一時トークン)を取得し、それをAPIキーとして使用します。この一時トークンの取得については、Oracle APEXが動作するデータベース・サーバーで実行します。

作成したAPEXアプリケーションは以下のように動作します。GIF動画なので音声が含まれていないため動作が分かりませんが、ボタンStartを押してConnection Stateopenになってから「OpenAIの創業者について教えてください。」と聞いています。Realtime APIのレスポンスに音声のトランスクリプトが含まれるため、それを取り出してページ・アイテムに設定しています。同時にWebブラウザでは、トランスクリプトの読み上げが行われています。Realtime APIから返された次の文章が読み上げられています。

OpenAIは、2015年に設立されました。創業者の一人には、イーロン・マスクやサム・アルトマンなどがいます。彼らは、人工知能の安全な発展と普及を目指して活動を始めました。設立当初は、営利目的ではなく、オープンソースでのAI研究を重視していました。


以下より実装について紹介します。

OpenAIのAPIサーバーより一時キーを取得するに当たって、OpenAIのAPIを呼び出します。このAPI呼び出しの認証には、OpenAIの(一時キーではない)APIキーを指定します。このAPIキーをOracle APEXのWeb資格証明として登録します。

Web資格証明静的IDOPENAI_API_KEYとしています。認証タイプHTTPヘッダー資格証明名はHTTPヘッダー名であるAuthenticationを設定します。資格証明シークレットBearerで始めて空白で区切り、skで始まるOpenAIのAPIキーを設定します。URLに対して有効については、https://api.openai.com/を設定します。


空のAPEXアプリケーションを作成し、デフォルトで作成されるホーム・ページにWebRTCによるOpenAI Realtime APIの呼び出しを実装します。

APEXアプリケーションの名前OpenAI WebRTCとします。

WebRTCの呼び出しを含んだJavaScriptのコードは、すべて静的アプリケーション・ファイルjs/app.jsに記述します。ページ・プロパティJavaScriptファイルURLとして、以下を設定します。

[module,defer]#APP_FILES#js/app#MIN#.js


一時キーの取得は、Ajaxコールバックとして実装します。プロセスの名前GET_EPHEMERAL_TOKENとして、ソースPL/SQLコードに以下を記述します。

declare
C_TOKEN_URL constant varchar2(80) := 'https://api.openai.com/v1/realtime/sessions';
l_response clob;
l_response_json json_object_t;
l_request clob;
e_api_call_failed exception;
begin
l_request := json_object(
-- 'model' value 'gpt-4o-realtime-preview-2024-12-17'
-- gpt-4o-miniが選択できる。
'model' value 'gpt-4o-mini-realtime-preview-2024-12-17'
,'voice' value 'verse'
-- ephemeral token取得時にinstructionsを指定できるみたい。
,'instructions' value '日本語で会話をしてください。'
);
apex_web_service.clear_request_headers();
apex_web_service.set_request_headers('Content-Type', 'application/json');
apex_debug.info('request: %s', l_request);
l_response := apex_web_service.make_rest_request(
p_url => C_TOKEN_URL
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'OPENAI_API_KEY'
);
if apex_web_service.g_status_code <> 200 then
raise e_api_call_failed;
end if;
apex_debug.info('response', l_response);
htp.p(l_response);
end;

一時キーはレスポンスであるJSONドキュメントのclient_secret.valueとして返されます。


ホーム・ページにはAjaxコールバックGET_EPHEMERAL_TOKENの呼び出しとレスポンスを確認するため、ボタンGET_EPHEMERAL_TOKENと、そのボタンをクリックしたときに実行される動的アクションを定義しています。このボタンはあくまでデバッグ用で、WebRTCでのRealtime API呼び出しには使用されません。


WebRTCによる会話を開始するボタンSTARTと、終了するボタンSTOPを作成します。最初に静的コンテンツのリージョンを作成し、静的IDとしてCONTROLSを割り当てます。外観テンプレートとして、Buttons Containerを選択します。


作成した静的コンテンツのリージョンにボタンSTARTを作成します。動作アクションとして動的アクションで定義を選択し、カスタム属性としてdata-action="START"を設定します。この設定により、ボタンをクリックするとAPEXアクションとして定義された"START"が呼び出されます。


同様にボタンSTOPを作成します。詳細カスタム属性としてdata-action="STOP"を設定します。ボタンSTARTの右隣に配置するため、レイアウト新規行の開始オフにします。


静的アプリケーション・ファイルjs/app.jsの内容は以下になります。ボタンSTARTをクリックするとファンクションinitが呼び出されます。

OpenAIのドキュメントにあるサンプルに対して、以下のコードを追加しています。
  • 一時キーを取得するために、apex.server.processを使ってAjaxコールバックGET_EPHEMERAL_TOKENを呼び出しています。
  • データ・チャネルの状態をページ・アイテムP1_CONNECTION_STATEに表示しています。
  • OpenAI Realtime APIの音声応答のトランスクリプトを取り出して、ページ・アイテムP1_RESPONSEに表示しています。
  • WebRTCによる会話の開始と終了を、APEXアクションのSTARTとSTOPとして定義しています。

// instance of RTCDataChannel https://developer.mozilla.org/ja/docs/Web/API/RTCDataChannel
var dc = null;
// instaance of RTCPeerConnection https://developer.mozilla.org/ja/docs/Web/API/RTCPeerConnection
var pc = null;
/*
* OpenAIのRealtime APIのモデルと、WebRTCによる会話を開始する。
*/
async function init() {
/*
* Get an ephemeral key from the database running APEX app.
*/
let EPHEMERAL_KEY = null;
await apex.server.process("GET_EPHEMERAL_TOKEN", {},
{
success: (data) => {
EPHEMERAL_KEY = data.client_secret.value;
}
}
);
// apex.debug.info("EPHEMERAL_KEY: ", EPHEMERAL_KEY);
// Create a peer connection
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]);
// Set up data channel for sending and receiving events
dc = pc.createDataChannel("oai-events");
dc.addEventListener("message", (e) => {
// Realtime server events appear here!
console.log(e.data);
/*
* レスポンス完了時の音声データのトランスクリプトを
* APEXアプリケーションのP1_RESPONSEにセットする。
*/
const data = JSON.parse(e.data);
if ( data.type === "response.done" ) {
if ( data.response.status === "completed" ) {
if ( data.response.output.length === 0 ) {
return;
};
const content = data.response.output[0].content;
const audioContent = content.find(item => item.type === 'audio');
if ( audioContent != null ) {
apex.item("P1_RESPONSE").setValue(audioContent.transcript);
}
const textContent = content.find(item => item.type === 'text');
if ( textContent != null ) {
apex.item("P1_RESPONSE").setValue(textContent.text);
}
} else if ( data.response.status === "failed" ) {
apex.item("P1_RESPONSE").setValue( data.response.status_details.error.message );
}
}
});
/*
* データチャンネルの状態をページ・アイテムP1_CONNECTION_STATEにセットする。
*/
dc.addEventListener("open", (element) => {
apex.item("P1_CONNECTION_STATE").setValue("open");
});
dc.addEventListener("close", (element) => {
apex.item("P1_CONNECTION_STATE").setValue("close");
});
// Start the session using the Session Description Protocol (SDP)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const baseUrl = "https://api.openai.com/v1/realtime";
const model = "gpt-4o-realtime-preview-2024-12-17";
const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
method: "POST",
body: offer.sdp,
headers: {
Authorization: `Bearer ${EPHEMERAL_KEY}`,
"Content-Type": "application/sdp"
},
});
const answer = {
type: "answer",
sdp: await sdpResponse.text(),
};
await pc.setRemoteDescription(answer);
}
/*
* ボタンSTARTとSTOPを押した時に、OpenAIのRealtime APIのモデルとの会話を開始または終了する。
*/
const controls = apex.actions.createContext("controls", document.getElementById("CONTROLS"));
controls.add([
{
"name": "START",
action: (event, element, args) => {
init();
}
},
{
"name": "STOP",
action: (event, element, args) => {
pc.close();
}
},
{
"name": "SEND",
action: (event, element, args) => {
const message = apex.item("P1_TEXT").getValue();
const responseCreate = {
type: "response.create",
response: {
modalities: ["text"],
instructions: message
},
};
dc.send(JSON.stringify(responseCreate));
}
}
]);
view raw app.js hosted with ❤ by GitHub

以上でアプリケーションは完成です。

Realtime APIの呼び出しは、デバッグしているとクレジットがどんどん減るため、あまりデバッグできていません。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-openai-webrtc.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。