@jaak.ai/stamps 2.0.0-beta.2 → 2.0.0-beta.4

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 (84) hide show
  1. package/README.md +216 -212
  2. package/dist/cjs/{index-DGM9-FNg.js → index-BfhtOB0D.js} +5 -2
  3. package/dist/cjs/index-BfhtOB0D.js.map +1 -0
  4. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +2 -2
  5. package/dist/cjs/jaak-stamps.cjs.entry.js +1860 -783
  6. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  7. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  8. package/dist/cjs/loader.cjs.js +2 -2
  9. package/dist/collection/components/my-component/my-component.css +629 -72
  10. package/dist/collection/components/my-component/my-component.js +896 -781
  11. package/dist/collection/components/my-component/my-component.js.map +1 -1
  12. package/dist/collection/services/CameraService.js +453 -0
  13. package/dist/collection/services/CameraService.js.map +1 -0
  14. package/dist/collection/services/DetectionService.js +369 -0
  15. package/dist/collection/services/DetectionService.js.map +1 -0
  16. package/dist/collection/services/EventBusService.js +42 -0
  17. package/dist/collection/services/EventBusService.js.map +1 -0
  18. package/dist/collection/services/LoggerService.js +40 -0
  19. package/dist/collection/services/LoggerService.js.map +1 -0
  20. package/dist/collection/services/ServiceContainer.js +66 -0
  21. package/dist/collection/services/ServiceContainer.js.map +1 -0
  22. package/dist/collection/services/StateManagerService.js +109 -0
  23. package/dist/collection/services/StateManagerService.js.map +1 -0
  24. package/dist/collection/services/factories/DeviceStrategyFactory.js +16 -0
  25. package/dist/collection/services/factories/DeviceStrategyFactory.js.map +1 -0
  26. package/dist/collection/services/interfaces/ICameraService.js +2 -0
  27. package/dist/collection/services/interfaces/ICameraService.js.map +1 -0
  28. package/dist/collection/services/interfaces/IDetectionService.js +2 -0
  29. package/dist/collection/services/interfaces/IDetectionService.js.map +1 -0
  30. package/dist/collection/services/interfaces/IEventBus.js +2 -0
  31. package/dist/collection/services/interfaces/IEventBus.js.map +1 -0
  32. package/dist/collection/services/interfaces/ILogger.js +2 -0
  33. package/dist/collection/services/interfaces/ILogger.js.map +1 -0
  34. package/dist/collection/services/interfaces/IStateManager.js +2 -0
  35. package/dist/collection/services/interfaces/IStateManager.js.map +1 -0
  36. package/dist/collection/services/strategies/HighPerformanceDeviceStrategy.js +30 -0
  37. package/dist/collection/services/strategies/HighPerformanceDeviceStrategy.js.map +1 -0
  38. package/dist/collection/services/strategies/IDeviceStrategy.js +2 -0
  39. package/dist/collection/services/strategies/IDeviceStrategy.js.map +1 -0
  40. package/dist/collection/services/strategies/LowMemoryDeviceStrategy.js +30 -0
  41. package/dist/collection/services/strategies/LowMemoryDeviceStrategy.js.map +1 -0
  42. package/dist/collection/types/component-types.js +2 -0
  43. package/dist/collection/types/component-types.js.map +1 -0
  44. package/dist/components/index.js +3 -0
  45. package/dist/components/index.js.map +1 -1
  46. package/dist/components/jaak-stamps.js +1873 -797
  47. package/dist/components/jaak-stamps.js.map +1 -1
  48. package/dist/esm/{index-DqoVMnc7.js → index-BP1Q4KOg.js} +5 -2
  49. package/dist/esm/index-BP1Q4KOg.js.map +1 -0
  50. package/dist/esm/jaak-stamps-webcomponent.js +3 -3
  51. package/dist/esm/jaak-stamps.entry.js +1860 -783
  52. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  53. package/dist/esm/loader.js +3 -3
  54. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  55. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  56. package/dist/jaak-stamps-webcomponent/p-10ee2dab.entry.js +2 -0
  57. package/dist/jaak-stamps-webcomponent/p-10ee2dab.entry.js.map +1 -0
  58. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +3 -0
  59. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +1 -0
  60. package/dist/types/components/my-component/my-component.d.ts +90 -79
  61. package/dist/types/components.d.ts +32 -8
  62. package/dist/types/services/CameraService.d.ts +46 -0
  63. package/dist/types/services/DetectionService.d.ts +35 -0
  64. package/dist/types/services/EventBusService.d.ts +9 -0
  65. package/dist/types/services/LoggerService.d.ts +12 -0
  66. package/dist/types/services/ServiceContainer.d.ts +27 -0
  67. package/dist/types/services/StateManagerService.d.ts +15 -0
  68. package/dist/types/services/factories/DeviceStrategyFactory.d.ts +4 -0
  69. package/dist/types/services/interfaces/ICameraService.d.ts +34 -0
  70. package/dist/types/services/interfaces/IDetectionService.d.ts +31 -0
  71. package/dist/types/services/interfaces/IEventBus.d.ts +16 -0
  72. package/dist/types/services/interfaces/ILogger.d.ts +8 -0
  73. package/dist/types/services/interfaces/IStateManager.d.ts +35 -0
  74. package/dist/types/services/strategies/HighPerformanceDeviceStrategy.d.ts +6 -0
  75. package/dist/types/services/strategies/IDeviceStrategy.d.ts +21 -0
  76. package/dist/types/services/strategies/LowMemoryDeviceStrategy.d.ts +6 -0
  77. package/dist/types/types/component-types.d.ts +36 -0
  78. package/package.json +3 -3
  79. package/dist/cjs/index-DGM9-FNg.js.map +0 -1
  80. package/dist/esm/index-DqoVMnc7.js.map +0 -1
  81. package/dist/jaak-stamps-webcomponent/p-DqoVMnc7.js +0 -3
  82. package/dist/jaak-stamps-webcomponent/p-DqoVMnc7.js.map +0 -1
  83. package/dist/jaak-stamps-webcomponent/p-f1172b06.entry.js +0 -2
  84. package/dist/jaak-stamps-webcomponent/p-f1172b06.entry.js.map +0 -1
@@ -1,132 +1,231 @@
1
1
  import { h } from "@stencil/core";
2
+ import { ServiceContainer } from "../../services/ServiceContainer";
2
3
  export class JaakStamps {
3
4
  el;
4
5
  debug = false;
5
- alignmentTolerance = 10; // Tolerancia en píxeles para considerar un lado alineado (escalado para 320x320)
6
- maskSize = 90; // Porcentaje del video que ocupará la máscara (default 90%)
7
- cropMargin = 0; // Margen en píxeles para el recorte del documento (default 0)
8
- useDocumentClassification = false; // Habilita la clasificación automática de documentos para determinar si solicitar reverso
6
+ alignmentTolerance = 15;
7
+ maskSize = 80;
8
+ cropMargin = 20;
9
+ useDocumentClassification = false;
10
+ preferredCamera = 'auto';
11
+ captureDelay = 1500;
9
12
  captureCompleted;
10
13
  isReady;
11
- isVideoActive = false;
12
- statusMessage = 'Presione el botón para activar la cámara';
13
- statusColor = '#aaa';
14
- bestScore = 0;
15
- capturedFullFrame = null;
16
- capturedCroppedId = null;
17
- capturedBackFullFrame = null;
18
- capturedBackCroppedId = null;
19
- captureStep = 'front';
20
- isCapturing = false;
21
- showFlipAnimation = false;
22
- showSuccessAnimation = false;
23
- shouldMirrorVideo = true;
14
+ // State derived from services
15
+ detectionBoxes = [];
24
16
  sideAlignment = {
25
- top: false,
26
- right: false,
27
- bottom: false,
28
- left: false
17
+ top: false, right: false, bottom: false, left: false
29
18
  };
30
- isDetectionPaused = false;
31
- isLoading = false;
32
- isModelPreloaded = false;
33
19
  isMaskReady = false;
20
+ shouldMirrorVideo = true;
21
+ showCameraSelector = false;
22
+ isSwitchingCamera = false;
23
+ hasDocumentDetected = false;
24
+ currentStatus = {
25
+ message: 'Inicializando componente...',
26
+ description: 'Configurando servicios y cargando recursos',
27
+ type: 'initializing',
28
+ isInitialized: false
29
+ };
30
+ performanceData = {
31
+ fps: 0,
32
+ inferenceTime: 0,
33
+ memoryUsage: 0,
34
+ onnxLoadTime: 0,
35
+ frameProcessingTime: 0,
36
+ totalDetections: 0,
37
+ successfulDetections: 0,
38
+ detectionRate: 0
39
+ };
40
+ // Services
41
+ serviceContainer;
42
+ logger;
43
+ eventBus;
44
+ stateManager;
45
+ cameraService;
46
+ detectionService;
47
+ // UI References
34
48
  videoRef;
35
- canvasRef;
36
- session;
37
- startTime;
49
+ detectionContainer;
38
50
  videoStream;
39
51
  animationId;
40
- hasScreenshotTaken = false;
52
+ // Detection state
41
53
  lastDetectedBox;
42
- mobileNetSession;
43
- mobileNetClassMap;
44
- // Performance optimization properties
54
+ startTime;
55
+ hasScreenshotTaken = false;
56
+ alignmentStartTime;
57
+ alignmentTimer;
58
+ // Performance monitoring
59
+ performanceMetrics = {
60
+ fps: 0,
61
+ inferenceTime: 0,
62
+ memoryUsage: 0,
63
+ cpuUsage: 0,
64
+ onnxLoadTime: 0,
65
+ frameProcessingTime: 0,
66
+ totalDetections: 0,
67
+ successfulDetections: 0,
68
+ detectionRate: 0,
69
+ lastUpdateTime: 0
70
+ };
71
+ performanceUpdateInterval;
72
+ // Performance optimization
45
73
  frameSkipCounter = 0;
46
- FRAME_SKIP = 2; // Process every 3rd frame (reduces CPU by ~66%)
74
+ FRAME_SKIP = 2;
47
75
  consecutiveFailures = 0;
48
- MAX_FAILURES = 30; // 0.5 seconds without detection
76
+ MAX_FAILURES = 30;
49
77
  lastInferenceTime = 0;
50
- MIN_INFERENCE_INTERVAL = 50; // Minimum 50ms between inferences
51
- // Canvas pooling for memory optimization
52
- preprocessCanvas;
53
- preprocessCtx;
54
- captureCanvas;
55
- captureCtx;
56
- MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v1.onnx";
57
- MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
58
- MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
59
- INPUT_SIZE = 320;
60
- CONFIDENCE_THRESHOLD = 0.6;
61
- // ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
62
- ID1_ASPECT_RATIO = 85.60 / 53.98; // 1.5863320574...
63
- debugLog(...args) {
78
+ MIN_INFERENCE_INTERVAL = 50;
79
+ async componentDidLoad() {
80
+ this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
81
+ await this.initializeServices();
82
+ this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
83
+ await this.setupEventListeners();
84
+ this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
85
+ await this.initializeComponent();
86
+ if (this.debug) {
87
+ this.initializePerformanceMonitor();
88
+ }
89
+ }
90
+ async initializeServices() {
91
+ const config = {
92
+ debug: this.debug,
93
+ alignmentTolerance: this.alignmentTolerance,
94
+ maskSize: this.maskSize,
95
+ cropMargin: this.cropMargin,
96
+ useDocumentClassification: this.useDocumentClassification,
97
+ preferredCamera: this.preferredCamera,
98
+ captureDelay: this.captureDelay
99
+ };
100
+ this.serviceContainer = new ServiceContainer(config);
101
+ this.logger = this.serviceContainer.getLogger();
102
+ this.eventBus = this.serviceContainer.getEventBus();
103
+ this.stateManager = this.serviceContainer.getStateManager();
104
+ this.cameraService = this.serviceContainer.getCameraService();
105
+ this.detectionService = this.serviceContainer.getDetectionService();
106
+ this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
107
+ }
108
+ async setupEventListeners() {
109
+ this.eventBus.on('state-changed', (data) => {
110
+ this.handleStateChange(data);
111
+ });
112
+ this.eventBus.on('camera-changed', (cameraId) => {
113
+ this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
114
+ });
115
+ this.eventBus.on('error', (error) => {
116
+ this.logger.error('Error en servicio:', error);
117
+ });
118
+ }
119
+ async initializeComponent() {
120
+ this.logger.state('COMPONENTE_INICIALIZANDO', {
121
+ debug: this.debug,
122
+ maskSize: this.maskSize,
123
+ cropMargin: this.cropMargin,
124
+ useDocumentClassification: this.useDocumentClassification,
125
+ preferredCamera: this.preferredCamera,
126
+ captureDelay: this.captureDelay
127
+ });
128
+ this.validateProps();
64
129
  if (this.debug) {
65
- console.log(...args);
130
+ this.stateManager.updateCaptureState({ isLoading: true });
131
+ await new Promise(resolve => setTimeout(resolve, 500));
66
132
  }
133
+ this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
134
+ await this.cameraService.detectDeviceType();
135
+ await this.cameraService.enumerateDevices();
136
+ this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
137
+ await this.loadOnnxRuntime();
138
+ this.initializeResizeObserver();
67
139
  }
68
- validateMaskSize() {
140
+ validateProps() {
69
141
  if (this.maskSize < 50 || this.maskSize > 100) {
70
- console.warn(`maskSize debe estar entre 50 y 100. Valor actual: ${this.maskSize}. Usando valor por defecto: 90`);
142
+ this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
71
143
  this.maskSize = 90;
72
144
  }
73
- }
74
- validateCropMargin() {
75
145
  if (this.cropMargin < 0 || this.cropMargin > 100) {
76
- console.warn(`cropMargin debe estar entre 0 y 100. Valor actual: ${this.cropMargin}. Usando valor por defecto: 0`);
146
+ this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
77
147
  this.cropMargin = 0;
78
148
  }
149
+ const validOptions = ['auto', 'front', 'back'];
150
+ if (!validOptions.includes(this.preferredCamera)) {
151
+ this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
152
+ this.preferredCamera = 'auto';
153
+ }
154
+ if (this.captureDelay < 0 || this.captureDelay > 10000) {
155
+ this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
156
+ this.captureDelay = 1500;
157
+ }
79
158
  }
80
- emitReadyEvent() {
81
- const isDocumentReady = !!window.ort && this.isModelPreloaded;
82
- this.isReady.emit(isDocumentReady);
83
- this.debugLog('🟢 isReady event emitted:', isDocumentReady);
84
- }
85
- isRearCamera(stream) {
86
- const videoTrack = stream.getVideoTracks()[0];
87
- if (!videoTrack)
88
- return false;
89
- const settings = videoTrack.getSettings();
90
- return settings.facingMode === 'environment';
91
- }
92
- async componentDidLoad() {
93
- this.validateMaskSize();
94
- this.validateCropMargin();
159
+ async loadOnnxRuntime() {
95
160
  if (!window.ort) {
161
+ this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
96
162
  const script = document.createElement('script');
97
163
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
98
164
  document.head.appendChild(script);
99
- // Wait for ONNX runtime to load before emitting ready event
100
- script.onload = () => {
101
- this.emitReadyEvent();
102
- };
165
+ await new Promise((resolve) => {
166
+ script.onload = () => {
167
+ setTimeout(() => {
168
+ this.finalizeInitialization();
169
+ resolve(undefined);
170
+ }, 300);
171
+ };
172
+ });
103
173
  }
104
174
  else {
105
- // ONNX runtime already loaded
106
- this.emitReadyEvent();
175
+ setTimeout(() => {
176
+ this.finalizeInitialization();
177
+ }, 300);
178
+ }
179
+ }
180
+ finalizeInitialization() {
181
+ this.stateManager.updateCaptureState({ isLoading: false });
182
+ this.currentStatus = {
183
+ message: 'Listo para capturar',
184
+ description: '',
185
+ type: 'ready',
186
+ isInitialized: true
187
+ };
188
+ this.emitReadyEvent();
189
+ }
190
+ updateStatus(message, description, type = 'loading') {
191
+ this.currentStatus = {
192
+ message,
193
+ description,
194
+ type,
195
+ isInitialized: this.currentStatus.isInitialized
196
+ };
197
+ }
198
+ emitReadyEvent() {
199
+ const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
200
+ this.isReady.emit(isDocumentReady);
201
+ this.logger.state('COMPONENTE_LISTO', {
202
+ ortLibraryLoaded: !!window.ort,
203
+ modelPreloaded: this.detectionService.isModelLoaded(),
204
+ isReady: isDocumentReady
205
+ });
206
+ }
207
+ handleStateChange(data) {
208
+ // React to state changes from StateManager
209
+ // This is where the component updates its internal state based on service state changes
210
+ if (data.changes.isLoading !== undefined) {
211
+ // Force re-render when loading state changes
212
+ // Note: Stencil automatically re-renders when @State changes
107
213
  }
108
- // Initialize canvas pool for better performance
109
- this.initializeCanvasPool();
110
- this.setupResizeObserver();
111
214
  }
112
- setupResizeObserver() {
113
- if ('ResizeObserver' in window && this.canvasRef) {
215
+ initializeResizeObserver() {
216
+ if ('ResizeObserver' in window && this.detectionContainer) {
114
217
  const resizeObserver = new ResizeObserver(() => {
115
218
  this.handleResize();
116
219
  });
117
- resizeObserver.observe(this.canvasRef.parentElement);
220
+ resizeObserver.observe(this.detectionContainer.parentElement);
118
221
  }
119
222
  }
120
223
  handleResize() {
121
- if (this.canvasRef) {
122
- const container = this.canvasRef.parentElement;
224
+ if (this.detectionContainer) {
225
+ const container = this.detectionContainer.parentElement;
123
226
  const rect = container.getBoundingClientRect();
124
- // Set canvas size to match container
125
- this.canvasRef.width = rect.width;
126
- this.canvasRef.height = rect.height;
127
- // Update mask positioning based on container and video dimensions
128
227
  this.updateMaskDimensions(rect);
129
- this.debugLog('📐 Canvas resized:', { width: rect.width, height: rect.height });
228
+ this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
130
229
  }
131
230
  }
132
231
  updateMaskDimensions(containerRect) {
@@ -143,32 +242,27 @@ export class JaakStamps {
143
242
  let displayedVideoWidth, displayedVideoHeight;
144
243
  let videoOffsetX = 0, videoOffsetY = 0;
145
244
  if (videoAspectRatio > containerAspectRatio) {
146
- // Video is wider - letterboxed (black bars top/bottom)
147
245
  displayedVideoWidth = containerRect.width;
148
246
  displayedVideoHeight = containerRect.width / videoAspectRatio;
149
247
  videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
150
248
  }
151
249
  else {
152
- // Video is taller - pillarboxed (black bars left/right)
153
250
  displayedVideoHeight = containerRect.height;
154
251
  displayedVideoWidth = containerRect.height * videoAspectRatio;
155
252
  videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
156
253
  }
157
- // Calculate maximum possible mask size while preserving exact ID-1 aspect ratio
158
- // Determine the limiting dimension based on video orientation and ID-1 proportions
159
- // Calculate what would be the max width if limited by video height
160
- const maxWidthByHeight = displayedVideoHeight * this.ID1_ASPECT_RATIO;
254
+ // Calculate mask dimensions with ID-1 aspect ratio
255
+ const ID1_ASPECT_RATIO = 85.60 / 53.98;
256
+ const maxWidthByHeight = displayedVideoHeight * ID1_ASPECT_RATIO;
161
257
  let maskWidthInVideo, maskHeightInVideo;
162
258
  const sizeRatio = this.maskSize / 100;
163
259
  if (maxWidthByHeight <= displayedVideoWidth) {
164
- // Video height is the limiting factor
165
260
  maskHeightInVideo = displayedVideoHeight * sizeRatio;
166
- maskWidthInVideo = maskHeightInVideo * this.ID1_ASPECT_RATIO;
261
+ maskWidthInVideo = maskHeightInVideo * ID1_ASPECT_RATIO;
167
262
  }
168
263
  else {
169
- // Video width is the limiting factor
170
264
  maskWidthInVideo = displayedVideoWidth * sizeRatio;
171
- maskHeightInVideo = maskWidthInVideo / this.ID1_ASPECT_RATIO;
265
+ maskHeightInVideo = maskWidthInVideo / ID1_ASPECT_RATIO;
172
266
  }
173
267
  // Convert to percentages of the container
174
268
  const maskWidthPercent = (maskWidthInVideo / containerRect.width) * 100;
@@ -184,9 +278,8 @@ export class JaakStamps {
184
278
  this.el.style.setProperty('--mask-height', `${maskHeightPercent}%`);
185
279
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
186
280
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
187
- // Mark mask as ready now that dimensions are calculated
188
281
  this.isMaskReady = true;
189
- this.debugLog('🎯 Mask dimensions updated:', {
282
+ this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
190
283
  video: { width: videoWidth, height: videoHeight },
191
284
  displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
192
285
  mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
@@ -194,46 +287,16 @@ export class JaakStamps {
194
287
  offset: { x: videoOffsetX, y: videoOffsetY }
195
288
  });
196
289
  }
197
- initializeCanvasPool() {
198
- // Preprocess canvas - reused for every inference
199
- this.preprocessCanvas = document.createElement('canvas');
200
- this.preprocessCanvas.width = this.INPUT_SIZE;
201
- this.preprocessCanvas.height = this.INPUT_SIZE;
202
- this.preprocessCtx = this.preprocessCanvas.getContext('2d', {
203
- alpha: false, // No transparency needed
204
- willReadFrequently: true // Optimize for frequent getImageData calls
205
- });
206
- // Capture canvas - reused for screenshots
207
- this.captureCanvas = document.createElement('canvas');
208
- this.captureCtx = this.captureCanvas.getContext('2d', {
209
- alpha: false
210
- });
211
- this.debugLog('🎨 Canvas pool initialized for performance');
212
- }
213
- disconnectedCallback() {
214
- this.cleanup();
215
- }
290
+ // PUBLIC METHODS
216
291
  async getCapturedImages() {
217
- if (this.captureStep !== 'completed') {
292
+ const state = this.stateManager.getCaptureState();
293
+ if (state.step !== 'completed') {
218
294
  throw new Error('El proceso de captura no ha sido completado');
219
295
  }
220
- return {
221
- front: {
222
- fullFrame: this.capturedFullFrame,
223
- cropped: this.capturedCroppedId
224
- },
225
- back: {
226
- fullFrame: this.capturedBackFullFrame,
227
- cropped: this.capturedBackCroppedId
228
- },
229
- metadata: {
230
- hasBackCapture: !!(this.capturedBackFullFrame && this.capturedBackCroppedId),
231
- totalImages: (this.capturedBackFullFrame && this.capturedBackCroppedId) ? 4 : 2
232
- }
233
- };
296
+ return this.stateManager.getCapturedImages();
234
297
  }
235
298
  async isProcessCompleted() {
236
- return this.captureStep === 'completed';
299
+ return this.stateManager.isProcessCompleted();
237
300
  }
238
301
  async startCapture() {
239
302
  await this.startDetection();
@@ -244,395 +307,250 @@ export class JaakStamps {
244
307
  async resetCapture() {
245
308
  this.resetDetection();
246
309
  }
310
+ async skipBackCapture() {
311
+ const captureState = this.stateManager.getCaptureState();
312
+ const capturedImages = this.stateManager.getCapturedImages();
313
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
314
+ this.completeProcess(true);
315
+ }
316
+ }
247
317
  async getStatus() {
318
+ const captureState = this.stateManager.getCaptureState();
319
+ const capturedImages = this.stateManager.getCapturedImages();
248
320
  return {
249
- isVideoActive: this.isVideoActive,
250
- captureStep: this.captureStep,
251
- statusMessage: this.statusMessage,
252
- hasImages: !!(this.capturedFullFrame || this.capturedBackFullFrame),
253
- isProcessCompleted: this.captureStep === 'completed',
254
- isModelPreloaded: this.isModelPreloaded
321
+ isVideoActive: captureState.isVideoActive,
322
+ captureStep: captureState.step,
323
+ hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
324
+ isProcessCompleted: this.stateManager.isProcessCompleted(),
325
+ isModelPreloaded: this.detectionService.isModelLoaded()
255
326
  };
256
327
  }
257
328
  async preloadModel() {
258
- if (this.isModelPreloaded || this.session) {
259
- this.debugLog('🚀 Model already preloaded or session exists');
329
+ if (this.detectionService.isModelLoaded()) {
330
+ this.logger.state('MODELO_YA_PRECARGADO');
331
+ this.updateStatus('Modelos ya cargados', '', 'ready');
260
332
  return { success: true, message: 'Model already loaded' };
261
333
  }
262
334
  try {
263
- this.isLoading = true;
264
- this.statusMessage = "Precargando modelos...";
265
- this.statusColor = "#007bff";
266
- const modelPath = this.MODEL_PATH;
267
- this.debugLog('🤖 Preloading detection model:', modelPath);
268
- // Configure ONNX Runtime with device-specific optimizations
269
- const sessionOptions = this.getSessionOptions();
270
- this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
271
- // Preload MobileNet model and classes only if classification is enabled
272
- if (this.useDocumentClassification) {
273
- await this.loadMobileNetModel();
335
+ const loadStartTime = performance.now();
336
+ this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
337
+ this.stateManager.updateCaptureState({ isLoading: true });
338
+ await this.detectionService.loadModel();
339
+ this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
340
+ await this.detectionService.loadClassificationModel();
341
+ const loadEndTime = performance.now();
342
+ const loadTime = loadEndTime - loadStartTime;
343
+ // Record ONNX load time
344
+ if (this.debug) {
345
+ this.recordOnnxPerformance(loadTime, 0);
274
346
  }
275
- this.isModelPreloaded = true;
276
- this.isLoading = false;
277
- this.statusMessage = "Modelos precargados. Listo para comenzar detección";
278
- this.statusColor = "#28a745";
347
+ this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
348
+ await new Promise(resolve => setTimeout(resolve, 300));
349
+ this.updateStatus('Modelos precargados', '', 'ready');
350
+ this.stateManager.updateCaptureState({ isLoading: false });
279
351
  this.emitReadyEvent();
280
- this.debugLog(' Models preloaded successfully');
352
+ this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
281
353
  return { success: true, message: 'Models preloaded successfully' };
282
354
  }
283
355
  catch (error) {
284
- this.debugLog('Error preloading models:', error);
285
- this.isLoading = false;
286
- this.statusMessage = "Error al precargar los modelos";
287
- this.statusColor = "#ff6b6b";
356
+ this.logger.error('Error al precargar modelos:', error);
357
+ this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
358
+ this.stateManager.updateCaptureState({ isLoading: false });
288
359
  return { success: false, error: error.message };
289
360
  }
290
361
  }
291
- async skipBackCapture() {
292
- if (this.captureStep === 'back' && this.capturedFullFrame) {
293
- this.completeProcess(true);
294
- }
295
- }
296
- async loadMobileNetModel() {
297
- try {
298
- this.debugLog('🤖 Loading MobileNet model...');
299
- // Load class map
300
- const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
301
- if (!classResponse.ok) {
302
- throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
303
- }
304
- this.mobileNetClassMap = await classResponse.json();
305
- this.debugLog('📋 MobileNet classes loaded:', this.mobileNetClassMap);
306
- // Load model
307
- const sessionOptions = this.getSessionOptions();
308
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
309
- this.debugLog('✅ MobileNet model loaded successfully');
310
- }
311
- catch (error) {
312
- this.debugLog('❌ Error loading MobileNet model:', error);
313
- throw error;
314
- }
315
- }
316
- preprocessMobileNet(canvas) {
317
- // Create a temporary canvas for MobileNet preprocessing (224x224)
318
- const tempCanvas = document.createElement('canvas');
319
- tempCanvas.width = 224;
320
- tempCanvas.height = 224;
321
- const tempCtx = tempCanvas.getContext('2d');
322
- // Draw the cropped image onto the 224x224 canvas
323
- tempCtx.drawImage(canvas, 0, 0, 224, 224);
324
- // Get image data and preprocess for MobileNet
325
- const imageData = tempCtx.getImageData(0, 0, 224, 224);
326
- const data = imageData.data;
327
- const hw = 224 * 224;
328
- const arr = new Float32Array(3 * hw);
329
- // Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
330
- for (let i = 0; i < hw; i++) {
331
- arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
332
- arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
333
- arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
334
- }
335
- return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
336
- }
337
- async classifyDocument(canvas) {
338
- if (!this.mobileNetSession || !this.mobileNetClassMap) {
339
- this.debugLog('⚠️ MobileNet model not loaded');
340
- return null;
341
- }
342
- try {
343
- this.debugLog('🔍 Classifying document...');
344
- // Preprocess image for MobileNet
345
- const inputTensor = this.preprocessMobileNet(canvas);
346
- // Run inference
347
- const feeds = { input: inputTensor };
348
- const results = await this.mobileNetSession.run(feeds);
349
- const output = results[Object.keys(results)[0]].data;
350
- // Find the class with highest confidence
351
- const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
352
- const confidence = output[maxIdx];
353
- const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
354
- this.debugLog('📄 Document classification result:', {
355
- class: className,
356
- confidence: confidence.toFixed(3),
357
- classIndex: maxIdx
358
- });
359
- return {
360
- class: className,
361
- confidence: confidence,
362
- classIndex: maxIdx
363
- };
364
- }
365
- catch (error) {
366
- this.debugLog('❌ Error classifying document:', error);
367
- return null;
368
- }
362
+ async getCameraInfo() {
363
+ return this.cameraService.getCameraInfo();
369
364
  }
370
- cleanup() {
371
- if (this.animationId) {
372
- cancelAnimationFrame(this.animationId);
373
- }
374
- if (this.videoStream) {
375
- this.videoStream.getTracks().forEach(track => track.stop());
376
- }
377
- // Limpiar canvas en cleanup general
378
- if (this.canvasRef) {
379
- const ctx = this.canvasRef.getContext("2d");
380
- if (ctx) {
381
- ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
365
+ async setPreferredCamera(camera) {
366
+ this.preferredCamera = camera;
367
+ this.serviceContainer.updateConfig({ preferredCamera: camera });
368
+ await this.cameraService.enumerateDevices();
369
+ const captureState = this.stateManager.getCaptureState();
370
+ if (captureState.isVideoActive) {
371
+ const selectedCameraId = this.cameraService.getSelectedCameraId();
372
+ if (selectedCameraId) {
373
+ await this.cameraService.switchCamera(selectedCameraId);
382
374
  }
383
375
  }
384
- // OPTIMIZATION: Clean up canvas pool
385
- this.cleanupCanvasPool();
376
+ return {
377
+ success: true,
378
+ selectedCamera: this.cameraService.getSelectedCameraId(),
379
+ availableCameras: this.cameraService.getAvailableCameras().length
380
+ };
386
381
  }
387
- cleanupCanvasPool() {
388
- // Release canvas references for garbage collection
389
- if (this.preprocessCanvas) {
390
- this.preprocessCtx = undefined;
391
- this.preprocessCanvas = undefined;
392
- }
393
- if (this.captureCanvas) {
394
- this.captureCtx = undefined;
395
- this.captureCanvas = undefined;
396
- }
397
- // Disposed ONNX sessions
398
- if (this.session) {
399
- this.session.release?.();
400
- this.session = undefined;
401
- }
402
- if (this.mobileNetSession) {
403
- this.mobileNetSession.release?.();
404
- this.mobileNetSession = undefined;
405
- }
406
- this.mobileNetClassMap = undefined;
407
- this.isModelPreloaded = false;
408
- this.debugLog('🧹 Canvas pool cleaned up');
409
- }
410
- async getMaxResolution() {
411
- try {
412
- // Primero obtén un stream básico para acceder a las capacidades
413
- const tempStream = await navigator.mediaDevices.getUserMedia({
414
- video: { facingMode: "environment" }
415
- });
416
- const videoTrack = tempStream.getVideoTracks()[0];
417
- const capabilities = videoTrack.getCapabilities();
418
- // Detener el stream temporal
419
- tempStream.getTracks().forEach(track => track.stop());
420
- // Construir restricciones con resolución optimizada para tablets
421
- const constraints = {
422
- facingMode: "environment"
423
- };
424
- if (capabilities.width && capabilities.height) {
425
- // Limitar resolución máxima para tablets para evitar problemas de rendimiento
426
- const maxWidth = Math.min(capabilities.width.max, 1920);
427
- const maxHeight = Math.min(capabilities.height.max, 1080);
428
- // Para tablets, usar resolución moderada en lugar de máxima
429
- const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
430
- if (isTablet) {
431
- constraints.width = { ideal: Math.min(maxWidth, 1280) };
432
- constraints.height = { ideal: Math.min(maxHeight, 720) };
433
- }
434
- else {
435
- constraints.width = { ideal: maxWidth };
436
- constraints.height = { ideal: maxHeight };
437
- }
438
- this.debugLog('📐 Resolution capabilities:', {
439
- maxWidth: capabilities.width.max,
440
- maxHeight: capabilities.height.max,
441
- selectedWidth: constraints.width.ideal,
442
- selectedHeight: constraints.height.ideal,
443
- isTablet
444
- });
445
- }
446
- return constraints;
447
- }
448
- catch (err) {
449
- this.debugLog('⚠️ Could not get capabilities, using fallback');
450
- // Fallback optimizado para tablets
451
- const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
452
- return {
453
- facingMode: "environment",
454
- width: { ideal: isTablet ? 1280 : 1920 },
455
- height: { ideal: isTablet ? 720 : 1080 }
456
- };
382
+ async setCaptureDelay(delay) {
383
+ if (delay < 0 || delay > 10000) {
384
+ throw new Error('Capture delay must be between 0 and 10000 milliseconds');
457
385
  }
386
+ this.captureDelay = delay;
387
+ this.serviceContainer.updateConfig({ captureDelay: delay });
388
+ return {
389
+ success: true,
390
+ captureDelay: this.captureDelay
391
+ };
458
392
  }
459
- async setupCamera() {
460
- try {
461
- const constraints = await this.getMaxResolution();
462
- const stream = await navigator.mediaDevices.getUserMedia({
463
- video: constraints,
464
- audio: false
465
- });
466
- if (this.videoRef) {
467
- this.videoRef.srcObject = stream;
468
- this.videoStream = stream;
469
- // Determine if video should be mirrored
470
- const isRear = this.isRearCamera(stream);
471
- this.shouldMirrorVideo = !isRear;
472
- this.debugLog('📹 Rear camera:', isRear);
473
- this.debugLog('🪞 Should mirror video:', this.shouldMirrorVideo);
474
- return new Promise((resolve) => {
475
- this.videoRef.onloadedmetadata = async () => {
476
- await this.videoRef.play();
477
- this.isVideoActive = true;
478
- // Update mask dimensions once video is loaded
479
- if (this.canvasRef) {
480
- const container = this.canvasRef.parentElement;
481
- const rect = container.getBoundingClientRect();
482
- this.updateMaskDimensions(rect);
483
- }
484
- resolve();
485
- };
486
- });
487
- }
488
- }
489
- catch (err) {
490
- this.debugLog("❌ No se pudo acceder a la cámara:", err);
491
- this.statusMessage = "Error: No se pudo acceder a la cámara.";
492
- this.statusColor = "#ff6b6b";
493
- }
393
+ async getCaptureDelay() {
394
+ return this.captureDelay;
494
395
  }
495
- preprocess(video) {
496
- // OPTIMIZATION: Reuse canvas instead of creating new ones
497
- if (!this.preprocessCanvas || !this.preprocessCtx) {
498
- this.initializeCanvasPool();
499
- }
500
- // Clear and redraw on reused canvas
501
- this.preprocessCtx.clearRect(0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
502
- this.preprocessCtx.drawImage(video, 0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
503
- const imageData = this.preprocessCtx.getImageData(0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
504
- const [R, G, B] = [[], [], []];
505
- const { data } = imageData;
506
- // Optimized pixel processing
507
- for (let i = 0; i < data.length; i += 4) {
508
- R.push(data[i] / 255);
509
- G.push(data[i + 1] / 255);
510
- B.push(data[i + 2] / 255);
511
- }
512
- const transposedData = new Float32Array(R.concat(G, B));
513
- return new window.ort.Tensor("float32", transposedData, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
396
+ async setTorchEnabled(enabled) {
397
+ const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
398
+ return {
399
+ success: torchEnabled,
400
+ enabled: torchEnabled ? enabled : false
401
+ };
514
402
  }
515
- getSessionOptions() {
403
+ async focusAtPoint(x, y) {
404
+ const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
516
405
  return {
517
- executionProviders: [
518
- // Try WebGL first for GPU acceleration, fallback to WASM
519
- 'webgl',
520
- 'wasm'
521
- ],
522
- graphOptimizationLevel: 'all', // Maximum optimization
523
- logSeverityLevel: this.debug ? 2 : 4, // Reduce logging overhead in production
524
- logVerbosityLevel: 0,
525
- enableCpuMemArena: true,
526
- enableMemPattern: true,
527
- executionMode: 'parallel',
528
- interOpNumThreads: 2,
529
- intraOpNumThreads: 2,
406
+ success: focused,
407
+ coordinates: { x, y }
530
408
  };
531
409
  }
410
+ // DETECTION METHODS
532
411
  async startDetection() {
412
+ this.logger.state('INICIANDO_DETECCION');
533
413
  try {
534
- // Check if model is already preloaded
535
- if (!this.session) {
536
- this.isLoading = true;
537
- this.statusMessage = "Cargando modelos...";
538
- this.statusColor = "#007bff";
539
- const modelPath = this.MODEL_PATH;
540
- this.debugLog('🤖 Loading detection model:', modelPath);
541
- const sessionOptions = this.getSessionOptions();
542
- this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
543
- // Load MobileNet model if classification is enabled and not already loaded
544
- if (this.useDocumentClassification && !this.mobileNetSession) {
545
- await this.loadMobileNetModel();
414
+ // Paso 1: Verificar y cargar modelos si es necesario
415
+ if (!this.detectionService.isModelLoaded()) {
416
+ const loadStartTime = performance.now();
417
+ this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
418
+ this.stateManager.updateCaptureState({ isLoading: true });
419
+ await this.detectionService.loadModel();
420
+ this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
421
+ await this.detectionService.loadClassificationModel();
422
+ const loadEndTime = performance.now();
423
+ const loadTime = loadEndTime - loadStartTime;
424
+ // Record ONNX load time
425
+ if (this.debug) {
426
+ this.recordOnnxPerformance(loadTime, 0);
546
427
  }
547
- this.isModelPreloaded = true;
548
- this.emitReadyEvent();
428
+ this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
549
429
  }
550
- else {
551
- this.debugLog('🚀 Using preloaded models');
552
- this.statusMessage = "Usando modelos precargados...";
553
- this.statusColor = "#007bff";
430
+ // Paso 2: Detectar y configurar dispositivos
431
+ this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
432
+ await this.cameraService.enumerateDevices();
433
+ // Paso 3: Configurar cámara seleccionada
434
+ this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
435
+ const stream = await this.cameraService.setupCamera();
436
+ // Paso 4: Inicializar video y ocultar estado inmediatamente
437
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
438
+ await this.initializeVideoStream(stream);
439
+ // Paso 5: Calibrar máscara
440
+ if (this.detectionContainer) {
441
+ const container = this.detectionContainer.parentElement;
442
+ const rect = container.getBoundingClientRect();
443
+ this.updateMaskDimensions(rect);
554
444
  }
555
- this.statusMessage = "Configurando cámara...";
556
- await this.setupCamera();
445
+ // Finalizar e iniciar captura
557
446
  this.startTime = Date.now();
558
- this.isLoading = false;
447
+ this.stateManager.updateCaptureState({ isLoading: false });
559
448
  this.detectFrame();
560
449
  }
561
450
  catch (err) {
562
- this.debugLog("Error al inicializar:", err);
563
- this.statusMessage = "Error al inicializar el detector";
564
- this.statusColor = "#ff6b6b";
565
- this.isLoading = false;
451
+ this.logger.error('Error al inicializar detección:', err);
452
+ this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
453
+ this.stateManager.updateCaptureState({ isLoading: false });
454
+ }
455
+ }
456
+ async initializeVideoStream(stream) {
457
+ if (this.videoRef) {
458
+ this.videoRef.srcObject = stream;
459
+ this.videoStream = stream;
460
+ const isRear = this.cameraService.isRearCamera(stream);
461
+ this.shouldMirrorVideo = !isRear;
462
+ this.logger.state('CAMARA_CONFIGURADA', {
463
+ isRearCamera: isRear,
464
+ shouldMirrorVideo: this.shouldMirrorVideo
465
+ });
466
+ return new Promise((resolve) => {
467
+ this.videoRef.onloadedmetadata = async () => {
468
+ await this.videoRef.play();
469
+ this.stateManager.updateCaptureState({ isVideoActive: true });
470
+ resolve();
471
+ };
472
+ });
473
+ }
474
+ else {
475
+ throw new Error('Video element not available');
566
476
  }
567
477
  }
568
478
  async detectFrame() {
569
479
  try {
570
- if (!this.videoRef || !this.canvasRef || !this.session)
480
+ const frameStartTime = performance.now();
481
+ const captureState = this.stateManager.getCaptureState();
482
+ if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
571
483
  return;
572
- // Pausar detección si está marcado como pausado
573
- if (this.isDetectionPaused) {
574
- // Solo continuar el bucle sin procesar detección
575
- if (this.captureStep !== 'completed') {
484
+ if (captureState.isDetectionPaused) {
485
+ if (captureState.step !== 'completed') {
576
486
  this.animationId = requestAnimationFrame(() => this.detectFrame());
577
487
  }
578
488
  return;
579
489
  }
580
- // OPTIMIZATION 1: Frame skipping for performance
490
+ // Frame skipping for performance
581
491
  this.frameSkipCounter++;
582
492
  if (this.frameSkipCounter <= this.FRAME_SKIP) {
583
- if (this.captureStep !== 'completed') {
493
+ if (captureState.step !== 'completed') {
584
494
  this.animationId = requestAnimationFrame(() => this.detectFrame());
585
495
  }
586
496
  return;
587
497
  }
588
498
  this.frameSkipCounter = 0;
589
- // OPTIMIZATION 2: Throttle inference based on time
499
+ // Throttle inference
590
500
  const currentTime = Date.now();
591
501
  if (currentTime - this.lastInferenceTime < this.MIN_INFERENCE_INTERVAL) {
592
- if (this.captureStep !== 'completed') {
502
+ if (captureState.step !== 'completed') {
593
503
  this.animationId = requestAnimationFrame(() => this.detectFrame());
594
504
  }
595
505
  return;
596
506
  }
597
507
  this.lastInferenceTime = currentTime;
598
- const ctx = this.canvasRef.getContext("2d");
599
- const inputTensor = this.preprocess(this.videoRef);
600
- const feeds = { [this.session.inputNames[0]]: inputTensor };
601
- const results = await this.session.run(feeds);
602
- const output = results[this.session.outputNames[0]].data;
603
- const finalBoxes = [];
604
- for (let i = 0; i < output.length; i += 6) {
605
- const [x1, y1, x2, y2, score, classId] = output.slice(i, i + 6);
606
- if (score > this.CONFIDENCE_THRESHOLD) {
607
- finalBoxes.push({ x: x1, y: y1, w: x2 - x1, h: y2 - y1, score, classId });
608
- if (this.startTime && Date.now() - this.startTime < 5000) {
609
- const currentBox = { x: x1, y: y1, w: x2 - x1, h: y2 - y1, score };
610
- const isWellPositioned = this.isCardInFrame(currentBox);
611
- const effectiveScore = isWellPositioned ? score * 1.2 : score;
612
- if (effectiveScore > this.bestScore) {
613
- this.bestScore = effectiveScore;
614
- }
508
+ // Measure preprocessing and inference time
509
+ const inferenceStartTime = performance.now();
510
+ const inputTensor = this.detectionService.preprocess(this.videoRef);
511
+ // Standard detection without quality validation
512
+ const detections = await this.detectionService.runInference(inputTensor);
513
+ const inferenceTime = performance.now() - inferenceStartTime;
514
+ // Record ONNX performance metrics
515
+ if (this.debug) {
516
+ this.recordOnnxPerformance(0, inferenceTime);
517
+ }
518
+ // Update best score logic
519
+ if (this.startTime && Date.now() - this.startTime < 5000) {
520
+ detections.forEach((detection) => {
521
+ const isWellPositioned = this.detectionService.isCardInFrame(detection);
522
+ const effectiveScore = isWellPositioned ? detection.score * 1.2 : detection.score;
523
+ if (effectiveScore > captureState.bestScore) {
524
+ this.stateManager.updateCaptureState({ bestScore: effectiveScore });
615
525
  }
616
- }
526
+ });
617
527
  }
618
- // OPTIMIZATION 3: Adaptive frame rate based on detection success
619
- if (finalBoxes.length === 0) {
528
+ // Update document detection state
529
+ this.hasDocumentDetected = detections.length > 0;
530
+ // Adaptive frame rate
531
+ if (detections.length === 0) {
620
532
  this.consecutiveFailures++;
621
533
  }
622
534
  else {
623
535
  this.consecutiveFailures = 0;
624
536
  }
625
- ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
537
+ // Update detection boxes for debug
538
+ if (this.debug) {
539
+ this.updateDetectionBoxes(detections);
540
+ }
541
+ else {
542
+ this.detectionBoxes = [];
543
+ }
544
+ this.updateMaskColor(detections);
545
+ // Record frame processing metrics
626
546
  if (this.debug) {
627
- this.drawDetections(ctx, finalBoxes);
547
+ const frameEndTime = performance.now();
548
+ const totalFrameTime = frameEndTime - frameStartTime;
549
+ this.recordFrameProcessing(totalFrameTime, detections.length);
628
550
  }
629
- this.updateGuidanceStatus(finalBoxes);
630
- this.updateMaskColor(finalBoxes);
631
- // Solo continuar si no hemos completado el proceso
632
- if (this.captureStep !== 'completed') {
633
- // OPTIMIZATION 4: Reduce frame rate when no detection for better battery life
551
+ // Continue detection loop
552
+ if (captureState.step !== 'completed') {
634
553
  if (this.consecutiveFailures > this.MAX_FAILURES) {
635
- // Reduce to ~5fps when no detection for 0.5 seconds
636
554
  setTimeout(() => this.detectFrame(), 200);
637
555
  }
638
556
  else {
@@ -641,102 +559,101 @@ export class JaakStamps {
641
559
  }
642
560
  }
643
561
  catch (e) {
644
- this.debugLog("Error en inferencia:", e);
645
- // Solo continuar si no hemos completado el proceso
646
- if (this.captureStep !== 'completed') {
647
- // On error, wait longer before retrying
562
+ this.logger.error('Error en inferencia de modelo:', e);
563
+ const captureState = this.stateManager.getCaptureState();
564
+ if (captureState.step !== 'completed') {
648
565
  setTimeout(() => this.detectFrame(), 100);
649
566
  }
650
567
  }
651
568
  }
652
- isCardInFrame(box) {
653
- const cardCenterX = box.x + box.w / 2;
654
- const cardCenterY = box.y + box.h / 2;
655
- const frameCenterX = this.INPUT_SIZE / 2;
656
- const frameCenterY = this.INPUT_SIZE / 2;
657
- const toleranceX = 40;
658
- const toleranceY = 30;
659
- const isCentered = Math.abs(cardCenterX - frameCenterX) < toleranceX &&
660
- Math.abs(cardCenterY - frameCenterY) < toleranceY;
661
- const isGoodSize = box.w > 150 && box.w < 300 && box.h > 90 && box.h < 200;
662
- return isCentered && isGoodSize;
569
+ // ... (continuing with remaining methods)
570
+ // Due to length constraints, I'll provide the key architectural changes
571
+ disconnectedCallback() {
572
+ this.cleanup();
663
573
  }
664
- checkSideAlignment(box) {
665
- if (!this.videoRef)
666
- return { top: false, right: false, bottom: false, left: false };
667
- // Get video dimensions to calculate actual mask boundaries in model space
574
+ cleanup() {
575
+ if (this.animationId) {
576
+ cancelAnimationFrame(this.animationId);
577
+ }
578
+ if (this.videoStream) {
579
+ this.videoStream.getTracks().forEach(track => track.stop());
580
+ }
581
+ if (this.alignmentTimer) {
582
+ clearTimeout(this.alignmentTimer);
583
+ this.alignmentTimer = undefined;
584
+ }
585
+ if (this.performanceUpdateInterval) {
586
+ clearInterval(this.performanceUpdateInterval);
587
+ this.performanceUpdateInterval = undefined;
588
+ }
589
+ this.detectionBoxes = [];
590
+ this.alignmentStartTime = undefined;
591
+ this.hasDocumentDetected = false;
592
+ this.serviceContainer?.cleanup();
593
+ }
594
+ render() {
595
+ const captureState = this.stateManager?.getCaptureState() || {
596
+ isVideoActive: false,
597
+ isLoading: false,
598
+ showFlipAnimation: false,
599
+ showSuccessAnimation: false,
600
+ step: 'front',
601
+ isCapturing: false
602
+ };
603
+ const cameraInfo = this.cameraService?.getCameraInfo() || {
604
+ availableCameras: [],
605
+ isMultipleCamerasAvailable: false,
606
+ selectedCameraId: null,
607
+ deviceType: 'desktop',
608
+ preferredFacing: null
609
+ };
610
+ return (h("div", { key: '7af79a0ff25b51790a9477cd7b338a0584172fc2', class: "detector-container" }, h("div", { key: 'ebf6a3811e0e599f29e98325b975a645e3bfb5e0', class: "video-container" }, h("video", { key: '5208fc9574fa78b1149be5839bc26d04bf9bcdfc', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: 'b507c786eccbd21acdd884ef50bc6ec280b9d437', 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: {
611
+ position: 'absolute',
612
+ left: `${box.x}px`,
613
+ top: `${box.y}px`,
614
+ width: `${box.w}px`,
615
+ height: `${box.h}px`,
616
+ border: '2px solid #32406C',
617
+ pointerEvents: 'none',
618
+ boxSizing: 'border-box'
619
+ } })))), this.isMaskReady && (h("div", { key: '53066c626b23daae351ea9a80409e8882434b0dd', class: "overlay-mask" }, h("div", { key: '77bb4048d43e328e57a5c7e1909e0a59c8957217', class: "card-outline" }, h("div", { key: 'ea0f2610d903f985286856ab69f8be24fd42148e', class: "side side-top" }), h("div", { key: 'dc671111ef4adf33aa41f4457fdf344ede23eb80', class: "side side-right" }), h("div", { key: '2475ec41577dca92b425542c8861c570ed7ecd3b', class: "side side-bottom" }), h("div", { key: 'a06171c8eb1bd6353a6464485604560d73ac04b7', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'ecfd98f36d457fe7282b26f6a0efef5361380c76', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("button", { key: 'f3c7b66500110437e9e4a7442cb327f2fdc6d71f', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")), captureState.isVideoActive && (h("div", { key: 'b5b2a8dfc6dbcf381c32c4caa562c27ced2e3e56', class: "camera-controls" }, h("button", { key: '6b40d4649dfa22c5ea8ca90cacce35cd0ca53ff3', 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: '41feec90287510b08bd1090ad15586b323007c30', class: "camera-selector-dropdown" }, h("div", { key: '8310ebe3db842c463ad428b973f841053315a76f', class: "camera-selector-header" }, h("span", { key: '6693bdfa33643dfe73798d4f25b0c2d9de7aac7c' }, "Seleccionar C\u00E1mara"), h("button", { key: '8552d45e1482c570d7ff85588d4404eec7c369d7', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '5333ec0e5fb2c50a07a7c0b7d4dfb1bb4ee0df1c', 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}`), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '8adf70b0e632c56a4784dec44621688809a3ff56', class: "device-info" }, h("small", { key: '2ea23922386ecda4a9011b50faba5efe4b4e60bb' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '8e693db67fe1e6ae8f11082b8993682b6d19b11d', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '474489ed7848cdc163c492a5e19f5f13231e17b3', class: "flip-animation" }, h("div", { key: 'a9bd24ca21330882f001b7811077630eb1c30f50', class: "id-card-icon" }), h("div", { key: 'ffadb660a61e0f5e3e29bdc473d987e87dc4ad8a', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: 'd2522ee33aa159d49f040a293775892fb1cb81b5', class: "success-animation" }, h("div", { key: 'e7f899fac58bc9a3d51cf2cb89ee33bb320b241f', class: "check-icon" }), h("div", { key: 'd5d0ee1f37dbc659ca625ad03cbce325be0cf0a6', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '425d347666032c3982a660ebee13b47670b996cb', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '4d396fa107872f9a1e526fd59dc4edf279a05792', class: "status-spinner" })), h("div", { key: '232e9ae24c8cf5ef8ac0e3442e45a920799c724d', class: "status-content" }, h("div", { key: 'fe9dc0ca6d8e8276052bac7d2d44f1c7c9730364', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: '6910c7ac78fd89e6fa1165446ec6d46fb356035c', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '7282b8afaf03da53b01a23d04babd772528c3c14', class: "performance-monitor" }, h("div", { key: 'b582b3124fed13387cd70a02a34ab2bb541f3bfc', class: "performance-expanded" }, h("div", { key: '764bac2d61224b9a5f306f50934d410e1bd27787', class: "metrics-row" }, h("div", { key: '261563f8314a9f6069b1ac6362eb9c38936a9dc7', class: "metric-compact" }, h("span", { key: '4feea98eff86620c1bc90eb65d9799102423109d', class: "metric-label" }, "FPS"), h("span", { key: 'eb15f13b27b8aa50701ad3d11c4cf87200010b67', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '0813c35c2c33286f2276bfb5adce59b32c7bb597', class: "metric-compact" }, h("span", { key: '201e7bcb556e63b141e42a3ffc3419eb4284bfa8', class: "metric-label" }, "MEM"), h("span", { key: '4f6703cbe0128440d0f2f22f631035ab85422c64', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '6343c361c93138b9ea66e8ae8acd361b93333f8c', class: "metrics-row" }, h("div", { key: 'd3c81fe597004bf380fae2d32f3a8314317e7769', class: "metric-compact" }, h("span", { key: 'fce572af3e200a611974dceef4da02f6aecedffc', class: "metric-label" }, "INF"), h("span", { key: '18dd78d54dcecfb70ddf59b782f87f8c6f208b0f', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '4c31ebfd835b017fd3e41b23d6bd9b19add511aa', class: "metric-compact" }, h("span", { key: '66b1a380b9479a8fc1e8c2385cb72aa4158dc74e', class: "metric-label" }, "FRAME"), h("span", { key: '3c6e41f7f11123c524d3a9df409345929d63f352', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '3b522e2cdfe4bd9f1dd6eb0f91ee3577dc43e490', class: "metrics-row" }, h("div", { key: 'fa1e6f687b43aa2582b8b3426feb03a16c6493fc', class: "metric-compact" }, h("span", { key: 'a9355091e71a33a6a0d7cc6c11171dcda2e51b22', class: "metric-label" }, "DET"), h("span", { key: 'beca5a7ccf4853675cf8366bcf577ebfbd46c4f0', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '4b07fc9b3d35f99bc6e77c8726192cde0476a787', class: "metric-compact" }, h("span", { key: 'a1674b538f45328ee0afb00e582c07e0b4ccf553', class: "metric-label" }, "RATE"), h("span", { key: 'fd384a5137bc3df6c939d3d698a3ef80e9087030', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: 'ebbaf15a0ad96e4da5bbf746c32d7a4abb51b973', class: "watermark" }, h("img", { key: '8e7d274b529d1f7020ec4a7225579792763f871a', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
620
+ }
621
+ // Utility methods
622
+ updateDetectionBoxes(boxes) {
623
+ if (!this.videoRef || !this.detectionContainer)
624
+ return;
668
625
  const videoWidth = this.videoRef.videoWidth;
669
626
  const videoHeight = this.videoRef.videoHeight;
670
- if (videoWidth === 0 || videoHeight === 0) {
671
- return { top: false, right: false, bottom: false, left: false };
672
- }
673
- // Calculate video aspect ratio
627
+ const container = this.detectionContainer.parentElement;
628
+ const containerRect = container.getBoundingClientRect();
629
+ const containerWidth = containerRect.width;
630
+ const containerHeight = containerRect.height;
631
+ if (videoWidth === 0 || videoHeight === 0)
632
+ return;
674
633
  const videoAspectRatio = videoWidth / videoHeight;
675
- // The model sees video stretched to 320x320, but we need to calculate where
676
- // the mask should be in this distorted space to match the visual mask
677
- // In the visual display, we calculate mask size based on the limiting dimension
678
- // We need to replicate this logic but account for the distortion in model space
679
- // Calculate the "display" dimensions (what the visual mask calculations use)
680
- const containerAspectRatio = 1; // Assume square container for simplicity
634
+ const containerAspectRatio = containerWidth / containerHeight;
681
635
  let displayedVideoWidth, displayedVideoHeight;
636
+ let videoOffsetX = 0, videoOffsetY = 0;
682
637
  if (videoAspectRatio > containerAspectRatio) {
683
- // Video is wider - letterboxed in display
684
- displayedVideoWidth = this.INPUT_SIZE;
685
- displayedVideoHeight = this.INPUT_SIZE / videoAspectRatio;
686
- }
687
- else {
688
- // Video is taller - pillarboxed in display
689
- displayedVideoHeight = this.INPUT_SIZE;
690
- displayedVideoWidth = this.INPUT_SIZE * videoAspectRatio;
691
- }
692
- // Calculate mask dimensions using the same logic as updateMaskDimensions
693
- const maxWidthByHeight = displayedVideoHeight * this.ID1_ASPECT_RATIO;
694
- let visualMaskWidth, visualMaskHeight;
695
- const sizeRatio = this.maskSize / 100;
696
- if (maxWidthByHeight <= displayedVideoWidth) {
697
- // Video height is the limiting factor
698
- visualMaskHeight = displayedVideoHeight * sizeRatio;
699
- visualMaskWidth = visualMaskHeight * this.ID1_ASPECT_RATIO;
638
+ displayedVideoWidth = containerWidth;
639
+ displayedVideoHeight = containerWidth / videoAspectRatio;
640
+ videoOffsetY = (containerHeight - displayedVideoHeight) / 2;
700
641
  }
701
642
  else {
702
- // Video width is the limiting factor
703
- visualMaskWidth = displayedVideoWidth * sizeRatio;
704
- visualMaskHeight = visualMaskWidth / this.ID1_ASPECT_RATIO;
705
- }
706
- // Now map these visual mask dimensions to the distorted model space
707
- // The model stretches video to 320x320, so we need to apply the inverse transformation
708
- const modelMaskWidth = visualMaskWidth * (this.INPUT_SIZE / displayedVideoWidth);
709
- const modelMaskHeight = visualMaskHeight * (this.INPUT_SIZE / displayedVideoHeight);
710
- // Calculate mask boundaries in model coordinate system (always centered in 320x320)
711
- const maskCenterX = this.INPUT_SIZE / 2;
712
- const maskCenterY = this.INPUT_SIZE / 2;
713
- const maskLeft = maskCenterX - (modelMaskWidth / 2);
714
- const maskRight = maskCenterX + (modelMaskWidth / 2);
715
- const maskTop = maskCenterY - (modelMaskHeight / 2);
716
- const maskBottom = maskCenterY + (modelMaskHeight / 2);
717
- // Obtener las coordenadas del documento detectado
718
- let docLeft = box.x;
719
- let docRight = box.x + box.w;
720
- const docTop = box.y;
721
- const docBottom = box.y + box.h;
722
- // Si el video está en modo mirror, invertir las coordenadas horizontales
723
- if (this.shouldMirrorVideo) {
724
- const originalDocLeft = docLeft;
725
- const originalDocRight = docRight;
726
- docLeft = this.INPUT_SIZE - originalDocRight;
727
- docRight = this.INPUT_SIZE - originalDocLeft;
643
+ displayedVideoHeight = containerHeight;
644
+ displayedVideoWidth = containerHeight * videoAspectRatio;
645
+ videoOffsetX = (containerWidth - displayedVideoWidth) / 2;
728
646
  }
729
- // Tolerancia para considerar que un lado está alineado (en píxeles)
730
- const tolerance = this.alignmentTolerance;
731
- return {
732
- top: Math.abs(docTop - maskTop) <= tolerance,
733
- right: Math.abs(docRight - maskRight) <= tolerance,
734
- bottom: Math.abs(docBottom - maskBottom) <= tolerance,
735
- left: Math.abs(docLeft - maskLeft) <= tolerance
736
- };
737
- }
738
- areAllSidesAligned(alignment) {
739
- return alignment.top && alignment.right && alignment.bottom && alignment.left;
647
+ const INPUT_SIZE = 320;
648
+ const scaleX = displayedVideoWidth / INPUT_SIZE;
649
+ const scaleY = displayedVideoHeight / INPUT_SIZE;
650
+ this.detectionBoxes = boxes.map(det => ({
651
+ x: det.x * scaleX + videoOffsetX,
652
+ y: det.y * scaleY + videoOffsetY,
653
+ w: det.w * scaleX,
654
+ h: det.h * scaleY,
655
+ score: det.score
656
+ }));
740
657
  }
741
658
  updateMaskColor(boxes) {
742
659
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -749,297 +666,327 @@ export class JaakStamps {
749
666
  let currentAlignment = { top: false, right: false, bottom: false, left: false };
750
667
  if (boxes.length > 0) {
751
668
  bestBox = boxes.reduce((best, current) => current.score > best.score ? current : best);
752
- currentAlignment = this.checkSideAlignment(bestBox);
669
+ const maskConfig = {
670
+ INPUT_SIZE: 320,
671
+ ID1_ASPECT_RATIO: 85.60 / 53.98,
672
+ shouldMirrorVideo: this.shouldMirrorVideo,
673
+ alignmentTolerance: this.alignmentTolerance,
674
+ maskSize: this.maskSize,
675
+ videoRef: this.videoRef
676
+ };
677
+ currentAlignment = this.detectionService.checkSideAlignment(bestBox, maskConfig);
753
678
  this.sideAlignment = currentAlignment;
754
679
  }
755
680
  else {
756
- // Reset alignment when no detection
757
681
  this.sideAlignment = { top: false, right: false, bottom: false, left: false };
758
682
  }
759
- // Actualizar colores de cada lado individualmente
760
683
  topSide?.classList.toggle('aligned', currentAlignment.top);
761
684
  rightSide?.classList.toggle('aligned', currentAlignment.right);
762
685
  bottomSide?.classList.toggle('aligned', currentAlignment.bottom);
763
686
  leftSide?.classList.toggle('aligned', currentAlignment.left);
764
- // Verificar si todos los lados están alineados
765
- const allSidesAligned = this.areAllSidesAligned(currentAlignment);
687
+ const allSidesAligned = this.detectionService.areAllSidesAligned(currentAlignment);
766
688
  if (allSidesAligned && bestBox) {
767
689
  cardOutline?.classList.add('perfect-match');
768
690
  corners?.forEach(corner => corner.classList.add('perfect-match'));
691
+ // Debug logging
692
+ this.logger.state('CAPTURE_EVALUATION', {
693
+ allSidesAligned: true,
694
+ hasScreenshotTaken: this.hasScreenshotTaken,
695
+ captureDelay: this.captureDelay,
696
+ alignmentStartTime: this.alignmentStartTime
697
+ });
769
698
  if (!this.hasScreenshotTaken) {
770
- this.lastDetectedBox = bestBox;
771
- this.takeScreenshot().catch(error => {
772
- this.debugLog('❌ Error taking screenshot:', error);
699
+ const currentTime = Date.now();
700
+ // Initialize alignment start time if not set
701
+ if (!this.alignmentStartTime) {
702
+ this.alignmentStartTime = currentTime;
703
+ this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
704
+ }
705
+ // Check if document has been aligned for the configured delay
706
+ const alignmentDuration = currentTime - this.alignmentStartTime;
707
+ this.logger.state('ALIGNMENT_DURATION_CHECK', {
708
+ alignmentDuration,
709
+ captureDelay: this.captureDelay,
710
+ readyToCapture: alignmentDuration >= this.captureDelay
773
711
  });
774
- this.hasScreenshotTaken = true;
775
- // Reset para permitir segunda captura
776
- setTimeout(() => {
777
- this.hasScreenshotTaken = false;
778
- }, 2000);
712
+ if (alignmentDuration >= this.captureDelay) {
713
+ this.logger.state('TRIGGERING_CAPTURE', {
714
+ alignmentDuration,
715
+ captureDelay: this.captureDelay
716
+ });
717
+ this.lastDetectedBox = bestBox;
718
+ this.takeScreenshot().catch(error => {
719
+ this.logger.error('Error al tomar captura de pantalla:', error);
720
+ });
721
+ this.hasScreenshotTaken = true;
722
+ this.alignmentStartTime = undefined;
723
+ setTimeout(() => {
724
+ this.hasScreenshotTaken = false;
725
+ }, 2000);
726
+ }
779
727
  }
780
728
  }
781
729
  else {
782
730
  cardOutline?.classList.remove('perfect-match');
783
731
  corners?.forEach(corner => corner.classList.remove('perfect-match'));
784
- }
785
- }
786
- updateGuidanceStatus(boxes) {
787
- if (boxes.length === 0) {
788
- this.statusMessage = "Posicione la identificación dentro del marco";
789
- this.statusColor = "#ff6b6b";
790
- }
791
- else {
792
- const bestBox = boxes.reduce((best, current) => current.score > best.score ? current : best);
793
- const alignment = this.checkSideAlignment(bestBox);
794
- const alignedSides = [alignment.top, alignment.right, alignment.bottom, alignment.left].filter(Boolean).length;
795
- const allSidesAligned = this.areAllSidesAligned(alignment);
796
- if (allSidesAligned) {
797
- this.statusMessage = "Identificación perfectamente alineada. Mantenga inmóvil";
798
- this.statusColor = "#00ff00";
799
- }
800
- else if (alignedSides > 0) {
801
- this.statusMessage = `Alinee los lados restantes (${alignedSides}/4 lados correctos)`;
802
- this.statusColor = "#ffb366";
732
+ // Reset alignment timer when document moves out of position
733
+ if (this.alignmentStartTime) {
734
+ this.alignmentStartTime = undefined;
803
735
  }
804
- else if (bestBox.w < 150) {
805
- this.statusMessage = "Identificación detectada. Acerque al marco";
806
- this.statusColor = "#ffb366";
807
- }
808
- else {
809
- this.statusMessage = "Identificación detectada. Alinee con el marco";
810
- this.statusColor = "#ffb366";
736
+ if (this.alignmentTimer) {
737
+ clearTimeout(this.alignmentTimer);
738
+ this.alignmentTimer = undefined;
811
739
  }
812
740
  }
813
741
  }
814
- drawDetections(ctx, boxes) {
815
- if (!this.videoRef)
816
- return;
817
- // Get video and container dimensions
818
- const videoWidth = this.videoRef.videoWidth;
819
- const videoHeight = this.videoRef.videoHeight;
820
- const containerWidth = this.canvasRef.width;
821
- const containerHeight = this.canvasRef.height;
822
- if (videoWidth === 0 || videoHeight === 0)
823
- return;
824
- // Calculate video aspect ratio and container aspect ratio
825
- const videoAspectRatio = videoWidth / videoHeight;
826
- const containerAspectRatio = containerWidth / containerHeight;
827
- // Determine how video fits in container (same logic as updateMaskDimensions)
828
- let displayedVideoWidth, displayedVideoHeight;
829
- let videoOffsetX = 0, videoOffsetY = 0;
830
- if (videoAspectRatio > containerAspectRatio) {
831
- // Video is wider - letterboxed (black bars top/bottom)
832
- displayedVideoWidth = containerWidth;
833
- displayedVideoHeight = containerWidth / videoAspectRatio;
834
- videoOffsetY = (containerHeight - displayedVideoHeight) / 2;
835
- }
836
- else {
837
- // Video is taller - pillarboxed (black bars left/right)
838
- displayedVideoHeight = containerHeight;
839
- displayedVideoWidth = containerHeight * videoAspectRatio;
840
- videoOffsetX = (containerWidth - displayedVideoWidth) / 2;
841
- }
842
- // Scale factor from model coordinates (320x320) to displayed video area
843
- const scaleX = displayedVideoWidth / this.INPUT_SIZE;
844
- const scaleY = displayedVideoHeight / this.INPUT_SIZE;
845
- boxes.forEach(det => {
846
- // Convert model coordinates to displayed video coordinates
847
- const x = det.x * scaleX + videoOffsetX;
848
- const y = det.y * scaleY + videoOffsetY;
849
- const w = det.w * scaleX;
850
- const h = det.h * scaleY;
851
- ctx.strokeStyle = "#32406C";
852
- ctx.lineWidth = 2;
853
- ctx.strokeRect(x, y, w, h);
854
- });
855
- }
856
742
  async takeScreenshot() {
857
743
  if (!this.videoRef || !this.lastDetectedBox)
858
744
  return;
859
- // Activar animación
745
+ this.logger.state('INICIANDO_CAPTURA', {
746
+ captureStep: this.stateManager.getCaptureState().step,
747
+ detectedBox: this.lastDetectedBox,
748
+ videoResolution: {
749
+ width: this.videoRef.videoWidth,
750
+ height: this.videoRef.videoHeight
751
+ }
752
+ });
753
+ this.stateManager.updateCaptureState({ isCapturing: true });
860
754
  this.triggerCaptureAnimation();
861
- // OPTIMIZATION: Reuse capture canvas for full frame
862
- if (!this.captureCanvas || !this.captureCtx) {
863
- this.initializeCanvasPool();
864
- }
865
- // Resize reused canvas for full frame
866
- this.captureCanvas.width = this.videoRef.videoWidth;
867
- this.captureCanvas.height = this.videoRef.videoHeight;
868
- this.captureCtx.drawImage(this.videoRef, 0, 0, this.captureCanvas.width, this.captureCanvas.height);
869
- // Calcular las coordenadas de recorte basadas en la detección
870
- const scaleX = this.videoRef.videoWidth / this.INPUT_SIZE;
871
- const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
755
+ // Create capture canvas
756
+ const captureCanvas = document.createElement('canvas');
757
+ captureCanvas.width = this.videoRef.videoWidth;
758
+ captureCanvas.height = this.videoRef.videoHeight;
759
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
760
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
761
+ // Calculate crop coordinates
762
+ const INPUT_SIZE = 320;
763
+ const scaleX = this.videoRef.videoWidth / INPUT_SIZE;
764
+ const scaleY = this.videoRef.videoHeight / INPUT_SIZE;
872
765
  const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
873
766
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
874
767
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
875
768
  const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
876
- // OPTIMIZATION: Create temporary canvas only for cropped version
877
- // (We reuse main capture canvas for full frame)
769
+ // Create cropped version
878
770
  const croppedCanvas = document.createElement('canvas');
879
771
  croppedCanvas.width = cropWidth;
880
772
  croppedCanvas.height = cropHeight;
881
773
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
882
- // Recortar la identificación del frame completo
883
- croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, // Área de origen
884
- 0, 0, cropWidth, cropHeight // Área de destino
885
- );
886
- if (this.captureStep === 'front') {
887
- // Captura del frente usando canvas reutilizado
888
- this.capturedFullFrame = this.captureCanvas.toDataURL('image/png');
889
- this.capturedCroppedId = croppedCanvas.toDataURL('image/png');
774
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
775
+ const captureState = this.stateManager.getCaptureState();
776
+ if (captureState.step === 'front') {
777
+ this.stateManager.setCapturedImages({
778
+ front: {
779
+ fullFrame: captureCanvas.toDataURL('image/png'),
780
+ cropped: croppedCanvas.toDataURL('image/png')
781
+ }
782
+ });
890
783
  // Check if document classification is enabled
891
784
  if (this.useDocumentClassification) {
892
- // Load MobileNet model if not loaded yet (lazy loading)
893
- if (!this.mobileNetSession) {
894
- try {
895
- await this.loadMobileNetModel();
896
- }
897
- catch (error) {
898
- this.debugLog('⚠️ Failed to load classification model, continuing without classification:', error);
899
- }
900
- }
901
- // Classify the cropped document if model is available
902
- if (this.mobileNetSession) {
903
- const classification = await this.classifyDocument(croppedCanvas);
904
- if (classification && classification.class === 'passport') {
905
- // If it's a passport, skip back capture since passports don't have a back side
906
- this.debugLog('📄 Passport detected - skipping back capture');
907
- this.completeProcess(true);
908
- return;
909
- }
785
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
786
+ if (classification && classification.class === 'passport') {
787
+ this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
788
+ this.completeProcess(true);
789
+ return;
910
790
  }
911
791
  }
912
- // For other IDs, continue with normal flow (back capture)
913
- this.captureStep = 'back';
914
- this.statusMessage = "Voltee la identificación y muestre la parte trasera";
915
- this.statusColor = "#007bff";
916
- // Pausar detección durante la animación de giro
917
- this.isDetectionPaused = true;
918
- // Mostrar animación de giro después de la captura
792
+ this.stateManager.updateCaptureState({
793
+ step: 'back',
794
+ isDetectionPaused: true,
795
+ showFlipAnimation: true
796
+ });
919
797
  setTimeout(() => {
920
- this.showFlipAnimation = true;
921
- setTimeout(() => {
922
- this.showFlipAnimation = false;
923
- // Reanudar detección después de que termine la animación
924
- this.isDetectionPaused = false;
925
- }, 3000);
926
- }, 800);
927
- this.debugLog('📸 FRENTE capturado. Esperando trasera...');
798
+ this.stateManager.updateCaptureState({
799
+ showFlipAnimation: false,
800
+ isDetectionPaused: false
801
+ });
802
+ }, 3000);
928
803
  }
929
- else if (this.captureStep === 'back') {
930
- // Captura de la trasera usando canvas reutilizado
931
- this.capturedBackFullFrame = this.captureCanvas.toDataURL('image/png');
932
- this.capturedBackCroppedId = croppedCanvas.toDataURL('image/png');
804
+ else if (captureState.step === 'back') {
805
+ this.stateManager.setCapturedImages({
806
+ back: {
807
+ fullFrame: captureCanvas.toDataURL('image/png'),
808
+ cropped: croppedCanvas.toDataURL('image/png')
809
+ }
810
+ });
933
811
  this.completeProcess(false);
934
- this.debugLog('📸 TRASERA capturada. Proceso completado. Detector detenido. Imágenes emitidas.');
935
812
  }
936
813
  }
937
814
  triggerCaptureAnimation() {
938
- this.isCapturing = true;
939
- // Agregar clase de animación al marco
940
815
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
941
816
  cardOutline?.classList.add('capturing');
942
- // Limpiar animación después de que termine
943
817
  setTimeout(() => {
944
- this.isCapturing = false;
818
+ this.stateManager.updateCaptureState({ isCapturing: false });
945
819
  cardOutline?.classList.remove('capturing');
946
820
  }, 600);
947
821
  }
822
+ completeProcess(skippedBack = false) {
823
+ this.stateManager.updateCaptureState({
824
+ step: 'completed',
825
+ showSuccessAnimation: true
826
+ });
827
+ const capturedImages = this.stateManager.getCapturedImages();
828
+ capturedImages.metadata.processCompleted = true;
829
+ capturedImages.metadata.backCaptureSkipped = skippedBack;
830
+ this.stateManager.setCapturedImages(capturedImages);
831
+ this.stopDetection();
832
+ this.updateStatus('Proceso completado', `${capturedImages.metadata.totalImages} imágenes capturadas exitosamente`, 'ready');
833
+ const finalImages = {
834
+ ...capturedImages,
835
+ timestamp: new Date().toISOString()
836
+ };
837
+ this.captureCompleted.emit(finalImages);
838
+ setTimeout(() => {
839
+ this.stateManager.updateCaptureState({ showSuccessAnimation: false });
840
+ }, 3000);
841
+ this.logger.state('PROCESO_COMPLETADO', {
842
+ skippedBack,
843
+ totalImages: capturedImages.metadata.totalImages,
844
+ timestamp: new Date().toISOString()
845
+ });
846
+ }
948
847
  stopDetection() {
949
848
  if (this.animationId) {
950
849
  cancelAnimationFrame(this.animationId);
951
850
  this.animationId = undefined;
952
851
  }
953
- // Limpiar canvas para eliminar cualquier detección visual residual
954
- if (this.canvasRef) {
955
- const ctx = this.canvasRef.getContext("2d");
956
- ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
852
+ this.detectionBoxes = [];
853
+ this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
854
+ }
855
+ toggleCameraSelector() {
856
+ if (this.isSwitchingCamera)
857
+ return; // Don't toggle if switching camera
858
+ this.showCameraSelector = !this.showCameraSelector;
859
+ }
860
+ async handleCameraSwitch(cameraId) {
861
+ if (this.isSwitchingCamera)
862
+ return; // Prevent multiple simultaneous switches
863
+ try {
864
+ // Close the selector immediately when user selects a camera
865
+ this.showCameraSelector = false;
866
+ this.isSwitchingCamera = true;
867
+ this.logger.state('INICIANDO_CAMBIO_CAMARA', {
868
+ from: this.cameraService.getSelectedCameraId(),
869
+ to: cameraId
870
+ });
871
+ // Stop current video stream
872
+ if (this.videoStream) {
873
+ this.videoStream.getTracks().forEach(track => track.stop());
874
+ }
875
+ // Switch camera in service
876
+ await this.cameraService.switchCamera(cameraId);
877
+ // Setup new camera stream
878
+ const newStream = await this.cameraService.setupCamera();
879
+ // Update video element
880
+ await this.initializeVideoStream(newStream);
881
+ this.logger.state('CAMBIO_CAMARA_EXITOSO', {
882
+ newCameraId: cameraId,
883
+ isRearCamera: this.cameraService.isRearCamera(newStream)
884
+ });
885
+ }
886
+ catch (error) {
887
+ this.logger.error('Error al cambiar cámara:', error);
888
+ // Try to restore previous camera if switch failed
889
+ try {
890
+ const fallbackStream = await this.cameraService.setupCamera();
891
+ await this.initializeVideoStream(fallbackStream);
892
+ }
893
+ catch (fallbackError) {
894
+ this.logger.error('Error al restaurar cámara anterior:', fallbackError);
895
+ this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
896
+ }
897
+ }
898
+ finally {
899
+ this.isSwitchingCamera = false;
957
900
  }
958
- this.debugLog('🛑 Detector de identificación detenido');
959
901
  }
960
902
  resetDetection() {
961
- this.bestScore = 0;
962
- this.startTime = Date.now();
963
- this.statusMessage = "Sistema reiniciado. Posicione la identificación";
964
- this.statusColor = "#aaa";
903
+ const currentCaptureState = this.stateManager.getCaptureState();
904
+ const wasVideoActive = currentCaptureState.isVideoActive;
905
+ this.stateManager.reset();
906
+ if (wasVideoActive) {
907
+ this.stateManager.updateCaptureState({ isVideoActive: true });
908
+ }
965
909
  this.hasScreenshotTaken = false;
966
- this.capturedFullFrame = null;
967
- this.capturedCroppedId = null;
968
- this.capturedBackFullFrame = null;
969
- this.capturedBackCroppedId = null;
970
- this.captureStep = 'front';
971
- this.isCapturing = false;
972
- this.showFlipAnimation = false;
973
- this.showSuccessAnimation = false;
974
- this.isDetectionPaused = false;
975
- this.isLoading = false;
976
- this.lastDetectedBox = undefined;
977
- this.isModelPreloaded = false;
978
- // Reset performance counters
910
+ this.startTime = Date.now();
979
911
  this.frameSkipCounter = 0;
980
912
  this.consecutiveFailures = 0;
981
913
  this.lastInferenceTime = 0;
982
- if (this.canvasRef) {
983
- const ctx = this.canvasRef.getContext("2d");
984
- ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
914
+ this.detectionBoxes = [];
915
+ this.alignmentStartTime = undefined;
916
+ this.hasDocumentDetected = false;
917
+ if (this.alignmentTimer) {
918
+ clearTimeout(this.alignmentTimer);
919
+ this.alignmentTimer = undefined;
985
920
  }
986
- // Reiniciar el detector si estaba funcionando
987
- if (this.isVideoActive && this.session) {
921
+ if (wasVideoActive && this.detectionService.isModelLoaded()) {
922
+ this.updateStatus('Captura reiniciada', 'Buscando documento en el marco de captura', 'active');
988
923
  this.detectFrame();
989
924
  }
990
- }
991
- completeProcess(skippedBack = false) {
992
- this.captureStep = 'completed';
993
- this.statusMessage = skippedBack ?
994
- "Proceso completado (solo frente capturado)" :
995
- "Proceso de captura completado exitosamente";
996
- this.statusColor = "#28a745";
997
- // Detener el detector
998
- this.stopDetection();
999
- // Emitir evento con las imágenes capturadas
1000
- const capturedImages = {
1001
- front: {
1002
- fullFrame: this.capturedFullFrame,
1003
- cropped: this.capturedCroppedId
1004
- },
1005
- back: {
1006
- fullFrame: this.capturedBackFullFrame,
1007
- cropped: this.capturedBackCroppedId
1008
- },
1009
- timestamp: new Date().toISOString(),
1010
- metadata: {
1011
- totalImages: skippedBack ? 2 : 4,
1012
- processCompleted: true,
1013
- backCaptureSkipped: skippedBack
1014
- }
1015
- };
1016
- this.captureCompleted.emit(capturedImages);
1017
- // Mostrar animación de éxito después de la captura
1018
- setTimeout(() => {
1019
- this.showSuccessAnimation = true;
1020
- setTimeout(() => {
1021
- this.showSuccessAnimation = false;
1022
- }, 3000);
1023
- }, 800);
925
+ else {
926
+ this.updateStatus('Listo para capturar', '', 'ready');
927
+ }
1024
928
  }
1025
929
  exitSession() {
1026
930
  if (this.videoStream) {
1027
931
  this.videoStream.getTracks().forEach(track => track.stop());
1028
932
  this.videoStream = undefined;
1029
- this.isVideoActive = false;
1030
- this.statusMessage = "Sesión finalizada.";
1031
- this.statusColor = "#aaa";
1032
- }
1033
- this.isLoading = false;
1034
- // Limpiar canvas al finalizar sesión
1035
- if (this.canvasRef) {
1036
- const ctx = this.canvasRef.getContext("2d");
1037
- ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
933
+ this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
1038
934
  }
935
+ this.isMaskReady = false;
936
+ this.updateStatus('Sesión finalizada', '', 'ready');
937
+ this.detectionBoxes = [];
1039
938
  this.cleanup();
1040
939
  }
1041
- render() {
1042
- return (h("div", { key: '42591479495df221b101555198a23288d7d765b0', class: "detector-container" }, h("div", { key: '2b9ab48572aa2fb5542b8a8dc1b70a0f2a6eca89', class: "video-container" }, h("video", { key: 'b9e84f2eb67fa6f7f158e19b90871a1eebee624b', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: this.isVideoActive ? 'block' : 'none' } }), h("canvas", { key: 'a82fc4826e220e696799b07b3510456f174a6628', ref: el => this.canvasRef = el, class: this.shouldMirrorVideo ? 'mirror' : '' }), this.isMaskReady && (h("div", { key: '9b3b838f99030e3dfa760fa0dd7c89ff24f26ed9', class: "overlay-mask" }, h("div", { key: 'f6a3b1ac2f12c1fb081f6b5eb853e627040396f4', class: "card-outline" }, h("div", { key: '6626259249ac5e0fd6ba665ab802af1ac2f9fc17', class: "side side-top" }), h("div", { key: '7a3195c96b2406b40b122f4267c2a251614cdede', class: "side side-right" }), h("div", { key: 'ece78305cfb083ada1d44de1d7d5226730879cc3', class: "side side-bottom" }), h("div", { key: '107468c583bbb01dd46c36c096e49be8e0b574ba', class: "side side-left" }), h("div", { key: 'c590f13b80c5e37c0ac0c68cfc2e2904b4345ebc', class: "corner corner-tl" }), h("div", { key: 'ab1e5aab179068ad1bc7b58ef3628d9618a877a8', class: "corner corner-tr" }), h("div", { key: '3a1db6cf1105bd3a2dfe7d4b998e79f6e40625ec', class: "corner corner-bl" }), h("div", { key: '5f9bc4e4557e5539335894160dccbaa2a6d6ac2d', class: "corner corner-br" }), !this.showFlipAnimation && !this.showSuccessAnimation && (h("div", { key: '8226d6721b0ae800f935e978a93113bd3b99656a', class: "guide-text" }, this.statusMessage))), this.captureStep === 'back' && !this.showFlipAnimation && !this.showSuccessAnimation && (h("button", { key: '16cec7ca4a80ba0db1ace768826da17063eddb3e', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")))), this.isCapturing && (h("div", { key: 'bfdca0059eb50d37a77e8e43ef963ba7ed953edf', class: "capture-animation" })), this.showFlipAnimation && (h("div", { key: 'a1ad4b4f884d7d9f6090f148ddd6df19250073dd', class: "flip-animation" }, h("div", { key: 'e159ce385824dc049a6e9409d3747105c6f0a08e', class: "id-card-icon" }), h("div", { key: '6fcff650a3c7fdb16f8d327750e06055b9a93d0d', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), this.showSuccessAnimation && (h("div", { key: 'c4b76ecb19c65969676e5156689c2c0b1c9a584e', class: "success-animation" }, h("div", { key: '12203a8fe7ee878089b3dda1d1142cde5489eed0', class: "check-icon" }), h("div", { key: 'a2d41c8944873d33106994b8d56c3ee1ee1ae9fc', class: "success-text" }, "\u00A1Proceso completado!"))), this.isLoading && (h("div", { key: 'a155cd999fa40c35fa7b391cde7c1026edb250a7', class: "loading-overlay" }, h("div", { key: 'ccba76c5d4a472158c28bc564ba95bd65829b58d', class: "loading-spinner" }), h("div", { key: '23168f9bdcf4e993539288ff5703acaed13bb16a', class: "loading-text" }, this.statusMessage))), h("div", { key: '0107b82ad19726c1bab131e17892a46b04e08cfd', class: "watermark" }, h("img", { key: 'ad8831a6f77fa61805f22e3a27f5c5619ac1cc0e', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
940
+ // PERFORMANCE MONITORING METHODS
941
+ initializePerformanceMonitor() {
942
+ this.performanceMetrics.lastUpdateTime = performance.now();
943
+ // Update performance metrics every 500ms
944
+ this.performanceUpdateInterval = window.setInterval(() => {
945
+ this.updatePerformanceMetrics();
946
+ }, 500);
947
+ this.logger.debug('Monitor de performance inicializado');
948
+ }
949
+ updatePerformanceMetrics() {
950
+ const currentTime = performance.now();
951
+ const deltaTime = currentTime - this.performanceMetrics.lastUpdateTime;
952
+ // Calculate FPS
953
+ if (deltaTime > 0) {
954
+ this.performanceMetrics.fps = Math.round(1000 / (deltaTime / this.frameSkipCounter || 1));
955
+ }
956
+ // Get memory usage if available
957
+ if ('memory' in performance) {
958
+ const memInfo = performance.memory;
959
+ this.performanceMetrics.memoryUsage = Math.round(memInfo.usedJSHeapSize / 1048576); // MB
960
+ }
961
+ // Calculate detection success rate
962
+ if (this.performanceMetrics.totalDetections > 0) {
963
+ this.performanceMetrics.successfulDetections = this.performanceMetrics.successfulDetections;
964
+ const detectionRate = (this.performanceMetrics.successfulDetections / this.performanceMetrics.totalDetections) * 100;
965
+ this.performanceMetrics.detectionRate = Math.round(detectionRate);
966
+ }
967
+ // Update state for rendering
968
+ this.performanceData = {
969
+ fps: this.performanceMetrics.fps,
970
+ inferenceTime: this.performanceMetrics.inferenceTime,
971
+ memoryUsage: this.performanceMetrics.memoryUsage,
972
+ onnxLoadTime: this.performanceMetrics.onnxLoadTime,
973
+ frameProcessingTime: this.performanceMetrics.frameProcessingTime,
974
+ totalDetections: this.performanceMetrics.totalDetections,
975
+ successfulDetections: this.performanceMetrics.successfulDetections,
976
+ detectionRate: this.performanceMetrics.detectionRate
977
+ };
978
+ this.performanceMetrics.lastUpdateTime = currentTime;
979
+ }
980
+ recordOnnxPerformance(loadTime, inferenceTime) {
981
+ this.performanceMetrics.onnxLoadTime = Math.round(loadTime);
982
+ this.performanceMetrics.inferenceTime = Math.round(inferenceTime);
983
+ }
984
+ recordFrameProcessing(processingTime, detectionsFound) {
985
+ this.performanceMetrics.frameProcessingTime = Math.round(processingTime);
986
+ this.performanceMetrics.totalDetections++;
987
+ if (detectionsFound > 0) {
988
+ this.performanceMetrics.successfulDetections++;
989
+ }
1043
990
  }
1044
991
  static get is() { return "jaak-stamps"; }
1045
992
  static get encapsulation() { return "shadow"; }
@@ -1093,7 +1040,7 @@ export class JaakStamps {
1093
1040
  "getter": false,
1094
1041
  "setter": false,
1095
1042
  "reflect": false,
1096
- "defaultValue": "10"
1043
+ "defaultValue": "15"
1097
1044
  },
1098
1045
  "maskSize": {
1099
1046
  "type": "number",
@@ -1113,7 +1060,7 @@ export class JaakStamps {
1113
1060
  "getter": false,
1114
1061
  "setter": false,
1115
1062
  "reflect": false,
1116
- "defaultValue": "90"
1063
+ "defaultValue": "80"
1117
1064
  },
1118
1065
  "cropMargin": {
1119
1066
  "type": "number",
@@ -1133,7 +1080,7 @@ export class JaakStamps {
1133
1080
  "getter": false,
1134
1081
  "setter": false,
1135
1082
  "reflect": false,
1136
- "defaultValue": "0"
1083
+ "defaultValue": "20"
1137
1084
  },
1138
1085
  "useDocumentClassification": {
1139
1086
  "type": "boolean",
@@ -1154,29 +1101,60 @@ export class JaakStamps {
1154
1101
  "setter": false,
1155
1102
  "reflect": false,
1156
1103
  "defaultValue": "false"
1104
+ },
1105
+ "preferredCamera": {
1106
+ "type": "string",
1107
+ "attribute": "preferred-camera",
1108
+ "mutable": false,
1109
+ "complexType": {
1110
+ "original": "'auto' | 'front' | 'back'",
1111
+ "resolved": "\"auto\" | \"back\" | \"front\"",
1112
+ "references": {}
1113
+ },
1114
+ "required": false,
1115
+ "optional": false,
1116
+ "docs": {
1117
+ "tags": [],
1118
+ "text": ""
1119
+ },
1120
+ "getter": false,
1121
+ "setter": false,
1122
+ "reflect": false,
1123
+ "defaultValue": "'auto'"
1124
+ },
1125
+ "captureDelay": {
1126
+ "type": "number",
1127
+ "attribute": "capture-delay",
1128
+ "mutable": false,
1129
+ "complexType": {
1130
+ "original": "number",
1131
+ "resolved": "number",
1132
+ "references": {}
1133
+ },
1134
+ "required": false,
1135
+ "optional": false,
1136
+ "docs": {
1137
+ "tags": [],
1138
+ "text": ""
1139
+ },
1140
+ "getter": false,
1141
+ "setter": false,
1142
+ "reflect": false,
1143
+ "defaultValue": "1500"
1157
1144
  }
1158
1145
  };
1159
1146
  }
1160
1147
  static get states() {
1161
1148
  return {
1162
- "isVideoActive": {},
1163
- "statusMessage": {},
1164
- "statusColor": {},
1165
- "bestScore": {},
1166
- "capturedFullFrame": {},
1167
- "capturedCroppedId": {},
1168
- "capturedBackFullFrame": {},
1169
- "capturedBackCroppedId": {},
1170
- "captureStep": {},
1171
- "isCapturing": {},
1172
- "showFlipAnimation": {},
1173
- "showSuccessAnimation": {},
1174
- "shouldMirrorVideo": {},
1149
+ "detectionBoxes": {},
1175
1150
  "sideAlignment": {},
1176
- "isDetectionPaused": {},
1177
- "isLoading": {},
1178
- "isModelPreloaded": {},
1179
- "isMaskReady": {}
1151
+ "isMaskReady": {},
1152
+ "shouldMirrorVideo": {},
1153
+ "showCameraSelector": {},
1154
+ "isSwitchingCamera": {},
1155
+ "hasDocumentDetected": {},
1156
+ "currentStatus": {},
1157
+ "performanceData": {}
1180
1158
  };
1181
1159
  }
1182
1160
  static get events() {
@@ -1216,15 +1194,20 @@ export class JaakStamps {
1216
1194
  return {
1217
1195
  "getCapturedImages": {
1218
1196
  "complexType": {
1219
- "signature": "() => Promise<{ front: { fullFrame: string; cropped: string; }; back: { fullFrame: string; cropped: string; }; metadata: { hasBackCapture: boolean; totalImages: number; }; }>",
1197
+ "signature": "() => Promise<CapturedImagesResponse>",
1220
1198
  "parameters": [],
1221
1199
  "references": {
1222
1200
  "Promise": {
1223
1201
  "location": "global",
1224
1202
  "id": "global::Promise"
1203
+ },
1204
+ "CapturedImagesResponse": {
1205
+ "location": "import",
1206
+ "path": "../../types/component-types",
1207
+ "id": "src/types/component-types.ts::CapturedImagesResponse"
1225
1208
  }
1226
1209
  },
1227
- "return": "Promise<{ front: { fullFrame: string; cropped: string; }; back: { fullFrame: string; cropped: string; }; metadata: { hasBackCapture: boolean; totalImages: number; }; }>"
1210
+ "return": "Promise<CapturedImagesResponse>"
1228
1211
  },
1229
1212
  "docs": {
1230
1213
  "text": "",
@@ -1299,17 +1282,39 @@ export class JaakStamps {
1299
1282
  "tags": []
1300
1283
  }
1301
1284
  },
1285
+ "skipBackCapture": {
1286
+ "complexType": {
1287
+ "signature": "() => Promise<void>",
1288
+ "parameters": [],
1289
+ "references": {
1290
+ "Promise": {
1291
+ "location": "global",
1292
+ "id": "global::Promise"
1293
+ }
1294
+ },
1295
+ "return": "Promise<void>"
1296
+ },
1297
+ "docs": {
1298
+ "text": "",
1299
+ "tags": []
1300
+ }
1301
+ },
1302
1302
  "getStatus": {
1303
1303
  "complexType": {
1304
- "signature": "() => Promise<{ isVideoActive: boolean; captureStep: \"front\" | \"back\" | \"completed\"; statusMessage: string; hasImages: boolean; isProcessCompleted: boolean; isModelPreloaded: boolean; }>",
1304
+ "signature": "() => Promise<StatusResponse>",
1305
1305
  "parameters": [],
1306
1306
  "references": {
1307
1307
  "Promise": {
1308
1308
  "location": "global",
1309
1309
  "id": "global::Promise"
1310
+ },
1311
+ "StatusResponse": {
1312
+ "location": "import",
1313
+ "path": "../../types/component-types",
1314
+ "id": "src/types/component-types.ts::StatusResponse"
1310
1315
  }
1311
1316
  },
1312
- "return": "Promise<{ isVideoActive: boolean; captureStep: \"front\" | \"back\" | \"completed\"; statusMessage: string; hasImages: boolean; isProcessCompleted: boolean; isModelPreloaded: boolean; }>"
1317
+ "return": "Promise<StatusResponse>"
1313
1318
  },
1314
1319
  "docs": {
1315
1320
  "text": "",
@@ -1333,17 +1338,127 @@ export class JaakStamps {
1333
1338
  "tags": []
1334
1339
  }
1335
1340
  },
1336
- "skipBackCapture": {
1341
+ "getCameraInfo": {
1337
1342
  "complexType": {
1338
- "signature": "() => Promise<void>",
1343
+ "signature": "() => Promise<CameraInfoResponse>",
1339
1344
  "parameters": [],
1340
1345
  "references": {
1341
1346
  "Promise": {
1342
1347
  "location": "global",
1343
1348
  "id": "global::Promise"
1349
+ },
1350
+ "CameraInfoResponse": {
1351
+ "location": "import",
1352
+ "path": "../../types/component-types",
1353
+ "id": "src/types/component-types.ts::CameraInfoResponse"
1344
1354
  }
1345
1355
  },
1346
- "return": "Promise<void>"
1356
+ "return": "Promise<CameraInfoResponse>"
1357
+ },
1358
+ "docs": {
1359
+ "text": "",
1360
+ "tags": []
1361
+ }
1362
+ },
1363
+ "setPreferredCamera": {
1364
+ "complexType": {
1365
+ "signature": "(camera: \"auto\" | \"front\" | \"back\") => Promise<{ success: boolean; selectedCamera: string; availableCameras: number; }>",
1366
+ "parameters": [{
1367
+ "name": "camera",
1368
+ "type": "\"auto\" | \"front\" | \"back\"",
1369
+ "docs": ""
1370
+ }],
1371
+ "references": {
1372
+ "Promise": {
1373
+ "location": "global",
1374
+ "id": "global::Promise"
1375
+ }
1376
+ },
1377
+ "return": "Promise<{ success: boolean; selectedCamera: string; availableCameras: number; }>"
1378
+ },
1379
+ "docs": {
1380
+ "text": "",
1381
+ "tags": []
1382
+ }
1383
+ },
1384
+ "setCaptureDelay": {
1385
+ "complexType": {
1386
+ "signature": "(delay: number) => Promise<{ success: boolean; captureDelay: number; }>",
1387
+ "parameters": [{
1388
+ "name": "delay",
1389
+ "type": "number",
1390
+ "docs": ""
1391
+ }],
1392
+ "references": {
1393
+ "Promise": {
1394
+ "location": "global",
1395
+ "id": "global::Promise"
1396
+ }
1397
+ },
1398
+ "return": "Promise<{ success: boolean; captureDelay: number; }>"
1399
+ },
1400
+ "docs": {
1401
+ "text": "",
1402
+ "tags": []
1403
+ }
1404
+ },
1405
+ "getCaptureDelay": {
1406
+ "complexType": {
1407
+ "signature": "() => Promise<number>",
1408
+ "parameters": [],
1409
+ "references": {
1410
+ "Promise": {
1411
+ "location": "global",
1412
+ "id": "global::Promise"
1413
+ }
1414
+ },
1415
+ "return": "Promise<number>"
1416
+ },
1417
+ "docs": {
1418
+ "text": "",
1419
+ "tags": []
1420
+ }
1421
+ },
1422
+ "setTorchEnabled": {
1423
+ "complexType": {
1424
+ "signature": "(enabled: boolean) => Promise<{ success: boolean; enabled: boolean; }>",
1425
+ "parameters": [{
1426
+ "name": "enabled",
1427
+ "type": "boolean",
1428
+ "docs": ""
1429
+ }],
1430
+ "references": {
1431
+ "Promise": {
1432
+ "location": "global",
1433
+ "id": "global::Promise"
1434
+ }
1435
+ },
1436
+ "return": "Promise<{ success: boolean; enabled: boolean; }>"
1437
+ },
1438
+ "docs": {
1439
+ "text": "",
1440
+ "tags": []
1441
+ }
1442
+ },
1443
+ "focusAtPoint": {
1444
+ "complexType": {
1445
+ "signature": "(x: number, y: number) => Promise<{ success: boolean; coordinates: { x: number; y: number; }; }>",
1446
+ "parameters": [{
1447
+ "name": "x",
1448
+ "type": "number",
1449
+ "docs": ""
1450
+ }, {
1451
+ "name": "y",
1452
+ "type": "number",
1453
+ "docs": ""
1454
+ }],
1455
+ "references": {
1456
+ "Promise": {
1457
+ "location": "global",
1458
+ "id": "global::Promise"
1459
+ }
1460
+ },
1461
+ "return": "Promise<{ success: boolean; coordinates: { x: number; y: number; }; }>"
1347
1462
  },
1348
1463
  "docs": {
1349
1464
  "text": "",