はじめに
Twilioの新しいAPI「ConversationRelay」は、WebSocketを使ってAIボットを簡単に構築できる素晴らしい機能です。しかし、2025年現在、Conversation Relayが公式にサポートしている日本語の音声認識(STT)プロバイダーはGoogleとDeepgramのみです。
「日本語の認識精度にもっとこだわりたい!」
「AmiVoiceを使いたい!」
そんな要望を叶えるために、Twilio Media Streamで音声をAmiVoiceに送りつつ、発話(TTS)はConversationRelayに任せる という「ハイブリッド構成(標準STT迂回)」を実装してみました。
実装内容
今回ご紹介するサンプルコードです。
-
入力 (STT): Twilio
<Start><Stream>を使って音声をWebSocketでサーバーに送信し、そこから AmiVoice API に転送して文字起こし -
LLM: Google Gemini 2.5 Flash クライアントで文脈を保持した対話を生成
-
出力 (TTS): Twilio
<Connect><ConversationRelay>を使い、生成されたテキストをTwilioに送り返して音声合成(Google Neural2/Chirp)で再生
この構成により、「AmiVoiceの高精度な日本語認識」と「ConversationRelayの低遅延な音声合成・割り込み制御」 のいいとこ取りができます。
アーキテクチャ
FastAPIサーバーがハブとなり、3つのWebSocket接続を管理します。
- Twilio Media Stream (Input): ユーザーの音声をリアルタイムでAmiVoiceへ中継
- AmiVoice WebSocket API: 音声を受け取り、認識結果(テキスト)をサーバーに返す
- Twilio ConversationRelay (Output): LLMが生成したテキストを受け取り、TTSで再生する
実装のポイント
1. TwiMLでの「二刀流」接続
main.py の /voice エンドポイントで、Media StreamとConversationRelayの両方を起動するTwiMLを返します。
# main.py (抜粋)
response = VoiceResponse()
# 1. Media Streamを開始 (AmiVoiceへ音声を送るため)
start = Start()
stream = start.stream(url=f"wss://{host}/stream")
stream.parameter(name="session_id", value=session_id)
response.append(start)
# 2. ConversationRelayに接続 (TTSとして利用)
connect = Connect()
cr = connect.conversation_relay(
url=f"wss://{host}/relay?session_id={session_id}",
language="ja-JP",
tts_provider="google",
voice="ja-JP-Chirp3-HD-Aoede"
)
response.append(connect)
<Start><Stream> は非同期でバックグラウンド実行されるため、通話自体は <Connect> でConversationRelayに接続された状態になります。これで「音声は抜き取れるが、通話の制御権はRelayにある」状態が作れます。
2. AmiVoiceへの音声転送
Media Streamから届く音声(MULAW 8kHz形式)は、トランスコード不要でそのままAmiVoiceに送れます。これが地味に嬉しいポイントです。
# amivoice_client.py (抜粋)
# スタートコマンド (MULAWを指定)
command = f"s MULAW -a-general authorization={app_key} ..."
await ws.send(command)
# 音声データ送信 ('p'コマンド + バイナリ)
await ws.send(b'p' + mulaw_chunk)
3. セッション管理
2つのWebSocket(Stream用とRelay用)は別々の接続としてサーバーに来るため、session_id を使ってこれらを1つの「通話セッション」として紐付けます。
-
Stream側:
<Stream>のcustomParametersにsession_idを埋め込んで送信 -
Relay側: URLパラメータ
?session_id=...で送信
サーバー側ではこのIDをキーにして、同じ Session オブジェクトを参照させます。
4. LLMへの役割注入
Gemini 2.5 Flash Liteを使用し、chats.create でセッションを開始することで、会話の文脈(コンテキスト)を維持します。
# llm_client.py
self.chat = self.client.aio.chats.create(
model=self.model,
config=types.GenerateContentConfig(
system_instruction="あなたはプロフェッショナルな電話応対AIです..."
)
)
感想
ConversationRelayは「話す(TTS)」機能だけでも非常に優秀です。特に interruptible(割り込み)の設定などをTwilio側がよしなにやってくれるため、自前でバリバリ実装するよりもスムーズな対話感が得られます。
今回はSTT部分をあえてMedia Streamに切り出すことで、AmiVoiceの最強日本語認識を組み込むことができました。これが現状、日本語で電話系AIを作る際の「解」のひとつかもしれません。
ソースコード
完全なソースコードはこちらのリポジトリで公開しています。
[GitHubリポジトリURL]
参考
ConversationRelayについてもう少し理解したい方はこちらを参考にしてください
AIボットと電話を繋ぐ - Twilio Voice - ConversationRelay