@jaak.ai/stamps 2.0.0-dev.22 → 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
@@ -1,15 +1,15 @@
1
1
  # Jaak Stamps - Document Detector Web Component
2
2
 
3
- [![Built With Stencil](https://img.shields.io/badge/-Built%20With%20Stencil-16161d.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI%2BCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI%2BCgkuc3Qwe2ZpbGw6I0ZGRkZGRjt9Cjwvc3R5bGU%2BCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik00MjQuNywzNzMuOWMwLDM3LjYtNTUuMSw2OC42LTkyLjcsNjguNkgxODAuNGMtMzcuOSwwLTkyLjctMzAuNy05Mi43LTY4LjZ2LTMuNmgzMzYuOVYzNzMuOXoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTQyNC43LDI5Mi4xSDE4MC40Yy0zNy42LDAtOTIuNy0zMS05Mi43LTY4LjZ2LTMuNkgzMzJjMzcuNiwwLDkyLjcsMzEsOTIuNyw2OC42VjI5Mi4xeiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNDI0LjcsMTQxLjdIODcuN3YtMy42YzAtMzcuNiw1NC44LTY4LjYsOTIuNy02OC42SDMzMmMzNy45LDAsOTIuNywzMC43LDkyLjcsNjguNlYxNDEuN3oiLz4KPC9zdmc%2BCg%3D%3D&colorA=16161d&style=flat-square)](https://stenciljs.com)
4
-
5
3
  Un componente web para la detección y captura automática de documentos de identificación usando visión artificial y redes neuronales.
6
4
 
7
5
  ## Características
8
6
 
9
7
  - 📷 **Detección automática** de documentos de identificación en tiempo real
10
8
  - 🎯 **Guía visual** para posicionamiento óptimo del documento
11
- - 📋 **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
12
11
  - 🖼 **Doble salida** para cada lado: imagen completa y recorte del documento
12
+ - ✂️ **Recorte configurable** con margen personalizable alrededor del documento
13
13
  - 🔧 **API programática** para control total del proceso
14
14
  - 📱 **Responsive** y compatible con dispositivos móviles
15
15
  - ⚡ **Optimización automática** usando ONNX Runtime Web
@@ -43,7 +43,7 @@ yarn add @jaak.ai/stamps
43
43
  <script type="module" src="https://unpkg.com/@jaak.ai/stamps/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js"></script>
44
44
  </head>
45
45
  <body>
46
- <jaak-stamps id="detector" debug="false" mask-size="90"></jaak-stamps>
46
+ <jaak-stamps id="detector" debug="false" mask-size="90" crop-margin="10"></jaak-stamps>
47
47
 
48
48
  <button onclick="startCapture()">Iniciar Captura</button>
49
49
  <button onclick="stopCapture()">Detener</button>
@@ -141,6 +141,7 @@ import { Component, ElementRef, ViewChild, OnInit } from '@angular/core';
141
141
  #detector
142
142
  [debug]="false"
143
143
  [maskSize]="90"
144
+ [cropMargin]="10"
144
145
  (captureCompleted)="onCaptureCompleted($event)"
145
146
  (isReady)="onComponentReady($event)">
146
147
  </jaak-stamps>
@@ -242,7 +243,7 @@ import React, { useRef, useEffect, useImperativeHandle, forwardRef } from 'react
242
243
  // Importar el web component
243
244
  import '@jaak.ai/stamps/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js';
244
245
 
245
- const JaakStampsWrapper = forwardRef(({ debug = false, maskSize = 90, onCaptureCompleted, onIsReady }, ref) => {
246
+ const JaakStampsWrapper = forwardRef(({ debug = false, maskSize = 90, cropMargin = 0, onCaptureCompleted, onIsReady }, ref) => {
246
247
  const elementRef = useRef(null);
247
248
 
248
249
  useImperativeHandle(ref, () => ({
@@ -287,6 +288,7 @@ const JaakStampsWrapper = forwardRef(({ debug = false, maskSize = 90, onCaptureC
287
288
  ref={elementRef}
288
289
  debug={debug}
289
290
  mask-size={maskSize}
291
+ crop-margin={cropMargin}
290
292
  />
291
293
  );
292
294
  });
@@ -367,6 +369,7 @@ function App() {
367
369
  ref={detectorRef}
368
370
  debug={false}
369
371
  maskSize={90}
372
+ cropMargin={10}
370
373
  onCaptureCompleted={handleCaptureCompleted}
371
374
  onIsReady={handleIsReady}
372
375
  />
@@ -439,6 +442,7 @@ export default App;
439
442
  | `debug` | `boolean` | `false` | Activa el modo debug para mostrar información de depuración |
440
443
  | `alignmentTolerance` | `number` | `10` | Tolerancia en píxeles para considerar un lado alineado |
441
444
  | `maskSize` | `number` | `90` | Porcentaje del video que ocupará la máscara de detección (50-100) |
445
+ | `cropMargin` | `number` | `0` | Margen en píxeles agregado al recorte del documento (0-100) |
442
446
 
443
447
  ### Métodos Públicos
444
448
 
@@ -494,18 +498,29 @@ interface Status {
494
498
 
495
499
  ### Flujo Estándar
496
500
  1. **Inicialización**: El componente se prepara para la detección
497
- 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
498
502
  3. **Captura del frente**: El usuario posiciona el documento y se captura automáticamente
499
- 4. **Captura del reverso**: Se solicita voltear el documento para capturar el reverso
500
- 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
501
508
 
502
509
  ### Flujo con Precarga (Recomendado)
503
- 1. **Precarga del modelo**: Se llama a `preloadModel()` para cargar el modelo en memoria
504
- 2. **Inicio optimizado**: Al iniciar la captura, se usa el modelo ya cargado
505
- 3. **Captura del frente**: Detección más rápida con el modelo precargado
506
- 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
507
516
  5. **Completado**: Se emite el evento `captureCompleted` con todas las imágenes
508
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
+
509
524
  ## Requisitos del Sistema
510
525
 
511
526
  - **Navegadores**: Chrome/Edge 88+, Firefox 85+, Safari 14+
@@ -516,7 +531,8 @@ interface Status {
516
531
 
517
532
  - Las imágenes se procesan localmente en el navegador
518
533
  - No se envían datos a servidores externos
519
- - 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
520
536
  - Las imágenes capturadas solo están disponibles en el contexto de tu aplicación
521
537
 
522
538
  ## Desarrollo
@@ -18,7 +18,7 @@ var patchBrowser = () => {
18
18
 
19
19
  patchBrowser().then(async (options) => {
20
20
  await index.globalScripts();
21
- return index.bootstrapLazy([["jaak-stamps.cjs",[[1,"jaak-stamps",{"debug":[4],"alignmentTolerance":[2,"alignment-tolerance"],"maskSize":[2,"mask-size"],"isVideoActive":[32],"statusMessage":[32],"statusColor":[32],"bestScore":[32],"capturedFullFrame":[32],"capturedCroppedId":[32],"capturedBackFullFrame":[32],"capturedBackCroppedId":[32],"captureStep":[32],"isCapturing":[32],"showFlipAnimation":[32],"showSuccessAnimation":[32],"shouldMirrorVideo":[32],"sideAlignment":[32],"isDetectionPaused":[32],"isLoading":[32],"isModelPreloaded":[32],"isMaskReady":[32],"getCapturedImages":[64],"isProcessCompleted":[64],"startCapture":[64],"stopCapture":[64],"resetCapture":[64],"getStatus":[64],"preloadModel":[64],"skipBackCapture":[64]}]]]], options);
21
+ return index.bootstrapLazy([["jaak-stamps.cjs",[[1,"jaak-stamps",{"debug":[4],"alignmentTolerance":[2,"alignment-tolerance"],"maskSize":[2,"mask-size"],"cropMargin":[2,"crop-margin"],"isVideoActive":[32],"statusMessage":[32],"statusColor":[32],"bestScore":[32],"capturedFullFrame":[32],"capturedCroppedId":[32],"capturedBackFullFrame":[32],"capturedBackCroppedId":[32],"captureStep":[32],"isCapturing":[32],"showFlipAnimation":[32],"showSuccessAnimation":[32],"shouldMirrorVideo":[32],"sideAlignment":[32],"isDetectionPaused":[32],"isLoading":[32],"isModelPreloaded":[32],"isMaskReady":[32],"getCapturedImages":[64],"isProcessCompleted":[64],"startCapture":[64],"stopCapture":[64],"resetCapture":[64],"getStatus":[64],"preloadModel":[64],"skipBackCapture":[64]}]]]], options);
22
22
  });
23
23
 
24
24
  exports.setNonce = index.setNonce;
@@ -14,6 +14,7 @@ const JaakStamps = class {
14
14
  debug = false;
15
15
  alignmentTolerance = 10; // Tolerancia en píxeles para considerar un lado alineado (escalado para 320x320)
16
16
  maskSize = 90; // Porcentaje del video que ocupará la máscara (default 90%)
17
+ cropMargin = 0; // Margen en píxeles para el recorte del documento (default 0)
17
18
  captureCompleted;
18
19
  isReady;
19
20
  isVideoActive = false;
@@ -47,6 +48,8 @@ const JaakStamps = class {
47
48
  animationId;
48
49
  hasScreenshotTaken = false;
49
50
  lastDetectedBox;
51
+ mobileNetSession;
52
+ mobileNetClassMap;
50
53
  // Performance optimization properties
51
54
  frameSkipCounter = 0;
52
55
  FRAME_SKIP = 2; // Process every 3rd frame (reduces CPU by ~66%)
@@ -59,7 +62,9 @@ const JaakStamps = class {
59
62
  preprocessCtx;
60
63
  captureCanvas;
61
64
  captureCtx;
62
- 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";
63
68
  INPUT_SIZE = 320;
64
69
  CONFIDENCE_THRESHOLD = 0.6;
65
70
  // ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
@@ -75,6 +80,12 @@ const JaakStamps = class {
75
80
  this.maskSize = 90;
76
81
  }
77
82
  }
83
+ validateCropMargin() {
84
+ if (this.cropMargin < 0 || this.cropMargin > 100) {
85
+ console.warn(`cropMargin debe estar entre 0 y 100. Valor actual: ${this.cropMargin}. Usando valor por defecto: 0`);
86
+ this.cropMargin = 0;
87
+ }
88
+ }
78
89
  emitReadyEvent() {
79
90
  const isDocumentReady = !!window.ort && this.isModelPreloaded;
80
91
  this.isReady.emit(isDocumentReady);
@@ -89,6 +100,7 @@ const JaakStamps = class {
89
100
  }
90
101
  async componentDidLoad() {
91
102
  this.validateMaskSize();
103
+ this.validateCropMargin();
92
104
  if (!window.ort) {
93
105
  const script = document.createElement('script');
94
106
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
@@ -258,25 +270,27 @@ const JaakStamps = class {
258
270
  }
259
271
  try {
260
272
  this.isLoading = true;
261
- this.statusMessage = "Precargando modelo de detección...";
273
+ this.statusMessage = "Precargando modelos...";
262
274
  this.statusColor = "#007bff";
263
275
  const modelPath = this.MODEL_PATH;
264
- this.debugLog('🤖 Preloading model:', modelPath);
276
+ this.debugLog('🤖 Preloading detection model:', modelPath);
265
277
  // Configure ONNX Runtime with device-specific optimizations
266
278
  const sessionOptions = this.getSessionOptions();
267
279
  this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
280
+ // Preload MobileNet model and classes
281
+ await this.loadMobileNetModel();
268
282
  this.isModelPreloaded = true;
269
283
  this.isLoading = false;
270
- this.statusMessage = "Modelo precargado. Listo para comenzar detección";
284
+ this.statusMessage = "Modelos precargados. Listo para comenzar detección";
271
285
  this.statusColor = "#28a745";
272
286
  this.emitReadyEvent();
273
- this.debugLog('✅ Model preloaded successfully');
274
- return { success: true, message: 'Model preloaded successfully' };
287
+ this.debugLog('✅ Models preloaded successfully');
288
+ return { success: true, message: 'Models preloaded successfully' };
275
289
  }
276
290
  catch (error) {
277
- this.debugLog('❌ Error preloading model:', error);
291
+ this.debugLog('❌ Error preloading models:', error);
278
292
  this.isLoading = false;
279
- this.statusMessage = "Error al precargar el modelo";
293
+ this.statusMessage = "Error al precargar los modelos";
280
294
  this.statusColor = "#ff6b6b";
281
295
  return { success: false, error: error.message };
282
296
  }
@@ -286,6 +300,80 @@ const JaakStamps = class {
286
300
  this.completeProcess(true);
287
301
  }
288
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
+ }
289
377
  cleanup() {
290
378
  if (this.animationId) {
291
379
  cancelAnimationFrame(this.animationId);
@@ -313,11 +401,16 @@ const JaakStamps = class {
313
401
  this.captureCtx = undefined;
314
402
  this.captureCanvas = undefined;
315
403
  }
316
- // Disposed ONNX session
404
+ // Disposed ONNX sessions
317
405
  if (this.session) {
318
406
  this.session.release?.();
319
407
  this.session = undefined;
320
408
  }
409
+ if (this.mobileNetSession) {
410
+ this.mobileNetSession.release?.();
411
+ this.mobileNetSession = undefined;
412
+ }
413
+ this.mobileNetClassMap = undefined;
321
414
  this.isModelPreloaded = false;
322
415
  this.debugLog('🧹 Canvas pool cleaned up');
323
416
  }
@@ -448,18 +541,22 @@ const JaakStamps = class {
448
541
  // Check if model is already preloaded
449
542
  if (!this.session) {
450
543
  this.isLoading = true;
451
- this.statusMessage = "Cargando modelo de detección...";
544
+ this.statusMessage = "Cargando modelos...";
452
545
  this.statusColor = "#007bff";
453
546
  const modelPath = this.MODEL_PATH;
454
- this.debugLog('🤖 Loading model:', modelPath);
547
+ this.debugLog('🤖 Loading detection model:', modelPath);
455
548
  const sessionOptions = this.getSessionOptions();
456
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
+ }
457
554
  this.isModelPreloaded = true;
458
555
  this.emitReadyEvent();
459
556
  }
460
557
  else {
461
- this.debugLog('🚀 Using preloaded model');
462
- this.statusMessage = "Usando modelo precargado...";
558
+ this.debugLog('🚀 Using preloaded models');
559
+ this.statusMessage = "Usando modelos precargados...";
463
560
  this.statusColor = "#007bff";
464
561
  }
465
562
  this.statusMessage = "Configurando cámara...";
@@ -678,7 +775,9 @@ const JaakStamps = class {
678
775
  corners?.forEach(corner => corner.classList.add('perfect-match'));
679
776
  if (!this.hasScreenshotTaken) {
680
777
  this.lastDetectedBox = bestBox;
681
- this.takeScreenshot();
778
+ this.takeScreenshot().catch(error => {
779
+ this.debugLog('❌ Error taking screenshot:', error);
780
+ });
682
781
  this.hasScreenshotTaken = true;
683
782
  // Reset para permitir segunda captura
684
783
  setTimeout(() => {
@@ -761,7 +860,7 @@ const JaakStamps = class {
761
860
  ctx.strokeRect(x, y, w, h);
762
861
  });
763
862
  }
764
- takeScreenshot() {
863
+ async takeScreenshot() {
765
864
  if (!this.videoRef || !this.lastDetectedBox)
766
865
  return;
767
866
  // Activar animación
@@ -777,10 +876,10 @@ const JaakStamps = class {
777
876
  // Calcular las coordenadas de recorte basadas en la detección
778
877
  const scaleX = this.videoRef.videoWidth / this.INPUT_SIZE;
779
878
  const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
780
- const cropX = Math.max(0, this.lastDetectedBox.x * scaleX);
781
- const cropY = Math.max(0, this.lastDetectedBox.y * scaleY);
782
- const cropWidth = Math.min(this.lastDetectedBox.w * scaleX, this.videoRef.videoWidth - cropX);
783
- const cropHeight = Math.min(this.lastDetectedBox.h * scaleY, this.videoRef.videoHeight - cropY);
879
+ const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
880
+ const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
881
+ const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
882
+ const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
784
883
  // OPTIMIZATION: Create temporary canvas only for cropped version
785
884
  // (We reuse main capture canvas for full frame)
786
885
  const croppedCanvas = document.createElement('canvas');
@@ -795,6 +894,15 @@ const JaakStamps = class {
795
894
  // Captura del frente usando canvas reutilizado
796
895
  this.capturedFullFrame = this.captureCanvas.toDataURL('image/png');
797
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)
798
906
  this.captureStep = 'back';
799
907
  this.statusMessage = "Voltee la identificación y muestre la parte trasera";
800
908
  this.statusColor = "#007bff";
@@ -924,7 +1032,7 @@ const JaakStamps = class {
924
1032
  this.cleanup();
925
1033
  }
926
1034
  render() {
927
- return (index.h("div", { key: 'd812c8d6a607b4f2889be8f84384156f67ca51e3', class: "detector-container" }, index.h("div", { key: '215b581f8ebafa9295133fe397f4dcaea952e2bc', class: "video-container" }, index.h("video", { key: '2ac492b3c535857d8ee681dc0bfb91e6de25a4d0', 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: 'e51c2e4a4751d89cc662a26eff3d8360fa199f4b', ref: el => this.canvasRef = el, class: this.shouldMirrorVideo ? 'mirror' : '' }), this.isMaskReady && (index.h("div", { key: '2e0bb7df390e6286a758098abf929ce23e0820f2', class: "overlay-mask" }, index.h("div", { key: '3a8c73cef589adcf98991719e0adc0791cf51e13', class: "card-outline" }, index.h("div", { key: '908bdd704ff89f8ba9f2f30da068d2d2074f43ed', class: "side side-top" }), index.h("div", { key: 'd27227f2e03905d288cb5509cb4f8b98a5ade59e', class: "side side-right" }), index.h("div", { key: '7bdce198e7ff2dc459768b001edd3c52b0cc28a2', class: "side side-bottom" }), index.h("div", { key: '9f3c97b8d154a7e48b36accac1eb7e33ee2b3184', class: "side side-left" }), index.h("div", { key: '2ec0bfd6308505d8780453a22aeb9b25108b8a14', class: "corner corner-tl" }), index.h("div", { key: 'd6d1589901a1a2cba38451b31126524ccc009a9b', class: "corner corner-tr" }), index.h("div", { key: 'db4ddc071c5b29d2d5912f7a880cdf8654c0db70', class: "corner corner-bl" }), index.h("div", { key: 'cf9cf5d60189e512a5f4bfc08e24d2bf9ed3a499', class: "corner corner-br" }), !this.showFlipAnimation && !this.showSuccessAnimation && (index.h("div", { key: '0412e6f6ecca1af59710256a27d9ceb7b11c5998', class: "guide-text" }, this.statusMessage))), this.captureStep === 'back' && !this.showFlipAnimation && !this.showSuccessAnimation && (index.h("button", { key: '26bc516d80fda38ea94926e574812362cd77a816', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")))), this.isCapturing && (index.h("div", { key: '49c64155438f74dc3e0c5e981dac62844315180d', class: "capture-animation" })), this.showFlipAnimation && (index.h("div", { key: '738adb391c667de96a0c30750e75de43803ae613', class: "flip-animation" }, index.h("div", { key: '9df006a94a68b9d6e4dd351fb0b88cf78e62b2ec', class: "id-card-icon" }), index.h("div", { key: '3e1736f4aeb5b14a3edc4f52991fc34ff85169ad', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), this.showSuccessAnimation && (index.h("div", { key: 'dc46b715ef3e4fb98e5ee3f8449520db6bb68e53', class: "success-animation" }, index.h("div", { key: '641dda07b6545e9ca391e8c6149c7e29251d68ae', class: "check-icon" }), index.h("div", { key: 'ea53a4d547be89cc0e79d5b616a289405fea1e75', class: "success-text" }, "\u00A1Proceso completado!"))), this.isLoading && (index.h("div", { key: 'bf30969faeb9b6bee4099d2462b6abeee7eca1ff', class: "loading-overlay" }, index.h("div", { key: 'c32d185c1376975ac20640c532b0f4e593caf0c0', class: "loading-spinner" }), index.h("div", { key: '2c5a64af6abd379f247640498dafa01efe661c5c', class: "loading-text" }, this.statusMessage))), index.h("div", { key: 'd16122cf20d25e51dc7a3c91753f541f41c2747e', class: "watermark" }, index.h("img", { key: 'ce18db8a5997dfa6e2ebae47b9a61b665f8106bd', 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" })))));
928
1036
  }
929
1037
  };
930
1038
  JaakStamps.style = myComponentCss;