@incodetech/core 0.0.0-dev-20260803-ebbd53d1 → 0.0.0-dev-20260803-e2b5d35d

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.
Files changed (67) hide show
  1. package/dist/{3dAvatarProvider-D9H4rR6_.esm.js → 3dAvatarProvider-1zpeX-h8.esm.js} +2 -2
  2. package/dist/Face2DProvider-TLwFIRn4.esm.js +437 -0
  3. package/dist/{PrivacyLensProvider-DA3rp6sf.esm.js → PrivacyLensProvider-D8PS7AJa.esm.js} +3 -325
  4. package/dist/ae-signature.esm.js +1 -1
  5. package/dist/{apiError-CbwItA4n.esm.js → apiError-C_08gwg2.esm.js} +1 -1
  6. package/dist/authentication.esm.js +2 -2
  7. package/dist/{authenticationManager-CJnTBpA5.esm.js → authenticationManager-D76jxYNX.esm.js} +2 -2
  8. package/dist/{authenticationStateMachine-C9E6Bi5e.esm.js → authenticationStateMachine-CJILuEbH.esm.js} +2 -2
  9. package/dist/avatar.esm.js +1 -1
  10. package/dist/avatar2dWarm-DqucNFX4.esm.js +1330 -0
  11. package/dist/{avatarStats-BIB3SREM.esm.js → avatarStats-CdUOb3VD.esm.js} +12 -22
  12. package/dist/consent.d.ts +1 -1
  13. package/dist/{consentManager-3Pbtj-RV.d.ts → consentManager-uhDPdU6i.d.ts} +4 -4
  14. package/dist/createFaceAvatar-AF92I3xf.esm.js +103 -0
  15. package/dist/{deepsightLoader-CFgYDzIx.esm.js → deepsightLoader-DqEfPplN.esm.js} +3 -3
  16. package/dist/document-upload.d.ts +4 -4
  17. package/dist/dynamic-forms.esm.js +2 -2
  18. package/dist/{dynamicFormsManager-CGvQv7G3.esm.js → dynamicFormsManager-BHfeedY2.esm.js} +1 -1
  19. package/dist/{dynamicFormsStateMachine-D9QzY6PF.esm.js → dynamicFormsStateMachine-BeKhow9E.esm.js} +1 -1
  20. package/dist/ekyb.esm.js +3 -3
  21. package/dist/{ekybManager-CNYQNvLb.esm.js → ekybManager-3pPpvD6Z.esm.js} +1 -1
  22. package/dist/{ekybStateMachine-CZEkravN.esm.js → ekybStateMachine-BH8TaB7v.esm.js} +1 -1
  23. package/dist/electronic-signature.esm.js +1 -1
  24. package/dist/{electronicSignatureManager-Ow5bfK_H.esm.js → electronicSignatureManager-DHC0bqr9.esm.js} +1 -1
  25. package/dist/extensibility.d.ts +2 -2
  26. package/dist/extensibility.esm.js +8 -8
  27. package/dist/{faceCaptureManagerFactory-bQm5Ehbz.esm.js → faceCaptureManagerFactory-DqQCDsDY.esm.js} +1 -1
  28. package/dist/{faceCaptureSetup-DalEA-yE.esm.js → faceCaptureSetup-5Ye-LXbB.esm.js} +25 -10
  29. package/dist/flow.esm.js +3 -3
  30. package/dist/{flowCompletionService-B-R8PTe_.esm.js → flowCompletionService-DEO2OqAI.esm.js} +1 -1
  31. package/dist/{flowServices-B8Vudgcc.esm.js → flowServices-D6T8fwIf.esm.js} +3 -3
  32. package/dist/geolocation.esm.js +2 -2
  33. package/dist/{geolocationManager-B4YMJjCC.esm.js → geolocationManager-IbghOO6X.esm.js} +1 -1
  34. package/dist/{geolocationStateMachine-BG7Cyrol.esm.js → geolocationStateMachine-B5WkRLnz.esm.js} +1 -1
  35. package/dist/id-selfie-flow.esm.js +6 -6
  36. package/dist/id.esm.js +3 -3
  37. package/dist/{idCaptureManager-CxAQ5uN8.esm.js → idCaptureManager-C_q4C_aV.esm.js} +1 -1
  38. package/dist/{idCaptureServices-BEOo3Az7.esm.js → idCaptureServices-DlnEwYJJ.esm.js} +2 -2
  39. package/dist/{idCaptureStateMachine-rCoBc9xL.esm.js → idCaptureStateMachine-CKJ_aV6s.esm.js} +7 -7
  40. package/dist/index.esm.js +1 -1
  41. package/dist/mandatory-consent.d.ts +1 -1
  42. package/dist/{mandatoryConsentManager-DyPzUmsZ.d.ts → mandatoryConsentManager-B29kR0XA.d.ts} +4 -4
  43. package/dist/{mediapipeShared-B0iQQ5k7.esm.js → mediapipeShared-D0syL_Gp.esm.js} +6 -1
  44. package/dist/{onDeviceFaceResults-DhAPaF-B.esm.js → onDeviceFaceResults-D53Rr_B1.esm.js} +3 -3
  45. package/dist/personhood.esm.js +3 -3
  46. package/dist/privacyLensWorker-DI-Rg83h.esm.js +355 -0
  47. package/dist/qe-signature.esm.js +1 -1
  48. package/dist/{recordingService-BhvOeRAU.esm.js → recordingService-Dv5Ztm5Y.esm.js} +1 -1
  49. package/dist/{runChildModule-CDY_0-gd.esm.js → runChildModule-Dh9HjxbH.esm.js} +1 -1
  50. package/dist/selfie.esm.js +4 -4
  51. package/dist/{selfieManager-xn3h9FHA.esm.js → selfieManager-CwLK0icS.esm.js} +2 -2
  52. package/dist/{selfieStateMachine-B4jZaYKT.esm.js → selfieStateMachine-C1ErWDmF.esm.js} +2 -2
  53. package/dist/{session-D2HB_4kC.esm.js → session-DvCKSn2k.esm.js} +4 -4
  54. package/dist/session.esm.js +4 -4
  55. package/dist/{sessionInitializer-DIlag2I4.esm.js → sessionInitializer-DR77TDcE.esm.js} +2 -2
  56. package/dist/{setup-vMbDXtw1.esm.js → setup-DSLbrjHG.esm.js} +4 -4
  57. package/dist/trueSight-CGZ_9YL1.esm.js +1 -0
  58. package/dist/{trueSightService-D_YtkNAI.esm.js → trueSightService-PXOP5bS9.esm.js} +1 -1
  59. package/dist/video-selfie.esm.js +3 -3
  60. package/dist/{videoSelfieCameraWarmer-BQdhYAnF.esm.js → videoSelfieCameraWarmer-BLrAyxUW.esm.js} +2 -2
  61. package/dist/wasm.esm.js +1 -1
  62. package/dist/workflow.d.ts +4 -4
  63. package/dist/workflow.esm.js +14 -14
  64. package/package.json +1 -1
  65. package/dist/Face2DProvider-BZCsS_sf.esm.js +0 -824
  66. package/dist/createFaceAvatar-x5aaJwVM.esm.js +0 -42
  67. package/dist/trueSight-CVAkl7wM.esm.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { t as createHiddenVideoElement } from "./video-BtSWtHy1.esm.js";
2
- import { n as createFaceLandmarker, r as dtAlpha, t as asRvfc } from "./mediapipeShared-B0iQQ5k7.esm.js";
2
+ import { n as createFaceLandmarker, r as dtAlpha, t as asRvfc } from "./mediapipeShared-D0syL_Gp.esm.js";
3
3
  /**
4
4
  * @license
5
5
  * Copyright 2010-2024 Three.js Authors
@@ -28142,4 +28142,4 @@ function createProvider() {
28142
28142
  return new Avatar3DProvider();
28143
28143
  }
28144
28144
  //#endregion
28145
- export { createProvider };
28145
+ export { Avatar3DProvider, createProvider };
@@ -0,0 +1,437 @@
1
+ import { t as createHiddenVideoElement } from "./video-BtSWtHy1.esm.js";
2
+ import { a as publishAvatarStats, r as isAvatarStatsEnabled, t as createAvatarStatsReporter } from "./avatarStats-CdUOb3VD.esm.js";
3
+ import { n as createFaceLandmarker, t as asRvfc } from "./mediapipeShared-D0syL_Gp.esm.js";
4
+ import { i as detectWorkerSourceInstrumentation, o as createFramePacer, r as createTrackProcessor } from "./privacyLensWorker-DI-Rg83h.esm.js";
5
+ import { a as createAvatar2DPoseCore, c as SatinSmileyRenderer, i as AVATAR2D_POSE_CORE_DEFAULTS, l as resolveAvatar2DAssets, n as buildAvatar2DTrackWorkerSource, o as createAvatar2DPoseCoreDeps, r as canUseAvatar2DWorker, s as extractCompactFace, t as buildAvatar2DRenderWorkerSource, takeWarmAvatar2DWorker } from "./avatar2dWarm-DqucNFX4.esm.js";
6
+ //#region ../infra/src/avatar/2d-default-avatar/Face2DProvider.ts
7
+ /**
8
+ * 2D satin-face avatar — MediaPipe-tracked, 2D sprite renderer.
9
+ * **Self-contained** (its own MediaPipe loop; see FACE_AVATAR.md → Isolation and ownership).
10
+ * Carries mobile robustness patterns (FACE_AVATAR.md → Mobile hardening): `window.Module` shield,
11
+ * CPU delegate, transformation matrix OFF, landmark-derived pose, NaN guards.
12
+ */
13
+ const DEFAULT_MAX_FPS = 30;
14
+ const DETECT_MAX_EDGE = 320;
15
+ const DETECT_MIN_INTERVAL_MS = 66;
16
+ const DETECT_INTERVAL_TOLERANCE_MS = 8;
17
+ const MAX_DETECT_FAILURES = 30;
18
+ const WORKER_START_TIMEOUT_MS = 5e3;
19
+ /** Grace period for the worker to ack 'stop' before hard termination. */
20
+ const WORKER_STOP_GRACE_MS = 500;
21
+ /** Cadence of the main→worker dev statsEnabled/state-override bridge. */
22
+ const BRIDGE_INTERVAL_MS = 250;
23
+ /** Dev override global (poc demos): workers can't see it, so it is bridged. */
24
+ function readAvatarStateOverride() {
25
+ const v = globalThis.__incodeAvatarState;
26
+ return v === "detecting" || v === "success" || v === "error" ? v : null;
27
+ }
28
+ var Avatar2DProvider = class {
29
+ constructor() {
30
+ this.renderer = new SatinSmileyRenderer(512);
31
+ this.videoHandle = null;
32
+ this.landmarker = null;
33
+ this.inputCanvas = document.createElement("canvas");
34
+ this.inputCtx = null;
35
+ this.rafId = null;
36
+ this.pacer = createFramePacer(DEFAULT_MAX_FPS);
37
+ this.stats = createAvatarStatsReporter("avatar-2d");
38
+ this.stopped = false;
39
+ this.renderWorker = null;
40
+ this.trackWorker = null;
41
+ this.clonedTrack = null;
42
+ this.bridgeId = null;
43
+ this.lastBridgedStatsEnabled = false;
44
+ this.lastBridgedOverride = null;
45
+ this.mediapipeModulePath = "";
46
+ this.mediapipeWasmPath = "";
47
+ this.faceLandmarkerModelPath = "";
48
+ this.detectFailures = 0;
49
+ this.poseCore = createAvatar2DPoseCore(AVATAR2D_POSE_CORE_DEFAULTS, createAvatar2DPoseCoreDeps());
50
+ this.lastDetectAt = 0;
51
+ this.usingRvfc = false;
52
+ this.rvfcHandle = null;
53
+ this.lastSetState = null;
54
+ this.loop = (now) => {
55
+ if (this.stopped) return;
56
+ this.scheduleNextFrame();
57
+ if (!this.pacer.shouldRender(now)) {
58
+ this.stats.frameDropped();
59
+ return;
60
+ }
61
+ const measure = isAvatarStatsEnabled();
62
+ const frameStart = measure ? performance.now() : 0;
63
+ let detectMs;
64
+ const video = this.videoHandle?.element;
65
+ if (video && video.readyState >= 2 && this.landmarker) this.detectOnMainThread(video, now, measure, (ms) => {
66
+ detectMs = ms;
67
+ });
68
+ this.renderer.render(this.poseCore.tick(now));
69
+ if (measure) this.stats.frameRendered(now, detectMs, performance.now() - frameStart);
70
+ };
71
+ }
72
+ getCanvas() {
73
+ return this.renderer.getCanvas();
74
+ }
75
+ setState(state) {
76
+ this.renderer.setState(state);
77
+ this.lastSetState = state;
78
+ try {
79
+ this.renderWorker?.postMessage({
80
+ type: "state",
81
+ state
82
+ });
83
+ } catch {}
84
+ }
85
+ async start(stream, options = {}) {
86
+ const assets = resolveAvatar2DAssets({
87
+ mediapipeModulePath: options.assets?.mediapipeModulePath,
88
+ mediapipeWasmPath: options.assets?.mediapipeWasmPath,
89
+ faceLandmarkerModelPath: options.assets?.faceLandmarkerModelPath
90
+ });
91
+ this.mediapipeModulePath = assets.mediapipeModulePath;
92
+ this.mediapipeWasmPath = assets.mediapipeWasmPath;
93
+ this.faceLandmarkerModelPath = assets.faceLandmarkerModelPath;
94
+ const maxFps = options.maxFps ?? DEFAULT_MAX_FPS;
95
+ this.pacer = createFramePacer(maxFps);
96
+ this.poseCore = createAvatar2DPoseCore({
97
+ ...AVATAR2D_POSE_CORE_DEFAULTS,
98
+ dtSeedMs: 1e3 / maxFps
99
+ }, createAvatar2DPoseCoreDeps());
100
+ if (await this.tryStartWorker(stream, maxFps)) return;
101
+ if (this.stopped) return;
102
+ this.videoHandle = createHiddenVideoElement(stream, { keepStreamAlive: true });
103
+ try {
104
+ this.renderer.init();
105
+ const video = this.videoHandle.element;
106
+ this.usingRvfc = !!asRvfc(video);
107
+ this.scheduleNextFrame();
108
+ this.startMainTracking();
109
+ } catch (error) {
110
+ this.stop();
111
+ throw error;
112
+ }
113
+ }
114
+ /**
115
+ * Background landmarker bring-up for the MAIN-THREAD path. On failure the
116
+ * avatar stays in its neutral searching state (the face remains concealed)
117
+ * rather than tearing down the already-mounted canvas.
118
+ */
119
+ async startMainTracking() {
120
+ try {
121
+ this.landmarker = await this.createLandmarker();
122
+ if (this.stopped) {
123
+ this.landmarker?.close();
124
+ this.landmarker = null;
125
+ return;
126
+ }
127
+ this.stats.dispose();
128
+ this.stats = createAvatarStatsReporter("avatar-2d", "main");
129
+ } catch (error) {
130
+ console.warn("[face-avatar] avatar-2d tracking failed to start — avatar stays in searching state (capture unaffected)", error);
131
+ }
132
+ }
133
+ /** rVFC (one callback per camera frame) when available, else capped rAF. (§1.) */
134
+ scheduleNextFrame() {
135
+ if (this.stopped) return;
136
+ const video = this.videoHandle?.element;
137
+ const rvfc = this.usingRvfc && video ? asRvfc(video) : null;
138
+ if (rvfc) this.rvfcHandle = rvfc.requestVideoFrameCallback(this.loop);
139
+ else this.rafId = requestAnimationFrame(this.loop);
140
+ }
141
+ stop() {
142
+ this.stopped = true;
143
+ this.stats.dispose();
144
+ if (this.bridgeId != null) {
145
+ clearInterval(this.bridgeId);
146
+ this.bridgeId = null;
147
+ }
148
+ if (this.renderWorker || this.trackWorker) {
149
+ try {
150
+ this.renderWorker?.postMessage({ type: "stop" });
151
+ this.trackWorker?.postMessage({ type: "stop" });
152
+ } catch {}
153
+ const pendingRender = this.renderWorker;
154
+ const pendingTrack = this.trackWorker;
155
+ setTimeout(() => {
156
+ if (this.renderWorker === pendingRender || this.trackWorker === pendingTrack) this.finalizeWorkers();
157
+ }, WORKER_STOP_GRACE_MS);
158
+ }
159
+ if (this.rafId != null) {
160
+ cancelAnimationFrame(this.rafId);
161
+ this.rafId = null;
162
+ }
163
+ const stoppingVideo = this.videoHandle?.element;
164
+ if (stoppingVideo && this.rvfcHandle != null) {
165
+ asRvfc(stoppingVideo)?.cancelVideoFrameCallback(this.rvfcHandle);
166
+ this.rvfcHandle = null;
167
+ }
168
+ this.landmarker?.close();
169
+ this.landmarker = null;
170
+ this.renderer.dispose();
171
+ this.videoHandle?.dispose();
172
+ this.videoHandle = null;
173
+ }
174
+ /**
175
+ * Worker transport, TWO workers: clone the track, feed it to a
176
+ * MediaStreamTrackProcessor, adopt (or boot) the TRACK worker, boot the
177
+ * RENDER worker, wire them with a MessageChannel, transfer the renderer's
178
+ * canvas + the readable, and wait for the render worker's ms-fast 'started'
179
+ * handshake (the track worker's MediaPipe bring-up runs in the background —
180
+ * or already ran via the tutorial warm-up). Any failure tears down —
181
+ * recreating the renderer if its canvas was already transferred — and
182
+ * returns false so the main-thread path takes over. Never throws.
183
+ */
184
+ async tryStartWorker(stream, maxFps) {
185
+ if (!canUseAvatar2DWorker()) return false;
186
+ const sourceTrack = stream.getVideoTracks()[0];
187
+ if (!sourceTrack) return false;
188
+ let track = null;
189
+ let processor;
190
+ try {
191
+ track = sourceTrack.clone();
192
+ processor = createTrackProcessor(track);
193
+ } catch {
194
+ track?.stop();
195
+ return false;
196
+ }
197
+ let trackWorker = takeWarmAvatar2DWorker({
198
+ mediapipeModulePath: this.mediapipeModulePath,
199
+ mediapipeWasmPath: this.mediapipeWasmPath,
200
+ faceLandmarkerModelPath: this.faceLandmarkerModelPath
201
+ });
202
+ let trackBlobUrl = null;
203
+ if (!trackWorker) {
204
+ const trackSource = buildAvatar2DTrackWorkerSource();
205
+ const trackInstrumenter = detectWorkerSourceInstrumentation(trackSource);
206
+ if (trackInstrumenter) {
207
+ console.error(`[face-avatar] avatar-2d worker source was instrumented by "${trackInstrumenter}" and cannot run in a worker; using the slower main-thread avatar. Exclude packages/infra from that transform (see docs/features/FACE_AVATAR.md → Worker transport).`);
208
+ track.stop();
209
+ return false;
210
+ }
211
+ try {
212
+ trackBlobUrl = URL.createObjectURL(new Blob([trackSource], { type: "text/javascript" }));
213
+ trackWorker = new Worker(trackBlobUrl);
214
+ } catch {
215
+ if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
216
+ track.stop();
217
+ return false;
218
+ }
219
+ }
220
+ const renderSource = buildAvatar2DRenderWorkerSource();
221
+ const renderInstrumenter = detectWorkerSourceInstrumentation(renderSource);
222
+ let renderWorker = null;
223
+ let renderBlobUrl = null;
224
+ if (!renderInstrumenter) try {
225
+ renderBlobUrl = URL.createObjectURL(new Blob([renderSource], { type: "text/javascript" }));
226
+ renderWorker = new Worker(renderBlobUrl);
227
+ } catch {
228
+ if (renderBlobUrl) URL.revokeObjectURL(renderBlobUrl);
229
+ renderBlobUrl = null;
230
+ renderWorker = null;
231
+ }
232
+ if (!renderWorker) {
233
+ if (renderInstrumenter) console.error(`[face-avatar] avatar-2d worker source was instrumented by "${renderInstrumenter}" and cannot run in a worker; using the slower main-thread avatar. Exclude packages/infra from that transform (see docs/features/FACE_AVATAR.md → Worker transport).`);
234
+ trackWorker.terminate();
235
+ if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
236
+ track.stop();
237
+ return false;
238
+ }
239
+ trackWorker.onmessage = (event) => {
240
+ const message = event.data;
241
+ if (message?.type === "trackingFatal") {
242
+ console.warn("[face-avatar] avatar-2d worker tracking failed to start — avatar stays in searching state (capture unaffected):", message.message);
243
+ trackWorker.terminate();
244
+ if (this.trackWorker === trackWorker) this.trackWorker = null;
245
+ track.stop();
246
+ if (this.clonedTrack === track) this.clonedTrack = null;
247
+ } else if (message?.type === "stopped") {
248
+ trackWorker.terminate();
249
+ if (this.trackWorker === trackWorker) this.trackWorker = null;
250
+ track.stop();
251
+ if (this.clonedTrack === track) this.clonedTrack = null;
252
+ }
253
+ };
254
+ trackWorker.onerror = () => {
255
+ console.warn("[face-avatar] avatar-2d track worker error — pose frozen (capture unaffected)");
256
+ trackWorker.terminate();
257
+ if (this.trackWorker === trackWorker) this.trackWorker = null;
258
+ track.stop();
259
+ if (this.clonedTrack === track) this.clonedTrack = null;
260
+ };
261
+ let started;
262
+ try {
263
+ const channel = new MessageChannel();
264
+ const offscreen = this.renderer.getCanvas().transferControlToOffscreen();
265
+ const handshakeWorker = renderWorker;
266
+ const startedTrackWorker = trackWorker;
267
+ started = await new Promise((resolve) => {
268
+ const timer = setTimeout(() => resolve(false), WORKER_START_TIMEOUT_MS);
269
+ handshakeWorker.onmessage = (event) => {
270
+ if (event.data?.type === "started") {
271
+ clearTimeout(timer);
272
+ resolve(true);
273
+ } else if (event.data?.type === "fatal") {
274
+ clearTimeout(timer);
275
+ resolve(false);
276
+ }
277
+ };
278
+ handshakeWorker.onerror = () => {
279
+ clearTimeout(timer);
280
+ resolve(false);
281
+ };
282
+ handshakeWorker.postMessage({
283
+ type: "start",
284
+ canvas: offscreen,
285
+ port: channel.port1,
286
+ maxFps,
287
+ poseConfig: {
288
+ ...AVATAR2D_POSE_CORE_DEFAULTS,
289
+ dtSeedMs: 1e3 / maxFps
290
+ },
291
+ statsEnabled: isAvatarStatsEnabled()
292
+ }, [offscreen, channel.port1]);
293
+ startedTrackWorker.postMessage({
294
+ type: "start",
295
+ readable: processor.readable,
296
+ port: channel.port2,
297
+ mediapipeModulePath: this.mediapipeModulePath,
298
+ mediapipeWasmPath: this.mediapipeWasmPath,
299
+ faceLandmarkerModelPath: this.faceLandmarkerModelPath,
300
+ detectMaxEdge: DETECT_MAX_EDGE,
301
+ detectMinIntervalMs: DETECT_MIN_INTERVAL_MS,
302
+ detectIntervalToleranceMs: DETECT_INTERVAL_TOLERANCE_MS
303
+ }, [processor.readable, channel.port2]);
304
+ });
305
+ } catch {
306
+ renderWorker.terminate();
307
+ trackWorker.terminate();
308
+ if (renderBlobUrl) URL.revokeObjectURL(renderBlobUrl);
309
+ if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
310
+ track.stop();
311
+ if (!this.stopped) this.renderer = new SatinSmileyRenderer(512);
312
+ return false;
313
+ }
314
+ if (renderBlobUrl) URL.revokeObjectURL(renderBlobUrl);
315
+ if (trackBlobUrl) URL.revokeObjectURL(trackBlobUrl);
316
+ if (!started || this.stopped) {
317
+ renderWorker.terminate();
318
+ trackWorker.terminate();
319
+ track.stop();
320
+ if (!this.stopped) this.renderer = new SatinSmileyRenderer(512);
321
+ return this.stopped;
322
+ }
323
+ this.renderWorker = renderWorker;
324
+ this.trackWorker = trackWorker;
325
+ this.clonedTrack = track;
326
+ this.lastBridgedStatsEnabled = isAvatarStatsEnabled();
327
+ this.lastBridgedOverride = null;
328
+ renderWorker.onmessage = (event) => {
329
+ const message = event.data;
330
+ if (message?.type === "stats") {
331
+ if (!this.stopped) publishAvatarStats(message.snapshot);
332
+ } else if (message?.type === "stopped") {
333
+ this.renderWorker?.terminate();
334
+ this.renderWorker = null;
335
+ }
336
+ };
337
+ renderWorker.onerror = () => {
338
+ console.warn("[face-avatar] avatar-2d render worker error — avatar frozen (capture unaffected)");
339
+ if (this.bridgeId != null) {
340
+ clearInterval(this.bridgeId);
341
+ this.bridgeId = null;
342
+ }
343
+ this.finalizeWorkers();
344
+ };
345
+ if (this.lastSetState) renderWorker.postMessage({
346
+ type: "state",
347
+ state: this.lastSetState
348
+ });
349
+ this.bridgeId = setInterval(() => {
350
+ if (!this.renderWorker) return;
351
+ const enabled = isAvatarStatsEnabled();
352
+ if (enabled !== this.lastBridgedStatsEnabled) {
353
+ this.lastBridgedStatsEnabled = enabled;
354
+ this.renderWorker.postMessage({
355
+ type: "statsEnabled",
356
+ value: enabled
357
+ });
358
+ }
359
+ const override = readAvatarStateOverride();
360
+ if (override !== this.lastBridgedOverride) {
361
+ this.lastBridgedOverride = override;
362
+ this.renderWorker.postMessage({
363
+ type: "override",
364
+ state: override
365
+ });
366
+ }
367
+ }, BRIDGE_INTERVAL_MS);
368
+ return true;
369
+ }
370
+ finalizeWorkers() {
371
+ this.renderWorker?.terminate();
372
+ this.renderWorker = null;
373
+ this.trackWorker?.terminate();
374
+ this.trackWorker = null;
375
+ this.clonedTrack?.stop();
376
+ this.clonedTrack = null;
377
+ }
378
+ /** Loads the FaceLandmarker via the shared shielded init (see mediapipeShared). */
379
+ createLandmarker() {
380
+ return createFaceLandmarker({
381
+ mediapipeModulePath: this.mediapipeModulePath,
382
+ mediapipeWasmPath: this.mediapipeWasmPath,
383
+ faceLandmarkerModelPath: this.faceLandmarkerModelPath
384
+ });
385
+ }
386
+ /** Main-thread detect fallback: downscale → detectForVideo → ingest. */
387
+ detectOnMainThread(video, now, measure, onDetectMs) {
388
+ if (!this.landmarker) return;
389
+ const vw = video.videoWidth;
390
+ const vh = video.videoHeight;
391
+ const detectDue = now - this.lastDetectAt >= DETECT_MIN_INTERVAL_MS - DETECT_INTERVAL_TOLERANCE_MS;
392
+ if (vw > 0 && vh > 0 && detectDue) {
393
+ if (!this.inputCtx) this.inputCtx = this.inputCanvas.getContext("2d");
394
+ if (this.inputCtx) {
395
+ this.lastDetectAt = now;
396
+ const dscale = Math.min(1, DETECT_MAX_EDGE / Math.max(vw, vh));
397
+ const dw = Math.max(1, Math.round(vw * dscale));
398
+ const dh = Math.max(1, Math.round(vh * dscale));
399
+ if (this.inputCanvas.width !== dw || this.inputCanvas.height !== dh) {
400
+ this.inputCanvas.width = dw;
401
+ this.inputCanvas.height = dh;
402
+ }
403
+ this.inputCtx.drawImage(video, 0, 0, dw, dh);
404
+ let result = null;
405
+ const detectStart = measure ? performance.now() : 0;
406
+ try {
407
+ result = this.landmarker.detectForVideo(this.inputCanvas, now);
408
+ this.detectFailures = 0;
409
+ } catch {
410
+ result = null;
411
+ this.detectFailures += 1;
412
+ if (this.detectFailures >= MAX_DETECT_FAILURES) this.recreateLandmarker();
413
+ }
414
+ if (measure) onDetectMs(performance.now() - detectStart);
415
+ this.poseCore.ingest(result ? extractCompactFace(result) : null, dw, dh, now);
416
+ }
417
+ }
418
+ }
419
+ recreateLandmarker() {
420
+ this.detectFailures = 0;
421
+ this.landmarker?.close();
422
+ this.landmarker = null;
423
+ this.createLandmarker().then((lm) => {
424
+ if (this.stopped) {
425
+ lm.close();
426
+ return;
427
+ }
428
+ this.landmarker = lm;
429
+ }).catch(() => {});
430
+ }
431
+ };
432
+ /** Factory used by the avatar actor's provider selection (2D satin-face avatar). */
433
+ function createProvider() {
434
+ return new Avatar2DProvider();
435
+ }
436
+ //#endregion
437
+ export { Avatar2DProvider, createProvider };