OpenAI Agents SDKを活用した音声対話型雑学アシスタントの構築

## はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/a7534da2-77f5-4f95-bbaf-7b988a623c38.png)

OpenAIが提供する「Agents SDK」は、AIエージェントを簡単に構築できるフレームワークです。今回はその機能の一つである「Voice Pipeline」を使って、音声でエージェントと対話できる雑学アシスタントをWindows環境で実装する方法を解説します。

この記事では以下のことが学べます：
- Agents SDKの基本的な使い方
- 音声認識から音声合成までのパイプライン構築
- 複数言語対応エージェントの実装
- 映画情報、一般雑学、言語学習に関する機能の実装
- Windows環境での開発環境セットアップとマイク入力の活用

## Agents SDKと音声パイプラインの概要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/919c0984-aaf0-4369-bd11-a8f0d1c2afe1.png)

Agents SDKの音声機能「Voice Pipeline」は、以下の3つのステップで構成されています：

1. **Transcribe (音声認識)**: マイクからの音声入力をテキストに変換
2. **Agent処理**: エージェントがテキストを処理し応答を生成
3. **Text-to-Speech (音声合成)**: テキスト応答を音声に変換してスピーカーから出力

## Windows環境のセットアップ

### 1. Pythonのインストール

まだPythonをインストールしていない場合は、[Python公式サイト](https://www.python.org/downloads/windows/)からWindows用のインストーラをダウンロードしてインストールしましょう。バージョン3.8以上を推奨します。インストール時に「Add Python to PATH」にチェックを入れることを忘れないでください。

### 2. 仮想環境の作成

まずは専用の仮想環境を作成します。コマンドプロンプトまたはPowerShellを開き、以下のコマンドを実行します：

```bash
# プロジェクトディレクトリの作成
mkdir voice-agent-project
cd voice-agent-project

# 仮想環境の作成と有効化
python -m venv venv
venv\Scripts\activate
```

### 3. 必要なライブラリのインストール

次に、必要なライブラリをインストールします：

```bash
# OpenAI Agents SDKと音声処理用のライブラリをインストール
pip install openai-agents[voice] sounddevice numpy
```

Windows環境では、`sounddevice`ライブラリが依存している`PortAudio`も自動的にインストールされるため、別途インストールは不要です。

### 4. APIキーの設定

OpenAIのAPIキーを環境変数に設定します。Windows環境では以下のコマンドを使用します：

```bash
# コマンドプロンプトの場合
set OPENAI_API_KEY=your_api_key_here

# PowerShellの場合
$env:OPENAI_API_KEY = "your_api_key_here"
```

Windowsの「システムのプロパティ」→「環境変数」から設定できます。
または、開発中は以下のようにPythonコード内で直接設定することもできます：

```python
import os
os.environ["OPENAI_API_KEY"] = "your_api_key_here"
```

## 継続的な音声対話型雑学アシスタントの実装

すべての機能を1つのファイルにまとめた、シンプルな継続的対話システムを実装します：

```python
# trivia_assistant.py
import asyncio
import numpy as np
import sounddevice as sd
import os
import random

from agents import (
    Agent,
    function_tool,
)
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
from agents.voice import (
    AudioInput,
    SingleAgentVoiceWorkflow,
    VoicePipeline,
)

# 環境変数にAPIキーを設定（必要な場合）
# os.environ["OPENAI_API_KEY"] = "your_api_key_here"

# 映画情報ツールの実装
@function_tool
def get_movie_trivia(genre: str = None) -> str:
    """指定されたジャンルの映画に関する雑学やトリビアを提供します。"""
    print(f"[debug] get_movie_trivia called with genre: {genre}")
    
    trivia_by_genre = {
        "アクション": [
            "「ダイ・ハード」は当初、フランク・シナトラに主役のオファーが出されていた。",
            "「マトリックス」の撮影前に、主要キャストは6ヶ月間の武術トレーニングを受けた。",
            "「ミッション:インポッシブル」シリーズでは、トム・クルーズは多くのスタントを自分で行っている。"
        ],
        "コメディ": [
            "「ホーム・アローン」の悪役コンビは、実際に本物のレンガで叩かれるシーンを撮影した。",
            "「アンカーマン」の撮影では、即興のセリフが多く、使われなかった素材だけで別の映画が作れるほどだった。",
            "「ボラット」の多くのシーンは、一般人が本物のドキュメンタリーだと思い込んでいる状態で撮影された。"
        ],
        "SF": [
            "「2001年宇宙の旅」の制作時、スタンリー・キューブリックはNASAに協力し、後の月面着陸計画にも影響を与えた。",
            "「ブレードランナー」には7つの異なるバージョンが存在する。",
            "「インターステラー」の制作では、実際の物理学者がブラックホールの正確な視覚表現を作るのを手伝った。"
        ],
        "ホラー": [
            "「エクソシスト」の撮影中、セットが原因不明の火災で焼失するなど不可解な出来事が多発した。",
            "「シャイニング」の有名な「ここだよ！」のシーンは、ジャック・ニコルソンの即興だった。",
            "「サイコ」の有名なシャワーシーンの撮影には1週間かかり、70回以上のカメラアングルが使われた。"
        ],
        "ロマンス": [
            "「タイタニック」の船上でのジャックとローズのシーンは、実際にはグリーンスクリーンではなく、実物大のセットで夕暮れ時に撮影された。",
            "「プリティ・ウーマン」は当初、主人公が麻薬中毒で終わるダークな物語だった。",
            "「恋愛小説家」のメグ・ライアンの有名なレストランでのシーンは、彼女が即興で演じたものだった。"
        ]
    }
    
    # ジャンルが指定されていない場合はランダムに選択
    if not genre or genre not in trivia_by_genre:
        all_trivia = []
        for g_trivia in trivia_by_genre.values():
            all_trivia.extend(g_trivia)
        return random.choice(all_trivia)
    
    return random.choice(trivia_by_genre[genre])

# 一般雑学ツールの実装
@function_tool
def get_fun_fact() -> str:
    """様々な分野の面白い雑学や豆知識を提供します。"""
    print("[debug] get_fun_fact called")
    
    fun_facts = [
        "猫は1日の大半（約70%）を睡眠に費やしています。",
        "サメは泳ぎ続けなければ窒息してしまいます。",
        "人間の小腸の長さは約7メートルです。",
        "蜂は数学的に完璧な六角形の巣を作ります。",
        "1年間に世界中で生産されるチーズの量は、エッフェル塔約10個分の重さに相当します。",
        "アインシュタインは数学が苦手だったという都市伝説は間違いで、実際は幼少期から数学に秀でていました。",
        "オーストラリアには実際にビーチより多くの砂漠があります。",
        "人間の体内には宇宙の星の数より多くの細菌が存在します。",
        "バナナは実は木ではなく、世界最大の草の一種です。",
        "ゴリラは1日に最大27kgの植物を食べることがあります。"
    ]
    
    return random.choice(fun_facts)

# 言語学習ツールの実装
@function_tool
def get_language_fact(language: str = None) -> str:
    """様々な言語に関する興味深い事実を提供します。"""
    print(f"[debug] get_language_fact called with language: {language}")
    
    language_facts = {
        "日本語": [
            "日本語には、季節を表す「季語」が2000以上存在します。",
            "「旨い」という言葉は、味だけでなく、技術や方法が効果的であることも意味します。",
            "日本語の「木」という字は、実際に木の形を表しています。"
        ],
        "英語": [
            "英語で最も長い単語は「pneumonoultramicroscopicsilicovolcanoconiosis」で、45文字あります。",
            "シェイクスピアは1700以上の新しい単語や表現を英語に導入したと言われています。",
            "「I am」は英語で最も使用頻度の高い2語の組み合わせです。"
        ],
        "フランス語": [
            "フランス語には17の異なる動詞の時制があります。",
            "フランス語アカデミーは、新しい単語の公式な採用を管理する機関で、1635年に設立されました。",
            "フランス語では、すべての名詞に文法上の性別（男性または女性）があります。"
        ],
        "中国語": [
            "中国語の文字（漢字）は表意文字であり、発音ではなく意味を表します。",
            "標準中国語（普通話）には約400の異なる音節しかありません。",
            "中国の漢字は8000年以上前から使われており、世界最古の継続的に使用されている文字体系です。"
        ]
    }
    
    # 言語が指定されていない場合はランダムに選択
    if not language or language not in language_facts:
        all_facts = []
        for l_facts in language_facts.values():
            all_facts.extend(l_facts)
        return random.choice(all_facts)
    
    return random.choice(language_facts[language])

# 日本語対応エージェント
japanese_agent = Agent(
    name="Japanese",
    handoff_description="日本語で対応するエージェント",
    instructions=prompt_with_handoff_instructions(
        "あなたは人間と会話しています。丁寧かつ簡潔に日本語で応答してください。"
        "ユーザーが映画の雑学を求めている場合は get_movie_trivia ツールを、"
        "一般的な雑学を求めている場合は get_fun_fact ツールを、"
        "言語に関する雑学を求めている場合は get_language_fact ツールを使用してください。"
    ),
    model="gpt-4o-mini",
    tools=[get_movie_trivia, get_fun_fact, get_language_fact],
)

# メインエージェント
main_agent = Agent(
    name="Assistant",
    instructions=prompt_with_handoff_instructions(
        "あなたは人間と会話しています。丁寧かつ簡潔に応答してください。"
        "ユーザーが日本語で話した場合は、Japanese エージェントに引き継いでください。"
        "ユーザーが映画の雑学を求めている場合は get_movie_trivia ツールを、"
        "一般的な雑学を求めている場合は get_fun_fact ツールを、"
        "言語に関する雑学を求めている場合は get_language_fact ツールを使用してください。"
    ),
    model="gpt-4o-mini",
    handoffs=[japanese_agent],
    tools=[get_movie_trivia, get_fun_fact, get_language_fact],
)

# マイクから音声を録音する関数
def record_audio(duration=5, sample_rate=24000):
    """指定した秒数の音声をマイクから録音する"""
    print(f"{duration}秒間、マイクに向かって話してください...")
    recording = sd.rec(int(duration * sample_rate), 
                     samplerate=sample_rate, 
                     channels=1, 
                     dtype=np.int16)
    sd.wait()  # 録音完了まで待機
    print("録音完了！処理中...")
    return recording

# 音声対話を実行する関数
async def run_voice_conversation():
    # 音声パイプラインの作成
    pipeline = VoicePipeline(workflow=SingleAgentVoiceWorkflow(main_agent))
    
    print("\n===== 雑学・映画・言語情報アシスタント =====")
    print("このアシスタントでできること：")
    print("- 映画の雑学を提供（例:「アクション映画について教えて」）")
    print("- 面白い雑学を提供（例:「面白い雑学を教えて」）")
    print("- 言語に関する情報を提供（例:「日本語の面白い事実は？」）")
    print("- 日本語と英語の両方で対応")
    print("=======================================\n")
    
    try:
        while True:
            # マイクから音声を録音
            buffer = record_audio(duration=5)  # 5秒間の録音
            audio_input = AudioInput(buffer=buffer)
            
            # パイプラインを実行
            result = await pipeline.run(audio_input)
            
            print("AIの応答を再生中...")
            
            # 音声出力用のプレーヤーを準備
            player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16)
            player.start()
            
            # 生成された音声をストリーミング再生
            async for event in result.stream():
                if event.type == "voice_stream_event_audio":
                    player.write(event.data)
            
            player.stop()
            
            # 続けるかどうかを確認
            continue_convo = input("\n会話を続けますか？ (y/n): ")
            if continue_convo.lower() != 'y':
                print("会話を終了します。お疲れ様でした！")
                break
                
    except KeyboardInterrupt:
        print("\n会話を終了します。")

# メイン実行部分
if __name__ == "__main__":
    asyncio.run(run_voice_conversation())
```

## 実行方法

プロジェクトディレクトリ内で以下のコマンドを実行します：

```bash
python trivia_assistant.py
```

## 対話例

このアシスタントは以下のような対話が可能です：

1. 映画トリビアを尋ねる：
   - 「映画について教えて」→ ランダムな映画の雑学を提供
   - 「アクション映画の面白い話は？」→ アクション映画に関するトリビア
   - 「ホラー映画の裏話を教えて」→ ホラー映画に関するトリビア

2. 一般雑学を尋ねる：
   - 「面白い雑学を教えて」→ 様々な分野のランダムな雑学を提供
   - 「何か知らない事実を教えて」→ 興味深い雑学を教えてくれる

3. 言語に関する情報を尋ねる：
   - 「日本語について教えて」→ 日本語に関する面白い事実
   - 「英語の特徴は？」→ 英語に関する興味深い情報
   - 「フランス語の特殊な点は？」→ フランス語の特徴について説明

もちろん、これ以外の一般的な会話も可能です。日本語と英語の両方で対応しているため、どちらの言語でも自然に会話できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d234476d-d3f4-4412-add7-4935bdef96cf.png)

入力: おすすめのアクション映画は？
映画の雑学提供のエージェントで指定されたジャンル (アクション) から回答している。


<iframe width="560" height="315" src="https://www.youtube.com/embed/LcQPOBgcUcU?si=Bt37I7p0JCAspYMs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## トラブルシューティング

### マイク入力が検出されない場合

Windowsでは複数のオーディオデバイスが存在する場合があります。使用可能なデバイスを確認するには以下のコードを実行してください：

```python
import sounddevice as sd

print("利用可能なオーディオデバイス:")
print(sd.query_devices())

# デフォルトのデバイスを確認
print("\nデフォルト入力デバイス:", sd.query_devices(kind='input'))
print("デフォルト出力デバイス:", sd.query_devices(kind='output'))
```

特定のデバイスを使用する場合は、以下のようにデバイスを指定できます：

```python
def record_audio(duration=5, sample_rate=24000, device=None):
    """指定したデバイスで録音する"""
    recording = sd.rec(int(duration * sample_rate), 
                     samplerate=sample_rate, 
                     channels=1, 
                     dtype=np.int16,
                     device=device)  # デバイスを指定
    sd.wait()
    return recording
```

### その他の一般的な問題と解決策

1. **PortAudio関連のエラー**:
   - 最新版の`sounddevice`をインストール: `pip install --upgrade sounddevice`
   - Windowsの場合、Visual C++ Redistributableのインストールが必要な場合があります

2. **APIキーの問題**:
   - APIキーが正しく設定されているか確認
   - OpenAIのアカウントに十分な利用枠があるか確認

3. **音声品質の問題**:
   - サンプルレートを調整（例: 16000Hzに変更）
   - 静かな環境で録音する
   - マイクの距離を調整する

4. **言語認識の問題**:
   - はっきりと話す
   - バックグラウンドノイズを減らす
   - 録音時間を長くする（5秒→10秒など）

## 拡張アイデア

このコードをベースに、さまざまな拡張が可能です：

1. **コンテンツの拡充**:
   - 映画データベースを拡充（より多くの映画やジャンルに対応）
   - 科学、歴史、文学など他の分野の雑学を追加
   - クイズモードの実装（雑学問題を出題して回答を受け付ける）

2. **UI/UX改善**:
   - PyQtやTkinterを使ったグラフィカルインターフェースの実装
   - ボタン操作で録音開始/停止を制御
   - 会話履歴の表示と保存機能

3. **機能拡張**:
   - 会話コンテキストの維持（前の会話を覚えておく）
   - 音声認識の精度向上（ノイズキャンセリングの実装）
   - 複数言語対応の拡充（フランス語、スペイン語など）
   - カスタムウェイクワード対応（「OK、アシスタント」など）

4. **パフォーマンス向上**:
   - 応答速度の最適化
   - より軽量なモデルへの切り替えオプション

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/230601ba-6756-4f88-b397-9c656d2c9180.png)

Windows環境でOpenAI Agents SDKを使った音声対話型雑学アシスタントの実装方法を解説しました。主なポイントは以下の通りです：

- LLMの知識を活用した雑学エージェントが簡単に構築できる
- 映画トリビア、一般雑学、言語学習といった楽しい機能を実装できる
- 音声認識→エージェント処理→音声合成のパイプラインがAgents SDKで簡単に構築できる
- 複数言語対応や専門エージェントへの引き継ぎも容易に実装可能
- `sounddevice`ライブラリを使ってWindows環境でもマイク入力を簡単に取得できる

## 参考リンク

- [OpenAI Agents SDK公式ドキュメント](https://openai.com/ja-JP/index/new-tools-for-building-agents/)
- [Python sounddeviceライブラリ](https://python-sounddevice.readthedocs.io/)
- [PortAudio公式サイト](http://www.portaudio.com/)

---

本記事で使用したコードは、OpenAIのサンプルコードを参考に作成しています。実際の利用にあたっては、OpenAIのガイドラインに従ってください。