# Your GAN Beauty Effect Needs a Device Budget, Not a Universal “On” Switch

> Source: <https://dev.to/susiewang/your-gan-beauty-effect-needs-a-device-budget-not-a-universal-on-switch-2njp>
> Published: 2026-08-20 04:12:41+00:00

A GAN-powered beauty effect can look convincing in a product demo and still be the wrong default for a real session.

The uncomfortable part is not whether the effect is “AI.” It is deciding what the application should do when appearance processing, segmentation, rendering, and video compete for a limited device budget. If the answer is simply “enable everything and hope,” lower-capability devices pay the price.

This is also where writing less integration code can create more engineering responsibility. The durable skill is not producing another effect toggle. It is defining consent, capability, fallback, and verification rules that remain understandable when the renderer fails.

In this tutorial, we will build an application-owned Beauty AR controller that:

Tencent RTC Beauty AR supports scenarios including real-time beauty filters, makeup, stickers, virtual backgrounds, avatars, gesture recognition, and image or video enhancement. The official overview is the appropriate starting point for checking the features available to your integration:

[https://trtc.io/document/beauty-ar-overview](https://trtc.io/document/beauty-ar-overview)

For device-tier and degradation guidance, Tencent RTC’s low-end optimization guide recommends adapting the configuration to device capability, using performance-oriented modes, controlling resolution and frame rate, and disabling expensive segmentation or 3D/GAN effects where necessary:

[https://trtc.io/document/66968](https://trtc.io/document/66968)

The code below is deliberately an application policy, not a replacement for the official platform-specific integration instructions.

A useful Beauty AR contract separates three decisions that are often collapsed into one checkbox:

A user selecting “Full” should not force a constrained device to run every effect. It means the application may use the richest profile that its capability policy currently permits.

We will use these states:

``` php
awaiting-consent
       |
       +-- denied --------------------------> off
       |
       +-- granted --> probing --> applying --> running
                                      |            |
                                      |            +-- sustained pressure
                                      |                       |
                                      +-- failure             v
                                             applying safer profile
                                                        |
                                                        +--> degraded
                                                        |
                                                        +--> failed --> off
```

The distinction between `degraded`

and `failed`

matters. Degraded means the session is still providing an intentionally reduced visual experience. Failed means no approved profile could be applied safely, so the effects are off.

Use Node.js with TypeScript for the policy and tests:

```
mkdir beauty-budget
cd beauty-budget
npm init -y
npm install --save-dev typescript tsx @types/node
mkdir src test
```

Add these scripts to `package.json`

:

```
{
  "scripts": {
    "test": "tsx --test test/**/*.test.ts"
  }
}
```

The policy can be tested without a camera, a live room, or a specific Beauty AR SDK method. That is intentional: renderer callbacks should provide evidence to the policy, not contain the policy themselves.

Create `src/policy.ts`

:

```
export type Tier = "constrained" | "balanced" | "capable";
export type Preference = "off" | "auto" | "basic" | "full";
export type Phase =
  | "awaiting-consent"
  | "probing"
  | "applying"
  | "running"
  | "degraded"
  | "off"
  | "failed";

export type Feature =
  | "beauty"
  | "makeup"
  | "stickers"
  | "segmentation"
  | "gan"
  | "avatar";

export interface RenderProfile {
  id: string;
  mode: "performance" | "quality";
  inputHeight: number;
  targetFps: number;
  features: Feature[];
}

const tierProfiles: Record<Tier, RenderProfile> = {
  constrained: {
    id: "constrained-v1",
    mode: "performance",
    inputHeight: 480,
    targetFps: 15,
    features: ["beauty"]
  },
  balanced: {
    id: "balanced-v1",
    mode: "performance",
    inputHeight: 720,
    targetFps: 24,
    features: ["beauty", "makeup", "stickers"]
  },
  capable: {
    id: "capable-v1",
    mode: "quality",
    inputHeight: 720,
    targetFps: 30,
    features: [
      "beauty",
      "makeup",
      "stickers",
      "segmentation",
      "gan",
      "avatar"
    ]
  }
};

function profileFor(tier: Tier, preference: Preference): RenderProfile {
  const base = tierProfiles[tier];

  if (preference === "basic") {
    return {
      ...base,
      id: `${base.id}-basic`,
      features: base.features.filter(feature => feature === "beauty")
    };
  }

  return { ...base, features: [...base.features] };
}

export interface State {
  phase: Phase;
  consent: "unknown" | "granted" | "denied";
  preference: Preference;
  tier?: Tier;
  profile?: RenderProfile;
  revision: number;
  badWindows: number;
  hasDegraded: boolean;
  error?: string;
}

export type Event =
  | { type: "CONSENT_GRANTED" }
  | { type: "CONSENT_DENIED" }
  | { type: "PROBE_COMPLETED"; tier: Tier }
  | { type: "PREFERENCE_CHANGED"; preference: Preference }
  | { type: "FRAME_WINDOW"; observedFps: number }
  | { type: "PROFILE_APPLIED"; revision: number }
  | { type: "PROFILE_FAILED"; revision: number; error: string };

export type Command =
  | { type: "APPLY_PROFILE"; revision: number; profile: RenderProfile }
  | { type: "DISABLE_EFFECTS" };

export function initialState(): State {
  return {
    phase: "awaiting-consent",
    consent: "unknown",
    preference: "auto",
    revision: 0,
    badWindows: 0,
    hasDegraded: false
  };
}

function lowerTier(tier: Tier): Tier | undefined {
  if (tier === "capable") return "balanced";
  if (tier === "balanced") return "constrained";
  return undefined;
}

function startApply(
  state: State,
  tier: Tier,
  degraded: boolean
): [State, Command[]] {
  const revision = state.revision + 1;
  const profile = profileFor(tier, state.preference);

  return [
    {
      ...state,
      phase: "applying",
      tier,
      profile,
      revision,
      badWindows: 0,
      hasDegraded: state.hasDegraded || degraded,
      error: undefined
    },
    [{ type: "APPLY_PROFILE", revision, profile }]
  ];
}

export function reduce(state: State, event: Event): [State, Command[]] {
  switch (event.type) {
    case "CONSENT_GRANTED":
      return [
        { ...state, consent: "granted", phase: "probing", error: undefined },
        []
      ];

    case "CONSENT_DENIED":
      return [
        {
          ...state,
          consent: "denied",
          preference: "off",
          phase: "off",
          profile: undefined,
          revision: state.revision + 1,
          badWindows: 0
        },
        [{ type: "DISABLE_EFFECTS" }]
      ];

    case "PROBE_COMPLETED":
      if (state.consent !== "granted" || state.preference === "off") {
        return [state, []];
      }
      return startApply(state, event.tier, false);

    case "PREFERENCE_CHANGED": {
      if (event.preference === "off") {
        return [
          {
            ...state,
            preference: "off",
            phase: "off",
            profile: undefined,
            revision: state.revision + 1,
            badWindows: 0
          },
          [{ type: "DISABLE_EFFECTS" }]
        ];
      }

      const next = { ...state, preference: event.preference };

      if (next.consent !== "granted") return [next, []];
      if (!next.tier) return [{ ...next, phase: "probing" }, []];

      return startApply(next, next.tier, false);
    }

    case "FRAME_WINDOW": {
      if (
        (state.phase !== "running" && state.phase !== "degraded") ||
        !state.profile ||
        !state.tier
      ) {
        return [state, []];
      }

      // An application-owned starting threshold, not a product benchmark.
      const belowBudget = event.observedFps < state.profile.targetFps * 0.8;
      const badWindows = belowBudget ? state.badWindows + 1 : 0;

      if (badWindows < 3) {
        return [{ ...state, badWindows }, []];
      }

      const saferTier = lowerTier(state.tier);

      if (!saferTier) {
        return [
          {
            ...state,
            phase: "failed",
            profile: undefined,
            badWindows: 0,
            error: "Frame budget was not sustained on the safest profile"
          },
          [{ type: "DISABLE_EFFECTS" }]
        ];
      }

      return startApply({ ...state, badWindows: 0 }, saferTier, true);
    }

    case "PROFILE_APPLIED":
      // The user may have turned effects off while an apply was in flight.
      if (event.revision !== state.revision || state.phase !== "applying") {
        return [state, []];
      }

      return [
        {
          ...state,
          phase: state.hasDegraded ? "degraded" : "running",
          badWindows: 0
        },
        []
      ];

    case "PROFILE_FAILED": {
      if (event.revision !== state.revision || state.phase !== "applying") {
        return [state, []];
      }

      const saferTier = state.tier ? lowerTier(state.tier) : undefined;

      if (saferTier) {
        return startApply(
          { ...state, error: event.error },
          saferTier,
          true
        );
      }

      return [
        {
          ...state,
          phase: "failed",
          profile: undefined,
          error: event.error
        },
        [{ type: "DISABLE_EFFECTS" }]
      ];
    }
  }
}
```

The numeric profile values and the 80% threshold are example application defaults, not Tencent RTC performance guarantees. Calibrate them using measurements from the devices your application actually supports.

The more important property is the ordering:

``` php
capable -> balanced -> constrained -> effects off
```

GAN and segmentation disappear before basic beauty processing does. The user still gets a valid session rather than an all-or-nothing renderer.

Keep platform-specific SDK calls behind a narrow adapter:

``` python
import type { Command, Event, RenderProfile } from "./policy.js";

export interface BeautyRenderer {
  apply(profile: RenderProfile): Promise<void>;
  disable(): Promise<void>;
}

export async function execute(
  command: Command,
  renderer: BeautyRenderer,
  dispatch: (event: Event) => void
): Promise<void> {
  if (command.type === "DISABLE_EFFECTS") {
    await renderer.disable();
    return;
  }

  try {
    await renderer.apply(command.profile);
    dispatch({
      type: "PROFILE_APPLIED",
      revision: command.revision
    });
  } catch (error) {
    dispatch({
      type: "PROFILE_FAILED",
      revision: command.revision,
      error: error instanceof Error ? error.message : "Unknown renderer error"
    });
  }
}
```

`BeautyRenderer.apply`

is an application-defined port, not a Tencent RTC API name. Its concrete implementation should map the selected profile to the documented Beauty AR configuration for your target platform.

That adapter is also the right place to normalize errors. A production implementation should distinguish at least:

A permanent configuration error should normally stop immediately rather than trying every performance tier. The generic sample falls back because it cannot know the platform-specific error taxonomy.

Do not infer “capable” solely from a device model or user-agent string. Two nominally identical devices can differ because of thermal state, background load, browser behavior, camera configuration, or power settings.

A practical probe can combine:

Run the probe only after consent, and avoid publishing or retaining camera frames merely to classify the device. The policy needs a tier result, not the images used to produce it.

Use a conservative tier when measurement is missing. “Unknown” should not silently mean “capable.”

There is another measurement trap: poor remote video can come from the network, encoder, decoder, or receiver. Do not downgrade local Beauty AR solely because a remote participant reports low frame rate. Feed this policy measurements attributable to the local rendering path.

Create `test/policy.test.ts`

:

``` python
import test from "node:test";
import assert from "node:assert/strict";
import { initialState, reduce, type State } from "../src/policy.js";

function grantAndProbe(tier: "constrained" | "balanced" | "capable") {
  let state = initialState();
  [state] = reduce(state, { type: "CONSENT_GRANTED" });
  const [applying, commands] = reduce(state, {
    type: "PROBE_COMPLETED",
    tier
  });

  return { state: applying, commands };
}

test("does not apply a profile before consent", () => {
  const [state, commands] = reduce(initialState(), {
    type: "PROBE_COMPLETED",
    tier: "capable"
  });

  assert.equal(state.phase, "awaiting-consent");
  assert.equal(commands.length, 0);
});

test("a capable device may receive the GAN profile", () => {
  const { state, commands } = grantAndProbe("capable");

  assert.equal(state.phase, "applying");
  assert.equal(state.profile?.features.includes("gan"), true);
  assert.equal(commands[0]?.type, "APPLY_PROFILE");
});

test("three bad windows cause one downgrade", () => {
  let { state } = grantAndProbe("capable");
  [state] = reduce(state, {
    type: "PROFILE_APPLIED",
    revision: state.revision
  });

  let commands = [] as ReturnType<typeof reduce>[1];

  for (let i = 0; i < 3; i++) {
    [state, commands] = reduce(state, {
      type: "FRAME_WINDOW",
      observedFps: 10
    });
  }

  assert.equal(state.tier, "balanced");
  assert.equal(state.phase, "applying");
  assert.equal(state.hasDegraded, true);
  assert.equal(commands[0]?.type, "APPLY_PROFILE");
  assert.equal(state.profile?.features.includes("gan"), false);
});

test("an apply failure tries a safer tier", () => {
  let { state } = grantAndProbe("balanced");
  const failedRevision = state.revision;

  const [next, commands] = reduce(state, {
    type: "PROFILE_FAILED",
    revision: failedRevision,
    error: "renderer initialization failed"
  });

  assert.equal(next.tier, "constrained");
  assert.equal(next.phase, "applying");
  assert.equal(commands[0]?.type, "APPLY_PROFILE");
});

test("a late success cannot resurrect effects after user turns them off", () => {
  let state: State = grantAndProbe("capable").state;
  const oldRevision = state.revision;

  [state] = reduce(state, {
    type: "PREFERENCE_CHANGED",
    preference: "off"
  });

  [state] = reduce(state, {
    type: "PROFILE_APPLIED",
    revision: oldRevision
  });

  assert.equal(state.phase, "off");
  assert.equal(state.profile, undefined);
});
```

Run the suite:

```
npm test
```

These tests verify the policy’s invariants rather than whether one machine can render a particular effect.

An obvious extension is to upgrade again as soon as frame rate recovers. That usually creates a new problem: oscillation.

A device may briefly recover after an expensive effect is disabled. If the application immediately restores the effect, frame pressure returns, causing another downgrade. The user sees repeated visual changes while the renderer does unnecessary work.

A safer default is:

If you do implement automatic recovery, require substantially more good evidence than bad evidence. For example, degradation might require three poor windows while recovery requires a much longer stable interval. Those values still need calibration rather than guesswork.

The revision check prevents a late `PROFILE_APPLIED`

event from moving the application back to `running`

. The renderer adapter must also release loaded resources and stop processing.

Choose the conservative profile. Do not translate missing data into permission to enable segmentation, avatars, or GAN effects.

The sample tries the constrained profile. In production, classify errors first. A missing credential or invalid configuration will not be repaired by reducing resolution.

Disable Beauty AR while preserving the underlying call or live session. Beauty effects are optional; communication should not depend on them.

Graphics resources may no longer be in the state your controller expects. Treat restoration as revalidation: check consent, verify that the revision is still current, and reapply no more than the last approved tier.

Keep the constrained profile and explain why richer effects are unavailable. Do not quietly pretend the GAN effect is active, but do not override the safety policy either.

GAN-based visual processing can produce effects that would be difficult to implement with simple color adjustments. That demonstrated capability does not answer the product questions around it:

Those are application and human decisions, not outputs to delegate to a model.

For many developers, the underlying tension is that integrating an advanced effect can now take less code, while being confident in the result requires more judgment. That is not a loss of engineering value. The valuable work has moved toward defining constraints, making degradation visible, and proving that user control survives asynchronous failure.

Before shipping, verify all of the following:

The goal is not to make every device render the most impressive effect. It is to make every outcome intentional: rich where supported, restrained where necessary, off when requested, and recoverable when rendering fails.

**Disclosure:** I wrote this article in connection with Tencent RTC, and I used the official Tencent RTC Beauty AR documentation as the implementation reference.
