アーキテクチャ — 社長アバター会話デモ

← デモに戻る

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必須
Embeddinggemini-embedding-0013072次元、Google Generative AI
LLM指揮者gemini-3.1-flash-lite-preview軽量LLM、temperature=0
TTS (事前生成)Gemini 2.5 Flash TTS PreviewOrus音声、MP3 128kbps
アバターThree.js 0.164.1 + @pixiv/three-vrm v3VRM形式、CDN配信
サーバーFastAPI + uvicornPython、ポート8001
リバースプロキシnginx + Let's EncryptHTTPS終端、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: ペルソナ別ロールプレイ育成

カバレッジが一定水準に達したら、複数のペルソナ(想定来訪者)を設定し、自然な会話形式のロールプレイで回答プールをさらに育てます。

Phase 4: Embeddingデータ構築 + TTS音声生成

育成が完了した回答プールに対して、以下のデータを事前生成します。

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 Top1LLM 選択補足
「本社が東京にある理由は?」 M004 (sim=0.85)
東京拠点の住所説明
M112
富山本社の由来・歴史
LLMは「理由」という意図を読み取り、適切な回答を選択
「TISIになることで何が変わる?」 M111 (sim=0.74) M070
体制変更の具体的影響
「変化」の意図に合う回答をLLMが判断

実測49件のログで、LLM選択とTop1が一致するのは 67.3%。残り33%で LLM がより適切な回答を選んでおり、精度向上に明確に貢献しています。

4. アバター演出

うなずき(ヘッドノッド)

ユーザーが話している間(マイク録音中)、アバターが一定間隔で頷きます。

相槌(あいづち)音声

うなずきに連動して、短い相槌音声を再生します。

リップシンク

回答音声の再生中、周波数解析で口の動きを制御します。

瞬き

フォーマルポーズ

社長らしい佇まいとして、手を前で組むポーズを常時適用。6ボーン(左右の上腕・前腕・手)の回転角度を制御。

5. 回答プール全文一覧

以下は本デモが持つ 234件の回答プールの全文です。各回答には20秒版(短い回答)と40秒版(詳しい回答)の2バリアントがあります。トピックをクリックすると展開します。

読み込み中…

6. 別シナリオへの適用手順

この仕組みを「別の人物」「別の組織」に適用する場合の大まかな手順です。

1

事実資料の収集

対象人物/組織の公式資料(会社概要、経歴、FAQ等)を集める。これが回答の上限を決める。

2

初期Q&A生成

思考モードLLMに資料を渡し、想定質問と回答ペア(20s版/40s版)を生成。初期100件程度を目指す。

3

カバレッジ育成

自動テストで事実カバレッジを測定し、不足分を繰り返し補完。目標90%以上。

4

ペルソナ別ロールプレイ

複数のペルソナ(想定来訪者)を設定し、会話形式でプールを拡充。口語的・文脈依存の質問を補完。

5

Embedding + TTS生成

全質問をembedding化。全回答のTTS音声(MP3)を事前生成。

6

VRM + デプロイ

対象人物のVRMアバターを用意。設定ファイルを差し替えてデプロイ。

差し替えが必要なファイル

ファイル内容作業
事実資料回答の根拠となる公式情報新規収集
answer_variants.jsonlQ&AペアLLMで生成 → カバレッジ育成
answer_embeddings.jsonl質問のembeddingスクリプトで自動生成
audio/*.mp3回答音声generate_tts.py で自動生成
audio/aizuchi_*.mp3相槌音声generate_aizuchi.py で自動生成
avatar/*.vrmVRMモデルVRoid Studio 等で作成
server.py 内プロンプト指揮者の人格設定テキスト修正
index.html タイトル等UI文言テキスト修正

想定作業期間

資料が揃っている前提で、回答プール育成からデプロイまで 約1〜2週間 が目安です。最も時間がかかるのはPhase 2-3のカバレッジ育成(LLM APIコスト含む)です。