ImageFlux Live StreamingでChatGPTとの会話をライブ配信

こんにちは、テリーです。待望のNVIDIA RTX5090は手に入りましたか?価格はとんでもないことになっていますが、品薄のようです。AIの進化は相変わらず絶好調で、トレンドを追いかけるだけでも大変なくらい広範囲に広がっています。ChatGPTのライバルも続々と登場し、価格競争が激しいのはユーザーとしてはありがたいところです。

昨年秋にOpenAIは音声版のLLM「RealTime API」を公開しました。人間の声を入力すると、STT(Speech To Text)とTTS(TextToSpeech)を介さずに音声で応答を返します。Siriと違うのは、自社固有の情報を差し込むなど、アプリケーションに組み込めるところです。ブラウザからの接続にはWebRTCが使われています。WebRTCが使えるマイコンからでも利用できるサンプルが公開されており、家電やロボットの音声対話機能の実装が今年は急拡大するだろうと予測されます。

さて、WebRTCと言えばImageFlux Live Streamingでも使われています。ChatGPTと会話している様子を第三者にライブ配信したい場合、どのように実装すれば良いか、興味が沸きましたので作ってみました。

動作確認環境

  • Chrome 133.0.6943.54
  • openai-realtime-console 2月9日付 コミットID a6b2611

RealTime APIのサンプルを解説

openai-realtime-console が最もシンプルな公式サンプルです。まずはオリジナルのコードを実行し、動作を確認します。

git clone https://github.com/openai/openai-realtime-console
cd openai-realtime-console
npm i
.envを修正して環境変数 OPENAI_API_KEY を設定
npm run dev

ブラウザで http://localhost:3000/ にアクセスし、ChatGPTと会話ができることを確認します。

ChatGPTとの接続部分のコードを解説します。

client/components/App.jsxというファイルを見て下さい。14行目付近のstartSession関数がマイクデバイスのオープンとWebRTCの接続をしています。

14.	  async function startSession() {
15.	    // Get an ephemeral key from the Fastify server
16.	    const tokenResponse = await fetch("/token");
17.	    const data = await tokenResponse.json();
18.	    const EPHEMERAL_KEY = data.client_secret.value;
19.	
20.	    // Create a peer connection
21.	    const pc = new RTCPeerConnection();
22.	
23.	    // Set up to play remote audio from the model
24.	    audioElement.current = document.createElement("audio");
25.	    audioElement.current.autoplay = true;
26.	    pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
27.	
28.	    // Add local audio track for microphone input in the browser
29.	    const ms = await navigator.mediaDevices.getUserMedia({
30.	      audio: true,
31.	    });
32.	    pc.addTrack(ms.getTracks()[0]);
33.	
34.	    // Set up data channel for sending and receiving events
35.	    const dc = pc.createDataChannel("oai-events");
36.	    setDataChannel(dc);
37.	
38.	    // Start the session using the Session Description Protocol (SDP)
39.	    const offer = await pc.createOffer();
40.	    await pc.setLocalDescription(offer);
41.	
42.	    const baseUrl = "https://api.openai.com/v1/realtime";
43.	    const model = "gpt-4o-realtime-preview-2024-12-17";
44.	    const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
45.	      method: "POST",
46.	      body: offer.sdp,
47.	      headers: {
48.	        Authorization: `Bearer ${EPHEMERAL_KEY}`,
49.	        "Content-Type": "application/sdp",
50.	      },
51.	    });
52.	
53.	    const answer = {
54.	      type: "answer",
55.	      sdp: await sdpResponse.text(),
56.	    };
57.	    await pc.setRemoteDescription(answer);
58.	
59.	    peerConnection.current = pc;60.	 
60.	  }
16.	    const tokenResponse = await fetch("/token");
17.	    const data = await tokenResponse.json();
18.	    const EPHEMERAL_KEY = data.client_secret.value;

16行目から18行目はChatGPTのRealTime API特有の処理です。API_KEYの値をブラウザに直接持たせると不正に無制限に利用されてしまうため、有効期間が60秒のワンタイムパスワードを発行して、そのワンタイムパスワード文字列を受け取っています。

ワンタイムパスワードの発行自体は server.js の18〜41行目でサーバ側で行っています。サーバ側には環境変数としてAPI_KEYの値を持たせています。

このワンタイムパスワードの発行の仕組みにより、仮にワンタイムパスワードが漏洩したとしても、すぐに使えなくなります。

本サンプルではワンタイムパスワードを発行するAPI /token が誰でもアクセスできるようになっていますが、ユーザー認証をこの前段に行い、特定できる登録済みユーザーに /token へのアクセスを許可する処理が必要です。

24.	    audioElement.current = document.createElement("audio");
25.	    audioElement.current.autoplay = true;
26.	    pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);

24行目から26行目は、オーディオの再生処理です。ChatGPTとのWebRTC接続が成功した直後にontrackイベントが呼ばれます。そのtrackを<audio>タグと関連付け、ブラウザから音声が再生されます。<audio>タグは動的に生成されますが、画面上には表示されません。

29.	    const ms = await navigator.mediaDevices.getUserMedia({
30.	      audio: true,
31.	    });
32.	    pc.addTrack(ms.getTracks()[0]);
33.	
34.	    // Set up data channel for sending and receiving events
35.	    const dc = pc.createDataChannel("oai-events");
36.	    setDataChannel(dc);
37.	
38.	    // Start the session using the Session Description Protocol (SDP)
39.	    const offer = await pc.createOffer();
40.	    await pc.setLocalDescription(offer);

29行目はマイクデバイスのオープン処理です。ここではパソコンのOSで設定されたデフォルトマイクが暗黙的に指定されています。

32行目から40行目では、WebRTC接続に必要なOffer SDPを生成しています。マイクを追加し、データチャンネルを追加しています。カメラデバイスを追加していないので、ビデオの通信は行われません。

42.	    const baseUrl = "https://api.openai.com/v1/realtime";
43.	    const model = "gpt-4o-realtime-preview-2024-12-17";
44.	    const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
45.	      method: "POST",
46.	      body: offer.sdp,
47.	      headers: {
48.	        Authorization: `Bearer ${EPHEMERAL_KEY}`,
49.	        "Content-Type": "application/sdp",
50.	      },
51.	    });
52.	
53.	    const answer = {
54.	      type: "answer",
55.	      sdp: await sdpResponse.text(),
56.	    };
57.	    await pc.setRemoteDescription(answer);

42行目から51行目では作成したOffer SDPをChatGPTのWHIPサーバにPOSTしています。48行目のAuthorizationヘッダでは、18行目で受け取ったワンタイムパスワードを使用しています。

fetchをして受け取ったレスポンスにはAnswer SDPが書かれています。57行目のようにsetRemoteDescription関数にAnswer SDPをセットすることでWebRTCの接続処理は完了し、データのやりとりが始まります。

これを元にImageFlux Live Streamingへ配信を行います。

ImageFlux Live Streamingに配信

次にImageFlux Live StreamingにChatGPTの音声を配信してみましょう。

プロジェクトにsora-js-sdkライブラリを追加します。

npm i sora-js-sdk

次にWebページApp.jsxの修正です。

6.	import Sora from "sora-js-sdk";
7.	
8.	export default function App() {
9.	  const sora = useRef(null);
10.	  const publisher = useRef(null);
11.	  const pub_stream = useRef(null);

まずファイル先頭でsora-js-sdkをimportします。次にsoraの関連変数を保持する変数を宣言します。

61.	   await pc.setRemoteDescription(answer);
62.	
63.	    peerConnection.current = pc;
64.	    // ここから下を追加
65.	    const signalingUrl = "your_signaling_url";
66.	    const channelId = "your_channel_id";
67.	    pub_stream.current = new MediaStream([peerConnection.current?.getReceivers()[0].track]);
68.	    const options = {};
69.	    sora.current = Sora.connection(signalingUrl, false);
70.	    publisher.current = sora.current.sendonly(channelId, null, options);
71.	    await publisher.current.connect(pub_stream.current);
72.	  }

63行目のpeerConnection.current = pc;の後に、ImageFlux Live Streamingへの接続処理を追加します。signaling_urlとchannel_idは所定の手続きで取得します。取得方法については省略します。

67行目が今回の記事の最大のポイントです。ChatGPTとブラウザ間のWebRTC接続に利用されているPeerConnectionの受信オーディオトラックを参照するにはこのように書きます。

そのオーディオトラックをMediaStreamでラップし、ImageFlux Live Streamingの配信に使用することで、受信したChatGPTの音声をそのままImageFlux Live Streamingに配信することができます。

75.	  function stopSession() {
76.	    publisher.current?.disconnect(); // 追加
77.	
78.	    if (dataChannel) {
79.	      dataChannel.close();
80.	    }

stopSession関数の先頭、disconnect関数を呼び、ImageFlux Live Streamingへの接続を切断します。

マイク音声とChatGPT音声をミキシングして配信

上記の修正だけでは、ChatGPTの音声だけの配信となるため、間延びして意味がわかりません。マイク音声とChatGPT音声をミキシングして配信することで、会話をしている感じが伝わります。詳しくはこちらの記事を参照ください。

7.	import Mixer from "./Mixer";
8.	
9.	export default function App() {
10.	  const mixer = useRef(null);

前述の記事のMixer.tsをApp.jsxと同じフォルダに作成し、7行目でimportします。10行目で変数を宣言します。

73.	    const signalingUrl = "your_signaling_url";
74.	    const channelId = "your_channel_id";
75.	    const chatgpt_stream = new MediaStream([peerConnection.current?.getReceivers()[0].track]);
76.	    const audioContext = new AudioContext();
77.	    mixer.current = new Mixer(audioContext);
78.	    mixer.current.append(ms);
79.	    mixer.current.append(chatgpt_stream);
80.	    pub_stream.current = mixer.current.getMixedStream();
81.	    const options = {};
82.	    sora.current = Sora.connection(signalingUrl, false);
83.	    publisher.current = sora.current.sendonly(channelId, null, options);
84.	    await publisher.current.connect(pub_stream.current);85.	  }

2つの音声をミキシングするためのクラスMixerを76,77行目で作成します。

78行目ではマイク音声のMediaStreamを追加、79行目ではChatGPTのMediaStreamを追加します。84行目のconnect関数に渡す引数には80行目で取得したミキシング済みのMediaStreamを指定します。

修正は以上です。ImageFlux Live StreamingのHLS配信機能を使用して、人とChatGPTが会話をしている様子を第三者に聞かせることができます。

サンプル

最後にライブ配信したサンプル動画をお見せします。このように一つの映像として配信・収録が可能です。

まとめ

ChatGPTのRealTime APIを使って会話している様子をImageFlux Live Streamingに配信するサンプルを紹介しました。本記事では音声のみですが、ChatGPTに相当するキャラクターを表示することでより、会話映像っぽくなります。文字起こしのデータを配信画面に表示することもできます。ぜひ挑戦してみてください。より詳しい実装方法に興味がある方はお気軽にご相談ください。

参考

ライブ配信エンジン ImageFlux LiveStreaming サービス紹介資料