AIアバターが会話しながら裏で処理を進める体験は便利です。しかし、表情や口元が自然であるほど、利用者は「もう処理も終わった」と受け取りやすくなります。
開発側にも別の緊張があります。AIを賢くすれば解決するように見える一方、実際に人が引き受けるべき仕事は、完了条件、表示上の約束、同意、低性能端末での縮退方法を決めることです。
本稿では、次のBeauty ARシナリオを再現します。
- アバターは会話を継続する
- バックエンドでは検索や登録処理が非同期に動く
- アバターの自然さを処理成功の証拠にしない
- 端末性能が低い場合も、会話自体は止めず表示だけを軽くする
- アバター利用への同意が取り消されたら、即座に代替表示へ切り替える
結論:アバターの表情ではなく、確定した業務状態を表示する
実装の中心は、次の4状態を別々に持つことです。
- 会話状態:聞いている、話している、無音
- 業務状態:未実行、処理中、確認待ち、成功、失敗、有人対応
- 端末Capability:high、mid、low
- 利用者同意:アバター、背景分離、動きの抑制
LLMには回答や意図候補を生成させても、succeededへの変更権限は渡しません。成功表示は、予約DBや業務APIなど、実際の処理主体から届いた確定イベントだけで更新します。
また、低性能端末では会話を終了するのではなく、3D表現、背景分離、解像度、フレームレートなど、映像側のコストを落とします。
Tencent RTCのBeauty ARは、通話やライブ配信におけるリアルタイムの美顔、メイク、ステッカー、バーチャル背景、アバターなどを扱います。対応範囲は公式概要を確認してください。
低性能端末向けガイドでは、端末Tierに応じた性能モードの選択、高コストな背景分離や3D/GAN系表現の無効化、解像度・フレームレートの制御が案内されています。
前提:AI、業務処理、Beauty ARを一つの成功状態にしない
今回の責任分界は次のとおりです。
音声入力
└─ 音声認識
└─ LLM ── 発話候補
└─ TTS ── 音声再生
業務API ── 確定イベント ── 業務状態
会話状態 + 業務状態 + 端末Tier + 同意
└─ Presentation Policy
└─ Beauty AR Adapter
Beauty ARは映像表現を担当します。LLMが自然に「確認しています」と話せることと、業務APIが成功したことは別です。
この分離により、AIや業務APIの応答が遅れても、次のような表示が可能になります。
| 業務状態 | 画面上の表現 | アバターが話せる内容 |
|---|---|---|
running |
「確認中」 | 待ち時間の案内、追加質問 |
awaiting_confirmation |
確認ボタンを表示 | 実行前の確認 |
succeeded |
「完了」 | 確定した結果 |
failed |
再試行・有人対応 | 失敗した事実と次の選択肢 |
handoff |
担当者へ接続中 | 引き継ぎ範囲の説明 |
手順1:端末名ではなく、計測結果からTierを決める
機種名やUser-Agentだけで性能を固定すると、発熱、他アプリの負荷、OS更新などを反映できません。入室前の短いプレビューで、実際に利用するエフェクト候補を描画し、フレーム時間と欠落率を記録します。
以下の閾値はTencent RTCの保証値ではなく、実装を再現するための仮値です。対象端末で計測して調整してください。
type DeviceTier = "high" | "mid" | "low";
type ProbeSample = {
frameMs: number;
dropped: boolean;
};
function percentile(values: number[], ratio: number): number {
const sorted = [...values].sort((a, b) => a - b);
const index = Math.min(
sorted.length - 1,
Math.floor(sorted.length * ratio)
);
return sorted[index];
}
export function classifyDevice(samples: ProbeSample[]): DeviceTier {
if (samples.length === 0) return "low";
const p95FrameMs = percentile(
samples.map((sample) => sample.frameMs),
0.95
);
const droppedRatio =
samples.filter((sample) => sample.dropped).length / samples.length;
// アプリ側の初期値。実機検証後に変更する。
if (p95FrameMs <= 20 && droppedRatio <= 0.02) return "high";
if (p95FrameMs <= 35 && droppedRatio <= 0.08) return "mid";
return "low";
}
Tier判定に使うエフェクトと、本番で使うエフェクトの負荷が大きく異なると計測の意味がなくなります。最低でも、アバター描画、背景分離の有無、入力解像度を本番候補へ合わせます。
手順2:会話状態と業務状態を別のデータにする
バックエンドイベントには単調増加するrevisionを付けます。イベント到着順が逆転しても、古い「処理中」や「失敗」で最新状態を上書きしないためです。
type ConversationPhase = "silent" | "listening" | "speaking";
type TaskPhase =
| "idle"
| "running"
| "awaiting_confirmation"
| "succeeded"
| "failed"
| "handoff";
type AppState = {
conversation: ConversationPhase;
task: {
revision: number;
phase: TaskPhase;
publicMessage?: string;
};
};
type TaskEvent = {
revision: number;
phase: TaskPhase;
publicMessage?: string;
};
export function acceptTaskEvent(
state: AppState,
event: TaskEvent
): AppState {
if (event.revision <= state.task.revision) {
return state;
}
return {
...state,
task: {
revision: event.revision,
phase: event.phase,
publicMessage: event.publicMessage,
},
};
}
publicMessageには画面へ出せる文だけを入れます。内部エラー、検索クエリ、個人情報をそのままアバター表示へ流さないようにします。
手順3:同意と端末Tierから表示計画を作る
ここではBeauty AR SDKを直接操作せず、まずアプリ内の表示計画へ変換します。SDK固有の設定はAdapter側へ閉じ込めます。
type Consent = {
avatar: boolean;
virtualBackground: boolean;
reducedMotion: boolean;
};
type RenderMode =
| "hidden"
| "static_portrait"
| "lightweight_avatar"
| "full_avatar";
type PresentationPlan = {
renderMode: RenderMode;
mouthSync: boolean;
segmentation: boolean;
advanced3D: boolean;
targetFps: 15 | 24 | 30;
renderScale: 0.5 | 0.75 | 1;
statusLabel: string;
};
function taskLabel(phase: TaskPhase): string {
switch (phase) {
case "idle":
return "お話しください";
case "running":
return "確認中です";
case "awaiting_confirmation":
return "実行前の確認が必要です";
case "succeeded":
return "完了しました";
case "failed":
return "処理を完了できませんでした";
case "handoff":
return "担当者へ引き継いでいます";
}
}
export function derivePresentation(
state: AppState,
tier: DeviceTier,
consent: Consent
): PresentationPlan {
const statusLabel = taskLabel(state.task.phase);
if (!consent.avatar) {
return {
renderMode: "hidden",
mouthSync: false,
segmentation: false,
advanced3D: false,
targetFps: 15,
renderScale: 0.5,
statusLabel,
};
}
if (consent.reducedMotion || tier === "low") {
return {
renderMode: "static_portrait",
mouthSync: false,
segmentation: false,
advanced3D: false,
targetFps: 15,
renderScale: 0.5,
statusLabel,
};
}
if (tier === "mid") {
return {
renderMode: "lightweight_avatar",
mouthSync: state.conversation === "speaking",
segmentation: false,
advanced3D: false,
targetFps: 24,
renderScale: 0.75,
statusLabel,
};
}
return {
renderMode: "full_avatar",
mouthSync: state.conversation === "speaking",
segmentation: consent.virtualBackground,
advanced3D: true,
targetFps: 30,
renderScale: 1,
statusLabel,
};
}
targetFpsとrenderScaleも説明用の初期値であり、全端末に適した推奨値ではありません。重要なのは数値そのものではなく、lowで高コスト機能を確実に選ばないことです。
また、static_portraitはBeauty ARの固有機能名ではなく、アプリが用意する代替UIです。SDKへ存在しないAPI名やモード名として扱わないでください。
コード:古いイベントと同意撤回を検証する
上記を同じavatar-policy.tsへ保存し、次のコードを末尾へ追加します。
import assert from "node:assert/strict";
let state: AppState = {
conversation: "speaking",
task: {
revision: 0,
phase: "idle",
},
};
state = acceptTaskEvent(state, {
revision: 1,
phase: "running",
publicMessage: "空き状況を確認しています",
});
const runningPlan = derivePresentation(state, "high", {
avatar: true,
virtualBackground: true,
reducedMotion: false,
});
assert.equal(runningPlan.statusLabel, "確認中です");
assert.equal(runningPlan.mouthSync, true);
// revision 3で成功が確定した後、遅れて届いたrevision 2は無視する。
state = acceptTaskEvent(state, {
revision: 3,
phase: "succeeded",
publicMessage: "予約が確定しました",
});
state = acceptTaskEvent(state, {
revision: 2,
phase: "failed",
});
assert.equal(state.task.phase, "succeeded");
const lowTierPlan = derivePresentation(state, "low", {
avatar: true,
virtualBackground: true,
reducedMotion: false,
});
assert.equal(lowTierPlan.renderMode, "static_portrait");
assert.equal(lowTierPlan.segmentation, false);
assert.equal(lowTierPlan.advanced3D, false);
const revokedPlan = derivePresentation(state, "high", {
avatar: false,
virtualBackground: true,
reducedMotion: false,
});
assert.equal(revokedPlan.renderMode, "hidden");
assert.equal(revokedPlan.mouthSync, false);
console.log("all checks passed");
実行環境を作ります。
mkdir beauty-ar-avatar-policy
cd beauty-ar-avatar-policy
npm init -y
npm install --save-dev typescript tsx
npx tsx avatar-policy.ts
成功時は次のように表示されます。
all checks passed
手順4:Beauty AR Adapterへ渡す
実際のアプリでは、PresentationPlanを各プラットフォームのBeauty AR設定へ変換します。
interface BeautyARAdapter {
apply(plan: PresentationPlan): Promise<void>;
showStatus(label: string): void;
}
export async function updatePresentation(
adapter: BeautyARAdapter,
state: AppState,
tier: DeviceTier,
consent: Consent
): Promise<void> {
const plan = derivePresentation(state, tier, consent);
// 状態ラベルは映像処理の成否に依存させない。
adapter.showStatus(plan.statusLabel);
try {
await adapter.apply(plan);
} catch {
// アバター初期化失敗を業務失敗として扱わない。
await adapter.apply({
...plan,
renderMode: "static_portrait",
mouthSync: false,
segmentation: false,
advanced3D: false,
targetFps: 15,
renderScale: 0.5,
});
}
}
ここで重要なのは、Beauty ARの初期化失敗と業務処理の失敗を混同しないことです。アバターを軽量表示へ切り替えても、業務APIが成功していれば「完了」の状態は維持します。反対に、アバターが滑らかに話していても、業務APIがrunningなら完了表示にはしません。
具体的なSDK組み込み方法や利用可能な機能は、対象プラットフォームの公式ドキュメントに合わせてAdapterへ実装してください。
確認方法:正常系より「見た目と事実がずれる瞬間」を試す
1. 話している途中で業務APIを遅延させる
- アバターは待ち時間の案内を話せる
- ステータスは
確認中ですのまま - 発話終了だけで
完了しましたにならない
2. 成功後に古い失敗イベントを送る
- 小さい
revisionは破棄される - 成功表示が失敗へ巻き戻らない
3. low Tierを強制する
- 背景分離が無効になる
- 3D系表現を選ばない
- 静止画になっても状態ラベルと音声会話は残る
4. 会話中にアバター同意を取り消す
- 次のセッションまで待たず
hiddenへ移る - 口元同期を停止する
- 業務状態と音声会話は必要に応じて継続できる
5. Beauty AR初期化を失敗させる
- 軽量な代替表示へ切り替わる
- 「予約失敗」など、無関係な業務エラーを表示しない
6. 確認待ちでAIに完了文を生成させる
LLMが「手続きは完了しました」と生成しても、画面の確定表示はawaiting_confirmationを維持します。可能ならTTSへ渡す前にも業務状態を照合し、未確定の完了表現を拒否します。
AIに任せる範囲、人が所有する範囲
AIが有効なのは、利用者の言い換えを理解する、待ち時間に追加質問をする、確定結果を分かりやすく説明するといった部分です。
一方、次の判断は決定論的なコードと運用担当者が所有します。
- 何をもって処理成功とするか
- 本人確認や実行前確認が必要な操作
- どの端末Tierでどの表現を止めるか
- アバター、背景分離、保存データへの同意
- 失敗時に誰へ引き継ぐか
- どの状態文言を利用者へ約束として表示するか
「AIによって自分の役割がなくなるのでは」という不安は、実装上は別の問いに置き換えられます。自然な会話の裏で、誰が結果の正しさと利用者への約束を所有するのかです。この所有者は、モデルの性能が上がっても必要です。
注意点とトレードオフ
表情を処理進捗へ直接対応させない
「笑顔になったから成功」「考える表情だから処理中」のような表現だけでは、見え方や文化的解釈に依存します。状態ラベル、確認ボタン、エラー表示を併用します。
自動縮退を黙って行わない
表示を軽量化したときは「端末負荷を抑える表示に切り替えました」のように説明し、利用者が戻すか、そのまま使うかを選べるようにします。ただし、再び過負荷になる可能性も併記します。
新しいフィードバック窓口を増やさない
アバター表示への不満を集めるためだけに、所有者不明のチャットやフォームを増やす必要はありません。既存のセッション終了画面へ、例えば次の理由コードを追加します。
type AvatarFeedbackReason =
| "motion_uncomfortable"
| "device_became_hot"
| "status_unclear"
| "preferred_static_view"
| "other";
自由記述より先に理由コードを保存すると、端末Tierや選択された表示計画と関連付けて集計できます。保存目的、保持期間、閲覧担当も既存の運用責任へ紐付けます。
Capability判定を恒久的な端末評価にしない
lowは端末や利用者の価値を表す分類ではなく、そのセッションで安全に使える映像予算です。発熱やバックグラウンド負荷により変化し得るため、必要に応じて再計測できる設計にします。
実装チェックリスト
- LLMの文章だけで業務状態を成功へ変更していない
- 会話状態と業務状態を別フィールドで保持している
- 業務イベントに連番または同等の順序保証がある
- low Tierで背景分離、3D/GAN系表現を無効化できる
- 解像度とフレームレートを段階的に調整できる
- アバター同意をセッション中に撤回できる
- アバター停止後も状態ラベルが残る
- Beauty AR障害と業務API障害を別々に表示する
-
running、failed、handoffを実機で確認した - フィードバックの閲覧担当と対応期限が決まっている
まとめ
会話を止めないAIアバターで難しいのは、口元を動かすことだけではありません。自然な映像、バックエンドの事実、利用者が受け取る約束を一致させる必要があります。
そのためには、会話状態と業務状態を分離し、成功は業務システムの確定イベントからのみ反映します。Beauty AR側は端末Capabilityと同意に基づいて表示を選び、低性能端末では高コストな表現から静止画へ縮退させます。
AIは会話を助けられますが、完了条件、同意、縮退ポリシー、有人対応の所有者にはなりません。ここをコードと運用で明示することが、本番のアバター体験をデモで終わらせない次の一歩です。
関係性の開示:本稿はTencent RTCに関わるコンテンツ制作の一環として執筆し、実装上の参照資料としてTencent RTC公式ドキュメントを使用しました。