@jaak.ai/stamps 2.0.0-dev.23 → 2.0.0-dev.24
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 +22 -9
- package/dist/cjs/jaak-stamps.cjs.entry.js +116 -16
- package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
- package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
- package/dist/collection/components/my-component/my-component.js +116 -16
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/components/jaak-stamps.js +116 -16
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/jaak-stamps.entry.js +116 -16
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-0521d8f5.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-0521d8f5.entry.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +7 -0
- package/package.json +1 -1
- package/dist/jaak-stamps-webcomponent/p-380b1ebe.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-380b1ebe.entry.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Un componente web para la detección y captura automática de documentos de iden
|
|
|
6
6
|
|
|
7
7
|
- 📷 **Detección automática** de documentos de identificación en tiempo real
|
|
8
8
|
- 🎯 **Guía visual** para posicionamiento óptimo del documento
|
|
9
|
-
-
|
|
9
|
+
- 🤖 **Clasificación inteligente** de documentos para determinar si requieren reverso
|
|
10
|
+
- 📋 **Captura adaptativa** - automáticamente detecta si el documento tiene reverso
|
|
10
11
|
- 🖼 **Doble salida** para cada lado: imagen completa y recorte del documento
|
|
11
12
|
- ✂️ **Recorte configurable** con margen personalizable alrededor del documento
|
|
12
13
|
- 🔧 **API programática** para control total del proceso
|
|
@@ -497,18 +498,29 @@ interface Status {
|
|
|
497
498
|
|
|
498
499
|
### Flujo Estándar
|
|
499
500
|
1. **Inicialización**: El componente se prepara para la detección
|
|
500
|
-
2. **Inicio de captura**: Se
|
|
501
|
+
2. **Inicio de captura**: Se cargan automáticamente los modelos de detección y clasificación
|
|
501
502
|
3. **Captura del frente**: El usuario posiciona el documento y se captura automáticamente
|
|
502
|
-
4. **
|
|
503
|
-
5. **
|
|
503
|
+
4. **Clasificación automática**: El sistema determina si el documento requiere captura de reverso
|
|
504
|
+
5. **Flujo adaptativo**:
|
|
505
|
+
- **Si no requiere reverso**: Se completa automáticamente el proceso
|
|
506
|
+
- **Si requiere reverso**: Se solicita voltear el documento para capturar el reverso
|
|
507
|
+
6. **Completado**: Se emite el evento `captureCompleted` con las imágenes correspondientes
|
|
504
508
|
|
|
505
509
|
### Flujo con Precarga (Recomendado)
|
|
506
|
-
1. **Precarga
|
|
507
|
-
2. **Inicio optimizado**: Al iniciar la captura, se
|
|
508
|
-
3. **Captura del frente**: Detección más
|
|
509
|
-
4. **
|
|
510
|
+
1. **Precarga de modelos**: Se llama a `preloadModel()` para cargar ambos modelos en memoria
|
|
511
|
+
2. **Inicio optimizado**: Al iniciar la captura, se usan los modelos ya cargados
|
|
512
|
+
3. **Captura del frente**: Detección y clasificación más rápidas con modelos precargados
|
|
513
|
+
4. **Flujo inteligente**:
|
|
514
|
+
- **Documento sin reverso**: Proceso completado inmediatamente
|
|
515
|
+
- **Documento con reverso**: Se solicita captura del reverso
|
|
510
516
|
5. **Completado**: Se emite el evento `captureCompleted` con todas las imágenes
|
|
511
517
|
|
|
518
|
+
### Clasificación de Documentos
|
|
519
|
+
El sistema utiliza un modelo MobileNet especializado para determinar automáticamente si el documento requiere captura de reverso:
|
|
520
|
+
- **Documentos sin reverso**: Se omite automáticamente la captura del reverso
|
|
521
|
+
- **Documentos con reverso**: Requieren captura de ambos lados (frente y reverso)
|
|
522
|
+
- **Botón "Saltar reverso"**: Siempre disponible para permitir flexibilidad manual
|
|
523
|
+
|
|
512
524
|
## Requisitos del Sistema
|
|
513
525
|
|
|
514
526
|
- **Navegadores**: Chrome/Edge 88+, Firefox 85+, Safari 14+
|
|
@@ -519,7 +531,8 @@ interface Status {
|
|
|
519
531
|
|
|
520
532
|
- Las imágenes se procesan localmente en el navegador
|
|
521
533
|
- No se envían datos a servidores externos
|
|
522
|
-
-
|
|
534
|
+
- Los modelos de IA (detección y clasificación) se ejecutan completamente en el cliente
|
|
535
|
+
- La clasificación de documentos se realiza de forma privada y local
|
|
523
536
|
- Las imágenes capturadas solo están disponibles en el contexto de tu aplicación
|
|
524
537
|
|
|
525
538
|
## Desarrollo
|
|
@@ -48,6 +48,8 @@ const JaakStamps = class {
|
|
|
48
48
|
animationId;
|
|
49
49
|
hasScreenshotTaken = false;
|
|
50
50
|
lastDetectedBox;
|
|
51
|
+
mobileNetSession;
|
|
52
|
+
mobileNetClassMap;
|
|
51
53
|
// Performance optimization properties
|
|
52
54
|
frameSkipCounter = 0;
|
|
53
55
|
FRAME_SKIP = 2; // Process every 3rd frame (reduces CPU by ~66%)
|
|
@@ -60,7 +62,9 @@ const JaakStamps = class {
|
|
|
60
62
|
preprocessCtx;
|
|
61
63
|
captureCanvas;
|
|
62
64
|
captureCtx;
|
|
63
|
-
MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/
|
|
65
|
+
MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v1.onnx";
|
|
66
|
+
MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
|
|
67
|
+
MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
|
|
64
68
|
INPUT_SIZE = 320;
|
|
65
69
|
CONFIDENCE_THRESHOLD = 0.6;
|
|
66
70
|
// ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
|
|
@@ -266,25 +270,27 @@ const JaakStamps = class {
|
|
|
266
270
|
}
|
|
267
271
|
try {
|
|
268
272
|
this.isLoading = true;
|
|
269
|
-
this.statusMessage = "Precargando
|
|
273
|
+
this.statusMessage = "Precargando modelos...";
|
|
270
274
|
this.statusColor = "#007bff";
|
|
271
275
|
const modelPath = this.MODEL_PATH;
|
|
272
|
-
this.debugLog('🤖 Preloading model:', modelPath);
|
|
276
|
+
this.debugLog('🤖 Preloading detection model:', modelPath);
|
|
273
277
|
// Configure ONNX Runtime with device-specific optimizations
|
|
274
278
|
const sessionOptions = this.getSessionOptions();
|
|
275
279
|
this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
|
|
280
|
+
// Preload MobileNet model and classes
|
|
281
|
+
await this.loadMobileNetModel();
|
|
276
282
|
this.isModelPreloaded = true;
|
|
277
283
|
this.isLoading = false;
|
|
278
|
-
this.statusMessage = "
|
|
284
|
+
this.statusMessage = "Modelos precargados. Listo para comenzar detección";
|
|
279
285
|
this.statusColor = "#28a745";
|
|
280
286
|
this.emitReadyEvent();
|
|
281
|
-
this.debugLog('✅
|
|
282
|
-
return { success: true, message: '
|
|
287
|
+
this.debugLog('✅ Models preloaded successfully');
|
|
288
|
+
return { success: true, message: 'Models preloaded successfully' };
|
|
283
289
|
}
|
|
284
290
|
catch (error) {
|
|
285
|
-
this.debugLog('❌ Error preloading
|
|
291
|
+
this.debugLog('❌ Error preloading models:', error);
|
|
286
292
|
this.isLoading = false;
|
|
287
|
-
this.statusMessage = "Error al precargar
|
|
293
|
+
this.statusMessage = "Error al precargar los modelos";
|
|
288
294
|
this.statusColor = "#ff6b6b";
|
|
289
295
|
return { success: false, error: error.message };
|
|
290
296
|
}
|
|
@@ -294,6 +300,80 @@ const JaakStamps = class {
|
|
|
294
300
|
this.completeProcess(true);
|
|
295
301
|
}
|
|
296
302
|
}
|
|
303
|
+
async loadMobileNetModel() {
|
|
304
|
+
try {
|
|
305
|
+
this.debugLog('🤖 Loading MobileNet model...');
|
|
306
|
+
// Load class map
|
|
307
|
+
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
308
|
+
if (!classResponse.ok) {
|
|
309
|
+
throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
|
|
310
|
+
}
|
|
311
|
+
this.mobileNetClassMap = await classResponse.json();
|
|
312
|
+
this.debugLog('📋 MobileNet classes loaded:', this.mobileNetClassMap);
|
|
313
|
+
// Load model
|
|
314
|
+
const sessionOptions = this.getSessionOptions();
|
|
315
|
+
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
|
|
316
|
+
this.debugLog('✅ MobileNet model loaded successfully');
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
this.debugLog('❌ Error loading MobileNet model:', error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
preprocessMobileNet(canvas) {
|
|
324
|
+
// Create a temporary canvas for MobileNet preprocessing (224x224)
|
|
325
|
+
const tempCanvas = document.createElement('canvas');
|
|
326
|
+
tempCanvas.width = 224;
|
|
327
|
+
tempCanvas.height = 224;
|
|
328
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
329
|
+
// Draw the cropped image onto the 224x224 canvas
|
|
330
|
+
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
331
|
+
// Get image data and preprocess for MobileNet
|
|
332
|
+
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
333
|
+
const data = imageData.data;
|
|
334
|
+
const hw = 224 * 224;
|
|
335
|
+
const arr = new Float32Array(3 * hw);
|
|
336
|
+
// Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
|
|
337
|
+
for (let i = 0; i < hw; i++) {
|
|
338
|
+
arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
|
|
339
|
+
arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
|
|
340
|
+
arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
|
|
341
|
+
}
|
|
342
|
+
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
343
|
+
}
|
|
344
|
+
async classifyDocument(canvas) {
|
|
345
|
+
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
346
|
+
this.debugLog('⚠️ MobileNet model not loaded');
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
this.debugLog('🔍 Classifying document...');
|
|
351
|
+
// Preprocess image for MobileNet
|
|
352
|
+
const inputTensor = this.preprocessMobileNet(canvas);
|
|
353
|
+
// Run inference
|
|
354
|
+
const feeds = { input: inputTensor };
|
|
355
|
+
const results = await this.mobileNetSession.run(feeds);
|
|
356
|
+
const output = results[Object.keys(results)[0]].data;
|
|
357
|
+
// Find the class with highest confidence
|
|
358
|
+
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
359
|
+
const confidence = output[maxIdx];
|
|
360
|
+
const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
|
|
361
|
+
this.debugLog('📄 Document classification result:', {
|
|
362
|
+
class: className,
|
|
363
|
+
confidence: confidence.toFixed(3),
|
|
364
|
+
classIndex: maxIdx
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
class: className,
|
|
368
|
+
confidence: confidence,
|
|
369
|
+
classIndex: maxIdx
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
this.debugLog('❌ Error classifying document:', error);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
297
377
|
cleanup() {
|
|
298
378
|
if (this.animationId) {
|
|
299
379
|
cancelAnimationFrame(this.animationId);
|
|
@@ -321,11 +401,16 @@ const JaakStamps = class {
|
|
|
321
401
|
this.captureCtx = undefined;
|
|
322
402
|
this.captureCanvas = undefined;
|
|
323
403
|
}
|
|
324
|
-
// Disposed ONNX
|
|
404
|
+
// Disposed ONNX sessions
|
|
325
405
|
if (this.session) {
|
|
326
406
|
this.session.release?.();
|
|
327
407
|
this.session = undefined;
|
|
328
408
|
}
|
|
409
|
+
if (this.mobileNetSession) {
|
|
410
|
+
this.mobileNetSession.release?.();
|
|
411
|
+
this.mobileNetSession = undefined;
|
|
412
|
+
}
|
|
413
|
+
this.mobileNetClassMap = undefined;
|
|
329
414
|
this.isModelPreloaded = false;
|
|
330
415
|
this.debugLog('🧹 Canvas pool cleaned up');
|
|
331
416
|
}
|
|
@@ -456,18 +541,22 @@ const JaakStamps = class {
|
|
|
456
541
|
// Check if model is already preloaded
|
|
457
542
|
if (!this.session) {
|
|
458
543
|
this.isLoading = true;
|
|
459
|
-
this.statusMessage = "Cargando
|
|
544
|
+
this.statusMessage = "Cargando modelos...";
|
|
460
545
|
this.statusColor = "#007bff";
|
|
461
546
|
const modelPath = this.MODEL_PATH;
|
|
462
|
-
this.debugLog('🤖 Loading model:', modelPath);
|
|
547
|
+
this.debugLog('🤖 Loading detection model:', modelPath);
|
|
463
548
|
const sessionOptions = this.getSessionOptions();
|
|
464
549
|
this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
|
|
550
|
+
// Load MobileNet model if not already loaded
|
|
551
|
+
if (!this.mobileNetSession) {
|
|
552
|
+
await this.loadMobileNetModel();
|
|
553
|
+
}
|
|
465
554
|
this.isModelPreloaded = true;
|
|
466
555
|
this.emitReadyEvent();
|
|
467
556
|
}
|
|
468
557
|
else {
|
|
469
|
-
this.debugLog('🚀 Using preloaded
|
|
470
|
-
this.statusMessage = "Usando
|
|
558
|
+
this.debugLog('🚀 Using preloaded models');
|
|
559
|
+
this.statusMessage = "Usando modelos precargados...";
|
|
471
560
|
this.statusColor = "#007bff";
|
|
472
561
|
}
|
|
473
562
|
this.statusMessage = "Configurando cámara...";
|
|
@@ -686,7 +775,9 @@ const JaakStamps = class {
|
|
|
686
775
|
corners?.forEach(corner => corner.classList.add('perfect-match'));
|
|
687
776
|
if (!this.hasScreenshotTaken) {
|
|
688
777
|
this.lastDetectedBox = bestBox;
|
|
689
|
-
this.takeScreenshot()
|
|
778
|
+
this.takeScreenshot().catch(error => {
|
|
779
|
+
this.debugLog('❌ Error taking screenshot:', error);
|
|
780
|
+
});
|
|
690
781
|
this.hasScreenshotTaken = true;
|
|
691
782
|
// Reset para permitir segunda captura
|
|
692
783
|
setTimeout(() => {
|
|
@@ -769,7 +860,7 @@ const JaakStamps = class {
|
|
|
769
860
|
ctx.strokeRect(x, y, w, h);
|
|
770
861
|
});
|
|
771
862
|
}
|
|
772
|
-
takeScreenshot() {
|
|
863
|
+
async takeScreenshot() {
|
|
773
864
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
774
865
|
return;
|
|
775
866
|
// Activar animación
|
|
@@ -803,6 +894,15 @@ const JaakStamps = class {
|
|
|
803
894
|
// Captura del frente usando canvas reutilizado
|
|
804
895
|
this.capturedFullFrame = this.captureCanvas.toDataURL('image/png');
|
|
805
896
|
this.capturedCroppedId = croppedCanvas.toDataURL('image/png');
|
|
897
|
+
// Classify the cropped document
|
|
898
|
+
const classification = await this.classifyDocument(croppedCanvas);
|
|
899
|
+
if (classification && classification.class === 'passport') {
|
|
900
|
+
// If it's a passport, skip back capture since passports don't have a back side
|
|
901
|
+
this.debugLog('📄 Passport detected - skipping back capture');
|
|
902
|
+
this.completeProcess(true);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
// For other IDs, continue with normal flow (back capture)
|
|
806
906
|
this.captureStep = 'back';
|
|
807
907
|
this.statusMessage = "Voltee la identificación y muestre la parte trasera";
|
|
808
908
|
this.statusColor = "#007bff";
|
|
@@ -932,7 +1032,7 @@ const JaakStamps = class {
|
|
|
932
1032
|
this.cleanup();
|
|
933
1033
|
}
|
|
934
1034
|
render() {
|
|
935
|
-
return (index.h("div", { key: '
|
|
1035
|
+
return (index.h("div", { key: 'd6cd4225500c44e5c7fb4f2b3dc453a055467066', class: "detector-container" }, index.h("div", { key: '6bd49b0bcd686648aeffb8fb95203dc0779f1d8a', class: "video-container" }, index.h("video", { key: 'd2666ce9d1f5b84703ac86fd4eb901f4298ed7d5', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: this.isVideoActive ? 'block' : 'none' } }), index.h("canvas", { key: 'e997f063450d81d2a15398f0761c9d06c28a85c9', ref: el => this.canvasRef = el, class: this.shouldMirrorVideo ? 'mirror' : '' }), this.isMaskReady && (index.h("div", { key: '4ccb8bf05091a7173255181eae1a8699fe512b56', class: "overlay-mask" }, index.h("div", { key: 'b35defa8c207b05e41d64c8e1eefc905d31fad55', class: "card-outline" }, index.h("div", { key: '6ed8dad63d6d79c1bb4a64f0b28467f5b29d778a', class: "side side-top" }), index.h("div", { key: '403d4a5ba681a58ad1870a4339e19b4f037a8795', class: "side side-right" }), index.h("div", { key: '3cb3276bb184a4b76419b4cb0b4fcb151a4df631', class: "side side-bottom" }), index.h("div", { key: 'ed7c9bec0e708ab282ba8b24a08d21b8126af27e', class: "side side-left" }), index.h("div", { key: '157be211355bf9260dc7a73d03831dbb1a2945e3', class: "corner corner-tl" }), index.h("div", { key: 'b19af5f319333ff05f981063af939d2c4cfc9b5f', class: "corner corner-tr" }), index.h("div", { key: '6fcd3e2c8842261dcd32c36e6df5b2f3dd0c8fc8', class: "corner corner-bl" }), index.h("div", { key: '7caab5eaee62f3f9039075b3748083c89c8127e0', class: "corner corner-br" }), !this.showFlipAnimation && !this.showSuccessAnimation && (index.h("div", { key: 'e3dd42ddb75b22aeef438d8651955f3dc039f00c', class: "guide-text" }, this.statusMessage))), this.captureStep === 'back' && !this.showFlipAnimation && !this.showSuccessAnimation && (index.h("button", { key: '54b8651817cd52bad2f34ac1413a4b1e21166cee', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")))), this.isCapturing && (index.h("div", { key: '853707a4fa71eeacbee7985e619fca80d6522cbd', class: "capture-animation" })), this.showFlipAnimation && (index.h("div", { key: '246ed3f5dc57ee57345105cdebe5f950d5ff4c90', class: "flip-animation" }, index.h("div", { key: 'f03c6c87c8f5e98a32176cca061d369a521da2e8', class: "id-card-icon" }), index.h("div", { key: 'e357785b076a9e41ea4f9b3d9c14e847bacc9604', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), this.showSuccessAnimation && (index.h("div", { key: 'df3adaaecd3d547938638c58ccc0e89f8325fc23', class: "success-animation" }, index.h("div", { key: 'a023985b8afd6ed72f9e1c2ee63e2c7ba9fb960b', class: "check-icon" }), index.h("div", { key: '739ff7f329f2131061a6860529c07c8819800aab', class: "success-text" }, "\u00A1Proceso completado!"))), this.isLoading && (index.h("div", { key: '9b02df640528a19cb15e5dbabf9ecb43afbceffa', class: "loading-overlay" }, index.h("div", { key: '3652c8e355ea7fe623c478229bf548bdf30e399a', class: "loading-spinner" }), index.h("div", { key: '334f4469b5672f6af78b8679c15ed8c59ecefa85', class: "loading-text" }, this.statusMessage))), index.h("div", { key: 'a0b7c9b92771cff96e97bd67da2fd3090b080f8a', class: "watermark" }, index.h("img", { key: 'b5c8f0de1d8138d4a0c80526fd33b2daefa86b16', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
|
|
936
1036
|
}
|
|
937
1037
|
};
|
|
938
1038
|
JaakStamps.style = myComponentCss;
|