@sssxyd/face-liveness-detector 0.2.29 → 0.2.31

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/dist/index.esm.js CHANGED
@@ -66,6 +66,7 @@ const DEFAULT_CONFIG = Object.freeze({
66
66
  // resource paths
67
67
  human_model_path: undefined,
68
68
  tensorflow_wasm_path: undefined,
69
+ tensorflow_backend: 'auto', // 'auto' | 'webgl' | 'wasm'
69
70
  // DetectionSettings defaults
70
71
  video_width: 640,
71
72
  video_height: 640,
@@ -118,6 +119,9 @@ function mergeConfig(userConfig) {
118
119
  if (userConfig.tensorflow_wasm_path !== undefined) {
119
120
  merged.tensorflow_wasm_path = userConfig.tensorflow_wasm_path;
120
121
  }
122
+ if (userConfig.tensorflow_backend !== undefined) {
123
+ merged.tensorflow_backend = userConfig.tensorflow_backend;
124
+ }
121
125
  if (userConfig.video_width !== undefined) {
122
126
  merged.video_width = userConfig.video_width;
123
127
  }
@@ -304,18 +308,68 @@ function _isWebGLAvailable() {
304
308
  return false;
305
309
  }
306
310
  }
307
- function _detectOptimalBackend() {
311
+ function _detectBrowserEngine(userAgent) {
312
+ const ua = userAgent.toLowerCase();
313
+ // 检测 Gecko (Firefox)
314
+ if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
315
+ return 'gecko';
316
+ }
317
+ // 检测 WebKit (Safari, iOS browsers)
318
+ // Safari 的特征:有 Safari 但没有 Chrome
319
+ if (/safari/i.test(ua) && !/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
320
+ return 'webkit';
321
+ }
322
+ // 检测 Chromium/Blink
323
+ // Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
324
+ if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
325
+ return 'chromium';
326
+ }
327
+ // 默认为 other
328
+ return 'other';
329
+ }
330
+ function _getOptimalBackendForEngine(engine) {
331
+ // 针对不同内核的优化策略
332
+ const backendConfig = {
333
+ chromium: 'webgl', // Chromium 内核:优先 WebGL
334
+ webkit: 'wasm', // WebKit(Safari、iOS):使用 WASM
335
+ gecko: 'webgl', // Firefox:优先 WebGL
336
+ other: 'wasm' // 未知浏览器:保守使用 WASM
337
+ };
338
+ return backendConfig[engine];
339
+ }
340
+ function _detectOptimalBackend(preferredBackend) {
341
+ // If user explicitly specified a backend, honor it (unless it's 'auto')
342
+ if (preferredBackend && preferredBackend !== 'auto') {
343
+ console.log('[Backend Detection] Using user-specified backend:', {
344
+ backend: preferredBackend,
345
+ userAgent: navigator.userAgent
346
+ });
347
+ return preferredBackend;
348
+ }
308
349
  const userAgent = navigator.userAgent.toLowerCase();
309
- // Special browsers: prefer WASM
310
- if ((/safari/.test(userAgent) && !/chrome/.test(userAgent)) ||
311
- /micromessenger/i.test(userAgent) ||
312
- /alipay/.test(userAgent) ||
313
- /qq/.test(userAgent) ||
314
- /(wechat|alipay|qq)webview/i.test(userAgent)) {
315
- return 'wasm';
316
- }
317
- // Desktop: prefer WebGL
318
- return _isWebGLAvailable() ? 'webgl' : 'wasm';
350
+ const engine = _detectBrowserEngine(userAgent);
351
+ console.log('[Backend Detection] Detected browser engine:', {
352
+ engine,
353
+ userAgent: navigator.userAgent
354
+ });
355
+ // 获取该内核的推荐后端
356
+ let preferredBackendForEngine = _getOptimalBackendForEngine(engine);
357
+ // 对于 Chromium 和 Gecko,检查 WebGL 是否可用
358
+ if (preferredBackendForEngine === 'webgl') {
359
+ const hasWebGL = _isWebGLAvailable();
360
+ console.log('[Backend Detection] WebGL availability check:', {
361
+ engine,
362
+ hasWebGL,
363
+ selectedBackend: hasWebGL ? 'webgl' : 'wasm'
364
+ });
365
+ return hasWebGL ? 'webgl' : 'wasm';
366
+ }
367
+ // 对于 WebKit 和 other,直接使用 WASM
368
+ console.log('[Backend Detection] Using backend for engine:', {
369
+ engine,
370
+ backend: preferredBackendForEngine
371
+ });
372
+ return preferredBackendForEngine;
319
373
  }
320
374
  /**
321
375
  * 预加载 OpenCV.js 以确保全局 cv 对象可用
@@ -450,10 +504,6 @@ async function loadOpenCV(timeout = 30000) {
450
504
  try {
451
505
  await opencvInitPromise;
452
506
  cv = getCvSync();
453
- if (cv && cv.Mat) {
454
- console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
455
- return { cv };
456
- }
457
507
  }
458
508
  catch (error) {
459
509
  // 失败后清除 Promise,允许重试
@@ -473,10 +523,6 @@ async function loadOpenCV(timeout = 30000) {
473
523
  try {
474
524
  await opencvInitPromise;
475
525
  cv = getCvSync();
476
- if (cv && cv.Mat) {
477
- console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
478
- return { cv };
479
- }
480
526
  }
481
527
  catch (error) {
482
528
  // 失败后清除 Promise,允许重试
@@ -484,13 +530,17 @@ async function loadOpenCV(timeout = 30000) {
484
530
  throw error;
485
531
  }
486
532
  }
487
- // 如果到这里还没有返回,说明加载失败
488
- console.error('[FaceDetectionEngine] OpenCV module is invalid:', {
489
- hasMat: cv && cv.Mat,
490
- type: typeof cv,
491
- keys: cv ? Object.keys(cv).slice(0, 10) : 'N/A'
492
- });
493
- throw new Error('OpenCV.js loaded but module is invalid (no Mat class found)');
533
+ // 最终验证
534
+ if (!cv || !cv.Mat) {
535
+ console.error('[FaceDetectionEngine] OpenCV module is invalid:', {
536
+ hasMat: cv && cv.Mat,
537
+ type: typeof cv,
538
+ keys: cv ? Object.keys(cv).slice(0, 10) : 'N/A'
539
+ });
540
+ throw new Error('OpenCV.js loaded but module is invalid (no Mat class found)');
541
+ }
542
+ console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
543
+ return { cv };
494
544
  }
495
545
  catch (error) {
496
546
  console.error('[FaceDetectionEngine] Failed to load OpenCV.js:', error);
@@ -516,11 +566,12 @@ function getCvSync() {
516
566
  * Load Human.js
517
567
  * @param modelPath - Path to model files (optional)
518
568
  * @param wasmPath - Path to WASM files (optional)
569
+ * @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
519
570
  * @returns Promise that resolves with Human instance
520
571
  */
521
- async function loadHuman(modelPath, wasmPath) {
572
+ async function loadHuman(modelPath, wasmPath, preferredBackend) {
522
573
  const config = {
523
- backend: _detectOptimalBackend(),
574
+ backend: _detectOptimalBackend(preferredBackend),
524
575
  face: {
525
576
  enabled: true,
526
577
  detector: { rotation: false, return: true },
@@ -544,30 +595,90 @@ async function loadHuman(modelPath, wasmPath) {
544
595
  console.log('[FaceDetectionEngine] Human.js config:', {
545
596
  backend: config.backend,
546
597
  modelBasePath: config.modelBasePath || '(using default)',
547
- wasmPath: config.wasmPath || '(using default)'
598
+ wasmPath: config.wasmPath || '(using default)',
599
+ userAgent: navigator.userAgent,
600
+ platform: navigator.platform
548
601
  });
549
602
  const initStartTime = performance.now();
550
603
  console.log('[FaceDetectionEngine] Creating Human instance...');
551
- const human = new Human(config);
604
+ let human;
605
+ try {
606
+ human = new Human(config);
607
+ }
608
+ catch (error) {
609
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error during Human instantiation';
610
+ console.error('[FaceDetectionEngine] Failed to create Human instance:', errorMsg);
611
+ throw new Error(`Human instantiation failed: ${errorMsg}`);
612
+ }
552
613
  const instanceCreateTime = performance.now() - initStartTime;
553
614
  console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
615
+ // 验证 Human 实例
616
+ if (!human) {
617
+ throw new Error('Human instance is null after creation');
618
+ }
554
619
  console.log('[FaceDetectionEngine] Loading Human.js models...');
555
620
  const modelLoadStartTime = performance.now();
556
621
  try {
557
- await human.load();
622
+ // 添加超时机制防止无限等待
623
+ const loadTimeout = new Promise((_, reject) => {
624
+ const timeoutId = setTimeout(() => {
625
+ reject(new Error('Human.js load() timeout after 60 seconds - possible issue with model loading on mobile'));
626
+ }, 60000);
627
+ });
628
+ // 竞速:哪个先完成就用哪个
629
+ await Promise.race([
630
+ human.load(),
631
+ loadTimeout
632
+ ]);
558
633
  const loadTime = performance.now() - modelLoadStartTime;
559
634
  const totalTime = performance.now() - initStartTime;
560
635
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
561
636
  modelLoadTime: `${loadTime.toFixed(2)}ms`,
562
637
  totalInitTime: `${totalTime.toFixed(2)}ms`,
563
- version: human.version
638
+ version: human.version,
639
+ config: human.config
564
640
  });
641
+ // 验证加载后的 Human 实例有必要的方法和属性
642
+ if (typeof human.detect !== 'function') {
643
+ throw new Error('Human.detect method not available after loading');
644
+ }
645
+ if (!human.version) {
646
+ console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
647
+ }
648
+ // 打印加载的模型信息
649
+ if (human.models) {
650
+ const loadedModels = Object.entries(human.models).map(([name, model]) => ({
651
+ name,
652
+ loaded: model?.loaded || model?.state === 'loaded',
653
+ type: typeof model
654
+ }));
655
+ console.log('[FaceDetectionEngine] Loaded models:', {
656
+ totalModels: Object.keys(human.models).length,
657
+ models: loadedModels,
658
+ allModels: Object.keys(human.models)
659
+ });
660
+ }
661
+ else {
662
+ console.warn('[FaceDetectionEngine] human.models is not available');
663
+ }
664
+ // 额外验证:检查模型是否加载成功
665
+ if (!human.models || Object.keys(human.models).length === 0) {
666
+ console.warn('[FaceDetectionEngine] Warning: human.models appears to be empty after loading');
667
+ }
565
668
  return human;
566
669
  }
567
670
  catch (error) {
568
671
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
569
- console.error('[FaceDetectionEngine] Human.js load failed:', errorMsg);
570
- throw error;
672
+ const stack = error instanceof Error ? error.stack : 'N/A';
673
+ console.error('[FaceDetectionEngine] Human.js load failed:', {
674
+ errorMsg,
675
+ stack,
676
+ userAgent: navigator.userAgent,
677
+ platform: navigator.platform,
678
+ humanVersion: human?.version,
679
+ humanConfig: human?.config
680
+ });
681
+ throw new Error(`Human.js loading failed: ${errorMsg}`);
571
682
  }
572
683
  }
573
684
  /**
@@ -1603,7 +1714,29 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1603
1714
  console.log('[FaceDetectionEngine] Loading Human.js models...');
1604
1715
  this.emitDebug('initialization', 'Loading Human.js...');
1605
1716
  const humanStartTime = performance.now();
1606
- this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path);
1717
+ try {
1718
+ this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path, this.config.tensorflow_backend);
1719
+ }
1720
+ catch (humanError) {
1721
+ const errorMsg = humanError instanceof Error ? humanError.message : 'Unknown error';
1722
+ console.error('[FaceDetectionEngine] Human.js loading failed with error:', errorMsg);
1723
+ this.emitDebug('initialization', 'Human.js loading failed with exception', {
1724
+ error: errorMsg,
1725
+ stack: humanError instanceof Error ? humanError.stack : 'N/A',
1726
+ userAgent: navigator.userAgent,
1727
+ platform: navigator.platform,
1728
+ browser: this.detectBrowserInfo()
1729
+ }, 'error');
1730
+ this.emit('detector-loaded', {
1731
+ success: false,
1732
+ error: `Failed to load Human.js: ${errorMsg}`
1733
+ });
1734
+ this.emit('detector-error', {
1735
+ code: ErrorCode.DETECTOR_NOT_INITIALIZED,
1736
+ message: `Human.js loading error: ${errorMsg}`
1737
+ });
1738
+ return;
1739
+ }
1607
1740
  const humanLoadTime = performance.now() - humanStartTime;
1608
1741
  if (!this.human) {
1609
1742
  const errorMsg = 'Failed to load Human.js: instance is null';
@@ -1619,13 +1752,35 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1619
1752
  });
1620
1753
  return;
1621
1754
  }
1755
+ // Verify Human.js instance has required properties
1756
+ if (!this.human.version || typeof this.human.detect !== 'function') {
1757
+ const errorMsg = 'Human.js instance is incomplete: missing version or detect method';
1758
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1759
+ this.emitDebug('initialization', errorMsg, {
1760
+ hasVersion: !!this.human.version,
1761
+ hasDetect: typeof this.human.detect === 'function',
1762
+ instanceKeys: Object.keys(this.human || {})
1763
+ }, 'error');
1764
+ this.emit('detector-loaded', {
1765
+ success: false,
1766
+ error: errorMsg
1767
+ });
1768
+ this.emit('detector-error', {
1769
+ code: ErrorCode.DETECTOR_NOT_INITIALIZED,
1770
+ message: errorMsg
1771
+ });
1772
+ return;
1773
+ }
1622
1774
  this.emitDebug('initialization', 'Human.js loaded successfully', {
1623
1775
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1624
- version: this.human.version
1776
+ version: this.human.version,
1777
+ backend: this.human.config?.backend || 'unknown',
1778
+ config: this.human.config
1625
1779
  });
1626
1780
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
1627
1781
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1628
- version: this.human.version
1782
+ version: this.human.version,
1783
+ backend: this.human.config?.backend || 'unknown'
1629
1784
  });
1630
1785
  this.isReady = true;
1631
1786
  const loadedData = {
@@ -1902,13 +2057,31 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1902
2057
  try {
1903
2058
  // Check video is ready
1904
2059
  if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1905
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2060
+ this.scheduleNextDetection(this.config.error_retry_delay);
1906
2061
  return;
1907
2062
  }
1908
2063
  // Perform face detection
1909
- const result = await this.human.detect(this.videoElement);
2064
+ let result;
2065
+ try {
2066
+ result = await this.human.detect(this.videoElement);
2067
+ }
2068
+ catch (detectError) {
2069
+ const errorMsg = detectError instanceof Error ? detectError.message : 'Unknown error';
2070
+ this.emitDebug('detection', 'Human.detect() call failed', {
2071
+ error: errorMsg,
2072
+ stack: detectError instanceof Error ? detectError.stack : 'N/A',
2073
+ hasHuman: !!this.human,
2074
+ humanVersion: this.human?.version,
2075
+ videoReadyState: this.videoElement?.readyState,
2076
+ videoWidth: this.videoElement?.videoWidth,
2077
+ videoHeight: this.videoElement?.videoHeight
2078
+ }, 'error');
2079
+ this.scheduleNextDetection(this.config.error_retry_delay);
2080
+ return;
2081
+ }
1910
2082
  if (!result) {
1911
- this.scheduleNextDetection(this.config.error_retry_delay); // DETECTION_FRAME_DELAY
2083
+ this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
2084
+ this.scheduleNextDetection(this.config.error_retry_delay);
1912
2085
  return;
1913
2086
  }
1914
2087
  const faces = result.face || [];
@@ -1921,18 +2094,24 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1921
2094
  }
1922
2095
  }
1923
2096
  catch (error) {
1924
- this.emitDebug('detection', 'Detection error', {
1925
- error: error.message,
1926
- stack: error.stack
2097
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
2098
+ this.emitDebug('detection', 'Unexpected error in detection loop', {
2099
+ error: errorMsg,
2100
+ stack: error instanceof Error ? error.stack : 'N/A'
1927
2101
  }, 'error');
1928
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2102
+ this.scheduleNextDetection(this.config.error_retry_delay);
1929
2103
  }
1930
2104
  }
1931
2105
  getPerformActionCount() {
1932
2106
  if (this.config.liveness_action_count <= 0) {
2107
+ this.emitDebug('config', 'liveness_action_count is 0 or negative', { count: this.config.liveness_action_count }, 'warn');
1933
2108
  return 0;
1934
2109
  }
1935
- return Math.min(this.config.liveness_action_count, this.config.liveness_action_list.length);
2110
+ const actionListLength = this.config.liveness_action_list?.length ?? 0;
2111
+ if (actionListLength === 0) {
2112
+ this.emitDebug('config', 'liveness_action_list is empty', { actionListLength }, 'warn');
2113
+ }
2114
+ return Math.min(this.config.liveness_action_count, actionListLength);
1936
2115
  }
1937
2116
  /**
1938
2117
  * Handle single face detection
@@ -1984,6 +2163,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1984
2163
  validateFaceBox(faceBox) {
1985
2164
  if (!faceBox) {
1986
2165
  console.warn('[FaceDetector] Face detected but no box/boxRaw property');
2166
+ this.emitDebug('detection', 'Face box is missing - face detected but no box/boxRaw property', {}, 'warn');
1987
2167
  this.scheduleNextDetection(this.config.error_retry_delay);
1988
2168
  return false;
1989
2169
  }
@@ -2157,20 +2337,25 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2157
2337
  this.detectionState.collectCount++;
2158
2338
  return;
2159
2339
  }
2160
- const frameImageData = this.captureFrame();
2161
- if (!frameImageData) {
2162
- this.emitDebug('detection', 'Failed to capture current frame image', {}, 'warn');
2163
- return;
2340
+ try {
2341
+ const frameImageData = this.captureFrame();
2342
+ if (!frameImageData) {
2343
+ this.emitDebug('detection', 'Failed to capture current frame image', { frameQuality, bestQualityScore: this.detectionState.bestQualityScore }, 'warn');
2344
+ return;
2345
+ }
2346
+ const faceImageData = this.captureFrame(faceBox);
2347
+ if (!faceImageData) {
2348
+ this.emitDebug('detection', 'Failed to capture face image', { faceBox }, 'warn');
2349
+ return;
2350
+ }
2351
+ this.detectionState.collectCount++;
2352
+ this.detectionState.bestQualityScore = frameQuality;
2353
+ this.detectionState.bestFrameImage = frameImageData;
2354
+ this.detectionState.bestFaceImage = faceImageData;
2164
2355
  }
2165
- const faceImageData = this.captureFrame(faceBox);
2166
- if (!faceImageData) {
2167
- this.emitDebug('detection', 'Failed to capture face image', {}, 'warn');
2168
- return;
2356
+ catch (error) {
2357
+ this.emitDebug('detection', 'Error during image collection', { error: error.message }, 'error');
2169
2358
  }
2170
- this.detectionState.collectCount++;
2171
- this.detectionState.bestQualityScore = frameQuality;
2172
- this.detectionState.bestFrameImage = frameImageData;
2173
- this.detectionState.bestFaceImage = faceImageData;
2174
2359
  }
2175
2360
  emitLivenessDetected(passed, size, frontal = 0, quality = 0, real = 0, live = 0) {
2176
2361
  this.emit('face-detected', { passed, size, frontal, quality, real, live });
@@ -2181,6 +2366,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2181
2366
  selectNextAction() {
2182
2367
  const availableActions = (this.config.liveness_action_list ?? []).filter(action => !this.detectionState.completedActions.has(action));
2183
2368
  if (availableActions.length === 0) {
2369
+ this.emitDebug('liveness', 'No available actions to perform', { completedActions: Array.from(this.detectionState.completedActions), totalActions: this.config.liveness_action_list?.length ?? 0 }, 'warn');
2184
2370
  return;
2185
2371
  }
2186
2372
  let nextAction = availableActions[0];
@@ -2226,36 +2412,45 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2226
2412
  * Detect specific action
2227
2413
  */
2228
2414
  detectAction(action, gestures) {
2229
- if (!gestures || gestures.length === 0)
2415
+ if (!gestures || gestures.length === 0) {
2416
+ this.emitDebug('liveness', 'No gestures detected for action verification', { action, gestureCount: gestures?.length ?? 0 }, 'info');
2417
+ return false;
2418
+ }
2419
+ try {
2420
+ switch (action) {
2421
+ case LivenessAction.BLINK:
2422
+ return gestures.some(g => {
2423
+ if (!g.gesture)
2424
+ return false;
2425
+ return g.gesture.includes('blink');
2426
+ });
2427
+ case LivenessAction.MOUTH_OPEN:
2428
+ return gestures.some(g => {
2429
+ const gestureStr = g.gesture;
2430
+ if (!gestureStr || !gestureStr.includes('mouth'))
2431
+ return false;
2432
+ const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
2433
+ if (!percentMatch || !percentMatch[1])
2434
+ return false;
2435
+ const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
2436
+ return percent > (this.config.min_mouth_open_percent ?? 0.2);
2437
+ });
2438
+ case LivenessAction.NOD:
2439
+ return gestures.some(g => {
2440
+ if (!g.gesture)
2441
+ return false;
2442
+ // Check for continuous head movement (up -> down or down -> up)
2443
+ const headPattern = g.gesture.match(/head\s+(up|down)/i);
2444
+ return !!headPattern && !!headPattern[1];
2445
+ });
2446
+ default:
2447
+ this.emitDebug('liveness', 'Unknown action type in detection', { action }, 'warn');
2448
+ return false;
2449
+ }
2450
+ }
2451
+ catch (error) {
2452
+ this.emitDebug('liveness', 'Error during action detection', { action, error: error.message }, 'error');
2230
2453
  return false;
2231
- switch (action) {
2232
- case LivenessAction.BLINK:
2233
- return gestures.some(g => {
2234
- if (!g.gesture)
2235
- return false;
2236
- return g.gesture.includes('blink');
2237
- });
2238
- case LivenessAction.MOUTH_OPEN:
2239
- return gestures.some(g => {
2240
- const gestureStr = g.gesture;
2241
- if (!gestureStr || !gestureStr.includes('mouth'))
2242
- return false;
2243
- const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
2244
- if (!percentMatch || !percentMatch[1])
2245
- return false;
2246
- const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
2247
- return percent > (this.config.min_mouth_open_percent ?? 0.2);
2248
- });
2249
- case LivenessAction.NOD:
2250
- return gestures.some(g => {
2251
- if (!g.gesture)
2252
- return false;
2253
- // Check for continuous head movement (up -> down or down -> up)
2254
- const headPattern = g.gesture.match(/head\s+(up|down)/i);
2255
- return !!headPattern && !!headPattern[1];
2256
- });
2257
- default:
2258
- return false;
2259
2454
  }
2260
2455
  }
2261
2456
  /**
@@ -2268,6 +2463,29 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2268
2463
  };
2269
2464
  this.emit('status-prompt', promptData);
2270
2465
  }
2466
+ /**
2467
+ * Detect browser information for debugging
2468
+ */
2469
+ detectBrowserInfo() {
2470
+ const ua = navigator.userAgent.toLowerCase();
2471
+ if (/quark/i.test(ua))
2472
+ return 'Quark Browser';
2473
+ if (/micromessenger/i.test(ua))
2474
+ return 'WeChat';
2475
+ if (/alipay/i.test(ua))
2476
+ return 'Alipay';
2477
+ if (/qq/i.test(ua))
2478
+ return 'QQ';
2479
+ if (/safari/i.test(ua) && !/chrome/i.test(ua))
2480
+ return 'Safari';
2481
+ if (/chrome/i.test(ua))
2482
+ return 'Chrome';
2483
+ if (/firefox/i.test(ua))
2484
+ return 'Firefox';
2485
+ if (/edge/i.test(ua))
2486
+ return 'Edge';
2487
+ return 'Unknown';
2488
+ }
2271
2489
  /**
2272
2490
  * Emit debug event
2273
2491
  */
@@ -2380,26 +2598,35 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2380
2598
  */
2381
2599
  captureFrame(box) {
2382
2600
  if (!this.frameCanvasElement) {
2601
+ this.emitDebug('capture', 'Frame canvas element is null, cannot capture frame', {}, 'error');
2383
2602
  return null;
2384
2603
  }
2385
2604
  if (!box) {
2386
2605
  return this.canvasToBase64(this.frameCanvasElement);
2387
2606
  }
2388
- const x = box[0], y = box[1], width = box[2], height = box[3];
2389
- // If cached canvas size does not match, recreate it
2390
- if (!this.faceCanvasElement || this.faceCanvasElement.width !== width || this.faceCanvasElement.height !== height) {
2391
- this.clearFaceCanvas();
2392
- this.faceCanvasElement = document.createElement('canvas');
2607
+ try {
2608
+ const x = box[0], y = box[1], width = box[2], height = box[3];
2609
+ // If cached canvas size does not match, recreate it
2610
+ if (!this.faceCanvasElement || this.faceCanvasElement.width !== width || this.faceCanvasElement.height !== height) {
2611
+ this.clearFaceCanvas();
2612
+ this.faceCanvasElement = document.createElement('canvas');
2613
+ this.faceCanvasElement.width = width;
2614
+ this.faceCanvasElement.height = height;
2615
+ this.faceCanvasContext = this.faceCanvasElement.getContext('2d');
2616
+ }
2617
+ if (!this.faceCanvasContext) {
2618
+ this.emitDebug('capture', 'Failed to get face canvas 2D context', { width, height }, 'error');
2619
+ return null;
2620
+ }
2393
2621
  this.faceCanvasElement.width = width;
2394
2622
  this.faceCanvasElement.height = height;
2395
- this.faceCanvasContext = this.faceCanvasElement.getContext('2d');
2623
+ this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
2624
+ return this.canvasToBase64(this.faceCanvasElement);
2396
2625
  }
2397
- if (!this.faceCanvasContext)
2626
+ catch (error) {
2627
+ this.emitDebug('capture', 'Error during face frame capture', { box, error: error.message }, 'error');
2398
2628
  return null;
2399
- this.faceCanvasElement.width = width;
2400
- this.faceCanvasElement.height = height;
2401
- this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
2402
- return this.canvasToBase64(this.faceCanvasElement);
2629
+ }
2403
2630
  }
2404
2631
  }
2405
2632