@sssxyd/face-liveness-detector 0.2.30 → 0.2.32

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.js CHANGED
@@ -88,6 +88,7 @@
88
88
  // resource paths
89
89
  human_model_path: undefined,
90
90
  tensorflow_wasm_path: undefined,
91
+ tensorflow_backend: 'auto', // 'auto' | 'webgl' | 'wasm'
91
92
  // DetectionSettings defaults
92
93
  video_width: 640,
93
94
  video_height: 640,
@@ -140,6 +141,9 @@
140
141
  if (userConfig.tensorflow_wasm_path !== undefined) {
141
142
  merged.tensorflow_wasm_path = userConfig.tensorflow_wasm_path;
142
143
  }
144
+ if (userConfig.tensorflow_backend !== undefined) {
145
+ merged.tensorflow_backend = userConfig.tensorflow_backend;
146
+ }
143
147
  if (userConfig.video_width !== undefined) {
144
148
  merged.video_width = userConfig.video_width;
145
149
  }
@@ -326,18 +330,72 @@
326
330
  return false;
327
331
  }
328
332
  }
329
- function _detectOptimalBackend() {
333
+ function _detectBrowserEngine(userAgent) {
334
+ const ua = userAgent.toLowerCase();
335
+ // 1. 检测 Gecko (Firefox)
336
+ if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
337
+ return 'gecko';
338
+ }
339
+ // 2. 检测 WebKit
340
+ // 包括:Safari、iOS 浏览器、以及那些虽然包含 Chrome 标识但实际是 WebKit 的浏览器(Quark、支付宝、微信等)
341
+ if (/webkit/i.test(ua)) {
342
+ // WebKit 特征明显,包括以下几种情况:
343
+ // - 真正的 Safari(有 Safari 标识)
344
+ // - iOS 浏览器(有 Mobile Safari 标识)
345
+ // - Quark、支付宝、微信等虽然包含 Chrome 标识但是基于 WebKit 的浏览器
346
+ return 'webkit';
347
+ }
348
+ // 3. 检测 Chromium/Blink
349
+ // Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
350
+ if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
351
+ return 'chromium';
352
+ }
353
+ // 4. 其他浏览器 - 保守方案,使用 WASM
354
+ return 'other';
355
+ }
356
+ function _getOptimalBackendForEngine(engine) {
357
+ // 针对不同内核的优化策略
358
+ const backendConfig = {
359
+ chromium: 'webgl', // Chromium 内核:优先 WebGL
360
+ webkit: 'wasm', // WebKit(Safari、iOS):使用 WASM
361
+ gecko: 'webgl', // Firefox:优先 WebGL
362
+ other: 'wasm' // 未知浏览器:保守使用 WASM
363
+ };
364
+ return backendConfig[engine];
365
+ }
366
+ function _detectOptimalBackend(preferredBackend) {
367
+ // If user explicitly specified a backend, honor it (unless it's 'auto')
368
+ if (preferredBackend && preferredBackend !== 'auto') {
369
+ console.log('[Backend Detection] Using user-specified backend:', {
370
+ backend: preferredBackend,
371
+ userAgent: navigator.userAgent
372
+ });
373
+ return preferredBackend;
374
+ }
330
375
  const userAgent = navigator.userAgent.toLowerCase();
331
- // Special browsers: prefer WASM
332
- if ((/safari/.test(userAgent) && !/chrome/.test(userAgent)) ||
333
- /micromessenger/i.test(userAgent) ||
334
- /alipay/.test(userAgent) ||
335
- /qq/.test(userAgent) ||
336
- /(wechat|alipay|qq)webview/i.test(userAgent)) {
337
- return 'wasm';
338
- }
339
- // Desktop: prefer WebGL
340
- return _isWebGLAvailable() ? 'webgl' : 'wasm';
376
+ const engine = _detectBrowserEngine(userAgent);
377
+ console.log('[Backend Detection] Detected browser engine:', {
378
+ engine,
379
+ userAgent: navigator.userAgent
380
+ });
381
+ // 获取该内核的推荐后端
382
+ let preferredBackendForEngine = _getOptimalBackendForEngine(engine);
383
+ // 对于 Chromium 和 Gecko,检查 WebGL 是否可用
384
+ if (preferredBackendForEngine === 'webgl') {
385
+ const hasWebGL = _isWebGLAvailable();
386
+ console.log('[Backend Detection] WebGL availability check:', {
387
+ engine,
388
+ hasWebGL,
389
+ selectedBackend: hasWebGL ? 'webgl' : 'wasm'
390
+ });
391
+ return hasWebGL ? 'webgl' : 'wasm';
392
+ }
393
+ // 对于 WebKit 和 other,直接使用 WASM
394
+ console.log('[Backend Detection] Using backend for engine:', {
395
+ engine,
396
+ backend: preferredBackendForEngine
397
+ });
398
+ return preferredBackendForEngine;
341
399
  }
342
400
  /**
343
401
  * 预加载 OpenCV.js 以确保全局 cv 对象可用
@@ -534,11 +592,13 @@
534
592
  * Load Human.js
535
593
  * @param modelPath - Path to model files (optional)
536
594
  * @param wasmPath - Path to WASM files (optional)
595
+ * @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
537
596
  * @returns Promise that resolves with Human instance
538
597
  */
539
- async function loadHuman(modelPath, wasmPath) {
598
+ async function loadHuman(modelPath, wasmPath, preferredBackend) {
599
+ const selectedBackend = _detectOptimalBackend(preferredBackend);
540
600
  const config = {
541
- backend: _detectOptimalBackend(),
601
+ backend: selectedBackend,
542
602
  face: {
543
603
  enabled: true,
544
604
  detector: { rotation: false, return: true },
@@ -562,30 +622,176 @@
562
622
  console.log('[FaceDetectionEngine] Human.js config:', {
563
623
  backend: config.backend,
564
624
  modelBasePath: config.modelBasePath || '(using default)',
565
- wasmPath: config.wasmPath || '(using default)'
625
+ wasmPath: config.wasmPath || '(using default)',
626
+ userAgent: navigator.userAgent,
627
+ platform: navigator.platform
566
628
  });
567
629
  const initStartTime = performance.now();
568
630
  console.log('[FaceDetectionEngine] Creating Human instance...');
569
- const human = new Human(config);
631
+ let human;
632
+ try {
633
+ human = new Human(config);
634
+ }
635
+ catch (error) {
636
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error during Human instantiation';
637
+ const stack = error instanceof Error ? error.stack : 'N/A';
638
+ console.error('[FaceDetectionEngine] Failed to create Human instance:', {
639
+ errorMsg,
640
+ stack,
641
+ backend: config.backend,
642
+ userAgent: navigator.userAgent
643
+ });
644
+ throw new Error(`Human instantiation failed: ${errorMsg}`);
645
+ }
570
646
  const instanceCreateTime = performance.now() - initStartTime;
571
647
  console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
648
+ // 验证 Human 实例
649
+ if (!human) {
650
+ throw new Error('Human instance is null after creation');
651
+ }
652
+ // 验证 Human 实例结构(早期检测 WASM 问题)
653
+ if (!human.config) {
654
+ console.warn('[FaceDetectionEngine] Warning: human.config is missing');
655
+ }
572
656
  console.log('[FaceDetectionEngine] Loading Human.js models...');
573
657
  const modelLoadStartTime = performance.now();
574
658
  try {
575
- await human.load();
659
+ // 添加超时机制防止无限等待
660
+ const loadTimeout = new Promise((_, reject) => {
661
+ const timeoutId = setTimeout(() => {
662
+ reject(new Error('Human.js load() timeout after 60 seconds - possible issue with model loading on mobile'));
663
+ }, 60000);
664
+ });
665
+ // 竞速:哪个先完成就用哪个
666
+ try {
667
+ await Promise.race([
668
+ human.load(),
669
+ loadTimeout
670
+ ]);
671
+ }
672
+ catch (raceError) {
673
+ // 如果是超时错误,直接抛出
674
+ if (raceError instanceof Error && raceError.message.includes('timeout')) {
675
+ throw raceError;
676
+ }
677
+ // 其他错误,提供更多上下文
678
+ const raceErrorMsg = raceError instanceof Error ? raceError.message : 'Unknown error';
679
+ const raceErrorStack = raceError instanceof Error ? raceError.stack : 'N/A';
680
+ console.error('[FaceDetectionEngine] Error during human.load():', {
681
+ errorMsg: raceErrorMsg,
682
+ stack: raceErrorStack,
683
+ backend: config.backend,
684
+ hasModels: !!human.models,
685
+ modelsKeys: human.models ? Object.keys(human.models).length : 0
686
+ });
687
+ throw new Error(`Model loading error: ${raceErrorMsg}`);
688
+ }
576
689
  const loadTime = performance.now() - modelLoadStartTime;
577
690
  const totalTime = performance.now() - initStartTime;
578
691
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
579
692
  modelLoadTime: `${loadTime.toFixed(2)}ms`,
580
693
  totalInitTime: `${totalTime.toFixed(2)}ms`,
581
- version: human.version
694
+ version: human.version,
695
+ config: human.config
582
696
  });
697
+ // 验证加载后的 Human 实例有必要的方法和属性
698
+ if (typeof human.detect !== 'function') {
699
+ throw new Error('Human.detect method not available after loading');
700
+ }
701
+ if (!human.version) {
702
+ console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
703
+ }
704
+ // 关键验证:检查模型是否真的加载了
705
+ if (!human.models || Object.keys(human.models).length === 0) {
706
+ console.error('[FaceDetectionEngine] CRITICAL: human.models is empty after loading!');
707
+ throw new Error('No models were loaded - human.models is empty');
708
+ }
709
+ // 详细检查每个关键模型及其结构
710
+ const criticalModels = ['face', 'antispoof', 'liveness'];
711
+ const missingModels = [];
712
+ for (const modelName of criticalModels) {
713
+ const model = human.models[modelName];
714
+ if (!model) {
715
+ missingModels.push(modelName);
716
+ console.error(`[FaceDetectionEngine] CRITICAL: Model '${modelName}' is missing!`);
717
+ }
718
+ else {
719
+ const isLoaded = model.loaded || model.state === 'loaded' || !!model.model;
720
+ // 检查模型是否有必要的内部结构(防止 "Cannot read properties of undefined (reading 'inputs')" 错误)
721
+ const hasExecutor = !!model['executor'];
722
+ const hasInputs = !!model.inputs && Array.isArray(model.inputs) && model.inputs.length > 0;
723
+ const hasModelUrl = !!model['modelUrl'];
724
+ console.log(`[FaceDetectionEngine] Model '${modelName}':`, {
725
+ loaded: isLoaded,
726
+ state: model.state,
727
+ hasModel: !!model.model,
728
+ hasExecutor,
729
+ hasInputs,
730
+ hasModelUrl,
731
+ inputsType: typeof model.inputs,
732
+ inputsLength: Array.isArray(model.inputs) ? model.inputs.length : 'N/A'
733
+ });
734
+ // 严格检查:模型必须有以下结构才能正常工作
735
+ if (!isLoaded || !hasExecutor || !hasModelUrl) {
736
+ missingModels.push(`${modelName} (incomplete)`);
737
+ console.error(`[FaceDetectionEngine] WARNING: Model '${modelName}' may not be fully loaded - missing structure`);
738
+ }
739
+ // 如果 inputs 未定义会导致 "Cannot read properties of undefined (reading 'inputs')" 错误
740
+ if (!hasInputs && modelName !== 'antispoof') {
741
+ console.warn(`[FaceDetectionEngine] WARNING: Model '${modelName}' has no inputs - may cause errors during detection`);
742
+ missingModels.push(`${modelName} (no inputs)`);
743
+ }
744
+ }
745
+ }
746
+ if (missingModels.length > 0) {
747
+ console.error('[FaceDetectionEngine] Some critical models failed to load:', missingModels);
748
+ throw new Error(`Critical models not loaded: ${missingModels.join(', ')}`);
749
+ }
750
+ // 打印加载的模型信息
751
+ if (human.models) {
752
+ const loadedModels = Object.entries(human.models).map(([name, model]) => ({
753
+ name,
754
+ loaded: model?.loaded || model?.state === 'loaded',
755
+ type: typeof model,
756
+ hasModel: !!model?.model
757
+ }));
758
+ console.log('[FaceDetectionEngine] All loaded models:', {
759
+ totalModels: Object.keys(human.models).length,
760
+ models: loadedModels,
761
+ allModelNames: Object.keys(human.models)
762
+ });
763
+ }
583
764
  return human;
584
765
  }
585
766
  catch (error) {
586
767
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
587
- console.error('[FaceDetectionEngine] Human.js load failed:', errorMsg);
588
- throw error;
768
+ const stack = error instanceof Error ? error.stack : 'N/A';
769
+ console.error('[FaceDetectionEngine] Human.js load failed:', {
770
+ errorMsg,
771
+ stack,
772
+ userAgent: navigator.userAgent,
773
+ platform: navigator.platform,
774
+ humanVersion: human?.version,
775
+ humanConfig: human?.config,
776
+ backend: config.backend
777
+ });
778
+ // 如果是 WASM 后端失败,尝试降级到 WebGL
779
+ if (selectedBackend === 'wasm' && !errorMsg.includes('timeout') && _isWebGLAvailable()) {
780
+ console.warn('[FaceDetectionEngine] WASM backend failed, attempting fallback to WebGL...');
781
+ config.backend = 'webgl';
782
+ try {
783
+ console.log('[FaceDetectionEngine] Retrying with WebGL backend');
784
+ human = new Human(config);
785
+ await human.load();
786
+ console.log('[FaceDetectionEngine] Successfully loaded with WebGL fallback');
787
+ return human;
788
+ }
789
+ catch (fallbackError) {
790
+ const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : 'Unknown error';
791
+ console.error('[FaceDetectionEngine] WebGL fallback also failed:', fallbackMsg);
792
+ }
793
+ }
794
+ throw new Error(`Human.js loading failed: ${errorMsg}`);
589
795
  }
590
796
  }
591
797
  /**
@@ -1621,11 +1827,64 @@
1621
1827
  console.log('[FaceDetectionEngine] Loading Human.js models...');
1622
1828
  this.emitDebug('initialization', 'Loading Human.js...');
1623
1829
  const humanStartTime = performance.now();
1624
- this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path);
1830
+ try {
1831
+ this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path, this.config.tensorflow_backend);
1832
+ }
1833
+ catch (humanError) {
1834
+ const errorMsg = humanError instanceof Error ? humanError.message : 'Unknown error';
1835
+ const stack = humanError instanceof Error ? humanError.stack : 'N/A';
1836
+ // 分析错误类型,提供针对性的建议
1837
+ let errorContext = {
1838
+ error: errorMsg,
1839
+ stack,
1840
+ userAgent: navigator.userAgent,
1841
+ platform: navigator.platform,
1842
+ browser: this.detectBrowserInfo(),
1843
+ backend: this.config.tensorflow_backend,
1844
+ source: 'human.js'
1845
+ };
1846
+ // 特定错误类型的诊断
1847
+ if (errorMsg.includes('inputs')) {
1848
+ errorContext.diagnosis = 'Human.js internal error: Model structure incomplete';
1849
+ errorContext.rootCause = 'Human.js library issue - models not fully loaded or WASM backend initialization incomplete';
1850
+ errorContext.suggestion = 'This is a Human.js library issue. Models may not have proper executor or inputs structure. Check WASM initialization and model integrity.';
1851
+ }
1852
+ else if (errorMsg.includes('timeout')) {
1853
+ errorContext.diagnosis = 'Model loading timeout';
1854
+ errorContext.suggestion = 'Network issue or model file too large - check network conditions';
1855
+ }
1856
+ else if (errorMsg.includes('Critical models not loaded')) {
1857
+ errorContext.diagnosis = 'Human.js failed to load required models';
1858
+ errorContext.rootCause = 'Models (face, antispoof, liveness) are missing or incomplete';
1859
+ errorContext.suggestion = 'Check model files and ensure WASM backend is properly initialized';
1860
+ }
1861
+ else if (errorMsg.includes('empty')) {
1862
+ errorContext.diagnosis = 'Models object is empty after loading';
1863
+ errorContext.suggestion = 'Model path may be incorrect or HTTP response failed';
1864
+ }
1865
+ else if (errorMsg.includes('incomplete')) {
1866
+ errorContext.diagnosis = 'Models loaded but structure is incomplete';
1867
+ errorContext.rootCause = 'Human.js internal issue - missing executor, inputs, or modelUrl';
1868
+ errorContext.suggestion = 'Ensure all model resources are fully loaded and accessible';
1869
+ }
1870
+ console.error('[FaceDetectionEngine] Human.js loading failed with detailed error:', errorContext);
1871
+ this.emitDebug('initialization', 'Human.js loading failed with exception', errorContext, 'error');
1872
+ this.emit('detector-loaded', {
1873
+ success: false,
1874
+ error: `Failed to load Human.js: ${errorMsg}`,
1875
+ details: errorContext
1876
+ });
1877
+ this.emit('detector-error', {
1878
+ code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
1879
+ message: `Human.js loading error: ${errorMsg}`,
1880
+ details: errorContext
1881
+ });
1882
+ return;
1883
+ }
1625
1884
  const humanLoadTime = performance.now() - humanStartTime;
1626
1885
  if (!this.human) {
1627
1886
  const errorMsg = 'Failed to load Human.js: instance is null';
1628
- console.log('[FaceDetectionEngine] ' + errorMsg);
1887
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1629
1888
  this.emitDebug('initialization', errorMsg, { loadTime: humanLoadTime }, 'error');
1630
1889
  this.emit('detector-loaded', {
1631
1890
  success: false,
@@ -1637,13 +1896,35 @@
1637
1896
  });
1638
1897
  return;
1639
1898
  }
1899
+ // Verify Human.js instance has required properties
1900
+ if (!this.human.version || typeof this.human.detect !== 'function') {
1901
+ const errorMsg = 'Human.js instance is incomplete: missing version or detect method';
1902
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1903
+ this.emitDebug('initialization', errorMsg, {
1904
+ hasVersion: !!this.human.version,
1905
+ hasDetect: typeof this.human.detect === 'function',
1906
+ instanceKeys: Object.keys(this.human || {})
1907
+ }, 'error');
1908
+ this.emit('detector-loaded', {
1909
+ success: false,
1910
+ error: errorMsg
1911
+ });
1912
+ this.emit('detector-error', {
1913
+ code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
1914
+ message: errorMsg
1915
+ });
1916
+ return;
1917
+ }
1640
1918
  this.emitDebug('initialization', 'Human.js loaded successfully', {
1641
1919
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1642
- version: this.human.version
1920
+ version: this.human.version,
1921
+ backend: this.human.config?.backend || 'unknown',
1922
+ config: this.human.config
1643
1923
  });
1644
1924
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
1645
1925
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1646
- version: this.human.version
1926
+ version: this.human.version,
1927
+ backend: this.human.config?.backend || 'unknown'
1647
1928
  });
1648
1929
  this.isReady = true;
1649
1930
  const loadedData = {
@@ -1920,13 +2201,31 @@
1920
2201
  try {
1921
2202
  // Check video is ready
1922
2203
  if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1923
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2204
+ this.scheduleNextDetection(this.config.error_retry_delay);
1924
2205
  return;
1925
2206
  }
1926
2207
  // Perform face detection
1927
- const result = await this.human.detect(this.videoElement);
2208
+ let result;
2209
+ try {
2210
+ result = await this.human.detect(this.videoElement);
2211
+ }
2212
+ catch (detectError) {
2213
+ const errorMsg = detectError instanceof Error ? detectError.message : 'Unknown error';
2214
+ this.emitDebug('detection', 'Human.detect() call failed', {
2215
+ error: errorMsg,
2216
+ stack: detectError instanceof Error ? detectError.stack : 'N/A',
2217
+ hasHuman: !!this.human,
2218
+ humanVersion: this.human?.version,
2219
+ videoReadyState: this.videoElement?.readyState,
2220
+ videoWidth: this.videoElement?.videoWidth,
2221
+ videoHeight: this.videoElement?.videoHeight
2222
+ }, 'error');
2223
+ this.scheduleNextDetection(this.config.error_retry_delay);
2224
+ return;
2225
+ }
1928
2226
  if (!result) {
1929
- this.scheduleNextDetection(this.config.error_retry_delay); // DETECTION_FRAME_DELAY
2227
+ this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
2228
+ this.scheduleNextDetection(this.config.error_retry_delay);
1930
2229
  return;
1931
2230
  }
1932
2231
  const faces = result.face || [];
@@ -1939,11 +2238,12 @@
1939
2238
  }
1940
2239
  }
1941
2240
  catch (error) {
1942
- this.emitDebug('detection', 'Detection error', {
1943
- error: error.message,
1944
- stack: error.stack
2241
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
2242
+ this.emitDebug('detection', 'Unexpected error in detection loop', {
2243
+ error: errorMsg,
2244
+ stack: error instanceof Error ? error.stack : 'N/A'
1945
2245
  }, 'error');
1946
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2246
+ this.scheduleNextDetection(this.config.error_retry_delay);
1947
2247
  }
1948
2248
  }
1949
2249
  getPerformActionCount() {
@@ -2307,6 +2607,29 @@
2307
2607
  };
2308
2608
  this.emit('status-prompt', promptData);
2309
2609
  }
2610
+ /**
2611
+ * Detect browser information for debugging
2612
+ */
2613
+ detectBrowserInfo() {
2614
+ const ua = navigator.userAgent.toLowerCase();
2615
+ if (/quark/i.test(ua))
2616
+ return 'Quark Browser';
2617
+ if (/micromessenger/i.test(ua))
2618
+ return 'WeChat';
2619
+ if (/alipay/i.test(ua))
2620
+ return 'Alipay';
2621
+ if (/qq/i.test(ua))
2622
+ return 'QQ';
2623
+ if (/safari/i.test(ua) && !/chrome/i.test(ua))
2624
+ return 'Safari';
2625
+ if (/chrome/i.test(ua))
2626
+ return 'Chrome';
2627
+ if (/firefox/i.test(ua))
2628
+ return 'Firefox';
2629
+ if (/edge/i.test(ua))
2630
+ return 'Edge';
2631
+ return 'Unknown';
2632
+ }
2310
2633
  /**
2311
2634
  * Emit debug event
2312
2635
  */