@sssxyd/face-liveness-detector 0.2.30 → 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 +230 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +230 -29
- 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 对象可用
|
|
@@ -534,11 +588,12 @@
|
|
|
534
588
|
* Load Human.js
|
|
535
589
|
* @param modelPath - Path to model files (optional)
|
|
536
590
|
* @param wasmPath - Path to WASM files (optional)
|
|
591
|
+
* @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
|
|
537
592
|
* @returns Promise that resolves with Human instance
|
|
538
593
|
*/
|
|
539
|
-
async function loadHuman(modelPath, wasmPath) {
|
|
594
|
+
async function loadHuman(modelPath, wasmPath, preferredBackend) {
|
|
540
595
|
const config = {
|
|
541
|
-
backend: _detectOptimalBackend(),
|
|
596
|
+
backend: _detectOptimalBackend(preferredBackend),
|
|
542
597
|
face: {
|
|
543
598
|
enabled: true,
|
|
544
599
|
detector: { rotation: false, return: true },
|
|
@@ -562,30 +617,90 @@
|
|
|
562
617
|
console.log('[FaceDetectionEngine] Human.js config:', {
|
|
563
618
|
backend: config.backend,
|
|
564
619
|
modelBasePath: config.modelBasePath || '(using default)',
|
|
565
|
-
wasmPath: config.wasmPath || '(using default)'
|
|
620
|
+
wasmPath: config.wasmPath || '(using default)',
|
|
621
|
+
userAgent: navigator.userAgent,
|
|
622
|
+
platform: navigator.platform
|
|
566
623
|
});
|
|
567
624
|
const initStartTime = performance.now();
|
|
568
625
|
console.log('[FaceDetectionEngine] Creating Human instance...');
|
|
569
|
-
|
|
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
|
+
}
|
|
570
635
|
const instanceCreateTime = performance.now() - initStartTime;
|
|
571
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
|
+
}
|
|
572
641
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
573
642
|
const modelLoadStartTime = performance.now();
|
|
574
643
|
try {
|
|
575
|
-
|
|
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
|
+
]);
|
|
576
655
|
const loadTime = performance.now() - modelLoadStartTime;
|
|
577
656
|
const totalTime = performance.now() - initStartTime;
|
|
578
657
|
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
579
658
|
modelLoadTime: `${loadTime.toFixed(2)}ms`,
|
|
580
659
|
totalInitTime: `${totalTime.toFixed(2)}ms`,
|
|
581
|
-
version: human.version
|
|
660
|
+
version: human.version,
|
|
661
|
+
config: human.config
|
|
582
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
|
+
}
|
|
583
690
|
return human;
|
|
584
691
|
}
|
|
585
692
|
catch (error) {
|
|
586
693
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
587
|
-
|
|
588
|
-
|
|
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}`);
|
|
589
704
|
}
|
|
590
705
|
}
|
|
591
706
|
/**
|
|
@@ -1621,7 +1736,29 @@
|
|
|
1621
1736
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
1622
1737
|
this.emitDebug('initialization', 'Loading Human.js...');
|
|
1623
1738
|
const humanStartTime = performance.now();
|
|
1624
|
-
|
|
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
|
+
}
|
|
1625
1762
|
const humanLoadTime = performance.now() - humanStartTime;
|
|
1626
1763
|
if (!this.human) {
|
|
1627
1764
|
const errorMsg = 'Failed to load Human.js: instance is null';
|
|
@@ -1637,13 +1774,35 @@
|
|
|
1637
1774
|
});
|
|
1638
1775
|
return;
|
|
1639
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
|
+
}
|
|
1640
1796
|
this.emitDebug('initialization', 'Human.js loaded successfully', {
|
|
1641
1797
|
loadTime: `${humanLoadTime.toFixed(2)}ms`,
|
|
1642
|
-
version: this.human.version
|
|
1798
|
+
version: this.human.version,
|
|
1799
|
+
backend: this.human.config?.backend || 'unknown',
|
|
1800
|
+
config: this.human.config
|
|
1643
1801
|
});
|
|
1644
1802
|
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
1645
1803
|
loadTime: `${humanLoadTime.toFixed(2)}ms`,
|
|
1646
|
-
version: this.human.version
|
|
1804
|
+
version: this.human.version,
|
|
1805
|
+
backend: this.human.config?.backend || 'unknown'
|
|
1647
1806
|
});
|
|
1648
1807
|
this.isReady = true;
|
|
1649
1808
|
const loadedData = {
|
|
@@ -1920,13 +2079,31 @@
|
|
|
1920
2079
|
try {
|
|
1921
2080
|
// Check video is ready
|
|
1922
2081
|
if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1923
|
-
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2082
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1924
2083
|
return;
|
|
1925
2084
|
}
|
|
1926
2085
|
// Perform face detection
|
|
1927
|
-
|
|
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
|
+
}
|
|
1928
2104
|
if (!result) {
|
|
1929
|
-
this.
|
|
2105
|
+
this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
|
|
2106
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1930
2107
|
return;
|
|
1931
2108
|
}
|
|
1932
2109
|
const faces = result.face || [];
|
|
@@ -1939,11 +2116,12 @@
|
|
|
1939
2116
|
}
|
|
1940
2117
|
}
|
|
1941
2118
|
catch (error) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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'
|
|
1945
2123
|
}, 'error');
|
|
1946
|
-
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2124
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1947
2125
|
}
|
|
1948
2126
|
}
|
|
1949
2127
|
getPerformActionCount() {
|
|
@@ -2307,6 +2485,29 @@
|
|
|
2307
2485
|
};
|
|
2308
2486
|
this.emit('status-prompt', promptData);
|
|
2309
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
|
+
}
|
|
2310
2511
|
/**
|
|
2311
2512
|
* Emit debug event
|
|
2312
2513
|
*/
|