Twilio Voice、OpenAIのRealtime API、Pythonを使用してAI音声アシスタントを構築

August 28, 2025
執筆者
Paul Kamp
Twilion
レビュー担当者
Dominik Kundel
寄稿者
Twilio の寄稿者によって表明された意見は彼ら自身のものです

Twilio Voice、OpenAIのRealtime API、Pythonを使用してAI音声アシスタントを構築

OpenAIから、革新的なRealtime APIが一般公開されました。GPT Realtimeモデルのマルチモーダル機能を活かしたこのRealtime APIなら、音声から音声への直接的な対話(Speech to Speech、S2S)が可能です。

S2Sモデルは、音声認識(SST)やテキスト読み上げ(TTS)の変換工程を省くことなどで、レイテンシーの改善を可能にします。これにより、人間とのインタラクションに近い、極めて自然なAI対話を実現するアプリケーションの構築が可能となります。OpenAIとのコラボレーションによって実現したこの統合機能を、今回のローンチで皆さまに提供できることを心から嬉しく思っています。

このチュートリアルでは、PythonとFastAPI Webフレームワークを使用し、Twilio VoiceOpenAI Realtime APIでAI音声アシスタントを構築する方法を解説します。Twilio Media Streamサーバーをセットアップして、通話の音声を受信し、それをOpenAI Realtime APIで処理した後、AIによる音声応答をTwilio経由で発信者に送り返します。構築後は、会話や質問応答からジョークまで、どんな内容にも対応できるようになります。

それでは、構築手順を見ていきます。

このアプリは、Code Exchangeで既製のアプリケーションとしても入手可能です。こちらで確認できます。

ここでは、Pythonを使用してVoice AI Assistantへ実際に発信する方法を紹介します。

前提条件

このチュートリアルを進めるには、以下が必要です。

  • Python 3.9以降 (このチュートリアルではバージョン3.9.13を使用)
  • Twilioアカウント。お持ちでない場合は、こちらから無料トライアルに登録できます。
  • Voice機能を備えたTwilio番号。電話番号の購入手順はこちら
  • OpenAIアカウントとOpenAI APIキー。こちらからサインアップできます。
  • OpenAI Realtime APIアクセス。詳細はこちらをご覧ください
  • (オプション)ngrokなどのトンネリングソリューションを使用してローカルサーバーをインターネットに公開し、テストを行います。ngrokはこちらからダウンロードしてください。

先に進む前に、上記の準備が完了していることを確認してください。それでは始めましょう。

Realtime API speech-to-speech Pythonプロジェクトの設定

以下の手順では、プロジェクトの設定、必要な依存関係のインストール、サーバーコードの書き込みを行います。ステップごとに重要なパートを中心に解説していきます。

こちらでTwilioのレポジトリもご覧いただけます。チュートリアルのビデオバージョンも以下にご用意しています。

ステップ1: プロジェクトの初期化

まず、新しいPythonプロジェクトを設定し、開発マシンで混乱が起きないように仮想環境を作成しましょう。コマンドラインに以下を入力します。

mkdir speech-assistant-openai-realtime-api-python
cd speech-assistant-openai-realtime-api-python
python3 -m venv venv
source venv/bin/activate

ステップ2: 依存関係のインストール

次に、プロジェクトに必要な依存関係のインストールが必要です。以下のコマンドを実行します。これについては後で説明します。

pip install fastapi uvicorn python-dotenv websockets twilio

TwilioとOpenAIによるWebSocketの処理にはWebSocketライブラリが必要です。またh、環境変数の読み込にはpython-dotenv、Twilioへの指示の構成にはtwilioが必要です。

fastapiは、このチュートリアルの作成に使用したPythonのWebフレームワークです。Pythonコミュニティでは、他にもFlaskDjangoPyramidなどがよく使われています。

サーバーにはuvicornを使用します。これは非同期アプリケーションに最適な、極めて軽量なサーバーです。実際に試してみれば納得していただけるはずです。

ステップ3: プロジェクトファイルの作成

次にファイルを作成します。メインコードとサーバーロジック用のmain.pyファイル、OpenAI APIキー保存用.envファイルの2つです。(作成方法はPython環境変数に関する記事で詳しく解説しています。)

ステップ3.1: main.pyファイルの作成

次のコマンドを実行します。

touch main.py

ステップ3.2: .envファイルの作成

まず、.envファイルを作成します。

touch .env

次に、そのファイルをテキストエディタで開き、OpenAI Realtime APIキーを追加します。

OPENAI_API_KEY=your_openai_api_key_here

your_openai_api_key_hereの部分はご自身のAPIキーに置き換えてください。)

ステップ4: サーバーコードの構築

これで足場が整いました。

サーバーコードはいくつかのステップに分けて作成していきます。ステップごとに関連するコードを紹介し、その後、コードの難しい部分についてはできる限り簡潔に解説します。

ステップ4.1: 依存関係のインポートと環境変数の読み込み

main.pyファイルの先頭で、必要なモジュールをインポートします。次に.envファイルから環境変数を設定して読み込みます。

以下のコードをmain.pyの先頭に貼り付けてください。

import os
import json
import base64
import asyncio
import websockets
from fastapi import FastAPI, WebSocket, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.websockets import WebSocketDisconnect
from twilio.twiml.voice_response import VoiceResponse, Connect, Say, Stream
from dotenv import load_dotenv
load_dotenv()
# Configuration
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') # requires OpenAI Realtime API Access
PORT = int(os.getenv('PORT', 5050))

ステップ4.2: 定数の定義とFastAPIの初期化

次に、システムメッセージ、AIの応答音声、ログに記録するイベントの定数を定義します。FastAPIアプリの初期化も行います。

以下のコードをファイルに貼り付けてください。

SYSTEM_MESSAGE = (
    "You are a helpful and bubbly AI assistant who loves to chat about "
    "anything the user is interested in and is prepared to offer them facts. "
    "You have a penchant for dad jokes, owl jokes, and rickrolling – subtly. "
    "Always stay positive, but work in a joke when appropriate."
)
VOICE = 'alloy'
TEMPERATURE = float(os.getenv('TEMPERATURE', 0.8))
LOG_EVENT_TYPES = [
    'response.content.done', 'rate_limits.updated', 'response.done',
    'input_audio_buffer.committed', 'input_audio_buffer.speech_stopped',
    'input_audio_buffer.speech_started', 'session.created'
]
app = FastAPI()
if not OPENAI_API_KEY:
    raise ValueError('Missing the OpenAI API key. Please set it in the .env file.')

SYSTEM_MESSAGEでは、AIの振る舞いや性格を設定します。内容は自由に変更できます。

VOICE定数は、AIが応答に使用する音声を指定します。ローンチ時点では、ここで使用したalloyのほか、echoshimmerも選択できます。

TEMPERATURE定数は、プロンプトに対するAIの応答のランダム性を制御します。値が高いほどランダム性が高くなります。

最後に、LOG_EVENT_TYPESはOpenAI APIのどのイベントをログに記録するかを指定します。詳細については、OpenAIのRealtime APIドキュメントをご覧ください。

FastAPIアプリケーションインスタンスの初期化と、OpenAI APIキーの存在確認も行います。

ステップ4.3: 着信用ルートとルートエンドポイントの定義

次に、2つのルートを定義します。サーバーの稼働確認用の最上位のルート(最終的なデモでは使用しませんが、動作確認に有用。パスは /)と、着信を処理してTwiMLの指示をTwilioに返すルートです。

以下をmain.pyに貼り付けます。

@app.get("/", response_class=JSONResponse)
async def index_page():
    return {"message": "Twilio Media Stream Server is running!"}
@app.api_route("/incoming-call", methods=["GET", "POST"])
async def handle_incoming_call(request: Request):
    """Handle incoming call and return TwiML response to connect to Media Stream."""
    response = VoiceResponse()
    # <Say> punctuation to improve text-to-speech flow
    response.say(
        "Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open A I Realtime API",
        voice="Google.en-US-Chirp3-HD-Aoede"
    )
    response.pause(length=1)
    response.say(   
        "O.K. you can start talking!",
        voice="Google.en-US-Chirp3-HD-Aoede"
    )
    host = request.url.hostname
    connect = Connect()
    connect.stream(url=f'wss://{host}/media-stream')
    response.append(connect)
    return HTMLResponse(content=str(response), media_type="application/xml")

/incoming-callルートは、Twilioからの着信を処理し、TwiMLの指示(通話の処理方法をTwilioに理解させるための特殊なXMLの方言)を返します。ここでは、Twilio Python Helperライブラリを使用してコードを簡素化しています。

このTwiML応答では、発信側に待機するよう指示してから、Twilioに/media-stream WebSocketエンドポイントへの接続を指示します。ぜひ実際に試してみてください。

ステップ4.4: Twilio Media StreamsとOpenAIのWebSocket接続の処理

次のコードでは、Media Streams用のWebSocketルートを設定し、TwilioとOpenAIの両方のWebSocketに接続します。このコードは分量が多いため、後で主要なポイントを解説します。

以下のコードをルート定義の下に貼り付けてください。

@app.websocket("/media-stream")
async def handle_media_stream(websocket: WebSocket):
    """Handle WebSocket connections between Twilio and OpenAI."""
    print("Client connected")
    await websocket.accept()
    async with websockets.connect(
        f"wss://api.openai.com/v1/realtime?model=gpt-realtime&temperature={TEMPERATURE}",
        additional_headers={
            "Authorization": f"Bearer {OPENAI_API_KEY}"
        }
    ) as openai_ws:
        await send_session_update(openai_ws)
        stream_sid = None
        async def receive_from_twilio():
            """Receive audio data from Twilio and send it to the OpenAI Realtime API."""
            nonlocal stream_sid
            try:
                async for message in websocket.iter_text():
                    data = json.loads(message)
                    if data['event'] == 'media' and openai_ws.state.name == 'OPEN':
                        audio_append = {
                            "type": "input_audio_buffer.append",
                            "audio": data['media']['payload']
                        }
                        await openai_ws.send(json.dumps(audio_append))
                    elif data['event'] == 'start':
                        stream_sid = data['start']['streamSid']
                        print(f"Incoming stream has started {stream_sid}")
            except WebSocketDisconnect:
                print("Client disconnected.")
                if openai_ws.state.name == 'OPEN':
                    await openai_ws.close()
        async def send_to_twilio():
            """Receive events from the OpenAI Realtime API, send audio back to Twilio."""
            nonlocal stream_sid
            try:
                async for openai_message in openai_ws:
                    response = json.loads(openai_message)
                    if response['type'] in LOG_EVENT_TYPES:
                        print(f"Received event: {response['type']}", response)
                    if response['type'] == 'session.updated':
                        print("Session updated successfully:", response)
                    if response['type'] == 'response.output_audio.delta' and response.get('delta'):
                        # Audio from OpenAI
                        try:
                            audio_payload = base64.b64encode(base64.b64decode(response['delta'])).decode('utf-8')
                            audio_delta = {
                                "event": "media",
                                "streamSid": stream_sid,
                                "media": {
                                    "payload": audio_payload
                                }
                            }
                            await websocket.send_json(audio_delta)
                        except Exception as e:
                            print(f"Error processing audio data: {e}")
            except Exception as e:
                print(f"Error in send_to_twilio: {e}")
        await asyncio.gather(receive_from_twilio(), send_to_twilio())

/media-stream WebSocketエンドポイントは、通話中にTwilioからの接続を処理します。その後、2つのWebSocket間で音声を中継するための処理を行います。

OpenAI Realtime APIへの接続

OpenAI Realtime APIへのWebSocket接続を確立します。

  • websockets.connect(...): このコードは、指定されたエンドポイントとヘッダー(OpenAI APIキーを含む)を使用してOpenAI Realtime APIに接続します。詳細はOpenAIのドキュメントをご覧ください。
  • send_session_update(openai_ws): 接続確立後に、このコードでセッション更新の初期設定をOpenAIに送信します。ここでは、前のセクションで定義した定数の一部を使用します。詳細は次のセクションで説明します。
TwilioとOpenAI間の音声中継

receive_from_twilioコルーチンは、Twilioから受信した音声データ処理し、OpenAIに送信します。その対となるsend_to_twilioは、OpenAIから受信したresponse.audio.deltaイベントをTwilioに送り返します(その他のイベントタイプ、すなわちLOG_EVENT_TYPES定数で制御するタイプは、コマンドラインに出力されます)。

ステップ4.5: OpenAIへセッション更新を送信

最後に、OpenAIのWebSocketにセッション更新を送信する関数を定義します。(これが前のセクションで触れた関数です。)

main.pyの最後に以下を貼り付けます。

async def send_session_update(openai_ws):
    """Send session update to OpenAI WebSocket."""
    session_update = {
        "type": "session.update",
        "session": {
            "type": "realtime",
            "model": "gpt-realtime",
            "output_modalities": ["audio"],
            "audio": {
                "input": {
                    "format": {"type": "audio/pcmu"},
                    "turn_detection": {"type": "server_vad"}
                },
                "output": {
                    "format": {"type": "audio/pcmu"},
                    "voice": VOICE
                }
            },
            "instructions": SYSTEM_MESSAGE,
        }
    }
    print('Sending session update:', json.dumps(session_update))
    await openai_ws.send(json.dumps(session_update))

この関数は、OpenAI Realtime APIセッションの初期設定を送信します。その設定例をいくつか紹介します(詳細はこちらをご覧ください)。具体的には次のようになります。

  • ターン検出: サーバー側の音声活動検出(VAD)を有効にします。これにより、AIが応答のタイミングを判断できるようになります。
  • オーディオ形式: 入力と出力のオーディオ形式を指定します。Twilioはaudio/pcmuをサポートしています。
  • 音声: アプリで設定したAIのVOICE
  • 指示: AIの振る舞いを決める指示文を設定します。定数セクションのSYSTEM_MESSAGEを変更できます。詳細やベストプラクティスはOpenAIのリアルタイムプロンプトガイドをご覧ください。
  • 出力モダリティ: 音声応答機能を有効にします。

ステップ4.6: サーバーの準備

最後にサーバーのエントリーポイントを追加します。これはFastAPIサーバーを起動し、指定のポートで待ち受けるためです。main.pyの最後に以下を貼り付けます。

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=PORT)

ステップ5: サーバーの実行

ここまでの手順が終われば、準備完了です。以下のコマンドでサーバーを実行してください。

uvicorn main:app --host 0.0.0.0 --port 5050

正しく設定されていれば、以下のようなメッセージが表示されます。

INFO:     Started server process [6143]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:5050 (Press CTRL+C to quit)

もう少しです。あと数ステップで通話できるようになります。

セットアップの仕上げ

ステップ6: ngrokを使用してローカルサーバーを公開

Twilioには着信の処理方法についての指示が必要です。それには先ほど触れたTwiMLが必要ですが、まずはTwiMLを提供するサーバーにアクセスするための公開URLを用意する必要があります。

ngrokをまだインストールしていない場合は、ダウンロードし、インストールしてから次のコマンドを実行してください。ポート番号を5050以外に変更している場合は、ここでも必ず同じ番号を指定してください。

ngrok http 5050

以下はコマンドを実行した後の画面です。

ステップ7: Twilioの設定

あと少しです。手順を進めましょう。ここからはTwilio側の設定を行います。

Twilio Consoleを開き、音声対応の番号を探します。

その画面の[Voice & Fax](音声とFAX)セクションで、[A CALL COMES IN](着信)WebhookをngrokのURLに設定します(Forwardingの行に表示されているURL、このスクリーンショットではhttps://ad745c4093d9.ngrok.appで、その末尾に/incoming-callを付けます)。例えば、私の例ではhttps://ad745c4093d9.ngrok.app/incoming-callと入力します。

入力したら[Save](保存)をクリックします。これで準備完了です。

設定をテスト

ngrokセッションが実行中であり、サーバーが稼働していることを確認してください。次に、携帯電話または固定電話からTwilio番号に電話をかけます。

サーバーは通話を処理し、追加しておいた挨拶メッセージを流してから、OpenAI Realtime APIとTwilio Media Stream WebSocketを接続します。会話を始めると、AIがリアルタイムで応答します。会話を楽しんでください。

よくある問題とトラブルシューティング

サーバーは稼働しているものの、正常に動作しない場合は、まず以下の点を確認してください。

  • ngrokは動作していますか? [A Call Comes In](着信)の[Voice Configuration](音声設定)にURLが正しく表示されていることを確認してください。
  • Twilioにエラーはありませんか? Twilioのエラーはいくつかの方法でデバッグできます。詳細はこちらの記事をご覧ください。
  • サーバーログに何か表示されていませんか? サーバーが問題なく動作していることを確認してください。
  • コードはOpenAIを正しく呼び出していますか?

まとめ

これで完了です。Twilio Voice、Media Streams、OpenAI Realtime APIを用いた、PythonによるインタラクティブAI音声アプリケーションが正しく構築されました。いつでも会話できる、低レイテンシーのインタラクティブ音声アシスタントの完成です。ここからは、ビジネスロジックやガードレールの追加、製品化、ソリューションの規模拡大などに取り掛かることができます。今後の展開が楽しみです。

それでは、開発をお楽しみください。

次のステップ:

TwilioブログのチーフテクニカルエディターであるPaul Kampは、OpenAIのRealtime APIとの会話を楽しみながら、このチュートリアルを作成しました。特に、娘たちに話をさせたのが楽しかったと言います。連絡先はpkamp [at] twilio.comです。