id-scanner-lib 1.6.7 → 2.0.0
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/id-scanner-lib.esm.js +994 -1139
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +995 -1144
- package/dist/id-scanner-lib.js.map +1 -1
- package/package.json +1 -1
- package/src/compat/index.ts +7 -0
- package/src/compat/v1-adapter.ts +84 -0
- package/src/core/camera-manager.ts +43 -76
- package/src/core/camera-stream-manager.ts +318 -0
- package/src/core/config.ts +113 -267
- package/src/core/errors.ts +68 -117
- package/src/core/logger.ts +158 -81
- package/src/core/resource-manager.ts +150 -0
- package/src/core/scanner.ts +109 -0
- package/src/core/utils/browser.ts +7 -0
- package/src/core/utils/canvas-pool.ts +171 -0
- package/src/core/utils/canvas.ts +7 -0
- package/src/core/utils/image.ts +7 -0
- package/src/core/utils/index.ts +9 -0
- package/src/core/utils/resource-manager.ts +155 -0
- package/src/core/utils/validate.ts +7 -0
- package/src/core/utils/worker.ts +130 -0
- package/src/modules/face/comparator/comparator.ts +45 -0
- package/src/modules/face/comparator/index.ts +1 -0
- package/src/modules/face/detector/detector.ts +83 -0
- package/src/modules/face/detector/index.ts +2 -0
- package/src/modules/face/detector/types.ts +80 -0
- package/src/modules/face/face-comparator.ts +150 -0
- package/src/modules/face/face-detector-options.ts +104 -0
- package/src/modules/face/face-detector.ts +121 -376
- package/src/modules/face/face-detector.ts.bak +991 -0
- package/src/modules/face/face-model-loader.ts +222 -0
- package/src/modules/face/face-result-converter.ts +225 -0
- package/src/modules/face/face-tracker.ts +207 -0
- package/src/modules/face/liveness/index.ts +7 -0
- package/src/modules/face/liveness-detector.ts +2 -2
- package/src/modules/face/tracker/index.ts +7 -0
- package/src/modules/id-card/anti-fake/index.ts +7 -0
- package/src/modules/id-card/detector/index.ts +7 -0
- package/src/modules/id-card/id-card-text-parser.ts +151 -0
- package/src/modules/id-card/ocr-processor.ts +20 -257
- package/src/modules/id-card/ocr-worker.ts +2 -183
- package/src/modules/id-card/parser/index.ts +7 -0
- package/src/modules/qr/scanner/index.ts +7 -0
- package/src/utils/canvas-pool.ts +273 -0
- package/src/utils/edge-detector.ts +232 -0
- package/src/utils/image-processing.ts +92 -419
- package/src/utils/index.ts +1 -0
package/src/core/logger.ts
CHANGED
|
@@ -62,19 +62,22 @@ export class ConsoleLogHandler implements LogHandler {
|
|
|
62
62
|
handle(entry: LogEntry): void {
|
|
63
63
|
const timestamp = new Date(entry.timestamp).toISOString();
|
|
64
64
|
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
// 优先使用 error,其次使用 data
|
|
67
|
+
const extra = entry.error || entry.data || '';
|
|
68
|
+
|
|
66
69
|
switch (entry.level) {
|
|
67
70
|
case LoggerLevel.DEBUG:
|
|
68
|
-
console.debug(prefix, entry.message,
|
|
71
|
+
console.debug(prefix, entry.message, extra);
|
|
69
72
|
break;
|
|
70
73
|
case LoggerLevel.INFO:
|
|
71
|
-
console.info(prefix, entry.message,
|
|
74
|
+
console.info(prefix, entry.message, extra);
|
|
72
75
|
break;
|
|
73
76
|
case LoggerLevel.WARN:
|
|
74
|
-
console.warn(prefix, entry.message,
|
|
77
|
+
console.warn(prefix, entry.message, extra);
|
|
75
78
|
break;
|
|
76
79
|
case LoggerLevel.ERROR:
|
|
77
|
-
console.error(prefix, entry.message,
|
|
80
|
+
console.error(prefix, entry.message, extra);
|
|
78
81
|
break;
|
|
79
82
|
default:
|
|
80
83
|
// 输出什么也不做
|
|
@@ -158,10 +161,14 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
158
161
|
/** 发送间隔(毫秒) */
|
|
159
162
|
private flushInterval: number;
|
|
160
163
|
/** 定时发送的计时器ID */
|
|
161
|
-
private timerId:
|
|
164
|
+
private timerId: ReturnType<typeof setInterval> | null = null;
|
|
162
165
|
/** 是否在浏览器环境 */
|
|
163
166
|
private readonly isBrowser: boolean;
|
|
164
|
-
|
|
167
|
+
/** 最大连续失败次数,超过则丢弃日志 */
|
|
168
|
+
private readonly maxConsecutiveFailures: number;
|
|
169
|
+
/** 当前连续失败计数 */
|
|
170
|
+
private consecutiveFailures: number = 0;
|
|
171
|
+
|
|
165
172
|
/**
|
|
166
173
|
* 构造函数
|
|
167
174
|
* @param endpoint 远程服务器URL
|
|
@@ -173,10 +180,11 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
173
180
|
this.maxQueueSize = maxQueueSize;
|
|
174
181
|
this.flushInterval = flushInterval;
|
|
175
182
|
this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
|
|
176
|
-
|
|
183
|
+
this.maxConsecutiveFailures = 10;
|
|
184
|
+
|
|
177
185
|
// 设置定时发送
|
|
178
186
|
this.startTimer();
|
|
179
|
-
|
|
187
|
+
|
|
180
188
|
// 页面卸载前尝试发送剩余日志
|
|
181
189
|
if (this.isBrowser) {
|
|
182
190
|
window.addEventListener('beforeunload', () => {
|
|
@@ -184,7 +192,7 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
184
192
|
});
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
/**
|
|
189
197
|
* 处理日志条目
|
|
190
198
|
* @param entry 日志条目
|
|
@@ -193,86 +201,96 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
193
201
|
// 只处理INFO以上级别的日志
|
|
194
202
|
if (entry.level >= LoggerLevel.INFO) {
|
|
195
203
|
this.queue.push(entry);
|
|
196
|
-
|
|
204
|
+
|
|
197
205
|
// 如果队列满了,立即发送
|
|
198
206
|
if (this.queue.length >= this.maxQueueSize) {
|
|
199
207
|
this.flush();
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
}
|
|
203
|
-
|
|
211
|
+
|
|
204
212
|
/**
|
|
205
213
|
* 发送队列中的日志
|
|
206
214
|
*/
|
|
207
215
|
flush(): void {
|
|
208
216
|
if (this.queue.length === 0) return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
// 防止在 fetch 失败时无限重试
|
|
214
|
-
const sendCount = (this as any)._sendCount || 0;
|
|
215
|
-
(this as any)._sendCount = sendCount + 1;
|
|
216
|
-
|
|
217
|
-
// 如果发送次数过多,停止发送以防止无限循环
|
|
218
|
-
if (sendCount > 10) {
|
|
219
|
-
console.warn('RemoteLogHandler: Too many failed sends, stopping. Clear queue.');
|
|
217
|
+
|
|
218
|
+
// 如果连续失败次数过多,停止发送以防止无限循环
|
|
219
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
220
|
+
console.warn('RemoteLogHandler: Too many consecutive failures, stopping. Clear queue.');
|
|
220
221
|
this.queue = [];
|
|
221
|
-
|
|
222
|
+
this.consecutiveFailures = 0;
|
|
222
223
|
return;
|
|
223
224
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'Content-Type': 'application/json'
|
|
230
|
-
},
|
|
231
|
-
body: JSON.stringify(entriesToSend),
|
|
232
|
-
keepalive: true
|
|
233
|
-
}).catch((err: Error) => {
|
|
234
|
-
console.error('Failed to send logs to remote server:', err);
|
|
235
|
-
|
|
236
|
-
// 防止无限重试 - 如果失败次数过多,丢弃日志
|
|
237
|
-
if ((this as any)._sendCount > 10) {
|
|
238
|
-
console.warn('RemoteLogHandler: Max retry exceeded, discarding logs');
|
|
239
|
-
this.queue = []; // 清空队列,避免内存泄漏
|
|
240
|
-
(this as any)._sendCount = 0;
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 失败时把日志放回队列,但防止无限增长
|
|
245
|
-
if (this.queue.length < this.maxQueueSize) {
|
|
246
|
-
const maxReturn = Math.min(entriesToSend.length, this.maxQueueSize - this.queue.length);
|
|
247
|
-
const returnedEntries = entriesToSend.slice(0, maxReturn);
|
|
248
|
-
this.queue = [...returnedEntries, ...this.queue];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
(this as any)._sendCount = 0;
|
|
252
|
-
});
|
|
253
|
-
} catch (error) {
|
|
254
|
-
console.error('Error sending logs:', error);
|
|
255
|
-
}
|
|
225
|
+
|
|
226
|
+
const entriesToSend = [...this.queue];
|
|
227
|
+
this.queue = [];
|
|
228
|
+
|
|
229
|
+
this.sendLogEntries(entriesToSend);
|
|
256
230
|
}
|
|
257
|
-
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 发送日志条目到远程服务器
|
|
234
|
+
* @param entries 日志条目数组
|
|
235
|
+
*/
|
|
236
|
+
private sendLogEntries(entries: LogEntry[]): void {
|
|
237
|
+
if (entries.length === 0) return;
|
|
238
|
+
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s 超时
|
|
241
|
+
|
|
242
|
+
fetch(this.endpoint, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json'
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify(entries),
|
|
248
|
+
keepalive: true,
|
|
249
|
+
signal: controller.signal
|
|
250
|
+
}).then(() => {
|
|
251
|
+
clearTimeout(timeoutId);
|
|
252
|
+
this.consecutiveFailures = 0; // 发送成功,重置失败计数
|
|
253
|
+
}).catch((err: Error) => {
|
|
254
|
+
clearTimeout(timeoutId);
|
|
255
|
+
console.error('Failed to send logs to remote server:', err);
|
|
256
|
+
|
|
257
|
+
this.consecutiveFailures++;
|
|
258
|
+
|
|
259
|
+
// 如果失败次数过多,丢弃日志防止内存泄漏
|
|
260
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
261
|
+
console.warn('RemoteLogHandler: Max consecutive failures exceeded, discarding logs');
|
|
262
|
+
this.queue = [];
|
|
263
|
+
this.consecutiveFailures = 0;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 失败时把日志放回队列,但防止无限增长
|
|
268
|
+
const maxReturn = Math.min(entries.length, this.maxQueueSize - this.queue.length);
|
|
269
|
+
if (maxReturn > 0) {
|
|
270
|
+
const returnedEntries = entries.slice(0, maxReturn);
|
|
271
|
+
this.queue = [...returnedEntries, ...this.queue];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
258
276
|
/**
|
|
259
277
|
* 开始定时发送
|
|
260
278
|
*/
|
|
261
279
|
startTimer(): void {
|
|
262
280
|
if (!this.isBrowser) return;
|
|
263
281
|
if (this.timerId !== null) return;
|
|
264
|
-
|
|
265
|
-
this.timerId =
|
|
282
|
+
|
|
283
|
+
this.timerId = setInterval(() => {
|
|
266
284
|
this.flush();
|
|
267
285
|
}, this.flushInterval);
|
|
268
286
|
}
|
|
269
|
-
|
|
287
|
+
|
|
270
288
|
/**
|
|
271
289
|
* 停止定时发送
|
|
272
290
|
*/
|
|
273
291
|
stopTimer(): void {
|
|
274
292
|
if (this.timerId !== null) {
|
|
275
|
-
|
|
293
|
+
clearInterval(this.timerId);
|
|
276
294
|
this.timerId = null;
|
|
277
295
|
}
|
|
278
296
|
}
|
|
@@ -364,40 +382,56 @@ export class Logger {
|
|
|
364
382
|
* 记录调试级别日志
|
|
365
383
|
* @param tag 标签
|
|
366
384
|
* @param message 消息
|
|
367
|
-
* @param
|
|
385
|
+
* @param errorOrData 错误对象或结构化数据
|
|
368
386
|
*/
|
|
369
|
-
debug(tag: string, message: string,
|
|
370
|
-
|
|
387
|
+
debug(tag: string, message: string, errorOrData?: Error | object): void {
|
|
388
|
+
if (errorOrData instanceof Error) {
|
|
389
|
+
this.log(LoggerLevel.DEBUG, tag, message, errorOrData);
|
|
390
|
+
} else {
|
|
391
|
+
this.logWithData(LoggerLevel.DEBUG, tag, message, errorOrData);
|
|
392
|
+
}
|
|
371
393
|
}
|
|
372
|
-
|
|
394
|
+
|
|
373
395
|
/**
|
|
374
396
|
* 记录信息级别日志
|
|
375
397
|
* @param tag 标签
|
|
376
398
|
* @param message 消息
|
|
377
|
-
* @param
|
|
399
|
+
* @param errorOrData 错误对象或结构化数据
|
|
378
400
|
*/
|
|
379
|
-
info(tag: string, message: string,
|
|
380
|
-
|
|
401
|
+
info(tag: string, message: string, errorOrData?: Error | object): void {
|
|
402
|
+
if (errorOrData instanceof Error) {
|
|
403
|
+
this.log(LoggerLevel.INFO, tag, message, errorOrData);
|
|
404
|
+
} else {
|
|
405
|
+
this.logWithData(LoggerLevel.INFO, tag, message, errorOrData);
|
|
406
|
+
}
|
|
381
407
|
}
|
|
382
|
-
|
|
408
|
+
|
|
383
409
|
/**
|
|
384
410
|
* 记录警告级别日志
|
|
385
411
|
* @param tag 标签
|
|
386
412
|
* @param message 消息
|
|
387
|
-
* @param
|
|
413
|
+
* @param errorOrData 错误对象或结构化数据
|
|
388
414
|
*/
|
|
389
|
-
warn(tag: string, message: string,
|
|
390
|
-
|
|
415
|
+
warn(tag: string, message: string, errorOrData?: Error | object): void {
|
|
416
|
+
if (errorOrData instanceof Error) {
|
|
417
|
+
this.log(LoggerLevel.WARN, tag, message, errorOrData);
|
|
418
|
+
} else {
|
|
419
|
+
this.logWithData(LoggerLevel.WARN, tag, message, errorOrData);
|
|
420
|
+
}
|
|
391
421
|
}
|
|
392
|
-
|
|
422
|
+
|
|
393
423
|
/**
|
|
394
424
|
* 记录错误级别日志
|
|
395
425
|
* @param tag 标签
|
|
396
426
|
* @param message 消息
|
|
397
|
-
* @param
|
|
427
|
+
* @param errorOrData 错误对象或结构化数据
|
|
398
428
|
*/
|
|
399
|
-
error(tag: string, message: string,
|
|
400
|
-
|
|
429
|
+
error(tag: string, message: string, errorOrData?: Error | object): void {
|
|
430
|
+
if (errorOrData instanceof Error) {
|
|
431
|
+
this.log(LoggerLevel.ERROR, tag, message, errorOrData);
|
|
432
|
+
} else {
|
|
433
|
+
this.logWithData(LoggerLevel.ERROR, tag, message, errorOrData);
|
|
434
|
+
}
|
|
401
435
|
}
|
|
402
436
|
|
|
403
437
|
/**
|
|
@@ -448,6 +482,46 @@ export class Logger {
|
|
|
448
482
|
}
|
|
449
483
|
}
|
|
450
484
|
|
|
485
|
+
/**
|
|
486
|
+
* 记录日志(支持结构化数据)
|
|
487
|
+
* @param level 日志级别
|
|
488
|
+
* @param tag 标签
|
|
489
|
+
* @param message 消息
|
|
490
|
+
* @param data 结构化数据
|
|
491
|
+
*/
|
|
492
|
+
private logWithData(level: LoggerLevel, tag: string, message: string, data?: object): void {
|
|
493
|
+
// 检查日志级别
|
|
494
|
+
const levelValue = this.getLevelValue(level);
|
|
495
|
+
const currentLevelValue = this.getLevelValue(this.logLevel);
|
|
496
|
+
|
|
497
|
+
if (levelValue < currentLevelValue) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 创建日志条目
|
|
502
|
+
const entry: LogEntry = {
|
|
503
|
+
timestamp: Date.now(),
|
|
504
|
+
level: level,
|
|
505
|
+
tag: tag || this.defaultTag,
|
|
506
|
+
message,
|
|
507
|
+
data
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// 分发到所有处理程序
|
|
511
|
+
for (const handler of this.handlers) {
|
|
512
|
+
try {
|
|
513
|
+
handler.handle(entry);
|
|
514
|
+
} catch (handlerError) {
|
|
515
|
+
console.error(`[Logger] 处理程序错误:`, handlerError);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 如果没有处理程序,使用控制台
|
|
520
|
+
if (this.handlers.length === 0) {
|
|
521
|
+
this.consoleOutput(entry);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
451
525
|
/**
|
|
452
526
|
* 控制台输出
|
|
453
527
|
* @param entry 日志条目
|
|
@@ -455,19 +529,22 @@ export class Logger {
|
|
|
455
529
|
private consoleOutput(entry: LogEntry): void {
|
|
456
530
|
const timestamp = new Date(entry.timestamp).toISOString();
|
|
457
531
|
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
|
|
458
|
-
|
|
532
|
+
|
|
533
|
+
// 构造日志内容:错误对象或数据对象
|
|
534
|
+
const extra = entry.error || entry.data || '';
|
|
535
|
+
|
|
459
536
|
switch (entry.level) {
|
|
460
537
|
case LoggerLevel.DEBUG:
|
|
461
|
-
console.debug(`${prefix} ${entry.message}`,
|
|
538
|
+
console.debug(`${prefix} ${entry.message}`, extra);
|
|
462
539
|
break;
|
|
463
540
|
case LoggerLevel.INFO:
|
|
464
|
-
console.info(`${prefix} ${entry.message}`,
|
|
541
|
+
console.info(`${prefix} ${entry.message}`, extra);
|
|
465
542
|
break;
|
|
466
543
|
case LoggerLevel.WARN:
|
|
467
|
-
console.warn(`${prefix} ${entry.message}`,
|
|
544
|
+
console.warn(`${prefix} ${entry.message}`, extra);
|
|
468
545
|
break;
|
|
469
546
|
case LoggerLevel.ERROR:
|
|
470
|
-
console.error(`${prefix} ${entry.message}`,
|
|
547
|
+
console.error(`${prefix} ${entry.message}`, extra);
|
|
471
548
|
break;
|
|
472
549
|
}
|
|
473
550
|
}
|
|
@@ -10,6 +10,156 @@ import { ResourceLoadError } from './errors';
|
|
|
10
10
|
import { EventEmitter } from './event-emitter';
|
|
11
11
|
import { Result } from './result';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 模型分片信息
|
|
15
|
+
*/
|
|
16
|
+
export interface ModelShard {
|
|
17
|
+
name: string; // e.g. 'face-detector'
|
|
18
|
+
version: string; // e.g. '1.0.0'
|
|
19
|
+
url: string; // 完整 URL
|
|
20
|
+
size: number; // bytes
|
|
21
|
+
hash?: string; // 完整性校验
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 模型缓存信息
|
|
26
|
+
*/
|
|
27
|
+
export interface ModelCacheInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
version: string;
|
|
30
|
+
cachedAt: number;
|
|
31
|
+
size: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* IndexedDB 模型缓存管理器
|
|
36
|
+
* 提供分片模型的持久化缓存功能
|
|
37
|
+
*/
|
|
38
|
+
export class ModelCacheManager {
|
|
39
|
+
private _dbName = 'id-scanner-lib-models';
|
|
40
|
+
private _storeName = 'model-cache';
|
|
41
|
+
private _db: IDBDatabase | null = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 初始化缓存管理器(打开 IndexedDB)
|
|
45
|
+
*/
|
|
46
|
+
async init(): Promise<void> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const request = indexedDB.open(this._dbName, 1);
|
|
49
|
+
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
|
50
|
+
request.onsuccess = () => {
|
|
51
|
+
this._db = request.result;
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
request.onupgradeneeded = (event) => {
|
|
55
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
56
|
+
if (!db.objectStoreNames.contains(this._storeName)) {
|
|
57
|
+
const store = db.createObjectStore(this._storeName, { keyPath: 'key' });
|
|
58
|
+
store.createIndex('name', 'name', { unique: false });
|
|
59
|
+
store.createIndex('version', 'version', { unique: false });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 生成缓存键
|
|
67
|
+
*/
|
|
68
|
+
private _makeKey(model: ModelShard): string {
|
|
69
|
+
return `${model.name}@${model.version}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 检查模型是否已缓存
|
|
74
|
+
*/
|
|
75
|
+
async has(model: ModelShard): Promise<boolean> {
|
|
76
|
+
if (!this._db) return false;
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const tx = this._db!.transaction(this._storeName, 'readonly');
|
|
79
|
+
const store = tx.objectStore(this._storeName);
|
|
80
|
+
const req = store.get(this._makeKey(model));
|
|
81
|
+
req.onsuccess = () => resolve(!!req.result);
|
|
82
|
+
req.onerror = () => reject(req.error);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 存储模型数据
|
|
88
|
+
*/
|
|
89
|
+
async store(model: ModelShard, data: ArrayBuffer): Promise<void> {
|
|
90
|
+
if (!this._db) throw new Error('ModelCacheManager not initialized');
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const tx = this._db!.transaction(this._storeName, 'readwrite');
|
|
93
|
+
const store = tx.objectStore(this._storeName);
|
|
94
|
+
const record = {
|
|
95
|
+
key: this._makeKey(model),
|
|
96
|
+
name: model.name,
|
|
97
|
+
version: model.version,
|
|
98
|
+
url: model.url,
|
|
99
|
+
size: model.size,
|
|
100
|
+
data,
|
|
101
|
+
cachedAt: Date.now(),
|
|
102
|
+
};
|
|
103
|
+
const req = store.put(record);
|
|
104
|
+
req.onsuccess = () => resolve();
|
|
105
|
+
req.onerror = () => reject(req.error);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 加载模型数据
|
|
111
|
+
*/
|
|
112
|
+
async load(model: ModelShard): Promise<ArrayBuffer | null> {
|
|
113
|
+
if (!this._db) throw new Error('ModelCacheManager not initialized');
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const tx = this._db!.transaction(this._storeName, 'readonly');
|
|
116
|
+
const store = tx.objectStore(this._storeName);
|
|
117
|
+
const req = store.get(this._makeKey(model));
|
|
118
|
+
req.onsuccess = () => resolve(req.result ? req.result.data : null);
|
|
119
|
+
req.onerror = () => reject(req.error);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 清除所有缓存
|
|
125
|
+
*/
|
|
126
|
+
async clear(): Promise<void> {
|
|
127
|
+
if (!this._db) return;
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const tx = this._db!.transaction(this._storeName, 'readwrite');
|
|
130
|
+
const store = tx.objectStore(this._storeName);
|
|
131
|
+
const req = store.clear();
|
|
132
|
+
req.onsuccess = () => resolve();
|
|
133
|
+
req.onerror = () => reject(req.error);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 列出已缓存模型
|
|
139
|
+
*/
|
|
140
|
+
async listCached(): Promise<ModelCacheInfo[]> {
|
|
141
|
+
if (!this._db) return [];
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const tx = this._db!.transaction(this._storeName, 'readonly');
|
|
144
|
+
const store = tx.objectStore(this._storeName);
|
|
145
|
+
const req = store.getAll();
|
|
146
|
+
req.onsuccess = () => {
|
|
147
|
+
const results: ModelCacheInfo[] = req.result.map((r: any) => ({
|
|
148
|
+
name: r.name,
|
|
149
|
+
version: r.version,
|
|
150
|
+
cachedAt: r.cachedAt,
|
|
151
|
+
size: r.size,
|
|
152
|
+
}));
|
|
153
|
+
resolve(results);
|
|
154
|
+
};
|
|
155
|
+
req.onerror = () => reject(req.error);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 全局模型缓存管理器实例 */
|
|
161
|
+
export const modelCache = new ModelCacheManager();
|
|
162
|
+
|
|
13
163
|
/**
|
|
14
164
|
* 资源类型枚举
|
|
15
165
|
*/
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Scanner Core Class
|
|
3
|
+
* @description Main entry point for the Scanner library
|
|
4
|
+
* @module core/scanner
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ScannerConfig, ScannerModule, ImageSource, Face } from './config';
|
|
8
|
+
import { ScannerError, ErrorCodes } from './errors';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scanner main class
|
|
12
|
+
* Provides unified interface for all scanning operations
|
|
13
|
+
*/
|
|
14
|
+
export class Scanner {
|
|
15
|
+
/**
|
|
16
|
+
* Internal configuration
|
|
17
|
+
*/
|
|
18
|
+
private _config: Required<ScannerConfig>;
|
|
19
|
+
/**
|
|
20
|
+
* Loaded modules
|
|
21
|
+
*/
|
|
22
|
+
private _modules: Map<string, ScannerModule>;
|
|
23
|
+
/**
|
|
24
|
+
* Initialization state
|
|
25
|
+
*/
|
|
26
|
+
private _initialized: boolean = false;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new Scanner instance
|
|
30
|
+
* @param config Scanner configuration
|
|
31
|
+
*/
|
|
32
|
+
constructor(config: ScannerConfig = {}) {
|
|
33
|
+
this._config = this._normalizeConfig(config);
|
|
34
|
+
this._modules = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if scanner is initialized
|
|
39
|
+
*/
|
|
40
|
+
get initialized(): boolean {
|
|
41
|
+
return this._initialized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize configuration with defaults
|
|
46
|
+
* @param config User provided config
|
|
47
|
+
* @returns Normalized config with defaults
|
|
48
|
+
*/
|
|
49
|
+
private _normalizeConfig(config: ScannerConfig): Required<ScannerConfig> {
|
|
50
|
+
return {
|
|
51
|
+
debug: config.debug ?? false,
|
|
52
|
+
modules: {
|
|
53
|
+
face: config.modules?.face ?? true,
|
|
54
|
+
faceComparator: config.modules?.faceComparator ?? false,
|
|
55
|
+
faceLiveness: config.modules?.faceLiveness ?? false,
|
|
56
|
+
idCard: config.modules?.idCard ?? false,
|
|
57
|
+
qr: config.modules?.qr ?? false,
|
|
58
|
+
},
|
|
59
|
+
performance: {
|
|
60
|
+
maxCanvasWidth: config.performance?.maxCanvasWidth ?? 1280,
|
|
61
|
+
useWorker: config.performance?.useWorker ?? true,
|
|
62
|
+
lazyLoad: config.performance?.lazyLoad ?? true,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the scanner and load enabled modules
|
|
69
|
+
*/
|
|
70
|
+
async initialize(): Promise<void> {
|
|
71
|
+
// Immediately load face core module
|
|
72
|
+
const { FaceDetector } = await import('../modules/face/detector');
|
|
73
|
+
const detector = new FaceDetector();
|
|
74
|
+
await detector.initialize();
|
|
75
|
+
this._modules.set('face-detector', detector);
|
|
76
|
+
this._initialized = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detect faces in an image source
|
|
81
|
+
* @param input Image source (video, canvas, image, etc.)
|
|
82
|
+
* @returns Array of detected faces
|
|
83
|
+
*/
|
|
84
|
+
async detectFace(input: ImageSource): Promise<Face[]> {
|
|
85
|
+
const detector = this._modules.get('face-detector');
|
|
86
|
+
if (!detector) {
|
|
87
|
+
throw new ScannerError(
|
|
88
|
+
'Scanner not initialized. Call initialize() first.',
|
|
89
|
+
ErrorCodes.NOT_INITIALIZED
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return (detector as any).detect(input);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Destroy the scanner and release all resources
|
|
97
|
+
*/
|
|
98
|
+
async destroy(): Promise<void> {
|
|
99
|
+
for (const module of this._modules.values()) {
|
|
100
|
+
await module.destroy();
|
|
101
|
+
}
|
|
102
|
+
this._modules.clear();
|
|
103
|
+
this._initialized = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Re-export types
|
|
108
|
+
export { ScannerConfig, ScannerModule, ImageSource, Face } from './config';
|
|
109
|
+
export { ScannerError, ErrorCodes } from './errors';
|