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