この記事は筆者オンリーのAdvent Calendar 202514日目の記事です。
Rustで、SIPのUASとして通話を受けて、相手の音声をASR→LLM→TTS(VOICEVOX)で返す「ずんだもん音声ボット」を作っています。
この記事は「このプロジェクト、どういう方向性でRustコードを書いていくか?」という 方針(ガイド) を、ざっくりまとめたメモです。
(SIPの細かいRFCや、Zoiperの設定、RTPの地獄などは別記事に回します)
目標:いったん “通話が成立して会話できる” を最短で作る
最初にやりたいことはこれだけです。
- SIP UASとしてINVITEを受ける
- RTPで相手の音声を受ける
- ASRでテキスト化する
- LLMで返答文を作る
- VOICEVOXで音声を作る
- RTPで相手に返す
「全部完璧」より、まず 通話ができて会話が回る ことを優先します。
大方針:イベント駆動 + レイヤ分離で“迷子”を防ぐ
SIP/RTP(プロトコル)と、会話ロジックと、AI連携が混ざると、一気に破綻します。
なので最初から、役割を分けて“混ぜない”ことを徹底します。
ざっくり分けるとこう:
- transport:UDP/TCPの生パケットの送受信だけ
- sip:SIPメッセージ/トランザクション/タイマ(プロトコル)
- rtp:RTPのパケット処理・ジッタ吸収・PCM化(プロトコル)
- session:1通話のライフサイクル管理(SIP+RTPをまとめる)
- app:会話の状態機械(ASR→LLM→TTSのオーケストレーション)
- ai:ASR/LLM/TTSのクライアント(差し替え前提)
- http/media:通話ログや録音の参照・配信(プロトコルと混ぜない)
重要なのは「依存方向」です。
- app → session → sip/rtp → transport
- app → ai
逆向きに呼び出さない。逆向きはイベントで通知する、というルールにします。
“何かしたくなったらイベント” に寄せる
モジュール間のやり取りは、基本 enumイベント + 非同期チャネルでつなぐ想定です。
例(イメージ):
- transport → sip : RawPacket(bytes + addr)
- sip → session : IncomingInvite / IncomingBye
- rtp → app : PcmInputChunk
- app → ai::llm : LlmRequest
- ai::tts → rtp : PcmOutputChunk
- app → session : 「200 OK返して」「BYE送って」みたいな高レベル指示
こうすると「どこで判断してるか」が分かりやすくなって、後から改修しやすいです。
並行処理の方針:1通話 = 1タスク(Actorっぽく)
Tokioで実装するときの基本方針は:
- transportは受信ループ(SIP用/RTP用)
- sipはプロトコル処理タスク
- rtpは受信/送信処理タスク(必要ならキュー)
- sessionは 1通話ごとに1タスク
- app(会話)は session からイベントを受けて進める
共有Mutexで全体を守るのではなく、セッションタスク内に状態を閉じ込める方向で考えます。
AI部分の方針:差し替え前提で “Port/Adapter” にする
ASR/LLM/TTSは、将来的にローカル/クラウドを切り替える可能性が高いので、最初から差し替え前提にします。
- appが見るのは「trait(Port)」だけ
- 実装は ai モジュールの「Adapter」に閉じる
この方針にすると、例えば
- ASR:ローカル→クラウド
- LLM:クラウド→ローカル
- TTS:VOICEVOX前提(ずんだもん使うのでここは固定寄り)
みたいな変更がしやすくなります。
音声ボットなので“タイムアウトと失敗”を前提にする
音声ボットは「無言」が一番つらいです。
- ASRが詰まる
- LLMが遅い
- TTSが落ちる
- RTPが途切れる
こういうのは普通に起こるので、最初から
- 各処理にタイムアウトを置く
- 失敗したら定型文(「ごめんなさい、もう一度お願いします」)を返して継続
- 連続失敗なら切断(BYE)
みたいな雑なポリシーを app 側に持たせる想定です。
ざっくり実装の進め方(おすすめ順)
自分はこの順で作ると迷子になりにくいと思っています。
- SIPで着信できる(INVITE→200→ACK、BYEで切れる)
- RTPで音声の受信ができる(まずは録音して確認でもOK)
- RTP→PCM化して、appまでイベントとして届く
- ASRをつなぐ(ローカル/クラウドどちらでも)
- LLMをつなぐ(まずは固定返答でもOK)
- VOICEVOXで音声を作る(ずんだもん)
- PCM→RTPで相手に返して“喋る”
- 最後に録音・HTTP参照・可視化を足す
おわりに
この記事は「SIP音声ボットをRustで作る時に、どういう方向でコードを書くか」というメモでした。
次は、どれを書くかで悩んでいます:
- Zoiper/SIP側(UASとして最低限どこまでやるか)
- RTP周り(8kHz/G.711/ジッタ吸収)
- VOICEVOXの音声を“通話に返す”ところ(リアルタイム性)
自分の理解が進んだら、順番に記事にしていきます。