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