@sssxyd/face-liveness-detector 0.2.31 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,53 +31,6 @@ pnpm add @sssxyd/face-liveness-detector @vladmandic/human @techstark/opencv-js
31
31
 
32
32
  > **Note**: `@vladmandic/human` and `@techstark/opencv-js` are peer dependencies and must be installed separately to avoid bundling large libraries. This keeps your final bundle size smaller if you're already using these libraries elsewhere in your project.
33
33
 
34
- ## Quick Start - Using Local Model Files (Recommended)
35
-
36
- To improve performance and reduce external dependencies, you can download and use local copies of model files:
37
-
38
- ### Step 1: Download Model Files
39
-
40
- ```bash
41
- # Copy Human.js models locally
42
- node copy-human-models.js
43
-
44
- # Download TensorFlow.js WASM files
45
- node download-tensorflow-wasm.js
46
- ```
47
-
48
- This will create:
49
- - `public/models/` - Human.js face detection models
50
- - `public/wasm/` - TensorFlow.js WASM backend files
51
-
52
- ### Step 2: Initialize Engine with Local Files
53
-
54
- ```typescript
55
- import FaceDetectionEngine from '@sssxyd/face-liveness-detector'
56
-
57
- // Configure to use local model files
58
- const engine = new FaceDetectionEngine({
59
- human_model_path: '/models', // Path to downloaded models
60
- tensorflow_wasm_path: '/wasm', // Path to WASM files
61
- min_face_ratio: 0.5,
62
- max_face_ratio: 0.9,
63
- liveness_action_count: 1,
64
- liveness_action_list: ['blink']
65
- })
66
-
67
- // Initialize and start detection
68
- await engine.initialize()
69
- const videoElement = document.getElementById('video') as HTMLVideoElement
70
- await engine.startDetection(videoElement)
71
- ```
72
-
73
- ### Step 3: Serve Static Files
74
-
75
- Make sure your web server serves the `public/` directory:
76
-
77
- ```typescript
78
- // Express.js example
79
- app.use(express.static('public'))
80
- ```
81
34
 
82
35
  ## Quick Start - Using Default CDN Files
83
36
 
package/dist/index.esm.js CHANGED
@@ -310,21 +310,26 @@ function _isWebGLAvailable() {
310
310
  }
311
311
  function _detectBrowserEngine(userAgent) {
312
312
  const ua = userAgent.toLowerCase();
313
- // 检测 Gecko (Firefox)
313
+ // 1. 检测 Gecko (Firefox)
314
314
  if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
315
315
  return 'gecko';
316
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
317
+ // 2. 检测 Chromium/Blink(必须在 WebKit 之前,因为 Chrome 的 user-agent 也包含 WebKit)
323
318
  // Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
324
319
  if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
325
320
  return 'chromium';
326
321
  }
327
- // 默认为 other
322
+ // 3. 检测 WebKit(真正的 Safari 和 iOS 浏览器)
323
+ // 注意:真正的 WebKit 浏览器(Safari)user-agent 不包含 Chrome 标识
324
+ // 包括:Safari、iOS 浏览器、以及那些虽然包含 Chrome 标识但实际是 WebKit 的浏览器(Quark、支付宝、微信等)
325
+ if (/webkit/i.test(ua)) {
326
+ // WebKit 特征明显,包括以下几种情况:
327
+ // - 真正的 Safari(有 Safari 标识)
328
+ // - iOS 浏览器(有 Mobile Safari 标识)
329
+ // - Quark、支付宝、微信等虽然包含 Chrome 标识但是基于 WebKit 的浏览器
330
+ return 'webkit';
331
+ }
332
+ // 4. 其他浏览器 - 保守方案,使用 WASM
328
333
  return 'other';
329
334
  }
330
335
  function _getOptimalBackendForEngine(engine) {
@@ -563,15 +568,11 @@ function getCvSync() {
563
568
  return null;
564
569
  }
565
570
  /**
566
- * Load Human.js
567
- * @param modelPath - Path to model files (optional)
568
- * @param wasmPath - Path to WASM files (optional)
569
- * @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
570
- * @returns Promise that resolves with Human instance
571
+ * Create Human.js configuration object
571
572
  */
572
- async function loadHuman(modelPath, wasmPath, preferredBackend) {
573
+ function _createHumanConfig(backend, modelPath, wasmPath) {
573
574
  const config = {
574
- backend: _detectOptimalBackend(preferredBackend),
575
+ backend,
575
576
  face: {
576
577
  enabled: true,
577
578
  detector: { rotation: false, return: true },
@@ -585,102 +586,196 @@ async function loadHuman(modelPath, wasmPath, preferredBackend) {
585
586
  object: { enabled: false },
586
587
  gesture: { enabled: true }
587
588
  };
588
- // 只在提供了路径时才设置,否则让 Human.js 使用默认加载策略
589
589
  if (modelPath) {
590
590
  config.modelBasePath = modelPath;
591
591
  }
592
592
  if (wasmPath) {
593
593
  config.wasmPath = wasmPath;
594
594
  }
595
- console.log('[FaceDetectionEngine] Human.js config:', {
596
- backend: config.backend,
597
- modelBasePath: config.modelBasePath || '(using default)',
598
- wasmPath: config.wasmPath || '(using default)',
599
- userAgent: navigator.userAgent,
600
- platform: navigator.platform
595
+ return config;
596
+ }
597
+ /**
598
+ * Load and verify Human.js models
599
+ */
600
+ async function _loadAndVerifyHuman(human) {
601
+ const modelLoadStartTime = performance.now();
602
+ try {
603
+ await human.load();
604
+ }
605
+ catch (error) {
606
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
607
+ const errorStack = error instanceof Error ? error.stack : 'N/A';
608
+ console.error('[FaceDetectionEngine] Error during human.load():', {
609
+ errorMsg,
610
+ stack: errorStack,
611
+ backend: human.config?.backend,
612
+ hasModels: !!human.models,
613
+ modelsKeys: human.models ? Object.keys(human.models).length : 0
614
+ });
615
+ throw new Error(`Model loading error: ${errorMsg}`);
616
+ }
617
+ const loadTime = performance.now() - modelLoadStartTime;
618
+ console.log('[FaceDetectionEngine] Human.js loaded successfully', {
619
+ modelLoadTime: `${loadTime.toFixed(2)}ms`,
620
+ version: human.version,
621
+ config: human.config
601
622
  });
623
+ // 验证加载后的 Human 实例有必要的方法和属性
624
+ if (typeof human.detect !== 'function') {
625
+ throw new Error('Human.detect method not available after loading');
626
+ }
627
+ if (!human.version) {
628
+ console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
629
+ }
630
+ // 关键验证:检查模型是否真的加载了
631
+ if (!human.models || Object.keys(human.models).length === 0) {
632
+ console.error('[FaceDetectionEngine] CRITICAL: human.models is empty after loading!');
633
+ throw new Error('No models were loaded - human.models is empty');
634
+ }
635
+ // 详细检查每个关键模型及其结构
636
+ const criticalModels = ['face', 'antispoof', 'liveness'];
637
+ const missingModels = [];
638
+ for (const modelName of criticalModels) {
639
+ const model = human.models[modelName];
640
+ if (!model) {
641
+ missingModels.push(modelName);
642
+ console.error(`[FaceDetectionEngine] CRITICAL: Model '${modelName}' is missing!`);
643
+ }
644
+ else {
645
+ const isLoaded = model.loaded || model.state === 'loaded' || !!model.model;
646
+ // 检查模型是否有必要的内部结构(防止 "Cannot read properties of undefined (reading 'inputs')" 错误)
647
+ const hasExecutor = !!model['executor'];
648
+ const hasInputs = !!model.inputs && Array.isArray(model.inputs) && model.inputs.length > 0;
649
+ const hasModelUrl = !!model['modelUrl'];
650
+ console.log(`[FaceDetectionEngine] Model '${modelName}':`, {
651
+ loaded: isLoaded,
652
+ state: model.state,
653
+ hasModel: !!model.model,
654
+ hasExecutor,
655
+ hasInputs,
656
+ hasModelUrl,
657
+ inputsType: typeof model.inputs,
658
+ inputsLength: Array.isArray(model.inputs) ? model.inputs.length : 'N/A'
659
+ });
660
+ // 严格检查:模型必须有以下结构才能正常工作
661
+ if (!isLoaded || !hasExecutor || !hasModelUrl) {
662
+ missingModels.push(`${modelName} (incomplete)`);
663
+ console.error(`[FaceDetectionEngine] WARNING: Model '${modelName}' may not be fully loaded - missing structure`);
664
+ }
665
+ // 如果 inputs 未定义会导致 "Cannot read properties of undefined (reading 'inputs')" 错误
666
+ if (!hasInputs && modelName !== 'antispoof') {
667
+ console.warn(`[FaceDetectionEngine] WARNING: Model '${modelName}' has no inputs - may cause errors during detection`);
668
+ missingModels.push(`${modelName} (no inputs)`);
669
+ }
670
+ }
671
+ }
672
+ if (missingModels.length > 0) {
673
+ console.error('[FaceDetectionEngine] Some critical models failed to load:', missingModels);
674
+ throw new Error(`Critical models not loaded: ${missingModels.join(', ')}`);
675
+ }
676
+ // 打印加载的模型信息
677
+ if (human.models) {
678
+ const loadedModels = Object.entries(human.models).map(([name, model]) => ({
679
+ name,
680
+ loaded: model?.loaded || model?.state === 'loaded',
681
+ type: typeof model,
682
+ hasModel: !!model?.model
683
+ }));
684
+ console.log('[FaceDetectionEngine] All loaded models:', {
685
+ backend: human.config?.backend,
686
+ modelBasePath: human.config?.modelBasePath,
687
+ wasmPath: human.config?.wasmPath,
688
+ totalModels: Object.keys(human.models).length,
689
+ models: loadedModels,
690
+ allModelNames: Object.keys(human.models)
691
+ });
692
+ }
693
+ }
694
+ /**
695
+ * Try to load Human with a specific backend
696
+ * @param config The configuration object
697
+ * @param backend The backend to try
698
+ * @returns Human instance or null if fails
699
+ */
700
+ async function _tryLoadHumanWithBackend(backend, modelPath, wasmPath) {
701
+ const config = _createHumanConfig(backend, modelPath, wasmPath);
602
702
  const initStartTime = performance.now();
603
- console.log('[FaceDetectionEngine] Creating Human instance...');
604
703
  let human;
605
704
  try {
606
705
  human = new Human(config);
607
706
  }
608
707
  catch (error) {
609
708
  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}`);
709
+ const stack = error instanceof Error ? error.stack : 'N/A';
710
+ console.error(`[FaceDetectionEngine] Failed to create Human instance (${backend}):`, {
711
+ errorMsg,
712
+ stack,
713
+ backend: config.backend,
714
+ userAgent: navigator.userAgent
715
+ });
716
+ return null;
612
717
  }
613
- const instanceCreateTime = performance.now() - initStartTime;
614
- console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
615
718
  // 验证 Human 实例
616
719
  if (!human) {
617
- throw new Error('Human instance is null after creation');
720
+ console.error(`[FaceDetectionEngine] Human instance is null (${backend})`);
721
+ return null;
722
+ }
723
+ // 验证 Human 实例结构(早期检测 WASM 问题)
724
+ if (!human.config) {
725
+ console.warn('[FaceDetectionEngine] Warning: human.config is missing');
618
726
  }
619
- console.log('[FaceDetectionEngine] Loading Human.js models...');
620
- const modelLoadStartTime = performance.now();
621
727
  try {
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
- ]);
633
- const loadTime = performance.now() - modelLoadStartTime;
728
+ await _loadAndVerifyHuman(human);
634
729
  const totalTime = performance.now() - initStartTime;
635
- console.log('[FaceDetectionEngine] Human.js loaded successfully', {
636
- modelLoadTime: `${loadTime.toFixed(2)}ms`,
637
- totalInitTime: `${totalTime.toFixed(2)}ms`,
638
- version: human.version,
639
- config: human.config
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
- }
730
+ console.log(`[FaceDetectionEngine] Successfully loaded Human.js with ${backend} backend in ${totalTime.toFixed(2)}ms`);
668
731
  return human;
669
732
  }
670
733
  catch (error) {
671
734
  const errorMsg = error instanceof Error ? error.message : 'Unknown 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}`);
735
+ console.error(`[FaceDetectionEngine] Failed to load models with ${backend} backend:`, errorMsg);
736
+ return null;
682
737
  }
683
738
  }
739
+ /**
740
+ * Load Human.js
741
+ * @param modelPath - Path to model files (optional)
742
+ * @param wasmPath - Path to WASM files (optional)
743
+ * @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
744
+ * @returns Promise that resolves with Human instance
745
+ */
746
+ async function loadHuman(modelPath, wasmPath, preferredBackend) {
747
+ const selectedBackend = _detectOptimalBackend(preferredBackend);
748
+ console.log('[FaceDetectionEngine] Starting Human.js initialization:', {
749
+ selectedBackend,
750
+ modelBasePath: modelPath || '(using default)',
751
+ wasmPath: wasmPath || '(using default)',
752
+ userAgent: navigator.userAgent,
753
+ platform: navigator.platform
754
+ });
755
+ // 尝试用主后端加载
756
+ const human = await _tryLoadHumanWithBackend(selectedBackend, modelPath, wasmPath);
757
+ if (human) {
758
+ return human;
759
+ }
760
+ console.log(`[FaceDetectionEngine] Human.js loading failed with ${selectedBackend} backend.`);
761
+ // 尝试用备选后端加载(最多一次降级)
762
+ let fallbackBackend;
763
+ if (selectedBackend === 'wasm' && _isWebGLAvailable()) {
764
+ fallbackBackend = 'webgl';
765
+ }
766
+ else if (selectedBackend === 'webgl') {
767
+ fallbackBackend = 'wasm';
768
+ }
769
+ if (fallbackBackend) {
770
+ console.warn(`[FaceDetectionEngine] Primary backend (${selectedBackend}) failed, attempting fallback to ${fallbackBackend}...`);
771
+ const humanFallback = await _tryLoadHumanWithBackend(fallbackBackend, modelPath, wasmPath);
772
+ if (humanFallback) {
773
+ return humanFallback;
774
+ }
775
+ throw new Error(`Human.js loading failed: both ${selectedBackend} and ${fallbackBackend} backends failed`);
776
+ }
777
+ throw new Error(`Human.js loading failed: ${selectedBackend} backend failed (no fallback available)`);
778
+ }
684
779
  /**
685
780
  * Extract OpenCV version from getBuildInformation
686
781
  * @returns version string like "4.12.0"
@@ -1719,28 +1814,59 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1719
1814
  }
1720
1815
  catch (humanError) {
1721
1816
  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', {
1817
+ const stack = humanError instanceof Error ? humanError.stack : 'N/A';
1818
+ // 分析错误类型,提供针对性的建议
1819
+ let errorContext = {
1724
1820
  error: errorMsg,
1725
- stack: humanError instanceof Error ? humanError.stack : 'N/A',
1821
+ stack,
1726
1822
  userAgent: navigator.userAgent,
1727
1823
  platform: navigator.platform,
1728
- browser: this.detectBrowserInfo()
1729
- }, 'error');
1824
+ browser: this.detectBrowserInfo(),
1825
+ backend: this.config.tensorflow_backend,
1826
+ source: 'human.js'
1827
+ };
1828
+ // 特定错误类型的诊断
1829
+ if (errorMsg.includes('inputs')) {
1830
+ errorContext.diagnosis = 'Human.js internal error: Model structure incomplete';
1831
+ errorContext.rootCause = 'Human.js library issue - models not fully loaded or WASM backend initialization incomplete';
1832
+ errorContext.suggestion = 'This is a Human.js library issue. Models may not have proper executor or inputs structure. Check WASM initialization and model integrity.';
1833
+ }
1834
+ else if (errorMsg.includes('timeout')) {
1835
+ errorContext.diagnosis = 'Model loading timeout';
1836
+ errorContext.suggestion = 'Network issue or model file too large - check network conditions';
1837
+ }
1838
+ else if (errorMsg.includes('Critical models not loaded')) {
1839
+ errorContext.diagnosis = 'Human.js failed to load required models';
1840
+ errorContext.rootCause = 'Models (face, antispoof, liveness) are missing or incomplete';
1841
+ errorContext.suggestion = 'Check model files and ensure WASM backend is properly initialized';
1842
+ }
1843
+ else if (errorMsg.includes('empty')) {
1844
+ errorContext.diagnosis = 'Models object is empty after loading';
1845
+ errorContext.suggestion = 'Model path may be incorrect or HTTP response failed';
1846
+ }
1847
+ else if (errorMsg.includes('incomplete')) {
1848
+ errorContext.diagnosis = 'Models loaded but structure is incomplete';
1849
+ errorContext.rootCause = 'Human.js internal issue - missing executor, inputs, or modelUrl';
1850
+ errorContext.suggestion = 'Ensure all model resources are fully loaded and accessible';
1851
+ }
1852
+ console.error('[FaceDetectionEngine] Human.js loading failed with detailed error:', errorContext);
1853
+ this.emitDebug('initialization', 'Human.js loading failed with exception', errorContext, 'error');
1730
1854
  this.emit('detector-loaded', {
1731
1855
  success: false,
1732
- error: `Failed to load Human.js: ${errorMsg}`
1856
+ error: `Failed to load Human.js: ${errorMsg}`,
1857
+ details: errorContext
1733
1858
  });
1734
1859
  this.emit('detector-error', {
1735
1860
  code: ErrorCode.DETECTOR_NOT_INITIALIZED,
1736
- message: `Human.js loading error: ${errorMsg}`
1861
+ message: `Human.js loading error: ${errorMsg}`,
1862
+ details: errorContext
1737
1863
  });
1738
1864
  return;
1739
1865
  }
1740
1866
  const humanLoadTime = performance.now() - humanStartTime;
1741
1867
  if (!this.human) {
1742
1868
  const errorMsg = 'Failed to load Human.js: instance is null';
1743
- console.log('[FaceDetectionEngine] ' + errorMsg);
1869
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1744
1870
  this.emitDebug('initialization', errorMsg, { loadTime: humanLoadTime }, 'error');
1745
1871
  this.emit('detector-loaded', {
1746
1872
  success: false,