@jaak.ai/stamps 2.1.0-dev.1 → 2.1.0-dev.11

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.
Files changed (56) hide show
  1. package/README.md +11 -1
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  3. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  4. package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
  5. package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
  6. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
  7. package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
  8. package/dist/cjs/jaak-stamps.cjs.entry.js +644 -60
  9. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  10. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  11. package/dist/cjs/loader.cjs.js +4 -3
  12. package/dist/cjs/loader.cjs.js.map +1 -1
  13. package/dist/collection/collection-manifest.json +1 -1
  14. package/dist/collection/components/my-component/my-component.css +210 -11
  15. package/dist/collection/components/my-component/my-component.js +618 -58
  16. package/dist/collection/components/my-component/my-component.js.map +1 -1
  17. package/dist/collection/services/DetectionService.js +48 -5
  18. package/dist/collection/services/DetectionService.js.map +1 -1
  19. package/dist/collection/services/ServiceContainer.js +3 -2
  20. package/dist/collection/services/ServiceContainer.js.map +1 -1
  21. package/dist/components/index.js +165 -113
  22. package/dist/components/index.js.map +1 -1
  23. package/dist/components/jaak-stamps.js +650 -60
  24. package/dist/components/jaak-stamps.js.map +1 -1
  25. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  26. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  27. package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
  28. package/dist/esm/index-Drbzcuq-.js.map +1 -0
  29. package/dist/esm/jaak-stamps-webcomponent.js +5 -4
  30. package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
  31. package/dist/esm/jaak-stamps.entry.js +644 -60
  32. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  33. package/dist/esm/loader.js +4 -3
  34. package/dist/esm/loader.js.map +1 -1
  35. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  36. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
  37. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  38. package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
  39. package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js +2 -0
  40. package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js.map +1 -0
  41. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
  42. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
  43. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
  44. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
  45. package/dist/types/components/my-component/my-component.d.ts +25 -0
  46. package/dist/types/components.d.ts +8 -0
  47. package/dist/types/services/DetectionService.d.ts +2 -1
  48. package/dist/types/services/ServiceContainer.d.ts +1 -0
  49. package/dist/types/stencil-public-runtime.d.ts +12 -5
  50. package/package.json +3 -1
  51. package/dist/cjs/index-BfhtOB0D.js.map +0 -1
  52. package/dist/esm/index-BP1Q4KOg.js.map +0 -1
  53. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js +0 -2
  54. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js.map +0 -1
  55. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
  56. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +0 -1
@@ -7,6 +7,7 @@ export class JaakStamps {
7
7
  maskSize = 90;
8
8
  cropMargin = 20;
9
9
  useDocumentClassification = false;
10
+ useDocumentDetector = true;
10
11
  preferredCamera = 'auto';
11
12
  captureDelay = 1500;
12
13
  enableBackDocumentTimer = false;
@@ -47,6 +48,11 @@ export class JaakStamps {
47
48
  detectionRate: 0
48
49
  };
49
50
  backDocumentTimerRemaining = 0;
51
+ showManualCaptureButton = false;
52
+ performanceDegradedMode = false; // Auto-switched to manual mode due to performance
53
+ showPerformanceMessage = false; // Show performance message temporarily
54
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
55
+ processingButton = null; // Track which button is processing
50
56
  // Services
51
57
  serviceContainer;
52
58
  eventBus;
@@ -65,12 +71,12 @@ export class JaakStamps {
65
71
  alignmentStartTime;
66
72
  alignmentTimer;
67
73
  backDocumentTimer;
74
+ orientationTimer;
68
75
  // Performance monitoring
69
76
  performanceMetrics = {
70
77
  fps: 0,
71
78
  inferenceTime: 0,
72
79
  memoryUsage: 0,
73
- cpuUsage: 0,
74
80
  onnxLoadTime: 0,
75
81
  frameProcessingTime: 0,
76
82
  totalDetections: 0,
@@ -89,18 +95,29 @@ export class JaakStamps {
89
95
  MIN_INFERENCE_INTERVAL = 50;
90
96
  performanceHistory = [];
91
97
  PERFORMANCE_HISTORY_SIZE = 10;
98
+ // Sistema simplificado de monitoreo de rendimiento
99
+ PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
100
+ SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
101
+ slowFrameCount = 0; // Contador de frames consecutivos lentos
102
+ processedFramesCount = 0; // Contador total de frames procesados
103
+ hasAutoSwitchedToManual = false;
92
104
  // Canvas pool for optimized screenshot capture
93
105
  canvasPool = [];
94
106
  MAX_CANVAS_POOL_SIZE = 3;
95
107
  async componentDidLoad() {
96
- this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
97
- await this.initializeServices();
98
- this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
99
- await this.setupEventListeners();
100
- this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
101
- await this.initializeComponent();
102
- if (this.debug) {
103
- this.initializePerformanceMonitor();
108
+ try {
109
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
110
+ await this.initializeServices();
111
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
112
+ await this.setupEventListeners();
113
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
114
+ await this.initializeComponent();
115
+ if (this.debug) {
116
+ this.initializePerformanceMonitor();
117
+ }
118
+ }
119
+ catch (error) {
120
+ this.updateStatus('Error de inicialización', error.message, 'error');
104
121
  }
105
122
  }
106
123
  async initializeServices() {
@@ -110,6 +127,7 @@ export class JaakStamps {
110
127
  maskSize: this.maskSize,
111
128
  cropMargin: this.cropMargin,
112
129
  useDocumentClassification: this.useDocumentClassification,
130
+ useDocumentDetector: this.useDocumentDetector,
113
131
  preferredCamera: this.preferredCamera,
114
132
  captureDelay: this.captureDelay
115
133
  };
@@ -120,14 +138,8 @@ export class JaakStamps {
120
138
  this.detectionService = this.serviceContainer.getDetectionService();
121
139
  }
122
140
  async setupEventListeners() {
123
- this.eventBus.on('state-changed', (data) => {
124
- this.handleStateChange(data);
125
- });
126
- this.eventBus.on('camera-changed', () => {
127
- // Camera changed event
128
- });
129
- this.eventBus.on('error', () => {
130
- // Handle service errors
141
+ this.eventBus.on('state-changed', () => {
142
+ this.handleStateChange();
131
143
  });
132
144
  }
133
145
  async initializeComponent() {
@@ -167,27 +179,39 @@ export class JaakStamps {
167
179
  document.head.appendChild(script);
168
180
  await new Promise((resolve) => {
169
181
  script.onload = () => {
170
- setTimeout(() => {
171
- this.finalizeInitialization();
182
+ setTimeout(async () => {
183
+ await this.finalizeInitialization();
172
184
  resolve(undefined);
173
185
  }, 300);
174
186
  };
175
187
  });
176
188
  }
177
189
  else {
178
- setTimeout(() => {
179
- this.finalizeInitialization();
190
+ setTimeout(async () => {
191
+ await this.finalizeInitialization();
180
192
  }, 300);
181
193
  }
182
194
  }
183
- finalizeInitialization() {
195
+ async finalizeInitialization() {
184
196
  this.stateManager.updateCaptureState({ isLoading: false });
185
- this.currentStatus = {
186
- message: 'Listo para capturar',
187
- description: '',
188
- type: 'ready',
189
- isInitialized: true
190
- };
197
+ // Check camera permissions before showing "ready" status
198
+ const hasPermissions = await this.checkCameraPermissions();
199
+ if (hasPermissions) {
200
+ this.currentStatus = {
201
+ message: 'Listo para capturar',
202
+ description: '',
203
+ type: 'ready',
204
+ isInitialized: true
205
+ };
206
+ }
207
+ else {
208
+ this.currentStatus = {
209
+ message: 'Permisos de cámara requeridos',
210
+ description: 'Por favor, otorgue permisos de cámara para continuar',
211
+ type: 'error',
212
+ isInitialized: true
213
+ };
214
+ }
191
215
  this.emitReadyEvent();
192
216
  }
193
217
  updateStatus(message, description, type = 'loading') {
@@ -202,14 +226,29 @@ export class JaakStamps {
202
226
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
203
227
  this.isReady.emit(isDocumentReady);
204
228
  }
205
- handleStateChange(data) {
206
- // React to state changes from StateManager
207
- // This is where the component updates its internal state based on service state changes
208
- if (data.changes.isLoading !== undefined) {
209
- // Force re-render when loading state changes
210
- // Note: Stencil automatically re-renders when @State changes
229
+ async checkCameraPermissions() {
230
+ try {
231
+ if (!navigator.permissions) {
232
+ // Fallback: try to access camera directly
233
+ try {
234
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
235
+ stream.getTracks().forEach(track => track.stop());
236
+ return true;
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ }
242
+ const permission = await navigator.permissions.query({ name: 'camera' });
243
+ return permission.state === 'granted';
244
+ }
245
+ catch (error) {
246
+ return false;
211
247
  }
212
248
  }
249
+ handleStateChange() {
250
+ // Handle state changes from StateManager if needed
251
+ }
213
252
  initializeResizeObserver() {
214
253
  if ('ResizeObserver' in window && this.detectionContainer) {
215
254
  const resizeObserver = new ResizeObserver(() => {
@@ -217,6 +256,18 @@ export class JaakStamps {
217
256
  });
218
257
  resizeObserver.observe(this.detectionContainer.parentElement);
219
258
  }
259
+ // Handle mobile device orientation changes
260
+ window.addEventListener('orientationchange', () => {
261
+ // Clear any existing orientation timer
262
+ if (this.orientationTimer) {
263
+ clearTimeout(this.orientationTimer);
264
+ }
265
+ // Add delay to ensure video dimensions are updated after rotation
266
+ this.orientationTimer = window.setTimeout(() => {
267
+ this.handleResize();
268
+ this.orientationTimer = undefined;
269
+ }, 300);
270
+ });
220
271
  }
221
272
  handleResize() {
222
273
  if (this.detectionContainer) {
@@ -225,6 +276,36 @@ export class JaakStamps {
225
276
  this.updateMaskDimensions(rect);
226
277
  }
227
278
  }
279
+ triggerRerender() {
280
+ this.captureStateVersion = this.captureStateVersion + 1;
281
+ }
282
+ getGuideText(step) {
283
+ if (step === 'completed') {
284
+ return 'Proceso completado';
285
+ }
286
+ // Check if we should show performance message
287
+ if (this.showPerformanceMessage && step === 'front') {
288
+ return 'Modo manual activado por rendimiento. Use el botón para capturar.';
289
+ }
290
+ if (step === 'front') {
291
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
292
+ if (this.showManualCaptureButton) {
293
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
294
+ }
295
+ // Instrucción básica de alineación
296
+ return 'Alinee su identificación con el marco';
297
+ }
298
+ else if (step === 'back') {
299
+ // Para el reverso, mostrar instrucciones específicas según el modo
300
+ if (this.useDocumentDetector) {
301
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
302
+ }
303
+ else {
304
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
305
+ }
306
+ }
307
+ return 'Alinee su identificación con el marco';
308
+ }
228
309
  updateMaskDimensions(containerRect) {
229
310
  if (!this.videoRef)
230
311
  return;
@@ -363,8 +444,14 @@ export class JaakStamps {
363
444
  }
364
445
  }
365
446
  async skipBackCapture() {
447
+ console.log('🟡 Botón clicked: Saltar reverso');
448
+ // Set processing state immediately
449
+ this.processingButton = 'skip-back';
450
+ // Add small delay to show spinner
451
+ await new Promise(resolve => setTimeout(resolve, 100));
366
452
  const readyCheck = this.isComponentReady();
367
453
  if (!readyCheck.ready) {
454
+ this.processingButton = null;
368
455
  return { success: false, error: readyCheck.message };
369
456
  }
370
457
  try {
@@ -381,6 +468,10 @@ export class JaakStamps {
381
468
  catch (error) {
382
469
  return { success: false, error: error.message };
383
470
  }
471
+ finally {
472
+ // Always clear processing state when done
473
+ this.processingButton = null;
474
+ }
384
475
  }
385
476
  async getStatus() {
386
477
  const readyCheck = this.isComponentReady();
@@ -435,9 +526,21 @@ export class JaakStamps {
435
526
  if (this.stateManager) {
436
527
  this.stateManager.updateCaptureState({ isLoading: true });
437
528
  }
438
- await this.detectionService.loadModel();
529
+ try {
530
+ await this.detectionService.loadModel();
531
+ }
532
+ catch (modelError) {
533
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
534
+ throw modelError;
535
+ }
439
536
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
440
- await this.detectionService.loadClassificationModel();
537
+ try {
538
+ await this.detectionService.loadClassificationModel();
539
+ }
540
+ catch (classificationError) {
541
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
542
+ throw classificationError;
543
+ }
441
544
  const loadEndTime = performance.now();
442
545
  const loadTime = loadEndTime - loadStartTime;
443
546
  // Record ONNX load time
@@ -546,17 +649,63 @@ export class JaakStamps {
546
649
  // Always allow getting capture delay, even during initialization
547
650
  return this.captureDelay;
548
651
  }
652
+ // Memory and device capability detection
653
+ checkDeviceCapabilities() {
654
+ const deviceMemory = navigator.deviceMemory;
655
+ // Siempre intentar usar ML, independientemente del dispositivo
656
+ // Las restricciones anteriores no eran confiables
657
+ return {
658
+ canUseML: true,
659
+ reason: 'Dispositivo compatible con detección automática.',
660
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
661
+ };
662
+ }
549
663
  // DETECTION METHODS
550
664
  async startDetection() {
551
665
  try {
666
+ // Verificar capacidades del dispositivo antes de cargar modelos
667
+ const capabilities = this.checkDeviceCapabilities();
668
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
669
+ if (!capabilities.canUseML && this.useDocumentDetector) {
670
+ this.switchToManualModeWithWarning(capabilities.reason);
671
+ return;
672
+ }
552
673
  // Paso 1: Verificar y cargar modelos si es necesario
553
674
  if (!this.detectionService.isModelLoaded()) {
554
675
  const loadStartTime = performance.now();
555
676
  this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
556
677
  this.stateManager.updateCaptureState({ isLoading: true });
557
- await this.detectionService.loadModel();
678
+ try {
679
+ await this.detectionService.loadModel();
680
+ }
681
+ catch (modelError) {
682
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
683
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
684
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
685
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
686
+ return;
687
+ }
688
+ else {
689
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
690
+ throw modelError;
691
+ }
692
+ }
558
693
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
559
- await this.detectionService.loadClassificationModel();
694
+ try {
695
+ await this.detectionService.loadClassificationModel();
696
+ }
697
+ catch (classificationError) {
698
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
699
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
700
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
701
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
702
+ return;
703
+ }
704
+ else {
705
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
706
+ throw classificationError;
707
+ }
708
+ }
560
709
  const loadEndTime = performance.now();
561
710
  const loadTime = loadEndTime - loadStartTime;
562
711
  // Record ONNX load time
@@ -566,14 +715,40 @@ export class JaakStamps {
566
715
  }
567
716
  // Paso 2: Detectar y configurar dispositivos
568
717
  this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
569
- await this.cameraService.enumerateDevices();
570
- await this.updateCameraInfoWithAutofocus();
718
+ try {
719
+ await this.cameraService.enumerateDevices();
720
+ }
721
+ catch (enumerateError) {
722
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
723
+ }
724
+ try {
725
+ await this.updateCameraInfoWithAutofocus();
726
+ }
727
+ catch (cameraInfoError) {
728
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
729
+ }
571
730
  // Paso 3: Configurar cámara seleccionada
572
731
  this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
573
- const stream = await this.cameraService.setupCamera();
574
- // Paso 4: Inicializar video y ocultar estado inmediatamente
575
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
576
- await this.initializeVideoStream(stream);
732
+ let stream;
733
+ try {
734
+ stream = await this.cameraService.setupCamera();
735
+ }
736
+ catch (setupError) {
737
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
738
+ }
739
+ // Paso 4: Inicializar video y mostrar estado de análisis inicial
740
+ if (this.useDocumentDetector) {
741
+ this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
742
+ }
743
+ else {
744
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
745
+ }
746
+ try {
747
+ await this.initializeVideoStream(stream);
748
+ }
749
+ catch (videoError) {
750
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
751
+ }
577
752
  // Paso 5: Calibrar máscara
578
753
  if (this.detectionContainer) {
579
754
  const container = this.detectionContainer.parentElement;
@@ -583,6 +758,10 @@ export class JaakStamps {
583
758
  // Finalizar e iniciar captura
584
759
  this.startTime = Date.now();
585
760
  this.stateManager.updateCaptureState({ isLoading: false });
761
+ // Mostrar botón manual si el detector está deshabilitado
762
+ if (!this.useDocumentDetector) {
763
+ this.showManualCaptureButton = true;
764
+ }
586
765
  this.detectFrame();
587
766
  }
588
767
  catch (err) {
@@ -600,6 +779,12 @@ export class JaakStamps {
600
779
  this.videoRef.onloadedmetadata = async () => {
601
780
  await this.videoRef.play();
602
781
  this.stateManager.updateCaptureState({ isVideoActive: true });
782
+ // Recalculate mask dimensions when new camera stream loads
783
+ if (this.detectionContainer) {
784
+ const container = this.detectionContainer.parentElement;
785
+ const rect = container.getBoundingClientRect();
786
+ this.updateMaskDimensions(rect);
787
+ }
603
788
  resolve();
604
789
  };
605
790
  });
@@ -614,6 +799,18 @@ export class JaakStamps {
614
799
  const captureState = this.stateManager.getCaptureState();
615
800
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
616
801
  return;
802
+ // Show manual capture button when document detector is disabled or performance is degraded
803
+ if (!this.useDocumentDetector || this.performanceDegradedMode) {
804
+ this.showManualCaptureButton = true;
805
+ // Continue the loop for UI updates but skip detection logic
806
+ if (captureState.step !== 'completed') {
807
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
808
+ }
809
+ return;
810
+ }
811
+ else if (!this.performanceDegradedMode) {
812
+ this.showManualCaptureButton = false;
813
+ }
617
814
  if (captureState.isDetectionPaused) {
618
815
  if (captureState.step !== 'completed') {
619
816
  this.animationId = requestAnimationFrame(() => this.detectFrame());
@@ -641,10 +838,27 @@ export class JaakStamps {
641
838
  this.lastInferenceTime = currentTime;
642
839
  // Measure preprocessing and inference time
643
840
  const inferenceStartTime = performance.now();
644
- const inputTensor = this.detectionService.preprocess(this.videoRef);
645
- // Standard detection without quality validation
646
- const detections = await this.detectionService.runInference(inputTensor);
647
- const inferenceTime = performance.now() - inferenceStartTime;
841
+ let inputTensor;
842
+ let detections;
843
+ let inferenceTime;
844
+ try {
845
+ inputTensor = this.detectionService.preprocess(this.videoRef);
846
+ }
847
+ catch (preprocessError) {
848
+ this.switchToManualModeWithError('Error al procesar imagen');
849
+ return;
850
+ }
851
+ try {
852
+ // Standard detection without timeout (timeout is handled by frame counting)
853
+ detections = await this.detectionService.runInference(inputTensor);
854
+ inferenceTime = performance.now() - inferenceStartTime;
855
+ }
856
+ catch (inferenceError) {
857
+ this.switchToManualModeWithError('Error en detección automática');
858
+ return;
859
+ }
860
+ // Increment frame count
861
+ this.processedFramesCount++;
648
862
  // Record ONNX performance metrics
649
863
  if (this.debug) {
650
864
  this.recordOnnxPerformance(0, inferenceTime);
@@ -683,9 +897,30 @@ export class JaakStamps {
683
897
  }
684
898
  this.updateMaskColor(detections);
685
899
  // Record frame processing metrics
900
+ const frameEndTime = performance.now();
901
+ const totalFrameTime = frameEndTime - frameStartTime;
902
+ // Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
903
+ if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
904
+ // Verificar si el frame es lento
905
+ if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
906
+ this.slowFrameCount++;
907
+ // Si tenemos suficientes frames lentos consecutivos, cambiar a manual
908
+ if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
909
+ this.switchToManualMode();
910
+ }
911
+ }
912
+ else {
913
+ // Frame rápido, reiniciar contador
914
+ if (this.slowFrameCount > 0) {
915
+ this.slowFrameCount = 0;
916
+ }
917
+ }
918
+ // Cambiar mensaje de estado después de analizar los primeros 2 frames
919
+ if (this.processedFramesCount === 2) {
920
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
921
+ }
922
+ }
686
923
  if (this.debug) {
687
- const frameEndTime = performance.now();
688
- const totalFrameTime = frameEndTime - frameStartTime;
689
924
  this.recordFrameProcessing(totalFrameTime, detections.length);
690
925
  }
691
926
  // Continue detection loop
@@ -721,6 +956,12 @@ export class JaakStamps {
721
956
  clearTimeout(this.alignmentTimer);
722
957
  this.alignmentTimer = undefined;
723
958
  }
959
+ if (this.orientationTimer) {
960
+ clearTimeout(this.orientationTimer);
961
+ this.orientationTimer = undefined;
962
+ }
963
+ // Liberar recursos de ONNX al limpiar el componente
964
+ this.releaseOnnxResources();
724
965
  if (this.performanceUpdateInterval) {
725
966
  clearInterval(this.performanceUpdateInterval);
726
967
  this.performanceUpdateInterval = undefined;
@@ -742,7 +983,7 @@ export class JaakStamps {
742
983
  isCapturing: false
743
984
  };
744
985
  const cameraInfo = this.cameraInfoWithAutofocus;
745
- return (h("div", { key: '69a8df7969288e19088e8809f1fefcc89b237cda', class: "detector-container" }, h("div", { key: '79e59a315d266be9d4fb5eed5740e5ad825d5890', class: "video-container" }, h("video", { key: 'a3312788c739748b774a25acb30c2dcea7f01695', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: 'ddab107500c2dab24c140049bef0b44aafc35dc7', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
986
+ return (h("div", { key: 'dda3e3054a18dac59fd6dab4bf722f405d1c1b0f', class: "detector-container" }, h("div", { key: 'c260bf52307e072a634cddcc273c1f55037d01ef', class: "video-container" }, h("video", { key: '5f20ce9d95c5ece7ab90a2da30f28034b86a57a0', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '6afd8a46c16592c67e6bb93f1655de1660338f30', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
746
987
  position: 'absolute',
747
988
  left: `${box.x}px`,
748
989
  top: `${box.y}px`,
@@ -751,9 +992,9 @@ export class JaakStamps {
751
992
  border: '2px solid #32406C',
752
993
  pointerEvents: 'none',
753
994
  boxSizing: 'border-box'
754
- } })))), this.isMaskReady && (h("div", { key: '370337089a3de949b8cb4ae84dc0037e6044b2d5', class: "overlay-mask" }, h("div", { key: 'cb35c2e42bf1729ad02a8fe7e7abaf27cf1a8d7a', class: "card-outline" }, h("div", { key: '637d72ada63805e7f25051cd1ad9093030cb025a', class: "side side-top" }), h("div", { key: 'c7a465aa686afa41f80433d949715b40f6f349dc', class: "side side-right" }), h("div", { key: '384aade0610b5bc7bdd7db350a70d82f9bebd8ab', class: "side side-bottom" }), h("div", { key: '5506cc346f3fd718e3fda4365f69ea780304b6d6', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'f5b9b62937eabab7c096b7417a13bbf6f822ee8a', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '74b122125a42839d2600cf99fb855140042ab761', class: "skip-section" }, h("div", { key: '0b4a39d1599854d6c445e2c35dcc89a0e4970846', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: 'b52b47df390cc099c8847ca8a676270bb154294d', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
995
+ } })))), this.isMaskReady && (h("div", { key: '178f2022bed334f8c9c8c866a92b44c8cb0369b1', class: "overlay-mask" }, h("div", { key: 'acb1bc996409095afd29ffa5a038c3eeae6c7ed9', class: "card-outline" }, h("div", { key: 'dcc952d9011c5fd13b229f3a759cc74721a1ee11', class: "side side-top" }), h("div", { key: 'addb25f6c41ad10a09b6918e6357052294d14adf', class: "side side-right" }), h("div", { key: 'a1ec08f3bf1b16731a224c034c76dd8c6412597b', class: "side side-bottom" }), h("div", { key: '22f3c158f2c2bb1dc582f796c39ea60bc11505dd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '5b150f7df5b0c5402e42bae4601936d4e5d1f5d4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '17b0a81437eb8691cb4dc5042c72bda01ac92be0', class: "back-capture-section" }, h("div", { key: 'a65ccbd5f53b06ec340b36a0a2348cf04920b3df', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (h("button", { key: '383d7398079dbb8b1adfe695407cc598059b2a94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (h("span", { key: '86bddcfeb6f90da9b18926f4c6e1208296502dcf', class: "button-spinner" })), "Capturar Reverso")), h("button", { key: '5c9d116cbb9b5bc16481b2b90c8451182ad436c5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (h("span", { key: '881f00cfc3a24167bc6b8da5849cb404101b692d', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
755
996
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
756
- : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'f04e47749a25197c434b849844c67eadca5c6af3', class: "camera-controls" }, h("button", { key: '82625eef057f59b6dac11c4443ccb315231eac7f', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: '61cfb89cd9bb549cfff2cb70293db06ee6cdf06b', class: "camera-selector-dropdown" }, h("div", { key: '60f751d8db7b305f2bdfdc089d2110bcff940150', class: "camera-selector-header" }, h("span", { key: '9dae045a34cc06f323ff4850921b495fc845be47' }, "Seleccionar C\u00E1mara"), h("button", { key: '4584d7964d91ddd7f3d980c3e5e90493530d33bd', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '9881f2b4fc6ffbdc76db06a7b49d060cefbe6277', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '3dfe9ffc7b4cc9afb199a78026cef6d4b469805e', class: "device-info" }, h("small", { key: 'bddc865a30469bd2724624b2f7c8f641f9d97a07' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2fd963e44f69f80d17c6bbbfa3c9b5e11bc816be', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'ba4872ea5388a0ac3134be61feff5412373465ab', class: "flip-animation" }, h("div", { key: 'e83a92cea79f3e85aefc6964c5d9de7b495d511e', class: "id-card-icon" }), h("div", { key: '1d06b51fd6ad14d9b37aa6bca72bd13c951a386a', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '503c377896d5fdc3ae8476b09f511d571f23ae72', class: "success-animation" }, h("div", { key: 'e1929c245b0d1479a6fb1513b0b37c53070c4291', class: "check-icon" }), h("div", { key: '024c897c70427b205390770fc3dfb999ffcc84d3', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '54e350707628e5c88ae2900e412e6bda2eace943', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '82ea10569ed46285686cf3e63885ca1941bb559c', class: "status-spinner" })), h("div", { key: '1fecf179c10965cccf1be3dc0a54afd377ba38ad', class: "status-content" }, h("div", { key: '1b301c1bbb02e5cac2e6babcb20c190662d45c68', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: '5b6c61308d17176daade02c951a436d114a74100', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'b8104db64208dc87a617a072957c007f28055194', class: "performance-monitor" }, h("div", { key: 'e66aec071f10821be2713858dcc251c04f480e63', class: "performance-expanded" }, h("div", { key: '1ae09a91d0704ab21b9eb51097cf452fd5f44359', class: "metrics-row" }, h("div", { key: '340d4d16d048a5217fe13d5de63618c9ea221165', class: "metric-compact" }, h("span", { key: '20cfda14969ef70d232edd68f2e9902bd44b5d9a', class: "metric-label" }, "FPS"), h("span", { key: '3a089c2814a71ed6513bf05b0120a66aa159daee', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '23f700def988c3220fc5df37aeb57a0a20ff56b3', class: "metric-compact" }, h("span", { key: '441e8490a871469813a6d832114e03583d375e91', class: "metric-label" }, "MEM"), h("span", { key: '51f7c49164c9d5e699141c92c88375fc99b312ed', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '6547fee80e2a1db7c9ff889a1c478fc8248159e6', class: "metrics-row" }, h("div", { key: '7b2d1642fd122ee0fbed36387afb23152afe10c2', class: "metric-compact" }, h("span", { key: 'a9137119bf7da366f18feaf8f28807ee40ad7a25', class: "metric-label" }, "INF"), h("span", { key: '5cbc0c040102688f1039cdcd4bb58020248cf700', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'cb4a86f5da46e4081d997c8b8bb6a899f06dfcd2', class: "metric-compact" }, h("span", { key: '88ad5e1d842eb156cf6813ecaa9d2bec76a5999e', class: "metric-label" }, "FRAME"), h("span", { key: 'a9b14cd98b64dcfc058d3b68a3c168a9e092fb3c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '86e01f3596682cb8ad2ff5464748e6ecb7db374a', class: "metrics-row" }, h("div", { key: 'e73b9d39e9bcbc5d46fa0ba0a737700e3e5566d9', class: "metric-compact" }, h("span", { key: '8ecfe969dfe77c87d9da76d7390706014b4872f3', class: "metric-label" }, "DET"), h("span", { key: 'bd1a6a52e0bd1fb4d377413d7515e4e0f00da10f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '36539ae98787fd60642082db1bf89031003c4bef', class: "metric-compact" }, h("span", { key: '7f3a81629c09053e3beeca97f485da0f60412052', class: "metric-label" }, "RATE"), h("span", { key: '72399e0ac39270c82b265ce64e402f84cda9fb2c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '4e3a914ba2f5993f85de7b9dd6dc93d76b41c96d', class: "watermark" }, h("img", { key: 'aa634c2ba2090f64ee9b41aa1d75a9114d46fd63', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
997
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'd929fc0eeeeee944f3d722fde98c52642443e9a5', class: "camera-controls" }, h("button", { key: '1afcfae9d4a23aaede153f27ba6895a33182ada1', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: 'f37feefd9def935417467dffb52b9fe8d8acea79', class: "camera-selector-dropdown" }, h("div", { key: 'a8479ec6a6c4076b5dc40677e1988295100e4d0d', class: "camera-selector-header" }, h("span", { key: '074f02967354293a1e5df4e3ac8a126faee33d04' }, "Seleccionar C\u00E1mara"), h("button", { key: 'd6ba0252fb269c3a04967fb0e5c075166f202d71', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '63f5f9be2b05da71f4fe5a4e38b11ab3e39fb1e0', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '4ca46fdebe0eaa078f1dae4d2f1af03321577c7f', class: "device-info" }, h("small", { key: '10c54ddc5a196b11a69077b045119187ed09f4be' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '25393018948798f4d9d5f44738e61c7a0a6be990', class: "manual-capture-section" }, h("button", { key: '7a6c8686c0db3e352f9689bc41f7fd3f612c800c', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (h("span", { key: '1916b97da3f8dce7d06b86b21f0361dd7ce10b95', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (h("div", { key: 'aed05a3280dc96121564ffea87945f441d4286f6', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '63d262e5e10325d145fada8e91a878c47305f2b9', class: "flip-animation" }, h("div", { key: '2a3699f47e595c5e4ab00dfd11e1e66ab2e13039', class: "id-card-icon" }), h("div", { key: 'df195fe9a6db4ec3e14b04b4e932d55adb7c3f3c', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '670341c302a31c8017a9083ebf35e2988122ac3f', class: "success-animation" }, h("div", { key: '27b0b0d91de6a882182b6ccdebecc7c440997e36', class: "check-icon" }), h("div", { key: '71ee5327faf48ea26c2ae36a8a6906c448c3a389', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '39c92aa0b82f6169058b1092894845fcba3f27b4', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '74b02ba81756f0e8bfb6e2feafdb2c44fb80afad', class: "status-spinner" })), h("div", { key: 'c7419efb2c6e2760019ecce619534e24ae3d7351', class: "status-content" }, h("div", { key: '57f315384630f17f6332eddfea628b333299e831', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'ac8632fdd68c24ed00192cb04a13026ada1f953d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'e51286a839795b18cfac2874e7e1f6e9f8e3bb4b', class: "performance-monitor" }, h("div", { key: 'f8cd289d260af9df2daf23bdfb8db70fe8bf6f45', class: "performance-expanded" }, h("div", { key: 'bf2becb2ee3ad3bffb82b0e90246a4385a19f567', class: "metrics-row" }, h("div", { key: 'f334c2b5de45df73aec6e06cd371151110789a45', class: "metric-compact" }, h("span", { key: 'cbf32f72eb3aaf1bf770d651c32123a43b81035c', class: "metric-label" }, "FPS"), h("span", { key: '9d2d2f46ba3fdb23c010f781044f00b1dd1250e6', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'f29406e8a5b00f5838a144de9fda77ce53177d43', class: "metric-compact" }, h("span", { key: '8f44e29f6134c340f0a58f8aa321612c8a9bbfb8', class: "metric-label" }, "MEM"), h("span", { key: '7a58e164d292329d95054b2679a90e031f6fe4c2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'd0284a0e65fc2879557d73fc2e03295c29f512ae', class: "metrics-row" }, h("div", { key: '78dcbce552e65c95fcd01158f3aa8916ef0e7175', class: "metric-compact" }, h("span", { key: '673cfbc8c9dabb020a3d3c18ee07558a61da9b51', class: "metric-label" }, "INF"), h("span", { key: '833761b7a875c931a8b31913930c3b132f71f618', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '324801e3e69a8de181755367201d8e8c643e8d72', class: "metric-compact" }, h("span", { key: 'f928d9b2dfbf81f5738ab08a42ebb76ff4a483de', class: "metric-label" }, "FRAME"), h("span", { key: 'dc9f2e4a1e848be4d240d89c89126203555ae203', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '7ca0a7b44719b1884334edab586267f021cf86c1', class: "metrics-row" }, h("div", { key: '262667c4320d337dae2220b77f142cf293542562', class: "metric-compact" }, h("span", { key: '578b53d251e58933f654a5afe558931e4333f022', class: "metric-label" }, "DET"), h("span", { key: '4fb36eaebebd73e5cb06d582fa3b4ece61463761', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'c17192e3964164ad603427de3befa1615b027022', class: "metric-compact" }, h("span", { key: 'd004e74797b9468e94c2af996bb62c1a3ca8d58e', class: "metric-label" }, "RATE"), h("span", { key: '0960e8760fd0ac2456a9e76598e5392c24e42d3b', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '3588fba1fe51af1335d643107ba0355aca6f4f8c', class: "watermark" }, h("img", { key: 'd53ec6b0ee9f97f602f4f8857ec605427b2ee478', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
757
998
  }
758
999
  // Utility methods
759
1000
  updateDetectionBoxes(boxes) {
@@ -868,6 +1109,141 @@ export class JaakStamps {
868
1109
  }
869
1110
  }
870
1111
  }
1112
+ async takeManualScreenshot() {
1113
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
1114
+ // Set processing state immediately
1115
+ const captureState = this.stateManager?.getCaptureState();
1116
+ if (captureState?.step === 'front') {
1117
+ this.processingButton = 'capture-front';
1118
+ }
1119
+ else if (captureState?.step === 'back') {
1120
+ this.processingButton = 'capture-back';
1121
+ }
1122
+ console.log('🔵 processingButton set to:', this.processingButton);
1123
+ // Add small delay to show spinner
1124
+ await new Promise(resolve => setTimeout(resolve, 100));
1125
+ if (!this.videoRef) {
1126
+ this.processingButton = null;
1127
+ return;
1128
+ }
1129
+ // When using manual capture, use mask coordinates for cropping
1130
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
1131
+ await this.takeScreenshotWithMaskCoordinates();
1132
+ }
1133
+ async takeScreenshotWithMaskCoordinates() {
1134
+ if (!this.videoRef || !this.detectionContainer)
1135
+ return;
1136
+ this.stateManager.updateCaptureState({ isCapturing: true });
1137
+ this.triggerCaptureAnimation();
1138
+ // Use pooled canvas for optimization
1139
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
1140
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
1141
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
1142
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
1143
+ // Calculate mask coordinates for cropping
1144
+ const container = this.detectionContainer.parentElement;
1145
+ const containerRect = container.getBoundingClientRect();
1146
+ // Get mask dimensions from CSS properties
1147
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
1148
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
1149
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
1150
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
1151
+ // Convert percentages to actual video coordinates
1152
+ const videoWidth = this.videoRef.videoWidth;
1153
+ const videoHeight = this.videoRef.videoHeight;
1154
+ const videoAspectRatio = videoWidth / videoHeight;
1155
+ const containerAspectRatio = containerRect.width / containerRect.height;
1156
+ let displayedVideoWidth, displayedVideoHeight;
1157
+ let videoOffsetX = 0, videoOffsetY = 0;
1158
+ if (videoAspectRatio > containerAspectRatio) {
1159
+ displayedVideoWidth = containerRect.width;
1160
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
1161
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
1162
+ }
1163
+ else {
1164
+ displayedVideoHeight = containerRect.height;
1165
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
1166
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
1167
+ }
1168
+ // Calculate mask coordinates in video space
1169
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
1170
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
1171
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
1172
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
1173
+ // Convert to video coordinates
1174
+ const scaleX = videoWidth / displayedVideoWidth;
1175
+ const scaleY = videoHeight / displayedVideoHeight;
1176
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
1177
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
1178
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
1179
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
1180
+ // Calculate crop coordinates
1181
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
1182
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
1183
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
1184
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
1185
+ // Use pooled canvas for cropped version
1186
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
1187
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
1188
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
1189
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
1190
+ const captureState = this.stateManager.getCaptureState();
1191
+ if (captureState.step === 'front') {
1192
+ this.stateManager.setCapturedImages({
1193
+ front: {
1194
+ fullFrame: captureCanvas.toDataURL('image/png'),
1195
+ cropped: croppedCanvas.toDataURL('image/png')
1196
+ }
1197
+ });
1198
+ // Check if document classification is enabled (independent of detector)
1199
+ if (this.useDocumentClassification) {
1200
+ try {
1201
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
1202
+ if (classification && classification.class === 'passport') {
1203
+ this.completeProcess(true);
1204
+ this.returnCanvasToPool(captureCanvas);
1205
+ this.returnCanvasToPool(croppedCanvas);
1206
+ // Clear processing button when passport is detected
1207
+ this.processingButton = null;
1208
+ return;
1209
+ }
1210
+ }
1211
+ catch (classifyError) {
1212
+ // No cambiar a manual aquí, solo registrar el error y continuar
1213
+ }
1214
+ }
1215
+ this.stateManager.updateCaptureState({
1216
+ step: 'back',
1217
+ isDetectionPaused: true,
1218
+ showFlipAnimation: true
1219
+ });
1220
+ this.triggerRerender();
1221
+ setTimeout(() => {
1222
+ this.stateManager.updateCaptureState({
1223
+ showFlipAnimation: false,
1224
+ isDetectionPaused: false
1225
+ });
1226
+ this.triggerRerender();
1227
+ this.startBackDocumentTimer();
1228
+ // Clear processing button after animation completes
1229
+ this.processingButton = null;
1230
+ }, 3000);
1231
+ }
1232
+ else if (captureState.step === 'back') {
1233
+ this.stateManager.setCapturedImages({
1234
+ back: {
1235
+ fullFrame: captureCanvas.toDataURL('image/png'),
1236
+ cropped: croppedCanvas.toDataURL('image/png')
1237
+ }
1238
+ });
1239
+ this.completeProcess(false);
1240
+ // Clear processing button after back capture completes
1241
+ this.processingButton = null;
1242
+ }
1243
+ // Return canvases to pool after use
1244
+ this.returnCanvasToPool(captureCanvas);
1245
+ this.returnCanvasToPool(croppedCanvas);
1246
+ }
871
1247
  async takeScreenshot() {
872
1248
  if (!this.videoRef || !this.lastDetectedBox)
873
1249
  return;
@@ -901,10 +1277,15 @@ export class JaakStamps {
901
1277
  });
902
1278
  // Check if document classification is enabled
903
1279
  if (this.useDocumentClassification) {
904
- const classification = await this.detectionService.classifyDocument(croppedCanvas);
905
- if (classification && classification.class === 'passport') {
906
- this.completeProcess(true);
907
- return;
1280
+ try {
1281
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
1282
+ if (classification && classification.class === 'passport') {
1283
+ this.completeProcess(true);
1284
+ return;
1285
+ }
1286
+ }
1287
+ catch (classifyError) {
1288
+ // No cambiar a manual aquí, solo registrar el error y continuar
908
1289
  }
909
1290
  }
910
1291
  this.stateManager.updateCaptureState({
@@ -941,6 +1322,135 @@ export class JaakStamps {
941
1322
  cardOutline?.classList.remove('capturing');
942
1323
  }, 600);
943
1324
  }
1325
+ // Método simplificado para cambio a modo manual
1326
+ switchToManualMode() {
1327
+ // Only switch if detector is enabled and not already switched
1328
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
1329
+ return;
1330
+ }
1331
+ // Detener todo el monitoreo de performance
1332
+ this.stopPerformanceMonitoring();
1333
+ // Liberar recursos de ONNX
1334
+ this.releaseOnnxResources();
1335
+ // Resetear máscara a blanco y limpiar detecciones
1336
+ this.resetMaskToWhite();
1337
+ this.detectionBoxes = [];
1338
+ this.hasAutoSwitchedToManual = true;
1339
+ this.performanceDegradedMode = true;
1340
+ this.showManualCaptureButton = true;
1341
+ this.showPerformanceMessage = true;
1342
+ // Hide performance message after 5 seconds
1343
+ setTimeout(() => {
1344
+ this.showPerformanceMessage = false;
1345
+ }, 5000);
1346
+ }
1347
+ // Método para cambiar a modo manual por errores de ONNX
1348
+ switchToManualModeWithWarning(warningMessage) {
1349
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
1350
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
1351
+ return;
1352
+ }
1353
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
1354
+ // Resetear máscara a blanco y limpiar detecciones
1355
+ this.resetMaskToWhite();
1356
+ this.detectionBoxes = [];
1357
+ // Cambiar a modo manual
1358
+ this.hasAutoSwitchedToManual = true;
1359
+ this.performanceDegradedMode = true;
1360
+ this.showManualCaptureButton = true;
1361
+ this.useDocumentDetector = false; // Desactivar detector automático
1362
+ // Actualizar el estado con mensaje informativo (no error)
1363
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
1364
+ // Continuar con la configuración de cámara
1365
+ this.continueWithCameraSetup();
1366
+ }
1367
+ async continueWithCameraSetup() {
1368
+ try {
1369
+ // Paso 2: Detectar y configurar dispositivos
1370
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1371
+ try {
1372
+ await this.cameraService.enumerateDevices();
1373
+ }
1374
+ catch (enumerateError) {
1375
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
1376
+ }
1377
+ try {
1378
+ await this.updateCameraInfoWithAutofocus();
1379
+ }
1380
+ catch (cameraInfoError) {
1381
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
1382
+ }
1383
+ // Paso 3: Configurar cámara seleccionada
1384
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1385
+ let stream;
1386
+ try {
1387
+ stream = await this.cameraService.setupCamera();
1388
+ }
1389
+ catch (setupError) {
1390
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
1391
+ }
1392
+ // Paso 4: Inicializar video
1393
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
1394
+ try {
1395
+ await this.initializeVideoStream(stream);
1396
+ }
1397
+ catch (videoError) {
1398
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
1399
+ }
1400
+ // Paso 5: Calibrar máscara
1401
+ if (this.detectionContainer) {
1402
+ const container = this.detectionContainer.parentElement;
1403
+ const rect = container.getBoundingClientRect();
1404
+ this.updateMaskDimensions(rect);
1405
+ }
1406
+ // Finalizar
1407
+ this.startTime = Date.now();
1408
+ this.stateManager.updateCaptureState({ isLoading: false });
1409
+ // Mostrar botón manual (ya debe estar habilitado)
1410
+ this.showManualCaptureButton = true;
1411
+ }
1412
+ catch (error) {
1413
+ this.updateStatus('Error de cámara', error.message, 'error');
1414
+ throw error;
1415
+ }
1416
+ }
1417
+ switchToManualModeWithError(errorMessage) {
1418
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
1419
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
1420
+ return;
1421
+ }
1422
+ // Liberar recursos de ONNX inmediatamente
1423
+ this.releaseOnnxResources();
1424
+ // Resetear máscara a blanco y limpiar detecciones
1425
+ this.resetMaskToWhite();
1426
+ this.detectionBoxes = [];
1427
+ // Cambiar a modo manual
1428
+ this.hasAutoSwitchedToManual = true;
1429
+ this.performanceDegradedMode = true;
1430
+ this.showManualCaptureButton = true;
1431
+ this.useDocumentDetector = false; // Desactivar detector automático
1432
+ // Actualizar el estado con mensaje de error
1433
+ this.updateStatus('Modo manual activado', errorMessage, 'error');
1434
+ // Ocultar mensaje después de 5 segundos
1435
+ setTimeout(() => {
1436
+ this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
1437
+ }, 5000);
1438
+ }
1439
+ stopPerformanceMonitoring() {
1440
+ // Nota: No necesitamos limpiar más variables porque ya no se usarán
1441
+ // El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
1442
+ }
1443
+ // Método para liberar recursos de ONNX cuando se cambia a modo manual
1444
+ releaseOnnxResources() {
1445
+ try {
1446
+ // Liberar recursos del servicio de detección
1447
+ if (this.detectionService) {
1448
+ this.detectionService.cleanup();
1449
+ }
1450
+ }
1451
+ catch (error) {
1452
+ }
1453
+ }
944
1454
  completeProcess(skippedBack = false) {
945
1455
  this.stateManager.updateCaptureState({
946
1456
  step: 'completed',
@@ -1054,6 +1564,13 @@ export class JaakStamps {
1054
1564
  this.detectionBoxes = [];
1055
1565
  this.alignmentStartTime = undefined;
1056
1566
  this.hasDocumentDetected = false;
1567
+ // Reset sistema simplificado
1568
+ this.processedFramesCount = 0;
1569
+ this.slowFrameCount = 0;
1570
+ // Reset performance degradation state
1571
+ this.hasAutoSwitchedToManual = false;
1572
+ this.performanceDegradedMode = false;
1573
+ this.showPerformanceMessage = false;
1057
1574
  if (this.alignmentTimer) {
1058
1575
  clearTimeout(this.alignmentTimer);
1059
1576
  this.alignmentTimer = undefined;
@@ -1157,6 +1674,24 @@ export class JaakStamps {
1157
1674
  // Performance is good, use base frame skip
1158
1675
  return this.BASE_FRAME_SKIP;
1159
1676
  }
1677
+ // Método para resetear la máscara a color blanco
1678
+ resetMaskToWhite() {
1679
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
1680
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
1681
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
1682
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
1683
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
1684
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
1685
+ // Remover todas las clases de alineación y estado perfecto
1686
+ cardOutline?.classList.remove('perfect-match');
1687
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
1688
+ topSide?.classList.remove('aligned');
1689
+ rightSide?.classList.remove('aligned');
1690
+ bottomSide?.classList.remove('aligned');
1691
+ leftSide?.classList.remove('aligned');
1692
+ // Resetear estado de alineación
1693
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
1694
+ }
1160
1695
  // Canvas pool management for screenshots
1161
1696
  getPooledCanvas(width, height) {
1162
1697
  // Try to find a canvas with matching dimensions
@@ -1301,6 +1836,26 @@ export class JaakStamps {
1301
1836
  "reflect": false,
1302
1837
  "defaultValue": "false"
1303
1838
  },
1839
+ "useDocumentDetector": {
1840
+ "type": "boolean",
1841
+ "attribute": "use-document-detector",
1842
+ "mutable": false,
1843
+ "complexType": {
1844
+ "original": "boolean",
1845
+ "resolved": "boolean",
1846
+ "references": {}
1847
+ },
1848
+ "required": false,
1849
+ "optional": false,
1850
+ "docs": {
1851
+ "tags": [],
1852
+ "text": ""
1853
+ },
1854
+ "getter": false,
1855
+ "setter": false,
1856
+ "reflect": false,
1857
+ "defaultValue": "true"
1858
+ },
1304
1859
  "preferredCamera": {
1305
1860
  "type": "string",
1306
1861
  "attribute": "preferred-camera",
@@ -1395,7 +1950,12 @@ export class JaakStamps {
1395
1950
  "cameraInfoWithAutofocus": {},
1396
1951
  "currentStatus": {},
1397
1952
  "performanceData": {},
1398
- "backDocumentTimerRemaining": {}
1953
+ "backDocumentTimerRemaining": {},
1954
+ "showManualCaptureButton": {},
1955
+ "performanceDegradedMode": {},
1956
+ "showPerformanceMessage": {},
1957
+ "captureStateVersion": {},
1958
+ "processingButton": {}
1399
1959
  };
1400
1960
  }
1401
1961
  static get events() {