facebetter 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,235 +1,309 @@
1
- /**
2
- * Facebetter Error Classes
3
- * Platform-agnostic error handling
4
- */
5
-
6
- /**
7
- * Facebetter error class
8
- */
9
- class FacebetterError extends Error {
10
- constructor(message, code = -1) {
11
- super(message);
12
- this.name = 'FacebetterError';
13
- this.code = code;
14
- }
15
- }
16
-
17
- /**
18
- * Checks if a result code indicates success
19
- * @param {number} result - Result code from WASM function
20
- * @throws {FacebetterError} If result indicates failure
21
- */
22
- function checkResult(result, errorMessage = 'Operation failed') {
23
- if (result !== 0) {
24
- throw new FacebetterError(`${errorMessage} (error code: ${result})`, result);
25
- }
26
- }
27
-
28
1
  /**
29
2
  * Engine Configuration
30
3
  * Platform-agnostic configuration class
31
4
  */
32
5
 
33
-
34
6
  /**
35
7
  * Engine configuration class
36
- * Similar to EngineConfig struct in C++/Java/OC
8
+ * Analogous to EngineConfig in C++ / Java / Objective-C, with Web-only auth fields.
37
9
  */
38
10
  class EngineConfig {
39
11
  /**
40
- * Creates a new EngineConfig instance
41
- * @param {Object} config - Configuration object
42
- * @param {string} [config.appId] - Application ID (required if licenseJson is not provided)
43
- * @param {string} [config.appKey] - Application key (required if licenseJson is not provided)
44
- * @param {string} [config.licenseJson] - License JSON string (optional, if provided, appId and appKey are not required)
12
+ * @param {Object} config
13
+ * @param {string} [config.licenseToken] Compact JWS, or the raw `{success, token}`
14
+ * HTTP response body from your auth server.
15
+ * @param {string} [config.authProxyUrl] Production: forward the WASM auth
16
+ * challenge to your server, which holds app_id/app_key and calls Cloudflare.
17
+ * @param {Function} [config.fetchAuthResponse] Custom auth hook with signature
18
+ * `async (challenge) => string|object`. For local debugging you can use
19
+ * `createDirectAuthFetcher` to hit Cloudflare directly — never ship keys in
20
+ * a production frontend bundle.
21
+ * @param {boolean} [config.externalContext] Kept for cross-platform alignment;
22
+ * unused on Web/WASM today.
45
23
  */
46
24
  constructor(config = {}) {
47
- this.appId = config.appId || null;
48
- this.appKey = config.appKey || null;
49
- this.licenseJson = config.licenseJson || null;
25
+ this.licenseToken = config.licenseToken || null;
26
+ this.authProxyUrl = config.authProxyUrl || null;
27
+ this.fetchAuthResponse = config.fetchAuthResponse || null;
50
28
  this.resourcePath = '/resource.fbd';
51
29
  /**
52
30
  * Whether to use an external GL context (native platforms only).
53
- * In Web/WASM environments, this field is kept for configuration structure alignment,
54
- * but it does not take effect in the current implementation.
31
+ * On Web/WASM this field is kept for config-shape alignment and has no effect.
55
32
  */
56
33
  this.externalContext = !!config.externalContext;
57
34
  }
58
35
 
59
- /**
60
- * Validates the configuration
61
- * @returns {boolean} True if valid
62
- */
63
36
  isValid() {
64
- // If licenseJson is provided, use it for validation
65
- if (this.licenseJson) {
66
- return typeof this.licenseJson === 'string' && this.licenseJson.trim() !== '';
37
+ if (this.licenseToken && typeof this.licenseToken === 'string' &&
38
+ this.licenseToken.trim() !== '') {
39
+ return true;
67
40
  }
68
- // Otherwise appId and appKey are required
69
- return this.appId && typeof this.appId === 'string' && this.appId.trim() !== '' &&
70
- this.appKey && typeof this.appKey === 'string' && this.appKey.trim() !== '';
41
+ if (typeof this.fetchAuthResponse === 'function') {
42
+ return true;
43
+ }
44
+ if (this.authProxyUrl && typeof this.authProxyUrl === 'string' &&
45
+ this.authProxyUrl.trim() !== '') {
46
+ return true;
47
+ }
48
+ return false;
71
49
  }
72
50
 
73
- /**
74
- * Returns a string representation of the config
75
- * @returns {string} String representation
76
- */
77
51
  toString() {
78
- return `EngineConfig{appId='${this.appId ? this.appId.substring(0, Math.min(this.appId.length, 8)) + '...' : 'null'}', appKey='${this.appKey ? '***' : 'null'}', licenseJson=${this.licenseJson ? 'provided' : 'null'}}`;
52
+ const mode = this.licenseToken
53
+ ? 'licenseToken'
54
+ : this.fetchAuthResponse
55
+ ? 'fetchAuthResponse'
56
+ : this.authProxyUrl
57
+ ? 'authProxyUrl'
58
+ : 'invalid';
59
+ return `EngineConfig{mode='${mode}'}`;
79
60
  }
80
61
  }
81
62
 
82
63
  /**
83
- * Facebetter Constants and Enums
84
- * Platform-agnostic constants
64
+ * Facebetter Error Classes
65
+ * Platform-agnostic error handling
85
66
  */
86
67
 
87
68
  /**
88
- * Beauty type enumeration
69
+ * Facebetter error class
89
70
  */
90
- const BeautyType$1 = {
91
- Basic: 0, // Basic beauty (smoothing, whitening, etc.)
92
- Reshape: 1, // Face reshaping (face thinning, big eyes, etc.)
93
- Makeup: 2, // Makeup effects (lipstick, blush, etc.)
94
- VirtualBackground: 3, // Virtual background (blur, image replacement)
95
- ChromaKey: 4, // Chroma key (green screen removal)
96
- Filter: 5, // LUT based filter
97
- Sticker: 6 // 2D/3D sticker
98
- };
71
+ class FacebetterError extends Error {
72
+ constructor(message, code = -1) {
73
+ super(message);
74
+ this.name = 'FacebetterError';
75
+ this.code = code;
76
+ }
77
+ }
99
78
 
100
79
  /**
101
- * Basic beauty parameter enumeration
102
- * All values should be in range [0.0, 1.0]
80
+ * Checks if a result code indicates success
81
+ * @param {number} result - Result code from WASM function
82
+ * @throws {FacebetterError} If result indicates failure
103
83
  */
104
- const BasicParam$1 = {
105
- Smoothing: 0, // Skin smoothing (0.0: none, 1.0: maximum)
106
- Sharpening: 1, // Image sharpening (0.0: none, 1.0: maximum)
107
- Whitening: 2, // Skin whitening (0.0: none, 1.0: maximum)
108
- Rosiness: 3 // Skin rosiness (0.0: none, 1.0: maximum)
109
- };
84
+ function checkResult(result, errorMessage = 'Operation failed') {
85
+ if (result !== 0) {
86
+ throw new FacebetterError(`${errorMessage} (error code: ${result})`, result);
87
+ }
88
+ }
110
89
 
111
90
  /**
112
- * Face reshape parameter enumeration
113
- * All values should be in range [0.0, 1.0]
91
+ * Facebetter Constants and Enums
92
+ * Platform-agnostic constants. Ordinals match facebetter::beauty_params.
114
93
  */
115
- const ReshapeParam$1 = {
116
- FaceThinning: 0, // Face thinning (0.0: none, 1.0: maximum)
117
- FaceVShape: 1, // V-shape face (0.0: none, 1.0: maximum)
118
- FaceNarrowing: 2, // Face narrowing (0.0: none, 1.0: maximum)
119
- FaceShortening: 3,// Face shortening (0.0: none, 1.0: maximum)
120
- Cheekbone: 4, // Cheekbone slimming (0.0: none, 1.0: maximum)
121
- Jawbone: 5, // Jawbone slimming (0.0: none, 1.0: maximum)
122
- Chin: 6, // Chin length adjustment (0.0: none, 1.0: maximum)
123
- NoseSlimming: 7, // Nose slimming (0.0: none, 1.0: maximum)
124
- EyeSize: 8, // Eye size (0.0: normal, 1.0: maximum)
125
- EyeDistance: 9 // Eye distance (0.0: normal, 1.0: maximum)
94
+
95
+ const WhiteningStyle$1 = {
96
+ ColdWhite: 0, // Cool white
97
+ PinkWhite: 1, // Pink white
98
+ WarmWhite: 2, // Warm white
99
+ Wheat: 3, // Light wheat / olive
100
+ Tan: 4 // Tan / bronzed
126
101
  };
127
102
 
128
- /**
129
- * Makeup parameter enumeration
130
- * All values should be in range [0.0, 1.0]
131
- */
132
- const MakeupParam$1 = {
133
- Lipstick: 0, // Lipstick intensity (0.0: none, 1.0: maximum)
134
- Blush: 1 // Blush intensity (0.0: none, 1.0: maximum)
103
+ const SmoothingStyle$1 = {
104
+ Natural: 0, // Natural: keeps pores
105
+ Texture: 1, // Cleaner skin while retaining texture
106
+ Smooth: 2 // Creamy / porcelain finish
135
107
  };
136
108
 
137
109
  /**
138
- * Lipstick style types
110
+ * Face reshape parameters.
111
+ * Values match facebetter::beauty_params::Reshape.
112
+ * Intensity range is [-1.0, 1.0]; 0 is off. Comments list + / - directions.
139
113
  */
140
- const LipstickStyle$1 = {
141
- Rouge: 0, // Rose red
142
- Coral: 1, // Coral
143
- Pink: 2 // Pink
114
+ const Reshape$1 = {
115
+ FaceThin: 0, // +slim face / -fuller cheeks
116
+ FaceVShape: 1, // +V-shape jaw / -square jaw
117
+ FaceNarrow: 2, // +narrow face / -wider face
118
+ FaceShort: 3, // +shorter face / -longer face
119
+ Cheekbone: 4, // +slim cheekbones / -wider cheekbones
120
+ Jawbone: 5, // +slim jaw / -wider jaw
121
+ Chin: 6, // +longer chin / -shorter chin
122
+ NoseSlim: 7, // +slimmer nose / -wider nose
123
+ EyeSize: 8, // +larger eyes / -smaller eyes
124
+ EyeDistance: 9, // +wider eye spacing / -closer eyes
125
+ FaceSmall: 10, // +smaller face / -larger face
126
+ Forehead: 11, // +fuller forehead / -lower forehead
127
+ NoseLong: 12, // +longer nose / -shorter nose
128
+ Philtrum: 13, // +shorter philtrum / -longer philtrum
129
+ MouthSize: 14, // +larger mouth / -smaller mouth
130
+ MouthPosition: 15, // +mouth lower / -mouth higher
131
+ MouthSmile: 16, // +smile lift / -droop corners
132
+ LipThickness: 17, // +thicker lips / -thinner lips
133
+ EyeRound: 18, // +rounder eyes / -narrower eyes
134
+ EyePosition: 19, // +eyes lower / -eyes higher
135
+ EyeAngle: 20, // +outer corner up / -outer corner down
136
+ EyeCornerOpen: 21, // +open eye corners / -close eye corners
137
+ LowerEyelid: 22, // +lower eyelid down / -lift lower eyelid
138
+ BrowPosition: 23, // +brows higher / -brows lower
139
+ BrowDistance: 24, // +wider brow spacing / -closer brows
140
+ BrowThickness: 25 // +thicker brows / -thinner brows
141
+ };
142
+
143
+ const EngineEventCode$1 = {
144
+ LicenseValidationSuccess: 0,
145
+ LicenseValidationFailed: 1,
146
+ InitializationComplete: 100,
147
+ InitializationFailed: 101
148
+ };
149
+
150
+ const LipstickColor$1 = {
151
+ Rouge: 0, // Classic rose
152
+ RetroRed: 1, // Retro red
153
+ Peach: 2, // Peach
154
+ CoralOrange: 3, // Coral orange
155
+ GentlePink: 4, // Soft pink
156
+ VitalityOrange: 5 // Bright orange
144
157
  };
145
158
 
146
- /**
147
- * Blush style types
148
- */
149
159
  const BlushStyle$1 = {
150
- Classic: 0, // Classic
151
- Peach: 1, // Peach
152
- Rose: 2 // Rose
160
+ SunKissed: 0, // Sun-kissed sweep across cheekbones and bridge
161
+ Igari: 1, // Flushed band across the nose bridge
162
+ Soft: 2, // Soft dual cheeks
163
+ Apple: 3, // Round apple cheeks
164
+ Classic: 4, // Classic two spots
165
+ Doll: 5, // Cheeks + nose tip
166
+ Rose: 6 // Rose dual cheeks
153
167
  };
154
168
 
155
- /**
156
- * Chroma Key parameter enumeration
157
- */
158
- const ChromaKeyParam$1 = {
159
- KeyColor: 0, // Key color (0.0: Green, 1.0: Blue, 2.0: Red)
160
- Similarity: 1, // Color similarity (0.0 - 1.0)
161
- Smoothness: 2, // Edge smoothness (0.0 - 1.0)
162
- Desaturation: 3 // Spill desaturation (0.0 - 1.0)
169
+ const BlushColor$1 = {
170
+ CoralPink: 0, // Coral pink
171
+ DustyRose: 1, // Dusty rose
172
+ VividRed: 2, // Vivid red
173
+ Berry: 3, // Berry
174
+ SunsetOrange: 4 // Sunset orange
163
175
  };
164
176
 
165
- /**
166
- * Background mode enumeration
167
- */
168
- const BackgroundMode$1 = {
169
- None: 0, // No background processing
170
- Blur: 1, // Blurred background
171
- Image: 2 // Background image replacement
177
+ const ContourStyle$1 = {
178
+ Natural: 0, // Soft everyday contour
179
+ Sculpt: 1, // Deeper sculpted contour
180
+ Glow: 2, // Highlight-focused
181
+ Slim: 3, // Slim cheekbones
182
+ Nose: 4, // Nose bridge lift
183
+ Glam: 5 // Spot highlights
172
184
  };
173
185
 
174
- /**
175
- * Frame type enumeration
176
- */
177
- const FrameType$1 = {
178
- Image: 0, // Image mode
179
- Video: 1 // Video mode
186
+ const EyeShadowStyle$1 = {
187
+ Soft: 0, // Soft wash
188
+ Crease: 1, // Crease deepen
189
+ Smoky: 2, // Smoky surround
190
+ Halo: 3, // Halo around the eye
191
+ Glow: 4, // Outer-corner glow
192
+ Drama: 5, // Full dramatic cover
193
+ Warm: 6 // Warm-tone wash
180
194
  };
181
195
 
182
- /**
183
- * Mirror mode enumeration (apply to input before processing)
184
- */
185
- const MirrorMode$1 = {
186
- None: 0, // No mirror
187
- Horizontal: 1, // Mirror horizontally (e.g. front camera selfie)
188
- Vertical: 2, // Mirror vertically
189
- Both: 3 // Mirror both axes
196
+ const EyeShadowColor$1 = {
197
+ Plum: 0, // Plum / mauve (default)
198
+ Brown: 1, // Warm brown
199
+ Gold: 2, // Soft gold
200
+ Pink: 3 // Dusty pink
190
201
  };
191
202
 
192
- /**
193
- * Virtual background options class
194
- * Matches the C++/Java/OC API design
195
- */
196
- let VirtualBackgroundOptions$1 = class VirtualBackgroundOptions {
197
- /**
198
- * Creates a new VirtualBackgroundOptions instance
199
- * @param {Object} [options] - Options object
200
- * @param {number} [options.mode] - Background mode (use BackgroundMode enum)
201
- * @param {ImageData|HTMLImageElement|HTMLCanvasElement} [options.backgroundImage] - Background image (required when mode is Image)
202
- */
203
- constructor(options = {}) {
204
- this.mode = options.mode !== undefined ? options.mode : BackgroundMode$1.None;
205
- this.backgroundImage = options.backgroundImage || null;
206
- }
203
+ const EyeLinerStyle$1 = {
204
+ Classic: 0, // Full liner with winged tip
205
+ Flick: 1, // Extended outer flick
206
+ CatEye: 2, // Short cat-eye lift
207
+ Natural: 3, // Thin soft natural line
208
+ Bold: 4, // Bold cover
209
+ Soft: 5 // Soft feathered wing
210
+ };
207
211
 
208
- /**
209
- * Validates the options
210
- * @returns {boolean} True if valid
211
- */
212
- isValid() {
213
- if (this.mode === BackgroundMode$1.Image) {
214
- return this.backgroundImage !== null;
215
- }
216
- return true;
217
- }
212
+ const EyeLinerColor$1 = {
213
+ Burgundy: 0, // Burgundy brown
214
+ Plum: 1, // Deep plum
215
+ Chocolate: 2, // Chocolate brown
216
+ Coffee: 3, // Near-black coffee
217
+ Mauve: 4 // Mauve grey
218
+ };
219
+
220
+ const EyebrowStyle$1 = {
221
+ Natural: 0, // Natural arch with hair strokes
222
+ Soft: 1, // Soft powdery thick brow
223
+ Feathered: 2, // Feathered hair strokes
224
+ Mist: 3, // Light powder mist
225
+ Arched: 4, // Classic high arch
226
+ Powder: 5, // Dense powder fill
227
+ Wild: 6, // Wispy upper edge
228
+ Full: 7, // Full balanced brow
229
+ Straight: 8 // Straight low arch
230
+ };
231
+
232
+ const EyebrowColor$1 = {
233
+ DarkBrown: 0, // Dark brown (default)
234
+ Black: 1, // Black
235
+ SoftBrown: 2 // Soft brown
236
+ };
237
+
238
+ const EyelashStyle$1 = {
239
+ Classic: 0, // Balanced clusters + lower lashes
240
+ Manga: 1, // Bold spiked clusters
241
+ Winged: 2, // Extended outer corner
242
+ Wispy: 3, // Fluffy lifted tips
243
+ Clustered: 4, // Distinct clusters
244
+ Doll: 5 // Short doll lashes
245
+ };
246
+
247
+ const EyelashColor$1 = {
248
+ Black: 0, // Near-black (default)
249
+ Brown: 1, // Soft brown
250
+ SoftBlack: 2 // Slightly softer black
251
+ };
252
+
253
+ const PupilColor$1 = {
254
+ Hazel: 0, // Amber / honey brown
255
+ Ice: 1, // Icy blue
256
+ Mocha: 2, // Dark brown with sparkle
257
+ Olive: 3, // Forest green
258
+ Gloss: 4, // Glossy dark brown
259
+ Moss: 5, // Muted olive
260
+ Sand: 6, // Sandy beige brown
261
+ Glow: 7, // Lower-iris crescent highlight
262
+ Slate: 8 // Cool blue-grey
263
+ };
264
+
265
+ const ChromaKeyColor$1 = {
266
+ Green: 0,
267
+ Blue: 1,
268
+ Red: 2
269
+ };
270
+
271
+ /** Frame type for processing (affects temporal smoothing / tracking). */
272
+ const FrameType$1 = {
273
+ Image: 0, // Still image
274
+ Video: 1 // Continuous video stream
275
+ };
276
+
277
+ /** Mirror mode applied to the input before processing. */
278
+ const MirrorMode$1 = {
279
+ None: 0, // No mirror
280
+ Horizontal: 1, // Horizontal (e.g. front-camera selfie)
281
+ Vertical: 2, // Vertical
282
+ Both: 3 // Both axes
218
283
  };
219
284
 
220
285
  var constants = /*#__PURE__*/Object.freeze({
221
286
  __proto__: null,
222
- BackgroundMode: BackgroundMode$1,
223
- BasicParam: BasicParam$1,
224
- BeautyType: BeautyType$1,
287
+ BlushColor: BlushColor$1,
225
288
  BlushStyle: BlushStyle$1,
226
- ChromaKeyParam: ChromaKeyParam$1,
289
+ ChromaKeyColor: ChromaKeyColor$1,
290
+ ContourStyle: ContourStyle$1,
291
+ EngineEventCode: EngineEventCode$1,
292
+ EyeLinerColor: EyeLinerColor$1,
293
+ EyeLinerStyle: EyeLinerStyle$1,
294
+ EyeShadowColor: EyeShadowColor$1,
295
+ EyeShadowStyle: EyeShadowStyle$1,
296
+ EyebrowColor: EyebrowColor$1,
297
+ EyebrowStyle: EyebrowStyle$1,
298
+ EyelashColor: EyelashColor$1,
299
+ EyelashStyle: EyelashStyle$1,
227
300
  FrameType: FrameType$1,
228
- LipstickStyle: LipstickStyle$1,
229
- MakeupParam: MakeupParam$1,
301
+ LipstickColor: LipstickColor$1,
230
302
  MirrorMode: MirrorMode$1,
231
- ReshapeParam: ReshapeParam$1,
232
- VirtualBackgroundOptions: VirtualBackgroundOptions$1
303
+ PupilColor: PupilColor$1,
304
+ Reshape: Reshape$1,
305
+ SmoothingStyle: SmoothingStyle$1,
306
+ WhiteningStyle: WhiteningStyle$1
233
307
  });
234
308
 
235
309
  /**
@@ -266,17 +340,28 @@ function logWarn(message) {
266
340
  console.warn(formatLogLine('warning', message));
267
341
  }
268
342
 
269
- /** @param {string} message */
270
- function logError(message) {
271
- console.error(formatLogLine('error', message));
272
- }
273
-
274
343
  /**
275
344
  * Beauty Effect Engine Core
276
345
  * Platform-agnostic engine implementation with dependency injection
277
346
  */
278
347
 
279
348
 
349
+ /** Byte layouts of fb_engine_config_t / fb_log_config_t on wasm32. */
350
+ const FB_ENGINE_CONFIG_SIZE = 28;
351
+ const FB_LOG_CONFIG_SIZE = 16;
352
+
353
+ function allocUtf8(Module, text) {
354
+ const value = text || '';
355
+ const len = Module.lengthBytesUTF8(value) + 1;
356
+ const ptr = Module._malloc(len);
357
+ Module.stringToUTF8(value, ptr, len);
358
+ return ptr;
359
+ }
360
+
361
+ function writeI32(Module, ptr, index, value) {
362
+ Module.HEAP32[(ptr >> 2) + index] = value;
363
+ }
364
+
280
365
  /**
281
366
  * Beauty effect engine class
282
367
  * Provides high-level API for face beauty effects processing
@@ -291,9 +376,8 @@ class BeautyEffectEngine {
291
376
  * @param {Function} platformAPI.getWasmModule - Get WASM module instance
292
377
  * @param {Function} platformAPI.getWasmBuffer - Get WASM memory buffer
293
378
  * @param {Function} platformAPI.toImageData - Convert to ImageData
294
- * @param {Function} platformAPI.verifyAppKeyOnline - Verify app key online
295
- * @param {Function} [platformAPI.ensureGPUPixelCanvas] - Ensure GPU canvas exists (browser only)
296
- * @param {Function} [platformAPI.cleanupGPUPixelCanvas] - Cleanup GPU canvas (browser only)
379
+ * @param {Function} [platformAPI.ensureFacebetterCanvas] - Ensure GPU canvas exists (browser only)
380
+ * @param {Function} [platformAPI.cleanupFacebetterCanvas] - Cleanup GPU canvas (browser only)
297
381
  */
298
382
  constructor(config, platformAPI) {
299
383
  if (!config) {
@@ -319,9 +403,9 @@ class BeautyEffectEngine {
319
403
  }
320
404
 
321
405
  this.config = engineConfig;
322
- this.appId = engineConfig.appId;
323
- this.appKey = engineConfig.appKey;
324
- this.licenseJson = engineConfig.licenseJson;
406
+ this.licenseToken = engineConfig.licenseToken;
407
+ this.authProxyUrl = engineConfig.authProxyUrl;
408
+ this.fetchAuthResponse = engineConfig.fetchAuthResponse;
325
409
  this.resourcePath = '/resource.fbd';
326
410
  this.enginePtr = null;
327
411
  this.initialized = false;
@@ -339,18 +423,18 @@ class BeautyEffectEngine {
339
423
  this._getWasmModule = platformAPI.getWasmModule;
340
424
  this._getWasmBuffer = platformAPI.getWasmBuffer;
341
425
  this._toImageData = platformAPI.toImageData;
342
- this._ensureGPUPixelCanvas = platformAPI.ensureGPUPixelCanvas;
343
- this._cleanupGPUPixelCanvas = platformAPI.cleanupGPUPixelCanvas;
426
+ this._ensureFacebetterCanvas = platformAPI.ensureFacebetterCanvas;
427
+ this._cleanupFacebetterCanvas = platformAPI.cleanupFacebetterCanvas;
344
428
 
345
429
  // Browser-specific state
346
- this._gpupixelCanvas = null;
347
- this._createdGPUPixelCanvas = false;
430
+ this._facebetterCanvas = null;
431
+ this._createdFacebetterCanvas = false;
348
432
  this._offscreenCanvas = null;
349
433
  this._offscreenCtx = null;
350
434
 
351
- // Automatically create gpupixel_canvas if it doesn't exist (browser only)
352
- if (this._ensureGPUPixelCanvas) {
353
- this._ensureGPUPixelCanvas();
435
+ // Automatically create facebetter_canvas if it doesn't exist (browser only)
436
+ if (this._ensureFacebetterCanvas) {
437
+ this._ensureFacebetterCanvas();
354
438
  }
355
439
 
356
440
  // Start loading WASM module immediately in constructor
@@ -447,6 +531,10 @@ class BeautyEffectEngine {
447
531
  if (!Module.onReportUsage) {
448
532
  Module.onReportUsage = async (payloadJson) => {
449
533
  try {
534
+ const payload = JSON.parse(payloadJson);
535
+ if (!payload.app_id || !payload.hmac_signature) {
536
+ return true;
537
+ }
450
538
  const url = 'https://facebetter.pixpark.net/facebetter/v1/report';
451
539
  const response = await fetch(url, {
452
540
  method: 'POST',
@@ -462,48 +550,97 @@ class BeautyEffectEngine {
462
550
  };
463
551
  }
464
552
 
465
- // Setup online auth proxy for WASM(纯在线,失败不回退本地缓存)。
466
- if (!Module.onOnlineAuth) {
467
- Module.onOnlineAuth = async (payloadJson) => {
468
- const authUrl = 'https://facebetter.pixpark.net/facebetter/v1/auth';
469
- logInfo('Online license: requesting auth server');
470
- try {
471
- const response = await fetch(authUrl, {
472
- method: 'POST',
473
- headers: {
474
- 'Content-Type': 'application/json',
475
- },
476
- body: payloadJson,
477
- });
478
- if (!response.ok) {
479
- logWarn(`Online license: HTTP ${response.status}`);
553
+ // Web v2 auth: never accept appId/appKey in the browser.
554
+ // - licenseToken: pre-fetched JWS or `{success, token}` body
555
+ // - authProxyUrl: production forward the challenge to your server
556
+ // - fetchAuthResponse: custom hook; locally use createDirectAuthFetcher
557
+ if (!this.licenseToken) {
558
+ if (typeof this.fetchAuthResponse === 'function') {
559
+ Module.onOnlineAuth = async (payloadJson) => {
560
+ try {
561
+ const challenge = JSON.parse(payloadJson);
562
+ const result = await this.fetchAuthResponse(challenge);
563
+ if (result && typeof result === 'object' && typeof result.response === 'string') {
564
+ return { response: result.response };
565
+ }
566
+ if (typeof result === 'string') {
567
+ return result;
568
+ }
569
+ if (result && typeof result === 'object') {
570
+ return JSON.stringify(result);
571
+ }
480
572
  return '';
573
+ } catch (err) {
574
+ logWarn(
575
+ `Online license: fetchAuthResponse error${err?.message ? `: ${err.message}` : ''}`
576
+ );
577
+ return JSON.stringify({
578
+ success: false,
579
+ error: 'auth_fetch_failed',
580
+ message: err?.message || 'auth fetch failed',
581
+ });
481
582
  }
482
- const text = await response.text();
483
- logInfo('Online license: server response ok');
484
- return text;
485
- } catch (err) {
486
- logWarn(
487
- `Online license: network error${err?.message ? `: ${err.message}` : ''}`
488
- );
489
- return '';
490
- }
491
- };
583
+ };
584
+ } else if (this.authProxyUrl) {
585
+ Module.onOnlineAuth = async (payloadJson) => {
586
+ logInfo('Online license: proxying auth challenge');
587
+ try {
588
+ const response = await fetch(this.authProxyUrl, {
589
+ method: 'POST',
590
+ headers: { 'Content-Type': 'application/json' },
591
+ body: payloadJson,
592
+ });
593
+ if (!response.ok) {
594
+ logWarn(`Online license: proxy HTTP ${response.status}`);
595
+ return '';
596
+ }
597
+ const text = await response.text();
598
+ logInfo('Online license: proxy response ok');
599
+ return text;
600
+ } catch (err) {
601
+ logWarn(
602
+ `Online license: proxy error${err?.message ? `: ${err.message}` : ''}`
603
+ );
604
+ return '';
605
+ }
606
+ };
607
+ } else {
608
+ throw new FacebetterError(
609
+ 'Web auth requires licenseToken, authProxyUrl, or fetchAuthResponse.',
610
+ 'LICENSE_ERROR'
611
+ );
612
+ }
492
613
  }
493
614
 
494
- // Create engine instance, auth (online/offline) is handled by WASM layer via onOnlineAuth
495
- const enginePtr = Module.ccall(
496
- 'CreateBeautyEffectEngineEx',
497
- 'number',
498
- ['string', 'string', 'string', 'string', 'number'],
499
- [
500
- this.resourcePath,
501
- this.licenseJson || '',
502
- this.appId || '',
503
- this.appKey || '',
504
- this.config.externalContext ? 1 : 0
505
- ]
506
- );
615
+ // C ABI still has app_id/app_key slots; leave them empty on Web.
616
+ const appIdPtr = allocUtf8(Module, '');
617
+ const appKeyPtr = allocUtf8(Module, '');
618
+ const licensePtr = allocUtf8(Module, this.licenseToken);
619
+ const resourcePtr = allocUtf8(Module, this.resourcePath);
620
+ const configPtr = Module._malloc(FB_ENGINE_CONFIG_SIZE);
621
+ writeI32(Module, configPtr, 0, appIdPtr);
622
+ writeI32(Module, configPtr, 1, appKeyPtr);
623
+ writeI32(Module, configPtr, 2, licensePtr);
624
+ writeI32(Module, configPtr, 3, resourcePtr);
625
+ writeI32(Module, configPtr, 4, 0);
626
+ writeI32(Module, configPtr, 5, this.config.externalContext ? 1 : 0);
627
+ writeI32(Module, configPtr, 6, 0);
628
+
629
+ let enginePtr = 0;
630
+ try {
631
+ enginePtr = Module.ccall(
632
+ 'fb_engine_create',
633
+ 'number',
634
+ ['number'],
635
+ [configPtr]
636
+ );
637
+ } finally {
638
+ Module._free(configPtr);
639
+ Module._free(appIdPtr);
640
+ Module._free(appKeyPtr);
641
+ Module._free(licensePtr);
642
+ Module._free(resourcePtr);
643
+ }
507
644
 
508
645
  if (!enginePtr) {
509
646
  throw new FacebetterError(
@@ -551,7 +688,10 @@ class BeautyEffectEngine {
551
688
  this._callbackSharedBufferSize = 0;
552
689
  }
553
690
 
554
- Module.ccall('DestroyBeautyEffectEngine', null, ['number'], [this.enginePtr]);
691
+ Module.ccall('fb_engine_destroy', null, ['number'], [this.enginePtr]);
692
+ if (Module._engine_event_callbacks) {
693
+ delete Module._engine_event_callbacks[this.enginePtr];
694
+ }
555
695
  this.enginePtr = null;
556
696
  this.initialized = false;
557
697
  this.bufferSize = 0;
@@ -563,15 +703,15 @@ class BeautyEffectEngine {
563
703
  this._offscreenCtx = null;
564
704
  }
565
705
 
566
- // Clean up gpupixel_canvas if we created it
567
- if (this._cleanupGPUPixelCanvas) {
568
- this._cleanupGPUPixelCanvas();
706
+ // Clean up facebetter_canvas if we created it
707
+ if (this._cleanupFacebetterCanvas) {
708
+ this._cleanupFacebetterCanvas();
569
709
  }
570
710
  }
571
711
 
572
712
  /**
573
713
  * Sets log configuration
574
- * Can be called before init() since SetLogConfig is a global function
714
+ * Can be called before init() since fb_set_log_config is a global function
575
715
  * @param {Object} config - Log configuration
576
716
  * @param {boolean} config.consoleEnabled - Enable console logging
577
717
  * @param {boolean} config.fileEnabled - Enable file logging
@@ -584,406 +724,415 @@ class BeautyEffectEngine {
584
724
  await this._wasmLoadPromise;
585
725
  const Module = this._getWasmModule();
586
726
 
587
- const result = Module.ccall(
588
- 'SetLogConfig',
589
- 'number',
590
- ['bool', 'bool', 'number', 'string'],
591
- [
592
- config.consoleEnabled || false,
593
- config.fileEnabled || false,
594
- config.level || 0,
595
- config.fileName || ''
596
- ]
597
- );
727
+ const fileNamePtr = allocUtf8(Module, config.fileName);
728
+ const configPtr = Module._malloc(FB_LOG_CONFIG_SIZE);
729
+ writeI32(Module, configPtr, 0, config.consoleEnabled ? 1 : 0);
730
+ writeI32(Module, configPtr, 1, config.fileEnabled ? 1 : 0);
731
+ writeI32(Module, configPtr, 2, config.level || 0);
732
+ writeI32(Module, configPtr, 3, fileNamePtr);
733
+
734
+ let result;
735
+ try {
736
+ result = Module.ccall(
737
+ 'fb_set_log_config',
738
+ 'number',
739
+ ['number'],
740
+ [configPtr]
741
+ );
742
+ } finally {
743
+ Module._free(configPtr);
744
+ Module._free(fileNamePtr);
745
+ }
598
746
 
599
747
  checkResult(result, 'Failed to set log config');
600
748
  }
601
749
 
602
750
  /**
603
- * [Deprecated] No-op in parameter-driven mode. Use:
604
- * - setBasicParam/setReshapeParam/setMakeupParam
605
- * - setVirtualBackground
606
- * - setFilter/setSticker
607
- * Kept for compatibility and may be removed in a future release.
608
- * @deprecated
609
- * @param {number} beautyType - Beauty type (use BeautyType enum)
610
- * @param {boolean} enabled - Enable or disable
751
+ * Sets skin smoothing intensity
752
+ * @param {number} value - Parameter value (0.0 - 1.0)
611
753
  */
612
- setBeautyTypeEnabled(beautyType, enabled) {
754
+ setSmoothing(value) {
613
755
  this._ensureInitialized();
614
- const Module = this._getWasmModule();
615
-
616
- const result = Module.ccall(
617
- 'SetBeautyTypeEnabled',
618
- 'number',
619
- ['number', 'number', 'number'],
620
- [this.enginePtr, beautyType, enabled ? 1 : 0]
621
- );
756
+ this._setIntensity('fb_set_smoothing', value);
757
+ }
622
758
 
623
- checkResult(result, `Failed to ${enabled ? 'enable' : 'disable'} beauty type`);
759
+ /**
760
+ * Sets skin smoothing style
761
+ * @param {number} style - Style (use SmoothingStyle enum, e.g., SmoothingStyle.Natural)
762
+ */
763
+ setSmoothingStyle(style) {
764
+ this._setMakeupStyle('fb_set_smoothing_style', style, 'Failed to set smoothing style');
624
765
  }
625
766
 
626
767
  /**
627
- * [Deprecated] Always returns false in parameter-driven mode.
628
- * Query your own UI state or parameters instead.
629
- * Kept for compatibility and may be removed in a future release.
630
- * @deprecated
631
- * @param {number} beautyType - Beauty type (use BeautyType enum)
632
- * @returns {boolean} True if enabled
768
+ * Sets skin whitening intensity
769
+ * @param {number} value - Parameter value (0.0 - 1.0)
633
770
  */
634
- isBeautyTypeEnabled(beautyType) {
771
+ setWhitening(value) {
635
772
  this._ensureInitialized();
636
- const Module = this._getWasmModule();
637
-
638
- const result = Module.ccall(
639
- 'IsBeautyTypeEnabled',
640
- 'number',
641
- ['number', 'number'],
642
- [this.enginePtr, beautyType]
643
- );
644
-
645
- if (result === -1) {
646
- throw new FacebetterError('Failed to check beauty type status');
647
- }
773
+ this._setIntensity('fb_set_whitening', value);
774
+ }
648
775
 
649
- return result === 1;
776
+ /**
777
+ * Sets whitening style by swapping the custom LUT
778
+ * @param {number} style - Style (use WhiteningStyle enum, e.g., WhiteningStyle.ColdWhite)
779
+ */
780
+ setWhiteningStyle(style) {
781
+ this._setMakeupStyle('fb_set_whitening_style', style, 'Failed to set whitening style');
650
782
  }
651
783
 
652
784
  /**
653
- * [Deprecated] No-op that returns success. To reset, explicitly zero parameters
654
- * and clear effects via parameter-driven APIs. Kept for compatibility and may
655
- * be removed in a future release.
656
- * @deprecated
785
+ * Sets image sharpening intensity
786
+ * @param {number} value - Parameter value (0.0 - 1.0)
657
787
  */
658
- disableAllBeautyTypes() {
788
+ setSharpening(value) {
659
789
  this._ensureInitialized();
660
- const Module = this._getWasmModule();
661
-
662
- const result = Module.ccall(
663
- 'DisableAllBeautyTypes',
664
- 'number',
665
- ['number'],
666
- [this.enginePtr]
667
- );
668
-
669
- checkResult(result, 'Failed to disable all beauty types');
790
+ this._setIntensity('fb_set_sharpening', value);
670
791
  }
671
792
 
672
793
  /**
673
- * Sets a basic beauty parameter
674
- * @param {number} param - Parameter (use BasicParam enum, e.g., BasicParam.Smoothing)
794
+ * Sets skin rosiness intensity
675
795
  * @param {number} value - Parameter value (0.0 - 1.0)
676
796
  */
677
- setBasicParam(param, value) {
797
+ setRosiness(value) {
678
798
  this._ensureInitialized();
679
- this._setBeautyParam('SetBeautyParamBasic', param, value);
799
+ this._setIntensity('fb_set_rosiness', value);
680
800
  }
681
801
 
682
802
  /**
683
803
  * Sets a reshape parameter
684
- * @param {number} param - Parameter (use ReshapeParam enum, e.g., ReshapeParam.FaceThinning)
685
- * @param {number} value - Parameter value (0.0 - 1.0)
804
+ * @param {number} param - Parameter (use Reshape enum, e.g., Reshape.FaceThin)
805
+ * @param {number} value - Parameter value in [-1.0, 1.0]. 0 is off.
686
806
  */
687
- setReshapeParam(param, value) {
807
+ setReshape(param, value) {
688
808
  this._ensureInitialized();
689
- this._setBeautyParam('SetBeautyParamReshape', param, value);
809
+ this._setBeautyParam('fb_set_reshape', param, value);
690
810
  }
691
811
 
692
812
  /**
693
- * Sets a makeup parameter
694
- * @param {number} param - Parameter (use MakeupParam enum, e.g., MakeupParam.Lipstick)
813
+ * Sets lipstick intensity
695
814
  * @param {number} value - Parameter value (0.0 - 1.0)
696
815
  */
697
- setMakeupParam(param, value) {
816
+ setLipstick(value) {
698
817
  this._ensureInitialized();
699
- this._setBeautyParam('SetBeautyParamMakeup', param, value);
818
+ this._setIntensity('fb_set_lipstick', value);
700
819
  }
701
820
 
702
821
  /**
703
- * Sets lipstick style/texture
704
- * @param {number} style - Style (use LipstickStyle enum, e.g., LipstickStyle.Rouge)
822
+ * Sets lipstick colour preset
823
+ * @param {number} style - Style (use LipstickColor enum, e.g., LipstickColor.Rouge)
705
824
  */
706
- setLipstickStyle(style) {
707
- this._ensureInitialized();
708
- const Module = this._getWasmModule();
709
- const result = Module.ccall(
710
- 'SetLipstickStyle',
711
- 'number',
712
- ['number', 'number'],
713
- [this.enginePtr, style]
714
- );
715
- checkResult(result, 'Failed to set lipstick style');
825
+ setLipstickColor(style) {
826
+ this._setMakeupStyle('fb_set_lipstick_color', style, 'Failed to set lipstick style');
716
827
  }
717
828
 
718
829
  /**
719
- * Sets blush style/texture
720
- * @param {number} style - Style (use BlushStyle enum, e.g., BlushStyle.Classic)
830
+ * Sets blush intensity
831
+ * @param {number} value - Parameter value (0.0 - 1.0)
721
832
  */
833
+ setBlush(value) {
834
+ this._ensureInitialized();
835
+ this._setIntensity('fb_set_blush', value);
836
+ }
837
+
722
838
  setBlushStyle(style) {
839
+ this._setMakeupStyle('fb_set_blush_style', style, 'Failed to set blush style');
840
+ }
841
+
842
+ setBlushColor(color) {
843
+ this._setMakeupStyle('fb_set_blush_color', color, 'Failed to set blush color');
844
+ }
845
+
846
+ setContour(value) {
723
847
  this._ensureInitialized();
724
- const Module = this._getWasmModule();
725
- const result = Module.ccall(
726
- 'SetBlushStyle',
727
- 'number',
728
- ['number', 'number'],
729
- [this.enginePtr, style]
730
- );
731
- checkResult(result, 'Failed to set blush style');
848
+ this._setIntensity('fb_set_contour', value);
732
849
  }
733
850
 
734
- /**
735
- * Sets chroma key parameter
736
- * @param {number} param - Parameter (use ChromaKeyParam enum, e.g., ChromaKeyParam.Similarity)
737
- * @param {number} value - Parameter value (KeyColor: 0.0=Green, 1.0=Blue, 2.0=Red; others 0.0-1.0)
738
- */
739
- setChromaKeyParam(param, value) {
851
+ setContourStyle(style) {
852
+ this._setMakeupStyle('fb_set_contour_style', style, 'Failed to set contour style');
853
+ }
854
+
855
+ setEyeShadow(value) {
740
856
  this._ensureInitialized();
741
- // For KeyColor, values are 0, 1, 2, not 0.0-1.0
742
- if (param === 0) { // ChromaKeyParam.KeyColor
743
- const Module = this._getWasmModule();
744
- const result = Module.ccall(
745
- 'SetBeautyParamChromaKey',
746
- 'number',
747
- ['number', 'number', 'number'],
748
- [this.enginePtr, param, value]
749
- );
750
- checkResult(result, `Failed to set chroma key parameter`);
751
- return;
752
- }
753
- this._setBeautyParam('SetBeautyParamChromaKey', param, value);
857
+ this._setIntensity('fb_set_eye_shadow', value);
858
+ }
859
+
860
+ setEyeShadowStyle(style) {
861
+ this._setMakeupStyle('fb_set_eye_shadow_style', style, 'Failed to set eyeshadow style');
862
+ }
863
+
864
+ setEyeShadowColor(style) {
865
+ this._setMakeupStyle('fb_set_eye_shadow_color', style, 'Failed to set eyeshadow style');
866
+ }
867
+
868
+ setEyeLiner(value) {
869
+ this._ensureInitialized();
870
+ this._setIntensity('fb_set_eye_liner', value);
871
+ }
872
+
873
+ setEyeLinerStyle(style) {
874
+ this._setMakeupStyle('fb_set_eye_liner_style', style, 'Failed to set eyeliner style');
875
+ }
876
+
877
+ setEyeLinerColor(style) {
878
+ this._setMakeupStyle('fb_set_eye_liner_color', style, 'Failed to set eyeliner style');
879
+ }
880
+
881
+ setEyebrow(value) {
882
+ this._ensureInitialized();
883
+ this._setIntensity('fb_set_eyebrow', value);
884
+ }
885
+
886
+ setEyebrowStyle(style) {
887
+ this._setMakeupStyle('fb_set_eyebrow_style', style, 'Failed to set eyebrow style');
888
+ }
889
+
890
+ setEyebrowColor(style) {
891
+ this._setMakeupStyle('fb_set_eyebrow_color', style, 'Failed to set eyebrow style');
892
+ }
893
+
894
+ setEyelash(value) {
895
+ this._ensureInitialized();
896
+ this._setIntensity('fb_set_eyelash', value);
897
+ }
898
+
899
+ setEyelashStyle(style) {
900
+ this._setMakeupStyle('fb_set_eyelash_style', style, 'Failed to set eyelash style');
901
+ }
902
+
903
+ setEyelashColor(style) {
904
+ this._setMakeupStyle('fb_set_eyelash_color', style, 'Failed to set eyelash style');
905
+ }
906
+
907
+ setPupil(value) {
908
+ this._ensureInitialized();
909
+ this._setIntensity('fb_set_pupil', value);
910
+ }
911
+
912
+ setPupilColor(style) {
913
+ this._setMakeupStyle('fb_set_pupil_color', style, 'Failed to set pupil style');
754
914
  }
755
915
 
756
916
  /**
757
- * Sets a LUT-based filter
758
- * @param {string} filterId - Unique identifier of the filter (e.g., "chuxin"). Pass "" to clear.
917
+ * Uses chroma keying as the virtual-background mask.
918
+ * Fill is still controlled by setVirtualBackgroundBlur / setVirtualBackground.
919
+ * @param {number} color - Key colour (use ChromaKeyColor enum)
759
920
  */
760
- setFilter(filterId) {
761
- this._ensureInitialized();
762
- const Module = this._getWasmModule();
763
- const result = Module.ccall(
764
- 'SetFilter',
765
- 'number',
766
- ['number', 'string'],
767
- [this.enginePtr, filterId || '']
768
- );
769
- checkResult(result, 'Failed to set filter');
921
+ setChromaKey(color) {
922
+ this._setMakeupStyle('fb_set_chroma_key', color, 'Failed to set chroma key');
770
923
  }
771
924
 
772
925
  /**
773
- * Sets the intensity of the current filter
774
- * @param {number} intensity - Filter intensity (0.0 - 1.0)
926
+ * Turns off chroma keying and restores portrait-segmentation as the mask.
775
927
  */
776
- setFilterIntensity(intensity) {
928
+ clearChromaKey() {
777
929
  this._ensureInitialized();
778
930
  const Module = this._getWasmModule();
779
931
  const result = Module.ccall(
780
- 'SetFilterIntensity',
932
+ 'fb_clear_chroma_key',
781
933
  'number',
782
- ['number', 'number'],
783
- [this.enginePtr, intensity]
934
+ ['number'],
935
+ [this.enginePtr]
784
936
  );
785
- checkResult(result, 'Failed to set filter intensity');
937
+ checkResult(result, 'Failed to clear chroma key');
786
938
  }
787
939
 
788
- /**
789
- * Sets a 2D sticker
790
- * @param {string} stickerId - Unique identifier of the sticker (e.g., "樱花"). Pass "" to clear.
791
- */
792
- setSticker(stickerId) {
940
+ /** How close a pixel must be to the key colour to be keyed out. [0.0, 1.0]. */
941
+ setChromaKeySimilarity(value) {
942
+ this._setChromaKeyFloat('fb_set_chroma_key_similarity', value);
943
+ }
944
+
945
+ /** Edge feather around the key. [0.0, 1.0]. */
946
+ setChromaKeySmoothness(value) {
947
+ this._setChromaKeyFloat('fb_set_chroma_key_smoothness', value);
948
+ }
949
+
950
+ /** Spill suppression on semi-transparent edges. [0.0, 1.0]. */
951
+ setChromaKeyDesaturation(value) {
952
+ this._setChromaKeyFloat('fb_set_chroma_key_desaturation', value);
953
+ }
954
+
955
+ _setChromaKeyFloat(cFuncName, value) {
793
956
  this._ensureInitialized();
794
957
  const Module = this._getWasmModule();
795
958
  const result = Module.ccall(
796
- 'SetSticker',
959
+ cFuncName,
797
960
  'number',
798
- ['number', 'string'],
799
- [this.enginePtr, stickerId || '']
961
+ ['number', 'number'],
962
+ [this.enginePtr, value]
800
963
  );
801
- checkResult(result, 'Failed to set sticker');
964
+ checkResult(result, `Failed to call ${cFuncName}`);
802
965
  }
803
966
 
804
967
  /**
805
- * Registers a filter from a file path or data
806
- * @param {string} filterId - Unique identifier for the filter
807
- * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data
968
+ * Applies a LUT filter from a file path or in-memory .fbd data.
969
+ * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data.
808
970
  */
809
- registerFilter(filterId, resource) {
971
+ setFilter(resource) {
810
972
  this._ensureInitialized();
811
973
  const Module = this._getWasmModule();
812
974
 
813
975
  if (typeof resource === 'string') {
976
+ if (!resource) {
977
+ throw new FacebetterError('Filter path must not be empty; use clearFilter()');
978
+ }
814
979
  const result = Module.ccall(
815
- 'RegisterFilterPath',
980
+ 'fb_set_filter',
816
981
  'number',
817
- ['number', 'string', 'string'],
818
- [this.enginePtr, filterId, resource]
982
+ ['number', 'string'],
983
+ [this.enginePtr, resource]
819
984
  );
820
- checkResult(result, `Failed to register filter path: ${filterId}`);
821
- } else if (resource instanceof Uint8Array) {
985
+ checkResult(result, 'Failed to set filter from path');
986
+ return;
987
+ }
988
+
989
+ if (resource instanceof Uint8Array) {
822
990
  const dataPtr = Module._malloc(resource.length);
823
991
  Module.HEAPU8.set(resource, dataPtr);
824
992
  try {
825
993
  const result = Module.ccall(
826
- 'RegisterFilterData',
994
+ 'fb_set_filter_data',
827
995
  'number',
828
- ['number', 'string', 'number', 'number'],
829
- [this.enginePtr, filterId, dataPtr, resource.length]
996
+ ['number', 'number', 'number'],
997
+ [this.enginePtr, dataPtr, resource.length]
830
998
  );
831
- checkResult(result, `Failed to register filter data: ${filterId}`);
999
+ checkResult(result, 'Failed to set filter from data');
832
1000
  } finally {
833
1001
  Module._free(dataPtr);
834
1002
  }
835
- } else {
836
- throw new FacebetterError('Resource must be a string path or Uint8Array data');
1003
+ return;
837
1004
  }
1005
+
1006
+ throw new FacebetterError('Filter resource must be a string path or Uint8Array');
1007
+ }
1008
+
1009
+ /**
1010
+ * Clears the current LUT filter.
1011
+ */
1012
+ clearFilter() {
1013
+ this._ensureInitialized();
1014
+ const Module = this._getWasmModule();
1015
+ const result = Module.ccall(
1016
+ 'fb_clear_filter',
1017
+ 'number',
1018
+ ['number'],
1019
+ [this.enginePtr]
1020
+ );
1021
+ checkResult(result, 'Failed to clear filter');
1022
+ }
1023
+
1024
+ /**
1025
+ * Sets the intensity of the current filter
1026
+ * @param {number} intensity - Filter intensity (0.0 - 1.0)
1027
+ */
1028
+ setFilterIntensity(intensity) {
1029
+ this._ensureInitialized();
1030
+ const Module = this._getWasmModule();
1031
+ const result = Module.ccall(
1032
+ 'fb_set_filter_intensity',
1033
+ 'number',
1034
+ ['number', 'number'],
1035
+ [this.enginePtr, intensity]
1036
+ );
1037
+ checkResult(result, 'Failed to set filter intensity');
838
1038
  }
839
1039
 
840
1040
  /**
841
- * Registers a sticker from a file path or data
842
- * @param {string} stickerId - Unique identifier for the sticker
843
- * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data
1041
+ * Applies a 2D sticker from a file path or in-memory .fbd data.
1042
+ * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data.
844
1043
  */
845
- registerSticker(stickerId, resource) {
1044
+ setSticker(resource) {
846
1045
  this._ensureInitialized();
847
1046
  const Module = this._getWasmModule();
848
1047
 
849
1048
  if (typeof resource === 'string') {
1049
+ if (!resource) {
1050
+ throw new FacebetterError('Sticker path must not be empty; use clearSticker()');
1051
+ }
850
1052
  const result = Module.ccall(
851
- 'RegisterStickerPath',
1053
+ 'fb_set_sticker',
852
1054
  'number',
853
- ['number', 'string', 'string'],
854
- [this.enginePtr, stickerId, resource]
1055
+ ['number', 'string'],
1056
+ [this.enginePtr, resource]
855
1057
  );
856
- checkResult(result, `Failed to register sticker path: ${stickerId}`);
857
- } else if (resource instanceof Uint8Array) {
1058
+ checkResult(result, 'Failed to set sticker from path');
1059
+ return;
1060
+ }
1061
+
1062
+ if (resource instanceof Uint8Array) {
858
1063
  const dataPtr = Module._malloc(resource.length);
859
1064
  Module.HEAPU8.set(resource, dataPtr);
860
1065
  try {
861
1066
  const result = Module.ccall(
862
- 'RegisterStickerData',
1067
+ 'fb_set_sticker_data',
863
1068
  'number',
864
- ['number', 'string', 'number', 'number'],
865
- [this.enginePtr, stickerId, dataPtr, resource.length]
1069
+ ['number', 'number', 'number'],
1070
+ [this.enginePtr, dataPtr, resource.length]
866
1071
  );
867
- checkResult(result, `Failed to register sticker data: ${stickerId}`);
1072
+ checkResult(result, 'Failed to set sticker from data');
868
1073
  } finally {
869
1074
  Module._free(dataPtr);
870
1075
  }
871
- } else {
872
- throw new FacebetterError('Resource must be a string path or Uint8Array data');
1076
+ return;
873
1077
  }
1078
+
1079
+ throw new FacebetterError('Sticker resource must be a string path or Uint8Array');
874
1080
  }
875
1081
 
876
1082
  /**
877
- * Unregisters a specific filter
878
- * @param {string} filterId - Filter ID to unregister
1083
+ * Clears the current 2D sticker.
879
1084
  */
880
- unregisterFilter(filterId) {
1085
+ clearSticker() {
881
1086
  this._ensureInitialized();
882
1087
  const Module = this._getWasmModule();
883
1088
  const result = Module.ccall(
884
- 'UnregisterFilter',
1089
+ 'fb_clear_sticker',
885
1090
  'number',
886
- ['number', 'string'],
887
- [this.enginePtr, filterId]
1091
+ ['number'],
1092
+ [this.enginePtr]
888
1093
  );
889
- checkResult(result, `Failed to unregister filter: ${filterId}`);
1094
+ checkResult(result, 'Failed to clear sticker');
890
1095
  }
891
1096
 
892
1097
  /**
893
- * Unregisters all filters
1098
+ * Internal method to set a single intensity parameter
1099
+ * @private
894
1100
  */
895
- unregisterAllFilters() {
896
- this._ensureInitialized();
1101
+ _setIntensity(functionName, value) {
1102
+ if (value < 0 || value > 1) {
1103
+ throw new FacebetterError('Parameter value must be between 0.0 and 1.0');
1104
+ }
1105
+
897
1106
  const Module = this._getWasmModule();
898
1107
  const result = Module.ccall(
899
- 'UnregisterAllFilters',
1108
+ functionName,
900
1109
  'number',
901
- ['number'],
902
- [this.enginePtr]
1110
+ ['number', 'number'],
1111
+ [this.enginePtr, value]
903
1112
  );
904
- checkResult(result, 'Failed to unregister all filters');
1113
+
1114
+ checkResult(result, `Failed to set beauty parameter`);
905
1115
  }
906
1116
 
907
- /**
908
- * Unregisters a specific sticker
909
- * @param {string} stickerId - Sticker ID to unregister
910
- */
911
- unregisterSticker(stickerId) {
1117
+ _setMakeupStyle(cFuncName, value, errorMessage) {
912
1118
  this._ensureInitialized();
913
1119
  const Module = this._getWasmModule();
914
1120
  const result = Module.ccall(
915
- 'UnregisterSticker',
1121
+ cFuncName,
916
1122
  'number',
917
- ['number', 'string'],
918
- [this.enginePtr, stickerId]
1123
+ ['number', 'number'],
1124
+ [this.enginePtr, value]
919
1125
  );
920
- checkResult(result, `Failed to unregister sticker: ${stickerId}`);
1126
+ checkResult(result, errorMessage);
921
1127
  }
922
1128
 
923
1129
  /**
924
- * Unregisters all stickers
925
- */
926
- unregisterAllStickers() {
927
- this._ensureInitialized();
928
- const Module = this._getWasmModule();
929
- const result = Module.ccall(
930
- 'UnregisterAllStickers',
931
- 'number',
932
- ['number'],
933
- [this.enginePtr]
934
- );
935
- checkResult(result, 'Failed to unregister all stickers');
936
- }
937
-
938
- /**
939
- * Gets the list of registered filter IDs
940
- * @returns {string[]} Array of registered filter IDs
941
- */
942
- getRegisteredFilters() {
943
- this._ensureInitialized();
944
- const Module = this._getWasmModule();
945
- const jsonStr = Module.ccall(
946
- 'GetRegisteredFilters',
947
- 'string',
948
- ['number'],
949
- [this.enginePtr]
950
- );
951
- try {
952
- return JSON.parse(jsonStr || '[]');
953
- } catch (e) {
954
- logError(`Failed to parse registered filters JSON: ${e}`);
955
- return [];
956
- }
957
- }
958
-
959
- /**
960
- * Gets the list of registered sticker IDs
961
- * @returns {string[]} Array of registered sticker IDs
962
- */
963
- getRegisteredStickers() {
964
- this._ensureInitialized();
965
- const Module = this._getWasmModule();
966
- const jsonStr = Module.ccall(
967
- 'GetRegisteredStickers',
968
- 'string',
969
- ['number'],
970
- [this.enginePtr]
971
- );
972
- try {
973
- return JSON.parse(jsonStr || '[]');
974
- } catch (e) {
975
- logError(`Failed to parse registered stickers JSON: ${e}`);
976
- return [];
977
- }
978
- }
979
-
980
- /**
981
- * Internal method to set beauty parameters
1130
+ * Internal method to set beauty parameters
982
1131
  * @private
983
1132
  */
984
1133
  _setBeautyParam(functionName, param, value) {
985
- if (value < 0 || value > 1) {
986
- throw new FacebetterError('Parameter value must be between 0.0 and 1.0');
1134
+ if (value < -1 || value > 1) {
1135
+ throw new FacebetterError('Parameter value must be between -1.0 and 1.0');
987
1136
  }
988
1137
 
989
1138
  const Module = this._getWasmModule();
@@ -998,15 +1147,25 @@ class BeautyEffectEngine {
998
1147
  }
999
1148
 
1000
1149
  /**
1001
- * Sets engine callbacks (face landmarks detection)
1150
+ * Sets engine callbacks (face landmarks and engine events)
1002
1151
  * @param {Object} callbacks - Callback functions
1003
- * @param {Function} callbacks.onFaceLandmarks - Callback for face landmarks detection
1152
+ * @param {Function} [callbacks.onFaceLandmarks] - Callback for face landmarks detection
1153
+ * @param {Function} [callbacks.onEngineEvent] - Callback for license / init events (code, message)
1004
1154
  * @param {number} [callbacks.maxFaces=10] - Maximum number of faces to support (affects shared buffer size)
1005
1155
  */
1006
1156
  setCallbacks(callbacks) {
1007
1157
  this._ensureInitialized();
1008
1158
  const Module = this._getWasmModule();
1009
-
1159
+
1160
+ if (!Module._engine_event_callbacks) {
1161
+ Module._engine_event_callbacks = {};
1162
+ }
1163
+ if (callbacks && typeof callbacks.onEngineEvent === 'function') {
1164
+ Module._engine_event_callbacks[this.enginePtr] = callbacks.onEngineEvent;
1165
+ } else {
1166
+ delete Module._engine_event_callbacks[this.enginePtr];
1167
+ }
1168
+
1010
1169
  // Initialize callback storage (if it doesn't exist)
1011
1170
  if (!Module._face_landmarks_callbacks) {
1012
1171
  Module._face_landmarks_callbacks = [];
@@ -1022,9 +1181,9 @@ class BeautyEffectEngine {
1022
1181
 
1023
1182
  // Pre-allocate shared memory
1024
1183
  // Metadata: 2 ints (frame_number, face_count) = 8 bytes
1025
- // Face data: maxFaces * 343 floats * 4 bytes = maxFaces * 1372 bytes
1184
+ // Face data: maxFaces * 342 floats * 4 bytes = maxFaces * 1368 bytes
1026
1185
  const metadataSize = 2 * 4; // 2 ints
1027
- const faceDataSize = maxFaces * 343 * 4; // 343 floats per face
1186
+ const faceDataSize = maxFaces * 342 * 4; // 342 floats per face
1028
1187
  sharedBufferSize = metadataSize + faceDataSize;
1029
1188
  sharedBufferPtr = Module._malloc(sharedBufferSize);
1030
1189
 
@@ -1041,7 +1200,7 @@ class BeautyEffectEngine {
1041
1200
  const dataOffset = dataPtr / 4; // float is 4 bytes
1042
1201
 
1043
1202
  const results = [];
1044
- const FLOATS_PER_FACE = 343;
1203
+ const FLOATS_PER_FACE = 342;
1045
1204
 
1046
1205
  for (let i = 0; i < faceCount; i++) {
1047
1206
  const faceOffset = dataOffset + i * FLOATS_PER_FACE;
@@ -1057,7 +1216,6 @@ class BeautyEffectEngine {
1057
1216
 
1058
1217
  // Read basic fields
1059
1218
  const faceId = Math.round(heap[offset++]);
1060
- const faceAction = Math.round(heap[offset++]);
1061
1219
  const score = heap[offset++];
1062
1220
  const pitch = heap[offset++];
1063
1221
  const roll = heap[offset++];
@@ -1083,7 +1241,6 @@ class BeautyEffectEngine {
1083
1241
  key_points: keyPoints,
1084
1242
  visibility,
1085
1243
  face_id: faceId,
1086
- face_action: faceAction,
1087
1244
  score,
1088
1245
  pitch,
1089
1246
  roll,
@@ -1102,7 +1259,7 @@ class BeautyEffectEngine {
1102
1259
 
1103
1260
  // Call C API
1104
1261
  const result = Module.ccall(
1105
- 'SetCallbacks',
1262
+ 'fb_engine_set_wasm_callbacks',
1106
1263
  'number',
1107
1264
  ['number', 'number', 'number', 'number'],
1108
1265
  [
@@ -1131,55 +1288,81 @@ class BeautyEffectEngine {
1131
1288
  }
1132
1289
 
1133
1290
  /**
1134
- * Sets virtual background options (unified API, matches C++/Java/OC)
1135
- * @param {VirtualBackgroundOptions|Object} options - Virtual background options
1136
- * @param {number} [options.mode] - Background mode (use BackgroundMode enum)
1137
- * @param {ImageData|HTMLImageElement|HTMLCanvasElement} [options.backgroundImage] - Background image (required when mode is Image)
1291
+ * Enables virtual background blur.
1292
+ * Level is continuous in [0.0, 1.0] (downsample + mix, no shader rebuild).
1293
+ * @param {number} level - Blur strength in [0.0, 1.0]. 0 clears the virtual background.
1138
1294
  */
1139
- setVirtualBackground(options) {
1295
+ setVirtualBackgroundBlur(level) {
1140
1296
  this._ensureInitialized();
1141
1297
  const Module = this._getWasmModule();
1142
-
1143
- // Support both VirtualBackgroundOptions object and plain object
1144
- const opts = options.mode !== undefined ? options : { mode: BackgroundMode$1.None, backgroundImage: null };
1145
-
1146
- let imageData = null;
1147
- let imageBufferPtr = null;
1148
-
1149
- try {
1150
- // If a background image is provided, convert to ImageData and prepare buffer
1151
- if (opts.backgroundImage) {
1152
- imageData = this._toImageData(opts.backgroundImage);
1153
- const bufferSize = imageData.width * imageData.height * 4;
1154
- imageBufferPtr = Module._malloc(bufferSize);
1155
-
1156
- const buffer = this._getWasmBuffer();
1157
- const view = new Uint8Array(buffer, imageBufferPtr, bufferSize);
1158
- view.set(imageData.data);
1298
+ const result = Module.ccall(
1299
+ 'fb_set_virtual_background_blur',
1300
+ 'number',
1301
+ ['number', 'number'],
1302
+ [this.enginePtr, level]
1303
+ );
1304
+ checkResult(result, 'Failed to set virtual background blur');
1305
+ }
1306
+
1307
+ /**
1308
+ * Replaces the background with an image file path or encoded png/jpg bytes.
1309
+ * @param {string|Uint8Array} resource - Image path or Uint8Array data.
1310
+ */
1311
+ setVirtualBackground(resource) {
1312
+ this._ensureInitialized();
1313
+ const Module = this._getWasmModule();
1314
+
1315
+ if (typeof resource === 'string') {
1316
+ if (!resource) {
1317
+ throw new FacebetterError(
1318
+ 'Virtual background path must not be empty; use clearVirtualBackground()'
1319
+ );
1159
1320
  }
1160
-
1161
- // Use unified SetVirtualBackground C interface (consistent with other platforms)
1162
1321
  const result = Module.ccall(
1163
- 'SetVirtualBackground',
1322
+ 'fb_set_virtual_background',
1164
1323
  'number',
1165
- ['number', 'number', 'number', 'number', 'number', 'number'],
1166
- [
1167
- this.enginePtr,
1168
- opts.mode,
1169
- imageBufferPtr || 0, // If null, pass 0
1170
- imageData ? imageData.width : 0,
1171
- imageData ? imageData.height : 0,
1172
- imageData ? imageData.width * 4 : 0
1173
- ]
1324
+ ['number', 'string'],
1325
+ [this.enginePtr, resource]
1174
1326
  );
1175
-
1176
- checkResult(result, 'Failed to set virtual background');
1177
- } finally {
1178
- // Free memory
1179
- if (imageBufferPtr) {
1180
- Module._free(imageBufferPtr);
1327
+ checkResult(result, 'Failed to set virtual background from path');
1328
+ return;
1329
+ }
1330
+
1331
+ if (resource instanceof Uint8Array) {
1332
+ const dataPtr = Module._malloc(resource.length);
1333
+ Module.HEAPU8.set(resource, dataPtr);
1334
+ try {
1335
+ const result = Module.ccall(
1336
+ 'fb_set_virtual_background_data',
1337
+ 'number',
1338
+ ['number', 'number', 'number'],
1339
+ [this.enginePtr, dataPtr, resource.length]
1340
+ );
1341
+ checkResult(result, 'Failed to set virtual background from data');
1342
+ } finally {
1343
+ Module._free(dataPtr);
1181
1344
  }
1345
+ return;
1182
1346
  }
1347
+
1348
+ throw new FacebetterError(
1349
+ 'setVirtualBackground expects a file path string or Uint8Array'
1350
+ );
1351
+ }
1352
+
1353
+ /**
1354
+ * Clears virtual background (blur or image replacement).
1355
+ */
1356
+ clearVirtualBackground() {
1357
+ this._ensureInitialized();
1358
+ const Module = this._getWasmModule();
1359
+ const result = Module.ccall(
1360
+ 'fb_clear_virtual_background',
1361
+ 'number',
1362
+ ['number'],
1363
+ [this.enginePtr]
1364
+ );
1365
+ checkResult(result, 'Failed to clear virtual background');
1183
1366
  }
1184
1367
 
1185
1368
  /**
@@ -1187,12 +1370,12 @@ class BeautyEffectEngine {
1187
1370
  * When enabled, beauty effects will only be applied to detected skin areas.
1188
1371
  * @param {boolean} enabled - True to enable skin-only beauty, false to apply to entire image.
1189
1372
  */
1190
- setSkinOnlyBeauty(enabled) {
1373
+ setBeautySkinOnly(enabled) {
1191
1374
  this._ensureInitialized();
1192
1375
  const Module = this._getWasmModule();
1193
1376
 
1194
1377
  const result = Module.ccall(
1195
- 'SetSkinOnlyBeauty',
1378
+ 'fb_set_beauty_skin_only',
1196
1379
  'number',
1197
1380
  ['number', 'number'],
1198
1381
  [this.enginePtr, enabled ? 1 : 0]
@@ -1201,6 +1384,35 @@ class BeautyEffectEngine {
1201
1384
  checkResult(result, 'Failed to set skin only beauty');
1202
1385
  }
1203
1386
 
1387
+ /**
1388
+ * Gets engine performance statistics.
1389
+ * @returns {{fps: number, avgProcessTimeMs: number, sessionTimeS: number}}
1390
+ */
1391
+ getStats() {
1392
+ this._ensureInitialized();
1393
+ const Module = this._getWasmModule();
1394
+ const ptr = Module._malloc(24);
1395
+ try {
1396
+ const result = Module.ccall(
1397
+ 'fb_engine_get_stats',
1398
+ 'number',
1399
+ ['number', 'number'],
1400
+ [this.enginePtr, ptr]
1401
+ );
1402
+ checkResult(result, 'Failed to get stats');
1403
+ if ((ptr & 7) !== 0) {
1404
+ throw new FacebetterError('Unaligned stats buffer');
1405
+ }
1406
+ const base = ptr >> 3;
1407
+ return {
1408
+ fps: Module.HEAPF64[base],
1409
+ avgProcessTimeMs: Module.HEAPF64[base + 1],
1410
+ sessionTimeS: Module.HEAPF64[base + 2]
1411
+ };
1412
+ } finally {
1413
+ Module._free(ptr);
1414
+ }
1415
+ }
1204
1416
 
1205
1417
  /**
1206
1418
  * Ensures buffers are allocated for the given dimensions
@@ -1254,7 +1466,7 @@ class BeautyEffectEngine {
1254
1466
  srcView.set(imageData.data);
1255
1467
 
1256
1468
  const result = Module.ccall(
1257
- 'ProcessImageRGBA',
1469
+ 'fb_process_rgba',
1258
1470
  'number',
1259
1471
  ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
1260
1472
  [
@@ -1315,7 +1527,7 @@ class BeautyEffectEngine {
1315
1527
 
1316
1528
  try {
1317
1529
  const result = Module.ccall(
1318
- 'ProcessImageTexture',
1530
+ 'fb_process_texture',
1319
1531
  'number',
1320
1532
  ['number', 'number', 'number', 'number', 'number', 'number', 'number'],
1321
1533
  [
@@ -1355,37 +1567,37 @@ class BeautyEffectEngine {
1355
1567
  }
1356
1568
 
1357
1569
  /**
1358
- * Ensures gpupixel_canvas exists in the DOM (browser only)
1570
+ * Ensures facebetter_canvas exists in the DOM (browser only)
1359
1571
  * This canvas is required by GPUPixel for WebGL context creation
1360
1572
  * @private
1361
1573
  */
1362
- _ensureGPUPixelCanvas() {
1574
+ _ensureFacebetterCanvas() {
1363
1575
  if (typeof document === 'undefined') {
1364
1576
  return;
1365
1577
  }
1366
1578
 
1367
- let canvas = document.getElementById('gpupixel_canvas');
1579
+ let canvas = document.getElementById('facebetter_canvas');
1368
1580
  if (!canvas) {
1369
1581
  canvas = document.createElement('canvas');
1370
- canvas.id = 'gpupixel_canvas';
1582
+ canvas.id = 'facebetter_canvas';
1371
1583
  canvas.style.display = 'none';
1372
1584
  canvas.width = 1;
1373
1585
  canvas.height = 1;
1374
1586
  document.body.appendChild(canvas);
1375
- this._createdGPUPixelCanvas = true;
1587
+ this._createdFacebetterCanvas = true;
1376
1588
  }
1377
- this._gpupixelCanvas = canvas;
1589
+ this._facebetterCanvas = canvas;
1378
1590
  }
1379
1591
 
1380
1592
  /**
1381
- * Cleans up gpupixel_canvas if it was created by this engine instance (browser only)
1593
+ * Cleans up facebetter_canvas if it was created by this engine instance (browser only)
1382
1594
  * @private
1383
1595
  */
1384
- _cleanupGPUPixelCanvas() {
1385
- if (this._createdGPUPixelCanvas && this._gpupixelCanvas) {
1386
- this._gpupixelCanvas.remove();
1387
- this._gpupixelCanvas = null;
1388
- this._createdGPUPixelCanvas = false;
1596
+ _cleanupFacebetterCanvas() {
1597
+ if (this._createdFacebetterCanvas && this._facebetterCanvas) {
1598
+ this._facebetterCanvas.remove();
1599
+ this._facebetterCanvas = null;
1600
+ this._createdFacebetterCanvas = false;
1389
1601
  }
1390
1602
  }
1391
1603
 
@@ -1414,6 +1626,102 @@ class BeautyEffectEngine {
1414
1626
 
1415
1627
  }
1416
1628
 
1629
+ /** Cloudflare v2 online auth URL. Proxy through your server in production; never ship keys in the frontend bundle. */
1630
+ const FACEBETTER_AUTH_URL =
1631
+ 'https://facebetter.pixpark.net/facebetter/v2/auth';
1632
+
1633
+ function toHex(buffer) {
1634
+ return Array.from(new Uint8Array(buffer), (b) =>
1635
+ b.toString(16).padStart(2, '0')
1636
+ ).join('');
1637
+ }
1638
+
1639
+ async function hmacSha256Hex(key, message) {
1640
+ const encoder = new TextEncoder();
1641
+ const cryptoKey = await crypto.subtle.importKey(
1642
+ 'raw',
1643
+ encoder.encode(key),
1644
+ { name: 'HMAC', hash: 'SHA-256' },
1645
+ false,
1646
+ ['sign']
1647
+ );
1648
+ const signature = await crypto.subtle.sign(
1649
+ 'HMAC',
1650
+ cryptoKey,
1651
+ encoder.encode(message)
1652
+ );
1653
+ return toHex(signature);
1654
+ }
1655
+
1656
+ /**
1657
+ * Call Cloudflare v2/auth directly with app_id / app_key.
1658
+ * Intended for local debugging, or environments where key leakage is not a risk.
1659
+ *
1660
+ * @param {Object} options
1661
+ * @param {string} options.appId
1662
+ * @param {string} options.appKey
1663
+ * @param {Object} options.challenge WASM challenge `{nonce, timestamp, platform, user_agent}`
1664
+ * @param {string} [options.authUrl]
1665
+ * @returns {Promise<string>} Raw HTTP body (`{success, token}` or an error envelope)
1666
+ */
1667
+ async function requestFacebetterAuth({
1668
+ appId,
1669
+ appKey,
1670
+ challenge,
1671
+ authUrl = FACEBETTER_AUTH_URL,
1672
+ }) {
1673
+ const id = typeof appId === 'string' ? appId.trim() : '';
1674
+ const key = typeof appKey === 'string' ? appKey.trim() : '';
1675
+ if (!id || !key) {
1676
+ throw new FacebetterError(
1677
+ 'requestFacebetterAuth requires appId and appKey',
1678
+ 'LICENSE_ERROR'
1679
+ );
1680
+ }
1681
+ if (!challenge || typeof challenge !== 'object') {
1682
+ throw new FacebetterError(
1683
+ 'requestFacebetterAuth requires a WASM auth challenge',
1684
+ 'LICENSE_ERROR'
1685
+ );
1686
+ }
1687
+
1688
+ const nonce = String(challenge.nonce || '');
1689
+ const timestamp = Number(challenge.timestamp);
1690
+ const platform = String(challenge.platform || 'web');
1691
+ const userAgent = String(challenge.user_agent || '');
1692
+ const payload = `v2|${id}|${timestamp}|${nonce}|${platform}`;
1693
+ const hmac = await hmacSha256Hex(key, payload);
1694
+
1695
+ const response = await fetch(authUrl, {
1696
+ method: 'POST',
1697
+ headers: { 'Content-Type': 'application/json' },
1698
+ body: JSON.stringify({
1699
+ app_id: id,
1700
+ hmac_signature: hmac,
1701
+ timestamp,
1702
+ nonce,
1703
+ platform,
1704
+ user_agent: userAgent,
1705
+ }),
1706
+ });
1707
+ return response.text();
1708
+ }
1709
+
1710
+ /**
1711
+ * Build an `EngineConfig.fetchAuthResponse` that talks to Cloudflare directly
1712
+ * (no customer proxy).
1713
+ *
1714
+ * @param {Object} options
1715
+ * @param {string} options.appId
1716
+ * @param {string} options.appKey
1717
+ * @param {string} [options.authUrl]
1718
+ * @returns {(challenge: Object) => Promise<string>}
1719
+ */
1720
+ function createDirectAuthFetcher({ appId, appKey, authUrl } = {}) {
1721
+ return (challenge) =>
1722
+ requestFacebetterAuth({ appId, appKey, challenge, authUrl });
1723
+ }
1724
+
1417
1725
  /**
1418
1726
  * Browser-specific Image Utilities
1419
1727
  * Uses DOM APIs (canvas, ImageData, etc.)
@@ -1576,15 +1884,15 @@ function getWasmBuffer() {
1576
1884
  }
1577
1885
 
1578
1886
  // Browser-specific GPU canvas management
1579
- function ensureGPUPixelCanvas() {
1887
+ function ensureFacebetterCanvas() {
1580
1888
  if (typeof document === 'undefined') {
1581
1889
  return;
1582
1890
  }
1583
1891
 
1584
- let canvas = document.getElementById('gpupixel_canvas');
1892
+ let canvas = document.getElementById('facebetter_canvas');
1585
1893
  if (!canvas) {
1586
1894
  canvas = document.createElement('canvas');
1587
- canvas.id = 'gpupixel_canvas';
1895
+ canvas.id = 'facebetter_canvas';
1588
1896
  canvas.style.display = 'none';
1589
1897
  canvas.width = 1;
1590
1898
  canvas.height = 1;
@@ -1594,7 +1902,7 @@ function ensureGPUPixelCanvas() {
1594
1902
  }
1595
1903
  }
1596
1904
 
1597
- function cleanupGPUPixelCanvas() {
1905
+ function cleanupFacebetterCanvas() {
1598
1906
  // Cleanup is handled by engine instance
1599
1907
  }
1600
1908
 
@@ -1604,8 +1912,8 @@ const platformAPI = {
1604
1912
  getWasmModule,
1605
1913
  getWasmBuffer,
1606
1914
  toImageData: toImageData,
1607
- ensureGPUPixelCanvas,
1608
- cleanupGPUPixelCanvas
1915
+ ensureFacebetterCanvas,
1916
+ cleanupFacebetterCanvas
1609
1917
  };
1610
1918
 
1611
1919
  // Export factory function
@@ -1623,26 +1931,38 @@ class ESMBeautyEffectEngine extends BeautyEffectEngine {
1623
1931
  }
1624
1932
 
1625
1933
  // Export constants with proper names
1626
- const BeautyType = BeautyType$1;
1627
- const BasicParam = BasicParam$1;
1628
- const ReshapeParam = ReshapeParam$1;
1629
- const MakeupParam = MakeupParam$1;
1630
- const LipstickStyle = LipstickStyle$1;
1934
+ const WhiteningStyle = WhiteningStyle$1;
1935
+ const SmoothingStyle = SmoothingStyle$1;
1936
+ const Reshape = Reshape$1;
1937
+ const LipstickColor = LipstickColor$1;
1631
1938
  const BlushStyle = BlushStyle$1;
1632
- const ChromaKeyParam = ChromaKeyParam$1;
1633
- const BackgroundMode = BackgroundMode$1;
1939
+ const BlushColor = BlushColor$1;
1940
+ const ContourStyle = ContourStyle$1;
1941
+ const EyeShadowStyle = EyeShadowStyle$1;
1942
+ const EyeShadowColor = EyeShadowColor$1;
1943
+ const EyeLinerStyle = EyeLinerStyle$1;
1944
+ const EyeLinerColor = EyeLinerColor$1;
1945
+ const EyebrowStyle = EyebrowStyle$1;
1946
+ const EyebrowColor = EyebrowColor$1;
1947
+ const EyelashStyle = EyelashStyle$1;
1948
+ const EyelashColor = EyelashColor$1;
1949
+ const PupilColor = PupilColor$1;
1950
+ const ChromaKeyColor = ChromaKeyColor$1;
1634
1951
  const FrameType = FrameType$1;
1635
1952
  const MirrorMode = MirrorMode$1;
1636
- const VirtualBackgroundOptions = VirtualBackgroundOptions$1;
1953
+ const EngineEventCode = EngineEventCode$1;
1637
1954
 
1638
1955
  // Default export
1639
1956
  var index = {
1640
1957
  BeautyEffectEngine: ESMBeautyEffectEngine,
1641
1958
  EngineConfig,
1642
1959
  FacebetterError,
1960
+ FACEBETTER_AUTH_URL,
1961
+ createDirectAuthFetcher,
1962
+ requestFacebetterAuth,
1643
1963
  ...constants,
1644
1964
  loadWasmModule
1645
1965
  };
1646
1966
 
1647
- export { BackgroundMode, BasicParam, ESMBeautyEffectEngine as BeautyEffectEngine, BeautyType, BlushStyle, ChromaKeyParam, EngineConfig, FacebetterError, FrameType, LipstickStyle, MakeupParam, MirrorMode, ReshapeParam, VirtualBackgroundOptions, createBeautyEffectEngine, index as default, getWasmBuffer, getWasmModule, loadWasmModule };
1967
+ export { ESMBeautyEffectEngine as BeautyEffectEngine, BlushColor, BlushStyle, ChromaKeyColor, ContourStyle, EngineConfig, EngineEventCode, EyeLinerColor, EyeLinerStyle, EyeShadowColor, EyeShadowStyle, EyebrowColor, EyebrowStyle, EyelashColor, EyelashStyle, FACEBETTER_AUTH_URL, FacebetterError, FrameType, LipstickColor, MirrorMode, PupilColor, Reshape, SmoothingStyle, WhiteningStyle, createBeautyEffectEngine, createDirectAuthFetcher, index as default, getWasmBuffer, getWasmModule, loadWasmModule, requestFacebetterAuth };
1648
1968
  //# sourceMappingURL=facebetter.esm.js.map