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.
- package/README.md +100 -30
- package/dist/facebetter.esm.js +970 -660
- package/dist/facebetter.esm.js.map +1 -1
- package/dist/facebetter.js +987 -654
- package/dist/facebetter.js.map +1 -1
- package/package.json +8 -3
package/dist/facebetter.esm.js
CHANGED
|
@@ -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
|
-
*
|
|
30
|
-
* Platform-agnostic
|
|
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
|
-
*
|
|
36
|
-
*
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
BasicParam: BasicParam$1,
|
|
224
|
-
BeautyType: BeautyType$1,
|
|
262
|
+
BlushColor: BlushColor$1,
|
|
225
263
|
BlushStyle: BlushStyle$1,
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
MakeupParam: MakeupParam$1,
|
|
276
|
+
LipstickColor: LipstickColor$1,
|
|
230
277
|
MirrorMode: MirrorMode$1,
|
|
231
|
-
|
|
232
|
-
|
|
278
|
+
PupilColor: PupilColor$1,
|
|
279
|
+
Reshape: Reshape$1,
|
|
280
|
+
SmoothingStyle: SmoothingStyle$1,
|
|
281
|
+
WhiteningStyle: WhiteningStyle$1
|
|
233
282
|
});
|
|
234
283
|
|
|
235
284
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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
|
-
/**
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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.
|
|
295
|
-
* @param {Function} [platformAPI.
|
|
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.
|
|
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.
|
|
343
|
-
this.
|
|
344
|
-
|
|
365
|
+
this._ensureFacebetterCanvas = platformAPI.ensureFacebetterCanvas;
|
|
366
|
+
this._cleanupFacebetterCanvas = platformAPI.cleanupFacebetterCanvas;
|
|
367
|
+
this._pendingLogConfig = null;
|
|
368
|
+
|
|
345
369
|
// Browser-specific state
|
|
346
|
-
this.
|
|
347
|
-
this.
|
|
370
|
+
this._facebetterCanvas = null;
|
|
371
|
+
this._createdFacebetterCanvas = false;
|
|
348
372
|
this._offscreenCanvas = null;
|
|
349
373
|
this._offscreenCtx = null;
|
|
350
374
|
|
|
351
|
-
// Automatically create
|
|
352
|
-
if (this.
|
|
353
|
-
this.
|
|
375
|
+
// Automatically create facebetter_canvas if it doesn't exist (browser only)
|
|
376
|
+
if (this._ensureFacebetterCanvas) {
|
|
377
|
+
this._ensureFacebetterCanvas();
|
|
354
378
|
}
|
|
355
|
-
|
|
356
|
-
|
|
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:
|
|
415
|
-
* @param {number} [options.authTimeout] -
|
|
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 ||
|
|
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
|
-
//
|
|
466
|
-
if (!
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
//
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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;
|
|
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('
|
|
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
|
|
567
|
-
if (this.
|
|
568
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
*
|
|
604
|
-
* -
|
|
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
|
-
|
|
666
|
+
setSmoothing(value) {
|
|
613
667
|
this._ensureInitialized();
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
628
|
-
*
|
|
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
|
-
|
|
683
|
+
setWhitening(value) {
|
|
635
684
|
this._ensureInitialized();
|
|
636
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
654
|
-
*
|
|
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
|
-
|
|
700
|
+
setSharpening(value) {
|
|
659
701
|
this._ensureInitialized();
|
|
660
|
-
|
|
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
|
|
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
|
-
|
|
709
|
+
setRosiness(value) {
|
|
678
710
|
this._ensureInitialized();
|
|
679
|
-
this.
|
|
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
|
|
685
|
-
* @param {number} value - Parameter value
|
|
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
|
-
|
|
719
|
+
setReshape(param, value) {
|
|
688
720
|
this._ensureInitialized();
|
|
689
|
-
this._setBeautyParam('
|
|
721
|
+
this._setBeautyParam('fb_set_reshape', param, value);
|
|
690
722
|
}
|
|
691
723
|
|
|
692
724
|
/**
|
|
693
|
-
* Sets
|
|
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
|
-
|
|
728
|
+
setLipstick(value) {
|
|
698
729
|
this._ensureInitialized();
|
|
699
|
-
this.
|
|
730
|
+
this._setIntensity('fb_set_lipstick', value);
|
|
700
731
|
}
|
|
701
732
|
|
|
702
733
|
/**
|
|
703
|
-
* Sets lipstick
|
|
704
|
-
* @param {number} style - Style (use
|
|
734
|
+
* Sets lipstick colour preset
|
|
735
|
+
* @param {number} style - Style (use LipstickColor enum, e.g., LipstickColor.Rouge)
|
|
705
736
|
*/
|
|
706
|
-
|
|
707
|
-
this.
|
|
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
|
|
720
|
-
* @param {number}
|
|
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
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
this.
|
|
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
|
-
*
|
|
758
|
-
*
|
|
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
|
-
|
|
761
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
840
|
+
clearChromaKey() {
|
|
777
841
|
this._ensureInitialized();
|
|
778
842
|
const Module = this._getWasmModule();
|
|
779
843
|
const result = Module.ccall(
|
|
780
|
-
'
|
|
844
|
+
'fb_clear_chroma_key',
|
|
781
845
|
'number',
|
|
782
|
-
['number'
|
|
783
|
-
[this.enginePtr
|
|
846
|
+
['number'],
|
|
847
|
+
[this.enginePtr]
|
|
784
848
|
);
|
|
785
|
-
checkResult(result, 'Failed to
|
|
849
|
+
checkResult(result, 'Failed to clear chroma key');
|
|
786
850
|
}
|
|
787
851
|
|
|
788
|
-
/**
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
871
|
+
cFuncName,
|
|
797
872
|
'number',
|
|
798
|
-
['number', '
|
|
799
|
-
[this.enginePtr,
|
|
873
|
+
['number', 'number'],
|
|
874
|
+
[this.enginePtr, value]
|
|
800
875
|
);
|
|
801
|
-
checkResult(result,
|
|
876
|
+
checkResult(result, `Failed to call ${cFuncName}`);
|
|
802
877
|
}
|
|
803
878
|
|
|
804
879
|
/**
|
|
805
|
-
*
|
|
806
|
-
* @param {string}
|
|
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
|
-
|
|
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
|
-
'
|
|
892
|
+
'fb_set_filter',
|
|
816
893
|
'number',
|
|
817
|
-
['number', 'string'
|
|
818
|
-
[this.enginePtr,
|
|
894
|
+
['number', 'string'],
|
|
895
|
+
[this.enginePtr, resource]
|
|
819
896
|
);
|
|
820
|
-
checkResult(result,
|
|
821
|
-
|
|
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
|
-
'
|
|
906
|
+
'fb_set_filter_data',
|
|
827
907
|
'number',
|
|
828
|
-
['number', '
|
|
829
|
-
[this.enginePtr,
|
|
908
|
+
['number', 'number', 'number'],
|
|
909
|
+
[this.enginePtr, dataPtr, resource.length]
|
|
830
910
|
);
|
|
831
|
-
checkResult(result,
|
|
911
|
+
checkResult(result, 'Failed to set filter from data');
|
|
832
912
|
} finally {
|
|
833
913
|
Module._free(dataPtr);
|
|
834
914
|
}
|
|
835
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
'
|
|
965
|
+
'fb_set_sticker',
|
|
852
966
|
'number',
|
|
853
|
-
['number', 'string'
|
|
854
|
-
[this.enginePtr,
|
|
967
|
+
['number', 'string'],
|
|
968
|
+
[this.enginePtr, resource]
|
|
855
969
|
);
|
|
856
|
-
checkResult(result,
|
|
857
|
-
|
|
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
|
-
'
|
|
979
|
+
'fb_set_sticker_data',
|
|
863
980
|
'number',
|
|
864
|
-
['number', '
|
|
865
|
-
[this.enginePtr,
|
|
981
|
+
['number', 'number', 'number'],
|
|
982
|
+
[this.enginePtr, dataPtr, resource.length]
|
|
866
983
|
);
|
|
867
|
-
checkResult(result,
|
|
984
|
+
checkResult(result, 'Failed to set sticker from data');
|
|
868
985
|
} finally {
|
|
869
986
|
Module._free(dataPtr);
|
|
870
987
|
}
|
|
871
|
-
|
|
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
|
-
*
|
|
878
|
-
* @param {string} filterId - Filter ID to unregister
|
|
995
|
+
* Clears the current 2D sticker.
|
|
879
996
|
*/
|
|
880
|
-
|
|
997
|
+
clearSticker() {
|
|
881
998
|
this._ensureInitialized();
|
|
882
999
|
const Module = this._getWasmModule();
|
|
883
1000
|
const result = Module.ccall(
|
|
884
|
-
'
|
|
1001
|
+
'fb_clear_sticker',
|
|
885
1002
|
'number',
|
|
886
|
-
['number'
|
|
887
|
-
[this.enginePtr
|
|
1003
|
+
['number'],
|
|
1004
|
+
[this.enginePtr]
|
|
888
1005
|
);
|
|
889
|
-
checkResult(result,
|
|
1006
|
+
checkResult(result, 'Failed to clear sticker');
|
|
890
1007
|
}
|
|
891
1008
|
|
|
892
1009
|
/**
|
|
893
|
-
*
|
|
1010
|
+
* Internal method to set a single intensity parameter
|
|
1011
|
+
* @private
|
|
894
1012
|
*/
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
1020
|
+
functionName,
|
|
900
1021
|
'number',
|
|
901
|
-
['number'],
|
|
902
|
-
[this.enginePtr]
|
|
1022
|
+
['number', 'number'],
|
|
1023
|
+
[this.enginePtr, value]
|
|
903
1024
|
);
|
|
904
|
-
|
|
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
|
-
|
|
1033
|
+
cFuncName,
|
|
916
1034
|
'number',
|
|
917
|
-
['number', '
|
|
918
|
-
[this.enginePtr,
|
|
1035
|
+
['number', 'number'],
|
|
1036
|
+
[this.enginePtr, value]
|
|
919
1037
|
);
|
|
920
|
-
checkResult(result,
|
|
1038
|
+
checkResult(result, errorMessage);
|
|
921
1039
|
}
|
|
922
1040
|
|
|
923
1041
|
/**
|
|
924
|
-
*
|
|
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 <
|
|
986
|
-
throw new FacebetterError('Parameter value must be between
|
|
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
|
|
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 *
|
|
1096
|
+
// Face data: maxFaces * 342 floats * 4 bytes = maxFaces * 1368 bytes
|
|
1026
1097
|
const metadataSize = 2 * 4; // 2 ints
|
|
1027
|
-
const faceDataSize = maxFaces *
|
|
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 =
|
|
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
|
-
'
|
|
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
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
* @param {number} [
|
|
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
|
-
|
|
1207
|
+
setVirtualBackgroundBlur(level) {
|
|
1140
1208
|
this._ensureInitialized();
|
|
1141
1209
|
const Module = this._getWasmModule();
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
'
|
|
1234
|
+
'fb_set_virtual_background',
|
|
1164
1235
|
'number',
|
|
1165
|
-
['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
|
-
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1285
|
+
setBeautySkinOnly(enabled) {
|
|
1191
1286
|
this._ensureInitialized();
|
|
1192
1287
|
const Module = this._getWasmModule();
|
|
1193
1288
|
|
|
1194
1289
|
const result = Module.ccall(
|
|
1195
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
|
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
|
-
|
|
1486
|
+
_ensureFacebetterCanvas() {
|
|
1363
1487
|
if (typeof document === 'undefined') {
|
|
1364
1488
|
return;
|
|
1365
1489
|
}
|
|
1366
1490
|
|
|
1367
|
-
let canvas = document.getElementById('
|
|
1491
|
+
let canvas = document.getElementById('facebetter_canvas');
|
|
1368
1492
|
if (!canvas) {
|
|
1369
1493
|
canvas = document.createElement('canvas');
|
|
1370
|
-
canvas.id = '
|
|
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.
|
|
1499
|
+
this._createdFacebetterCanvas = true;
|
|
1376
1500
|
}
|
|
1377
|
-
this.
|
|
1501
|
+
this._facebetterCanvas = canvas;
|
|
1378
1502
|
}
|
|
1379
1503
|
|
|
1380
1504
|
/**
|
|
1381
|
-
* Cleans up
|
|
1505
|
+
* Cleans up facebetter_canvas if it was created by this engine instance (browser only)
|
|
1382
1506
|
* @private
|
|
1383
1507
|
*/
|
|
1384
|
-
|
|
1385
|
-
if (this.
|
|
1386
|
-
this.
|
|
1387
|
-
this.
|
|
1388
|
-
this.
|
|
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
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
* @param {
|
|
1482
|
-
* @param {string} [options.wasmUrl]
|
|
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
|
-
|
|
1819
|
+
let factory;
|
|
1503
1820
|
if (options.wasmUrl) {
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
|
1880
|
+
function ensureFacebetterCanvas() {
|
|
1580
1881
|
if (typeof document === 'undefined') {
|
|
1581
1882
|
return;
|
|
1582
1883
|
}
|
|
1583
1884
|
|
|
1584
|
-
let canvas = document.getElementById('
|
|
1885
|
+
let canvas = document.getElementById('facebetter_canvas');
|
|
1585
1886
|
if (!canvas) {
|
|
1586
1887
|
canvas = document.createElement('canvas');
|
|
1587
|
-
canvas.id = '
|
|
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
|
|
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
|
-
|
|
1608
|
-
|
|
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
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
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
|
|
1633
|
-
const
|
|
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
|
|
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 {
|
|
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
|