1. 全体アーキテクチャ
本デモは「物量アバター」方式を採用しています。事前に大量の回答音声(234件×2バリアント=468ファイル)を生成しておき、ユーザーの発話に最も適した回答を高速に引き当てて再生する仕組みです。
┌──────────────────────────────────────────────────────────┐
│ ブラウザ (HTTPS) │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ WebSpeech │→│ Chat UI │→│ VRM アバター │ │
│ │ API (STT) │ │ (質問/回答) │ │ Three.js + VRM │ │
│ │ ja-JP │ │ │ │ リップシンク │ │
│ └─────┬─────┘ └──────┬───────┘ │ 瞬き / うなずき │ │
│ │ │ │ 相槌音声 │ │
│ │ isFinal確定 │ └──────────────────┘ │
│ └───────────────→│ │
│ fetch POST /api/ask │
└─────────────────────────┬────────────────────────────────┘
│ HTTPS (nginx → uvicorn:8001)
▼
┌──────────────────────────────────────────────────────────┐
│ FastAPI サーバー (Python) │
│ │
│ Step 1: Embedding API │
│ 質問テキスト → gemini-embedding-001 → 3072次元ベクトル │
│ (~400ms) │
│ │
│ Step 2: コサイン類似度検索 │
│ 質問ベクトル × 234件の事前embeddingを全件探索 │
│ → Top 5 候補を抽出 (~1ms) │
│ │
│ Step 3: LLM 指揮者 (Selector) │
│ Top 5 候補 + 質問文 → gemini-3.1-flash-lite-preview │
│ → 最適な回答IDとバリアント(20s/40s)を選択 │
│ (~1,300ms) │
│ │
│ Step 4: 回答解決 │
│ 選択ID → answer_variants.jsonl → 回答テキスト │
│ 選択ID → /audio/{ID}_{variant}.mp3 → 音声URL │
│ │
│ 合計レイテンシ: ~1,700ms(目標 2秒以内 → 達成) │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ 事前生成データ (静的) │
│ │
│ 📄 answer_variants.jsonl 234件の質問・回答ペア │
│ 📄 answer_embeddings.jsonl 234件の3072次元ベクトル │
│ 🔊 audio/*.mp3 468ファイル (234×2バリアント) │
│ 🧍 avatar/shingo.vrm VRMアバターモデル │
│ 🔊 audio/aizuchi_*.mp3 相槌音声 4ファイル │
└──────────────────────────────────────────────────────────┘
技術スタック
| レイヤー | 技術 | 補足 |
| 音声認識 (STT) | WebSpeech API | ブラウザ標準、ja-JP、HTTPS必須 |
| Embedding | gemini-embedding-001 | 3072次元、Google Generative AI |
| LLM指揮者 | gemini-3.1-flash-lite-preview | 軽量LLM、temperature=0 |
| TTS (事前生成) | Gemini 2.5 Flash TTS Preview | Orus音声、MP3 128kbps |
| アバター | Three.js 0.164.1 + @pixiv/three-vrm v3 | VRM形式、CDN配信 |
| サーバー | FastAPI + uvicorn | Python、ポート8001 |
| リバースプロキシ | nginx + Let's Encrypt | HTTPS終端、HTTP→HTTPS リダイレクト |
| インフラ | AWS EC2 (ap-northeast-1) | Ubuntu 24.04、Elastic IP |
2. 回答プールの作成方法
回答プールは以下の3段階で育成しました。最終的に 234件 の質問・回答ペア(各20秒版と40秒版の2バリアント)を持ちます。
Phase 1: 事実ベースの資料収集
まず、社長が回答する際の事実的根拠となる資料を収集します。
- 会社概要、沿革、事業内容、組織構成
- 研究開発テーマ一覧、技術領域
- 品質方針、セキュリティポリシー
- グループビジョン、中期経営計画
- 採用情報、問い合わせ先
これらの「事実」がプールの上限を定めます。資料に含まれない情報は回答できないため、事実の網羅性がカバレッジの上限になります。
Phase 2: 思考モードLLMによるカバレッジ育成
収集した事実資料を基に、思考モード(reasoning)のLLMを使って質問と回答のペアを自動生成します。
1初期生成
事実資料を入力し、想定される質問と、社長としての回答(20秒版・40秒版)をLLMに生成させる。初期プール: 約99件。
2カバレッジ測定
知識カバレッジテスト(knowledge_coverage.py)で、事実資料のどの部分がプール内の回答でカバーされているか自動測定。
3不足補完
カバーされていない事実について追加のQ&Aを生成。31% → 46% → 49% → … と繰り返し、事実レベルカバレッジ 94.3% まで到達。
Phase 3: ペルソナ別ロールプレイ育成
カバレッジが一定水準に達したら、複数のペルソナ(想定来訪者)を設定し、自然な会話形式のロールプレイで回答プールをさらに育てます。
- 7種類のペルソナを用意(就活生、営業先のお客様、IT記者、新入社員、提携先幹部 など)
- 各ペルソナになりきったLLMが社長アバターに質問し、社長として答えるシナリオを11ラウンド実施
- カバレッジ測定では出てこなかった「口語的な聞き方」「文脈に依存する質問」が補完される
- 回答プール: 99件 → 234件 に拡大
Phase 4: Embeddingデータ構築 + TTS音声生成
育成が完了した回答プールに対して、以下のデータを事前生成します。
- Embedding化: 各質問文を
gemini-embedding-001 で3072次元のベクトルに変換し、answer_embeddings.jsonl に格納。これが検索用インデックスとなる
- TTS音声生成: 各回答テキスト(234件×20s/40sの2バリアント=468件)を
Gemini 2.5 Flash TTS Preview(Orus音声)で読み上げ、MP3ファイルとして事前生成。合計約137MB
3. 回答引き当てロジック
ユーザーの発話から回答を選択するまでの処理フローです。
ユーザー発話
│
▼
┌────────────────────────────────────────┐
│ Step 1: 音声認識 (ブラウザ側) │
│ │
│ WebSpeech API (ja-JP) │
│ ・continuous=false, interimResults=true │
│ ・無音検出で自動確定 (~1.0-1.5秒) │
│ ・isFinal=true で文字列確定 │
│ │
│ ※ JavaScript標準API、追加コスト不要 │
└──────────┬─────────────────────────────┘
│ 確定テキスト
▼
┌────────────────────────────────────────┐
│ Step 2: Embedding変換 (~400ms) │
│ │
│ POST gemini-embedding-001 │
│ 入力: 質問テキスト │
│ 出力: 3072次元の数値ベクトル │
│ │
│ 「意味」を数値空間に変換することで、 │
│ 言い回しが異なっても意味が近い文を │
│ 数学的に比較可能にする │
└──────────┬─────────────────────────────┘
│ 質問ベクトル
▼
┌────────────────────────────────────────┐
│ Step 3: コサイン類似度検索 (~1ms) │
│ │
│ 質問ベクトルと234件の事前embedding を │
│ 全件比較(コサイン類似度) │
│ │
│ → 類似度の高い上位5件(Top-5)を抽出 │
│ │
│ 例: 「強みは何?」 │
│ 1位: M026 (sim=0.89) 「強みは?」 │
│ 2位: M027 (sim=0.83) 「他社との違い」│
│ 3位: M030 (sim=0.78) 「技術立社とは」│
│ 4位: M136 (sim=0.75) 「独自性は?」 │
│ 5位: M084 (sim=0.72) 「社名の由来」 │
└──────────┬─────────────────────────────┘
│ Top-5 候補
▼
┌────────────────────────────────────────┐
│ Step 4: LLM指揮者による選択 (~1,300ms)│
│ │
│ 軽量LLM: gemini-3.1-flash-lite-preview│
│ │
│ 入力: │
│ ・ユーザーの質問テキスト │
│ ・Top-5候補(ID, 質問文, 類似度) │
│ │
│ 出力: │
│ ・最適な回答ID │
│ ・バリアント選択 (20s or 40s) │
│ ・確信度 (high/low) │
│ │
│ embeddingだけでは「意味」の微妙な違い │
│ を取りこぼすケースをLLMが補完する │
│ 例: 「理由は?」→ 単純類似度ではなく │
│ 意図を読み取って適切な候補を選択 │
│ │
│ ※ 実測で33%のケースでTop1と異なる │
│ 回答をLLMが選択(精度向上に寄与) │
└──────────┬─────────────────────────────┘
│ 選択された回答ID + バリアント
▼
┌────────────────────────────────────────┐
│ Step 5: 回答テキスト + 音声URL │
│ │
│ answer_variants.jsonl から回答文を取得 │
│ /audio/{ID}_{variant}.mp3 のURLを返却 │
│ │
│ → ブラウザで音声再生 + リップシンク │
└────────────────────────────────────────┘
LLM指揮者の役割
Embeddingによる類似度検索は高速ですが、言い回しの表面的な類似に引きずられることがあります。LLM指揮者は、候補の中からユーザーの意図を最も的確に汲む回答を選択します。
| 質問 | Embedding Top1 | LLM 選択 | 補足 |
| 「本社が東京にある理由は?」 |
M004 (sim=0.85) 東京拠点の住所説明 |
M112 富山本社の由来・歴史 |
LLMは「理由」という意図を読み取り、適切な回答を選択 |
| 「TISIになることで何が変わる?」 |
M111 (sim=0.74) |
M070 体制変更の具体的影響 |
「変化」の意図に合う回答をLLMが判断 |
実測49件のログで、LLM選択とTop1が一致するのは 67.3%。残り33%で LLM がより適切な回答を選んでおり、精度向上に明確に貢献しています。
4. アバター演出
うなずき(ヘッドノッド)
ユーザーが話している間(マイク録音中)、アバターが一定間隔で頷きます。
- トリガー: WebSpeech API 録音中(
avatar:listening-start イベント)
- 初回: 発話開始から 約1秒後に必ず発動(デモ向けに確実に見せるため)
- 2回目以降: 1.5〜3.5秒のランダム間隔
- 動作: Head ボーンを X 軸方向に 5° 傾斜 → 復帰(down/up サイクル)
- 速度: 下り 8.0、上り 4.0(ゆっくり戻る自然な動き)
相槌(あいづち)音声
うなずきに連動して、短い相槌音声を再生します。
- ファイル: 4種類(「はい」「ええ」「はい、はい」「ええ、ええ」)、社長と同じ Orus 声で事前生成
- 初回: 確率100%で必ず再生(デモで確実に見せる)
- 2回目以降: 60%の確率で再生。同じ音声の連続再生を回避
- 音量: メイン回答音声の 50%(GainNode で制御)
- リップシンク: 相槌音声は AnalyserNode をバイパスするため、口は動かない(自然な挙動)
- WebSpeech干渉: スピーカー音量が小さいため、現時点では音声認識を阻害しない
リップシンク
回答音声の再生中、周波数解析で口の動きを制御します。
- 仕組み: AudioContext + BufferSource → AnalyserNode で周波数4帯域を取得
- 母音マッピング: 低域(あ/お)、中域(い/え)、高域(う)の5母音にスコア配分
- VRMモーフ:
aa, oh, ih, ee, ou の expression を重み付け適用
- 勝者総取り: 最もスコアの高い母音のみを表示し、クリアな口形を実現
瞬き
- 間隔: 約3秒(±30%のランダム揺らぎ)
- ダブルブリンク: 20%の確率で連続2回瞬き
- モーフ: VRM
blink expression、速度12(高速な開閉)
フォーマルポーズ
社長らしい佇まいとして、手を前で組むポーズを常時適用。6ボーン(左右の上腕・前腕・手)の回転角度を制御。
5. 回答プール全文一覧
以下は本デモが持つ 234件の回答プールの全文です。各回答には20秒版(短い回答)と40秒版(詳しい回答)の2バリアントがあります。トピックをクリックすると展開します。
6. 別シナリオへの適用手順
この仕組みを「別の人物」「別の組織」に適用する場合の大まかな手順です。
1事実資料の収集
対象人物/組織の公式資料(会社概要、経歴、FAQ等)を集める。これが回答の上限を決める。
2初期Q&A生成
思考モードLLMに資料を渡し、想定質問と回答ペア(20s版/40s版)を生成。初期100件程度を目指す。
3カバレッジ育成
自動テストで事実カバレッジを測定し、不足分を繰り返し補完。目標90%以上。
4ペルソナ別ロールプレイ
複数のペルソナ(想定来訪者)を設定し、会話形式でプールを拡充。口語的・文脈依存の質問を補完。
5Embedding + TTS生成
全質問をembedding化。全回答のTTS音声(MP3)を事前生成。
6VRM + デプロイ
対象人物のVRMアバターを用意。設定ファイルを差し替えてデプロイ。
差し替えが必要なファイル
| ファイル | 内容 | 作業 |
| 事実資料 | 回答の根拠となる公式情報 | 新規収集 |
| answer_variants.jsonl | Q&Aペア | LLMで生成 → カバレッジ育成 |
| answer_embeddings.jsonl | 質問のembedding | スクリプトで自動生成 |
| audio/*.mp3 | 回答音声 | generate_tts.py で自動生成 |
| audio/aizuchi_*.mp3 | 相槌音声 | generate_aizuchi.py で自動生成 |
| avatar/*.vrm | VRMモデル | VRoid Studio 等で作成 |
| server.py 内プロンプト | 指揮者の人格設定 | テキスト修正 |
| index.html タイトル等 | UI文言 | テキスト修正 |
想定作業期間
資料が揃っている前提で、回答プール育成からデプロイまで 約1〜2週間 が目安です。最も時間がかかるのはPhase 2-3のカバレッジ育成(LLM APIコスト含む)です。