1. LiveAvatar 対応とは
本デモでは、アバターの表現層に HeyGen LiveAvatar LITE モード を導入しました。
従来は 3D モデル(VRM)をブラウザ上で描画し、音声解析による口パクや瞬きを JavaScript で制御していました。LiveAvatar では、この映像生成を HeyGen のサーバーサイド で行い、WebRTC(LiveKit)経由でブラウザに配信します。
| VRM 方式(従来) | LiveAvatar 方式(今回追加) |
| 描画 |
ブラウザ内で Three.js + VRM を描画 |
HeyGen サーバーで映像生成 → WebRTC で配信 |
| リップシンク |
AudioContext で周波数解析 → 母音5種の口形 |
HeyGen がサーバー側で自動制御(高精度) |
| うなずき・瞬き |
JS で自前実装(タイミング・確率制御) |
HeyGen がサーバー側で自動生成 |
| 音声再生 |
ローカル AudioContext で MP3 再生 |
PCM 音声を WebSocket で送信 → LiveKit Audio Track で受信 |
| 見た目 |
3D キャラクター風 |
実写風のリアルなアバター映像 |
| クライアント負荷 |
GPU 描画あり(Three.js) |
動画再生のみ(軽量) |
| 外部サービス依存 |
なし(完全自前) |
HeyGen API(従量課金) |
💡 ポイント: 「物量アバター方式」の根幹 — 回答プール・QAエンジン・相槌ロジック — はそのまま維持しています。変わったのはアバターの表現層だけです。
🔄 今後の方針: VRM 方式は廃止ではなく、今後は VRM と LiveAvatar を切り替え可能にする 予定です。コスト不要で手軽に動かしたい場合は VRM、よりリアルな表現が必要な場面では LiveAvatar — シーンに応じて最適な方式を選択できる設計を目指します。
2. 変わったもの・変わらないもの
✅ 変わらないもの
- 回答プール — 234件のQ&Aペア(
answer_variants.jsonl)と事前生成した音声ファイル(468件)はそのまま使用
- QAエンジン — Gemini Embedding によるベクトル検索 + Flash-Lite による LLM 指揮者の2段階選択ロジック
- FastAPI バックエンド — Python / FastAPI の既存サーバー構成
- WebSpeech STT — マイク入力から音声認識テキストへの変換
- チャット UI — 右パネルの会話ログ・モード切替・メタ情報表示
- 相槌ロジック — TRP 検出・タイミング制御・パラメータ調整パネル
🔄 変わったもの
- アバター描画 — Three.js + VRM (canvas) → HeyGen LiveAvatar (video 要素, LiveKit WebRTC)
- リップシンク — クライアント側 AudioContext 解析 → HeyGen サーバー側自動制御
- うなずき・瞬き — JS 自前実装 → HeyGen サーバー側自動生成
- 音声再生経路 — ローカル AudioContext で MP3 直接再生 → PCM 24kHz に変換し WebSocket 経由で HeyGen に送信、LiveKit Audio Track で受信再生
- セッション管理 — ステートレス → HeyGen セッション(接続 / Keep-Alive / 切断)を明示的に管理
➕ 追加されたもの
| 追加要素 | 概要 |
| HeyGen セッション API | サーバー側で HeyGen の token 取得 → セッション開始を仲介 |
| PCM 音声変換 + キャッシュ | 既存 MP3 を PCM 24kHz 16bit mono に変換。起動時に全468件を一括キャッシュ |
| LiveKit Client SDK | WebRTC による映像・音声の受信(バンドル済み JS を静的配置) |
| 接続 / 切断ボタン | HeyGen クレジット保護のため、手動で接続を制御 |
| セッションタイマー | 接続中の経過時間を表示。課金状況の把握に使用 |
| 安全自動切断 | 290秒(約5分)で自動切断し、クレジットの無駄遣いを防止 |
3. アーキテクチャ比較
VRM 方式(従来)
ブラウザ FastAPI
┌─────────────────────┐ ┌────────────────────┐
│ WebSpeech STT │───POST───▶ │ /api/ask │
│ Chat UI │◀──JSON───── │ Embed + Selector │
│ Three.js + VRM │ │ │
│ AudioContext (再生) │◀─GET────── │ /audio/*.mp3 │
│ AnalyserNode(リップ)│ │ /model/shingo.vrm │
└─────────────────────┘ └────────────────────┘
ブラウザが全てを担う:
音声ファイルをダウンロード → AudioContext で再生
→ AnalyserNode で周波数解析 → VRM の口を動かす
LiveAvatar 方式(今回追加)
ブラウザ FastAPI
┌──────────────────────┐ ┌──────────────────────────┐
│ WebSpeech STT │───POST──▶ │ /api/ask │
│ Chat UI │◀──JSON──── │ Embed + Selector │
│ │ │ │
│ WebSocket │◀═══WSS═══▶ │ /api/heygen/session │
│ agent.speak (PCM) │ │ → HeyGen token + start │
│ agent.speak_end │ │ │
│ agent.interrupt │ │ /api/heygen/audio │
│ session.keep_alive │ │ → PCMキャッシュから返却 │
│ │ └──────────────────────────┘
│ LiveKit (WebRTC) │◀═══════════ HeyGen サーバー
│ video → <video> │ ↑
│ audio → <audio> │ LiveAvatar LITE
└──────────────────────┘ (映像+音声生成+リップシンク)
役割分担:
FastAPI → 接続情報の仲介 + PCM 音声の提供
HeyGen → 映像生成・リップシンク・音声再生
ブラウザ → 操作 UI + WebRTC 映像受信のみ(描画負荷なし)
技術スタック(追加分)
| レイヤー | 技術 | 補足 |
| 映像配信 | HeyGen LiveAvatar LITE | サーバーサイドレンダリング + WebRTC |
| WebRTC | LiveKit Client SDK v1.8.0 | UMD ビルドを静的配置 |
| 音声変換 | pydub + ffmpeg | MP3 → PCM 24kHz 16bit mono |
| HeyGen API 呼び出し | httpx(非同期) | token + start の2段階 |
既存の技術スタック(WebSpeech API, Gemini Embedding, Gemini Flash-Lite, FastAPI, nginx + Let's Encrypt, AWS EC2)はアーキテクチャページを参照してください。
4. 音声ストリーミングの仕組み
従来方式ではブラウザが MP3 ファイルをダウンロードして AudioContext で直接再生していましたが、LiveAvatar では HeyGen に音声データを送り込み、HeyGen 側で映像と同期させて配信します。
処理フロー
1回答選択
/api/ask で質問に最適な回答 ID + バリアント(20s/40s)を決定。ここまでは従来と同じ。
2PCM 取得
/api/heygen/audio に回答 ID を送り、サーバーのメモリキャッシュから PCM チャンク(Base64)を即座に取得。
3チャンク送信
WebSocket で agent.speak を 100ms 間隔で逐次送信。全チャンク送信後に agent.speak_end を送信。
4HeyGen 処理
HeyGen サーバーが PCM 音声を受け取り、リップシンク付きの映像をリアルタイム生成。
5WebRTC 配信
LiveKit 経由で映像(Video Track)と音声(Audio Track)がブラウザに配信され、<video> 要素で再生。
PCM 事前キャッシュ
全468件(234件 × 20s/40s の2バリアント)の MP3 ファイルを、サーバー起動時に PCM 24kHz 16bit mono に一括変換してメモリに保持しています。
- 変換方式: pydub (ffmpeg) による一括変換
- キャッシュ形式: 1秒ごと(48,000 bytes)の Base64 チャンク配列
- メモリ使用量: 約 23MB(468ファイル × 約50KB)
- 応答速度: キャッシュからの返却は 約15ms(変換レイテンシ 0ms)
割り込み(interrupt)
アバターが発話中に新しい質問が入った場合、agent.interrupt を即座に送信して前の発話を中断し、新しい回答の再生に切り替えます。チャンク送信ループも内部フラグで即座に停止します。
5. セッション管理とクレジット保護
HeyGen LiveAvatar は従量課金(1分単位・端数切り上げ)のため、セッション管理にはクレジットの無駄遣いを防ぐ仕組みを組み込んでいます。
セッションのライフサイクル
ユーザーが「接続」ボタンを押す
│
▼
┌────────────────────────────────────────┐
│ 1. Token 取得 │
│ FastAPI → HeyGen API (POST) │
│ → access_token を取得 │
├────────────────────────────────────────┤
│ 2. Session Start │
│ FastAPI → HeyGen API (POST) │
│ → sessionId, wsUrl, livekitUrl, │
│ livekitClientToken を取得 │
├────────────────────────────────────────┤
│ 3. WebSocket 接続 │
│ ブラウザ → HeyGen wsUrl に直接接続 │
├────────────────────────────────────────┤
│ 4. LiveKit Room 接続 │
│ ブラウザ → LiveKit 経由で映像受信 │
│ Video Track → <video> 要素 │
│ Audio Track → <audio> 要素 │
├────────────────────────────────────────┤
│ 5. Keep-Alive (30秒間隔) │
│ WebSocket で session.keep_alive │
│ → セッション維持 │
├────────────────────────────────────────┤
│ 6. 切断 │
│ 手動ボタン or 安全自動切断(290秒) │
│ → WebSocket close → LiveKit close │
└────────────────────────────────────────┘
クレジット保護の仕組み
| 機能 | 内容 |
| 手動接続 | ページを開いただけでは接続しない。ユーザーが「接続」ボタンを明示的に押す必要がある |
| セッションタイマー | 接続中の経過時間をリアルタイム表示(m:ss 形式)。切断後も最終値を保持 |
| 安全自動切断 | デフォルト ON。290秒(約5分)経過で自動切断。トグルスイッチで ON/OFF 切替可能 |
| 手動切断 | 接続中はいつでも「切断」ボタンで即座にセッションを終了可能 |
💡 なぜ 290秒? HeyGen の課金は1分単位・端数切り上げのため、5分ちょうどで切ると6分扱いになるリスクがあります。4分50秒(290秒)で切断することで、確実に5分以内に収めます。
6. 応答速度の内訳
質問してから回答が返るまでのレイテンシを分解すると、以下のようになります。
接続レイテンシ(初回のみ)
| 処理 | 所要時間 | 補足 |
| Token 取得 + Session Start | 数秒 | HeyGen API の外部処理 |
| WebSocket + LiveKit 接続 | 数秒 | WebRTC ハンドシェイク |
| 合計 | 約 12秒 | HeyGen 側の外部要因が支配的 |
応答レイテンシ(質問ごと)
| 処理 | 所要時間 | 補足 |
| Embedding 変換 | ~400ms | Gemini Embedding API(従来と同じ) |
| コサイン類似度検索 | ~1ms | 234件の全件探索(従来と同じ) |
| LLM 指揮者による選択 | ~1,300ms | Gemini Flash-Lite(従来と同じ) |
| PCM チャンク取得 | ~15ms | メモリキャッシュから即座に返却 |
| WebSocket チャンク送信開始 | ~100ms | 最初のチャンク送信まで |
| 合計(発話開始まで) | 約 2〜3秒 | QAエンジン処理が支配的(LiveAvatar による追加は微小) |
💡 ポイント: LiveAvatar 対応による追加レイテンシは PCM キャッシュ取得(~15ms)+ WebSocket 送信開始(~100ms)の合計 約 0.1秒 程度です。応答速度のボトルネックは従来と同じく QA エンジン(Embedding + LLM 指揮者)にあり、アバター方式の変更は速度にほぼ影響しません。
7. 相槌の扱い
本デモの特徴である「自然な相槌」は、現時点ではローカル再生方式を維持しています。
現在の方式(ローカル再生)
- 相槌音声(「はい」「ええ」等)はブラウザの AudioContext でローカル再生
- 相槌のタイミング制御(TRP 検出)やパラメータ調整パネルは従来通り動作
- HeyGen の発話状態(speaking)には影響を与えない
現時点での制約
相槌がローカル再生のため、相槌が鳴っても HeyGen 映像のアバターは口を動かしません。音声は聞こえるが映像には反映されない という不一致が生じます。デモとしてはこの状態で十分機能していますが、より自然な体験を目指す余地があります。
🔄 今後の取り組み: 相槌音声を HeyGen の agent.speak 経由で送信し、映像のリップシンクやうなずきと同期させる方式への移行を予定しています。これにより、相槌を打つ際のアバター映像もより自然になります。
8. 今後の展望
LiveAvatar 対応は完了しましたが、さらなる体験向上のための取り組みを計画しています。
| 項目 | 内容 | 効果 |
| VRM / LiveAvatar 切替 |
UI 上でアバター方式を選択可能にする。VRM(コスト不要・3D表現)と LiveAvatar(高品質・実写風)を場面に応じて使い分け |
コストを抑えたい場面ではVRM、リアルさが求められる場面ではLiveAvatar、という柔軟な運用 |
| 相槌のアバター同期 |
相槌音声を HeyGen 経由で送信し、リップシンク・うなずきと同期。現在のローカル再生から移行 |
相槌時にもアバターが口を動かす、より自然な会話体験 |
| FALLBACK 動的 TTS |
回答プールに該当がない場合、Gemini TTS でリアルタイム音声生成 → HeyGen に送信 |
回答プール外の質問にも音声付きで対応可能に |
| 各種デバイス対応 |
PROTOデバイス / iOS Safari などでのレイアウト最適化 |
PROTOデバイスやスマホでのデモ実施 |