facebetter 1.5.1 → 2.0.1

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,3 +1,40 @@
1
+ /**
2
+ * Engine Configuration
3
+ * Platform-agnostic configuration class
4
+ */
5
+
6
+ /**
7
+ * Engine configuration class
8
+ * Analogous to EngineConfig in C++ / Java / Objective-C. Web only accepts licenseToken.
9
+ */
10
+ class EngineConfig {
11
+ /**
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 {boolean} [config.externalContext] Kept for cross-platform alignment;
16
+ * unused on Web today.
17
+ */
18
+ constructor(config = {}) {
19
+ this.licenseToken = config.licenseToken || null;
20
+ this.resourcePath = '/resource.fbd';
21
+ /**
22
+ * Whether to use an external GL context (native platforms only).
23
+ * On Web this field is kept for config-shape alignment and has no effect.
24
+ */
25
+ this.externalContext = !!config.externalContext;
26
+ }
27
+
28
+ isValid() {
29
+ return typeof this.licenseToken === 'string' &&
30
+ this.licenseToken.trim() !== '';
31
+ }
32
+
33
+ toString() {
34
+ return `EngineConfig{mode='${this.isValid() ? 'licenseToken' : 'invalid'}'}`;
35
+ }
36
+ }
37
+
1
38
  /**
2
39
  * Facebetter Error Classes
3
40
  * Platform-agnostic error handling
@@ -26,257 +63,246 @@ function checkResult(result, errorMessage = 'Operation failed') {
26
63
  }
27
64
 
28
65
  /**
29
- * Engine Configuration
30
- * Platform-agnostic configuration class
66
+ * Facebetter Constants and Enums
67
+ * Platform-agnostic constants. Ordinals match facebetter::beauty_params.
31
68
  */
32
69
 
70
+ const WhiteningStyle$1 = {
71
+ ColdWhite: 0, // Cool white
72
+ PinkWhite: 1, // Pink white
73
+ WarmWhite: 2, // Warm white
74
+ Wheat: 3, // Light wheat / olive
75
+ Tan: 4 // Tan / bronzed
76
+ };
77
+
78
+ const SmoothingStyle$1 = {
79
+ Natural: 0, // Natural: keeps pores
80
+ Texture: 1, // Cleaner skin while retaining texture
81
+ Smooth: 2 // Creamy / porcelain finish
82
+ };
33
83
 
34
84
  /**
35
- * Engine configuration class
36
- * Similar to EngineConfig struct in C++/Java/OC
85
+ * Face reshape parameters.
86
+ * Values match facebetter::beauty_params::Reshape.
87
+ * Intensity range is [-1.0, 1.0]; 0 is off. Comments list + / - directions.
37
88
  */
38
- class EngineConfig {
39
- /**
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)
45
- */
46
- constructor(config = {}) {
47
- this.appId = config.appId || null;
48
- this.appKey = config.appKey || null;
49
- this.licenseJson = config.licenseJson || null;
50
- this.resourcePath = '/resource.fbd';
51
- /**
52
- * 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.
55
- */
56
- this.externalContext = !!config.externalContext;
57
- }
89
+ const Reshape$1 = {
90
+ FaceThin: 0, // +slim face / -fuller cheeks
91
+ FaceVShape: 1, // +V-shape jaw / -square jaw
92
+ FaceNarrow: 2, // +narrow face / -wider face
93
+ FaceShort: 3, // +shorter face / -longer face
94
+ Cheekbone: 4, // +slim cheekbones / -wider cheekbones
95
+ Jawbone: 5, // +slim jaw / -wider jaw
96
+ Chin: 6, // +longer chin / -shorter chin
97
+ NoseSlim: 7, // +slimmer nose / -wider nose
98
+ EyeSize: 8, // +larger eyes / -smaller eyes
99
+ EyeDistance: 9, // +wider eye spacing / -closer eyes
100
+ FaceSmall: 10, // +smaller face / -larger face
101
+ Forehead: 11, // +fuller forehead / -lower forehead
102
+ NoseLong: 12, // +longer nose / -shorter nose
103
+ Philtrum: 13, // +shorter philtrum / -longer philtrum
104
+ MouthSize: 14, // +larger mouth / -smaller mouth
105
+ MouthPosition: 15, // +mouth lower / -mouth higher
106
+ MouthSmile: 16, // +smile lift / -droop corners
107
+ LipThickness: 17, // +thicker lips / -thinner lips
108
+ EyeRound: 18, // +rounder eyes / -narrower eyes
109
+ EyePosition: 19, // +eyes lower / -eyes higher
110
+ EyeAngle: 20, // +outer corner up / -outer corner down
111
+ EyeCornerOpen: 21, // +open eye corners / -close eye corners
112
+ LowerEyelid: 22, // +lower eyelid down / -lift lower eyelid
113
+ BrowPosition: 23, // +brows higher / -brows lower
114
+ BrowDistance: 24, // +wider brow spacing / -closer brows
115
+ BrowThickness: 25 // +thicker brows / -thinner brows
116
+ };
58
117
 
59
- /**
60
- * Validates the configuration
61
- * @returns {boolean} True if valid
62
- */
63
- isValid() {
64
- // If licenseJson is provided, use it for validation
65
- if (this.licenseJson) {
66
- return typeof this.licenseJson === 'string' && this.licenseJson.trim() !== '';
67
- }
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() !== '';
71
- }
118
+ const EngineEventCode$1 = {
119
+ LicenseValidationSuccess: 0,
120
+ LicenseValidationFailed: 1,
121
+ InitializationComplete: 100,
122
+ InitializationFailed: 101
123
+ };
72
124
 
73
- /**
74
- * Returns a string representation of the config
75
- * @returns {string} String representation
76
- */
77
- 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'}}`;
79
- }
80
- }
125
+ const LipstickColor$1 = {
126
+ Rouge: 0, // Classic rose
127
+ RetroRed: 1, // Retro red
128
+ Peach: 2, // Peach
129
+ CoralOrange: 3, // Coral orange
130
+ GentlePink: 4, // Soft pink
131
+ VitalityOrange: 5 // Bright orange
132
+ };
81
133
 
82
- /**
83
- * Facebetter Constants and Enums
84
- * Platform-agnostic constants
85
- */
134
+ const BlushStyle$1 = {
135
+ SunKissed: 0, // Sun-kissed sweep across cheekbones and bridge
136
+ Igari: 1, // Flushed band across the nose bridge
137
+ Soft: 2, // Soft dual cheeks
138
+ Apple: 3, // Round apple cheeks
139
+ Classic: 4, // Classic two spots
140
+ Doll: 5, // Cheeks + nose tip
141
+ Rose: 6 // Rose dual cheeks
142
+ };
86
143
 
87
- /**
88
- * Beauty type enumeration
89
- */
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
144
+ const BlushColor$1 = {
145
+ CoralPink: 0, // Coral pink
146
+ DustyRose: 1, // Dusty rose
147
+ VividRed: 2, // Vivid red
148
+ Berry: 3, // Berry
149
+ SunsetOrange: 4 // Sunset orange
98
150
  };
99
151
 
100
- /**
101
- * Basic beauty parameter enumeration
102
- * All values should be in range [0.0, 1.0]
103
- */
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)
152
+ const ContourStyle$1 = {
153
+ Natural: 0, // Soft everyday contour
154
+ Sculpt: 1, // Deeper sculpted contour
155
+ Glow: 2, // Highlight-focused
156
+ Slim: 3, // Slim cheekbones
157
+ Nose: 4, // Nose bridge lift
158
+ Glam: 5 // Spot highlights
109
159
  };
110
160
 
111
- /**
112
- * Face reshape parameter enumeration
113
- * All values should be in range [0.0, 1.0]
114
- */
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)
161
+ const EyeShadowStyle$1 = {
162
+ Soft: 0, // Soft wash
163
+ Crease: 1, // Crease deepen
164
+ Smoky: 2, // Smoky surround
165
+ Halo: 3, // Halo around the eye
166
+ Glow: 4, // Outer-corner glow
167
+ Drama: 5, // Full dramatic cover
168
+ Warm: 6 // Warm-tone wash
126
169
  };
127
170
 
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)
171
+ const EyeShadowColor$1 = {
172
+ Plum: 0, // Plum / mauve (default)
173
+ Brown: 1, // Warm brown
174
+ Gold: 2, // Soft gold
175
+ Pink: 3 // Dusty pink
135
176
  };
136
177
 
137
- /**
138
- * Lipstick style types
139
- */
140
- const LipstickStyle$1 = {
141
- Rouge: 0, // Rose red
142
- Coral: 1, // Coral
143
- Pink: 2 // Pink
178
+ const EyeLinerStyle$1 = {
179
+ Classic: 0, // Full liner with winged tip
180
+ Flick: 1, // Extended outer flick
181
+ CatEye: 2, // Short cat-eye lift
182
+ Natural: 3, // Thin soft natural line
183
+ Bold: 4, // Bold cover
184
+ Soft: 5 // Soft feathered wing
144
185
  };
145
186
 
146
- /**
147
- * Blush style types
148
- */
149
- const BlushStyle$1 = {
150
- Classic: 0, // Classic
151
- Peach: 1, // Peach
152
- Rose: 2 // Rose
187
+ const EyeLinerColor$1 = {
188
+ Burgundy: 0, // Burgundy brown
189
+ Plum: 1, // Deep plum
190
+ Chocolate: 2, // Chocolate brown
191
+ Coffee: 3, // Near-black coffee
192
+ Mauve: 4 // Mauve grey
153
193
  };
154
194
 
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)
195
+ const EyebrowStyle$1 = {
196
+ Natural: 0, // Natural arch with hair strokes
197
+ Soft: 1, // Soft powdery thick brow
198
+ Feathered: 2, // Feathered hair strokes
199
+ Mist: 3, // Light powder mist
200
+ Arched: 4, // Classic high arch
201
+ Powder: 5, // Dense powder fill
202
+ Wild: 6, // Wispy upper edge
203
+ Full: 7, // Full balanced brow
204
+ Straight: 8 // Straight low arch
163
205
  };
164
206
 
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
207
+ const EyebrowColor$1 = {
208
+ DarkBrown: 0, // Dark brown (default)
209
+ Black: 1, // Black
210
+ SoftBrown: 2 // Soft brown
172
211
  };
173
212
 
174
- /**
175
- * Frame type enumeration
176
- */
177
- const FrameType$1 = {
178
- Image: 0, // Image mode
179
- Video: 1 // Video mode
213
+ const EyelashStyle$1 = {
214
+ Classic: 0, // Balanced clusters + lower lashes
215
+ Manga: 1, // Bold spiked clusters
216
+ Winged: 2, // Extended outer corner
217
+ Wispy: 3, // Fluffy lifted tips
218
+ Clustered: 4, // Distinct clusters
219
+ Doll: 5 // Short doll lashes
180
220
  };
181
221
 
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
222
+ const EyelashColor$1 = {
223
+ Black: 0, // Near-black (default)
224
+ Brown: 1, // Soft brown
225
+ SoftBlack: 2 // Slightly softer black
190
226
  };
191
227
 
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
- }
228
+ const PupilColor$1 = {
229
+ Hazel: 0, // Amber / honey brown
230
+ Ice: 1, // Icy blue
231
+ Mocha: 2, // Dark brown with sparkle
232
+ Olive: 3, // Forest green
233
+ Gloss: 4, // Glossy dark brown
234
+ Moss: 5, // Muted olive
235
+ Sand: 6, // Sandy beige brown
236
+ Glow: 7, // Lower-iris crescent highlight
237
+ Slate: 8 // Cool blue-grey
238
+ };
207
239
 
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
- }
240
+ const ChromaKeyColor$1 = {
241
+ Green: 0,
242
+ Blue: 1,
243
+ Red: 2
244
+ };
245
+
246
+ /** Frame type for processing (affects temporal smoothing / tracking). */
247
+ const FrameType$1 = {
248
+ Image: 0, // Still image
249
+ Video: 1 // Continuous video stream
250
+ };
251
+
252
+ /** Mirror mode applied to the input before processing. */
253
+ const MirrorMode$1 = {
254
+ None: 0, // No mirror
255
+ Horizontal: 1, // Horizontal (e.g. front-camera selfie)
256
+ Vertical: 2, // Vertical
257
+ Both: 3 // Both axes
218
258
  };
219
259
 
220
260
  var constants = /*#__PURE__*/Object.freeze({
221
261
  __proto__: null,
222
- BackgroundMode: BackgroundMode$1,
223
- BasicParam: BasicParam$1,
224
- BeautyType: BeautyType$1,
262
+ BlushColor: BlushColor$1,
225
263
  BlushStyle: BlushStyle$1,
226
- ChromaKeyParam: ChromaKeyParam$1,
264
+ ChromaKeyColor: ChromaKeyColor$1,
265
+ ContourStyle: ContourStyle$1,
266
+ EngineEventCode: EngineEventCode$1,
267
+ EyeLinerColor: EyeLinerColor$1,
268
+ EyeLinerStyle: EyeLinerStyle$1,
269
+ EyeShadowColor: EyeShadowColor$1,
270
+ EyeShadowStyle: EyeShadowStyle$1,
271
+ EyebrowColor: EyebrowColor$1,
272
+ EyebrowStyle: EyebrowStyle$1,
273
+ EyelashColor: EyelashColor$1,
274
+ EyelashStyle: EyelashStyle$1,
227
275
  FrameType: FrameType$1,
228
- LipstickStyle: LipstickStyle$1,
229
- MakeupParam: MakeupParam$1,
276
+ LipstickColor: LipstickColor$1,
230
277
  MirrorMode: MirrorMode$1,
231
- ReshapeParam: ReshapeParam$1,
232
- VirtualBackgroundOptions: VirtualBackgroundOptions$1
278
+ PupilColor: PupilColor$1,
279
+ Reshape: Reshape$1,
280
+ SmoothingStyle: SmoothingStyle$1,
281
+ WhiteningStyle: WhiteningStyle$1
233
282
  });
234
283
 
235
284
  /**
236
- * Browser-side console lines aligned with C++ Logger (src/base/logging.cc) Release pattern:
237
- * [%Y-%m-%d %H:%M:%S.%e] [facebetter] [%l] [%t] - %v
238
- * spdlog level names: info, warning, error (see spdlog SPDLOG_LEVEL_NAMES).
285
+ * Beauty Effect Engine Core
286
+ * Platform-agnostic engine implementation with dependency injection
239
287
  */
240
288
 
241
- function pad2(n) {
242
- return String(n).padStart(2, '0');
243
- }
244
-
245
- /**
246
- * @param {'info'|'warning'|'error'} levelTag — must match spdlog long level strings
247
- * @param {string} message
248
- * @returns {string}
249
- */
250
- function formatLogLine(levelTag, message) {
251
- const d = new Date();
252
- const ms = String(d.getMilliseconds()).padStart(3, '0');
253
- const ts = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(
254
- d.getHours()
255
- )}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${ms}`;
256
- return `[${ts}] [facebetter] [${levelTag}] [0] - ${message}`;
257
- }
258
289
 
259
- /** @param {string} message */
260
- function logInfo(message) {
261
- console.info(formatLogLine('info', message));
262
- }
290
+ /** Byte layouts of fb_engine_config_t / fb_log_config_t on wasm32. */
291
+ const FB_ENGINE_CONFIG_SIZE = 28;
292
+ const FB_LOG_CONFIG_SIZE = 16;
263
293
 
264
- /** @param {string} message */
265
- function logWarn(message) {
266
- console.warn(formatLogLine('warning', message));
294
+ function allocUtf8(Module, text) {
295
+ const value = text || '';
296
+ const len = Module.lengthBytesUTF8(value) + 1;
297
+ const ptr = Module._malloc(len);
298
+ Module.stringToUTF8(value, ptr, len);
299
+ return ptr;
267
300
  }
268
301
 
269
- /** @param {string} message */
270
- function logError(message) {
271
- console.error(formatLogLine('error', message));
302
+ function writeI32(Module, ptr, index, value) {
303
+ Module.HEAP32[(ptr >> 2) + index] = value;
272
304
  }
273
305
 
274
- /**
275
- * Beauty Effect Engine Core
276
- * Platform-agnostic engine implementation with dependency injection
277
- */
278
-
279
-
280
306
  /**
281
307
  * Beauty effect engine class
282
308
  * Provides high-level API for face beauty effects processing
@@ -291,9 +317,8 @@ class BeautyEffectEngine {
291
317
  * @param {Function} platformAPI.getWasmModule - Get WASM module instance
292
318
  * @param {Function} platformAPI.getWasmBuffer - Get WASM memory buffer
293
319
  * @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)
320
+ * @param {Function} [platformAPI.ensureFacebetterCanvas] - Ensure GPU canvas exists (browser only)
321
+ * @param {Function} [platformAPI.cleanupFacebetterCanvas] - Cleanup GPU canvas (browser only)
297
322
  */
298
323
  constructor(config, platformAPI) {
299
324
  if (!config) {
@@ -319,9 +344,7 @@ class BeautyEffectEngine {
319
344
  }
320
345
 
321
346
  this.config = engineConfig;
322
- this.appId = engineConfig.appId;
323
- this.appKey = engineConfig.appKey;
324
- this.licenseJson = engineConfig.licenseJson;
347
+ this.licenseToken = engineConfig.licenseToken;
325
348
  this.resourcePath = '/resource.fbd';
326
349
  this.enginePtr = null;
327
350
  this.initialized = false;
@@ -339,22 +362,22 @@ class BeautyEffectEngine {
339
362
  this._getWasmModule = platformAPI.getWasmModule;
340
363
  this._getWasmBuffer = platformAPI.getWasmBuffer;
341
364
  this._toImageData = platformAPI.toImageData;
342
- this._ensureGPUPixelCanvas = platformAPI.ensureGPUPixelCanvas;
343
- this._cleanupGPUPixelCanvas = platformAPI.cleanupGPUPixelCanvas;
344
-
365
+ this._ensureFacebetterCanvas = platformAPI.ensureFacebetterCanvas;
366
+ this._cleanupFacebetterCanvas = platformAPI.cleanupFacebetterCanvas;
367
+ this._pendingLogConfig = null;
368
+
345
369
  // Browser-specific state
346
- this._gpupixelCanvas = null;
347
- this._createdGPUPixelCanvas = false;
370
+ this._facebetterCanvas = null;
371
+ this._createdFacebetterCanvas = false;
348
372
  this._offscreenCanvas = null;
349
373
  this._offscreenCtx = null;
350
374
 
351
- // Automatically create gpupixel_canvas if it doesn't exist (browser only)
352
- if (this._ensureGPUPixelCanvas) {
353
- this._ensureGPUPixelCanvas();
375
+ // Automatically create facebetter_canvas if it doesn't exist (browser only)
376
+ if (this._ensureFacebetterCanvas) {
377
+ this._ensureFacebetterCanvas();
354
378
  }
355
-
356
- // Start loading WASM module immediately in constructor
357
- this._wasmLoadPromise = this._loadWasmModule();
379
+
380
+ this._wasmLoadPromise = null;
358
381
  this._initPromise = null;
359
382
  }
360
383
 
@@ -411,8 +434,12 @@ class BeautyEffectEngine {
411
434
  /**
412
435
  * Initializes the engine
413
436
  * @param {Object} [options] - Initialization options
414
- * @param {number} [options.timeout] - Timeout in milliseconds (default: 30000 for WASM, 10000 for auth)
415
- * @param {number} [options.authTimeout] - Timeout for online authentication in milliseconds (default: 10000)
437
+ * @param {number} [options.timeout] - Timeout in milliseconds (default: 120000)
438
+ * @param {number} [options.authTimeout] - Unused. Kept for compatibility.
439
+ * @param {function} [options.onProgress] - Download progress:
440
+ * `{ loaded, total, percent }` while fetching the runtime files.
441
+ * @param {string} [options.assetBaseUrl] - Directory that contains
442
+ * `facebetter-core.wasm` and `resource.fbd`. Optional when using npm.
416
443
  * @returns {Promise<void>} Promise that resolves when initialization is complete
417
444
  */
418
445
  async init(options = {}) {
@@ -429,9 +456,18 @@ class BeautyEffectEngine {
429
456
  // Create initialization Promise
430
457
  this._initPromise = (async () => {
431
458
  try {
432
- const wasmTimeout = options.timeout || 30000;
459
+ const wasmTimeout = options.timeout || 120000;
460
+
461
+ if (!this._wasmLoadPromise) {
462
+ this._wasmLoadPromise = this._loadWasmModule({
463
+ onProgress: options.onProgress,
464
+ assetBaseUrl: options.assetBaseUrl,
465
+ wasmUrl: options.wasmUrl,
466
+ wasmAssetUrl: options.wasmAssetUrl,
467
+ resourceAssetUrl: options.resourceAssetUrl,
468
+ });
469
+ }
433
470
 
434
- // Wait for WASM module loading (with timeout)
435
471
  try {
436
472
  await Promise.race([
437
473
  this._wasmLoadPromise,
@@ -441,12 +477,21 @@ class BeautyEffectEngine {
441
477
  throw this._enhanceError(error, 'Failed to load WASM module');
442
478
  }
443
479
 
480
+ if (this._pendingLogConfig) {
481
+ await this._applyLogConfig(this._pendingLogConfig);
482
+ this._pendingLogConfig = null;
483
+ }
484
+
444
485
  const Module = this._getWasmModule();
445
486
 
446
487
  // Setup usage report proxy for WASM
447
488
  if (!Module.onReportUsage) {
448
489
  Module.onReportUsage = async (payloadJson) => {
449
490
  try {
491
+ const payload = JSON.parse(payloadJson);
492
+ if (!payload.app_id || !payload.hmac_signature) {
493
+ return true;
494
+ }
450
495
  const url = 'https://facebetter.pixpark.net/facebetter/v1/report';
451
496
  const response = await fetch(url, {
452
497
  method: 'POST',
@@ -462,48 +507,43 @@ class BeautyEffectEngine {
462
507
  };
463
508
  }
464
509
 
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}`);
480
- return '';
481
- }
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
- };
510
+ // Web: fetch the token outside the engine, then pass licenseToken.
511
+ if (!this.licenseToken) {
512
+ throw new FacebetterError(
513
+ 'Web auth requires licenseToken. Fetch it from your server before init().',
514
+ 'LICENSE_ERROR'
515
+ );
492
516
  }
493
517
 
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
- );
518
+ // C ABI still has app_id/app_key slots; leave them empty on Web.
519
+ const appIdPtr = allocUtf8(Module, '');
520
+ const appKeyPtr = allocUtf8(Module, '');
521
+ const licensePtr = allocUtf8(Module, this.licenseToken);
522
+ const resourcePtr = allocUtf8(Module, this.resourcePath);
523
+ const configPtr = Module._malloc(FB_ENGINE_CONFIG_SIZE);
524
+ writeI32(Module, configPtr, 0, appIdPtr);
525
+ writeI32(Module, configPtr, 1, appKeyPtr);
526
+ writeI32(Module, configPtr, 2, licensePtr);
527
+ writeI32(Module, configPtr, 3, resourcePtr);
528
+ writeI32(Module, configPtr, 4, 0);
529
+ writeI32(Module, configPtr, 5, this.config.externalContext ? 1 : 0);
530
+ writeI32(Module, configPtr, 6, 0);
531
+
532
+ let enginePtr = 0;
533
+ try {
534
+ enginePtr = Module.ccall(
535
+ 'fb_engine_create',
536
+ 'number',
537
+ ['number'],
538
+ [configPtr]
539
+ );
540
+ } finally {
541
+ Module._free(configPtr);
542
+ Module._free(appIdPtr);
543
+ Module._free(appKeyPtr);
544
+ Module._free(licensePtr);
545
+ Module._free(resourcePtr);
546
+ }
507
547
 
508
548
  if (!enginePtr) {
509
549
  throw new FacebetterError(
@@ -516,7 +556,8 @@ class BeautyEffectEngine {
516
556
  this.initialized = true;
517
557
  this._initPromise = null; // Clear Promise cache, allow re-initialization (if needed)
518
558
  } catch (error) {
519
- this._initPromise = null; // Clear Promise cache, allow retry
559
+ this._initPromise = null;
560
+ this._wasmLoadPromise = null;
520
561
  throw error;
521
562
  }
522
563
  })();
@@ -551,7 +592,10 @@ class BeautyEffectEngine {
551
592
  this._callbackSharedBufferSize = 0;
552
593
  }
553
594
 
554
- Module.ccall('DestroyBeautyEffectEngine', null, ['number'], [this.enginePtr]);
595
+ Module.ccall('fb_engine_destroy', null, ['number'], [this.enginePtr]);
596
+ if (Module._engine_event_callbacks) {
597
+ delete Module._engine_event_callbacks[this.enginePtr];
598
+ }
555
599
  this.enginePtr = null;
556
600
  this.initialized = false;
557
601
  this.bufferSize = 0;
@@ -563,15 +607,16 @@ class BeautyEffectEngine {
563
607
  this._offscreenCtx = null;
564
608
  }
565
609
 
566
- // Clean up gpupixel_canvas if we created it
567
- if (this._cleanupGPUPixelCanvas) {
568
- this._cleanupGPUPixelCanvas();
610
+ // Clean up facebetter_canvas if we created it
611
+ if (this._cleanupFacebetterCanvas) {
612
+ this._cleanupFacebetterCanvas();
569
613
  }
570
614
  }
571
615
 
572
616
  /**
573
- * Sets log configuration
574
- * Can be called before init() since SetLogConfig is a global function
617
+ * Sets log configuration.
618
+ * If called before `init()`, the values are applied after the runtime downloads
619
+ * and before the engine is created.
575
620
  * @param {Object} config - Log configuration
576
621
  * @param {boolean} config.consoleEnabled - Enable console logging
577
622
  * @param {boolean} config.fileEnabled - Enable file logging
@@ -580,410 +625,426 @@ class BeautyEffectEngine {
580
625
  * @returns {Promise<void>} Promise that resolves when log config is set
581
626
  */
582
627
  async setLogConfig(config) {
583
- // Wait for WASM module to be loaded (started in constructor)
628
+ if (!this._wasmLoadPromise) {
629
+ this._pendingLogConfig = config;
630
+ return;
631
+ }
584
632
  await this._wasmLoadPromise;
633
+ await this._applyLogConfig(config);
634
+ }
635
+
636
+ async _applyLogConfig(config) {
585
637
  const Module = this._getWasmModule();
586
638
 
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
- );
639
+ const fileNamePtr = allocUtf8(Module, config.fileName);
640
+ const configPtr = Module._malloc(FB_LOG_CONFIG_SIZE);
641
+ writeI32(Module, configPtr, 0, config.consoleEnabled ? 1 : 0);
642
+ writeI32(Module, configPtr, 1, config.fileEnabled ? 1 : 0);
643
+ writeI32(Module, configPtr, 2, config.level || 0);
644
+ writeI32(Module, configPtr, 3, fileNamePtr);
645
+
646
+ let result;
647
+ try {
648
+ result = Module.ccall(
649
+ 'fb_set_log_config',
650
+ 'number',
651
+ ['number'],
652
+ [configPtr]
653
+ );
654
+ } finally {
655
+ Module._free(configPtr);
656
+ Module._free(fileNamePtr);
657
+ }
598
658
 
599
659
  checkResult(result, 'Failed to set log config');
600
660
  }
601
661
 
602
662
  /**
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
663
+ * Sets skin smoothing intensity
664
+ * @param {number} value - Parameter value (0.0 - 1.0)
611
665
  */
612
- setBeautyTypeEnabled(beautyType, enabled) {
666
+ setSmoothing(value) {
613
667
  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
- );
668
+ this._setIntensity('fb_set_smoothing', value);
669
+ }
622
670
 
623
- checkResult(result, `Failed to ${enabled ? 'enable' : 'disable'} beauty type`);
671
+ /**
672
+ * Sets skin smoothing style
673
+ * @param {number} style - Style (use SmoothingStyle enum, e.g., SmoothingStyle.Natural)
674
+ */
675
+ setSmoothingStyle(style) {
676
+ this._setMakeupStyle('fb_set_smoothing_style', style, 'Failed to set smoothing style');
624
677
  }
625
678
 
626
679
  /**
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
680
+ * Sets skin whitening intensity
681
+ * @param {number} value - Parameter value (0.0 - 1.0)
633
682
  */
634
- isBeautyTypeEnabled(beautyType) {
683
+ setWhitening(value) {
635
684
  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
- }
685
+ this._setIntensity('fb_set_whitening', value);
686
+ }
648
687
 
649
- return result === 1;
688
+ /**
689
+ * Sets whitening style by swapping the custom LUT
690
+ * @param {number} style - Style (use WhiteningStyle enum, e.g., WhiteningStyle.ColdWhite)
691
+ */
692
+ setWhiteningStyle(style) {
693
+ this._setMakeupStyle('fb_set_whitening_style', style, 'Failed to set whitening style');
650
694
  }
651
695
 
652
696
  /**
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
697
+ * Sets image sharpening intensity
698
+ * @param {number} value - Parameter value (0.0 - 1.0)
657
699
  */
658
- disableAllBeautyTypes() {
700
+ setSharpening(value) {
659
701
  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');
702
+ this._setIntensity('fb_set_sharpening', value);
670
703
  }
671
704
 
672
705
  /**
673
- * Sets a basic beauty parameter
674
- * @param {number} param - Parameter (use BasicParam enum, e.g., BasicParam.Smoothing)
706
+ * Sets skin rosiness intensity
675
707
  * @param {number} value - Parameter value (0.0 - 1.0)
676
708
  */
677
- setBasicParam(param, value) {
709
+ setRosiness(value) {
678
710
  this._ensureInitialized();
679
- this._setBeautyParam('SetBeautyParamBasic', param, value);
711
+ this._setIntensity('fb_set_rosiness', value);
680
712
  }
681
713
 
682
714
  /**
683
715
  * 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)
716
+ * @param {number} param - Parameter (use Reshape enum, e.g., Reshape.FaceThin)
717
+ * @param {number} value - Parameter value in [-1.0, 1.0]. 0 is off.
686
718
  */
687
- setReshapeParam(param, value) {
719
+ setReshape(param, value) {
688
720
  this._ensureInitialized();
689
- this._setBeautyParam('SetBeautyParamReshape', param, value);
721
+ this._setBeautyParam('fb_set_reshape', param, value);
690
722
  }
691
723
 
692
724
  /**
693
- * Sets a makeup parameter
694
- * @param {number} param - Parameter (use MakeupParam enum, e.g., MakeupParam.Lipstick)
725
+ * Sets lipstick intensity
695
726
  * @param {number} value - Parameter value (0.0 - 1.0)
696
727
  */
697
- setMakeupParam(param, value) {
728
+ setLipstick(value) {
698
729
  this._ensureInitialized();
699
- this._setBeautyParam('SetBeautyParamMakeup', param, value);
730
+ this._setIntensity('fb_set_lipstick', value);
700
731
  }
701
732
 
702
733
  /**
703
- * Sets lipstick style/texture
704
- * @param {number} style - Style (use LipstickStyle enum, e.g., LipstickStyle.Rouge)
734
+ * Sets lipstick colour preset
735
+ * @param {number} style - Style (use LipstickColor enum, e.g., LipstickColor.Rouge)
705
736
  */
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');
737
+ setLipstickColor(style) {
738
+ this._setMakeupStyle('fb_set_lipstick_color', style, 'Failed to set lipstick style');
716
739
  }
717
740
 
718
741
  /**
719
- * Sets blush style/texture
720
- * @param {number} style - Style (use BlushStyle enum, e.g., BlushStyle.Classic)
742
+ * Sets blush intensity
743
+ * @param {number} value - Parameter value (0.0 - 1.0)
721
744
  */
745
+ setBlush(value) {
746
+ this._ensureInitialized();
747
+ this._setIntensity('fb_set_blush', value);
748
+ }
749
+
722
750
  setBlushStyle(style) {
751
+ this._setMakeupStyle('fb_set_blush_style', style, 'Failed to set blush style');
752
+ }
753
+
754
+ setBlushColor(color) {
755
+ this._setMakeupStyle('fb_set_blush_color', color, 'Failed to set blush color');
756
+ }
757
+
758
+ setContour(value) {
723
759
  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');
760
+ this._setIntensity('fb_set_contour', value);
732
761
  }
733
762
 
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) {
763
+ setContourStyle(style) {
764
+ this._setMakeupStyle('fb_set_contour_style', style, 'Failed to set contour style');
765
+ }
766
+
767
+ setEyeShadow(value) {
740
768
  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);
769
+ this._setIntensity('fb_set_eye_shadow', value);
770
+ }
771
+
772
+ setEyeShadowStyle(style) {
773
+ this._setMakeupStyle('fb_set_eye_shadow_style', style, 'Failed to set eyeshadow style');
774
+ }
775
+
776
+ setEyeShadowColor(style) {
777
+ this._setMakeupStyle('fb_set_eye_shadow_color', style, 'Failed to set eyeshadow style');
778
+ }
779
+
780
+ setEyeLiner(value) {
781
+ this._ensureInitialized();
782
+ this._setIntensity('fb_set_eye_liner', value);
783
+ }
784
+
785
+ setEyeLinerStyle(style) {
786
+ this._setMakeupStyle('fb_set_eye_liner_style', style, 'Failed to set eyeliner style');
787
+ }
788
+
789
+ setEyeLinerColor(style) {
790
+ this._setMakeupStyle('fb_set_eye_liner_color', style, 'Failed to set eyeliner style');
791
+ }
792
+
793
+ setEyebrow(value) {
794
+ this._ensureInitialized();
795
+ this._setIntensity('fb_set_eyebrow', value);
796
+ }
797
+
798
+ setEyebrowStyle(style) {
799
+ this._setMakeupStyle('fb_set_eyebrow_style', style, 'Failed to set eyebrow style');
800
+ }
801
+
802
+ setEyebrowColor(style) {
803
+ this._setMakeupStyle('fb_set_eyebrow_color', style, 'Failed to set eyebrow style');
804
+ }
805
+
806
+ setEyelash(value) {
807
+ this._ensureInitialized();
808
+ this._setIntensity('fb_set_eyelash', value);
809
+ }
810
+
811
+ setEyelashStyle(style) {
812
+ this._setMakeupStyle('fb_set_eyelash_style', style, 'Failed to set eyelash style');
813
+ }
814
+
815
+ setEyelashColor(style) {
816
+ this._setMakeupStyle('fb_set_eyelash_color', style, 'Failed to set eyelash style');
817
+ }
818
+
819
+ setPupil(value) {
820
+ this._ensureInitialized();
821
+ this._setIntensity('fb_set_pupil', value);
822
+ }
823
+
824
+ setPupilColor(style) {
825
+ this._setMakeupStyle('fb_set_pupil_color', style, 'Failed to set pupil style');
754
826
  }
755
827
 
756
828
  /**
757
- * Sets a LUT-based filter
758
- * @param {string} filterId - Unique identifier of the filter (e.g., "chuxin"). Pass "" to clear.
829
+ * Uses chroma keying as the virtual-background mask.
830
+ * Fill is still controlled by setVirtualBackgroundBlur / setVirtualBackground.
831
+ * @param {number} color - Key colour (use ChromaKeyColor enum)
759
832
  */
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');
833
+ setChromaKey(color) {
834
+ this._setMakeupStyle('fb_set_chroma_key', color, 'Failed to set chroma key');
770
835
  }
771
836
 
772
837
  /**
773
- * Sets the intensity of the current filter
774
- * @param {number} intensity - Filter intensity (0.0 - 1.0)
838
+ * Turns off chroma keying and restores portrait-segmentation as the mask.
775
839
  */
776
- setFilterIntensity(intensity) {
840
+ clearChromaKey() {
777
841
  this._ensureInitialized();
778
842
  const Module = this._getWasmModule();
779
843
  const result = Module.ccall(
780
- 'SetFilterIntensity',
844
+ 'fb_clear_chroma_key',
781
845
  'number',
782
- ['number', 'number'],
783
- [this.enginePtr, intensity]
846
+ ['number'],
847
+ [this.enginePtr]
784
848
  );
785
- checkResult(result, 'Failed to set filter intensity');
849
+ checkResult(result, 'Failed to clear chroma key');
786
850
  }
787
851
 
788
- /**
789
- * Sets a 2D sticker
790
- * @param {string} stickerId - Unique identifier of the sticker (e.g., "樱花"). Pass "" to clear.
791
- */
792
- setSticker(stickerId) {
852
+ /** How close a pixel must be to the key colour to be keyed out. [0.0, 1.0]. */
853
+ setChromaKeySimilarity(value) {
854
+ this._setChromaKeyFloat('fb_set_chroma_key_similarity', value);
855
+ }
856
+
857
+ /** Edge feather around the key. [0.0, 1.0]. */
858
+ setChromaKeySmoothness(value) {
859
+ this._setChromaKeyFloat('fb_set_chroma_key_smoothness', value);
860
+ }
861
+
862
+ /** Spill suppression on semi-transparent edges. [0.0, 1.0]. */
863
+ setChromaKeyDesaturation(value) {
864
+ this._setChromaKeyFloat('fb_set_chroma_key_desaturation', value);
865
+ }
866
+
867
+ _setChromaKeyFloat(cFuncName, value) {
793
868
  this._ensureInitialized();
794
869
  const Module = this._getWasmModule();
795
870
  const result = Module.ccall(
796
- 'SetSticker',
871
+ cFuncName,
797
872
  'number',
798
- ['number', 'string'],
799
- [this.enginePtr, stickerId || '']
873
+ ['number', 'number'],
874
+ [this.enginePtr, value]
800
875
  );
801
- checkResult(result, 'Failed to set sticker');
876
+ checkResult(result, `Failed to call ${cFuncName}`);
802
877
  }
803
878
 
804
879
  /**
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
880
+ * Applies a LUT filter from a file path or in-memory .fbd data.
881
+ * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data.
808
882
  */
809
- registerFilter(filterId, resource) {
883
+ setFilter(resource) {
810
884
  this._ensureInitialized();
811
885
  const Module = this._getWasmModule();
812
886
 
813
887
  if (typeof resource === 'string') {
888
+ if (!resource) {
889
+ throw new FacebetterError('Filter path must not be empty; use clearFilter()');
890
+ }
814
891
  const result = Module.ccall(
815
- 'RegisterFilterPath',
892
+ 'fb_set_filter',
816
893
  'number',
817
- ['number', 'string', 'string'],
818
- [this.enginePtr, filterId, resource]
894
+ ['number', 'string'],
895
+ [this.enginePtr, resource]
819
896
  );
820
- checkResult(result, `Failed to register filter path: ${filterId}`);
821
- } else if (resource instanceof Uint8Array) {
897
+ checkResult(result, 'Failed to set filter from path');
898
+ return;
899
+ }
900
+
901
+ if (resource instanceof Uint8Array) {
822
902
  const dataPtr = Module._malloc(resource.length);
823
903
  Module.HEAPU8.set(resource, dataPtr);
824
904
  try {
825
905
  const result = Module.ccall(
826
- 'RegisterFilterData',
906
+ 'fb_set_filter_data',
827
907
  'number',
828
- ['number', 'string', 'number', 'number'],
829
- [this.enginePtr, filterId, dataPtr, resource.length]
908
+ ['number', 'number', 'number'],
909
+ [this.enginePtr, dataPtr, resource.length]
830
910
  );
831
- checkResult(result, `Failed to register filter data: ${filterId}`);
911
+ checkResult(result, 'Failed to set filter from data');
832
912
  } finally {
833
913
  Module._free(dataPtr);
834
914
  }
835
- } else {
836
- throw new FacebetterError('Resource must be a string path or Uint8Array data');
915
+ return;
837
916
  }
917
+
918
+ throw new FacebetterError('Filter resource must be a string path or Uint8Array');
838
919
  }
839
920
 
840
921
  /**
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
922
+ * Clears the current LUT filter.
844
923
  */
845
- registerSticker(stickerId, resource) {
924
+ clearFilter() {
925
+ this._ensureInitialized();
926
+ const Module = this._getWasmModule();
927
+ const result = Module.ccall(
928
+ 'fb_clear_filter',
929
+ 'number',
930
+ ['number'],
931
+ [this.enginePtr]
932
+ );
933
+ checkResult(result, 'Failed to clear filter');
934
+ }
935
+
936
+ /**
937
+ * Sets the intensity of the current filter
938
+ * @param {number} intensity - Filter intensity (0.0 - 1.0)
939
+ */
940
+ setFilterIntensity(intensity) {
941
+ this._ensureInitialized();
942
+ const Module = this._getWasmModule();
943
+ const result = Module.ccall(
944
+ 'fb_set_filter_intensity',
945
+ 'number',
946
+ ['number', 'number'],
947
+ [this.enginePtr, intensity]
948
+ );
949
+ checkResult(result, 'Failed to set filter intensity');
950
+ }
951
+
952
+ /**
953
+ * Applies a 2D sticker from a file path or in-memory .fbd data.
954
+ * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data.
955
+ */
956
+ setSticker(resource) {
846
957
  this._ensureInitialized();
847
958
  const Module = this._getWasmModule();
848
959
 
849
960
  if (typeof resource === 'string') {
961
+ if (!resource) {
962
+ throw new FacebetterError('Sticker path must not be empty; use clearSticker()');
963
+ }
850
964
  const result = Module.ccall(
851
- 'RegisterStickerPath',
965
+ 'fb_set_sticker',
852
966
  'number',
853
- ['number', 'string', 'string'],
854
- [this.enginePtr, stickerId, resource]
967
+ ['number', 'string'],
968
+ [this.enginePtr, resource]
855
969
  );
856
- checkResult(result, `Failed to register sticker path: ${stickerId}`);
857
- } else if (resource instanceof Uint8Array) {
970
+ checkResult(result, 'Failed to set sticker from path');
971
+ return;
972
+ }
973
+
974
+ if (resource instanceof Uint8Array) {
858
975
  const dataPtr = Module._malloc(resource.length);
859
976
  Module.HEAPU8.set(resource, dataPtr);
860
977
  try {
861
978
  const result = Module.ccall(
862
- 'RegisterStickerData',
979
+ 'fb_set_sticker_data',
863
980
  'number',
864
- ['number', 'string', 'number', 'number'],
865
- [this.enginePtr, stickerId, dataPtr, resource.length]
981
+ ['number', 'number', 'number'],
982
+ [this.enginePtr, dataPtr, resource.length]
866
983
  );
867
- checkResult(result, `Failed to register sticker data: ${stickerId}`);
984
+ checkResult(result, 'Failed to set sticker from data');
868
985
  } finally {
869
986
  Module._free(dataPtr);
870
987
  }
871
- } else {
872
- throw new FacebetterError('Resource must be a string path or Uint8Array data');
988
+ return;
873
989
  }
990
+
991
+ throw new FacebetterError('Sticker resource must be a string path or Uint8Array');
874
992
  }
875
993
 
876
994
  /**
877
- * Unregisters a specific filter
878
- * @param {string} filterId - Filter ID to unregister
995
+ * Clears the current 2D sticker.
879
996
  */
880
- unregisterFilter(filterId) {
997
+ clearSticker() {
881
998
  this._ensureInitialized();
882
999
  const Module = this._getWasmModule();
883
1000
  const result = Module.ccall(
884
- 'UnregisterFilter',
1001
+ 'fb_clear_sticker',
885
1002
  'number',
886
- ['number', 'string'],
887
- [this.enginePtr, filterId]
1003
+ ['number'],
1004
+ [this.enginePtr]
888
1005
  );
889
- checkResult(result, `Failed to unregister filter: ${filterId}`);
1006
+ checkResult(result, 'Failed to clear sticker');
890
1007
  }
891
1008
 
892
1009
  /**
893
- * Unregisters all filters
1010
+ * Internal method to set a single intensity parameter
1011
+ * @private
894
1012
  */
895
- unregisterAllFilters() {
896
- this._ensureInitialized();
1013
+ _setIntensity(functionName, value) {
1014
+ if (value < 0 || value > 1) {
1015
+ throw new FacebetterError('Parameter value must be between 0.0 and 1.0');
1016
+ }
1017
+
897
1018
  const Module = this._getWasmModule();
898
1019
  const result = Module.ccall(
899
- 'UnregisterAllFilters',
1020
+ functionName,
900
1021
  'number',
901
- ['number'],
902
- [this.enginePtr]
1022
+ ['number', 'number'],
1023
+ [this.enginePtr, value]
903
1024
  );
904
- checkResult(result, 'Failed to unregister all filters');
1025
+
1026
+ checkResult(result, `Failed to set beauty parameter`);
905
1027
  }
906
1028
 
907
- /**
908
- * Unregisters a specific sticker
909
- * @param {string} stickerId - Sticker ID to unregister
910
- */
911
- unregisterSticker(stickerId) {
1029
+ _setMakeupStyle(cFuncName, value, errorMessage) {
912
1030
  this._ensureInitialized();
913
1031
  const Module = this._getWasmModule();
914
1032
  const result = Module.ccall(
915
- 'UnregisterSticker',
1033
+ cFuncName,
916
1034
  'number',
917
- ['number', 'string'],
918
- [this.enginePtr, stickerId]
1035
+ ['number', 'number'],
1036
+ [this.enginePtr, value]
919
1037
  );
920
- checkResult(result, `Failed to unregister sticker: ${stickerId}`);
1038
+ checkResult(result, errorMessage);
921
1039
  }
922
1040
 
923
1041
  /**
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
1042
+ * Internal method to set beauty parameters
982
1043
  * @private
983
1044
  */
984
1045
  _setBeautyParam(functionName, param, value) {
985
- if (value < 0 || value > 1) {
986
- throw new FacebetterError('Parameter value must be between 0.0 and 1.0');
1046
+ if (value < -1 || value > 1) {
1047
+ throw new FacebetterError('Parameter value must be between -1.0 and 1.0');
987
1048
  }
988
1049
 
989
1050
  const Module = this._getWasmModule();
@@ -998,15 +1059,25 @@ class BeautyEffectEngine {
998
1059
  }
999
1060
 
1000
1061
  /**
1001
- * Sets engine callbacks (face landmarks detection)
1062
+ * Sets engine callbacks (face landmarks and engine events)
1002
1063
  * @param {Object} callbacks - Callback functions
1003
- * @param {Function} callbacks.onFaceLandmarks - Callback for face landmarks detection
1064
+ * @param {Function} [callbacks.onFaceLandmarks] - Callback for face landmarks detection
1065
+ * @param {Function} [callbacks.onEngineEvent] - Callback for license / init events (code, message)
1004
1066
  * @param {number} [callbacks.maxFaces=10] - Maximum number of faces to support (affects shared buffer size)
1005
1067
  */
1006
1068
  setCallbacks(callbacks) {
1007
1069
  this._ensureInitialized();
1008
1070
  const Module = this._getWasmModule();
1009
-
1071
+
1072
+ if (!Module._engine_event_callbacks) {
1073
+ Module._engine_event_callbacks = {};
1074
+ }
1075
+ if (callbacks && typeof callbacks.onEngineEvent === 'function') {
1076
+ Module._engine_event_callbacks[this.enginePtr] = callbacks.onEngineEvent;
1077
+ } else {
1078
+ delete Module._engine_event_callbacks[this.enginePtr];
1079
+ }
1080
+
1010
1081
  // Initialize callback storage (if it doesn't exist)
1011
1082
  if (!Module._face_landmarks_callbacks) {
1012
1083
  Module._face_landmarks_callbacks = [];
@@ -1022,9 +1093,9 @@ class BeautyEffectEngine {
1022
1093
 
1023
1094
  // Pre-allocate shared memory
1024
1095
  // Metadata: 2 ints (frame_number, face_count) = 8 bytes
1025
- // Face data: maxFaces * 343 floats * 4 bytes = maxFaces * 1372 bytes
1096
+ // Face data: maxFaces * 342 floats * 4 bytes = maxFaces * 1368 bytes
1026
1097
  const metadataSize = 2 * 4; // 2 ints
1027
- const faceDataSize = maxFaces * 343 * 4; // 343 floats per face
1098
+ const faceDataSize = maxFaces * 342 * 4; // 342 floats per face
1028
1099
  sharedBufferSize = metadataSize + faceDataSize;
1029
1100
  sharedBufferPtr = Module._malloc(sharedBufferSize);
1030
1101
 
@@ -1041,7 +1112,7 @@ class BeautyEffectEngine {
1041
1112
  const dataOffset = dataPtr / 4; // float is 4 bytes
1042
1113
 
1043
1114
  const results = [];
1044
- const FLOATS_PER_FACE = 343;
1115
+ const FLOATS_PER_FACE = 342;
1045
1116
 
1046
1117
  for (let i = 0; i < faceCount; i++) {
1047
1118
  const faceOffset = dataOffset + i * FLOATS_PER_FACE;
@@ -1057,7 +1128,6 @@ class BeautyEffectEngine {
1057
1128
 
1058
1129
  // Read basic fields
1059
1130
  const faceId = Math.round(heap[offset++]);
1060
- const faceAction = Math.round(heap[offset++]);
1061
1131
  const score = heap[offset++];
1062
1132
  const pitch = heap[offset++];
1063
1133
  const roll = heap[offset++];
@@ -1083,7 +1153,6 @@ class BeautyEffectEngine {
1083
1153
  key_points: keyPoints,
1084
1154
  visibility,
1085
1155
  face_id: faceId,
1086
- face_action: faceAction,
1087
1156
  score,
1088
1157
  pitch,
1089
1158
  roll,
@@ -1102,7 +1171,7 @@ class BeautyEffectEngine {
1102
1171
 
1103
1172
  // Call C API
1104
1173
  const result = Module.ccall(
1105
- 'SetCallbacks',
1174
+ 'fb_engine_set_wasm_callbacks',
1106
1175
  'number',
1107
1176
  ['number', 'number', 'number', 'number'],
1108
1177
  [
@@ -1131,55 +1200,81 @@ class BeautyEffectEngine {
1131
1200
  }
1132
1201
 
1133
1202
  /**
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)
1203
+ * Enables virtual background blur.
1204
+ * Level is continuous in [0.0, 1.0] (downsample + mix, no shader rebuild).
1205
+ * @param {number} level - Blur strength in [0.0, 1.0]. 0 clears the virtual background.
1138
1206
  */
1139
- setVirtualBackground(options) {
1207
+ setVirtualBackgroundBlur(level) {
1140
1208
  this._ensureInitialized();
1141
1209
  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);
1210
+ const result = Module.ccall(
1211
+ 'fb_set_virtual_background_blur',
1212
+ 'number',
1213
+ ['number', 'number'],
1214
+ [this.enginePtr, level]
1215
+ );
1216
+ checkResult(result, 'Failed to set virtual background blur');
1217
+ }
1218
+
1219
+ /**
1220
+ * Replaces the background with an image file path or encoded png/jpg bytes.
1221
+ * @param {string|Uint8Array} resource - Image path or Uint8Array data.
1222
+ */
1223
+ setVirtualBackground(resource) {
1224
+ this._ensureInitialized();
1225
+ const Module = this._getWasmModule();
1226
+
1227
+ if (typeof resource === 'string') {
1228
+ if (!resource) {
1229
+ throw new FacebetterError(
1230
+ 'Virtual background path must not be empty; use clearVirtualBackground()'
1231
+ );
1159
1232
  }
1160
-
1161
- // Use unified SetVirtualBackground C interface (consistent with other platforms)
1162
1233
  const result = Module.ccall(
1163
- 'SetVirtualBackground',
1234
+ 'fb_set_virtual_background',
1164
1235
  '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
- ]
1236
+ ['number', 'string'],
1237
+ [this.enginePtr, resource]
1174
1238
  );
1175
-
1176
- checkResult(result, 'Failed to set virtual background');
1177
- } finally {
1178
- // Free memory
1179
- if (imageBufferPtr) {
1180
- Module._free(imageBufferPtr);
1239
+ checkResult(result, 'Failed to set virtual background from path');
1240
+ return;
1241
+ }
1242
+
1243
+ if (resource instanceof Uint8Array) {
1244
+ const dataPtr = Module._malloc(resource.length);
1245
+ Module.HEAPU8.set(resource, dataPtr);
1246
+ try {
1247
+ const result = Module.ccall(
1248
+ 'fb_set_virtual_background_data',
1249
+ 'number',
1250
+ ['number', 'number', 'number'],
1251
+ [this.enginePtr, dataPtr, resource.length]
1252
+ );
1253
+ checkResult(result, 'Failed to set virtual background from data');
1254
+ } finally {
1255
+ Module._free(dataPtr);
1181
1256
  }
1257
+ return;
1182
1258
  }
1259
+
1260
+ throw new FacebetterError(
1261
+ 'setVirtualBackground expects a file path string or Uint8Array'
1262
+ );
1263
+ }
1264
+
1265
+ /**
1266
+ * Clears virtual background (blur or image replacement).
1267
+ */
1268
+ clearVirtualBackground() {
1269
+ this._ensureInitialized();
1270
+ const Module = this._getWasmModule();
1271
+ const result = Module.ccall(
1272
+ 'fb_clear_virtual_background',
1273
+ 'number',
1274
+ ['number'],
1275
+ [this.enginePtr]
1276
+ );
1277
+ checkResult(result, 'Failed to clear virtual background');
1183
1278
  }
1184
1279
 
1185
1280
  /**
@@ -1187,12 +1282,12 @@ class BeautyEffectEngine {
1187
1282
  * When enabled, beauty effects will only be applied to detected skin areas.
1188
1283
  * @param {boolean} enabled - True to enable skin-only beauty, false to apply to entire image.
1189
1284
  */
1190
- setSkinOnlyBeauty(enabled) {
1285
+ setBeautySkinOnly(enabled) {
1191
1286
  this._ensureInitialized();
1192
1287
  const Module = this._getWasmModule();
1193
1288
 
1194
1289
  const result = Module.ccall(
1195
- 'SetSkinOnlyBeauty',
1290
+ 'fb_set_beauty_skin_only',
1196
1291
  'number',
1197
1292
  ['number', 'number'],
1198
1293
  [this.enginePtr, enabled ? 1 : 0]
@@ -1201,6 +1296,35 @@ class BeautyEffectEngine {
1201
1296
  checkResult(result, 'Failed to set skin only beauty');
1202
1297
  }
1203
1298
 
1299
+ /**
1300
+ * Gets engine performance statistics.
1301
+ * @returns {{fps: number, avgProcessTimeMs: number, sessionTimeS: number}}
1302
+ */
1303
+ getStats() {
1304
+ this._ensureInitialized();
1305
+ const Module = this._getWasmModule();
1306
+ const ptr = Module._malloc(24);
1307
+ try {
1308
+ const result = Module.ccall(
1309
+ 'fb_engine_get_stats',
1310
+ 'number',
1311
+ ['number', 'number'],
1312
+ [this.enginePtr, ptr]
1313
+ );
1314
+ checkResult(result, 'Failed to get stats');
1315
+ if ((ptr & 7) !== 0) {
1316
+ throw new FacebetterError('Unaligned stats buffer');
1317
+ }
1318
+ const base = ptr >> 3;
1319
+ return {
1320
+ fps: Module.HEAPF64[base],
1321
+ avgProcessTimeMs: Module.HEAPF64[base + 1],
1322
+ sessionTimeS: Module.HEAPF64[base + 2]
1323
+ };
1324
+ } finally {
1325
+ Module._free(ptr);
1326
+ }
1327
+ }
1204
1328
 
1205
1329
  /**
1206
1330
  * Ensures buffers are allocated for the given dimensions
@@ -1254,7 +1378,7 @@ class BeautyEffectEngine {
1254
1378
  srcView.set(imageData.data);
1255
1379
 
1256
1380
  const result = Module.ccall(
1257
- 'ProcessImageRGBA',
1381
+ 'fb_process_rgba',
1258
1382
  'number',
1259
1383
  ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
1260
1384
  [
@@ -1315,7 +1439,7 @@ class BeautyEffectEngine {
1315
1439
 
1316
1440
  try {
1317
1441
  const result = Module.ccall(
1318
- 'ProcessImageTexture',
1442
+ 'fb_process_texture',
1319
1443
  'number',
1320
1444
  ['number', 'number', 'number', 'number', 'number', 'number', 'number'],
1321
1445
  [
@@ -1355,37 +1479,37 @@ class BeautyEffectEngine {
1355
1479
  }
1356
1480
 
1357
1481
  /**
1358
- * Ensures gpupixel_canvas exists in the DOM (browser only)
1482
+ * Ensures facebetter_canvas exists in the DOM (browser only)
1359
1483
  * This canvas is required by GPUPixel for WebGL context creation
1360
1484
  * @private
1361
1485
  */
1362
- _ensureGPUPixelCanvas() {
1486
+ _ensureFacebetterCanvas() {
1363
1487
  if (typeof document === 'undefined') {
1364
1488
  return;
1365
1489
  }
1366
1490
 
1367
- let canvas = document.getElementById('gpupixel_canvas');
1491
+ let canvas = document.getElementById('facebetter_canvas');
1368
1492
  if (!canvas) {
1369
1493
  canvas = document.createElement('canvas');
1370
- canvas.id = 'gpupixel_canvas';
1494
+ canvas.id = 'facebetter_canvas';
1371
1495
  canvas.style.display = 'none';
1372
1496
  canvas.width = 1;
1373
1497
  canvas.height = 1;
1374
1498
  document.body.appendChild(canvas);
1375
- this._createdGPUPixelCanvas = true;
1499
+ this._createdFacebetterCanvas = true;
1376
1500
  }
1377
- this._gpupixelCanvas = canvas;
1501
+ this._facebetterCanvas = canvas;
1378
1502
  }
1379
1503
 
1380
1504
  /**
1381
- * Cleans up gpupixel_canvas if it was created by this engine instance (browser only)
1505
+ * Cleans up facebetter_canvas if it was created by this engine instance (browser only)
1382
1506
  * @private
1383
1507
  */
1384
- _cleanupGPUPixelCanvas() {
1385
- if (this._createdGPUPixelCanvas && this._gpupixelCanvas) {
1386
- this._gpupixelCanvas.remove();
1387
- this._gpupixelCanvas = null;
1388
- this._createdGPUPixelCanvas = false;
1508
+ _cleanupFacebetterCanvas() {
1509
+ if (this._createdFacebetterCanvas && this._facebetterCanvas) {
1510
+ this._facebetterCanvas.remove();
1511
+ this._facebetterCanvas = null;
1512
+ this._createdFacebetterCanvas = false;
1389
1513
  }
1390
1514
  }
1391
1515
 
@@ -1414,6 +1538,206 @@ class BeautyEffectEngine {
1414
1538
 
1415
1539
  }
1416
1540
 
1541
+ const WASM_FILE = 'facebetter-core.wasm';
1542
+ const RESOURCE_FILE = 'resource.fbd';
1543
+ const MEMFS_RESOURCE_PATH = '/resource.fbd';
1544
+
1545
+ function emitProgress(onProgress, loaded, total) {
1546
+ if (typeof onProgress !== 'function') {
1547
+ return;
1548
+ }
1549
+ const safeTotal = total > 0 ? total : loaded;
1550
+ const percent =
1551
+ safeTotal > 0 ? Math.min(100, Math.round((loaded / safeTotal) * 100)) : 0;
1552
+ onProgress({ loaded, total: safeTotal, percent });
1553
+ }
1554
+
1555
+ async function readFileUrl(url, onChunk) {
1556
+ const { readFile } = await import('node:fs/promises');
1557
+ const { fileURLToPath } = await import('node:url');
1558
+ const buf = await readFile(fileURLToPath(url));
1559
+ const bytes = new Uint8Array(buf);
1560
+ onChunk(bytes.byteLength, bytes.byteLength);
1561
+ return bytes;
1562
+ }
1563
+
1564
+ /**
1565
+ * Fetch a binary with byte-level download progress.
1566
+ * @param {string} url
1567
+ * @param {function(number, number): void} onChunk loaded, total (0 if unknown)
1568
+ * @returns {Promise<Uint8Array>}
1569
+ */
1570
+ async function fetchBinary(url, onChunk) {
1571
+ if (typeof url === 'string' && url.startsWith('file:')) {
1572
+ return readFileUrl(url, onChunk);
1573
+ }
1574
+
1575
+ const response = await fetch(url);
1576
+ if (!response.ok) {
1577
+ throw new FacebetterError(
1578
+ `Failed to download ${url} (${response.status})`,
1579
+ 'WASM_LOAD_ERROR'
1580
+ );
1581
+ }
1582
+
1583
+ const totalHeader = Number(response.headers.get('content-length'));
1584
+ const total = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : 0;
1585
+
1586
+ if (!response.body || typeof response.body.getReader !== 'function') {
1587
+ const buffer = await response.arrayBuffer();
1588
+ const bytes = new Uint8Array(buffer);
1589
+ onChunk(bytes.byteLength, total || bytes.byteLength);
1590
+ return bytes;
1591
+ }
1592
+
1593
+ const reader = response.body.getReader();
1594
+ const chunks = [];
1595
+ let loaded = 0;
1596
+ while (true) {
1597
+ const { done, value } = await reader.read();
1598
+ if (done) {
1599
+ break;
1600
+ }
1601
+ chunks.push(value);
1602
+ loaded += value.byteLength;
1603
+ onChunk(loaded, total);
1604
+ }
1605
+
1606
+ const bytes = new Uint8Array(loaded);
1607
+ let offset = 0;
1608
+ for (const chunk of chunks) {
1609
+ bytes.set(chunk, offset);
1610
+ offset += chunk.byteLength;
1611
+ }
1612
+ onChunk(loaded, total || loaded);
1613
+ return bytes;
1614
+ }
1615
+
1616
+ async function fetchRuntimeAssets({ wasmUrl, resourceUrl, onProgress }) {
1617
+ let wasmLoaded = 0;
1618
+ let wasmTotal = 0;
1619
+ let resourceLoaded = 0;
1620
+ let resourceTotal = 0;
1621
+
1622
+ const emit = () => {
1623
+ emitProgress(
1624
+ onProgress,
1625
+ wasmLoaded + resourceLoaded,
1626
+ wasmTotal + resourceTotal
1627
+ );
1628
+ };
1629
+
1630
+ const [wasmBytes, resourceBytes] = await Promise.all([
1631
+ fetchBinary(wasmUrl, (loaded, total) => {
1632
+ wasmLoaded = loaded;
1633
+ wasmTotal = total || loaded;
1634
+ emit();
1635
+ }),
1636
+ fetchBinary(resourceUrl, (loaded, total) => {
1637
+ resourceLoaded = loaded;
1638
+ resourceTotal = total || loaded;
1639
+ emit();
1640
+ }),
1641
+ ]);
1642
+
1643
+ emitProgress(
1644
+ onProgress,
1645
+ wasmBytes.byteLength + resourceBytes.byteLength,
1646
+ wasmBytes.byteLength + resourceBytes.byteLength
1647
+ );
1648
+ return { wasmBytes, resourceBytes };
1649
+ }
1650
+
1651
+ function scriptDirectory() {
1652
+ if (typeof document === 'undefined') {
1653
+ return '';
1654
+ }
1655
+ if (document.currentScript?.src) {
1656
+ return document.currentScript.src.slice(
1657
+ 0,
1658
+ document.currentScript.src.lastIndexOf('/') + 1
1659
+ );
1660
+ }
1661
+ const scripts = document.getElementsByTagName('script');
1662
+ for (let i = scripts.length - 1; i >= 0; i--) {
1663
+ const src = scripts[i].src;
1664
+ if (src && (src.includes('facebetter.js') || src.includes('facebetter'))) {
1665
+ return src.slice(0, src.lastIndexOf('/') + 1);
1666
+ }
1667
+ }
1668
+ if (typeof location !== 'undefined') {
1669
+ return new URL('.', location.href).href;
1670
+ }
1671
+ return './';
1672
+ }
1673
+
1674
+ /**
1675
+ * Resolve sidecar wasm / resource URLs.
1676
+ * Prefer `assetBaseUrl`, then `facebetter-core/assets`, then the script directory.
1677
+ */
1678
+ async function resolveRuntimeAssetUrls(options = {}) {
1679
+ const joinBase = (base) => {
1680
+ const prefix = base.endsWith('/') ? base : `${base}/`;
1681
+ return {
1682
+ wasmUrl: options.wasmAssetUrl || `${prefix}${WASM_FILE}`,
1683
+ resourceUrl: options.resourceAssetUrl || `${prefix}${RESOURCE_FILE}`,
1684
+ };
1685
+ };
1686
+
1687
+ if (options.wasmAssetUrl && options.resourceAssetUrl) {
1688
+ return {
1689
+ wasmUrl: options.wasmAssetUrl,
1690
+ resourceUrl: options.resourceAssetUrl,
1691
+ };
1692
+ }
1693
+ if (options.assetBaseUrl) {
1694
+ return joinBase(options.assetBaseUrl);
1695
+ }
1696
+ if (typeof options.wasmUrl === 'string' && /^(https?:|file:|\/)/.test(options.wasmUrl)) {
1697
+ return joinBase(options.wasmUrl.slice(0, options.wasmUrl.lastIndexOf('/') + 1));
1698
+ }
1699
+
1700
+ try {
1701
+ const assets = await import('facebetter-core/assets');
1702
+ return {
1703
+ wasmUrl: options.wasmAssetUrl || assets.wasmUrl,
1704
+ resourceUrl: options.resourceAssetUrl || assets.resourceUrl,
1705
+ };
1706
+ } catch {
1707
+ return joinBase(scriptDirectory());
1708
+ }
1709
+ }
1710
+
1711
+ /**
1712
+ * Instantiate the Emscripten factory using already-downloaded bytes.
1713
+ */
1714
+ async function instantiateRuntime(factory, wasmBytes, resourceBytes) {
1715
+ if (!factory) {
1716
+ throw new FacebetterError(
1717
+ 'WASM module does not export createFaceBetterModule',
1718
+ 'WASM_LOAD_ERROR'
1719
+ );
1720
+ }
1721
+
1722
+ const module = await factory({
1723
+ instantiateWasm(imports, receiveInstance) {
1724
+ return WebAssembly.instantiate(wasmBytes, imports).then((result) => {
1725
+ receiveInstance(result.instance, result.module);
1726
+ });
1727
+ },
1728
+ });
1729
+
1730
+ if (!module) {
1731
+ throw new FacebetterError('Failed to initialize WASM module', 'WASM_LOAD_ERROR');
1732
+ }
1733
+ if (!module.FS) {
1734
+ throw new FacebetterError('Runtime filesystem is not available', 'WASM_LOAD_ERROR');
1735
+ }
1736
+ module.FS.writeFile(MEMFS_RESOURCE_PATH, resourceBytes);
1737
+ module.ready = true;
1738
+ return module;
1739
+ }
1740
+
1417
1741
  /**
1418
1742
  * Browser-specific Image Utilities
1419
1743
  * Uses DOM APIs (canvas, ImageData, etc.)
@@ -1470,21 +1794,19 @@ function toImageData(source, width, height) {
1470
1794
  */
1471
1795
 
1472
1796
 
1473
- // ESM-specific WASM loader
1474
1797
  let wasmModuleInstance = null;
1475
1798
  let wasmModulePromise = null;
1476
1799
 
1477
1800
  /**
1478
- * Loads the WebAssembly module (ESM version)
1479
- * Supports ESM format WASM modules (generated with MODULARIZE and EXPORT_ES6)
1480
- * With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
1481
- * @param {Object} [options] - Options
1482
- * @param {string} [options.wasmUrl] - Custom WASM module URL (defaults to facebetter-core npm package)
1483
- * @param {Function} [options.locateFile] - Custom locateFile function (usually not needed with SINGLE_FILE=1)
1484
- * @returns {Promise<Object>} Promise that resolves with the WASM module
1801
+ * Load the runtime factory, then download .wasm and resource.fbd with progress.
1802
+ * @param {Object} [options]
1803
+ * @param {function} [options.onProgress]
1804
+ * @param {string} [options.assetBaseUrl]
1805
+ * @param {string} [options.wasmUrl] Glue JS URL (optional)
1485
1806
  */
1486
1807
  async function loadWasmModule(options = {}) {
1487
1808
  if (wasmModuleInstance) {
1809
+ options.onProgress?.({ loaded: 1, total: 1, percent: 100 });
1488
1810
  return wasmModuleInstance;
1489
1811
  }
1490
1812
 
@@ -1493,53 +1815,32 @@ async function loadWasmModule(options = {}) {
1493
1815
  }
1494
1816
 
1495
1817
  wasmModulePromise = (async () => {
1496
- // Try to import the WASM module factory
1497
- // facebetter-core.js is an Emscripten-generated ESM file (with MODULARIZE and EXPORT_ES6)
1498
- // It's provided by the facebetter-core npm package
1499
- let FaceBetterModuleFactory;
1500
-
1501
1818
  try {
1502
- // Get the WASM module
1819
+ let factory;
1503
1820
  if (options.wasmUrl) {
1504
- // If custom URL is provided, use it (for backward compatibility or custom builds)
1505
- const wasmModule = await import(options.wasmUrl);
1506
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1821
+ const wasmModule = await import(/* @vite-ignore */ options.wasmUrl);
1822
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1507
1823
  } else {
1508
- // Import from facebetter-core npm package
1509
1824
  const wasmModule = await import('facebetter-core');
1510
-
1511
- // Get the factory function (default export or named export)
1512
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1825
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1513
1826
  }
1514
-
1515
- if (!FaceBetterModuleFactory) {
1516
- throw new Error('WASM module does not export createFaceBetterModule function');
1517
- }
1518
- } catch (e) {
1519
- throw new FacebetterError(`Failed to load WASM module: ${e.message}`);
1520
- }
1521
-
1522
- // Configure module options
1523
- // With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
1524
- // No need to handle .wasm and .data file paths separately
1525
- const moduleOptions = {
1526
- locateFile: options.locateFile || function(path, prefix) {
1527
- // With SINGLE_FILE=1, Emscripten handles embedded files automatically
1528
- // Custom locateFile is only needed if user provides one
1529
- return prefix + path;
1530
- },
1531
- ...options
1532
- };
1533
-
1534
- // Initialize the module (MODULARIZE returns a Promise)
1535
- wasmModuleInstance = await FaceBetterModuleFactory(moduleOptions);
1536
-
1537
- if (!wasmModuleInstance) {
1538
- throw new FacebetterError('Failed to initialize WASM module');
1827
+ const urls = await resolveRuntimeAssetUrls(options);
1828
+ const { wasmBytes, resourceBytes } = await fetchRuntimeAssets({
1829
+ ...urls,
1830
+ onProgress: options.onProgress,
1831
+ });
1832
+ wasmModuleInstance = await instantiateRuntime(
1833
+ factory,
1834
+ wasmBytes,
1835
+ resourceBytes
1836
+ );
1837
+ return wasmModuleInstance;
1838
+ } catch (error) {
1839
+ wasmModulePromise = null;
1840
+ throw error instanceof FacebetterError
1841
+ ? error
1842
+ : new FacebetterError(`Failed to load WASM module: ${error.message}`, 'WASM_LOAD_ERROR');
1539
1843
  }
1540
-
1541
- wasmModuleInstance.ready = true;
1542
- return wasmModuleInstance;
1543
1844
  })();
1544
1845
 
1545
1846
  return wasmModulePromise;
@@ -1576,15 +1877,15 @@ function getWasmBuffer() {
1576
1877
  }
1577
1878
 
1578
1879
  // Browser-specific GPU canvas management
1579
- function ensureGPUPixelCanvas() {
1880
+ function ensureFacebetterCanvas() {
1580
1881
  if (typeof document === 'undefined') {
1581
1882
  return;
1582
1883
  }
1583
1884
 
1584
- let canvas = document.getElementById('gpupixel_canvas');
1885
+ let canvas = document.getElementById('facebetter_canvas');
1585
1886
  if (!canvas) {
1586
1887
  canvas = document.createElement('canvas');
1587
- canvas.id = 'gpupixel_canvas';
1888
+ canvas.id = 'facebetter_canvas';
1588
1889
  canvas.style.display = 'none';
1589
1890
  canvas.width = 1;
1590
1891
  canvas.height = 1;
@@ -1594,18 +1895,18 @@ function ensureGPUPixelCanvas() {
1594
1895
  }
1595
1896
  }
1596
1897
 
1597
- function cleanupGPUPixelCanvas() {
1898
+ function cleanupFacebetterCanvas() {
1598
1899
  // Cleanup is handled by engine instance
1599
1900
  }
1600
1901
 
1601
1902
  // Create platform API
1602
1903
  const platformAPI = {
1603
- loadWasmModule: () => loadWasmModule(),
1904
+ loadWasmModule: (options) => loadWasmModule(options),
1604
1905
  getWasmModule,
1605
1906
  getWasmBuffer,
1606
1907
  toImageData: toImageData,
1607
- ensureGPUPixelCanvas,
1608
- cleanupGPUPixelCanvas
1908
+ ensureFacebetterCanvas,
1909
+ cleanupFacebetterCanvas
1609
1910
  };
1610
1911
 
1611
1912
  // Export factory function
@@ -1623,17 +1924,26 @@ class ESMBeautyEffectEngine extends BeautyEffectEngine {
1623
1924
  }
1624
1925
 
1625
1926
  // 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;
1927
+ const WhiteningStyle = WhiteningStyle$1;
1928
+ const SmoothingStyle = SmoothingStyle$1;
1929
+ const Reshape = Reshape$1;
1930
+ const LipstickColor = LipstickColor$1;
1631
1931
  const BlushStyle = BlushStyle$1;
1632
- const ChromaKeyParam = ChromaKeyParam$1;
1633
- const BackgroundMode = BackgroundMode$1;
1932
+ const BlushColor = BlushColor$1;
1933
+ const ContourStyle = ContourStyle$1;
1934
+ const EyeShadowStyle = EyeShadowStyle$1;
1935
+ const EyeShadowColor = EyeShadowColor$1;
1936
+ const EyeLinerStyle = EyeLinerStyle$1;
1937
+ const EyeLinerColor = EyeLinerColor$1;
1938
+ const EyebrowStyle = EyebrowStyle$1;
1939
+ const EyebrowColor = EyebrowColor$1;
1940
+ const EyelashStyle = EyelashStyle$1;
1941
+ const EyelashColor = EyelashColor$1;
1942
+ const PupilColor = PupilColor$1;
1943
+ const ChromaKeyColor = ChromaKeyColor$1;
1634
1944
  const FrameType = FrameType$1;
1635
1945
  const MirrorMode = MirrorMode$1;
1636
- const VirtualBackgroundOptions = VirtualBackgroundOptions$1;
1946
+ const EngineEventCode = EngineEventCode$1;
1637
1947
 
1638
1948
  // Default export
1639
1949
  var index = {
@@ -1644,5 +1954,5 @@ var index = {
1644
1954
  loadWasmModule
1645
1955
  };
1646
1956
 
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 };
1957
+ export { ESMBeautyEffectEngine as BeautyEffectEngine, BlushColor, BlushStyle, ChromaKeyColor, ContourStyle, EngineConfig, EngineEventCode, EyeLinerColor, EyeLinerStyle, EyeShadowColor, EyeShadowStyle, EyebrowColor, EyebrowStyle, EyelashColor, EyelashStyle, FacebetterError, FrameType, LipstickColor, MirrorMode, PupilColor, Reshape, SmoothingStyle, WhiteningStyle, createBeautyEffectEngine, index as default, getWasmBuffer, getWasmModule, loadWasmModule };
1648
1958
  //# sourceMappingURL=facebetter.esm.js.map