【OpenAI RealtimeAPI】リアルタイム会議ファシリテートAIアプリを作る

# はじめに
こんにちは！
ノベルワークスでアルバイトをしているザワッチ([@zawatti](https://x.com/zawattizawawa))です。

今回は、年末年始にせこせこと作ってきたリアルタイム会議ファシリテートAIアプリを記事にします。

# アプリを作った背景
現地時間 2024年12月18日にOpenAI Realtime APIのアップデートにより、従来より低コストでかつリアルタイムでAIと会話できることを知りました。

https://x.com/OpenAIDevs/status/1869116812551692778

https://news.yahoo.co.jp/articles/b0c145d62e71e40563955af64d137d0973f676eb

このアップデートのポイントとして、
- 従来のgpt-4oに加え、gpt-4o-miniが対応
- API料金の大幅な値下げ
- 音声品質の向上・入力品質の信頼性を向上させるWebRTCに対応
※今までは、realtime apiはWebSocketを使用し通信していましたが、本アップデートでWebRTCが推奨となりました。

このアップデートにより、
- リアルタイムで会話できてかつ、低コストで使用できるようになったことで、今のこのRealtime APIでどれだけのことができるのだろうという興味                 
- gpt-4o-miniは低パラメータで学習されており、gpt-4oより計算コストも低く、生成速度も速いため、よりリアルタイム性を追求できるのかという疑問 
- 最新のリアルタイム音声技術に触れることで、今後どのような進化・プロダクトが出てくるかを考えたい

が本アプリを作った背景です。

# アプリ概要
本アプリのベースは、OpenAIが提供しているWebRTCを使用したRealtime APIのハウツーレポジトリを参考にして作成しました。

https://github.com/openai/openai-realtime-console

やりたいこととしては、
- AIが会議の進行を行う（とりあえず社内会議から）
- 「議事録をまとめてちょ」と言ったら、会議中にこれまで話した内容が議事録としてリアルタイムで生成される
- 前回の議事録で話している内容と異なることを言ったら、AIが検知して正してくれる

です。

具体的なユースケースとして、会議が始まったら本アプリを起動し、一緒に話し合いながら（まるで社員の一員として）会議を行っていくことが一例として挙げられます。

最終的には、バックエンド、フロントエンドを構築したのち、デプロイまで行います。

## バックエンド
ほとんど上記のレポジトリのコードを流用しています。

### 技術スタック
ベースは、Node.jsのWebフレームワークであるFastifyで構築し、React + Viteを統合してフロントエンドと連携しています。
Fastifyはプラグインアーキテクチャなため、拡張性が高いという利点があります。

### 機能
- <h4>Realtime APIと接続するための中継サーバ</h4>
Realtime APIとクライアントが接続するためには、一時的な認証キー(Ephemeral Key)が必要になります。そのため、クライアントのAPIKey（本アプリではサーバーサイドで持っている)をもとに、中継サーバがRealtime APIにリクエストし、認証キーをクライアント側に返して、Realtime APIとP2Pで通信します。
これ以降、Realtime APIと通信している間は、この中継サーバを経由することはありません。

- <h4>ファシリテートAIの設定</h4>
一時的な認証キーをリクエストするボディの中に、使用するモデル、instructionプロンプトを記述することができます。

``` server.js
      const r = await fetch("https://api.openai.com/v1/realtime/sessions", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          model: "gpt-4o-mini-realtime-preview-2024-12-17",
          voice: "verse",
          instructions: "あなたはこの会議に同席している弊社の社員です。この会議は<前回の議事録>の情報に基づいて、進んでいきます。<前回の議事録>を頼りに、弊社の社員をアシストし、この会議をファシリテートしてください。<前回の議事録>"+ content
        }),
      });
```

    
後述しますが、本アプリの最初にアップロードもしくローテキストを入力された前回の議事録を、文字列を抽出し、このプロンプトの中にぶち込みます。

- <h4>ユーザ認証</h4>
最終的にデプロイする都合上、だれでもアプリを利用できるとまずいので、簡易的なミドルウェアとして、Basic認証を実装しました。

## フロントエンド
こちらもベースは上記のリポジトリを流用していますが、いろいろカスタマイズしました。

### 技術スタック
WebフレームワークのReact、Viteをビルドツール、CSSフレームワークにはTailwind CSSを使用しています。

### 機能
- <h4>会議セッションの管理</h4>
会議セッションの開始と停止を制御するボタンを実装しました。
開始ボタンが押されると、サーバサイドに認証キーを取得するリクエストを送り、レスポンスされた認証キーをもとに、WebRTCでRealtime APIとP2P通信を行います。（この処理の部分は、レポジトリまたはAPIリファレンスを見ながら実装しました）
停止ボタンが押されると、Realtime APIとの通信が切られます。


- <h4>前回の議事録のアップロード</h4>
開始ボタンが押されると、モーダルが表示され、テキストファイルのアップロードもしくはテキストエリアに文字を入力することができます。
ここに、前回の議事録を入れると、ファシリテートAIはそれをコンテキストとして保持することができます。


- <h4>ファシリテートAIとユーザの会話ログの表示</h4>
会議中のイベント（ユーザーの発言内容の文字起こし、Realtime APIの生成レスポンス）を状態管理し、チャットUI風にユーザーの話した内容とファシリテートAIの生成内容をリアルタイムで表示します。

::: note info
Realtime APIには、ユーザーの音声データをもとに文字おこしを行い、リアルタイムでその文字列をレスポンスする設定ができます。

session.updateのリクエストボディに以下のオブジェクトを追加します。

``` session.jsx
  type: "session.update",
  session: {
    input_audio_transcription: {
      model: "whisper-1"
    },
    ...
```

:::

- <h4>Function Callingの実装</h4>
本アプリの核となる部分である<strong>Function Calling</strong>があります。

https://platform.openai.com/docs/guides/function-calling

Function CallingとはOpenAIのAPIによって実行されるモデルが開発者によって定義されている関数のセットを、会話履歴に基づいて適切なタイミングで使用させることができる仕組みです。

使用された関数の結果はアプリケーション側に返答され、再度モデルに返すことができます。
以下に、Function Callingのスキーマ例を記述します。

``` sample.js
 [
  {
      type: "function",
      function: {
          name: "get_delivery_date",
          description: "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'",
          parameters: {
              type: "object",
              properties: {
                  order_id: {
                      type: "string",
                      description: "The customer's order ID.",
                  },
              },
              required: ["order_id"],
              additionalProperties: false,
          },
      },
  },
]
```

重要なのは、スキーマ内の<strong>description</strong>で定義した関数が呼び出されるタイミングをしっかりと明記することです。

このアプリにおけるユーザーのインターラクティヴなデータ形式は、主に音声データであるので、○○と話された時などと明記するとその文言が話されたとき、Realtime APIは<strong>Function Calling</strong>を実行します

そして、<strong>parameters</strong>はこの関数が呼び出されたときに生成するレスポンス形式を意味しています。アプリケーション側にレスポンスされたとき、このデータをモデルに再度入力したり、このデータを違う処理のインプットにすることができます。

Function Callingのスキーマで、議事録の生成と前回の議事録の内容と誤ったことを言ったときに呼び出される関数を作成します。

議事録のコンテンツは
- 背景と課題
- 実現したいこと
- 次のミーティングまでにやるべきこと

と簡易的な構成をとっています。

``` sample.tsx
      {
        type: "function",
        name: "detect_unreal_sentences",
        description: "ユーザが<前回の議事録>に記載されている文章と異なる内容を話したときに呼び出される。",
        parameters: {
          type: "object",
          strict: true,
          properties: {
            unreal_sentences: {
              type: "string",
              description: "ユーザが話した誤った文章"
            },
            correct_sentences: {
              type: "string",
              description: "前回の議事録に記載されてる正しい文章"
            },
          },
          required: ["unreal_sentences", "correct_sentences"],
        },
      },
      {
        type: "function",
        name: "create_summary",
        description: "ユーザーが「議事録を作成してください」と言ったときに呼び出される",
        parameters: {
          type: "object",
          strict: true,
          properties: {
            purposes: {
              type: "string",
              description: "背景と課題"
            },
            functions: {
              type: "string",
              description: "実現したいこと"
            },
            next_actions: {
              type: "string",
              description: "次のミーティングまでにやるべきこと"
            }
          },
          required: ["purposes","functions", "next_actions"],
        },
      },
    ],
```
- <h4>生成された議事録表示</h4>
会議中に生成された議事録を状態管理し、New Summary!ボタンを押すと、サイドバーが開き、議事録が表示されます。

新しい議事録が表示されると、ボタンが点滅し、ユーザーが視覚的にわかりやすいようになっています。

- <h4>（おまけ）ランダムな文字列をタイプライター風に表示 </h4>
このアプリを使う目的やメリット、ユースケースなどをタイプライター風にランダムに表示して、海外のイケてるアプリ感を出そうとしました。

## デプロイ
本アプリは、フロントエンド・バックエンドの両方が存在するフルスタックアプリケーションに該当します。また、両方ともNode.jsを実行環境とします。

そして、これらの制約をクリアしてかつ、費用を抑えたいという気持ちがあります。

こういった条件にうまくマッチしているRenderというプラットフォームは、フルスタックアプリケーションの実行環境がいい感じに整えられていて、簡単に素早くデプロイできます。

https://render.com/

Renderにもいろいろプランがあり、フリープランの特徴として、

- RAMが512MB
- GitHubの特定のブランチにpushされたら、自動でCI/CD
- マネージドTLS
- アプリケーションログの監視

など至れり尽くせりです。

https://render.com/pricing

# アプリ動作
実際に作成したアプリ動作をGif形式でお見せします。

## ホーム画面
ページにアクセスすると、タイプライター風にランダムで文字が表示されます。
Start Sessionを押すと、アップロードモーダルが表示されます。

![RealtimeFacilitateAI.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/b00d02ad-9e60-5878-8565-8f2b900db81e.gif)

## アップロードモーダル画面
アップロードモーダル画面で、前回の議事録ファイルもしくはローテキストデータを入力し、Submitボタンを押すと、いよいよファシリテートAIが動き出します。

![RealtimeFacilitateAI_upload_modal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/0eb546ee-de3f-f8a3-ff74-b562e1e023f8.gif)

## チャット画面
音声認識の精度はかなり荒いですが、LLMの文脈予測でなんやかんやで耐えている感はあります。

![RealtimeFacilitateAI_chatui.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/6c4906c9-1272-9bfe-a781-b85d75fe707f.gif)

## 議事録生成画面
「議事録を作成してください」と言うと、これまでのチャット履歴をもとに、議事録を生成します。

![RealtimeFacilitateAI_CreateSumary.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/2a5a1aa3-0a8a-e7ab-cd4f-e50efaeda1bc.gif)

## 議事録表示画面
Function Callingにおいて定義したレスポンス形式で表示されています。
サイドバーが録画で表示されなかったので、画像形式になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/7a63bdb7-fed3-40d9-1b28-61a0631ee117.png)

数十分に一回など、定期的に議事録生成させておくと、コンテキストの欠落も起きなさそうな感じがします（チャット管理はRealtime API依存なので、詳しくはわからない）

## 嘘検出画面
前回の議事録に書いてあることとは別のことを話すと、違うこと言ってますよと指摘してくれます。

![RealtimeFacilitateAI_Detect.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478853/d434d7d1-89e2-3ca3-9d7d-f01330611d1a.gif)


# おわりに
今回は、Realtime APIを使用して、リアルタイム会議ファシリテートAIを作りました。
gpt-4o-miniでもここまでできちゃうのかと正直驚きました。
Function callingの関数をうまく調整することで、様々な用途にあったリアルタイムAI会話アプリを作れるということが実感できました。（出力形式を固定化できるところがかなりいい）
また、Facilitate AIの音声出力するタイミングを調整して、話している人を遮らないようにすることでより人感を出せそうです。
今度は、エッジコンピュータでRealtime APIと通信するサーバーを立ち上げ、音声入出力機能を搭載させて、よりミニマムに動作させてみようと思います。