@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 +324 -97
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +324 -97
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/library-loader.d.ts +2 -1
- package/dist/types/library-loader.d.ts.map +1 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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);
|
|
2060
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1906
2061
|
return;
|
|
1907
2062
|
}
|
|
1908
2063
|
// Perform face detection
|
|
1909
|
-
|
|
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.
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
-
|
|
2166
|
-
|
|
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
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
this.
|
|
2392
|
-
|
|
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
|
|
2623
|
+
this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
|
|
2624
|
+
return this.canvasToBase64(this.faceCanvasElement);
|
|
2396
2625
|
}
|
|
2397
|
-
|
|
2626
|
+
catch (error) {
|
|
2627
|
+
this.emitDebug('capture', 'Error during face frame capture', { box, error: error.message }, 'error');
|
|
2398
2628
|
return null;
|
|
2399
|
-
|
|
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
|
|