@viji-dev/core 0.3.24 → 0.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,29 +4,422 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Global viji object available in artist code
8
- * @type {VijiAPI}
7
+ * Configuration for slider parameters
8
+ * @typedef {Object} SliderConfig
9
+ * @property {number} [min] - min property
10
+ * @property {number} [max] - max property
11
+ * @property {number} [step] - step property
12
+ * @property {string} label - label property
13
+ * @property {string} [description] - description property
14
+ * @property {string} [group] - group property
15
+ * @property {ParameterCategory} [category] - category property
9
16
  */
10
- declare const viji;
11
17
 
12
18
  /**
13
- * Type alias for render function
14
- * @typedef {function(VijiAPI): void} Render
19
+ * Configuration for color parameters
20
+ * @typedef {Object} ColorConfig
21
+ * @property {string} label - label property
22
+ * @property {string} [description] - description property
23
+ * @property {string} [group] - group property
24
+ * @property {ParameterCategory} [category] - category property
15
25
  */
16
26
 
17
27
  /**
18
- * Type alias for setup function
19
- * @typedef {function(VijiAPI): void} Setup
28
+ * Configuration for toggle parameters
29
+ * @typedef {Object} ToggleConfig
30
+ * @property {string} label - label property
31
+ * @property {string} [description] - description property
32
+ * @property {string} [group] - group property
33
+ * @property {ParameterCategory} [category] - category property
34
+ */
35
+
36
+ /**
37
+ * Configuration for select parameters
38
+ * @typedef {Object} SelectConfig
39
+ * @property {string[] | number[]} options - options property
40
+ * @property {string} label - label property
41
+ * @property {string} [description] - description property
42
+ * @property {string} [group] - group property
43
+ * @property {ParameterCategory} [category] - category property
44
+ */
45
+
46
+ /**
47
+ * Configuration for text parameters
48
+ * @typedef {Object} TextConfig
49
+ * @property {string} label - label property
50
+ * @property {string} [description] - description property
51
+ * @property {string} [group] - group property
52
+ * @property {ParameterCategory} [category] - category property
53
+ * @property {number} [maxLength] - maxLength property
54
+ */
55
+
56
+ /**
57
+ * Configuration for number parameters
58
+ * @typedef {Object} NumberConfig
59
+ * @property {number} [min] - min property
60
+ * @property {number} [max] - max property
61
+ * @property {number} [step] - step property
62
+ * @property {string} label - label property
63
+ * @property {string} [description] - description property
64
+ * @property {string} [group] - group property
65
+ * @property {ParameterCategory} [category] - category property
66
+ */
67
+
68
+ /**
69
+ * Configuration for image parameters
70
+ * @typedef {Object} ImageConfig
71
+ * @property {string} label - label property
72
+ * @property {string} [description] - description property
73
+ * @property {string} [group] - group property
74
+ * @property {ParameterCategory} [category] - category property
75
+ */
76
+
77
+ /**
78
+ * Configuration for button parameters
79
+ * @typedef {Object} ButtonConfig
80
+ * @property {string} label - label property
81
+ * @property {string} [description] - description property
82
+ * @property {string} [group] - group property
83
+ * @property {ParameterCategory} [category] - category property
84
+ */
85
+
86
+ /**
87
+ * Parameter object for slider parameters
88
+ * @typedef {Object} SliderParameter
89
+ * @property {number} value - value property
90
+ * @property {number} min - min property
91
+ * @property {number} max - max property
92
+ * @property {number} step - step property
93
+ * @property {string} label - label property
94
+ * @property {string} [description] - description property
95
+ * @property {string} group - group property
96
+ * @property {ParameterCategory} category - category property
97
+ */
98
+
99
+ /**
100
+ * Parameter object for color parameters
101
+ * @typedef {Object} ColorParameter
102
+ * @property {string} value - value property
103
+ * @property {string} label - label property
104
+ * @property {string} [description] - description property
105
+ * @property {string} group - group property
106
+ * @property {ParameterCategory} category - category property
20
107
  */
21
108
 
22
109
  /**
23
- * Artist render function - called every frame
24
- * @param {VijiAPI} viji - The viji API object with all capabilities
110
+ * Parameter object for toggle parameters
111
+ * @typedef {Object} ToggleParameter
112
+ * @property {boolean} value - value property
113
+ * @property {string} label - label property
114
+ * @property {string} [description] - description property
115
+ * @property {string} group - group property
116
+ * @property {ParameterCategory} category - category property
25
117
  */
26
- declare function render(viji): void;
27
118
 
28
119
  /**
29
- * Artist setup function - called once at initialization
30
- * @param {VijiAPI} viji - The viji API object with all capabilities
120
+ * Parameter object for select parameters
121
+ * @typedef {Object} SelectParameter
122
+ * @property {string | number} value - value property
123
+ * @property {string[] | number[]} options - options property
124
+ * @property {string} label - label property
125
+ * @property {string} [description] - description property
126
+ * @property {string} group - group property
127
+ * @property {ParameterCategory} category - category property
31
128
  */
32
- declare function setup(viji): void;
129
+
130
+ /**
131
+ * Parameter object for text parameters
132
+ * @typedef {Object} TextParameter
133
+ * @property {string} value - value property
134
+ * @property {number} [maxLength] - maxLength property
135
+ * @property {string} label - label property
136
+ * @property {string} [description] - description property
137
+ * @property {string} group - group property
138
+ * @property {ParameterCategory} category - category property
139
+ */
140
+
141
+ /**
142
+ * Parameter object for number parameters
143
+ * @typedef {Object} NumberParameter
144
+ * @property {number} value - value property
145
+ * @property {number} min - min property
146
+ * @property {number} max - max property
147
+ * @property {number} step - step property
148
+ * @property {string} label - label property
149
+ * @property {string} [description] - description property
150
+ * @property {string} group - group property
151
+ * @property {ParameterCategory} category - category property
152
+ */
153
+
154
+ /**
155
+ * Parameter object for image parameters
156
+ * @typedef {Object} ImageParameter
157
+ * @property {ImageBitmap |null} value - value property
158
+ * @property {string} label - label property
159
+ * @property {string} [description] - description property
160
+ * @property {string} group - group property
161
+ * @property {ParameterCategory} category - category property
162
+ */
163
+
164
+ /**
165
+ * Parameter object for button parameters
166
+ * @typedef {Object} ButtonParameter
167
+ * @property {boolean} value - value property
168
+ * @property {string} label - label property
169
+ * @property {string} [description] - description property
170
+ * @property {string} group - group property
171
+ * @property {ParameterCategory} category - category property
172
+ */
173
+
174
+ /**
175
+ * Audio analysis API - provides real-time audio data and frequency analysis
176
+ * @typedef {Object} AudioAPI
177
+ * @property {boolean} isConnected - isConnected property
178
+ * @property {Object} volume - volume property
179
+ * @property {number} volume.current - current property
180
+ * @property {number} volume.peak - peak property
181
+ * @property {number} volume.smoothed - smoothed property
182
+ */
183
+
184
+ /**
185
+ * Video frame API - provides access to video stream and computer vision data
186
+ * @typedef {Object} VideoAPI
187
+ * @property {boolean} isConnected - isConnected property
188
+ * @property {OffscreenCanvas | ImageBitmap |null} currentFrame - currentFrame property
189
+ * @property {number} frameWidth - frameWidth property
190
+ * @property {number} frameHeight - frameHeight property
191
+ * @property {number} frameRate - frameRate property
192
+ * @property {() => ImageData |null} getFrameData - getFrameData property
193
+ * @property {FaceData[]} faces - faces property
194
+ * @property {HandData[]} hands - hands property
195
+ * @property {PoseData |null} pose - pose property
196
+ * @property {SegmentationData |null} segmentation - segmentation property
197
+ * @property {Object} cv - cv property
198
+ */
199
+
200
+ /**
201
+ * Mouse interaction API
202
+ * @typedef {Object} MouseAPI
203
+ * @property {number} x - x property
204
+ * @property {number} y - y property
205
+ * @property {boolean} isInCanvas - isInCanvas property
206
+ * @property {boolean} isPressed - isPressed property
207
+ * @property {boolean} leftButton - leftButton property
208
+ * @property {boolean} rightButton - rightButton property
209
+ * @property {boolean} middleButton - middleButton property
210
+ * @property {number} deltaX - deltaX property
211
+ * @property {number} deltaY - deltaY property
212
+ * @property {number} wheelDelta - wheelDelta property
213
+ * @property {number} wheelX - wheelX property
214
+ * @property {number} wheelY - wheelY property
215
+ * @property {boolean} wasPressed - wasPressed property
216
+ * @property {boolean} wasReleased - wasReleased property
217
+ * @property {boolean} wasMoved - wasMoved property
218
+ */
219
+
220
+ /**
221
+ * Keyboard interaction API
222
+ * @typedef {Object} KeyboardAPI
223
+ * @property {Set<string>} activeKeys - activeKeys property
224
+ * @property {Set<string>} pressedThisFrame - pressedThisFrame property
225
+ * @property {Set<string>} releasedThisFrame - releasedThisFrame property
226
+ * @property {string} lastKeyPressed - lastKeyPressed property
227
+ * @property {string} lastKeyReleased - lastKeyReleased property
228
+ * @property {boolean} shift - shift property
229
+ * @property {boolean} ctrl - ctrl property
230
+ * @property {boolean} alt - alt property
231
+ * @property {boolean} meta - meta property
232
+ * @property {Uint8Array} textureData - textureData property
233
+ */
234
+
235
+ /**
236
+ * Touch interaction API
237
+ * @typedef {Object} TouchAPI
238
+ * @property {TouchPoint[]} points - points property
239
+ * @property {number} count - count property
240
+ * @property {TouchPoint[]} started - started property
241
+ * @property {TouchPoint[]} moved - moved property
242
+ * @property {TouchPoint[]} ended - ended property
243
+ * @property {TouchPoint |null} primary - primary property
244
+ */
245
+
246
+ /**
247
+ * Main Viji Artist API - provides access to canvas, timing, audio, video, and interactions
248
+ * @typedef {Object} VijiAPI
249
+ * @property {OffscreenCanvas} canvas - canvas property
250
+ * @property {OffscreenCanvasRenderingContext2D} [ctx] - ctx property
251
+ * @property {WebGLRenderingContext | WebGL2RenderingContext} [gl] - gl property
252
+ * @property {number} width - width property
253
+ * @property {number} height - height property
254
+ * @property {number} time - time property
255
+ * @property {number} deltaTime - deltaTime property
256
+ * @property {number} frameCount - frameCount property
257
+ * @property {number} fps - fps property
258
+ * @property {AudioAPI} audio - audio property
259
+ * @property {VideoAPI} video - video property
260
+ * @property {VideoAPI[]} streams - streams property
261
+ * @property {MouseAPI} mouse - mouse property
262
+ * @property {KeyboardAPI} keyboard - keyboard property
263
+ * @property {TouchAPI} touches - touches property
264
+ * @property {PointerAPI} pointer - pointer property
265
+ * @property {DeviceSensorState} device - device property
266
+ * @property {DeviceState[]} devices - devices property
267
+ * @property {(defaultValue: number, config: SliderConfig) => SliderParameter} slider - slider property
268
+ * @property {(defaultValue: string, config: ColorConfig) => ColorParameter} color - color property
269
+ * @property {(defaultValue: boolean, config: ToggleConfig) => ToggleParameter} toggle - toggle property
270
+ * @property {(defaultValue: string | number, config: SelectConfig) => SelectParameter} select - select property
271
+ * @property {(defaultValue: string, config: TextConfig) => TextParameter} text - text property
272
+ * @property {(defaultValue: number, config: NumberConfig) => NumberParameter} number - number property
273
+ * @property {(defaultValue: null, config: ImageConfig) => ImageParameter} image - image property
274
+ * @property {(config: ButtonConfig) => ButtonParameter} button - button property
275
+ */
276
+
277
+ /**
278
+ * FaceData interface
279
+ * @typedef {Object} FaceData
280
+ * @property {number} id - id property
281
+ * @property {Object} bounds - bounds property
282
+ * @property {number} bounds.x - x property
283
+ * @property {number} bounds.y - y property
284
+ * @property {number} bounds.width - width property
285
+ * @property {number} bounds.height - height property
286
+ */
287
+
288
+ /**
289
+ * FaceBlendshapes interface
290
+ * @typedef {Object} FaceBlendshapes
291
+ * @property {number} browDownLeft - browDownLeft property
292
+ * @property {number} browDownRight - browDownRight property
293
+ * @property {number} browInnerUp - browInnerUp property
294
+ * @property {number} browOuterUpLeft - browOuterUpLeft property
295
+ * @property {number} browOuterUpRight - browOuterUpRight property
296
+ * @property {number} cheekPuff - cheekPuff property
297
+ * @property {number} cheekSquintLeft - cheekSquintLeft property
298
+ * @property {number} cheekSquintRight - cheekSquintRight property
299
+ * @property {number} eyeBlinkLeft - eyeBlinkLeft property
300
+ * @property {number} eyeBlinkRight - eyeBlinkRight property
301
+ * @property {number} eyeLookDownLeft - eyeLookDownLeft property
302
+ * @property {number} eyeLookDownRight - eyeLookDownRight property
303
+ * @property {number} eyeLookInLeft - eyeLookInLeft property
304
+ * @property {number} eyeLookInRight - eyeLookInRight property
305
+ * @property {number} eyeLookOutLeft - eyeLookOutLeft property
306
+ * @property {number} eyeLookOutRight - eyeLookOutRight property
307
+ * @property {number} eyeLookUpLeft - eyeLookUpLeft property
308
+ * @property {number} eyeLookUpRight - eyeLookUpRight property
309
+ * @property {number} eyeSquintLeft - eyeSquintLeft property
310
+ * @property {number} eyeSquintRight - eyeSquintRight property
311
+ * @property {number} eyeWideLeft - eyeWideLeft property
312
+ * @property {number} eyeWideRight - eyeWideRight property
313
+ * @property {number} jawForward - jawForward property
314
+ * @property {number} jawLeft - jawLeft property
315
+ * @property {number} jawOpen - jawOpen property
316
+ * @property {number} jawRight - jawRight property
317
+ * @property {number} mouthClose - mouthClose property
318
+ * @property {number} mouthDimpleLeft - mouthDimpleLeft property
319
+ * @property {number} mouthDimpleRight - mouthDimpleRight property
320
+ * @property {number} mouthFrownLeft - mouthFrownLeft property
321
+ * @property {number} mouthFrownRight - mouthFrownRight property
322
+ * @property {number} mouthFunnel - mouthFunnel property
323
+ * @property {number} mouthLeft - mouthLeft property
324
+ * @property {number} mouthLowerDownLeft - mouthLowerDownLeft property
325
+ * @property {number} mouthLowerDownRight - mouthLowerDownRight property
326
+ * @property {number} mouthPressLeft - mouthPressLeft property
327
+ * @property {number} mouthPressRight - mouthPressRight property
328
+ * @property {number} mouthPucker - mouthPucker property
329
+ * @property {number} mouthRight - mouthRight property
330
+ * @property {number} mouthRollLower - mouthRollLower property
331
+ * @property {number} mouthRollUpper - mouthRollUpper property
332
+ * @property {number} mouthShrugLower - mouthShrugLower property
333
+ * @property {number} mouthShrugUpper - mouthShrugUpper property
334
+ * @property {number} mouthSmileLeft - mouthSmileLeft property
335
+ * @property {number} mouthSmileRight - mouthSmileRight property
336
+ * @property {number} mouthStretchLeft - mouthStretchLeft property
337
+ * @property {number} mouthStretchRight - mouthStretchRight property
338
+ * @property {number} mouthUpperUpLeft - mouthUpperUpLeft property
339
+ * @property {number} mouthUpperUpRight - mouthUpperUpRight property
340
+ * @property {number} noseSneerLeft - noseSneerLeft property
341
+ * @property {number} noseSneerRight - noseSneerRight property
342
+ * @property {number} tongueOut - tongueOut property
343
+ */
344
+
345
+ /**
346
+ * HandData interface
347
+ * @typedef {Object} HandData
348
+ * @property {number} id - id property
349
+ * @property {'left' | 'right'} handedness - handedness property
350
+ * @property {number} confidence - confidence property
351
+ * @property {Object} bounds - bounds property
352
+ * @property {number} bounds.x - x property
353
+ * @property {number} bounds.y - y property
354
+ * @property {number} bounds.width - width property
355
+ * @property {number} bounds.height - height property
356
+ */
357
+
358
+ /**
359
+ * PoseData interface
360
+ * @typedef {Object} PoseData
361
+ * @property {number} confidence - confidence property
362
+ * @property {Object} landmarks - landmarks property
363
+ * @property {number} landmarks.x - x property
364
+ * @property {number} landmarks.y - y property
365
+ * @property {number} landmarks.z - z property
366
+ * @property {number} landmarks.visibility - visibility property
367
+ */
368
+
369
+ /**
370
+ * SegmentationData interface
371
+ * @typedef {Object} SegmentationData
372
+ * @property {Uint8Array} mask - mask property
373
+ * @property {number} width - width property
374
+ * @property {number} height - height property
375
+ */
376
+
377
+ /**
378
+ * DeviceMotionData interface
379
+ * @typedef {Object} DeviceMotionData
380
+ * @property {Object} acceleration - Acceleration without gravity (m/s²)
381
+ * @property {number |null} acceleration.x - x property
382
+ * @property {number |null} acceleration.y - y property
383
+ * @property {number |null} acceleration.z - z property
384
+ */
385
+
386
+ /**
387
+ * DeviceOrientationData interface
388
+ * @typedef {Object} DeviceOrientationData
389
+ * @property {number |null} alpha - Rotation around Z-axis (0-360 degrees, compass heading)
390
+ * @property {number |null} beta - Rotation around X-axis (-180 to 180 degrees, front-to-back tilt)
391
+ * @property {number |null} gamma - Rotation around Y-axis (-90 to 90 degrees, left-to-right tilt)
392
+ * @property {boolean} absolute - True if using magnetometer (compass) for absolute orientation
393
+ */
394
+
395
+ /**
396
+ * DeviceSensorState interface
397
+ * @typedef {Object} DeviceSensorState
398
+ * @property {DeviceMotionData |null} motion - motion property
399
+ * @property {DeviceOrientationData |null} orientation - orientation property
400
+ */
401
+
402
+ /**
403
+ * DeviceState interface
404
+ * @typedef {Object} DeviceState
405
+ * @property {string} id - Unique device identifier
406
+ * @property {string} name - User-friendly device name
407
+ * @property {VideoAPI |null} video - Device camera video (null if not available)
408
+ */
409
+
410
+ /**
411
+ * @typedef {function(VijiAPI): void} Render
412
+ */
413
+
414
+ /**
415
+ * @typedef {function(VijiAPI): void} Setup
416
+ */
417
+
418
+ /** Global viji object available in artist code */
419
+ declare const viji: VijiAPI;
420
+
421
+ /** Artist render function - called every frame */
422
+ declare function render(viji: VijiAPI): void;
423
+
424
+ /** Artist setup function - called once at initialization */
425
+ declare function setup(viji: VijiAPI): void;
@@ -29,7 +29,7 @@ try {
29
29
  // MediaPipe model instances
30
30
  let faceDetector = null;
31
31
  let faceLandmarker = null;
32
- let handLandmarker = null;
32
+ let gestureRecognizer = null;
33
33
  let poseLandmarker = null;
34
34
  let imageSegmenter = null;
35
35
 
@@ -375,31 +375,31 @@ async function initializeFaceLandmarks() {
375
375
  }
376
376
 
377
377
  /**
378
- * Load and initialize Hand Tracking
378
+ * Load and initialize Hand Tracking via GestureRecognizer
379
+ * (provides landmarks + handedness + ML-based gesture classification in one call)
379
380
  */
380
381
  async function initializeHandTracking() {
381
- if (handLandmarker) return;
382
+ if (gestureRecognizer) return;
382
383
 
383
- // Ensure vision runtime is initialized first
384
384
  await initializeVision();
385
385
 
386
386
  try {
387
- log('Loading Hand Landmarker...');
387
+ log('Loading Gesture Recognizer...');
388
388
 
389
389
  const options = {
390
390
  baseOptions: {
391
- modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task',
391
+ modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/1/gesture_recognizer.task',
392
392
  delegate: 'GPU'
393
393
  },
394
394
  runningMode: 'VIDEO',
395
395
  numHands: 2
396
396
  };
397
397
 
398
- const HandLandmarker = self.HandLandmarker || self.module.exports.HandLandmarker || self.exports.HandLandmarker;
399
- handLandmarker = await HandLandmarker.createFromOptions(vision, options);
400
- log('✅ Hand Landmarker loaded');
398
+ const GestureRecognizer = self.GestureRecognizer || self.module.exports.GestureRecognizer || self.exports.GestureRecognizer;
399
+ gestureRecognizer = await GestureRecognizer.createFromOptions(vision, options);
400
+ log('✅ Gesture Recognizer loaded');
401
401
  } catch (error) {
402
- log('❌ Failed to load Hand Landmarker:', error);
402
+ log('❌ Failed to load Gesture Recognizer:', error);
403
403
  throw error;
404
404
  }
405
405
  }
@@ -465,6 +465,59 @@ async function initializeBodySegmentation() {
465
465
  }
466
466
  }
467
467
 
468
+ function computeHandBounds(landmarks) {
469
+ let minX = 1, minY = 1, maxX = 0, maxY = 0;
470
+ for (const p of landmarks) {
471
+ if (p.x < minX) minX = p.x;
472
+ if (p.y < minY) minY = p.y;
473
+ if (p.x > maxX) maxX = p.x;
474
+ if (p.y > maxY) maxY = p.y;
475
+ }
476
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
477
+ }
478
+
479
+ // Map MediaPipe GestureRecognizer category names to our API field names
480
+ const GESTURE_NAME_MAP = {
481
+ 'Closed_Fist': 'fist',
482
+ 'Open_Palm': 'openPalm',
483
+ 'Victory': 'peace',
484
+ 'Thumb_Up': 'thumbsUp',
485
+ 'Pointing_Up': 'pointing',
486
+ 'Thumb_Down': 'thumbsDown',
487
+ 'ILoveYou': 'iLoveYou'
488
+ };
489
+
490
+ function mapGestures(gestureCategories) {
491
+ const result = { fist: 0, openPalm: 0, peace: 0, thumbsUp: 0, pointing: 0, thumbsDown: 0, iLoveYou: 0 };
492
+ if (!gestureCategories) return result;
493
+ for (const cat of gestureCategories) {
494
+ const key = GESTURE_NAME_MAP[cat.categoryName];
495
+ if (key) result[key] = cat.score;
496
+ }
497
+ return result;
498
+ }
499
+
500
+ // BlazePose landmark index groups for named body parts
501
+ const POSE_FACE_INDICES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
502
+ const POSE_TORSO_INDICES = [11, 12, 23, 24];
503
+ const POSE_LEFT_ARM_INDICES = [11, 13, 15];
504
+ const POSE_RIGHT_ARM_INDICES = [12, 14, 16];
505
+ const POSE_LEFT_LEG_INDICES = [23, 25, 27, 29, 31];
506
+ const POSE_RIGHT_LEG_INDICES = [24, 26, 28, 30, 32];
507
+
508
+ function extractPoseGroup(landmarks, indices) {
509
+ return indices.map(i => {
510
+ const lm = landmarks[i];
511
+ return lm ? { x: lm.x, y: lm.y } : { x: 0, y: 0 };
512
+ });
513
+ }
514
+
515
+ function computePoseConfidence(landmarks) {
516
+ let sum = 0;
517
+ for (const lm of landmarks) sum += (lm.visibility || 0);
518
+ return landmarks.length > 0 ? sum / landmarks.length : 0;
519
+ }
520
+
468
521
  /**
469
522
  * Process video frame with active CV features
470
523
  * @param {ImageData|ImageBitmap} imageInput - Image input (ImageData or ImageBitmap)
@@ -478,7 +531,8 @@ async function processFrame(imageInput, timestamp, features) {
478
531
  // Process face detection
479
532
  if (features.includes('faceDetection') && faceDetector) {
480
533
  const detectionResult = faceDetector.detectForVideo(imageInput, timestamp);
481
- results.faces = detectionResult.detections.map((detection) => ({
534
+ results.faces = detectionResult.detections.map((detection, index) => ({
535
+ id: index,
482
536
  bounds: {
483
537
  x: detection.boundingBox.originX / imageInput.width,
484
538
  y: detection.boundingBox.originY / imageInput.height,
@@ -505,6 +559,7 @@ async function processFrame(imageInput, timestamp, features) {
505
559
 
506
560
  if (!results.faces) {
507
561
  results.faces = [{
562
+ id: 0,
508
563
  bounds: null,
509
564
  center: null,
510
565
  landmarks: [],
@@ -525,7 +580,6 @@ async function processFrame(imageInput, timestamp, features) {
525
580
  results.faces[0].landmarks = mappedLandmarks;
526
581
  results.faces[0].headPose = computeHeadPoseFromLandmarks(landmarks);
527
582
 
528
- // Populate emotion data when emotionDetection is active
529
583
  if (features.includes('emotionDetection') &&
530
584
  landmarkResult.faceBlendshapes &&
531
585
  landmarkResult.faceBlendshapes.length > 0) {
@@ -534,34 +588,55 @@ async function processFrame(imageInput, timestamp, features) {
534
588
  results.faces[0].expressions = mapBlendshapesToEmotions(bs);
535
589
  }
536
590
  }
591
+ } else if (!results.faces) {
592
+ results.faces = [];
537
593
  }
538
594
  }
539
595
 
540
- // Process hand tracking
541
- if (features.includes('handTracking') && handLandmarker) {
542
- const handResult = handLandmarker.detectForVideo(imageInput, timestamp);
543
- results.hands = handResult.landmarks.map((landmarks, index) => ({
544
- landmarks: landmarks.map((landmark) => ({
596
+ // Process hand tracking (GestureRecognizer provides landmarks + gestures in one call)
597
+ if (features.includes('handTracking') && gestureRecognizer) {
598
+ const handResult = gestureRecognizer.recognizeForVideo(imageInput, timestamp);
599
+ results.hands = handResult.landmarks.map((landmarks, index) => {
600
+ const mapped = landmarks.map((landmark) => ({
545
601
  x: landmark.x,
546
602
  y: landmark.y,
547
603
  z: landmark.z || 0
548
- })),
549
- handedness: handResult.handednesses[index]?.[0]?.categoryName || 'Unknown',
550
- confidence: handResult.handednesses[index]?.[0]?.score || 0
551
- }));
604
+ }));
605
+
606
+ const rawHandedness = handResult.handednesses[index]?.[0]?.categoryName || 'unknown';
607
+
608
+ return {
609
+ id: index,
610
+ handedness: rawHandedness.toLowerCase(),
611
+ confidence: handResult.handednesses[index]?.[0]?.score || 0,
612
+ bounds: computeHandBounds(mapped),
613
+ landmarks: mapped,
614
+ palm: mapped[9],
615
+ gestures: mapGestures(handResult.gestures[index])
616
+ };
617
+ });
552
618
  }
553
619
 
554
620
  // Process pose detection
555
621
  if (features.includes('poseDetection') && poseLandmarker) {
556
622
  const poseResult = poseLandmarker.detectForVideo(imageInput, timestamp);
557
623
  if (poseResult.landmarks.length > 0) {
624
+ const poseLandmarks = poseResult.landmarks[0].map((landmark) => ({
625
+ x: landmark.x,
626
+ y: landmark.y,
627
+ z: landmark.z || 0,
628
+ visibility: landmark.visibility || 1
629
+ }));
630
+
558
631
  results.pose = {
559
- landmarks: poseResult.landmarks[0].map((landmark) => ({
560
- x: landmark.x,
561
- y: landmark.y,
562
- z: landmark.z || 0,
563
- visibility: landmark.visibility || 1
564
- })),
632
+ confidence: computePoseConfidence(poseLandmarks),
633
+ landmarks: poseLandmarks,
634
+ face: extractPoseGroup(poseLandmarks, POSE_FACE_INDICES),
635
+ torso: extractPoseGroup(poseLandmarks, POSE_TORSO_INDICES),
636
+ leftArm: extractPoseGroup(poseLandmarks, POSE_LEFT_ARM_INDICES),
637
+ rightArm: extractPoseGroup(poseLandmarks, POSE_RIGHT_ARM_INDICES),
638
+ leftLeg: extractPoseGroup(poseLandmarks, POSE_LEFT_LEG_INDICES),
639
+ rightLeg: extractPoseGroup(poseLandmarks, POSE_RIGHT_LEG_INDICES),
565
640
  worldLandmarks: poseResult.worldLandmarks?.[0]?.map((landmark) => ({
566
641
  x: landmark.x,
567
642
  y: landmark.y,
@@ -569,6 +644,8 @@ async function processFrame(imageInput, timestamp, features) {
569
644
  visibility: landmark.visibility || 1
570
645
  })) || []
571
646
  };
647
+ } else {
648
+ results.pose = null;
572
649
  }
573
650
  }
574
651
 
@@ -577,18 +654,17 @@ async function processFrame(imageInput, timestamp, features) {
577
654
  const segmentResult = imageSegmenter.segment(imageInput);
578
655
  if (segmentResult.categoryMask) {
579
656
  try {
580
- // Extract data before closing the mask
581
657
  results.segmentation = {
582
658
  mask: segmentResult.categoryMask.getAsUint8Array(),
583
659
  width: segmentResult.categoryMask.width,
584
660
  height: segmentResult.categoryMask.height
585
661
  };
586
-
587
662
  log('Segmentation mask:', results.segmentation.width, 'x', results.segmentation.height);
588
663
  } finally {
589
- // CRITICAL: Close MPMask instance to prevent resource leaks
590
664
  segmentResult.categoryMask.close();
591
665
  }
666
+ } else {
667
+ results.segmentation = null;
592
668
  }
593
669
  }
594
670
 
@@ -697,8 +773,8 @@ async function handleConfigUpdateInternal(features) {
697
773
  }
698
774
  break;
699
775
  case 'handTracking':
700
- cleanupPromises.push(cleanupWasmInstance(handLandmarker, 'HandLandmarker'));
701
- handLandmarker = null;
776
+ cleanupPromises.push(cleanupWasmInstance(gestureRecognizer, 'GestureRecognizer'));
777
+ gestureRecognizer = null;
702
778
  break;
703
779
  case 'poseDetection':
704
780
  cleanupPromises.push(cleanupWasmInstance(poseLandmarker, 'PoseLandmarker'));