はじめに
Webアプリやサービスにキャラクターを実装しようと考えたとき、私たちは今、二つの大きな分岐点に立っています。
一つは、Live2Dに代表される「職人芸の世界」です。パーツを切り分け、緻密なリギングを施すことで、アニメやゲームレベルの完璧な制御と美しさを実現します。この「魂を込める」ような手作業が生み出す品質は、技術者としても深く尊敬すべき領域ですが、同時に膨大な時間と専門スキルを要求される、非常に険しい道でもあります。
もう一つは、生成AI(HeyGen等)による「自動リップシンクの世界」です。こちらはクリエイティブな事前準備をスキップし、1枚の画像(あるいはAIがその場で生成した画像)を即座に喋らせるアプローチです。「職人に依頼する」のではなく「AIに依頼する」ことで、準備コストを極限まで削ぎ落とします。
本稿では、どちらが良い悪いではなく、「今のAI技術で、準備なしにどこまでの印象を作れるのか」という到達点と、「運用コストや制限」という現実的な側面を整理します。
1. 概要
AIスタッフとのリアルタイムな音声会話において、HeyGenのリアルなアバターをリップシンクで動かす機能を実装しました。
今回は、HeyGenの「LITEモード」を採用しています。これは、自前のLLM(今回はGemini)とTTS(音声)を使い、HeyGenには「リップシンク映像の生成」のみを担当させる高度な開発者向けモードです。
使用した主要技術
- Gemini Multimodal Live API: 思考エンジン + 音声生成(TTS)
- HeyGen LiveAvatar LITE モード: リップシンク映像生成
- LiveKit: WebRTCによる低遅延な映像配信
2. システムアーキテクチャ
全体のデータフローは以下の通りです。
- ユーザーのマイク入力 → バックエンド経由で Gemini Live API へ。
- Gemini が音声(PCM 24kHz)を生成。
- バックエンドが音声を2分岐して転送:
- フロントエンド: ユーザーへのスピーカー再生用。
- HeyGen WebSocket: リップシンク映像生成用(
agent.speakコマンド)。
- HeyGen が映像を生成し、LiveKit 経由でフロントエンドに配信。
- フロントエンドで映像と音声を同期(500msのディレイ調整)して表示。
3. バックエンド・エージェント方式
今回の開発では、「フロントエンドSDKではなく、バックエンドから命令を送る」ことで成功しました。
失敗したアプローチ
- フロントエンドSDKの
repeat()メソッド → LITEモードでは使用不可。 - フロントエンドからの
repeatAudio()送信 → アバターが反応しない。 - フロントエンドから直接WebSocket送信 → 映像が動かない。
成功したアプローチ:バックエンド・エージェント方式
HeyGenのLITEモードの本質は、「バックエンドがエージェント(発話主体)として振る舞う」ことにあります。 フロントエンドはあくまで「視聴者」としてLiveKit Roomに接続し、バックエンドが「エージェント」としてWebSocket経由で音声を流し込むことで、初めてリップシンクが動作します。
4. 実装コードのポイント
バックエンド:セッション開始
バックエンド側でセッションを開始し、ws_url と livekit_client_token を取得します。
# 1. セッショントークンの取得
token_resp = requests.post(
"https://api.liveavatar.com/v1/sessions/token",
headers={"X-API-KEY": api_key},
json={
"mode": "LITE",
"avatar_id": "YOUR_AVATAR_ID",
"video_settings": {"quality": "high", "encoding": "VP8"},
},
)
# 2. バックエンドがセッションを開始(エージェントとして認識させる)
start_resp = requests.post(
"https://api.liveavatar.com/v1/sessions/start",
headers={"Authorization": f"Bearer {session_token}"},
)
フロントエンド:LiveKitへの直接接続
SDKの session.start() は使わず、バックエンドから受け取ったトークンでLiveKitに直接接続します。
// LiveKit Roomに直接接続(映像受信のみに特化)
const room = new lk.Room();
await room.connect(livekit_url, livekit_client_token);
// 参加者 'heygen' のVideoTrackが届いたら表示
room.on(lk.RoomEvent.TrackSubscribed, (track) => {
if (track.kind === 'video') {
track.attach(videoElement);
}
});
5. ハマったポイントと解決策
開発中に直面した主要な課題と、その解決策をまとめました。
| 課題 | 原因 | 解決策 |
|---|---|---|
| アバターが動かない | フロントエンドSDKからの命令は無視される | バックエンドの ws_url 経由で送信する |
| Session already exists エラー | SDKの start() とバックエンドの start が競合 | SDKを使わずLiveKitに直接接続する |
| 音声と口の動きがズレる | 映像のレンダリング遅延(約500ms) | ローカルの音声再生に setTimeout で遅延を入れる |
映像が muted のまま | サーバーが音声を受理していない | agent.speak コマンドのJSON構造とサンプリングレートを再確認 |
最後に
LiveAvatarのLITEのクレジットは1分に1クレジットとなっている。おそらく開発中にセッションのオープンクローズを繰り返したため、速攻で150クレジットを消費した。1クレジットが約20円ですが、ちりつもなので、テストの時は注意しましょう。