0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが選んだ3D Beauty ARをそのまま実行しない:許可済みRegistryと実行時サーキットブレーカーをTypeScriptで作る

0
Last updated at Posted at 2026-09-13

Beauty ARへAIを組み込むと、「ナチュラルなメイクにして」「このキャラクター風にして」といった要求からエフェクトを選べます。

しかし、AIがもっともらしいエフェクト名を返せることと、そのエフェクトを現在の端末で安全に実行できることは別問題です。特に背景分離、3Dアバター、GAN系エフェクトは、CPU/GPU負荷や端末発熱の影響を受けます。デモで一度動いても、ライブ配信中に複数機能が重なると維持できない場合があります。

この記事では、AIやUIから届いた要求を直接Beauty ARへ渡さず、次の4段階で制御する実装を作ります。

  1. 許可済みエフェクトRegistryでIDを解決する
  2. ユーザー同意と端末Tierを確認する
  3. 実行中のフレーム処理時間を監視する
  4. 過負荷が続いたら、一段だけ縮退するか停止する

結論:AIには「希望」を出させ、実行可能性は決定論的なコードで判定する

今回の責任分担は次のとおりです。

判断 担当
ユーザーの希望をスタイル候補へ変換 AIまたはUI
利用可能なエフェクトIDの管理 許可済みRegistry
顔・背景処理への同意 ユーザー
端末で開始可能か Capability Policy
実行中に継続可能か Runtime Governor
縮退後に高負荷機能へ戻すか ユーザーまたは運営ポリシー

生成AIが得意なのは、自然言語から候補を絞ることです。一方、実際のGPU余力、発熱、同意状態まで正確に判断できるわけではありません。そのため、AIの出力は自由なパラメーターではなく、Registryに登録済みのeffectIdだけに制限します。

前提:Beauty ARの機能数ではなく、同時実行時の予算を考える

Tencent RTCのBeauty ARでは、リアルタイムの美顔、メイク、ステッカー、バーチャル背景、アバター、ジェスチャー認識、画像・映像補正などが扱われます。

ただし、利用できる機能をすべて同時に有効化する設計にはしません。公式の低性能端末向けガイドでは、端末Tierに応じたパフォーマンスモードの選択、高負荷な背景分離・3D・GAN系エフェクトの無効化、解像度やフレームレートの制御が案内されています。

本稿ではこの考え方を、開始前の判定だけでなく実行中の縮退制御へ拡張します。

なお、以下で使うTierや閾値はTencent RTCの保証値ではありません。アプリ側で検証するための仮設定です。実運用では対象端末群で計測して調整してください。

先に再現する3つの失敗

1. AIがRegistryにないIDを返す

LLMがcinematic_avatar_v9のような存在しないIDを生成しても、文字列をそのままSDKへ渡してはいけません。未知のIDとして拒否します。

2. 3Dエフェクトを低Tier端末で開始する

AIの提案が妥当でも、端末条件を満たさない場合があります。ユーザーが縮退を許可していなければ、別の見た目へ勝手に変更せず停止します。

3. 開始時は動いたが、配信中に負荷が増える

端末Tierは静的な目安です。温度上昇、他アプリ、映像エンコード、背景分離の追加などにより実行時負荷は変わります。開始前判定を通過しても、継続監視が必要です。

手順1:検証用プロジェクトを作る

Node.js 20以降を前提にします。

mkdir beauty-runtime-governor
cd beauty-runtime-governor
npm init -y
npm install -D typescript tsx @types/node

governor.tsを作成し、以降のコードを保存します。

手順2:エフェクトを自由なJSONではなくRegistryへ登録する

Registryには、SDK固有のオブジェクトではなく、アプリが判断に使うメタデータを保持します。

type DeviceTier = 'low' | 'mid' | 'high';
type ConsentScope = 'face-processing' | 'background-processing';
type EffectKind = 'beauty' | 'makeup' | 'segmentation' | 'avatar-3d';

type EffectDefinition = Readonly<{
  id: string;
  kind: EffectKind;
  minTier: DeviceTier;
  requiredConsent: readonly ConsentScope[];
  fallbackId: string | null;
}>;

const registryVersion = '2026-09-14-demo';

const registry = new Map<string, EffectDefinition>([
  ['basic-soft', Object.freeze({
    id: 'basic-soft',
    kind: 'beauty',
    minTier: 'low',
    requiredConsent: ['face-processing'],
    fallbackId: null,
  })],
  ['natural-makeup', Object.freeze({
    id: 'natural-makeup',
    kind: 'makeup',
    minTier: 'mid',
    requiredConsent: ['face-processing'],
    fallbackId: 'basic-soft',
  })],
  ['background-blur', Object.freeze({
    id: 'background-blur',
    kind: 'segmentation',
    minTier: 'mid',
    requiredConsent: ['background-processing'],
    fallbackId: null,
  })],
  ['avatar-3d', Object.freeze({
    id: 'avatar-3d',
    kind: 'avatar-3d',
    minTier: 'high',
    requiredConsent: ['face-processing'],
    fallbackId: null,
  })],
]);

avatar-3dに勝手なフォールバックを設定していない点が重要です。3Dアバターを通常の美顔へ置き換えると、ユーザーが選んだ表現の意味自体が変わります。その場合は「低品質版へ落とす」のではなく、一度停止して選び直してもらう方が明確です。

実運用ではRegistryのバージョンに加え、アセットのハッシュ、配布元、対応SDKバージョン、審査状態も保存候補になります。リモート設定を使う場合も、未検証のIDをその場で追加できる構成にはしません。

手順3:同意・端末Tier・縮退許可をまとめて判定する

AIから受け取る要求は、次の小さな形式に限定します。

type EffectRequest = {
  effectId: string;
  source: 'user-ui' | 'ai-suggestion';
  allowFallback: boolean;
};

type RuntimeContext = {
  tier: DeviceTier;
  consent: ReadonlySet<ConsentScope>;
};

type EffectPlan =
  | { status: 'active'; effect: EffectDefinition; reason: string }
  | { status: 'blocked'; reason: string }
  | { status: 'paused'; reason: string };

const tierRank: Record<DeviceTier, number> = {
  low: 0,
  mid: 1,
  high: 2,
};

function hasConsent(
  effect: EffectDefinition,
  consent: ReadonlySet<ConsentScope>,
): boolean {
  return effect.requiredConsent.every(scope => consent.has(scope));
}

function resolveInitialPlan(
  request: EffectRequest,
  context: RuntimeContext,
): EffectPlan {
  let candidate = registry.get(request.effectId);

  if (!candidate) {
    return {
      status: 'blocked',
      reason: `unknown effect id: ${request.effectId}`,
    };
  }

  if (!hasConsent(candidate, context.consent)) {
    return {
      status: 'blocked',
      reason: `consent missing for ${candidate.id}`,
    };
  }

  while (tierRank[context.tier] < tierRank[candidate.minTier]) {
    if (!request.allowFallback || candidate.fallbackId === null) {
      return {
        status: 'paused',
        reason: `${candidate.id} is not allowed on tier ${context.tier}`,
      };
    }

    const fallback = registry.get(candidate.fallbackId);
    if (!fallback) {
      return {
        status: 'blocked',
        reason: `broken registry fallback: ${candidate.fallbackId}`,
      };
    }

    if (!hasConsent(fallback, context.consent)) {
      return {
        status: 'blocked',
        reason: `consent missing for fallback ${fallback.id}`,
      };
    }

    candidate = fallback;
  }

  return {
    status: 'active',
    effect: candidate,
    reason: candidate.id === request.effectId
      ? 'requested effect accepted'
      : `degraded from ${request.effectId} to ${candidate.id}`,
  };
}

AI提案では、allowFallbackの初期値をfalseにするのが安全です。AIが選んだ見た目から別の見た目へ自動変更するより、「この端末では軽量版へ変更できます」とUIで確認した方が、ユーザーの意図を保てます。

コード:連続過負荷で一段だけ縮退する

次に、エフェクト処理時間とドロップフレームを監視するRuntime Governorを追加します。

type RuntimeSample = {
  effectProcessingMs: number;
  configuredFps: number;
  droppedFramesSinceLastSample: number;
};

type GovernorEvent =
  | { type: 'none' }
  | { type: 'degraded'; from: string; to: string; reason: string }
  | { type: 'paused'; from: string; reason: string };

class RuntimeGovernor {
  private plan: EffectPlan;
  private consecutiveOverBudget = 0;

  constructor(
    initialPlan: EffectPlan,
    private readonly request: EffectRequest,
    private readonly context: RuntimeContext,
  ) {
    this.plan = initialPlan;
  }

  currentPlan(): EffectPlan {
    return this.plan;
  }

  observe(sample: RuntimeSample): GovernorEvent {
    if (this.plan.status !== 'active') {
      return { type: 'none' };
    }

    const frameBudgetMs = 1000 / sample.configuredFps;

    // 70%と3回連続は検証用の仮値。対象端末で要調整。
    const overloaded =
      sample.effectProcessingMs > frameBudgetMs * 0.7 ||
      sample.droppedFramesSinceLastSample > 0;

    this.consecutiveOverBudget = overloaded
      ? this.consecutiveOverBudget + 1
      : 0;

    if (this.consecutiveOverBudget < 3) {
      return { type: 'none' };
    }

    this.consecutiveOverBudget = 0;
    return this.degradeOnce();
  }

  private degradeOnce(): GovernorEvent {
    if (this.plan.status !== 'active') {
      return { type: 'none' };
    }

    const current = this.plan.effect;

    if (!this.request.allowFallback || current.fallbackId === null) {
      this.plan = {
        status: 'paused',
        reason: `runtime budget exceeded by ${current.id}`,
      };

      return {
        type: 'paused',
        from: current.id,
        reason: 'no approved runtime fallback',
      };
    }

    const fallback = registry.get(current.fallbackId);
    if (
      !fallback ||
      tierRank[this.context.tier] < tierRank[fallback.minTier] ||
      !hasConsent(fallback, this.context.consent)
    ) {
      this.plan = {
        status: 'paused',
        reason: `fallback validation failed for ${current.id}`,
      };

      return {
        type: 'paused',
        from: current.id,
        reason: 'fallback is unavailable or not consented',
      };
    }

    this.plan = {
      status: 'active',
      effect: fallback,
      reason: `runtime degradation from ${current.id}`,
    };

    return {
      type: 'degraded',
      from: current.id,
      to: fallback.id,
      reason: 'runtime budget exceeded repeatedly',
    };
  }
}

1回の遅いフレームだけでは変更せず、連続して予算を超えた場合だけ反応します。また、軽量化後の自動復帰は実装していません。負荷の境界付近で高負荷・低負荷エフェクトが往復するのを避けるためです。

高負荷機能へ戻す場合は、一定時間の安定確認に加え、ユーザー操作または明示的な運営ポリシーを通します。

手順4:Tencent RTCとの境界をAdapterへ閉じ込める

SDK固有のメソッドをPolicy内部へ書くと、Web、Android、iOSで判定ロジックが分裂します。アプリ側の境界を次のようにします。

interface BeautyRuntimeAdapter {
  applyRegisteredEffect(effectId: string): Promise<void>;
  disableCurrentEffect(): Promise<void>;
}

async function commitPlan(
  adapter: BeautyRuntimeAdapter,
  plan: EffectPlan,
): Promise<void> {
  if (plan.status === 'active') {
    await adapter.applyRegisteredEffect(plan.effect.id);
    return;
  }

  await adapter.disableCurrentEffect();
}

BeautyRuntimeAdapterは本稿のアプリ内インターフェースであり、Tencent RTC SDKのAPI名ではありません。実際の実装では、対象プラットフォームの公式ドキュメントに従って、登録済みIDをBeauty ARの設定・アセット適用処理へ対応付けてください。

フレームレートや解像度も縮退対象にする場合は、エフェクト変更と映像設定変更を同じ巨大な関数へ入れず、別のVideo Profile Adapterとして分けると検証しやすくなります。

確認方法:AIの回答品質ではなく、拒否と縮退をテストする

governor.tsの末尾へ次を追加します。

import assert from 'node:assert/strict';

const faceConsent = new Set<ConsentScope>(['face-processing']);

// 未登録IDは拒否する
assert.equal(
  resolveInitialPlan(
    { effectId: 'ai-generated-effect', source: 'ai-suggestion', allowFallback: true },
    { tier: 'high', consent: faceConsent },
  ).status,
  'blocked',
);

// 低Tierで3Dアバターを勝手に別表現へ変えない
assert.equal(
  resolveInitialPlan(
    { effectId: 'avatar-3d', source: 'ai-suggestion', allowFallback: false },
    { tier: 'low', consent: faceConsent },
  ).status,
  'paused',
);

// ユーザーが縮退を許可した場合だけメイクから基本美顔へ変更
const request: EffectRequest = {
  effectId: 'natural-makeup',
  source: 'user-ui',
  allowFallback: true,
};

const initial = resolveInitialPlan(request, {
  tier: 'high',
  consent: faceConsent,
});

assert.equal(initial.status, 'active');

const governor = new RuntimeGovernor(initial, request, {
  tier: 'high',
  consent: faceConsent,
});

const overloadedSample: RuntimeSample = {
  effectProcessingMs: 28,
  configuredFps: 30,
  droppedFramesSinceLastSample: 1,
};

assert.deepEqual(governor.observe(overloadedSample), { type: 'none' });
assert.deepEqual(governor.observe(overloadedSample), { type: 'none' });

const event = governor.observe(overloadedSample);
assert.equal(event.type, 'degraded');

const current = governor.currentPlan();
assert.equal(current.status, 'active');
if (current.status === 'active') {
  assert.equal(current.effect.id, 'basic-soft');
}

console.log({ registryVersion, event, current });

実行します。

npx tsx governor.ts

例外が発生せず、natural-makeupからbasic-softへの縮退イベントが表示されれば、最小の制御経路を確認できています。

実機での確認チェックリスト

モックを通過した後は、対象端末ごとに次を確認します。

  • Beauty ARなしの基準値を先に取得したか
  • 美顔、メイク、背景分離、3Dを個別に計測したか
  • 配信・通話の映像エンコードと同時に計測したか
  • 数分の確認だけでなく、発熱後も観察したか
  • フレーム処理時間だけでなく、ドロップ、音声品質、操作応答も確認したか
  • 縮退時に現在のエフェクト状態が画面へ表示されるか
  • 停止後も通話やライブ配信自体は継続できるか
  • AIを無効化しても手動選択が使えるか
  • 同意を撤回したら対象処理と関連アセット利用を止められるか

端末Tierは端末名だけで固定しない方が安全です。同じモデルでもOS、同時実行アプリ、電源状態などで条件が変わり得るため、Tierは開始時の目安、Runtime Governorは継続可否の判断として役割を分けます。

注意点とトレードオフ

1. フレーム処理時間だけではGPU負荷の全体像にならない

Webやモバイルでは、詳細なGPU利用率を常に同じ方法で取得できるとは限りません。取得しやすい処理時間、ドロップ数、描画間隔などを組み合わせ、観測できない状態を「余裕あり」と決めつけない設計が必要です。

2. 縮退は見た目の意味を変える

背景ぼかしを停止すると、ユーザーの周囲が映る可能性があります。これは単なる品質低下ではなくプライバシー上の変化です。背景処理を継続できない場合は、素の映像へ自動復帰せず、映像停止や代替画像を選べる設計も検討してください。

3. Registryには運用コストがある

エフェクト追加のたびに、端末Tier、同意範囲、フォールバック、アセット整合性、削除手順を更新する必要があります。一方で、この手間を省いてAIやリモート設定から任意のIDを実行可能にすると、検証範囲と障害原因が拡大します。

4. AIを経路上の必須依存にしない

AIが停止しても、登録済みエフェクトを手動で選べるようにします。AIは選択支援には使えても、Beauty ARやライブ配信の継続条件にしない方が復旧しやすくなります。

5. 閾値を全端末共通の真実にしない

コード中の70%、3回連続という値は説明用です。実際には端末群、目標FPS、映像解像度、同時利用機能を固定したシナリオテストから決め、Registryやポリシーのバージョンと一緒に記録します。

まとめ

Beauty ARへAIを追加するときに必要なのは、AIへGPU制御まで任せることではありません。

  • AIは登録済みエフェクトの候補だけを返す
  • Registryで機能、同意、端末Tier、縮退先を固定する
  • 開始前判定と実行中監視を分ける
  • 過負荷時は一段だけ縮退し、代替不能ならBeauty ARだけを停止する
  • 高負荷機能への復帰は自動化しすぎず、人の選択を残す

これにより、「AIが提案できる」というデモ上の能力と、「ライブ中に安全に維持できる」という製品上の判断を分離できます。


関係性の開示: 筆者はTencent RTCに関する技術コンテンツの制作に関与しています。本稿の製品仕様・実装方針の確認には、Tencent RTCの公式ドキュメントを参照しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?