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