この記事は誰が書いているか(一次情報であることの明示)
AI企業・クリスタルメソッド株式会社で、バーチャルヒューマン(対話型AIアバター)製品の開発にGoogleのGemini Live API(双方向音声対話・BidiGenerateContent)を統合した実装記録です。監修は代表・河合継(AIマルチモーダル関連の特許16件の発明者)。
「Gemini Liveでリアルタイム音声対話アバターを作るとどこでつまずくか」を、実装したコードベースから具体的に書きます。
1. 全体構成:なぜ「中継サーバー」を挟んだか
Gemini Live APIはWebSocketで直接双方向にやり取りするAPIです。ブラウザから直接つなぐ設計も可能ですが、私たちは次の構成にしました。
ブラウザ(マイク入力・音声再生・アバター描画)
↕ WebSocket
Node.js 中継サーバー
↕ WebSocket(wss://generativelanguage.googleapis.com/.../BidiGenerateContent)
Gemini Live API
理由は2つです。
- APIキーをブラウザに露出させたくない(クライアント直結だとキーがフロントに出る)
- 既存のWebSocket/Socket.IOサーバーと同居させる際、専用ポートに分離した方が事故が少ない(同一プロセス内で複数のWSサーバーを扱うと、接続の混線やライフサイクル管理が複雑になった)
中継サーバー側はシンプルなメッセージパススルーで、setup(モデル設定送信)→setupComplete受信→クライアントへready通知、という順序でハンドシェイクを管理します。ここを端折って先にクライアント側の音声送信を許可すると、Gemini側の初期化が終わる前にデータが飛んで無視される、という初歩的なハマりどころがありました。
2. Function Callingでアバターの「うなずき」を制御する
今回いちばん実用的だった設計がこれです。Gemini Live APIにはtools.functionDeclarationsで関数を渡せます。私たちはcontrol_avatarという関数を1つ定義し、action(nod=うなずき/smile=笑顔/empathy=共感/surprise=驚き/think=考え中/wave=手を振る)をGemini自身に会話の文脈から判断させて呼ばせるようにしました。
tools: [{
functionDeclarations: [{
name: 'control_avatar',
description: 'アバターの非言語行動を制御する',
parameters: {
type: 'OBJECT',
properties: {
action: { type: 'STRING', enum: ['nod', 'smile', 'empathy', 'surprise', 'think', 'wave'] },
reason: { type: 'STRING', description: '行動の理由' }
},
required: ['action']
}
}]
}]
ポイントは、「いつ・なぜその表情にするか」をあらかじめルール化しすぎず、system instructionで方針だけ与えてモデルの判断に委ねたことです。方針として与えたのは例えばこういう記述です。
- 相手が話している間は、適度にうなずく
- 相手が正解・良い反応を示したら笑顔にする
- 相手が困っていそうなら共感の表情にする
Function Callが飛んできたら、中継サーバーはそれをそのままクライアントへavatar_controlメッセージとして転送し、Gemini側にはtoolResponseで「実行した」と返します。この往復を怠るとGemini側が応答を止めてしまうため、Function Callには必ず結果を返すのはLive APIを使う上での必須ルールです。
3. 沈黙にどう対応するか(システムプロンプトでの閾値設計)
音声対話で地味に難しいのが「相手が黙っている時間」の扱いです。無音を検知する仕組みを別途実装する代わりに、私たちは沈黙への振る舞いもsystem instructionの記述だけで制御しました。
- 5秒程度の沈黙 → 考え中かもしれないのでまだ待つ
- 15秒以上の沈黙 → 「大丈夫ですか?」と優しく声をかける
- 30秒以上の沈黙 → ヒントを出すか話題を変える
- ただし、難しい問題を考えている最中は急かさない
これは検知ロジックをコードで書いたわけではなく、モデルに「どれくらい待つべきか」の目安を自然言語で渡しただけです。会話の文脈(内容の難易度)まで加味して待ち時間を調整してくれる点は、ルールベースの実装より柔軟でした。
4. 応答は「音声のみ」でもテキストが取れる
responseModalities: ['AUDIO'](音声のみ応答)を指定していても、serverContent.modelTurn.partsの中にtextパートが混じって返ってくることがあります。字幕表示やログ用のテキストが欲しい場合、このpart.textを拾えば、音声と別にテキストストリームAPIを叩く必要はありませんでした。逆にpart.inlineData(音声データ本体)とpart.textは同じparts配列に混在するため、受信側でtype分岐する実装が必須です。
5. 職業ごとに応答品質を検証する(多役割テスト手法)
汎用の対話AIアバターを作ると、「教師には教師らしく」「面接官には面接官らしく」振る舞い分けができているかが心配になります。私たちが実際にやったのは、同じAPI呼び出しの枠組みで、system instructionだけを差し替えた6職種のテストケースを流し、応答を横並びで確認するという単純な方法です。
検証した役割の例:
- 教師(数学)
- メンタルヘルスカウンセラー
- 病院受付
- 採用面接官
- 不動産営業アシスタント
- 高齢者施設のAIコンパニオン
各ケースについて、期待されるキーワード(例えば数学教師なら「方程式」「移項」等)を事前に定義し、実際の応答テキストにそれが含まれるか、アバターのアクションは呼ばれたか、応答完了までの時間はどれくらいか、をログに残して比較しました。LLMの応答は決定的ではないため厳密な自動合否判定にはしていませんが、「system instructionの一言を変えただけで役割としての自然さがどう変わるか」を目視で比較できる状態を作ったことが、プロンプトのチューニングを進める上で有効でした。
まとめ
- Gemini Live APIはブラウザ直結よりも中継サーバーを挟む構成が安全(キー保護・接続管理の分離)
- アバターの非言語表現は、細かくコード制御するよりFunction Calling+方針だけのsystem instructionの方が柔軟に動く
- 沈黙対応のような「間」の設計も自然言語のプロンプトだけで十分実用に耐える
- 音声応答モードでもテキストは取得できる(parts内のtype分岐が必要)
- 複数職種のsystem instructionを横並びでテストする簡易な手法だけでも、役割ごとの応答品質差は見えてくる
Geminiの技術的な全体像(モデル体系・料金・エージェント機能)は当社の解説記事にまとめています。