1. はじめに
LLMの進化に伴い、テキストチャットだけでなく「キャラクターの声で喋り、3Dモデルが表情豊かに身振り手振りしながら家電や予定を操作してくれるアシスタント」を自宅に作りたいと考えました。
本記事では、外部の重厚なWebフレームワーク(FastAPIやLangChain等)を一切使わず、Python標準ライブラリ中心で構築したデスクトップ&ブラウザ対応のAI音声アシスタント**「雨衣(うい)」**の設計と実装について紹介します。
成果物のデモイメージ・できること
- 3Dアバター連動: MMDAgent-EXをTCPソケット制御し、発話中のリアルタイム口パク(リップシンク)や感情に応じた案内モーション、待機中の自然な呼吸仕草を再現。
- 高品質キャラクターボイス: VoiSona Talk(雨衣 / しぐれういボイス)のローカルREST APIを叩き、人間味のある自然な日本語音声を即時合成。
- 超低遅延な音声認識: ブラウザ標準の Web Speech API を活用した完全ハンズフリー対話。
- スマートホーム&スケジュール連携: Gemini の Function Calling で Google カレンダー、Google ToDo、SwitchBot(エアコン・照明・カーテン操作や温湿度取得)をシームレスに操作。
2. 全体アーキテクチャ
システム全体は、**「UI層」「対話・思考エンジン」「ツール連携」「出力層(音声+3D)」**の4層に分かれています。
3. 技術選定とこだわり
① 外部依存ゼロ(Python標準ライブラリのみ)
依存パッケージの肥大化やバージョン競合を防ぐため、通信には urllib.request や socket、Webサーバーには http.server を使用しました。これにより、Macの初期状態に近い環境でも python3 chat_web.py だけで軽量に起動します。
② Gemini 2.5 Flash と Function Calling の生実装
GeminiのREST APIを直接叩くことで、SDKのオーバーヘッドを削減しています。また、別の日付の予定と混同しないよう、各ターンのリクエスト時に現在日時(JST)と「今日・明日の日付」をシステムプロンプト末尾に動的注入しています。
# 現在日時情報を動的にシステムプロンプトへ追加
now_dt = datetime.now()
w_str = ["月", "火", "水", "木", "金", "土", "日"][now_dt.weekday()]
tomorrow_dt = now_dt + timedelta(days=1)
time_context = (
f"\n\n【現在日時情報】本日は {now_dt.year}年{now_dt.month}月{now_dt.day}日({w_str}) {now_dt.strftime('%H:%M')} (JST) です。\n"
f"・今日(today) = {now_dt.strftime('%Y-%m-%d')}\n"
f"・明日(tomorrow) = {tomorrow_dt.strftime('%Y-%m-%d')}\n"
"※注意:予定案内時は、今日と明日を絶対に混同しないでください。"
)
dynamic_system_prompt = self.system_prompt + time_context
③ VoiSona Talk のローカルREST API連携 & 読み仮名補正
キャラクター名の「雨衣」は、そのまま音声合成エンジンに渡すと「あまぎぬ」と読まれてしまいます。
そのため、APIリクエスト送信直前に発話テキストを解析し、「雨衣」の表記を自動的に「うい」に差し替える補正パイプラインを挟んでいます。
def sanitize_text_for_speech(text: str) -> str:
# 固有名詞の読み仮名自動補正
text = text.replace("雨衣", "うい")
# 読み上げに不要なMarkdown装飾や記号の除去
text = re.sub(r"[\*\_#`]", "", text)
return text
④ MMDAgent-EX を TCP/IP ソケットで自在に操る
オープンソースの3Dエージェント実装基盤「MMDAgent-EX」は、外部プロセスとTCPソケット(デフォルトポート: 50001)で双方向通信が可能です。
-
思考中:
MOTION_ADD|0|thinking|motions/action/kashige.vmd|PART|LOOP|ON|OFFを送信して首をかしげる。 - 発話時: Geminiが選んだツールや発話感情(天気、ニュース、喜び、会釈)に応じてVMDモーションを選定。
-
リップシンク: 音声の長さ(ミリ秒)から母音シーケンス(a, i, u, e, o)を動的生成し、
LIPSYNC_START|0|...で口を開閉。 - 待機中: 12〜20秒おきに深呼吸や微細な姿勢変化モーションを送信し、「生きてその場にいる感」を演出。
⑤ Web Speech API による完全ハンズフリー自動音声認識
当初はローカルのマイク録音ツール(SoX)で音量エネルギー判定を行っていましたが、マイク感度や生活環境ノイズのチューニングに課題がありました。
そこで、WebチャットUI側でブラウザ標準の Web Speech API (webkitSpeechRecognition) を導入。
- 日本語のリアルタイム文字起こしがブラウザネイティブで動作
- 発話が終わると自動的にメッセージが送信され、雨衣が応答
- 雨衣の発話終了後に再びマイク認識が自動再開し、完全なハンズフリー対話を実現
4. 開発時のハマりどころとTips
1. MMDAgent-EX のソケットバッファ詰まり問題
MMDAgent-EX は内部ログやイベント(KEY|..., MOTION_EVENT_STOP|...)をソケットに流し続けます。Python側で「送信だけ」を行い受信処理を放置していると、OSのソケットバッファが溢れて送信がブロックされる現象に遭遇しました。
解決策: バックグラウンドスレッドで sock.recv(4096) を回し続け、受信バッファを常にクリアする設計にしました。
2. 音声合成向けプロンプトエンジニアリング
通常のチャットAIと異なり、音声アシスタントは「耳で聞いて心地よい文章」である必要があります。
プロンプトに以下の制約を設けることで、自然な聞き取りやすさを大幅に向上させました:
- マークダウン装飾・絵文字・URLの禁止(読み上げが破綻するため)
- 1〜3文以内の簡潔な口語表現
- 一人称の徹底指定(「私」を使い、「僕」やモデルの別名を使わせない)
5. まとめ
LLM(Gemini)の知能、VoiSona Talk の声、MMDAgent-EX の3D表現をシンプルにつなぎ合わせることで、驚くほど身近で愛着の湧くデスクトップAIアシスタントを構築することができました。
フレームワークを使わずに標準機能でシンプルに組んだことで、レイテンシも極めて小さく、トラブルシューティングも容易です。自分だけの3DアバターAIを作ってみたい方の参考になれば幸いです!
6. 謝辞
素敵な合成音声を提供してくださったしぐれうい先生・VoisonaTalkの皆さん