@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.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 对象可用
|
|
@@ -512,11 +566,12 @@ function getCvSync() {
|
|
|
512
566
|
* Load Human.js
|
|
513
567
|
* @param modelPath - Path to model files (optional)
|
|
514
568
|
* @param wasmPath - Path to WASM files (optional)
|
|
569
|
+
* @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
|
|
515
570
|
* @returns Promise that resolves with Human instance
|
|
516
571
|
*/
|
|
517
|
-
async function loadHuman(modelPath, wasmPath) {
|
|
572
|
+
async function loadHuman(modelPath, wasmPath, preferredBackend) {
|
|
518
573
|
const config = {
|
|
519
|
-
backend: _detectOptimalBackend(),
|
|
574
|
+
backend: _detectOptimalBackend(preferredBackend),
|
|
520
575
|
face: {
|
|
521
576
|
enabled: true,
|
|
522
577
|
detector: { rotation: false, return: true },
|
|
@@ -540,30 +595,90 @@ async function loadHuman(modelPath, wasmPath) {
|
|
|
540
595
|
console.log('[FaceDetectionEngine] Human.js config:', {
|
|
541
596
|
backend: config.backend,
|
|
542
597
|
modelBasePath: config.modelBasePath || '(using default)',
|
|
543
|
-
wasmPath: config.wasmPath || '(using default)'
|
|
598
|
+
wasmPath: config.wasmPath || '(using default)',
|
|
599
|
+
userAgent: navigator.userAgent,
|
|
600
|
+
platform: navigator.platform
|
|
544
601
|
});
|
|
545
602
|
const initStartTime = performance.now();
|
|
546
603
|
console.log('[FaceDetectionEngine] Creating Human instance...');
|
|
547
|
-
|
|
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
|
+
}
|
|
548
613
|
const instanceCreateTime = performance.now() - initStartTime;
|
|
549
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
|
+
}
|
|
550
619
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
551
620
|
const modelLoadStartTime = performance.now();
|
|
552
621
|
try {
|
|
553
|
-
|
|
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
|
+
]);
|
|
554
633
|
const loadTime = performance.now() - modelLoadStartTime;
|
|
555
634
|
const totalTime = performance.now() - initStartTime;
|
|
556
635
|
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
557
636
|
modelLoadTime: `${loadTime.toFixed(2)}ms`,
|
|
558
637
|
totalInitTime: `${totalTime.toFixed(2)}ms`,
|
|
559
|
-
version: human.version
|
|
638
|
+
version: human.version,
|
|
639
|
+
config: human.config
|
|
560
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
|
+
}
|
|
561
668
|
return human;
|
|
562
669
|
}
|
|
563
670
|
catch (error) {
|
|
564
671
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
565
|
-
|
|
566
|
-
|
|
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}`);
|
|
567
682
|
}
|
|
568
683
|
}
|
|
569
684
|
/**
|
|
@@ -1599,7 +1714,29 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
1599
1714
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
1600
1715
|
this.emitDebug('initialization', 'Loading Human.js...');
|
|
1601
1716
|
const humanStartTime = performance.now();
|
|
1602
|
-
|
|
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
|
+
}
|
|
1603
1740
|
const humanLoadTime = performance.now() - humanStartTime;
|
|
1604
1741
|
if (!this.human) {
|
|
1605
1742
|
const errorMsg = 'Failed to load Human.js: instance is null';
|
|
@@ -1615,13 +1752,35 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
1615
1752
|
});
|
|
1616
1753
|
return;
|
|
1617
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
|
+
}
|
|
1618
1774
|
this.emitDebug('initialization', 'Human.js loaded successfully', {
|
|
1619
1775
|
loadTime: `${humanLoadTime.toFixed(2)}ms`,
|
|
1620
|
-
version: this.human.version
|
|
1776
|
+
version: this.human.version,
|
|
1777
|
+
backend: this.human.config?.backend || 'unknown',
|
|
1778
|
+
config: this.human.config
|
|
1621
1779
|
});
|
|
1622
1780
|
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
1623
1781
|
loadTime: `${humanLoadTime.toFixed(2)}ms`,
|
|
1624
|
-
version: this.human.version
|
|
1782
|
+
version: this.human.version,
|
|
1783
|
+
backend: this.human.config?.backend || 'unknown'
|
|
1625
1784
|
});
|
|
1626
1785
|
this.isReady = true;
|
|
1627
1786
|
const loadedData = {
|
|
@@ -1898,13 +2057,31 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
1898
2057
|
try {
|
|
1899
2058
|
// Check video is ready
|
|
1900
2059
|
if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1901
|
-
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2060
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1902
2061
|
return;
|
|
1903
2062
|
}
|
|
1904
2063
|
// Perform face detection
|
|
1905
|
-
|
|
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
|
+
}
|
|
1906
2082
|
if (!result) {
|
|
1907
|
-
this.
|
|
2083
|
+
this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
|
|
2084
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1908
2085
|
return;
|
|
1909
2086
|
}
|
|
1910
2087
|
const faces = result.face || [];
|
|
@@ -1917,11 +2094,12 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
1917
2094
|
}
|
|
1918
2095
|
}
|
|
1919
2096
|
catch (error) {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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'
|
|
1923
2101
|
}, 'error');
|
|
1924
|
-
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
2102
|
+
this.scheduleNextDetection(this.config.error_retry_delay);
|
|
1925
2103
|
}
|
|
1926
2104
|
}
|
|
1927
2105
|
getPerformActionCount() {
|
|
@@ -2285,6 +2463,29 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
2285
2463
|
};
|
|
2286
2464
|
this.emit('status-prompt', promptData);
|
|
2287
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
|
+
}
|
|
2288
2489
|
/**
|
|
2289
2490
|
* Emit debug event
|
|
2290
2491
|
*/
|