@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 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
- - 📋 **Captura doble cara** (frente y reverso) con opción de omitir reverso
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 carga el modelo ONNX automáticamente al iniciar la detección
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. **Captura del reverso**: Se solicita voltear el documento para capturar el reverso
503
- 5. **Completado**: Se emite el evento `captureCompleted` con todas las imágenes
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 del modelo**: Se llama a `preloadModel()` para cargar el modelo en memoria
507
- 2. **Inicio optimizado**: Al iniciar la captura, se usa el modelo ya cargado
508
- 3. **Captura del frente**: Detección más rápida con el modelo precargado
509
- 4. **Captura del reverso**: Se solicita voltear el documento
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
- - El modelo de IA se ejecuta completamente en el cliente
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/detector-nano.onnx";
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 modelo de detección...";
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 = "Modelo precargado. Listo para comenzar detección";
284
+ this.statusMessage = "Modelos precargados. Listo para comenzar detección";
279
285
  this.statusColor = "#28a745";
280
286
  this.emitReadyEvent();
281
- this.debugLog('✅ Model preloaded successfully');
282
- return { success: true, message: 'Model preloaded successfully' };
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 model:', error);
291
+ this.debugLog('❌ Error preloading models:', error);
286
292
  this.isLoading = false;
287
- this.statusMessage = "Error al precargar el modelo";
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 session
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 modelo de detección...";
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 model');
470
- this.statusMessage = "Usando modelo precargado...";
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: '7cd80970b12ba60949ff34b5a9405b9782267918', class: "detector-container" }, index.h("div", { key: '0e66e46a524bd37f60f30504a270f72b64a97fc1', class: "video-container" }, index.h("video", { key: '0c1b0719c626659e54bec8ce3383f50d990c32c1', 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: '09fc3a2d511e48186099a0ff5d3d6378f15fe1c4', ref: el => this.canvasRef = el, class: this.shouldMirrorVideo ? 'mirror' : '' }), this.isMaskReady && (index.h("div", { key: '70a31b04c9543fc0834a848013daf06c654e4194', class: "overlay-mask" }, index.h("div", { key: '050dcdaa59b028b5c379de310a80ca7191bf8af8', class: "card-outline" }, index.h("div", { key: 'adef93f2e6b16b308dc5d67469f35c2025ca160f', class: "side side-top" }), index.h("div", { key: '4900bed8cc2607f92163a25bbf3c0d3a272b9840', class: "side side-right" }), index.h("div", { key: '11fc9da6cfe04b993bf4a72413da2344122b21e4', class: "side side-bottom" }), index.h("div", { key: 'ed95ac2578ba045e003d61144aecd09ef4f01561', class: "side side-left" }), index.h("div", { key: '37eec8aad0a0d449174d52caab7aec5a25d11731', class: "corner corner-tl" }), index.h("div", { key: 'c7247e71d8936d0a30f415f73377713a8c457420', class: "corner corner-tr" }), index.h("div", { key: 'e131e0c3bd66068ae1d33cd7211edafd92dfbe62', class: "corner corner-bl" }), index.h("div", { key: '89234ddcde735501907276e10287af983033ec85', class: "corner corner-br" }), !this.showFlipAnimation && !this.showSuccessAnimation && (index.h("div", { key: '4b2543dd05f173b3ee4e3053ed7b62d4e7b75e63', class: "guide-text" }, this.statusMessage))), this.captureStep === 'back' && !this.showFlipAnimation && !this.showSuccessAnimation && (index.h("button", { key: 'e7e2b2b1486f7024fc2d42073313833bca88e315', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")))), this.isCapturing && (index.h("div", { key: '6ca8283fe3eb1c5b2700c0d313afb51c2db3ea7f', class: "capture-animation" })), this.showFlipAnimation && (index.h("div", { key: 'eaed1200385a98a8750cdaef2dad9f8ff63914f3', class: "flip-animation" }, index.h("div", { key: 'dbe9bfc929fa8b13cadc60cdece603c48dbcbd4c', class: "id-card-icon" }), index.h("div", { key: '82b9de0910a993b6cf3af7ecdf43de41d495c181', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), this.showSuccessAnimation && (index.h("div", { key: '92ef47686b0fbf91f6a3f8c3c3f23a78a6fc76f8', class: "success-animation" }, index.h("div", { key: '35aeb41dce144fc844d7447a958473834f6b0843', class: "check-icon" }), index.h("div", { key: '2ba06161a8d665394803ce8cc90320b154048c0b', class: "success-text" }, "\u00A1Proceso completado!"))), this.isLoading && (index.h("div", { key: '8abc4f5724ca6543698f7ca9116ccfc9c042edbd', class: "loading-overlay" }, index.h("div", { key: '5c8f73bb7c30c6a4c304e96e4db2bf75dc89b89e', class: "loading-spinner" }), index.h("div", { key: '8f5665f21be98ddccb9d9d2c6412e4edf4571a51', class: "loading-text" }, this.statusMessage))), index.h("div", { key: '00316bee3c56f07b9771c400f5b84b7e14dca4af', class: "watermark" }, index.h("img", { key: 'd7f58ffa993405912f8aa26386f39caa72e7a000', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
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;