@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 +0 -47
- package/dist/index.esm.js +219 -93
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +219 -93
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/library-loader.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -332,21 +332,26 @@
|
|
|
332
332
|
}
|
|
333
333
|
function _detectBrowserEngine(userAgent) {
|
|
334
334
|
const ua = userAgent.toLowerCase();
|
|
335
|
-
// 检测 Gecko (Firefox)
|
|
335
|
+
// 1. 检测 Gecko (Firefox)
|
|
336
336
|
if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
|
|
337
337
|
return 'gecko';
|
|
338
338
|
}
|
|
339
|
-
// 检测 WebKit
|
|
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
|
|
339
|
+
// 2. 检测 Chromium/Blink(必须在 WebKit 之前,因为 Chrome 的 user-agent 也包含 WebKit)
|
|
345
340
|
// Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
|
|
346
341
|
if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
|
|
347
342
|
return 'chromium';
|
|
348
343
|
}
|
|
349
|
-
//
|
|
344
|
+
// 3. 检测 WebKit(真正的 Safari 和 iOS 浏览器)
|
|
345
|
+
// 注意:真正的 WebKit 浏览器(Safari)user-agent 不包含 Chrome 标识
|
|
346
|
+
// 包括:Safari、iOS 浏览器、以及那些虽然包含 Chrome 标识但实际是 WebKit 的浏览器(Quark、支付宝、微信等)
|
|
347
|
+
if (/webkit/i.test(ua)) {
|
|
348
|
+
// WebKit 特征明显,包括以下几种情况:
|
|
349
|
+
// - 真正的 Safari(有 Safari 标识)
|
|
350
|
+
// - iOS 浏览器(有 Mobile Safari 标识)
|
|
351
|
+
// - Quark、支付宝、微信等虽然包含 Chrome 标识但是基于 WebKit 的浏览器
|
|
352
|
+
return 'webkit';
|
|
353
|
+
}
|
|
354
|
+
// 4. 其他浏览器 - 保守方案,使用 WASM
|
|
350
355
|
return 'other';
|
|
351
356
|
}
|
|
352
357
|
function _getOptimalBackendForEngine(engine) {
|
|
@@ -585,15 +590,11 @@
|
|
|
585
590
|
return null;
|
|
586
591
|
}
|
|
587
592
|
/**
|
|
588
|
-
*
|
|
589
|
-
* @param modelPath - Path to model files (optional)
|
|
590
|
-
* @param wasmPath - Path to WASM files (optional)
|
|
591
|
-
* @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
|
|
592
|
-
* @returns Promise that resolves with Human instance
|
|
593
|
+
* Create Human.js configuration object
|
|
593
594
|
*/
|
|
594
|
-
|
|
595
|
+
function _createHumanConfig(backend, modelPath, wasmPath) {
|
|
595
596
|
const config = {
|
|
596
|
-
backend
|
|
597
|
+
backend,
|
|
597
598
|
face: {
|
|
598
599
|
enabled: true,
|
|
599
600
|
detector: { rotation: false, return: true },
|
|
@@ -607,102 +608,196 @@
|
|
|
607
608
|
object: { enabled: false },
|
|
608
609
|
gesture: { enabled: true }
|
|
609
610
|
};
|
|
610
|
-
// 只在提供了路径时才设置,否则让 Human.js 使用默认加载策略
|
|
611
611
|
if (modelPath) {
|
|
612
612
|
config.modelBasePath = modelPath;
|
|
613
613
|
}
|
|
614
614
|
if (wasmPath) {
|
|
615
615
|
config.wasmPath = wasmPath;
|
|
616
616
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
617
|
+
return config;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Load and verify Human.js models
|
|
621
|
+
*/
|
|
622
|
+
async function _loadAndVerifyHuman(human) {
|
|
623
|
+
const modelLoadStartTime = performance.now();
|
|
624
|
+
try {
|
|
625
|
+
await human.load();
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
629
|
+
const errorStack = error instanceof Error ? error.stack : 'N/A';
|
|
630
|
+
console.error('[FaceDetectionEngine] Error during human.load():', {
|
|
631
|
+
errorMsg,
|
|
632
|
+
stack: errorStack,
|
|
633
|
+
backend: human.config?.backend,
|
|
634
|
+
hasModels: !!human.models,
|
|
635
|
+
modelsKeys: human.models ? Object.keys(human.models).length : 0
|
|
636
|
+
});
|
|
637
|
+
throw new Error(`Model loading error: ${errorMsg}`);
|
|
638
|
+
}
|
|
639
|
+
const loadTime = performance.now() - modelLoadStartTime;
|
|
640
|
+
console.log('[FaceDetectionEngine] Human.js loaded successfully', {
|
|
641
|
+
modelLoadTime: `${loadTime.toFixed(2)}ms`,
|
|
642
|
+
version: human.version,
|
|
643
|
+
config: human.config
|
|
623
644
|
});
|
|
645
|
+
// 验证加载后的 Human 实例有必要的方法和属性
|
|
646
|
+
if (typeof human.detect !== 'function') {
|
|
647
|
+
throw new Error('Human.detect method not available after loading');
|
|
648
|
+
}
|
|
649
|
+
if (!human.version) {
|
|
650
|
+
console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
|
|
651
|
+
}
|
|
652
|
+
// 关键验证:检查模型是否真的加载了
|
|
653
|
+
if (!human.models || Object.keys(human.models).length === 0) {
|
|
654
|
+
console.error('[FaceDetectionEngine] CRITICAL: human.models is empty after loading!');
|
|
655
|
+
throw new Error('No models were loaded - human.models is empty');
|
|
656
|
+
}
|
|
657
|
+
// 详细检查每个关键模型及其结构
|
|
658
|
+
const criticalModels = ['face', 'antispoof', 'liveness'];
|
|
659
|
+
const missingModels = [];
|
|
660
|
+
for (const modelName of criticalModels) {
|
|
661
|
+
const model = human.models[modelName];
|
|
662
|
+
if (!model) {
|
|
663
|
+
missingModels.push(modelName);
|
|
664
|
+
console.error(`[FaceDetectionEngine] CRITICAL: Model '${modelName}' is missing!`);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
const isLoaded = model.loaded || model.state === 'loaded' || !!model.model;
|
|
668
|
+
// 检查模型是否有必要的内部结构(防止 "Cannot read properties of undefined (reading 'inputs')" 错误)
|
|
669
|
+
const hasExecutor = !!model['executor'];
|
|
670
|
+
const hasInputs = !!model.inputs && Array.isArray(model.inputs) && model.inputs.length > 0;
|
|
671
|
+
const hasModelUrl = !!model['modelUrl'];
|
|
672
|
+
console.log(`[FaceDetectionEngine] Model '${modelName}':`, {
|
|
673
|
+
loaded: isLoaded,
|
|
674
|
+
state: model.state,
|
|
675
|
+
hasModel: !!model.model,
|
|
676
|
+
hasExecutor,
|
|
677
|
+
hasInputs,
|
|
678
|
+
hasModelUrl,
|
|
679
|
+
inputsType: typeof model.inputs,
|
|
680
|
+
inputsLength: Array.isArray(model.inputs) ? model.inputs.length : 'N/A'
|
|
681
|
+
});
|
|
682
|
+
// 严格检查:模型必须有以下结构才能正常工作
|
|
683
|
+
if (!isLoaded || !hasExecutor || !hasModelUrl) {
|
|
684
|
+
missingModels.push(`${modelName} (incomplete)`);
|
|
685
|
+
console.error(`[FaceDetectionEngine] WARNING: Model '${modelName}' may not be fully loaded - missing structure`);
|
|
686
|
+
}
|
|
687
|
+
// 如果 inputs 未定义会导致 "Cannot read properties of undefined (reading 'inputs')" 错误
|
|
688
|
+
if (!hasInputs && modelName !== 'antispoof') {
|
|
689
|
+
console.warn(`[FaceDetectionEngine] WARNING: Model '${modelName}' has no inputs - may cause errors during detection`);
|
|
690
|
+
missingModels.push(`${modelName} (no inputs)`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (missingModels.length > 0) {
|
|
695
|
+
console.error('[FaceDetectionEngine] Some critical models failed to load:', missingModels);
|
|
696
|
+
throw new Error(`Critical models not loaded: ${missingModels.join(', ')}`);
|
|
697
|
+
}
|
|
698
|
+
// 打印加载的模型信息
|
|
699
|
+
if (human.models) {
|
|
700
|
+
const loadedModels = Object.entries(human.models).map(([name, model]) => ({
|
|
701
|
+
name,
|
|
702
|
+
loaded: model?.loaded || model?.state === 'loaded',
|
|
703
|
+
type: typeof model,
|
|
704
|
+
hasModel: !!model?.model
|
|
705
|
+
}));
|
|
706
|
+
console.log('[FaceDetectionEngine] All loaded models:', {
|
|
707
|
+
backend: human.config?.backend,
|
|
708
|
+
modelBasePath: human.config?.modelBasePath,
|
|
709
|
+
wasmPath: human.config?.wasmPath,
|
|
710
|
+
totalModels: Object.keys(human.models).length,
|
|
711
|
+
models: loadedModels,
|
|
712
|
+
allModelNames: Object.keys(human.models)
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Try to load Human with a specific backend
|
|
718
|
+
* @param config The configuration object
|
|
719
|
+
* @param backend The backend to try
|
|
720
|
+
* @returns Human instance or null if fails
|
|
721
|
+
*/
|
|
722
|
+
async function _tryLoadHumanWithBackend(backend, modelPath, wasmPath) {
|
|
723
|
+
const config = _createHumanConfig(backend, modelPath, wasmPath);
|
|
624
724
|
const initStartTime = performance.now();
|
|
625
|
-
console.log('[FaceDetectionEngine] Creating Human instance...');
|
|
626
725
|
let human;
|
|
627
726
|
try {
|
|
628
727
|
human = new Human(config);
|
|
629
728
|
}
|
|
630
729
|
catch (error) {
|
|
631
730
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error during Human instantiation';
|
|
632
|
-
|
|
633
|
-
|
|
731
|
+
const stack = error instanceof Error ? error.stack : 'N/A';
|
|
732
|
+
console.error(`[FaceDetectionEngine] Failed to create Human instance (${backend}):`, {
|
|
733
|
+
errorMsg,
|
|
734
|
+
stack,
|
|
735
|
+
backend: config.backend,
|
|
736
|
+
userAgent: navigator.userAgent
|
|
737
|
+
});
|
|
738
|
+
return null;
|
|
634
739
|
}
|
|
635
|
-
const instanceCreateTime = performance.now() - initStartTime;
|
|
636
|
-
console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
|
|
637
740
|
// 验证 Human 实例
|
|
638
741
|
if (!human) {
|
|
639
|
-
|
|
742
|
+
console.error(`[FaceDetectionEngine] Human instance is null (${backend})`);
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
// 验证 Human 实例结构(早期检测 WASM 问题)
|
|
746
|
+
if (!human.config) {
|
|
747
|
+
console.warn('[FaceDetectionEngine] Warning: human.config is missing');
|
|
640
748
|
}
|
|
641
|
-
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
642
|
-
const modelLoadStartTime = performance.now();
|
|
643
749
|
try {
|
|
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
|
-
]);
|
|
655
|
-
const loadTime = performance.now() - modelLoadStartTime;
|
|
750
|
+
await _loadAndVerifyHuman(human);
|
|
656
751
|
const totalTime = performance.now() - initStartTime;
|
|
657
|
-
console.log(
|
|
658
|
-
modelLoadTime: `${loadTime.toFixed(2)}ms`,
|
|
659
|
-
totalInitTime: `${totalTime.toFixed(2)}ms`,
|
|
660
|
-
version: human.version,
|
|
661
|
-
config: human.config
|
|
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
|
-
}
|
|
752
|
+
console.log(`[FaceDetectionEngine] Successfully loaded Human.js with ${backend} backend in ${totalTime.toFixed(2)}ms`);
|
|
690
753
|
return human;
|
|
691
754
|
}
|
|
692
755
|
catch (error) {
|
|
693
756
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
694
|
-
|
|
695
|
-
|
|
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}`);
|
|
757
|
+
console.error(`[FaceDetectionEngine] Failed to load models with ${backend} backend:`, errorMsg);
|
|
758
|
+
return null;
|
|
704
759
|
}
|
|
705
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Load Human.js
|
|
763
|
+
* @param modelPath - Path to model files (optional)
|
|
764
|
+
* @param wasmPath - Path to WASM files (optional)
|
|
765
|
+
* @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
|
|
766
|
+
* @returns Promise that resolves with Human instance
|
|
767
|
+
*/
|
|
768
|
+
async function loadHuman(modelPath, wasmPath, preferredBackend) {
|
|
769
|
+
const selectedBackend = _detectOptimalBackend(preferredBackend);
|
|
770
|
+
console.log('[FaceDetectionEngine] Starting Human.js initialization:', {
|
|
771
|
+
selectedBackend,
|
|
772
|
+
modelBasePath: modelPath || '(using default)',
|
|
773
|
+
wasmPath: wasmPath || '(using default)',
|
|
774
|
+
userAgent: navigator.userAgent,
|
|
775
|
+
platform: navigator.platform
|
|
776
|
+
});
|
|
777
|
+
// 尝试用主后端加载
|
|
778
|
+
const human = await _tryLoadHumanWithBackend(selectedBackend, modelPath, wasmPath);
|
|
779
|
+
if (human) {
|
|
780
|
+
return human;
|
|
781
|
+
}
|
|
782
|
+
console.log(`[FaceDetectionEngine] Human.js loading failed with ${selectedBackend} backend.`);
|
|
783
|
+
// 尝试用备选后端加载(最多一次降级)
|
|
784
|
+
let fallbackBackend;
|
|
785
|
+
if (selectedBackend === 'wasm' && _isWebGLAvailable()) {
|
|
786
|
+
fallbackBackend = 'webgl';
|
|
787
|
+
}
|
|
788
|
+
else if (selectedBackend === 'webgl') {
|
|
789
|
+
fallbackBackend = 'wasm';
|
|
790
|
+
}
|
|
791
|
+
if (fallbackBackend) {
|
|
792
|
+
console.warn(`[FaceDetectionEngine] Primary backend (${selectedBackend}) failed, attempting fallback to ${fallbackBackend}...`);
|
|
793
|
+
const humanFallback = await _tryLoadHumanWithBackend(fallbackBackend, modelPath, wasmPath);
|
|
794
|
+
if (humanFallback) {
|
|
795
|
+
return humanFallback;
|
|
796
|
+
}
|
|
797
|
+
throw new Error(`Human.js loading failed: both ${selectedBackend} and ${fallbackBackend} backends failed`);
|
|
798
|
+
}
|
|
799
|
+
throw new Error(`Human.js loading failed: ${selectedBackend} backend failed (no fallback available)`);
|
|
800
|
+
}
|
|
706
801
|
/**
|
|
707
802
|
* Extract OpenCV version from getBuildInformation
|
|
708
803
|
* @returns version string like "4.12.0"
|
|
@@ -1741,28 +1836,59 @@
|
|
|
1741
1836
|
}
|
|
1742
1837
|
catch (humanError) {
|
|
1743
1838
|
const errorMsg = humanError instanceof Error ? humanError.message : 'Unknown error';
|
|
1744
|
-
|
|
1745
|
-
|
|
1839
|
+
const stack = humanError instanceof Error ? humanError.stack : 'N/A';
|
|
1840
|
+
// 分析错误类型,提供针对性的建议
|
|
1841
|
+
let errorContext = {
|
|
1746
1842
|
error: errorMsg,
|
|
1747
|
-
stack
|
|
1843
|
+
stack,
|
|
1748
1844
|
userAgent: navigator.userAgent,
|
|
1749
1845
|
platform: navigator.platform,
|
|
1750
|
-
browser: this.detectBrowserInfo()
|
|
1751
|
-
|
|
1846
|
+
browser: this.detectBrowserInfo(),
|
|
1847
|
+
backend: this.config.tensorflow_backend,
|
|
1848
|
+
source: 'human.js'
|
|
1849
|
+
};
|
|
1850
|
+
// 特定错误类型的诊断
|
|
1851
|
+
if (errorMsg.includes('inputs')) {
|
|
1852
|
+
errorContext.diagnosis = 'Human.js internal error: Model structure incomplete';
|
|
1853
|
+
errorContext.rootCause = 'Human.js library issue - models not fully loaded or WASM backend initialization incomplete';
|
|
1854
|
+
errorContext.suggestion = 'This is a Human.js library issue. Models may not have proper executor or inputs structure. Check WASM initialization and model integrity.';
|
|
1855
|
+
}
|
|
1856
|
+
else if (errorMsg.includes('timeout')) {
|
|
1857
|
+
errorContext.diagnosis = 'Model loading timeout';
|
|
1858
|
+
errorContext.suggestion = 'Network issue or model file too large - check network conditions';
|
|
1859
|
+
}
|
|
1860
|
+
else if (errorMsg.includes('Critical models not loaded')) {
|
|
1861
|
+
errorContext.diagnosis = 'Human.js failed to load required models';
|
|
1862
|
+
errorContext.rootCause = 'Models (face, antispoof, liveness) are missing or incomplete';
|
|
1863
|
+
errorContext.suggestion = 'Check model files and ensure WASM backend is properly initialized';
|
|
1864
|
+
}
|
|
1865
|
+
else if (errorMsg.includes('empty')) {
|
|
1866
|
+
errorContext.diagnosis = 'Models object is empty after loading';
|
|
1867
|
+
errorContext.suggestion = 'Model path may be incorrect or HTTP response failed';
|
|
1868
|
+
}
|
|
1869
|
+
else if (errorMsg.includes('incomplete')) {
|
|
1870
|
+
errorContext.diagnosis = 'Models loaded but structure is incomplete';
|
|
1871
|
+
errorContext.rootCause = 'Human.js internal issue - missing executor, inputs, or modelUrl';
|
|
1872
|
+
errorContext.suggestion = 'Ensure all model resources are fully loaded and accessible';
|
|
1873
|
+
}
|
|
1874
|
+
console.error('[FaceDetectionEngine] Human.js loading failed with detailed error:', errorContext);
|
|
1875
|
+
this.emitDebug('initialization', 'Human.js loading failed with exception', errorContext, 'error');
|
|
1752
1876
|
this.emit('detector-loaded', {
|
|
1753
1877
|
success: false,
|
|
1754
|
-
error: `Failed to load Human.js: ${errorMsg}
|
|
1878
|
+
error: `Failed to load Human.js: ${errorMsg}`,
|
|
1879
|
+
details: errorContext
|
|
1755
1880
|
});
|
|
1756
1881
|
this.emit('detector-error', {
|
|
1757
1882
|
code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
|
|
1758
|
-
message: `Human.js loading error: ${errorMsg}
|
|
1883
|
+
message: `Human.js loading error: ${errorMsg}`,
|
|
1884
|
+
details: errorContext
|
|
1759
1885
|
});
|
|
1760
1886
|
return;
|
|
1761
1887
|
}
|
|
1762
1888
|
const humanLoadTime = performance.now() - humanStartTime;
|
|
1763
1889
|
if (!this.human) {
|
|
1764
1890
|
const errorMsg = 'Failed to load Human.js: instance is null';
|
|
1765
|
-
console.
|
|
1891
|
+
console.error('[FaceDetectionEngine] ' + errorMsg);
|
|
1766
1892
|
this.emitDebug('initialization', errorMsg, { loadTime: humanLoadTime }, 'error');
|
|
1767
1893
|
this.emit('detector-loaded', {
|
|
1768
1894
|
success: false,
|