@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.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,68 @@
|
|
|
326
330
|
return false;
|
|
327
331
|
}
|
|
328
332
|
}
|
|
329
|
-
function
|
|
333
|
+
function _detectBrowserEngine(userAgent) {
|
|
334
|
+
const ua = userAgent.toLowerCase();
|
|
335
|
+
// 检测 Gecko (Firefox)
|
|
336
|
+
if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
|
|
337
|
+
return 'gecko';
|
|
338
|
+
}
|
|
339
|
+
// 检测 WebKit (Safari, iOS browsers)
|
|
340
|
+
// Safari 的特征:有 Safari 但没有 Chrome
|
|
341
|
+
if (/safari/i.test(ua) && !/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
|
|
342
|
+
return 'webkit';
|
|
343
|
+
}
|
|
344
|
+
// 检测 Chromium/Blink
|
|
345
|
+
// Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
|
|
346
|
+
if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
|
|
347
|
+
return 'chromium';
|
|
348
|
+
}
|
|
349
|
+
// 默认为 other
|
|
350
|
+
return 'other';
|
|
351
|
+
}
|
|
352
|
+
function _getOptimalBackendForEngine(engine) {
|
|
353
|
+
// 针对不同内核的优化策略
|
|
354
|
+
const backendConfig = {
|
|
355
|
+
chromium: 'webgl', // Chromium 内核:优先 WebGL
|
|
356
|
+
webkit: 'wasm', // WebKit(Safari、iOS):使用 WASM
|
|
357
|
+
gecko: 'webgl', // Firefox:优先 WebGL
|
|
358
|
+
other: 'wasm' // 未知浏览器:保守使用 WASM
|
|
359
|
+
};
|
|
360
|
+
return backendConfig[engine];
|
|
361
|
+
}
|
|
362
|
+
function _detectOptimalBackend(preferredBackend) {
|
|
363
|
+
// If user explicitly specified a backend, honor it (unless it's 'auto')
|
|
364
|
+
if (preferredBackend && preferredBackend !== 'auto') {
|
|
365
|
+
console.log('[Backend Detection] Using user-specified backend:', {
|
|
366
|
+
backend: preferredBackend,
|
|
367
|
+
userAgent: navigator.userAgent
|
|
368
|
+
});
|
|
369
|
+
return preferredBackend;
|
|
370
|
+
}
|
|
330
371
|
const userAgent = navigator.userAgent.toLowerCase();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
const engine = _detectBrowserEngine(userAgent);
|
|
373
|
+
console.log('[Backend Detection] Detected browser engine:', {
|
|
374
|
+
engine,
|
|
375
|
+
userAgent: navigator.userAgent
|
|
376
|
+
});
|
|
377
|
+
// 获取该内核的推荐后端
|
|
378
|
+
let preferredBackendForEngine = _getOptimalBackendForEngine(engine);
|
|
379
|
+
// 对于 Chromium 和 Gecko,检查 WebGL 是否可用
|
|
380
|
+
if (preferredBackendForEngine === 'webgl') {
|
|
381
|
+
const hasWebGL = _isWebGLAvailable();
|
|
382
|
+
console.log('[Backend Detection] WebGL availability check:', {
|
|
383
|
+
engine,
|
|
384
|
+
hasWebGL,
|
|
385
|
+
selectedBackend: hasWebGL ? 'webgl' : 'wasm'
|
|
386
|
+
});
|
|
387
|
+
return hasWebGL ? 'webgl' : 'wasm';
|
|
388
|
+
}
|
|
389
|
+
// 对于 WebKit 和 other,直接使用 WASM
|
|
390
|
+
console.log('[Backend Detection] Using backend for engine:', {
|
|
391
|
+
engine,
|
|
392
|
+
backend: preferredBackendForEngine
|
|
393
|
+
});
|
|
394
|
+
return preferredBackendForEngine;
|
|
341
395
|
}
|
|
342
396
|
/**
|
|
343
397
|
* 预加载 OpenCV.js 以确保全局 cv 对象可用
|
|
@@ -472,10 +526,6 @@
|
|
|
472
526
|
try {
|
|
473
527
|
await opencvInitPromise;
|
|
474
528
|
cv = getCvSync();
|
|
475
|
-
if (cv && cv.Mat) {
|
|
476
|
-
console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
|
|
477
|
-
return { cv };
|
|
478
|
-
}
|
|
479
529
|
}
|
|
480
530
|
catch (error) {
|
|
481
531
|
// 失败后清除 Promise,允许重试
|
|
@@ -495,10 +545,6 @@
|
|
|
495
545
|
try {
|
|
496
546
|
await opencvInitPromise;
|
|
497
547
|
cv = getCvSync();
|
|
498
|
-
if (cv && cv.Mat) {
|
|
499
|
-
console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
|
|
500
|
-
return { cv };
|
|
501
|
-
}
|
|
502
548
|
}
|
|
503
549
|
catch (error) {
|
|
504
550
|
// 失败后清除 Promise,允许重试
|
|
@@ -506,13 +552,17 @@
|
|
|
506
552
|
throw error;
|
|
507
553
|
}
|
|
508
554
|
}
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
555
|
+
// 最终验证
|
|
556
|
+
if (!cv || !cv.Mat) {
|
|
557
|
+
console.error('[FaceDetectionEngine] OpenCV module is invalid:', {
|
|
558
|
+
hasMat: cv && cv.Mat,
|
|
559
|
+
type: typeof cv,
|
|
560
|
+
keys: cv ? Object.keys(cv).slice(0, 10) : 'N/A'
|
|
561
|
+
});
|
|
562
|
+
throw new Error('OpenCV.js loaded but module is invalid (no Mat class found)');
|
|
563
|
+
}
|
|
564
|
+
console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
|
|
565
|
+
return { cv };
|
|
516
566
|
}
|
|
517
567
|
catch (error) {
|
|
518
568
|
console.error('[FaceDetectionEngine] Failed to load OpenCV.js:', error);
|
|
@@ -538,11 +588,12 @@
|
|
|
538
588
|
* Load Human.js
|
|
539
589
|
* @param modelPath - Path to model files (optional)
|
|
540
590
|
* @param wasmPath - Path to WASM files (optional)
|
|
591
|
+
* @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
|
|
541
592
|
* @returns Promise that resolves with Human instance
|
|
542
593
|
*/
|
|
543
|
-
async function loadHuman(modelPath, wasmPath) {
|
|
594
|
+
async function loadHuman(modelPath, wasmPath, preferredBackend) {
|
|
544
595
|
const config = {
|
|
545
|
-
backend: _detectOptimalBackend(),
|
|
596
|
+
backend: _detectOptimalBackend(preferredBackend),
|
|
546
597
|
face: {
|
|
547
598
|
enabled: true,
|
|
548
599
|
detector: { rotation: false, return: true },
|
|
@@ -566,30 +617,90 @@
|
|
|
566
617
|
console.log('[FaceDetectionEngine] Human.js config:', {
|
|
567
618
|
backend: config.backend,
|
|
568
619
|
modelBasePath: config.modelBasePath || '(using default)',
|
|
569
|
-
wasmPath: config.wasmPath || '(using default)'
|
|
620
|
+
wasmPath: config.wasmPath || '(using default)',
|
|
621
|
+
userAgent: navigator.userAgent,
|
|
622
|
+
platform: navigator.platform
|
|
570
623
|
});
|
|
571
624
|
const initStartTime = performance.now();
|
|
572
625
|
console.log('[FaceDetectionEngine] Creating Human instance...');
|
|
573
|
-
|
|
626
|
+
let human;
|
|
627
|
+
try {
|
|
628
|
+
human = new Human(config);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error during Human instantiation';
|
|
632
|
+
console.error('[FaceDetectionEngine] Failed to create Human instance:', errorMsg);
|
|
633
|
+
throw new Error(`Human instantiation failed: ${errorMsg}`);
|
|
634
|
+
}
|
|
574
635
|
const instanceCreateTime = performance.now() - initStartTime;
|
|
575
636
|
console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
|
|
637
|
+
// 验证 Human 实例
|
|
638
|
+
if (!human) {
|
|
639
|
+
throw new Error('Human instance is null after creation');
|
|
640
|
+
}
|
|
576
641
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
577
642
|
const modelLoadStartTime = performance.now();
|
|
578
643
|
try {
|
|
579
|
-
|
|
644
|
+
// 添加超时机制防止无限等待
|
|
645
|
+
const loadTimeout = new Promise((_, reject) => {
|
|
646
|
+
const timeoutId = setTimeout(() => {
|
|
647
|
+
reject(new Error('Human.js load() timeout after 60 seconds - possible issue with model loading on mobile'));
|
|
648
|
+
}, 60000);
|
|
649
|
+
});
|
|
650
|
+
// 竞速:哪个先完成就用哪个
|
|
651
|
+
await Promise.race([
|
|
652
|
+
human.load(),
|
|
653
|
+
loadTimeout
|
|
654
|
+
]);
|
|
580
655
|
const loadTime = performance.now() - modelLoadStartTime;
|
|
581
656
|
const totalTime = performance.now() - initStartTime;
|
|
582
657
|
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
583
658
|
modelLoadTime: `${loadTime.toFixed(2)}ms`,
|
|
584
659
|
totalInitTime: `${totalTime.toFixed(2)}ms`,
|
|
585
|
-
version: human.version
|
|
660
|
+
version: human.version,
|
|
661
|
+
config: human.config
|
|
586
662
|
});
|
|
663
|
+
// 验证加载后的 Human 实例有必要的方法和属性
|
|
664
|
+
if (typeof human.detect !== 'function') {
|
|
665
|
+
throw new Error('Human.detect method not available after loading');
|
|
666
|
+
}
|
|
667
|
+
if (!human.version) {
|
|
668
|
+
console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
|
|
669
|
+
}
|
|
670
|
+
// 打印加载的模型信息
|
|
671
|
+
if (human.models) {
|
|
672
|
+
const loadedModels = Object.entries(human.models).map(([name, model]) => ({
|
|
673
|
+
name,
|
|
674
|
+
loaded: model?.loaded || model?.state === 'loaded',
|
|
675
|
+
type: typeof model
|
|
676
|
+
}));
|
|
677
|
+
console.log('[FaceDetectionEngine] Loaded models:', {
|
|
678
|
+
totalModels: Object.keys(human.models).length,
|
|
679
|
+
models: loadedModels,
|
|
680
|
+
allModels: Object.keys(human.models)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
console.warn('[FaceDetectionEngine] human.models is not available');
|
|
685
|
+
}
|
|
686
|
+
// 额外验证:检查模型是否加载成功
|
|
687
|
+
if (!human.models || Object.keys(human.models).length === 0) {
|
|
688
|
+
console.warn('[FaceDetectionEngine] Warning: human.models appears to be empty after loading');
|
|
689
|
+
}
|
|
587
690
|
return human;
|
|
588
691
|
}
|
|
589
692
|
catch (error) {
|
|
590
693
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
591
|
-
|
|
592
|
-
|
|
694
|
+
const stack = error instanceof Error ? error.stack : 'N/A';
|
|
695
|
+
console.error('[FaceDetectionEngine] Human.js load failed:', {
|
|
696
|
+
errorMsg,
|
|
697
|
+
stack,
|
|
698
|
+
userAgent: navigator.userAgent,
|
|
699
|
+
platform: navigator.platform,
|
|
700
|
+
humanVersion: human?.version,
|
|
701
|
+
humanConfig: human?.config
|
|
702
|
+
});
|
|
703
|
+
throw new Error(`Human.js loading failed: ${errorMsg}`);
|
|
593
704
|
}
|
|
594
705
|
}
|
|
595
706
|
/**
|
|
@@ -1625,7 +1736,29 @@
|
|
|
1625
1736
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
1626
1737
|
this.emitDebug('initialization', 'Loading Human.js...');
|
|
1627
1738
|
const humanStartTime = performance.now();
|
|
1628
|
-
|
|
1739
|
+
try {
|
|
1740
|
+
this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path, this.config.tensorflow_backend);
|
|
1741
|
+
}
|
|
1742
|
+
catch (humanError) {
|
|
1743
|
+
const errorMsg = humanError instanceof Error ? humanError.message : 'Unknown error';
|
|
1744
|
+
console.error('[FaceDetectionEngine] Human.js loading failed with error:', errorMsg);
|
|
1745
|
+
this.emitDebug('initialization', 'Human.js loading failed with exception', {
|
|
1746
|
+
error: errorMsg,
|
|
1747
|
+
stack: humanError instanceof Error ? humanError.stack : 'N/A',
|
|
1748
|
+
userAgent: navigator.userAgent,
|
|
1749
|
+
platform: navigator.platform,
|
|
1750
|
+
browser: this.detectBrowserInfo()
|
|
1751
|
+
}, 'error');
|
|
1752
|
+
this.emit('detector-loaded', {
|
|
1753
|
+
success: false,
|
|
1754
|
+
error: `Failed to load Human.js: ${errorMsg}`
|
|
1755
|
+
});
|
|
1756
|
+
this.emit('detector-error', {
|
|
1757
|
+
code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
|
|
1758
|
+
message: `Human.js loading error: ${errorMsg}`
|
|
1759
|
+
});
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1629
1762
|
const humanLoadTime = performance.now() - humanStartTime;
|
|
1630
1763
|
if (!this.human) {
|
|
1631
1764
|
const errorMsg = 'Failed to load Human.js: instance is null';
|
|
@@ -1641,13 +1774,35 @@
|
|
|
1641
1774
|
});
|
|
1642
1775
|
return;
|
|
1643
1776
|
}
|
|
1777
|
+
// Verify Human.js instance has required properties
|
|
1778
|
+
if (!this.human.version || typeof this.human.detect !== 'function') {
|
|
1779
|
+
const errorMsg = 'Human.js instance is incomplete: missing version or detect method';
|
|
1780
|
+
console.error('[FaceDetectionEngine] ' + errorMsg);
|
|
1781
|
+
this.emitDebug('initialization', errorMsg, {
|
|
1782
|
+
hasVersion: !!this.human.version,
|
|
1783
|
+
hasDetect: typeof this.human.detect === 'function',
|
|
1784
|
+
instanceKeys: Object.keys(this.human || {})
|
|
1785
|
+
}, 'error');
|
|
1786
|
+
this.emit('detector-loaded', {
|
|
1787
|
+
success: false,
|
|
1788
|
+
error: errorMsg
|
|
1789
|
+
});
|
|
1790
|
+
this.emit('detector-error', {
|
|
1791
|
+
code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
|
|
1792
|
+
message: errorMsg
|
|
1793
|
+
});
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1644
1796
|
this.emitDebug('initialization', 'Human.js loaded successfully', {
|
|
1645
1797
|
loadTime: `${humanLoadTime.toFixed(2)}ms`,
|
|
1646
|
-
version: this.human.version
|
|
1798
|
+
version: this.human.version,
|
|
1799
|
+
backend: this.human.config?.backend || 'unknown',
|
|
1800
|
+
config: this.human.config
|
|
1647
1801
|
});
|
|
1648
1802
|
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
1649
1803
|
loadTime: `${humanLoadTime.toFixed(2)}ms`,
|
|
1650
|
-
version: this.human.version
|
|
1804
|
+
version: this.human.version,
|
|
1805
|
+
backend: this.human.config?.backend || 'unknown'
|
|
1651
1806
|
});
|
|
1652
1807
|
this.isReady = true;
|
|
1653
1808
|
const loadedData = {
|
|
@@ -1924,13 +2079,31 @@
|
|
|
1924
2079
|
try {
|
|
1925
2080
|
// Check video is ready
|
|
1926
2081
|
if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1927
|
-
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2082
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1928
2083
|
return;
|
|
1929
2084
|
}
|
|
1930
2085
|
// Perform face detection
|
|
1931
|
-
|
|
2086
|
+
let result;
|
|
2087
|
+
try {
|
|
2088
|
+
result = await this.human.detect(this.videoElement);
|
|
2089
|
+
}
|
|
2090
|
+
catch (detectError) {
|
|
2091
|
+
const errorMsg = detectError instanceof Error ? detectError.message : 'Unknown error';
|
|
2092
|
+
this.emitDebug('detection', 'Human.detect() call failed', {
|
|
2093
|
+
error: errorMsg,
|
|
2094
|
+
stack: detectError instanceof Error ? detectError.stack : 'N/A',
|
|
2095
|
+
hasHuman: !!this.human,
|
|
2096
|
+
humanVersion: this.human?.version,
|
|
2097
|
+
videoReadyState: this.videoElement?.readyState,
|
|
2098
|
+
videoWidth: this.videoElement?.videoWidth,
|
|
2099
|
+
videoHeight: this.videoElement?.videoHeight
|
|
2100
|
+
}, 'error');
|
|
2101
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
1932
2104
|
if (!result) {
|
|
1933
|
-
this.
|
|
2105
|
+
this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
|
|
2106
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1934
2107
|
return;
|
|
1935
2108
|
}
|
|
1936
2109
|
const faces = result.face || [];
|
|
@@ -1943,18 +2116,24 @@
|
|
|
1943
2116
|
}
|
|
1944
2117
|
}
|
|
1945
2118
|
catch (error) {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2119
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
2120
|
+
this.emitDebug('detection', 'Unexpected error in detection loop', {
|
|
2121
|
+
error: errorMsg,
|
|
2122
|
+
stack: error instanceof Error ? error.stack : 'N/A'
|
|
1949
2123
|
}, 'error');
|
|
1950
|
-
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2124
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1951
2125
|
}
|
|
1952
2126
|
}
|
|
1953
2127
|
getPerformActionCount() {
|
|
1954
2128
|
if (this.config.liveness_action_count <= 0) {
|
|
2129
|
+
this.emitDebug('config', 'liveness_action_count is 0 or negative', { count: this.config.liveness_action_count }, 'warn');
|
|
1955
2130
|
return 0;
|
|
1956
2131
|
}
|
|
1957
|
-
|
|
2132
|
+
const actionListLength = this.config.liveness_action_list?.length ?? 0;
|
|
2133
|
+
if (actionListLength === 0) {
|
|
2134
|
+
this.emitDebug('config', 'liveness_action_list is empty', { actionListLength }, 'warn');
|
|
2135
|
+
}
|
|
2136
|
+
return Math.min(this.config.liveness_action_count, actionListLength);
|
|
1958
2137
|
}
|
|
1959
2138
|
/**
|
|
1960
2139
|
* Handle single face detection
|
|
@@ -2006,6 +2185,7 @@
|
|
|
2006
2185
|
validateFaceBox(faceBox) {
|
|
2007
2186
|
if (!faceBox) {
|
|
2008
2187
|
console.warn('[FaceDetector] Face detected but no box/boxRaw property');
|
|
2188
|
+
this.emitDebug('detection', 'Face box is missing - face detected but no box/boxRaw property', {}, 'warn');
|
|
2009
2189
|
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2010
2190
|
return false;
|
|
2011
2191
|
}
|
|
@@ -2179,20 +2359,25 @@
|
|
|
2179
2359
|
this.detectionState.collectCount++;
|
|
2180
2360
|
return;
|
|
2181
2361
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2362
|
+
try {
|
|
2363
|
+
const frameImageData = this.captureFrame();
|
|
2364
|
+
if (!frameImageData) {
|
|
2365
|
+
this.emitDebug('detection', 'Failed to capture current frame image', { frameQuality, bestQualityScore: this.detectionState.bestQualityScore }, 'warn');
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
const faceImageData = this.captureFrame(faceBox);
|
|
2369
|
+
if (!faceImageData) {
|
|
2370
|
+
this.emitDebug('detection', 'Failed to capture face image', { faceBox }, 'warn');
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
this.detectionState.collectCount++;
|
|
2374
|
+
this.detectionState.bestQualityScore = frameQuality;
|
|
2375
|
+
this.detectionState.bestFrameImage = frameImageData;
|
|
2376
|
+
this.detectionState.bestFaceImage = faceImageData;
|
|
2186
2377
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
this.emitDebug('detection', 'Failed to capture face image', {}, 'warn');
|
|
2190
|
-
return;
|
|
2378
|
+
catch (error) {
|
|
2379
|
+
this.emitDebug('detection', 'Error during image collection', { error: error.message }, 'error');
|
|
2191
2380
|
}
|
|
2192
|
-
this.detectionState.collectCount++;
|
|
2193
|
-
this.detectionState.bestQualityScore = frameQuality;
|
|
2194
|
-
this.detectionState.bestFrameImage = frameImageData;
|
|
2195
|
-
this.detectionState.bestFaceImage = faceImageData;
|
|
2196
2381
|
}
|
|
2197
2382
|
emitLivenessDetected(passed, size, frontal = 0, quality = 0, real = 0, live = 0) {
|
|
2198
2383
|
this.emit('face-detected', { passed, size, frontal, quality, real, live });
|
|
@@ -2203,6 +2388,7 @@
|
|
|
2203
2388
|
selectNextAction() {
|
|
2204
2389
|
const availableActions = (this.config.liveness_action_list ?? []).filter(action => !this.detectionState.completedActions.has(action));
|
|
2205
2390
|
if (availableActions.length === 0) {
|
|
2391
|
+
this.emitDebug('liveness', 'No available actions to perform', { completedActions: Array.from(this.detectionState.completedActions), totalActions: this.config.liveness_action_list?.length ?? 0 }, 'warn');
|
|
2206
2392
|
return;
|
|
2207
2393
|
}
|
|
2208
2394
|
let nextAction = availableActions[0];
|
|
@@ -2248,36 +2434,45 @@
|
|
|
2248
2434
|
* Detect specific action
|
|
2249
2435
|
*/
|
|
2250
2436
|
detectAction(action, gestures) {
|
|
2251
|
-
if (!gestures || gestures.length === 0)
|
|
2437
|
+
if (!gestures || gestures.length === 0) {
|
|
2438
|
+
this.emitDebug('liveness', 'No gestures detected for action verification', { action, gestureCount: gestures?.length ?? 0 }, 'info');
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
try {
|
|
2442
|
+
switch (action) {
|
|
2443
|
+
case exports.LivenessAction.BLINK:
|
|
2444
|
+
return gestures.some(g => {
|
|
2445
|
+
if (!g.gesture)
|
|
2446
|
+
return false;
|
|
2447
|
+
return g.gesture.includes('blink');
|
|
2448
|
+
});
|
|
2449
|
+
case exports.LivenessAction.MOUTH_OPEN:
|
|
2450
|
+
return gestures.some(g => {
|
|
2451
|
+
const gestureStr = g.gesture;
|
|
2452
|
+
if (!gestureStr || !gestureStr.includes('mouth'))
|
|
2453
|
+
return false;
|
|
2454
|
+
const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
|
|
2455
|
+
if (!percentMatch || !percentMatch[1])
|
|
2456
|
+
return false;
|
|
2457
|
+
const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
|
|
2458
|
+
return percent > (this.config.min_mouth_open_percent ?? 0.2);
|
|
2459
|
+
});
|
|
2460
|
+
case exports.LivenessAction.NOD:
|
|
2461
|
+
return gestures.some(g => {
|
|
2462
|
+
if (!g.gesture)
|
|
2463
|
+
return false;
|
|
2464
|
+
// Check for continuous head movement (up -> down or down -> up)
|
|
2465
|
+
const headPattern = g.gesture.match(/head\s+(up|down)/i);
|
|
2466
|
+
return !!headPattern && !!headPattern[1];
|
|
2467
|
+
});
|
|
2468
|
+
default:
|
|
2469
|
+
this.emitDebug('liveness', 'Unknown action type in detection', { action }, 'warn');
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
catch (error) {
|
|
2474
|
+
this.emitDebug('liveness', 'Error during action detection', { action, error: error.message }, 'error');
|
|
2252
2475
|
return false;
|
|
2253
|
-
switch (action) {
|
|
2254
|
-
case exports.LivenessAction.BLINK:
|
|
2255
|
-
return gestures.some(g => {
|
|
2256
|
-
if (!g.gesture)
|
|
2257
|
-
return false;
|
|
2258
|
-
return g.gesture.includes('blink');
|
|
2259
|
-
});
|
|
2260
|
-
case exports.LivenessAction.MOUTH_OPEN:
|
|
2261
|
-
return gestures.some(g => {
|
|
2262
|
-
const gestureStr = g.gesture;
|
|
2263
|
-
if (!gestureStr || !gestureStr.includes('mouth'))
|
|
2264
|
-
return false;
|
|
2265
|
-
const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
|
|
2266
|
-
if (!percentMatch || !percentMatch[1])
|
|
2267
|
-
return false;
|
|
2268
|
-
const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
|
|
2269
|
-
return percent > (this.config.min_mouth_open_percent ?? 0.2);
|
|
2270
|
-
});
|
|
2271
|
-
case exports.LivenessAction.NOD:
|
|
2272
|
-
return gestures.some(g => {
|
|
2273
|
-
if (!g.gesture)
|
|
2274
|
-
return false;
|
|
2275
|
-
// Check for continuous head movement (up -> down or down -> up)
|
|
2276
|
-
const headPattern = g.gesture.match(/head\s+(up|down)/i);
|
|
2277
|
-
return !!headPattern && !!headPattern[1];
|
|
2278
|
-
});
|
|
2279
|
-
default:
|
|
2280
|
-
return false;
|
|
2281
2476
|
}
|
|
2282
2477
|
}
|
|
2283
2478
|
/**
|
|
@@ -2290,6 +2485,29 @@
|
|
|
2290
2485
|
};
|
|
2291
2486
|
this.emit('status-prompt', promptData);
|
|
2292
2487
|
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Detect browser information for debugging
|
|
2490
|
+
*/
|
|
2491
|
+
detectBrowserInfo() {
|
|
2492
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
2493
|
+
if (/quark/i.test(ua))
|
|
2494
|
+
return 'Quark Browser';
|
|
2495
|
+
if (/micromessenger/i.test(ua))
|
|
2496
|
+
return 'WeChat';
|
|
2497
|
+
if (/alipay/i.test(ua))
|
|
2498
|
+
return 'Alipay';
|
|
2499
|
+
if (/qq/i.test(ua))
|
|
2500
|
+
return 'QQ';
|
|
2501
|
+
if (/safari/i.test(ua) && !/chrome/i.test(ua))
|
|
2502
|
+
return 'Safari';
|
|
2503
|
+
if (/chrome/i.test(ua))
|
|
2504
|
+
return 'Chrome';
|
|
2505
|
+
if (/firefox/i.test(ua))
|
|
2506
|
+
return 'Firefox';
|
|
2507
|
+
if (/edge/i.test(ua))
|
|
2508
|
+
return 'Edge';
|
|
2509
|
+
return 'Unknown';
|
|
2510
|
+
}
|
|
2293
2511
|
/**
|
|
2294
2512
|
* Emit debug event
|
|
2295
2513
|
*/
|
|
@@ -2402,26 +2620,35 @@
|
|
|
2402
2620
|
*/
|
|
2403
2621
|
captureFrame(box) {
|
|
2404
2622
|
if (!this.frameCanvasElement) {
|
|
2623
|
+
this.emitDebug('capture', 'Frame canvas element is null, cannot capture frame', {}, 'error');
|
|
2405
2624
|
return null;
|
|
2406
2625
|
}
|
|
2407
2626
|
if (!box) {
|
|
2408
2627
|
return this.canvasToBase64(this.frameCanvasElement);
|
|
2409
2628
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
this.
|
|
2414
|
-
|
|
2629
|
+
try {
|
|
2630
|
+
const x = box[0], y = box[1], width = box[2], height = box[3];
|
|
2631
|
+
// If cached canvas size does not match, recreate it
|
|
2632
|
+
if (!this.faceCanvasElement || this.faceCanvasElement.width !== width || this.faceCanvasElement.height !== height) {
|
|
2633
|
+
this.clearFaceCanvas();
|
|
2634
|
+
this.faceCanvasElement = document.createElement('canvas');
|
|
2635
|
+
this.faceCanvasElement.width = width;
|
|
2636
|
+
this.faceCanvasElement.height = height;
|
|
2637
|
+
this.faceCanvasContext = this.faceCanvasElement.getContext('2d');
|
|
2638
|
+
}
|
|
2639
|
+
if (!this.faceCanvasContext) {
|
|
2640
|
+
this.emitDebug('capture', 'Failed to get face canvas 2D context', { width, height }, 'error');
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2415
2643
|
this.faceCanvasElement.width = width;
|
|
2416
2644
|
this.faceCanvasElement.height = height;
|
|
2417
|
-
this.faceCanvasContext
|
|
2645
|
+
this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
|
|
2646
|
+
return this.canvasToBase64(this.faceCanvasElement);
|
|
2418
2647
|
}
|
|
2419
|
-
|
|
2648
|
+
catch (error) {
|
|
2649
|
+
this.emitDebug('capture', 'Error during face frame capture', { box, error: error.message }, 'error');
|
|
2420
2650
|
return null;
|
|
2421
|
-
|
|
2422
|
-
this.faceCanvasElement.height = height;
|
|
2423
|
-
this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
|
|
2424
|
-
return this.canvasToBase64(this.faceCanvasElement);
|
|
2651
|
+
}
|
|
2425
2652
|
}
|
|
2426
2653
|
}
|
|
2427
2654
|
|