2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Twilio ConversationRelay × AmiVoice で実現する「最高の日本語」音声AIボット

Last updated at Posted at 2025-12-15

はじめに

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接続を管理します。

  1. Twilio Media Stream (Input): ユーザーの音声をリアルタイムでAmiVoiceへ中継
  2. AmiVoice WebSocket API: 音声を受け取り、認識結果(テキスト)をサーバーに返す
  3. 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>customParameterssession_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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?