DEV Community

LunarDrift
LunarDrift

Posted on

Turn a GAN Beauty Demo Into a Release Decision: Build an Evidence Gate for Tencent RTC Beauty AR

A polished GAN beauty demo can create momentum—and an uncomfortable question for the engineers in the room:

Who decides whether this is actually ready to ship?

The model can generate an impressive effect. It cannot decide whether the visual result is respectful, whether the extra workload is acceptable on your supported devices, or whether an incomplete test run should count as evidence.

That uncertainty is not a sign that your role has become less relevant. It is a sign that the difficult work has moved. Instead of asking only whether the effect works, the team needs an explicit release contract covering performance, visual review, consent, failure behavior, and ownership.

In this tutorial, we will build that contract as a small TypeScript evidence gate for a Tencent RTC Beauty AR scenario. The gate will:

  • Compare a GAN candidate with a baseline on the same device.
  • Reject incomplete or incomparable measurements.
  • Keep performance evidence separate from human visual approval.
  • Produce one of four explicit release states.
  • Require a verified baseline fallback before approving the candidate.
  • Prevent a presentation or screenshot from becoming accidental release approval.

The code does not invent Tencent RTC SDK methods. It creates an application-owned test and release layer that you can connect to your actual Beauty AR integration.

The release question we are answering

Assume a team wants to add an optional GAN-based appearance effect to a live calling or streaming experience.

Tencent RTC Beauty AR supports real-time capabilities including beauty filters, makeup, stickers, virtual backgrounds, avatars, gesture recognition, and image or video enhancement. The product overview is here:

https://trtc.io/document/beauty-ar-overview

The low-end device optimization guide recommends treating effects as workloads. Depending on device capability, that can mean selecting an appropriate performance mode, controlling resolution and frame rate, or disabling expensive segmentation, 3D, and GAN effects:

https://trtc.io/document/66968

Those recommendations do not give every application one universal passing score. A live commerce stream, a casual call, and a recorded creator workflow may make different trade-offs.

Our application therefore defines its own release question:

Does the candidate retain enough of the baseline's measured rendering performance on every device in the declared release scope, pass visual review, preserve an off control, and recover to a verified baseline if activation fails?

The threshold belongs to the product team. It is not a Tencent RTC performance guarantee.

Turn the demo into an evidence artifact

A useful test run needs more than passed: true. It should identify:

  • The application and effect build.
  • The physical device or reproducible device class.
  • Whether the baseline or candidate was active.
  • The test scene and duration.
  • Observed frame-rate samples from application instrumentation.
  • Renderer errors and thermal warnings available to the host application.
  • Whether the user-facing off control was tested.
  • The human review result.
  • Whether the baseline fallback was verified.

Avoid retaining face images merely because the test harness can capture them. Performance samples and a reviewer decision may be sufficient. If visual artifacts must be stored, define participant consent, access, and deletion rules before recording.

Create the test project

mkdir beauty-release-gate
cd beauty-release-gate
npm init -y
npm install -D typescript vitest @types/node
npx tsc --init
mkdir src test
Enter fullscreen mode Exit fullscreen mode

Update package.json:

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create src/model.ts:

export type Profile = "baseline" | "gan-candidate";

export type RunState =
  | "planned"
  | "running"
  | "complete"
  | "cancelled"
  | "invalid";

export type ReviewDecision =
  | "pending"
  | "accepted"
  | "rejected";

export interface TestRun {
  id: string;
  deviceId: string;
  deviceClass: string;
  appBuild: string;
  effectBuild: string;
  sceneId: string;
  profile: Profile;
  state: RunState;

  // Values collected by the host application's instrumentation.
  observedFps: number[];
  rendererErrors: string[];
  thermalWarnings: number;

  offControlVerified: boolean;
  fallbackVerified: boolean;
  review: ReviewDecision;
}

export interface GatePolicy {
  requiredDeviceIds: string[];
  minimumFpsRetention: number;
  requireNoRendererErrors: boolean;
  requireNoThermalWarnings: boolean;
}

export type GateState =
  | "collecting-evidence"
  | "awaiting-human-review"
  | "approved"
  | "rejected";

export interface GateResult {
  state: GateState;
  reasons: string[];
}
Enter fullscreen mode Exit fullscreen mode

There are two deliberate separations here.

First, complete does not mean accepted. A technically complete run can still fail either performance policy or human review.

Second, the baseline and candidate are separate records. That prevents a candidate measured on one device or scene from being compared with an unrelated baseline.

Make invalid evidence impossible to overlook

Create src/gate.ts:

import {
  GatePolicy,
  GateResult,
  TestRun,
} from "./model.js";

function median(values: number[]): number | undefined {
  if (values.length === 0) return undefined;

  const sorted = [...values].sort((a, b) => a - b);
  const middle = Math.floor(sorted.length / 2);

  return sorted.length % 2 === 0
    ? (sorted[middle - 1] + sorted[middle]) / 2
    : sorted[middle];
}

function comparable(a: TestRun, b: TestRun): boolean {
  return (
    a.deviceId === b.deviceId &&
    a.appBuild === b.appBuild &&
    a.effectBuild === b.effectBuild &&
    a.sceneId === b.sceneId
  );
}

export function evaluateGate(
  runs: TestRun[],
  policy: GatePolicy,
): GateResult {
  const reasons: string[] = [];
  let waitingForReview = false;

  for (const deviceId of policy.requiredDeviceIds) {
    const baseline = runs.find(
      run => run.deviceId === deviceId && run.profile === "baseline",
    );
    const candidate = runs.find(
      run => run.deviceId === deviceId && run.profile === "gan-candidate",
    );

    if (!baseline || !candidate) {
      reasons.push(`${deviceId}: baseline or candidate run is missing`);
      continue;
    }

    if (baseline.state !== "complete" || candidate.state !== "complete") {
      reasons.push(`${deviceId}: both runs must be complete`);
      continue;
    }

    if (!comparable(baseline, candidate)) {
      reasons.push(`${deviceId}: runs use different builds or scenes`);
      continue;
    }

    const baselineFps = median(baseline.observedFps);
    const candidateFps = median(candidate.observedFps);

    if (baselineFps === undefined || candidateFps === undefined) {
      reasons.push(`${deviceId}: frame-rate evidence is missing`);
      continue;
    }

    const retention = candidateFps / baselineFps;

    if (retention < policy.minimumFpsRetention) {
      reasons.push(
        `${deviceId}: FPS retention ${retention.toFixed(2)} is below ` +
        `${policy.minimumFpsRetention.toFixed(2)}`,
      );
    }

    if (
      policy.requireNoRendererErrors &&
      candidate.rendererErrors.length > 0
    ) {
      reasons.push(`${deviceId}: candidate reported renderer errors`);
    }

    if (
      policy.requireNoThermalWarnings &&
      candidate.thermalWarnings > 0
    ) {
      reasons.push(`${deviceId}: candidate reported thermal warnings`);
    }

    if (!candidate.offControlVerified) {
      reasons.push(`${deviceId}: user-facing off control was not verified`);
    }

    if (!candidate.fallbackVerified) {
      reasons.push(`${deviceId}: baseline fallback was not verified`);
    }

    if (candidate.review === "rejected") {
      reasons.push(`${deviceId}: human visual review rejected the result`);
    } else if (candidate.review === "pending") {
      waitingForReview = true;
    }
  }

  if (reasons.length > 0) {
    const incomplete = reasons.some(reason =>
      reason.includes("missing") ||
      reason.includes("must be complete") ||
      reason.includes("different builds"),
    );

    return {
      state: incomplete ? "collecting-evidence" : "rejected",
      reasons,
    };
  }

  if (waitingForReview) {
    return {
      state: "awaiting-human-review",
      reasons: ["Performance evidence passed; visual review is pending"],
    };
  }

  return {
    state: "approved",
    reasons: [],
  };
}
Enter fullscreen mode Exit fullscreen mode

This gate treats missing data as missing data—not as a pass and not automatically as a product failure.

That distinction matters:

  • Collecting evidence means the test is incomplete or incomparable.
  • Awaiting human review means the measurable policy passed, but a person still needs to assess the result.
  • Rejected means complete evidence violated a release rule.
  • Approved means every declared requirement passed.

A team can now discuss the policy instead of arguing over whether a demo “felt smooth.”

Reproduce the disputed cases

Create test/gate.test.ts:

import { describe, expect, it } from "vitest";
import { evaluateGate } from "../src/gate.js";
import type { GatePolicy, TestRun } from "../src/model.js";

const policy: GatePolicy = {
  requiredDeviceIds: ["device-a"],

  // Example application policy, not a product guarantee.
  minimumFpsRetention: 0.85,
  requireNoRendererErrors: true,
  requireNoThermalWarnings: true,
};

function run(
  profile: TestRun["profile"],
  overrides: Partial<TestRun> = {},
): TestRun {
  return {
    id: `${profile}-run`,
    deviceId: "device-a",
    deviceClass: "release-matrix-entry-1",
    appBuild: "app-42",
    effectBuild: "effect-7",
    sceneId: "indoor-steady-light",
    profile,
    state: "complete",
    observedFps: profile === "baseline" ? [30, 30, 29] : [27, 27, 26],
    rendererErrors: [],
    thermalWarnings: 0,
    offControlVerified: true,
    fallbackVerified: true,
    review: profile === "baseline" ? "accepted" : "pending",
    ...overrides,
  };
}

describe("Beauty AR release gate", () => {
  it("does not turn performance evidence into visual approval", () => {
    const result = evaluateGate(
      [run("baseline"), run("gan-candidate")],
      policy,
    );

    expect(result.state).toBe("awaiting-human-review");
  });

  it("rejects a candidate that falls below the team's retention rule", () => {
    const candidate = run("gan-candidate", {
      observedFps: [20, 20, 19],
      review: "accepted",
    });

    const result = evaluateGate(
      [run("baseline"), candidate],
      policy,
    );

    expect(result.state).toBe("rejected");
    expect(result.reasons[0]).toContain("FPS retention");
  });

  it("does not compare different effect builds", () => {
    const candidate = run("gan-candidate", {
      effectBuild: "effect-8",
      review: "accepted",
    });

    const result = evaluateGate(
      [run("baseline"), candidate],
      policy,
    );

    expect(result.state).toBe("collecting-evidence");
  });

  it("requires a tested fallback before approval", () => {
    const candidate = run("gan-candidate", {
      review: "accepted",
      fallbackVerified: false,
    });

    const result = evaluateGate(
      [run("baseline"), candidate],
      policy,
    );

    expect(result.state).toBe("rejected");
    expect(result.reasons[0]).toContain("fallback");
  });

  it("approves only complete technical and human evidence", () => {
    const candidate = run("gan-candidate", {
      review: "accepted",
    });

    const result = evaluateGate(
      [run("baseline"), candidate],
      policy,
    );

    expect(result).toEqual({ state: "approved", reasons: [] });
  });
});
Enter fullscreen mode Exit fullscreen mode

Run it:

npm test
Enter fullscreen mode Exit fullscreen mode

The important test is not merely the happy path. It is the case where the measurements look acceptable but review is still pending. That is where an enthusiastic presentation can otherwise become an implicit approval.

Collect paired runs without biasing the result

A comparison is only useful when the runs are meaningfully comparable. Use this workflow for each physical device in your release matrix:

  1. Confirm participant consent before enabling any appearance effect or recording visual evidence.
  2. Record the application build, effect build, device identifier, and test scene.
  3. Run the baseline for the configured duration.
  4. Allow the device to return to the team's defined starting condition.
  5. Run the GAN candidate in the same scene.
  6. Repeat in the opposite order to expose warm-up or thermal-order bias.
  7. Exercise the visible off control during the candidate run.
  8. Force candidate activation to fail and verify restoration of the baseline.
  9. Submit the visual result for human review.
  10. Evaluate the complete device matrix rather than selecting the best run.

Lighting, camera movement, background complexity, and multiple effects can change the workload. Give each important combination a distinct sceneId instead of silently mixing its samples with an easier scene.

Do not use one median to hide an obviously unstable run. The example gate is intentionally small. A production policy can add duration requirements, variability checks, or sustained-window analysis, provided those rules are documented and tested.

Connect the gate to Beauty AR without guessing SDK APIs

The gate should not directly call a product-specific method. Put a narrow adapter around the Beauty AR integration used by your application:

export interface BeautyRendererPort {
  applyBaseline(): Promise<void>;
  applyGanCandidate(): Promise<void>;
}

export type RuntimeEffectState =
  | "off"
  | "applying-candidate"
  | "candidate-active"
  | "restoring-baseline"
  | "baseline-active"
  | "unavailable";

export interface ActivationResult {
  state: RuntimeEffectState;
  reason?: string;
}

export async function activateApprovedCandidate(
  renderer: BeautyRendererPort,
  releaseState: "approved" | "not-approved",
  consent: boolean,
): Promise<ActivationResult> {
  if (!consent) {
    return { state: "off", reason: "User consent is not active" };
  }

  if (releaseState !== "approved") {
    await renderer.applyBaseline();
    return {
      state: "baseline-active",
      reason: "GAN candidate is not approved for this release scope",
    };
  }

  try {
    await renderer.applyGanCandidate();
    return { state: "candidate-active" };
  } catch (candidateError) {
    try {
      await renderer.applyBaseline();
      return {
        state: "baseline-active",
        reason: "Candidate activation failed; baseline restored",
      };
    } catch (baselineError) {
      return {
        state: "unavailable",
        reason: "Candidate and baseline activation both failed",
      };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

BeautyRendererPort is application code, not a claimed Tencent RTC API. Its implementation should map to the documented Beauty AR integration in your target platform.

The runtime rule is intentionally conservative: failure does not trigger an endless candidate retry loop. The session returns to the verified baseline. If even that fails, the UI should state that the appearance effect is unavailable while leaving the underlying call or stream usable where possible.

Failure modes to decide before the review meeting

Failure Gate or runtime state Required response
Candidate run exists but baseline is missing collecting-evidence Run the matching baseline; do not estimate it from another device.
Candidate and baseline use different builds collecting-evidence Repeat the pair using identical builds and scenes.
Instrumentation returns no samples collecting-evidence Repair or replace the probe; absence of data is not success.
Candidate exceeds the team's workload budget rejected Optimize, narrow the supported scope, or omit the GAN effect.
Visual result is technically fast but unacceptable rejected Revise or remove the effect; performance cannot overrule review.
Reviewers disagree awaiting-human-review Use the named product or safety owner rather than averaging opinions anonymously.
User withdraws consent off Stop applying the effect and follow the application's retention policy for test artifacts.
Candidate activation fails at runtime baseline-active Restore the verified baseline and record the failure reason.
Candidate and baseline both fail unavailable Disable the appearance effect and keep the state visible.

For device classes that cannot sustain the intended workload, the official optimization guidance supports reducing the effect workload—for example, by controlling resolution and frame rate or excluding expensive segmentation, 3D, and GAN effects. That reduced configuration needs its own evidence. Do not call it a fallback merely because it exists in configuration.

What the AI contributes—and what remains a human decision

A GAN effect can contribute sophisticated real-time image transformation. That demonstrated capability is useful, but it does not settle the release decision.

Humans still need to decide:

  • Whether the effect should be offered at all in the product context.
  • Whether it is opt-in, remembered, or reset between sessions.
  • Which visual changes are acceptable.
  • Which physical devices belong in the supported scope.
  • How much quality or frame-rate retention the experience requires.
  • What happens when the effect fails halfway through a live session.
  • Whether captured review material should be stored.

This is a more useful framing than asking whether AI removes the need for engineers or testers. The valuable skill is no longer just spotting one broken frame. It is converting ambiguous quality expectations into observable states, comparable evidence, and a release rule that the organization can inspect.

Release verification checklist

Before marking a GAN candidate as approved, verify that:

  • [ ] Every device in the declared release scope has paired baseline and candidate runs.
  • [ ] Each pair uses the same application build, effect build, and scene.
  • [ ] Measurements come from real application instrumentation, not visual guesswork.
  • [ ] The test order is alternated or otherwise controlled for warm-up and heat.
  • [ ] Missing samples produce an incomplete state rather than a pass.
  • [ ] Renderer errors and available thermal signals are retained with the run.
  • [ ] The visible off control is exercised during a live effect session.
  • [ ] Consent withdrawal removes the effect without requiring a restart.
  • [ ] Human visual approval is recorded separately from performance evidence.
  • [ ] Candidate activation failure restores the verified baseline.
  • [ ] Baseline failure produces a visible unavailable state.
  • [ ] Any lower-workload configuration has its own test evidence.
  • [ ] Stored visual artifacts have an explicit access and deletion policy.
  • [ ] The release decision identifies its owner and the exact approved build.

The organizational change worth keeping

An AI presentation can be valuable without becoming an automatic mandate to ship. The healthiest change is not “the model impressed the room, so reorganize around it.” It is replacing informal confidence with an explicit decision process.

For Beauty AR, that means treating GAN effects as both computer-vision features and real-time workloads. The model produces the effect. Instrumentation reports what happened. A gate checks the declared policy. A person remains accountable for whether the result belongs in the product.

That is not resistance to AI. It is how a capable prototype becomes an operable live feature.


Relationship disclosure: I have a content relationship with Tencent RTC. Official Tencent RTC documentation was used as the implementation reference for this article.

Top comments (1)

Collapse
 
aifliproom profile image
AI Flip Room •

We run something close to this for AI room photos: every render goes through a checker before a customer sees it (windows, doors and walls must still match the original photo). The rule that saved us most is the one you describe: an incomplete check counts as a fail, not a pass. Otherwise the gate slowly turns into a formality.