@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.
- package/README.md +216 -212
- package/dist/cjs/{index-DGM9-FNg.js → index-BfhtOB0D.js} +5 -2
- package/dist/cjs/index-BfhtOB0D.js.map +1 -0
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +2 -2
- package/dist/cjs/jaak-stamps.cjs.entry.js +1860 -783
- 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 +2 -2
- package/dist/collection/components/my-component/my-component.css +629 -72
- package/dist/collection/components/my-component/my-component.js +896 -781
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/CameraService.js +453 -0
- package/dist/collection/services/CameraService.js.map +1 -0
- package/dist/collection/services/DetectionService.js +369 -0
- package/dist/collection/services/DetectionService.js.map +1 -0
- package/dist/collection/services/EventBusService.js +42 -0
- package/dist/collection/services/EventBusService.js.map +1 -0
- package/dist/collection/services/LoggerService.js +40 -0
- package/dist/collection/services/LoggerService.js.map +1 -0
- package/dist/collection/services/ServiceContainer.js +66 -0
- package/dist/collection/services/ServiceContainer.js.map +1 -0
- package/dist/collection/services/StateManagerService.js +109 -0
- package/dist/collection/services/StateManagerService.js.map +1 -0
- package/dist/collection/services/factories/DeviceStrategyFactory.js +16 -0
- package/dist/collection/services/factories/DeviceStrategyFactory.js.map +1 -0
- package/dist/collection/services/interfaces/ICameraService.js +2 -0
- package/dist/collection/services/interfaces/ICameraService.js.map +1 -0
- package/dist/collection/services/interfaces/IDetectionService.js +2 -0
- package/dist/collection/services/interfaces/IDetectionService.js.map +1 -0
- package/dist/collection/services/interfaces/IEventBus.js +2 -0
- package/dist/collection/services/interfaces/IEventBus.js.map +1 -0
- package/dist/collection/services/interfaces/ILogger.js +2 -0
- package/dist/collection/services/interfaces/ILogger.js.map +1 -0
- package/dist/collection/services/interfaces/IStateManager.js +2 -0
- package/dist/collection/services/interfaces/IStateManager.js.map +1 -0
- package/dist/collection/services/strategies/HighPerformanceDeviceStrategy.js +30 -0
- package/dist/collection/services/strategies/HighPerformanceDeviceStrategy.js.map +1 -0
- package/dist/collection/services/strategies/IDeviceStrategy.js +2 -0
- package/dist/collection/services/strategies/IDeviceStrategy.js.map +1 -0
- package/dist/collection/services/strategies/LowMemoryDeviceStrategy.js +30 -0
- package/dist/collection/services/strategies/LowMemoryDeviceStrategy.js.map +1 -0
- package/dist/collection/types/component-types.js +2 -0
- package/dist/collection/types/component-types.js.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/jaak-stamps.js +1873 -797
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/{index-DqoVMnc7.js → index-BP1Q4KOg.js} +5 -2
- package/dist/esm/index-BP1Q4KOg.js.map +1 -0
- package/dist/esm/jaak-stamps-webcomponent.js +3 -3
- package/dist/esm/jaak-stamps.entry.js +1860 -783
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +3 -3
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-10ee2dab.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-10ee2dab.entry.js.map +1 -0
- package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +3 -0
- package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +90 -79
- package/dist/types/components.d.ts +32 -8
- package/dist/types/services/CameraService.d.ts +46 -0
- package/dist/types/services/DetectionService.d.ts +35 -0
- package/dist/types/services/EventBusService.d.ts +9 -0
- package/dist/types/services/LoggerService.d.ts +12 -0
- package/dist/types/services/ServiceContainer.d.ts +27 -0
- package/dist/types/services/StateManagerService.d.ts +15 -0
- package/dist/types/services/factories/DeviceStrategyFactory.d.ts +4 -0
- package/dist/types/services/interfaces/ICameraService.d.ts +34 -0
- package/dist/types/services/interfaces/IDetectionService.d.ts +31 -0
- package/dist/types/services/interfaces/IEventBus.d.ts +16 -0
- package/dist/types/services/interfaces/ILogger.d.ts +8 -0
- package/dist/types/services/interfaces/IStateManager.d.ts +35 -0
- package/dist/types/services/strategies/HighPerformanceDeviceStrategy.d.ts +6 -0
- package/dist/types/services/strategies/IDeviceStrategy.d.ts +21 -0
- package/dist/types/services/strategies/LowMemoryDeviceStrategy.d.ts +6 -0
- package/dist/types/types/component-types.d.ts +36 -0
- package/package.json +3 -3
- package/dist/cjs/index-DGM9-FNg.js.map +0 -1
- package/dist/esm/index-DqoVMnc7.js.map +0 -1
- package/dist/jaak-stamps-webcomponent/p-DqoVMnc7.js +0 -3
- package/dist/jaak-stamps-webcomponent/p-DqoVMnc7.js.map +0 -1
- package/dist/jaak-stamps-webcomponent/p-f1172b06.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-f1172b06.entry.js.map +0 -1
|
@@ -1,6 +1,1147 @@
|
|
|
1
|
-
import { r as registerInstance, c as createEvent, a as getElement, h } from './index-
|
|
1
|
+
import { r as registerInstance, c as createEvent, a as getElement, h } from './index-BP1Q4KOg.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
class LoggerService {
|
|
4
|
+
debugMode;
|
|
5
|
+
constructor(debug = false) {
|
|
6
|
+
this.debugMode = debug;
|
|
7
|
+
}
|
|
8
|
+
setDebugMode(debug) {
|
|
9
|
+
this.debugMode = debug;
|
|
10
|
+
}
|
|
11
|
+
info(...args) {
|
|
12
|
+
if (this.debugMode) {
|
|
13
|
+
console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
warn(...args) {
|
|
17
|
+
if (this.debugMode) {
|
|
18
|
+
console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
error(...args) {
|
|
22
|
+
if (this.debugMode) {
|
|
23
|
+
console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
debug(...args) {
|
|
27
|
+
if (this.debugMode) {
|
|
28
|
+
console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
state(state, data) {
|
|
32
|
+
if (this.debugMode) {
|
|
33
|
+
console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
performance(operation, duration) {
|
|
37
|
+
if (this.debugMode) {
|
|
38
|
+
console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class EventBusService {
|
|
44
|
+
events = new Map();
|
|
45
|
+
on(event, callback) {
|
|
46
|
+
if (!this.events.has(event)) {
|
|
47
|
+
this.events.set(event, []);
|
|
48
|
+
}
|
|
49
|
+
this.events.get(event).push(callback);
|
|
50
|
+
}
|
|
51
|
+
off(event, callback) {
|
|
52
|
+
const callbacks = this.events.get(event);
|
|
53
|
+
if (callbacks) {
|
|
54
|
+
const index = callbacks.indexOf(callback);
|
|
55
|
+
if (index > -1) {
|
|
56
|
+
callbacks.splice(index, 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
emit(event, data) {
|
|
61
|
+
const callbacks = this.events.get(event);
|
|
62
|
+
if (callbacks) {
|
|
63
|
+
callbacks.forEach(callback => {
|
|
64
|
+
try {
|
|
65
|
+
callback(data);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`Error in event callback for ${event}:`, error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
once(event, callback) {
|
|
74
|
+
const onceCallback = (data) => {
|
|
75
|
+
callback(data);
|
|
76
|
+
this.off(event, onceCallback);
|
|
77
|
+
};
|
|
78
|
+
this.on(event, onceCallback);
|
|
79
|
+
}
|
|
80
|
+
clear() {
|
|
81
|
+
this.events.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class StateManagerService {
|
|
86
|
+
eventBus;
|
|
87
|
+
captureState = {
|
|
88
|
+
step: 'front',
|
|
89
|
+
isCapturing: false,
|
|
90
|
+
isDetectionPaused: false,
|
|
91
|
+
isVideoActive: false,
|
|
92
|
+
isLoading: false,
|
|
93
|
+
showFlipAnimation: false,
|
|
94
|
+
showSuccessAnimation: false,
|
|
95
|
+
bestScore: 0,
|
|
96
|
+
hasScreenshotTaken: false
|
|
97
|
+
};
|
|
98
|
+
capturedImages = {
|
|
99
|
+
front: {
|
|
100
|
+
fullFrame: null,
|
|
101
|
+
cropped: null
|
|
102
|
+
},
|
|
103
|
+
back: {
|
|
104
|
+
fullFrame: null,
|
|
105
|
+
cropped: null
|
|
106
|
+
},
|
|
107
|
+
metadata: {
|
|
108
|
+
totalImages: 0,
|
|
109
|
+
processCompleted: false,
|
|
110
|
+
backCaptureSkipped: false
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
constructor(eventBus) {
|
|
114
|
+
this.eventBus = eventBus;
|
|
115
|
+
}
|
|
116
|
+
getCaptureState() {
|
|
117
|
+
return { ...this.captureState };
|
|
118
|
+
}
|
|
119
|
+
updateCaptureState(updates) {
|
|
120
|
+
const previousState = { ...this.captureState };
|
|
121
|
+
this.captureState = { ...this.captureState, ...updates };
|
|
122
|
+
// Emit state change event
|
|
123
|
+
this.eventBus.emit('state-changed', {
|
|
124
|
+
previous: previousState,
|
|
125
|
+
current: this.captureState,
|
|
126
|
+
changes: updates
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
getCapturedImages() {
|
|
130
|
+
return JSON.parse(JSON.stringify(this.capturedImages));
|
|
131
|
+
}
|
|
132
|
+
setCapturedImages(images) {
|
|
133
|
+
this.capturedImages = {
|
|
134
|
+
...this.capturedImages,
|
|
135
|
+
...images,
|
|
136
|
+
metadata: {
|
|
137
|
+
...this.capturedImages.metadata,
|
|
138
|
+
...images.metadata
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
// Update total images count
|
|
142
|
+
let totalImages = 0;
|
|
143
|
+
if (this.capturedImages.front.fullFrame && this.capturedImages.front.cropped) {
|
|
144
|
+
totalImages += 2;
|
|
145
|
+
}
|
|
146
|
+
if (this.capturedImages.back.fullFrame && this.capturedImages.back.cropped) {
|
|
147
|
+
totalImages += 2;
|
|
148
|
+
}
|
|
149
|
+
this.capturedImages.metadata.totalImages = totalImages;
|
|
150
|
+
}
|
|
151
|
+
reset() {
|
|
152
|
+
this.captureState = {
|
|
153
|
+
step: 'front',
|
|
154
|
+
isCapturing: false,
|
|
155
|
+
isDetectionPaused: false,
|
|
156
|
+
isVideoActive: false,
|
|
157
|
+
isLoading: false,
|
|
158
|
+
showFlipAnimation: false,
|
|
159
|
+
showSuccessAnimation: false,
|
|
160
|
+
bestScore: 0,
|
|
161
|
+
hasScreenshotTaken: false
|
|
162
|
+
};
|
|
163
|
+
this.capturedImages = {
|
|
164
|
+
front: {
|
|
165
|
+
fullFrame: null,
|
|
166
|
+
cropped: null
|
|
167
|
+
},
|
|
168
|
+
back: {
|
|
169
|
+
fullFrame: null,
|
|
170
|
+
cropped: null
|
|
171
|
+
},
|
|
172
|
+
metadata: {
|
|
173
|
+
totalImages: 0,
|
|
174
|
+
processCompleted: false,
|
|
175
|
+
backCaptureSkipped: false
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
this.eventBus.emit('state-changed', {
|
|
179
|
+
previous: null,
|
|
180
|
+
current: this.captureState,
|
|
181
|
+
changes: { reset: true }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
isProcessCompleted() {
|
|
185
|
+
return this.captureState.step === 'completed';
|
|
186
|
+
}
|
|
187
|
+
canProceedToBack() {
|
|
188
|
+
return this.captureState.step === 'front' &&
|
|
189
|
+
this.capturedImages.front.fullFrame !== null &&
|
|
190
|
+
this.capturedImages.front.cropped !== null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class CameraService {
|
|
195
|
+
logger;
|
|
196
|
+
eventBus;
|
|
197
|
+
availableCameras = [];
|
|
198
|
+
selectedCameraId = null;
|
|
199
|
+
deviceType = 'desktop';
|
|
200
|
+
preferredCameraFacing = null;
|
|
201
|
+
preferredCamera = 'auto';
|
|
202
|
+
constructor(logger, eventBus, preferredCamera = 'auto') {
|
|
203
|
+
this.logger = logger;
|
|
204
|
+
this.eventBus = eventBus;
|
|
205
|
+
this.preferredCamera = preferredCamera;
|
|
206
|
+
}
|
|
207
|
+
async detectDeviceType() {
|
|
208
|
+
const userAgent = navigator.userAgent;
|
|
209
|
+
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
210
|
+
const isTablet = /iPad|Android/i.test(userAgent) && window.innerWidth >= 768;
|
|
211
|
+
if (isTablet) {
|
|
212
|
+
this.deviceType = 'tablet';
|
|
213
|
+
}
|
|
214
|
+
else if (isMobile) {
|
|
215
|
+
this.deviceType = 'mobile';
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.deviceType = 'desktop';
|
|
219
|
+
}
|
|
220
|
+
this.logger.state('DISPOSITIVO_DETECTADO', {
|
|
221
|
+
deviceType: this.deviceType,
|
|
222
|
+
userAgent: navigator.userAgent,
|
|
223
|
+
screenDimensions: { width: window.innerWidth, height: window.innerHeight }
|
|
224
|
+
});
|
|
225
|
+
return this.deviceType;
|
|
226
|
+
}
|
|
227
|
+
async enumerateDevices() {
|
|
228
|
+
try {
|
|
229
|
+
const permissionStatus = await this.checkCameraPermission();
|
|
230
|
+
if (permissionStatus === 'denied') {
|
|
231
|
+
this.logger.error('Permiso de cámara denegado por el usuario');
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
if (permissionStatus === 'prompt') {
|
|
235
|
+
const tempStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
236
|
+
tempStream.getTracks().forEach(track => track.stop());
|
|
237
|
+
}
|
|
238
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
239
|
+
this.availableCameras = devices.filter(device => device.kind === 'videoinput');
|
|
240
|
+
this.logger.state('CAMARAS_DETECTADAS', {
|
|
241
|
+
count: this.availableCameras.length,
|
|
242
|
+
cameras: this.availableCameras.map(cam => ({
|
|
243
|
+
id: cam.deviceId,
|
|
244
|
+
label: cam.label || 'Unknown Camera'
|
|
245
|
+
}))
|
|
246
|
+
});
|
|
247
|
+
this.setInitialCameraPreference();
|
|
248
|
+
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
249
|
+
return this.availableCameras;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
this.logger.error('Error al enumerar cámaras disponibles:', error);
|
|
253
|
+
this.handleCameraPermissionError(error);
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
getAvailableCameras() {
|
|
258
|
+
return [...this.availableCameras];
|
|
259
|
+
}
|
|
260
|
+
isMultipleCamerasAvailable() {
|
|
261
|
+
return this.availableCameras.length > 1;
|
|
262
|
+
}
|
|
263
|
+
getSelectedCameraId() {
|
|
264
|
+
return this.selectedCameraId;
|
|
265
|
+
}
|
|
266
|
+
async setSelectedCamera(cameraId) {
|
|
267
|
+
const camera = this.availableCameras.find(cam => cam.deviceId === cameraId);
|
|
268
|
+
if (!camera) {
|
|
269
|
+
throw new Error(`Camera with ID ${cameraId} not found`);
|
|
270
|
+
}
|
|
271
|
+
this.selectedCameraId = cameraId;
|
|
272
|
+
this.updatePreferredFacing(camera);
|
|
273
|
+
this.savePreference();
|
|
274
|
+
this.eventBus.emit('camera-changed', cameraId);
|
|
275
|
+
}
|
|
276
|
+
getPreferredFacing() {
|
|
277
|
+
return this.preferredCameraFacing;
|
|
278
|
+
}
|
|
279
|
+
async setupCamera(constraints) {
|
|
280
|
+
const finalConstraints = constraints || await this.getMaxResolution();
|
|
281
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
282
|
+
video: finalConstraints,
|
|
283
|
+
audio: false
|
|
284
|
+
});
|
|
285
|
+
return stream;
|
|
286
|
+
}
|
|
287
|
+
async switchCamera(cameraId) {
|
|
288
|
+
const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
|
|
289
|
+
if (!selectedCamera) {
|
|
290
|
+
this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
|
|
291
|
+
await this.enumerateDevices();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await this.setSelectedCamera(cameraId);
|
|
295
|
+
this.logger.state('CAMARA_CAMBIADA', {
|
|
296
|
+
label: selectedCamera.label,
|
|
297
|
+
deviceId: selectedCamera.deviceId
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async flipToNextCamera() {
|
|
301
|
+
if (!this.isMultipleCamerasAvailable())
|
|
302
|
+
return;
|
|
303
|
+
const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
|
|
304
|
+
const nextIndex = (currentIndex + 1) % this.availableCameras.length;
|
|
305
|
+
const nextCamera = this.availableCameras[nextIndex];
|
|
306
|
+
await this.switchCamera(nextCamera.deviceId);
|
|
307
|
+
}
|
|
308
|
+
savePreference() {
|
|
309
|
+
try {
|
|
310
|
+
const preference = {
|
|
311
|
+
cameraId: this.selectedCameraId,
|
|
312
|
+
facing: this.preferredCameraFacing,
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
};
|
|
315
|
+
localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
|
|
316
|
+
this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
this.logger.warn('Error al guardar preferencia de cámara:', error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
loadPreference() {
|
|
323
|
+
try {
|
|
324
|
+
const saved = localStorage.getItem('jaak-stamps-camera-preference');
|
|
325
|
+
if (saved) {
|
|
326
|
+
const preference = JSON.parse(saved);
|
|
327
|
+
const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
|
|
328
|
+
if (isStillAvailable) {
|
|
329
|
+
this.selectedCameraId = preference.cameraId;
|
|
330
|
+
this.preferredCameraFacing = preference.facing;
|
|
331
|
+
this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
this.logger.warn('Error al cargar preferencia de cámara:', error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
isRearCamera(stream) {
|
|
340
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
341
|
+
if (!videoTrack)
|
|
342
|
+
return false;
|
|
343
|
+
const settings = videoTrack.getSettings();
|
|
344
|
+
return settings.facingMode === 'environment';
|
|
345
|
+
}
|
|
346
|
+
getCameraInfo() {
|
|
347
|
+
return {
|
|
348
|
+
availableCameras: this.availableCameras.map(camera => ({
|
|
349
|
+
id: camera.deviceId,
|
|
350
|
+
label: camera.label || 'Unknown Camera',
|
|
351
|
+
selected: camera.deviceId === this.selectedCameraId
|
|
352
|
+
})),
|
|
353
|
+
selectedCameraId: this.selectedCameraId,
|
|
354
|
+
deviceType: this.deviceType,
|
|
355
|
+
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
|
|
356
|
+
preferredFacing: this.preferredCameraFacing
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async checkCameraPermission() {
|
|
360
|
+
try {
|
|
361
|
+
if (!navigator.permissions) {
|
|
362
|
+
return 'prompt';
|
|
363
|
+
}
|
|
364
|
+
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
365
|
+
return permission.state;
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
this.logger.warn('No se pudo verificar permisos de cámara:', error);
|
|
369
|
+
return 'prompt';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
handleCameraPermissionError(error) {
|
|
373
|
+
this.availableCameras = [];
|
|
374
|
+
this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
|
|
375
|
+
}
|
|
376
|
+
setInitialCameraPreference() {
|
|
377
|
+
if (this.availableCameras.length === 0)
|
|
378
|
+
return;
|
|
379
|
+
if (this.preferredCamera === 'front') {
|
|
380
|
+
this.selectFrontCamera();
|
|
381
|
+
}
|
|
382
|
+
else if (this.preferredCamera === 'back') {
|
|
383
|
+
this.selectBackCamera();
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
this.selectAutoCamera();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
selectFrontCamera() {
|
|
390
|
+
this.preferredCameraFacing = 'user';
|
|
391
|
+
const frontCamera = this.availableCameras.find(camera => camera.label.toLowerCase().includes('front') ||
|
|
392
|
+
camera.label.toLowerCase().includes('user') ||
|
|
393
|
+
camera.label.toLowerCase().includes('selfie') ||
|
|
394
|
+
!camera.label.toLowerCase().includes('back') && !camera.label.toLowerCase().includes('rear'));
|
|
395
|
+
this.selectedCameraId = frontCamera ? frontCamera.deviceId : this.availableCameras[0].deviceId;
|
|
396
|
+
this.logger.state('CAMARA_FRONTAL_SELECCIONADA', {
|
|
397
|
+
label: frontCamera?.label || this.availableCameras[0].label,
|
|
398
|
+
deviceId: this.selectedCameraId
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
selectBackCamera() {
|
|
402
|
+
this.preferredCameraFacing = 'environment';
|
|
403
|
+
const backCamera = this.availableCameras.find(camera => camera.label.toLowerCase().includes('back') ||
|
|
404
|
+
camera.label.toLowerCase().includes('rear') ||
|
|
405
|
+
camera.label.toLowerCase().includes('environment'));
|
|
406
|
+
this.selectedCameraId = backCamera ? backCamera.deviceId : this.availableCameras[0].deviceId;
|
|
407
|
+
this.logger.state('CAMARA_TRASERA_SELECCIONADA', {
|
|
408
|
+
label: backCamera?.label || this.availableCameras[0].label,
|
|
409
|
+
deviceId: this.selectedCameraId
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
selectAutoCamera() {
|
|
413
|
+
if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
|
|
414
|
+
this.selectBackCamera();
|
|
415
|
+
this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
this.selectedCameraId = this.availableCameras[0].deviceId;
|
|
419
|
+
this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
|
|
420
|
+
label: this.availableCameras[0].label,
|
|
421
|
+
deviceId: this.selectedCameraId
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
updatePreferredFacing(camera) {
|
|
426
|
+
const isRearCamera = camera.label.toLowerCase().includes('back') ||
|
|
427
|
+
camera.label.toLowerCase().includes('rear') ||
|
|
428
|
+
camera.label.toLowerCase().includes('environment');
|
|
429
|
+
this.preferredCameraFacing = isRearCamera ? 'environment' : 'user';
|
|
430
|
+
}
|
|
431
|
+
async getMaxResolution() {
|
|
432
|
+
try {
|
|
433
|
+
const videoConstraints = {};
|
|
434
|
+
if (this.selectedCameraId) {
|
|
435
|
+
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
436
|
+
}
|
|
437
|
+
else if (this.preferredCameraFacing) {
|
|
438
|
+
videoConstraints.facingMode = this.preferredCameraFacing;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
videoConstraints.facingMode = "environment";
|
|
442
|
+
}
|
|
443
|
+
const tempStream = await navigator.mediaDevices.getUserMedia({
|
|
444
|
+
video: videoConstraints
|
|
445
|
+
});
|
|
446
|
+
const videoTrack = tempStream.getVideoTracks()[0];
|
|
447
|
+
const capabilities = videoTrack.getCapabilities();
|
|
448
|
+
tempStream.getTracks().forEach(track => track.stop());
|
|
449
|
+
const constraints = { ...videoConstraints };
|
|
450
|
+
// Enhanced focus and image quality settings
|
|
451
|
+
this.applyAdvancedCameraSettings(constraints, capabilities);
|
|
452
|
+
if (capabilities.width && capabilities.height) {
|
|
453
|
+
const maxWidth = Math.min(capabilities.width.max, 1920);
|
|
454
|
+
const maxHeight = Math.min(capabilities.height.max, 1080);
|
|
455
|
+
const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
|
|
456
|
+
if (isTablet) {
|
|
457
|
+
constraints.width = { ideal: Math.min(maxWidth, 1280) };
|
|
458
|
+
constraints.height = { ideal: Math.min(maxHeight, 720) };
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
constraints.width = { ideal: maxWidth };
|
|
462
|
+
constraints.height = { ideal: maxHeight };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
|
|
466
|
+
constraints,
|
|
467
|
+
capabilities: this.getCapabilitiesSummary(capabilities)
|
|
468
|
+
});
|
|
469
|
+
return constraints;
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
this.logger.warn('No se pudieron obtener capacidades de cámara, usando configuración de respaldo');
|
|
473
|
+
const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
|
|
474
|
+
const fallbackConstraints = {
|
|
475
|
+
width: { ideal: isTablet ? 1280 : 1920 },
|
|
476
|
+
height: { ideal: isTablet ? 720 : 1080 }
|
|
477
|
+
};
|
|
478
|
+
// Apply basic focus settings even for fallback
|
|
479
|
+
this.applyBasicFocusSettings(fallbackConstraints);
|
|
480
|
+
if (this.selectedCameraId) {
|
|
481
|
+
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
482
|
+
}
|
|
483
|
+
else if (this.preferredCameraFacing) {
|
|
484
|
+
fallbackConstraints.facingMode = this.preferredCameraFacing;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
fallbackConstraints.facingMode = "environment";
|
|
488
|
+
}
|
|
489
|
+
return fallbackConstraints;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
applyAdvancedCameraSettings(constraints, capabilities) {
|
|
493
|
+
// Apply standard camera settings that are widely supported
|
|
494
|
+
// Frame rate optimization
|
|
495
|
+
if (capabilities.frameRate) {
|
|
496
|
+
constraints.frameRate = {
|
|
497
|
+
ideal: 30,
|
|
498
|
+
min: 15,
|
|
499
|
+
max: 60
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// Width and height constraints for optimal resolution
|
|
503
|
+
if (capabilities.width && capabilities.height) {
|
|
504
|
+
constraints.width = {
|
|
505
|
+
ideal: Math.min(1920, capabilities.width.max || 1920),
|
|
506
|
+
min: 640
|
|
507
|
+
};
|
|
508
|
+
constraints.height = {
|
|
509
|
+
ideal: Math.min(1080, capabilities.height.max || 1080),
|
|
510
|
+
min: 480
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// Apply enhanced constraints through advanced array for better browser compatibility
|
|
514
|
+
const advancedConstraints = [];
|
|
515
|
+
// Try to set optimal settings through advanced constraints
|
|
516
|
+
const advanced = {};
|
|
517
|
+
// Focus mode - use 'any' type to bypass TypeScript limitations
|
|
518
|
+
if (capabilities.focusMode) {
|
|
519
|
+
if (capabilities.focusMode.includes('continuous')) {
|
|
520
|
+
advanced.focusMode = 'continuous';
|
|
521
|
+
}
|
|
522
|
+
else if (capabilities.focusMode.includes('single-shot')) {
|
|
523
|
+
advanced.focusMode = 'single-shot';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Focus distance for close objects
|
|
527
|
+
if (capabilities.focusDistance) {
|
|
528
|
+
advanced.focusDistance = {
|
|
529
|
+
ideal: 0.4, // 40cm - optimal for document capture
|
|
530
|
+
min: 0.2,
|
|
531
|
+
max: 1.0
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// Zoom control
|
|
535
|
+
if (capabilities.zoom) {
|
|
536
|
+
advanced.zoom = {
|
|
537
|
+
ideal: 1.0,
|
|
538
|
+
min: capabilities.zoom.min,
|
|
539
|
+
max: Math.min(capabilities.zoom.max, 3.0)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// Add advanced constraints if any were set
|
|
543
|
+
if (Object.keys(advanced).length > 0) {
|
|
544
|
+
advancedConstraints.push(advanced);
|
|
545
|
+
}
|
|
546
|
+
if (advancedConstraints.length > 0) {
|
|
547
|
+
constraints.advanced = advancedConstraints;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
applyBasicFocusSettings(constraints) {
|
|
551
|
+
// Apply basic focus settings when capabilities are not available
|
|
552
|
+
// Use advanced constraints for non-standard properties
|
|
553
|
+
const advanced = {
|
|
554
|
+
focusMode: 'continuous',
|
|
555
|
+
exposureMode: 'continuous',
|
|
556
|
+
whiteBalanceMode: 'continuous'
|
|
557
|
+
};
|
|
558
|
+
if (!constraints.advanced) {
|
|
559
|
+
constraints.advanced = [];
|
|
560
|
+
}
|
|
561
|
+
constraints.advanced.push(advanced);
|
|
562
|
+
constraints.frameRate = { ideal: 30 };
|
|
563
|
+
}
|
|
564
|
+
getCapabilitiesSummary(capabilities) {
|
|
565
|
+
return {
|
|
566
|
+
hasAutoFocus: !!capabilities.focusMode,
|
|
567
|
+
hasFocusDistance: !!capabilities.focusDistance,
|
|
568
|
+
hasExposureControl: !!capabilities.exposureMode,
|
|
569
|
+
hasWhiteBalance: !!capabilities.whiteBalanceMode,
|
|
570
|
+
hasZoom: !!capabilities.zoom,
|
|
571
|
+
hasTorch: capabilities.torch !== undefined,
|
|
572
|
+
hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
|
|
573
|
+
resolutionRange: capabilities.width && capabilities.height ?
|
|
574
|
+
`${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// New method to enable torch/flash for better illumination
|
|
578
|
+
async setTorchEnabled(enabled, stream) {
|
|
579
|
+
try {
|
|
580
|
+
if (!stream) {
|
|
581
|
+
this.logger.warn('No hay stream disponible para controlar el flash');
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
585
|
+
if (!videoTrack) {
|
|
586
|
+
this.logger.warn('No hay track de video disponible');
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
const capabilities = videoTrack.getCapabilities();
|
|
590
|
+
if (capabilities.torch === undefined) {
|
|
591
|
+
this.logger.warn('El dispositivo no soporta control de flash');
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
await videoTrack.applyConstraints({
|
|
595
|
+
advanced: [{ torch: enabled }]
|
|
596
|
+
});
|
|
597
|
+
this.logger.state('FLASH_CONTROLADO', { enabled });
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
this.logger.error('Error al controlar el flash:', error);
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Method to manually trigger focus on a specific point
|
|
606
|
+
async focusAtPoint(x, y, stream) {
|
|
607
|
+
try {
|
|
608
|
+
if (!stream) {
|
|
609
|
+
this.logger.warn('No hay stream disponible para enfocar');
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
613
|
+
if (!videoTrack) {
|
|
614
|
+
this.logger.warn('No hay track de video disponible');
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
const capabilities = videoTrack.getCapabilities();
|
|
618
|
+
if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
|
|
619
|
+
this.logger.warn('El dispositivo no soporta enfoque manual');
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
// Trigger single-shot focus
|
|
623
|
+
await videoTrack.applyConstraints({
|
|
624
|
+
advanced: [{ focusMode: 'single-shot' }]
|
|
625
|
+
});
|
|
626
|
+
// Return to continuous focus after a short delay
|
|
627
|
+
setTimeout(async () => {
|
|
628
|
+
try {
|
|
629
|
+
await videoTrack.applyConstraints({
|
|
630
|
+
advanced: [{ focusMode: 'continuous' }]
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
this.logger.warn('Error al volver a enfoque continuo:', error);
|
|
635
|
+
}
|
|
636
|
+
}, 1000);
|
|
637
|
+
this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
this.logger.error('Error al enfocar manualmente:', error);
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
class LowMemoryDeviceStrategy {
|
|
648
|
+
getDeviceInfo() {
|
|
649
|
+
const nav = navigator;
|
|
650
|
+
const memory = nav.deviceMemory || nav.hardwareConcurrency || 4;
|
|
651
|
+
const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
652
|
+
const isSlowConnection = connection && (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g');
|
|
653
|
+
return {
|
|
654
|
+
estimatedRAM: memory,
|
|
655
|
+
isLowMemory: true,
|
|
656
|
+
isSlowConnection: isSlowConnection
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
getSessionOptions(_debug) {
|
|
660
|
+
return {
|
|
661
|
+
executionProviders: ['wasm'],
|
|
662
|
+
graphOptimizationLevel: 'basic',
|
|
663
|
+
logSeverityLevel: 4,
|
|
664
|
+
logVerbosityLevel: 0,
|
|
665
|
+
enableCpuMemArena: false,
|
|
666
|
+
enableMemPattern: false,
|
|
667
|
+
executionMode: 'sequential',
|
|
668
|
+
interOpNumThreads: 1,
|
|
669
|
+
intraOpNumThreads: 1,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
shouldUseSequentialLoading() {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
class HighPerformanceDeviceStrategy {
|
|
678
|
+
getDeviceInfo() {
|
|
679
|
+
const nav = navigator;
|
|
680
|
+
const memory = nav.deviceMemory || nav.hardwareConcurrency || 4;
|
|
681
|
+
const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
682
|
+
const isSlowConnection = connection && (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g');
|
|
683
|
+
return {
|
|
684
|
+
estimatedRAM: memory,
|
|
685
|
+
isLowMemory: false,
|
|
686
|
+
isSlowConnection: isSlowConnection
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
getSessionOptions(debug) {
|
|
690
|
+
return {
|
|
691
|
+
executionProviders: ['webgl', 'wasm'],
|
|
692
|
+
graphOptimizationLevel: 'all',
|
|
693
|
+
logSeverityLevel: debug ? 2 : 4,
|
|
694
|
+
logVerbosityLevel: 0,
|
|
695
|
+
enableCpuMemArena: true,
|
|
696
|
+
enableMemPattern: true,
|
|
697
|
+
executionMode: 'parallel',
|
|
698
|
+
interOpNumThreads: 2,
|
|
699
|
+
intraOpNumThreads: 2,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
shouldUseSequentialLoading() {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
class DeviceStrategyFactory {
|
|
708
|
+
static createStrategy() {
|
|
709
|
+
const nav = navigator;
|
|
710
|
+
const memory = nav.deviceMemory || nav.hardwareConcurrency || 4;
|
|
711
|
+
const isLowMemory = memory <= 4;
|
|
712
|
+
if (isLowMemory) {
|
|
713
|
+
return new LowMemoryDeviceStrategy();
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
return new HighPerformanceDeviceStrategy();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
class DetectionService {
|
|
722
|
+
logger;
|
|
723
|
+
debug;
|
|
724
|
+
useDocumentClassification;
|
|
725
|
+
session;
|
|
726
|
+
mobileNetSession;
|
|
727
|
+
mobileNetClassMap;
|
|
728
|
+
modelLoaded = false;
|
|
729
|
+
deviceStrategy;
|
|
730
|
+
MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v2.onnx";
|
|
731
|
+
MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
|
|
732
|
+
MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
|
|
733
|
+
INPUT_SIZE = 320;
|
|
734
|
+
CONFIDENCE_THRESHOLD = 0.6;
|
|
735
|
+
// Canvas pool for optimization
|
|
736
|
+
preprocessCanvas;
|
|
737
|
+
preprocessCtx;
|
|
738
|
+
captureCanvas;
|
|
739
|
+
constructor(logger, debug = false, useDocumentClassification = false) {
|
|
740
|
+
this.logger = logger;
|
|
741
|
+
this.debug = debug;
|
|
742
|
+
this.useDocumentClassification = useDocumentClassification;
|
|
743
|
+
this.deviceStrategy = DeviceStrategyFactory.createStrategy();
|
|
744
|
+
this.initializeCanvasPool();
|
|
745
|
+
}
|
|
746
|
+
async loadModel() {
|
|
747
|
+
if (this.modelLoaded || this.session) {
|
|
748
|
+
this.logger.state('MODELO_YA_PRECARGADO', {
|
|
749
|
+
sessionExists: !!this.session,
|
|
750
|
+
modelPreloaded: this.modelLoaded
|
|
751
|
+
});
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
|
|
756
|
+
const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
|
|
757
|
+
try {
|
|
758
|
+
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
if (error.message.includes('failed to allocate a buffer')) {
|
|
762
|
+
this.logger.warn('Fallo en asignación de buffer durante precarga, intentando con configuración mínima');
|
|
763
|
+
const fallbackOptions = {
|
|
764
|
+
executionProviders: ['wasm'],
|
|
765
|
+
graphOptimizationLevel: 'disabled',
|
|
766
|
+
logSeverityLevel: 4,
|
|
767
|
+
enableCpuMemArena: false,
|
|
768
|
+
enableMemPattern: false,
|
|
769
|
+
executionMode: 'sequential',
|
|
770
|
+
interOpNumThreads: 1,
|
|
771
|
+
intraOpNumThreads: 1,
|
|
772
|
+
};
|
|
773
|
+
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
this.modelLoaded = true;
|
|
780
|
+
this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
this.logger.error('Error al precargar modelo de detección:', error);
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async loadClassificationModel() {
|
|
788
|
+
if (!this.useDocumentClassification) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
|
|
793
|
+
// Load class map
|
|
794
|
+
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
795
|
+
if (!classResponse.ok) {
|
|
796
|
+
throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
|
|
797
|
+
}
|
|
798
|
+
this.mobileNetClassMap = await classResponse.json();
|
|
799
|
+
this.logger.state('CLASES_MOBILENET_CARGADAS', {
|
|
800
|
+
classCount: Object.keys(this.mobileNetClassMap).length
|
|
801
|
+
});
|
|
802
|
+
// Load model
|
|
803
|
+
const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
|
|
804
|
+
try {
|
|
805
|
+
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
if (error.message.includes('failed to allocate a buffer')) {
|
|
809
|
+
this.logger.warn('Fallo en asignación de buffer de MobileNet, intentando con configuración mínima');
|
|
810
|
+
const fallbackOptions = {
|
|
811
|
+
executionProviders: ['wasm'],
|
|
812
|
+
graphOptimizationLevel: 'disabled',
|
|
813
|
+
logSeverityLevel: 4,
|
|
814
|
+
enableCpuMemArena: false,
|
|
815
|
+
enableMemPattern: false,
|
|
816
|
+
executionMode: 'sequential',
|
|
817
|
+
interOpNumThreads: 1,
|
|
818
|
+
intraOpNumThreads: 1,
|
|
819
|
+
};
|
|
820
|
+
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
|
|
827
|
+
sessionCreated: !!this.mobileNetSession
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
this.logger.error('Error al cargar modelo MobileNet:', error);
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
isModelLoaded() {
|
|
836
|
+
return this.modelLoaded && !!this.session;
|
|
837
|
+
}
|
|
838
|
+
preprocess(video) {
|
|
839
|
+
if (!this.preprocessCanvas || !this.preprocessCtx) {
|
|
840
|
+
this.initializeCanvasPool();
|
|
841
|
+
}
|
|
842
|
+
this.preprocessCtx.clearRect(0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
|
|
843
|
+
this.preprocessCtx.drawImage(video, 0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
|
|
844
|
+
const imageData = this.preprocessCtx.getImageData(0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
|
|
845
|
+
const [R, G, B] = [[], [], []];
|
|
846
|
+
const { data } = imageData;
|
|
847
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
848
|
+
R.push(data[i] / 255);
|
|
849
|
+
G.push(data[i + 1] / 255);
|
|
850
|
+
B.push(data[i + 2] / 255);
|
|
851
|
+
}
|
|
852
|
+
const transposedData = new Float32Array(R.concat(G, B));
|
|
853
|
+
const float16Data = new Uint16Array(transposedData.length);
|
|
854
|
+
for (let i = 0; i < transposedData.length; i++) {
|
|
855
|
+
float16Data[i] = this.float32ToFloat16(transposedData[i]);
|
|
856
|
+
}
|
|
857
|
+
return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
|
|
858
|
+
}
|
|
859
|
+
async runInference(inputTensor) {
|
|
860
|
+
if (!this.session) {
|
|
861
|
+
throw new Error('Detection model not loaded');
|
|
862
|
+
}
|
|
863
|
+
const feeds = { [this.session.inputNames[0]]: inputTensor };
|
|
864
|
+
const results = await this.session.run(feeds);
|
|
865
|
+
const output = results[this.session.outputNames[0]].data;
|
|
866
|
+
const detections = [];
|
|
867
|
+
for (let i = 0; i < output.length; i += 6) {
|
|
868
|
+
const [x1, y1, x2, y2, score, classId] = output.slice(i, i + 6);
|
|
869
|
+
if (score > this.CONFIDENCE_THRESHOLD) {
|
|
870
|
+
detections.push({
|
|
871
|
+
x: x1,
|
|
872
|
+
y: y1,
|
|
873
|
+
w: x2 - x1,
|
|
874
|
+
h: y2 - y1,
|
|
875
|
+
score,
|
|
876
|
+
classId
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return detections;
|
|
881
|
+
}
|
|
882
|
+
async classifyDocument(canvas) {
|
|
883
|
+
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
884
|
+
this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
|
|
889
|
+
const inputTensor = this.preprocessMobileNet(canvas);
|
|
890
|
+
const feeds = { input: inputTensor };
|
|
891
|
+
const results = await this.mobileNetSession.run(feeds);
|
|
892
|
+
const output = results[Object.keys(results)[0]].data;
|
|
893
|
+
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
894
|
+
const confidence = output[maxIdx];
|
|
895
|
+
const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
|
|
896
|
+
this.logger.state('DOCUMENTO_CLASIFICADO', {
|
|
897
|
+
class: className,
|
|
898
|
+
confidence: confidence.toFixed(3),
|
|
899
|
+
classIndex: maxIdx,
|
|
900
|
+
timestamp: Date.now()
|
|
901
|
+
});
|
|
902
|
+
return {
|
|
903
|
+
class: className,
|
|
904
|
+
confidence: confidence,
|
|
905
|
+
classIndex: maxIdx
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
this.logger.error('Error al clasificar documento:', error);
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
checkSideAlignment(box, maskConfig) {
|
|
914
|
+
const { INPUT_SIZE, ID1_ASPECT_RATIO, shouldMirrorVideo, alignmentTolerance, maskSize, videoRef } = maskConfig;
|
|
915
|
+
if (!videoRef) {
|
|
916
|
+
return { top: false, right: false, bottom: false, left: false };
|
|
917
|
+
}
|
|
918
|
+
// Get video dimensions to calculate actual mask boundaries in model space
|
|
919
|
+
const videoWidth = videoRef.videoWidth;
|
|
920
|
+
const videoHeight = videoRef.videoHeight;
|
|
921
|
+
if (videoWidth === 0 || videoHeight === 0) {
|
|
922
|
+
return { top: false, right: false, bottom: false, left: false };
|
|
923
|
+
}
|
|
924
|
+
// Calculate video aspect ratio
|
|
925
|
+
const videoAspectRatio = videoWidth / videoHeight;
|
|
926
|
+
// The model sees video stretched to 320x320, but we need to calculate where
|
|
927
|
+
// the mask should be in this distorted space to match the visual mask
|
|
928
|
+
// In the visual display, we calculate mask size based on the limiting dimension
|
|
929
|
+
// We need to replicate this logic but account for the distortion in model space
|
|
930
|
+
// Calculate the "display" dimensions (what the visual mask calculations use)
|
|
931
|
+
const containerAspectRatio = 1; // Assume square container for simplicity
|
|
932
|
+
let displayedVideoWidth, displayedVideoHeight;
|
|
933
|
+
if (videoAspectRatio > containerAspectRatio) {
|
|
934
|
+
// Video is wider - letterboxed in display
|
|
935
|
+
displayedVideoWidth = INPUT_SIZE;
|
|
936
|
+
displayedVideoHeight = INPUT_SIZE / videoAspectRatio;
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
// Video is taller - pillarboxed in display
|
|
940
|
+
displayedVideoHeight = INPUT_SIZE;
|
|
941
|
+
displayedVideoWidth = INPUT_SIZE * videoAspectRatio;
|
|
942
|
+
}
|
|
943
|
+
// Calculate mask dimensions using the same logic as updateMaskDimensions
|
|
944
|
+
const maxWidthByHeight = displayedVideoHeight * ID1_ASPECT_RATIO;
|
|
945
|
+
let visualMaskWidth, visualMaskHeight;
|
|
946
|
+
const sizeRatio = maskSize / 100;
|
|
947
|
+
if (maxWidthByHeight <= displayedVideoWidth) {
|
|
948
|
+
// Video height is the limiting factor
|
|
949
|
+
visualMaskHeight = displayedVideoHeight * sizeRatio;
|
|
950
|
+
visualMaskWidth = visualMaskHeight * ID1_ASPECT_RATIO;
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
// Video width is the limiting factor
|
|
954
|
+
visualMaskWidth = displayedVideoWidth * sizeRatio;
|
|
955
|
+
visualMaskHeight = visualMaskWidth / ID1_ASPECT_RATIO;
|
|
956
|
+
}
|
|
957
|
+
// Now map these visual mask dimensions to the distorted model space
|
|
958
|
+
// The model stretches video to 320x320, so we need to apply the inverse transformation
|
|
959
|
+
const modelMaskWidth = visualMaskWidth * (INPUT_SIZE / displayedVideoWidth);
|
|
960
|
+
const modelMaskHeight = visualMaskHeight * (INPUT_SIZE / displayedVideoHeight);
|
|
961
|
+
// Calculate mask boundaries in model coordinate system (always centered in 320x320)
|
|
962
|
+
const maskCenterX = INPUT_SIZE / 2;
|
|
963
|
+
const maskCenterY = INPUT_SIZE / 2;
|
|
964
|
+
const maskLeft = maskCenterX - (modelMaskWidth / 2);
|
|
965
|
+
const maskRight = maskCenterX + (modelMaskWidth / 2);
|
|
966
|
+
const maskTop = maskCenterY - (modelMaskHeight / 2);
|
|
967
|
+
const maskBottom = maskCenterY + (modelMaskHeight / 2);
|
|
968
|
+
// Obtener las coordenadas del documento detectado
|
|
969
|
+
let docLeft = box.x;
|
|
970
|
+
let docRight = box.x + box.w;
|
|
971
|
+
const docTop = box.y;
|
|
972
|
+
const docBottom = box.y + box.h;
|
|
973
|
+
// Si el video está en modo mirror, invertir las coordenadas horizontales
|
|
974
|
+
if (shouldMirrorVideo) {
|
|
975
|
+
const originalDocLeft = docLeft;
|
|
976
|
+
const originalDocRight = docRight;
|
|
977
|
+
docLeft = INPUT_SIZE - originalDocRight;
|
|
978
|
+
docRight = INPUT_SIZE - originalDocLeft;
|
|
979
|
+
}
|
|
980
|
+
// Tolerancia para considerar que un lado está alineado (en píxeles)
|
|
981
|
+
const tolerance = alignmentTolerance;
|
|
982
|
+
// Verificar alineación de esquinas más estricta
|
|
983
|
+
// Una esquina está alineada solo si AMBOS bordes que la forman están alineados
|
|
984
|
+
const topAligned = Math.abs(docTop - maskTop) <= tolerance;
|
|
985
|
+
const rightAligned = Math.abs(docRight - maskRight) <= tolerance;
|
|
986
|
+
const bottomAligned = Math.abs(docBottom - maskBottom) <= tolerance;
|
|
987
|
+
const leftAligned = Math.abs(docLeft - maskLeft) <= tolerance;
|
|
988
|
+
return {
|
|
989
|
+
top: topAligned && leftAligned, // Esquina superior izquierda: borde top Y left alineados
|
|
990
|
+
right: topAligned && rightAligned, // Esquina superior derecha: borde top Y right alineados
|
|
991
|
+
bottom: bottomAligned && leftAligned, // Esquina inferior izquierda: borde bottom Y left alineados
|
|
992
|
+
left: bottomAligned && rightAligned // Esquina inferior derecha: borde bottom Y right alineados
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
isCardInFrame(box) {
|
|
996
|
+
const cardCenterX = box.x + box.w / 2;
|
|
997
|
+
const cardCenterY = box.y + box.h / 2;
|
|
998
|
+
const frameCenterX = this.INPUT_SIZE / 2;
|
|
999
|
+
const frameCenterY = this.INPUT_SIZE / 2;
|
|
1000
|
+
const toleranceX = 40;
|
|
1001
|
+
const toleranceY = 30;
|
|
1002
|
+
const isCentered = Math.abs(cardCenterX - frameCenterX) < toleranceX &&
|
|
1003
|
+
Math.abs(cardCenterY - frameCenterY) < toleranceY;
|
|
1004
|
+
const isGoodSize = box.w > 150 && box.w < 300 && box.h > 90 && box.h < 200;
|
|
1005
|
+
return isCentered && isGoodSize;
|
|
1006
|
+
}
|
|
1007
|
+
areAllSidesAligned(alignment) {
|
|
1008
|
+
return alignment.top && alignment.right && alignment.bottom && alignment.left;
|
|
1009
|
+
}
|
|
1010
|
+
cleanup() {
|
|
1011
|
+
this.cleanupCanvasPool();
|
|
1012
|
+
if (this.session) {
|
|
1013
|
+
this.session.release?.();
|
|
1014
|
+
this.session = undefined;
|
|
1015
|
+
}
|
|
1016
|
+
if (this.mobileNetSession) {
|
|
1017
|
+
this.mobileNetSession.release?.();
|
|
1018
|
+
this.mobileNetSession = undefined;
|
|
1019
|
+
}
|
|
1020
|
+
this.mobileNetClassMap = undefined;
|
|
1021
|
+
this.modelLoaded = false;
|
|
1022
|
+
this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
|
|
1023
|
+
}
|
|
1024
|
+
initializeCanvasPool() {
|
|
1025
|
+
this.preprocessCanvas = document.createElement('canvas');
|
|
1026
|
+
this.preprocessCanvas.width = this.INPUT_SIZE;
|
|
1027
|
+
this.preprocessCanvas.height = this.INPUT_SIZE;
|
|
1028
|
+
this.preprocessCtx = this.preprocessCanvas.getContext('2d', {
|
|
1029
|
+
alpha: false,
|
|
1030
|
+
willReadFrequently: true
|
|
1031
|
+
});
|
|
1032
|
+
this.captureCanvas = document.createElement('canvas');
|
|
1033
|
+
this.logger.state('CANVAS_POOL_INICIALIZADO', {
|
|
1034
|
+
preprocessCanvasSize: this.INPUT_SIZE
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
cleanupCanvasPool() {
|
|
1038
|
+
if (this.preprocessCanvas) {
|
|
1039
|
+
this.preprocessCtx = undefined;
|
|
1040
|
+
this.preprocessCanvas = undefined;
|
|
1041
|
+
}
|
|
1042
|
+
if (this.captureCanvas) {
|
|
1043
|
+
this.captureCanvas = undefined;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
float32ToFloat16(value) {
|
|
1047
|
+
const buffer = new ArrayBuffer(4);
|
|
1048
|
+
const view = new DataView(buffer);
|
|
1049
|
+
view.setFloat32(0, value, true);
|
|
1050
|
+
const f = view.getUint32(0, true);
|
|
1051
|
+
const sign = (f >> 31) & 0x1;
|
|
1052
|
+
const exp = (f >> 23) & 0xFF;
|
|
1053
|
+
const frac = f & 0x7FFFFF;
|
|
1054
|
+
let newExp = exp - 127 + 15;
|
|
1055
|
+
if (exp === 0) {
|
|
1056
|
+
newExp = 0;
|
|
1057
|
+
}
|
|
1058
|
+
else if (exp === 0xFF) {
|
|
1059
|
+
newExp = 0x1F;
|
|
1060
|
+
}
|
|
1061
|
+
else if (newExp >= 0x1F) {
|
|
1062
|
+
newExp = 0x1F;
|
|
1063
|
+
return (sign << 15) | (newExp << 10);
|
|
1064
|
+
}
|
|
1065
|
+
else if (newExp <= 0) {
|
|
1066
|
+
return (sign << 15);
|
|
1067
|
+
}
|
|
1068
|
+
return (sign << 15) | (newExp << 10) | (frac >> 13);
|
|
1069
|
+
}
|
|
1070
|
+
preprocessMobileNet(canvas) {
|
|
1071
|
+
const tempCanvas = document.createElement('canvas');
|
|
1072
|
+
tempCanvas.width = 224;
|
|
1073
|
+
tempCanvas.height = 224;
|
|
1074
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
1075
|
+
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
1076
|
+
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
1077
|
+
const data = imageData.data;
|
|
1078
|
+
const hw = 224 * 224;
|
|
1079
|
+
const arr = new Float32Array(3 * hw);
|
|
1080
|
+
for (let i = 0; i < hw; i++) {
|
|
1081
|
+
arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5;
|
|
1082
|
+
arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5;
|
|
1083
|
+
arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5;
|
|
1084
|
+
}
|
|
1085
|
+
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
class ServiceContainer {
|
|
1090
|
+
services = new Map();
|
|
1091
|
+
constructor(config) {
|
|
1092
|
+
this.initializeServices(config);
|
|
1093
|
+
}
|
|
1094
|
+
initializeServices(config) {
|
|
1095
|
+
// Initialize services in dependency order
|
|
1096
|
+
const eventBus = new EventBusService();
|
|
1097
|
+
const logger = new LoggerService(config.debug);
|
|
1098
|
+
const stateManager = new StateManagerService(eventBus);
|
|
1099
|
+
const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
|
|
1100
|
+
const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
|
|
1101
|
+
// Register services
|
|
1102
|
+
this.services.set('eventBus', eventBus);
|
|
1103
|
+
this.services.set('logger', logger);
|
|
1104
|
+
this.services.set('stateManager', stateManager);
|
|
1105
|
+
this.services.set('cameraService', cameraService);
|
|
1106
|
+
this.services.set('detectionService', detectionService);
|
|
1107
|
+
}
|
|
1108
|
+
get(serviceName) {
|
|
1109
|
+
const service = this.services.get(serviceName);
|
|
1110
|
+
if (!service) {
|
|
1111
|
+
throw new Error(`Service ${serviceName} not found`);
|
|
1112
|
+
}
|
|
1113
|
+
return service;
|
|
1114
|
+
}
|
|
1115
|
+
getLogger() {
|
|
1116
|
+
return this.get('logger');
|
|
1117
|
+
}
|
|
1118
|
+
getEventBus() {
|
|
1119
|
+
return this.get('eventBus');
|
|
1120
|
+
}
|
|
1121
|
+
getStateManager() {
|
|
1122
|
+
return this.get('stateManager');
|
|
1123
|
+
}
|
|
1124
|
+
getCameraService() {
|
|
1125
|
+
return this.get('cameraService');
|
|
1126
|
+
}
|
|
1127
|
+
getDetectionService() {
|
|
1128
|
+
return this.get('detectionService');
|
|
1129
|
+
}
|
|
1130
|
+
updateConfig(config) {
|
|
1131
|
+
if (config.debug !== undefined) {
|
|
1132
|
+
const logger = this.services.get('logger');
|
|
1133
|
+
logger.setDebugMode(config.debug);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
cleanup() {
|
|
1137
|
+
// Cleanup services in reverse order
|
|
1138
|
+
this.getDetectionService().cleanup();
|
|
1139
|
+
this.getEventBus().clear();
|
|
1140
|
+
this.services.clear();
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#fff;font-size:14px;font-weight:600;text-align:center;white-space:normal;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;z-index:20}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-button{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateX(-50%) translateY(0)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:space-between;align-items:center;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:8px;font-weight:400}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}";
|
|
4
1145
|
|
|
5
1146
|
const JaakStamps = class {
|
|
6
1147
|
constructor(hostRef) {
|
|
@@ -10,131 +1151,223 @@ const JaakStamps = class {
|
|
|
10
1151
|
}
|
|
11
1152
|
get el() { return getElement(this); }
|
|
12
1153
|
debug = false;
|
|
13
|
-
alignmentTolerance =
|
|
14
|
-
maskSize =
|
|
15
|
-
cropMargin =
|
|
16
|
-
useDocumentClassification = false;
|
|
1154
|
+
alignmentTolerance = 15;
|
|
1155
|
+
maskSize = 80;
|
|
1156
|
+
cropMargin = 20;
|
|
1157
|
+
useDocumentClassification = false;
|
|
1158
|
+
preferredCamera = 'auto';
|
|
1159
|
+
captureDelay = 1500;
|
|
17
1160
|
captureCompleted;
|
|
18
1161
|
isReady;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
statusColor = '#aaa';
|
|
22
|
-
bestScore = 0;
|
|
23
|
-
capturedFullFrame = null;
|
|
24
|
-
capturedCroppedId = null;
|
|
25
|
-
capturedBackFullFrame = null;
|
|
26
|
-
capturedBackCroppedId = null;
|
|
27
|
-
captureStep = 'front';
|
|
28
|
-
isCapturing = false;
|
|
29
|
-
showFlipAnimation = false;
|
|
30
|
-
showSuccessAnimation = false;
|
|
31
|
-
shouldMirrorVideo = true;
|
|
1162
|
+
// State derived from services
|
|
1163
|
+
detectionBoxes = [];
|
|
32
1164
|
sideAlignment = {
|
|
33
|
-
top: false,
|
|
34
|
-
right: false,
|
|
35
|
-
bottom: false,
|
|
36
|
-
left: false
|
|
1165
|
+
top: false, right: false, bottom: false, left: false
|
|
37
1166
|
};
|
|
38
|
-
isDetectionPaused = false;
|
|
39
|
-
isLoading = false;
|
|
40
|
-
isModelPreloaded = false;
|
|
41
1167
|
isMaskReady = false;
|
|
1168
|
+
shouldMirrorVideo = true;
|
|
1169
|
+
showCameraSelector = false;
|
|
1170
|
+
isSwitchingCamera = false;
|
|
1171
|
+
hasDocumentDetected = false;
|
|
1172
|
+
currentStatus = {
|
|
1173
|
+
message: 'Inicializando componente...',
|
|
1174
|
+
description: 'Configurando servicios y cargando recursos',
|
|
1175
|
+
type: 'initializing',
|
|
1176
|
+
isInitialized: false
|
|
1177
|
+
};
|
|
1178
|
+
performanceData = {
|
|
1179
|
+
fps: 0,
|
|
1180
|
+
inferenceTime: 0,
|
|
1181
|
+
memoryUsage: 0,
|
|
1182
|
+
onnxLoadTime: 0,
|
|
1183
|
+
frameProcessingTime: 0,
|
|
1184
|
+
totalDetections: 0,
|
|
1185
|
+
successfulDetections: 0,
|
|
1186
|
+
detectionRate: 0
|
|
1187
|
+
};
|
|
1188
|
+
// Services
|
|
1189
|
+
serviceContainer;
|
|
1190
|
+
logger;
|
|
1191
|
+
eventBus;
|
|
1192
|
+
stateManager;
|
|
1193
|
+
cameraService;
|
|
1194
|
+
detectionService;
|
|
1195
|
+
// UI References
|
|
42
1196
|
videoRef;
|
|
43
|
-
|
|
44
|
-
session;
|
|
45
|
-
startTime;
|
|
1197
|
+
detectionContainer;
|
|
46
1198
|
videoStream;
|
|
47
1199
|
animationId;
|
|
48
|
-
|
|
1200
|
+
// Detection state
|
|
49
1201
|
lastDetectedBox;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1202
|
+
startTime;
|
|
1203
|
+
hasScreenshotTaken = false;
|
|
1204
|
+
alignmentStartTime;
|
|
1205
|
+
alignmentTimer;
|
|
1206
|
+
// Performance monitoring
|
|
1207
|
+
performanceMetrics = {
|
|
1208
|
+
fps: 0,
|
|
1209
|
+
inferenceTime: 0,
|
|
1210
|
+
memoryUsage: 0,
|
|
1211
|
+
cpuUsage: 0,
|
|
1212
|
+
onnxLoadTime: 0,
|
|
1213
|
+
frameProcessingTime: 0,
|
|
1214
|
+
totalDetections: 0,
|
|
1215
|
+
successfulDetections: 0,
|
|
1216
|
+
detectionRate: 0,
|
|
1217
|
+
lastUpdateTime: 0
|
|
1218
|
+
};
|
|
1219
|
+
performanceUpdateInterval;
|
|
1220
|
+
// Performance optimization
|
|
53
1221
|
frameSkipCounter = 0;
|
|
54
|
-
FRAME_SKIP = 2;
|
|
1222
|
+
FRAME_SKIP = 2;
|
|
55
1223
|
consecutiveFailures = 0;
|
|
56
|
-
MAX_FAILURES = 30;
|
|
1224
|
+
MAX_FAILURES = 30;
|
|
57
1225
|
lastInferenceTime = 0;
|
|
58
|
-
MIN_INFERENCE_INTERVAL = 50;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1226
|
+
MIN_INFERENCE_INTERVAL = 50;
|
|
1227
|
+
async componentDidLoad() {
|
|
1228
|
+
this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
|
|
1229
|
+
await this.initializeServices();
|
|
1230
|
+
this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
|
|
1231
|
+
await this.setupEventListeners();
|
|
1232
|
+
this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
1233
|
+
await this.initializeComponent();
|
|
1234
|
+
if (this.debug) {
|
|
1235
|
+
this.initializePerformanceMonitor();
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async initializeServices() {
|
|
1239
|
+
const config = {
|
|
1240
|
+
debug: this.debug,
|
|
1241
|
+
alignmentTolerance: this.alignmentTolerance,
|
|
1242
|
+
maskSize: this.maskSize,
|
|
1243
|
+
cropMargin: this.cropMargin,
|
|
1244
|
+
useDocumentClassification: this.useDocumentClassification,
|
|
1245
|
+
preferredCamera: this.preferredCamera,
|
|
1246
|
+
captureDelay: this.captureDelay
|
|
1247
|
+
};
|
|
1248
|
+
this.serviceContainer = new ServiceContainer(config);
|
|
1249
|
+
this.logger = this.serviceContainer.getLogger();
|
|
1250
|
+
this.eventBus = this.serviceContainer.getEventBus();
|
|
1251
|
+
this.stateManager = this.serviceContainer.getStateManager();
|
|
1252
|
+
this.cameraService = this.serviceContainer.getCameraService();
|
|
1253
|
+
this.detectionService = this.serviceContainer.getDetectionService();
|
|
1254
|
+
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
1255
|
+
}
|
|
1256
|
+
async setupEventListeners() {
|
|
1257
|
+
this.eventBus.on('state-changed', (data) => {
|
|
1258
|
+
this.handleStateChange(data);
|
|
1259
|
+
});
|
|
1260
|
+
this.eventBus.on('camera-changed', (cameraId) => {
|
|
1261
|
+
this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
|
|
1262
|
+
});
|
|
1263
|
+
this.eventBus.on('error', (error) => {
|
|
1264
|
+
this.logger.error('Error en servicio:', error);
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
async initializeComponent() {
|
|
1268
|
+
this.logger.state('COMPONENTE_INICIALIZANDO', {
|
|
1269
|
+
debug: this.debug,
|
|
1270
|
+
maskSize: this.maskSize,
|
|
1271
|
+
cropMargin: this.cropMargin,
|
|
1272
|
+
useDocumentClassification: this.useDocumentClassification,
|
|
1273
|
+
preferredCamera: this.preferredCamera,
|
|
1274
|
+
captureDelay: this.captureDelay
|
|
1275
|
+
});
|
|
1276
|
+
this.validateProps();
|
|
72
1277
|
if (this.debug) {
|
|
73
|
-
|
|
1278
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1279
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
74
1280
|
}
|
|
1281
|
+
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
1282
|
+
await this.cameraService.detectDeviceType();
|
|
1283
|
+
await this.cameraService.enumerateDevices();
|
|
1284
|
+
this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
|
|
1285
|
+
await this.loadOnnxRuntime();
|
|
1286
|
+
this.initializeResizeObserver();
|
|
75
1287
|
}
|
|
76
|
-
|
|
1288
|
+
validateProps() {
|
|
77
1289
|
if (this.maskSize < 50 || this.maskSize > 100) {
|
|
78
|
-
|
|
1290
|
+
this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
|
|
79
1291
|
this.maskSize = 90;
|
|
80
1292
|
}
|
|
81
|
-
}
|
|
82
|
-
validateCropMargin() {
|
|
83
1293
|
if (this.cropMargin < 0 || this.cropMargin > 100) {
|
|
84
|
-
|
|
1294
|
+
this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
|
|
85
1295
|
this.cropMargin = 0;
|
|
86
1296
|
}
|
|
1297
|
+
const validOptions = ['auto', 'front', 'back'];
|
|
1298
|
+
if (!validOptions.includes(this.preferredCamera)) {
|
|
1299
|
+
this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
|
|
1300
|
+
this.preferredCamera = 'auto';
|
|
1301
|
+
}
|
|
1302
|
+
if (this.captureDelay < 0 || this.captureDelay > 10000) {
|
|
1303
|
+
this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
|
|
1304
|
+
this.captureDelay = 1500;
|
|
1305
|
+
}
|
|
87
1306
|
}
|
|
88
|
-
|
|
89
|
-
const isDocumentReady = !!window.ort && this.isModelPreloaded;
|
|
90
|
-
this.isReady.emit(isDocumentReady);
|
|
91
|
-
this.debugLog('🟢 isReady event emitted:', isDocumentReady);
|
|
92
|
-
}
|
|
93
|
-
isRearCamera(stream) {
|
|
94
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
95
|
-
if (!videoTrack)
|
|
96
|
-
return false;
|
|
97
|
-
const settings = videoTrack.getSettings();
|
|
98
|
-
return settings.facingMode === 'environment';
|
|
99
|
-
}
|
|
100
|
-
async componentDidLoad() {
|
|
101
|
-
this.validateMaskSize();
|
|
102
|
-
this.validateCropMargin();
|
|
1307
|
+
async loadOnnxRuntime() {
|
|
103
1308
|
if (!window.ort) {
|
|
1309
|
+
this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
|
|
104
1310
|
const script = document.createElement('script');
|
|
105
1311
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
106
1312
|
document.head.appendChild(script);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1313
|
+
await new Promise((resolve) => {
|
|
1314
|
+
script.onload = () => {
|
|
1315
|
+
setTimeout(() => {
|
|
1316
|
+
this.finalizeInitialization();
|
|
1317
|
+
resolve(undefined);
|
|
1318
|
+
}, 300);
|
|
1319
|
+
};
|
|
1320
|
+
});
|
|
111
1321
|
}
|
|
112
1322
|
else {
|
|
113
|
-
|
|
114
|
-
|
|
1323
|
+
setTimeout(() => {
|
|
1324
|
+
this.finalizeInitialization();
|
|
1325
|
+
}, 300);
|
|
115
1326
|
}
|
|
116
|
-
// Initialize canvas pool for better performance
|
|
117
|
-
this.initializeCanvasPool();
|
|
118
|
-
this.setupResizeObserver();
|
|
119
1327
|
}
|
|
120
|
-
|
|
121
|
-
|
|
1328
|
+
finalizeInitialization() {
|
|
1329
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1330
|
+
this.currentStatus = {
|
|
1331
|
+
message: 'Listo para capturar',
|
|
1332
|
+
description: '',
|
|
1333
|
+
type: 'ready',
|
|
1334
|
+
isInitialized: true
|
|
1335
|
+
};
|
|
1336
|
+
this.emitReadyEvent();
|
|
1337
|
+
}
|
|
1338
|
+
updateStatus(message, description, type = 'loading') {
|
|
1339
|
+
this.currentStatus = {
|
|
1340
|
+
message,
|
|
1341
|
+
description,
|
|
1342
|
+
type,
|
|
1343
|
+
isInitialized: this.currentStatus.isInitialized
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
emitReadyEvent() {
|
|
1347
|
+
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
1348
|
+
this.isReady.emit(isDocumentReady);
|
|
1349
|
+
this.logger.state('COMPONENTE_LISTO', {
|
|
1350
|
+
ortLibraryLoaded: !!window.ort,
|
|
1351
|
+
modelPreloaded: this.detectionService.isModelLoaded(),
|
|
1352
|
+
isReady: isDocumentReady
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
handleStateChange(data) {
|
|
1356
|
+
}
|
|
1357
|
+
initializeResizeObserver() {
|
|
1358
|
+
if ('ResizeObserver' in window && this.detectionContainer) {
|
|
122
1359
|
const resizeObserver = new ResizeObserver(() => {
|
|
123
1360
|
this.handleResize();
|
|
124
1361
|
});
|
|
125
|
-
resizeObserver.observe(this.
|
|
1362
|
+
resizeObserver.observe(this.detectionContainer.parentElement);
|
|
126
1363
|
}
|
|
127
1364
|
}
|
|
128
1365
|
handleResize() {
|
|
129
|
-
if (this.
|
|
130
|
-
const container = this.
|
|
1366
|
+
if (this.detectionContainer) {
|
|
1367
|
+
const container = this.detectionContainer.parentElement;
|
|
131
1368
|
const rect = container.getBoundingClientRect();
|
|
132
|
-
// Set canvas size to match container
|
|
133
|
-
this.canvasRef.width = rect.width;
|
|
134
|
-
this.canvasRef.height = rect.height;
|
|
135
|
-
// Update mask positioning based on container and video dimensions
|
|
136
1369
|
this.updateMaskDimensions(rect);
|
|
137
|
-
this.
|
|
1370
|
+
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
138
1371
|
}
|
|
139
1372
|
}
|
|
140
1373
|
updateMaskDimensions(containerRect) {
|
|
@@ -151,32 +1384,27 @@ const JaakStamps = class {
|
|
|
151
1384
|
let displayedVideoWidth, displayedVideoHeight;
|
|
152
1385
|
let videoOffsetX = 0, videoOffsetY = 0;
|
|
153
1386
|
if (videoAspectRatio > containerAspectRatio) {
|
|
154
|
-
// Video is wider - letterboxed (black bars top/bottom)
|
|
155
1387
|
displayedVideoWidth = containerRect.width;
|
|
156
1388
|
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
157
1389
|
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
158
1390
|
}
|
|
159
1391
|
else {
|
|
160
|
-
// Video is taller - pillarboxed (black bars left/right)
|
|
161
1392
|
displayedVideoHeight = containerRect.height;
|
|
162
1393
|
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
163
1394
|
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
164
1395
|
}
|
|
165
|
-
// Calculate
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const maxWidthByHeight = displayedVideoHeight * this.ID1_ASPECT_RATIO;
|
|
1396
|
+
// Calculate mask dimensions with ID-1 aspect ratio
|
|
1397
|
+
const ID1_ASPECT_RATIO = 85.60 / 53.98;
|
|
1398
|
+
const maxWidthByHeight = displayedVideoHeight * ID1_ASPECT_RATIO;
|
|
169
1399
|
let maskWidthInVideo, maskHeightInVideo;
|
|
170
1400
|
const sizeRatio = this.maskSize / 100;
|
|
171
1401
|
if (maxWidthByHeight <= displayedVideoWidth) {
|
|
172
|
-
// Video height is the limiting factor
|
|
173
1402
|
maskHeightInVideo = displayedVideoHeight * sizeRatio;
|
|
174
|
-
maskWidthInVideo = maskHeightInVideo *
|
|
1403
|
+
maskWidthInVideo = maskHeightInVideo * ID1_ASPECT_RATIO;
|
|
175
1404
|
}
|
|
176
1405
|
else {
|
|
177
|
-
// Video width is the limiting factor
|
|
178
1406
|
maskWidthInVideo = displayedVideoWidth * sizeRatio;
|
|
179
|
-
maskHeightInVideo = maskWidthInVideo /
|
|
1407
|
+
maskHeightInVideo = maskWidthInVideo / ID1_ASPECT_RATIO;
|
|
180
1408
|
}
|
|
181
1409
|
// Convert to percentages of the container
|
|
182
1410
|
const maskWidthPercent = (maskWidthInVideo / containerRect.width) * 100;
|
|
@@ -192,9 +1420,8 @@ const JaakStamps = class {
|
|
|
192
1420
|
this.el.style.setProperty('--mask-height', `${maskHeightPercent}%`);
|
|
193
1421
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
194
1422
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
195
|
-
// Mark mask as ready now that dimensions are calculated
|
|
196
1423
|
this.isMaskReady = true;
|
|
197
|
-
this.
|
|
1424
|
+
this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
|
|
198
1425
|
video: { width: videoWidth, height: videoHeight },
|
|
199
1426
|
displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
|
|
200
1427
|
mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
|
|
@@ -202,445 +1429,270 @@ const JaakStamps = class {
|
|
|
202
1429
|
offset: { x: videoOffsetX, y: videoOffsetY }
|
|
203
1430
|
});
|
|
204
1431
|
}
|
|
205
|
-
|
|
206
|
-
// Preprocess canvas - reused for every inference
|
|
207
|
-
this.preprocessCanvas = document.createElement('canvas');
|
|
208
|
-
this.preprocessCanvas.width = this.INPUT_SIZE;
|
|
209
|
-
this.preprocessCanvas.height = this.INPUT_SIZE;
|
|
210
|
-
this.preprocessCtx = this.preprocessCanvas.getContext('2d', {
|
|
211
|
-
alpha: false, // No transparency needed
|
|
212
|
-
willReadFrequently: true // Optimize for frequent getImageData calls
|
|
213
|
-
});
|
|
214
|
-
// Capture canvas - reused for screenshots
|
|
215
|
-
this.captureCanvas = document.createElement('canvas');
|
|
216
|
-
this.captureCtx = this.captureCanvas.getContext('2d', {
|
|
217
|
-
alpha: false
|
|
218
|
-
});
|
|
219
|
-
this.debugLog('🎨 Canvas pool initialized for performance');
|
|
220
|
-
}
|
|
221
|
-
disconnectedCallback() {
|
|
222
|
-
this.cleanup();
|
|
223
|
-
}
|
|
1432
|
+
// PUBLIC METHODS
|
|
224
1433
|
async getCapturedImages() {
|
|
225
|
-
|
|
1434
|
+
const state = this.stateManager.getCaptureState();
|
|
1435
|
+
if (state.step !== 'completed') {
|
|
226
1436
|
throw new Error('El proceso de captura no ha sido completado');
|
|
227
1437
|
}
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await this.startDetection();
|
|
248
|
-
}
|
|
249
|
-
async stopCapture() {
|
|
250
|
-
this.exitSession();
|
|
251
|
-
}
|
|
252
|
-
async resetCapture() {
|
|
253
|
-
this.resetDetection();
|
|
254
|
-
}
|
|
255
|
-
async getStatus() {
|
|
256
|
-
return {
|
|
257
|
-
isVideoActive: this.isVideoActive,
|
|
258
|
-
captureStep: this.captureStep,
|
|
259
|
-
statusMessage: this.statusMessage,
|
|
260
|
-
hasImages: !!(this.capturedFullFrame || this.capturedBackFullFrame),
|
|
261
|
-
isProcessCompleted: this.captureStep === 'completed',
|
|
262
|
-
isModelPreloaded: this.isModelPreloaded
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
async preloadModel() {
|
|
266
|
-
if (this.isModelPreloaded || this.session) {
|
|
267
|
-
this.debugLog('🚀 Model already preloaded or session exists');
|
|
268
|
-
return { success: true, message: 'Model already loaded' };
|
|
269
|
-
}
|
|
270
|
-
try {
|
|
271
|
-
this.isLoading = true;
|
|
272
|
-
this.statusMessage = "Precargando modelos...";
|
|
273
|
-
this.statusColor = "#007bff";
|
|
274
|
-
const modelPath = this.MODEL_PATH;
|
|
275
|
-
this.debugLog('🤖 Preloading detection model:', modelPath);
|
|
276
|
-
// Configure ONNX Runtime with device-specific optimizations
|
|
277
|
-
const sessionOptions = this.getSessionOptions();
|
|
278
|
-
this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
|
|
279
|
-
// Preload MobileNet model and classes only if classification is enabled
|
|
280
|
-
if (this.useDocumentClassification) {
|
|
281
|
-
await this.loadMobileNetModel();
|
|
282
|
-
}
|
|
283
|
-
this.isModelPreloaded = true;
|
|
284
|
-
this.isLoading = false;
|
|
285
|
-
this.statusMessage = "Modelos precargados. Listo para comenzar detección";
|
|
286
|
-
this.statusColor = "#28a745";
|
|
287
|
-
this.emitReadyEvent();
|
|
288
|
-
this.debugLog('✅ Models preloaded successfully');
|
|
289
|
-
return { success: true, message: 'Models preloaded successfully' };
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
this.debugLog('❌ Error preloading models:', error);
|
|
293
|
-
this.isLoading = false;
|
|
294
|
-
this.statusMessage = "Error al precargar los modelos";
|
|
295
|
-
this.statusColor = "#ff6b6b";
|
|
296
|
-
return { success: false, error: error.message };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async skipBackCapture() {
|
|
300
|
-
if (this.captureStep === 'back' && this.capturedFullFrame) {
|
|
301
|
-
this.completeProcess(true);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
async loadMobileNetModel() {
|
|
305
|
-
try {
|
|
306
|
-
this.debugLog('🤖 Loading MobileNet model...');
|
|
307
|
-
// Load class map
|
|
308
|
-
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
309
|
-
if (!classResponse.ok) {
|
|
310
|
-
throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
|
|
311
|
-
}
|
|
312
|
-
this.mobileNetClassMap = await classResponse.json();
|
|
313
|
-
this.debugLog('📋 MobileNet classes loaded:', this.mobileNetClassMap);
|
|
314
|
-
// Load model
|
|
315
|
-
const sessionOptions = this.getSessionOptions();
|
|
316
|
-
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
|
|
317
|
-
this.debugLog('✅ MobileNet model loaded successfully');
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
this.debugLog('❌ Error loading MobileNet model:', error);
|
|
321
|
-
throw error;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
preprocessMobileNet(canvas) {
|
|
325
|
-
// Create a temporary canvas for MobileNet preprocessing (224x224)
|
|
326
|
-
const tempCanvas = document.createElement('canvas');
|
|
327
|
-
tempCanvas.width = 224;
|
|
328
|
-
tempCanvas.height = 224;
|
|
329
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
330
|
-
// Draw the cropped image onto the 224x224 canvas
|
|
331
|
-
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
332
|
-
// Get image data and preprocess for MobileNet
|
|
333
|
-
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
334
|
-
const data = imageData.data;
|
|
335
|
-
const hw = 224 * 224;
|
|
336
|
-
const arr = new Float32Array(3 * hw);
|
|
337
|
-
// Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
|
|
338
|
-
for (let i = 0; i < hw; i++) {
|
|
339
|
-
arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
|
|
340
|
-
arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
|
|
341
|
-
arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
|
|
342
|
-
}
|
|
343
|
-
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
344
|
-
}
|
|
345
|
-
async classifyDocument(canvas) {
|
|
346
|
-
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
347
|
-
this.debugLog('⚠️ MobileNet model not loaded');
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
this.debugLog('🔍 Classifying document...');
|
|
352
|
-
// Preprocess image for MobileNet
|
|
353
|
-
const inputTensor = this.preprocessMobileNet(canvas);
|
|
354
|
-
// Run inference
|
|
355
|
-
const feeds = { input: inputTensor };
|
|
356
|
-
const results = await this.mobileNetSession.run(feeds);
|
|
357
|
-
const output = results[Object.keys(results)[0]].data;
|
|
358
|
-
// Find the class with highest confidence
|
|
359
|
-
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
360
|
-
const confidence = output[maxIdx];
|
|
361
|
-
const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
|
|
362
|
-
this.debugLog('📄 Document classification result:', {
|
|
363
|
-
class: className,
|
|
364
|
-
confidence: confidence.toFixed(3),
|
|
365
|
-
classIndex: maxIdx
|
|
366
|
-
});
|
|
367
|
-
return {
|
|
368
|
-
class: className,
|
|
369
|
-
confidence: confidence,
|
|
370
|
-
classIndex: maxIdx
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
this.debugLog('❌ Error classifying document:', error);
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
cleanup() {
|
|
379
|
-
if (this.animationId) {
|
|
380
|
-
cancelAnimationFrame(this.animationId);
|
|
381
|
-
}
|
|
382
|
-
if (this.videoStream) {
|
|
383
|
-
this.videoStream.getTracks().forEach(track => track.stop());
|
|
384
|
-
}
|
|
385
|
-
// Limpiar canvas en cleanup general
|
|
386
|
-
if (this.canvasRef) {
|
|
387
|
-
const ctx = this.canvasRef.getContext("2d");
|
|
388
|
-
if (ctx) {
|
|
389
|
-
ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// OPTIMIZATION: Clean up canvas pool
|
|
393
|
-
this.cleanupCanvasPool();
|
|
394
|
-
}
|
|
395
|
-
cleanupCanvasPool() {
|
|
396
|
-
// Release canvas references for garbage collection
|
|
397
|
-
if (this.preprocessCanvas) {
|
|
398
|
-
this.preprocessCtx = undefined;
|
|
399
|
-
this.preprocessCanvas = undefined;
|
|
400
|
-
}
|
|
401
|
-
if (this.captureCanvas) {
|
|
402
|
-
this.captureCtx = undefined;
|
|
403
|
-
this.captureCanvas = undefined;
|
|
404
|
-
}
|
|
405
|
-
// Disposed ONNX sessions
|
|
406
|
-
if (this.session) {
|
|
407
|
-
this.session.release?.();
|
|
408
|
-
this.session = undefined;
|
|
409
|
-
}
|
|
410
|
-
if (this.mobileNetSession) {
|
|
411
|
-
this.mobileNetSession.release?.();
|
|
412
|
-
this.mobileNetSession = undefined;
|
|
413
|
-
}
|
|
414
|
-
this.mobileNetClassMap = undefined;
|
|
415
|
-
this.isModelPreloaded = false;
|
|
416
|
-
this.debugLog('🧹 Canvas pool cleaned up');
|
|
417
|
-
}
|
|
418
|
-
async getMaxResolution() {
|
|
419
|
-
try {
|
|
420
|
-
// Primero obtén un stream básico para acceder a las capacidades
|
|
421
|
-
const tempStream = await navigator.mediaDevices.getUserMedia({
|
|
422
|
-
video: { facingMode: "environment" }
|
|
423
|
-
});
|
|
424
|
-
const videoTrack = tempStream.getVideoTracks()[0];
|
|
425
|
-
const capabilities = videoTrack.getCapabilities();
|
|
426
|
-
// Detener el stream temporal
|
|
427
|
-
tempStream.getTracks().forEach(track => track.stop());
|
|
428
|
-
// Construir restricciones con resolución optimizada para tablets
|
|
429
|
-
const constraints = {
|
|
430
|
-
facingMode: "environment"
|
|
431
|
-
};
|
|
432
|
-
if (capabilities.width && capabilities.height) {
|
|
433
|
-
// Limitar resolución máxima para tablets para evitar problemas de rendimiento
|
|
434
|
-
const maxWidth = Math.min(capabilities.width.max, 1920);
|
|
435
|
-
const maxHeight = Math.min(capabilities.height.max, 1080);
|
|
436
|
-
// Para tablets, usar resolución moderada en lugar de máxima
|
|
437
|
-
const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
|
|
438
|
-
if (isTablet) {
|
|
439
|
-
constraints.width = { ideal: Math.min(maxWidth, 1280) };
|
|
440
|
-
constraints.height = { ideal: Math.min(maxHeight, 720) };
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
constraints.width = { ideal: maxWidth };
|
|
444
|
-
constraints.height = { ideal: maxHeight };
|
|
445
|
-
}
|
|
446
|
-
this.debugLog('📐 Resolution capabilities:', {
|
|
447
|
-
maxWidth: capabilities.width.max,
|
|
448
|
-
maxHeight: capabilities.height.max,
|
|
449
|
-
selectedWidth: constraints.width.ideal,
|
|
450
|
-
selectedHeight: constraints.height.ideal,
|
|
451
|
-
isTablet
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return constraints;
|
|
455
|
-
}
|
|
456
|
-
catch (err) {
|
|
457
|
-
this.debugLog('⚠️ Could not get capabilities, using fallback');
|
|
458
|
-
// Fallback optimizado para tablets
|
|
459
|
-
const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
|
|
460
|
-
return {
|
|
461
|
-
facingMode: "environment",
|
|
462
|
-
width: { ideal: isTablet ? 1280 : 1920 },
|
|
463
|
-
height: { ideal: isTablet ? 720 : 1080 }
|
|
464
|
-
};
|
|
1438
|
+
return this.stateManager.getCapturedImages();
|
|
1439
|
+
}
|
|
1440
|
+
async isProcessCompleted() {
|
|
1441
|
+
return this.stateManager.isProcessCompleted();
|
|
1442
|
+
}
|
|
1443
|
+
async startCapture() {
|
|
1444
|
+
await this.startDetection();
|
|
1445
|
+
}
|
|
1446
|
+
async stopCapture() {
|
|
1447
|
+
this.exitSession();
|
|
1448
|
+
}
|
|
1449
|
+
async resetCapture() {
|
|
1450
|
+
this.resetDetection();
|
|
1451
|
+
}
|
|
1452
|
+
async skipBackCapture() {
|
|
1453
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1454
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1455
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
1456
|
+
this.completeProcess(true);
|
|
465
1457
|
}
|
|
466
1458
|
}
|
|
467
|
-
async
|
|
1459
|
+
async getStatus() {
|
|
1460
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1461
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1462
|
+
return {
|
|
1463
|
+
isVideoActive: captureState.isVideoActive,
|
|
1464
|
+
captureStep: captureState.step,
|
|
1465
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
1466
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
1467
|
+
isModelPreloaded: this.detectionService.isModelLoaded()
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
async preloadModel() {
|
|
1471
|
+
if (this.detectionService.isModelLoaded()) {
|
|
1472
|
+
this.logger.state('MODELO_YA_PRECARGADO');
|
|
1473
|
+
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
1474
|
+
return { success: true, message: 'Model already loaded' };
|
|
1475
|
+
}
|
|
468
1476
|
try {
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this.
|
|
480
|
-
this.debugLog('📹 Rear camera:', isRear);
|
|
481
|
-
this.debugLog('🪞 Should mirror video:', this.shouldMirrorVideo);
|
|
482
|
-
return new Promise((resolve) => {
|
|
483
|
-
this.videoRef.onloadedmetadata = async () => {
|
|
484
|
-
await this.videoRef.play();
|
|
485
|
-
this.isVideoActive = true;
|
|
486
|
-
// Update mask dimensions once video is loaded
|
|
487
|
-
if (this.canvasRef) {
|
|
488
|
-
const container = this.canvasRef.parentElement;
|
|
489
|
-
const rect = container.getBoundingClientRect();
|
|
490
|
-
this.updateMaskDimensions(rect);
|
|
491
|
-
}
|
|
492
|
-
resolve();
|
|
493
|
-
};
|
|
494
|
-
});
|
|
1477
|
+
const loadStartTime = performance.now();
|
|
1478
|
+
this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
|
|
1479
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1480
|
+
await this.detectionService.loadModel();
|
|
1481
|
+
this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
|
|
1482
|
+
await this.detectionService.loadClassificationModel();
|
|
1483
|
+
const loadEndTime = performance.now();
|
|
1484
|
+
const loadTime = loadEndTime - loadStartTime;
|
|
1485
|
+
// Record ONNX load time
|
|
1486
|
+
if (this.debug) {
|
|
1487
|
+
this.recordOnnxPerformance(loadTime, 0);
|
|
495
1488
|
}
|
|
1489
|
+
this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
|
|
1490
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1491
|
+
this.updateStatus('Modelos precargados', '', 'ready');
|
|
1492
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1493
|
+
this.emitReadyEvent();
|
|
1494
|
+
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
1495
|
+
return { success: true, message: 'Models preloaded successfully' };
|
|
496
1496
|
}
|
|
497
|
-
catch (
|
|
498
|
-
this.
|
|
499
|
-
this.
|
|
500
|
-
this.
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
this.logger.error('Error al precargar modelos:', error);
|
|
1499
|
+
this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
|
|
1500
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1501
|
+
return { success: false, error: error.message };
|
|
501
1502
|
}
|
|
502
1503
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1504
|
+
async getCameraInfo() {
|
|
1505
|
+
return this.cameraService.getCameraInfo();
|
|
1506
|
+
}
|
|
1507
|
+
async setPreferredCamera(camera) {
|
|
1508
|
+
this.preferredCamera = camera;
|
|
1509
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
1510
|
+
await this.cameraService.enumerateDevices();
|
|
1511
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1512
|
+
if (captureState.isVideoActive) {
|
|
1513
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
1514
|
+
if (selectedCameraId) {
|
|
1515
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
1516
|
+
}
|
|
507
1517
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
G.push(data[i + 1] / 255);
|
|
518
|
-
B.push(data[i + 2] / 255);
|
|
1518
|
+
return {
|
|
1519
|
+
success: true,
|
|
1520
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1521
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
async setCaptureDelay(delay) {
|
|
1525
|
+
if (delay < 0 || delay > 10000) {
|
|
1526
|
+
throw new Error('Capture delay must be between 0 and 10000 milliseconds');
|
|
519
1527
|
}
|
|
520
|
-
|
|
521
|
-
|
|
1528
|
+
this.captureDelay = delay;
|
|
1529
|
+
this.serviceContainer.updateConfig({ captureDelay: delay });
|
|
1530
|
+
return {
|
|
1531
|
+
success: true,
|
|
1532
|
+
captureDelay: this.captureDelay
|
|
1533
|
+
};
|
|
522
1534
|
}
|
|
523
|
-
|
|
1535
|
+
async getCaptureDelay() {
|
|
1536
|
+
return this.captureDelay;
|
|
1537
|
+
}
|
|
1538
|
+
async setTorchEnabled(enabled) {
|
|
1539
|
+
const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
|
|
524
1540
|
return {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
enableMemPattern: true,
|
|
535
|
-
executionMode: 'parallel',
|
|
536
|
-
interOpNumThreads: 2,
|
|
537
|
-
intraOpNumThreads: 2,
|
|
1541
|
+
success: torchEnabled,
|
|
1542
|
+
enabled: torchEnabled ? enabled : false
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
async focusAtPoint(x, y) {
|
|
1546
|
+
const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
|
|
1547
|
+
return {
|
|
1548
|
+
success: focused,
|
|
1549
|
+
coordinates: { x, y }
|
|
538
1550
|
};
|
|
539
1551
|
}
|
|
1552
|
+
// DETECTION METHODS
|
|
540
1553
|
async startDetection() {
|
|
1554
|
+
this.logger.state('INICIANDO_DETECCION');
|
|
541
1555
|
try {
|
|
542
|
-
//
|
|
543
|
-
if (!this.
|
|
544
|
-
|
|
545
|
-
this.
|
|
546
|
-
this.
|
|
547
|
-
|
|
548
|
-
this.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1556
|
+
// Paso 1: Verificar y cargar modelos si es necesario
|
|
1557
|
+
if (!this.detectionService.isModelLoaded()) {
|
|
1558
|
+
const loadStartTime = performance.now();
|
|
1559
|
+
this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
|
|
1560
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1561
|
+
await this.detectionService.loadModel();
|
|
1562
|
+
this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
|
|
1563
|
+
await this.detectionService.loadClassificationModel();
|
|
1564
|
+
const loadEndTime = performance.now();
|
|
1565
|
+
const loadTime = loadEndTime - loadStartTime;
|
|
1566
|
+
// Record ONNX load time
|
|
1567
|
+
if (this.debug) {
|
|
1568
|
+
this.recordOnnxPerformance(loadTime, 0);
|
|
554
1569
|
}
|
|
555
|
-
this.
|
|
556
|
-
this.emitReadyEvent();
|
|
1570
|
+
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
557
1571
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1572
|
+
// Paso 2: Detectar y configurar dispositivos
|
|
1573
|
+
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
|
|
1574
|
+
await this.cameraService.enumerateDevices();
|
|
1575
|
+
// Paso 3: Configurar cámara seleccionada
|
|
1576
|
+
this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
|
|
1577
|
+
const stream = await this.cameraService.setupCamera();
|
|
1578
|
+
// Paso 4: Inicializar video y ocultar estado inmediatamente
|
|
1579
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
1580
|
+
await this.initializeVideoStream(stream);
|
|
1581
|
+
// Paso 5: Calibrar máscara
|
|
1582
|
+
if (this.detectionContainer) {
|
|
1583
|
+
const container = this.detectionContainer.parentElement;
|
|
1584
|
+
const rect = container.getBoundingClientRect();
|
|
1585
|
+
this.updateMaskDimensions(rect);
|
|
562
1586
|
}
|
|
563
|
-
|
|
564
|
-
await this.setupCamera();
|
|
1587
|
+
// Finalizar e iniciar captura
|
|
565
1588
|
this.startTime = Date.now();
|
|
566
|
-
this.isLoading
|
|
1589
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
567
1590
|
this.detectFrame();
|
|
568
1591
|
}
|
|
569
1592
|
catch (err) {
|
|
570
|
-
this.
|
|
571
|
-
this.
|
|
572
|
-
this.
|
|
573
|
-
|
|
1593
|
+
this.logger.error('Error al inicializar detección:', err);
|
|
1594
|
+
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
1595
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
async initializeVideoStream(stream) {
|
|
1599
|
+
if (this.videoRef) {
|
|
1600
|
+
this.videoRef.srcObject = stream;
|
|
1601
|
+
this.videoStream = stream;
|
|
1602
|
+
const isRear = this.cameraService.isRearCamera(stream);
|
|
1603
|
+
this.shouldMirrorVideo = !isRear;
|
|
1604
|
+
this.logger.state('CAMARA_CONFIGURADA', {
|
|
1605
|
+
isRearCamera: isRear,
|
|
1606
|
+
shouldMirrorVideo: this.shouldMirrorVideo
|
|
1607
|
+
});
|
|
1608
|
+
return new Promise((resolve) => {
|
|
1609
|
+
this.videoRef.onloadedmetadata = async () => {
|
|
1610
|
+
await this.videoRef.play();
|
|
1611
|
+
this.stateManager.updateCaptureState({ isVideoActive: true });
|
|
1612
|
+
resolve();
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
else {
|
|
1617
|
+
throw new Error('Video element not available');
|
|
574
1618
|
}
|
|
575
1619
|
}
|
|
576
1620
|
async detectFrame() {
|
|
577
1621
|
try {
|
|
578
|
-
|
|
1622
|
+
const frameStartTime = performance.now();
|
|
1623
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1624
|
+
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
579
1625
|
return;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
// Solo continuar el bucle sin procesar detección
|
|
583
|
-
if (this.captureStep !== 'completed') {
|
|
1626
|
+
if (captureState.isDetectionPaused) {
|
|
1627
|
+
if (captureState.step !== 'completed') {
|
|
584
1628
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
585
1629
|
}
|
|
586
1630
|
return;
|
|
587
1631
|
}
|
|
588
|
-
//
|
|
1632
|
+
// Frame skipping for performance
|
|
589
1633
|
this.frameSkipCounter++;
|
|
590
1634
|
if (this.frameSkipCounter <= this.FRAME_SKIP) {
|
|
591
|
-
if (
|
|
1635
|
+
if (captureState.step !== 'completed') {
|
|
592
1636
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
593
1637
|
}
|
|
594
1638
|
return;
|
|
595
1639
|
}
|
|
596
1640
|
this.frameSkipCounter = 0;
|
|
597
|
-
//
|
|
1641
|
+
// Throttle inference
|
|
598
1642
|
const currentTime = Date.now();
|
|
599
1643
|
if (currentTime - this.lastInferenceTime < this.MIN_INFERENCE_INTERVAL) {
|
|
600
|
-
if (
|
|
1644
|
+
if (captureState.step !== 'completed') {
|
|
601
1645
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
602
1646
|
}
|
|
603
1647
|
return;
|
|
604
1648
|
}
|
|
605
1649
|
this.lastInferenceTime = currentTime;
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
1650
|
+
// Measure preprocessing and inference time
|
|
1651
|
+
const inferenceStartTime = performance.now();
|
|
1652
|
+
const inputTensor = this.detectionService.preprocess(this.videoRef);
|
|
1653
|
+
// Standard detection without quality validation
|
|
1654
|
+
const detections = await this.detectionService.runInference(inputTensor);
|
|
1655
|
+
const inferenceTime = performance.now() - inferenceStartTime;
|
|
1656
|
+
// Record ONNX performance metrics
|
|
1657
|
+
if (this.debug) {
|
|
1658
|
+
this.recordOnnxPerformance(0, inferenceTime);
|
|
1659
|
+
}
|
|
1660
|
+
// Update best score logic
|
|
1661
|
+
if (this.startTime && Date.now() - this.startTime < 5000) {
|
|
1662
|
+
detections.forEach((detection) => {
|
|
1663
|
+
const isWellPositioned = this.detectionService.isCardInFrame(detection);
|
|
1664
|
+
const effectiveScore = isWellPositioned ? detection.score * 1.2 : detection.score;
|
|
1665
|
+
if (effectiveScore > captureState.bestScore) {
|
|
1666
|
+
this.stateManager.updateCaptureState({ bestScore: effectiveScore });
|
|
623
1667
|
}
|
|
624
|
-
}
|
|
1668
|
+
});
|
|
625
1669
|
}
|
|
626
|
-
//
|
|
627
|
-
|
|
1670
|
+
// Update document detection state
|
|
1671
|
+
this.hasDocumentDetected = detections.length > 0;
|
|
1672
|
+
// Adaptive frame rate
|
|
1673
|
+
if (detections.length === 0) {
|
|
628
1674
|
this.consecutiveFailures++;
|
|
629
1675
|
}
|
|
630
1676
|
else {
|
|
631
1677
|
this.consecutiveFailures = 0;
|
|
632
1678
|
}
|
|
633
|
-
|
|
1679
|
+
// Update detection boxes for debug
|
|
1680
|
+
if (this.debug) {
|
|
1681
|
+
this.updateDetectionBoxes(detections);
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
this.detectionBoxes = [];
|
|
1685
|
+
}
|
|
1686
|
+
this.updateMaskColor(detections);
|
|
1687
|
+
// Record frame processing metrics
|
|
634
1688
|
if (this.debug) {
|
|
635
|
-
|
|
1689
|
+
const frameEndTime = performance.now();
|
|
1690
|
+
const totalFrameTime = frameEndTime - frameStartTime;
|
|
1691
|
+
this.recordFrameProcessing(totalFrameTime, detections.length);
|
|
636
1692
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// Solo continuar si no hemos completado el proceso
|
|
640
|
-
if (this.captureStep !== 'completed') {
|
|
641
|
-
// OPTIMIZATION 4: Reduce frame rate when no detection for better battery life
|
|
1693
|
+
// Continue detection loop
|
|
1694
|
+
if (captureState.step !== 'completed') {
|
|
642
1695
|
if (this.consecutiveFailures > this.MAX_FAILURES) {
|
|
643
|
-
// Reduce to ~5fps when no detection for 0.5 seconds
|
|
644
1696
|
setTimeout(() => this.detectFrame(), 200);
|
|
645
1697
|
}
|
|
646
1698
|
else {
|
|
@@ -649,102 +1701,97 @@ const JaakStamps = class {
|
|
|
649
1701
|
}
|
|
650
1702
|
}
|
|
651
1703
|
catch (e) {
|
|
652
|
-
this.
|
|
653
|
-
|
|
654
|
-
if (
|
|
655
|
-
// On error, wait longer before retrying
|
|
1704
|
+
this.logger.error('Error en inferencia de modelo:', e);
|
|
1705
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1706
|
+
if (captureState.step !== 'completed') {
|
|
656
1707
|
setTimeout(() => this.detectFrame(), 100);
|
|
657
1708
|
}
|
|
658
1709
|
}
|
|
659
1710
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const frameCenterY = this.INPUT_SIZE / 2;
|
|
665
|
-
const toleranceX = 40;
|
|
666
|
-
const toleranceY = 30;
|
|
667
|
-
const isCentered = Math.abs(cardCenterX - frameCenterX) < toleranceX &&
|
|
668
|
-
Math.abs(cardCenterY - frameCenterY) < toleranceY;
|
|
669
|
-
const isGoodSize = box.w > 150 && box.w < 300 && box.h > 90 && box.h < 200;
|
|
670
|
-
return isCentered && isGoodSize;
|
|
1711
|
+
// ... (continuing with remaining methods)
|
|
1712
|
+
// Due to length constraints, I'll provide the key architectural changes
|
|
1713
|
+
disconnectedCallback() {
|
|
1714
|
+
this.cleanup();
|
|
671
1715
|
}
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
1716
|
+
cleanup() {
|
|
1717
|
+
if (this.animationId) {
|
|
1718
|
+
cancelAnimationFrame(this.animationId);
|
|
1719
|
+
}
|
|
1720
|
+
if (this.videoStream) {
|
|
1721
|
+
this.videoStream.getTracks().forEach(track => track.stop());
|
|
1722
|
+
}
|
|
1723
|
+
if (this.alignmentTimer) {
|
|
1724
|
+
clearTimeout(this.alignmentTimer);
|
|
1725
|
+
this.alignmentTimer = undefined;
|
|
1726
|
+
}
|
|
1727
|
+
if (this.performanceUpdateInterval) {
|
|
1728
|
+
clearInterval(this.performanceUpdateInterval);
|
|
1729
|
+
this.performanceUpdateInterval = undefined;
|
|
1730
|
+
}
|
|
1731
|
+
this.detectionBoxes = [];
|
|
1732
|
+
this.alignmentStartTime = undefined;
|
|
1733
|
+
this.hasDocumentDetected = false;
|
|
1734
|
+
this.serviceContainer?.cleanup();
|
|
1735
|
+
}
|
|
1736
|
+
render() {
|
|
1737
|
+
const captureState = this.stateManager?.getCaptureState() || {
|
|
1738
|
+
isVideoActive: false,
|
|
1739
|
+
showFlipAnimation: false,
|
|
1740
|
+
showSuccessAnimation: false,
|
|
1741
|
+
step: 'front',
|
|
1742
|
+
isCapturing: false
|
|
1743
|
+
};
|
|
1744
|
+
const cameraInfo = this.cameraService?.getCameraInfo() || {
|
|
1745
|
+
availableCameras: [],
|
|
1746
|
+
selectedCameraId: null,
|
|
1747
|
+
deviceType: 'desktop'};
|
|
1748
|
+
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: {
|
|
1749
|
+
position: 'absolute',
|
|
1750
|
+
left: `${box.x}px`,
|
|
1751
|
+
top: `${box.y}px`,
|
|
1752
|
+
width: `${box.w}px`,
|
|
1753
|
+
height: `${box.h}px`,
|
|
1754
|
+
border: '2px solid #32406C',
|
|
1755
|
+
pointerEvents: 'none',
|
|
1756
|
+
boxSizing: 'border-box'
|
|
1757
|
+
} })))), 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" })))));
|
|
1758
|
+
}
|
|
1759
|
+
// Utility methods
|
|
1760
|
+
updateDetectionBoxes(boxes) {
|
|
1761
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
1762
|
+
return;
|
|
676
1763
|
const videoWidth = this.videoRef.videoWidth;
|
|
677
1764
|
const videoHeight = this.videoRef.videoHeight;
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1765
|
+
const container = this.detectionContainer.parentElement;
|
|
1766
|
+
const containerRect = container.getBoundingClientRect();
|
|
1767
|
+
const containerWidth = containerRect.width;
|
|
1768
|
+
const containerHeight = containerRect.height;
|
|
1769
|
+
if (videoWidth === 0 || videoHeight === 0)
|
|
1770
|
+
return;
|
|
682
1771
|
const videoAspectRatio = videoWidth / videoHeight;
|
|
683
|
-
|
|
684
|
-
// the mask should be in this distorted space to match the visual mask
|
|
685
|
-
// In the visual display, we calculate mask size based on the limiting dimension
|
|
686
|
-
// We need to replicate this logic but account for the distortion in model space
|
|
687
|
-
// Calculate the "display" dimensions (what the visual mask calculations use)
|
|
688
|
-
const containerAspectRatio = 1; // Assume square container for simplicity
|
|
1772
|
+
const containerAspectRatio = containerWidth / containerHeight;
|
|
689
1773
|
let displayedVideoWidth, displayedVideoHeight;
|
|
1774
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
690
1775
|
if (videoAspectRatio > containerAspectRatio) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
else {
|
|
696
|
-
// Video is taller - pillarboxed in display
|
|
697
|
-
displayedVideoHeight = this.INPUT_SIZE;
|
|
698
|
-
displayedVideoWidth = this.INPUT_SIZE * videoAspectRatio;
|
|
699
|
-
}
|
|
700
|
-
// Calculate mask dimensions using the same logic as updateMaskDimensions
|
|
701
|
-
const maxWidthByHeight = displayedVideoHeight * this.ID1_ASPECT_RATIO;
|
|
702
|
-
let visualMaskWidth, visualMaskHeight;
|
|
703
|
-
const sizeRatio = this.maskSize / 100;
|
|
704
|
-
if (maxWidthByHeight <= displayedVideoWidth) {
|
|
705
|
-
// Video height is the limiting factor
|
|
706
|
-
visualMaskHeight = displayedVideoHeight * sizeRatio;
|
|
707
|
-
visualMaskWidth = visualMaskHeight * this.ID1_ASPECT_RATIO;
|
|
1776
|
+
displayedVideoWidth = containerWidth;
|
|
1777
|
+
displayedVideoHeight = containerWidth / videoAspectRatio;
|
|
1778
|
+
videoOffsetY = (containerHeight - displayedVideoHeight) / 2;
|
|
708
1779
|
}
|
|
709
1780
|
else {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
// Now map these visual mask dimensions to the distorted model space
|
|
715
|
-
// The model stretches video to 320x320, so we need to apply the inverse transformation
|
|
716
|
-
const modelMaskWidth = visualMaskWidth * (this.INPUT_SIZE / displayedVideoWidth);
|
|
717
|
-
const modelMaskHeight = visualMaskHeight * (this.INPUT_SIZE / displayedVideoHeight);
|
|
718
|
-
// Calculate mask boundaries in model coordinate system (always centered in 320x320)
|
|
719
|
-
const maskCenterX = this.INPUT_SIZE / 2;
|
|
720
|
-
const maskCenterY = this.INPUT_SIZE / 2;
|
|
721
|
-
const maskLeft = maskCenterX - (modelMaskWidth / 2);
|
|
722
|
-
const maskRight = maskCenterX + (modelMaskWidth / 2);
|
|
723
|
-
const maskTop = maskCenterY - (modelMaskHeight / 2);
|
|
724
|
-
const maskBottom = maskCenterY + (modelMaskHeight / 2);
|
|
725
|
-
// Obtener las coordenadas del documento detectado
|
|
726
|
-
let docLeft = box.x;
|
|
727
|
-
let docRight = box.x + box.w;
|
|
728
|
-
const docTop = box.y;
|
|
729
|
-
const docBottom = box.y + box.h;
|
|
730
|
-
// Si el video está en modo mirror, invertir las coordenadas horizontales
|
|
731
|
-
if (this.shouldMirrorVideo) {
|
|
732
|
-
const originalDocLeft = docLeft;
|
|
733
|
-
const originalDocRight = docRight;
|
|
734
|
-
docLeft = this.INPUT_SIZE - originalDocRight;
|
|
735
|
-
docRight = this.INPUT_SIZE - originalDocLeft;
|
|
1781
|
+
displayedVideoHeight = containerHeight;
|
|
1782
|
+
displayedVideoWidth = containerHeight * videoAspectRatio;
|
|
1783
|
+
videoOffsetX = (containerWidth - displayedVideoWidth) / 2;
|
|
736
1784
|
}
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return alignment.top && alignment.right && alignment.bottom && alignment.left;
|
|
1785
|
+
const INPUT_SIZE = 320;
|
|
1786
|
+
const scaleX = displayedVideoWidth / INPUT_SIZE;
|
|
1787
|
+
const scaleY = displayedVideoHeight / INPUT_SIZE;
|
|
1788
|
+
this.detectionBoxes = boxes.map(det => ({
|
|
1789
|
+
x: det.x * scaleX + videoOffsetX,
|
|
1790
|
+
y: det.y * scaleY + videoOffsetY,
|
|
1791
|
+
w: det.w * scaleX,
|
|
1792
|
+
h: det.h * scaleY,
|
|
1793
|
+
score: det.score
|
|
1794
|
+
}));
|
|
748
1795
|
}
|
|
749
1796
|
updateMaskColor(boxes) {
|
|
750
1797
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -757,297 +1804,327 @@ const JaakStamps = class {
|
|
|
757
1804
|
let currentAlignment = { top: false, right: false, bottom: false, left: false };
|
|
758
1805
|
if (boxes.length > 0) {
|
|
759
1806
|
bestBox = boxes.reduce((best, current) => current.score > best.score ? current : best);
|
|
760
|
-
|
|
1807
|
+
const maskConfig = {
|
|
1808
|
+
INPUT_SIZE: 320,
|
|
1809
|
+
ID1_ASPECT_RATIO: 85.60 / 53.98,
|
|
1810
|
+
shouldMirrorVideo: this.shouldMirrorVideo,
|
|
1811
|
+
alignmentTolerance: this.alignmentTolerance,
|
|
1812
|
+
maskSize: this.maskSize,
|
|
1813
|
+
videoRef: this.videoRef
|
|
1814
|
+
};
|
|
1815
|
+
currentAlignment = this.detectionService.checkSideAlignment(bestBox, maskConfig);
|
|
761
1816
|
this.sideAlignment = currentAlignment;
|
|
762
1817
|
}
|
|
763
1818
|
else {
|
|
764
|
-
// Reset alignment when no detection
|
|
765
1819
|
this.sideAlignment = { top: false, right: false, bottom: false, left: false };
|
|
766
1820
|
}
|
|
767
|
-
// Actualizar colores de cada lado individualmente
|
|
768
1821
|
topSide?.classList.toggle('aligned', currentAlignment.top);
|
|
769
1822
|
rightSide?.classList.toggle('aligned', currentAlignment.right);
|
|
770
1823
|
bottomSide?.classList.toggle('aligned', currentAlignment.bottom);
|
|
771
1824
|
leftSide?.classList.toggle('aligned', currentAlignment.left);
|
|
772
|
-
|
|
773
|
-
const allSidesAligned = this.areAllSidesAligned(currentAlignment);
|
|
1825
|
+
const allSidesAligned = this.detectionService.areAllSidesAligned(currentAlignment);
|
|
774
1826
|
if (allSidesAligned && bestBox) {
|
|
775
1827
|
cardOutline?.classList.add('perfect-match');
|
|
776
1828
|
corners?.forEach(corner => corner.classList.add('perfect-match'));
|
|
1829
|
+
// Debug logging
|
|
1830
|
+
this.logger.state('CAPTURE_EVALUATION', {
|
|
1831
|
+
allSidesAligned: true,
|
|
1832
|
+
hasScreenshotTaken: this.hasScreenshotTaken,
|
|
1833
|
+
captureDelay: this.captureDelay,
|
|
1834
|
+
alignmentStartTime: this.alignmentStartTime
|
|
1835
|
+
});
|
|
777
1836
|
if (!this.hasScreenshotTaken) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1837
|
+
const currentTime = Date.now();
|
|
1838
|
+
// Initialize alignment start time if not set
|
|
1839
|
+
if (!this.alignmentStartTime) {
|
|
1840
|
+
this.alignmentStartTime = currentTime;
|
|
1841
|
+
this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
|
|
1842
|
+
}
|
|
1843
|
+
// Check if document has been aligned for the configured delay
|
|
1844
|
+
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
1845
|
+
this.logger.state('ALIGNMENT_DURATION_CHECK', {
|
|
1846
|
+
alignmentDuration,
|
|
1847
|
+
captureDelay: this.captureDelay,
|
|
1848
|
+
readyToCapture: alignmentDuration >= this.captureDelay
|
|
781
1849
|
});
|
|
782
|
-
this.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1850
|
+
if (alignmentDuration >= this.captureDelay) {
|
|
1851
|
+
this.logger.state('TRIGGERING_CAPTURE', {
|
|
1852
|
+
alignmentDuration,
|
|
1853
|
+
captureDelay: this.captureDelay
|
|
1854
|
+
});
|
|
1855
|
+
this.lastDetectedBox = bestBox;
|
|
1856
|
+
this.takeScreenshot().catch(error => {
|
|
1857
|
+
this.logger.error('Error al tomar captura de pantalla:', error);
|
|
1858
|
+
});
|
|
1859
|
+
this.hasScreenshotTaken = true;
|
|
1860
|
+
this.alignmentStartTime = undefined;
|
|
1861
|
+
setTimeout(() => {
|
|
1862
|
+
this.hasScreenshotTaken = false;
|
|
1863
|
+
}, 2000);
|
|
1864
|
+
}
|
|
787
1865
|
}
|
|
788
1866
|
}
|
|
789
1867
|
else {
|
|
790
1868
|
cardOutline?.classList.remove('perfect-match');
|
|
791
1869
|
corners?.forEach(corner => corner.classList.remove('perfect-match'));
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (boxes.length === 0) {
|
|
796
|
-
this.statusMessage = "Posicione la identificación dentro del marco";
|
|
797
|
-
this.statusColor = "#ff6b6b";
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
const bestBox = boxes.reduce((best, current) => current.score > best.score ? current : best);
|
|
801
|
-
const alignment = this.checkSideAlignment(bestBox);
|
|
802
|
-
const alignedSides = [alignment.top, alignment.right, alignment.bottom, alignment.left].filter(Boolean).length;
|
|
803
|
-
const allSidesAligned = this.areAllSidesAligned(alignment);
|
|
804
|
-
if (allSidesAligned) {
|
|
805
|
-
this.statusMessage = "Identificación perfectamente alineada. Mantenga inmóvil";
|
|
806
|
-
this.statusColor = "#00ff00";
|
|
1870
|
+
// Reset alignment timer when document moves out of position
|
|
1871
|
+
if (this.alignmentStartTime) {
|
|
1872
|
+
this.alignmentStartTime = undefined;
|
|
807
1873
|
}
|
|
808
|
-
|
|
809
|
-
this.
|
|
810
|
-
this.
|
|
1874
|
+
if (this.alignmentTimer) {
|
|
1875
|
+
clearTimeout(this.alignmentTimer);
|
|
1876
|
+
this.alignmentTimer = undefined;
|
|
811
1877
|
}
|
|
812
|
-
else if (bestBox.w < 150) {
|
|
813
|
-
this.statusMessage = "Identificación detectada. Acerque al marco";
|
|
814
|
-
this.statusColor = "#ffb366";
|
|
815
|
-
}
|
|
816
|
-
else {
|
|
817
|
-
this.statusMessage = "Identificación detectada. Alinee con el marco";
|
|
818
|
-
this.statusColor = "#ffb366";
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
drawDetections(ctx, boxes) {
|
|
823
|
-
if (!this.videoRef)
|
|
824
|
-
return;
|
|
825
|
-
// Get video and container dimensions
|
|
826
|
-
const videoWidth = this.videoRef.videoWidth;
|
|
827
|
-
const videoHeight = this.videoRef.videoHeight;
|
|
828
|
-
const containerWidth = this.canvasRef.width;
|
|
829
|
-
const containerHeight = this.canvasRef.height;
|
|
830
|
-
if (videoWidth === 0 || videoHeight === 0)
|
|
831
|
-
return;
|
|
832
|
-
// Calculate video aspect ratio and container aspect ratio
|
|
833
|
-
const videoAspectRatio = videoWidth / videoHeight;
|
|
834
|
-
const containerAspectRatio = containerWidth / containerHeight;
|
|
835
|
-
// Determine how video fits in container (same logic as updateMaskDimensions)
|
|
836
|
-
let displayedVideoWidth, displayedVideoHeight;
|
|
837
|
-
let videoOffsetX = 0, videoOffsetY = 0;
|
|
838
|
-
if (videoAspectRatio > containerAspectRatio) {
|
|
839
|
-
// Video is wider - letterboxed (black bars top/bottom)
|
|
840
|
-
displayedVideoWidth = containerWidth;
|
|
841
|
-
displayedVideoHeight = containerWidth / videoAspectRatio;
|
|
842
|
-
videoOffsetY = (containerHeight - displayedVideoHeight) / 2;
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
// Video is taller - pillarboxed (black bars left/right)
|
|
846
|
-
displayedVideoHeight = containerHeight;
|
|
847
|
-
displayedVideoWidth = containerHeight * videoAspectRatio;
|
|
848
|
-
videoOffsetX = (containerWidth - displayedVideoWidth) / 2;
|
|
849
1878
|
}
|
|
850
|
-
// Scale factor from model coordinates (320x320) to displayed video area
|
|
851
|
-
const scaleX = displayedVideoWidth / this.INPUT_SIZE;
|
|
852
|
-
const scaleY = displayedVideoHeight / this.INPUT_SIZE;
|
|
853
|
-
boxes.forEach(det => {
|
|
854
|
-
// Convert model coordinates to displayed video coordinates
|
|
855
|
-
const x = det.x * scaleX + videoOffsetX;
|
|
856
|
-
const y = det.y * scaleY + videoOffsetY;
|
|
857
|
-
const w = det.w * scaleX;
|
|
858
|
-
const h = det.h * scaleY;
|
|
859
|
-
ctx.strokeStyle = "#32406C";
|
|
860
|
-
ctx.lineWidth = 2;
|
|
861
|
-
ctx.strokeRect(x, y, w, h);
|
|
862
|
-
});
|
|
863
1879
|
}
|
|
864
1880
|
async takeScreenshot() {
|
|
865
1881
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
866
1882
|
return;
|
|
867
|
-
|
|
1883
|
+
this.logger.state('INICIANDO_CAPTURA', {
|
|
1884
|
+
captureStep: this.stateManager.getCaptureState().step,
|
|
1885
|
+
detectedBox: this.lastDetectedBox,
|
|
1886
|
+
videoResolution: {
|
|
1887
|
+
width: this.videoRef.videoWidth,
|
|
1888
|
+
height: this.videoRef.videoHeight
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
868
1892
|
this.triggerCaptureAnimation();
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
this.captureCanvas.width
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
|
|
1893
|
+
// Create capture canvas
|
|
1894
|
+
const captureCanvas = document.createElement('canvas');
|
|
1895
|
+
captureCanvas.width = this.videoRef.videoWidth;
|
|
1896
|
+
captureCanvas.height = this.videoRef.videoHeight;
|
|
1897
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
1898
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
1899
|
+
// Calculate crop coordinates
|
|
1900
|
+
const INPUT_SIZE = 320;
|
|
1901
|
+
const scaleX = this.videoRef.videoWidth / INPUT_SIZE;
|
|
1902
|
+
const scaleY = this.videoRef.videoHeight / INPUT_SIZE;
|
|
880
1903
|
const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
|
|
881
1904
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
882
1905
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
883
1906
|
const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
|
|
884
|
-
//
|
|
885
|
-
// (We reuse main capture canvas for full frame)
|
|
1907
|
+
// Create cropped version
|
|
886
1908
|
const croppedCanvas = document.createElement('canvas');
|
|
887
1909
|
croppedCanvas.width = cropWidth;
|
|
888
1910
|
croppedCanvas.height = cropHeight;
|
|
889
1911
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1912
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
1913
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1914
|
+
if (captureState.step === 'front') {
|
|
1915
|
+
this.stateManager.setCapturedImages({
|
|
1916
|
+
front: {
|
|
1917
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
1918
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
898
1921
|
// Check if document classification is enabled
|
|
899
1922
|
if (this.useDocumentClassification) {
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
catch (error) {
|
|
906
|
-
this.debugLog('⚠️ Failed to load classification model, continuing without classification:', error);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
// Classify the cropped document if model is available
|
|
910
|
-
if (this.mobileNetSession) {
|
|
911
|
-
const classification = await this.classifyDocument(croppedCanvas);
|
|
912
|
-
if (classification && classification.class === 'passport') {
|
|
913
|
-
// If it's a passport, skip back capture since passports don't have a back side
|
|
914
|
-
this.debugLog('📄 Passport detected - skipping back capture');
|
|
915
|
-
this.completeProcess(true);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
1923
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
1924
|
+
if (classification && classification.class === 'passport') {
|
|
1925
|
+
this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
|
|
1926
|
+
this.completeProcess(true);
|
|
1927
|
+
return;
|
|
918
1928
|
}
|
|
919
1929
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
this.isDetectionPaused = true;
|
|
926
|
-
// Mostrar animación de giro después de la captura
|
|
1930
|
+
this.stateManager.updateCaptureState({
|
|
1931
|
+
step: 'back',
|
|
1932
|
+
isDetectionPaused: true,
|
|
1933
|
+
showFlipAnimation: true
|
|
1934
|
+
});
|
|
927
1935
|
setTimeout(() => {
|
|
928
|
-
this.
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
this.
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1936
|
+
this.stateManager.updateCaptureState({
|
|
1937
|
+
showFlipAnimation: false,
|
|
1938
|
+
isDetectionPaused: false
|
|
1939
|
+
});
|
|
1940
|
+
}, 3000);
|
|
1941
|
+
}
|
|
1942
|
+
else if (captureState.step === 'back') {
|
|
1943
|
+
this.stateManager.setCapturedImages({
|
|
1944
|
+
back: {
|
|
1945
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
1946
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
941
1949
|
this.completeProcess(false);
|
|
942
|
-
this.debugLog('📸 TRASERA capturada. Proceso completado. Detector detenido. Imágenes emitidas.');
|
|
943
1950
|
}
|
|
944
1951
|
}
|
|
945
1952
|
triggerCaptureAnimation() {
|
|
946
|
-
this.isCapturing = true;
|
|
947
|
-
// Agregar clase de animación al marco
|
|
948
1953
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
949
1954
|
cardOutline?.classList.add('capturing');
|
|
950
|
-
// Limpiar animación después de que termine
|
|
951
1955
|
setTimeout(() => {
|
|
952
|
-
this.isCapturing
|
|
1956
|
+
this.stateManager.updateCaptureState({ isCapturing: false });
|
|
953
1957
|
cardOutline?.classList.remove('capturing');
|
|
954
1958
|
}, 600);
|
|
955
1959
|
}
|
|
1960
|
+
completeProcess(skippedBack = false) {
|
|
1961
|
+
this.stateManager.updateCaptureState({
|
|
1962
|
+
step: 'completed',
|
|
1963
|
+
showSuccessAnimation: true
|
|
1964
|
+
});
|
|
1965
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1966
|
+
capturedImages.metadata.processCompleted = true;
|
|
1967
|
+
capturedImages.metadata.backCaptureSkipped = skippedBack;
|
|
1968
|
+
this.stateManager.setCapturedImages(capturedImages);
|
|
1969
|
+
this.stopDetection();
|
|
1970
|
+
this.updateStatus('Proceso completado', `${capturedImages.metadata.totalImages} imágenes capturadas exitosamente`, 'ready');
|
|
1971
|
+
const finalImages = {
|
|
1972
|
+
...capturedImages,
|
|
1973
|
+
timestamp: new Date().toISOString()
|
|
1974
|
+
};
|
|
1975
|
+
this.captureCompleted.emit(finalImages);
|
|
1976
|
+
setTimeout(() => {
|
|
1977
|
+
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
1978
|
+
}, 3000);
|
|
1979
|
+
this.logger.state('PROCESO_COMPLETADO', {
|
|
1980
|
+
skippedBack,
|
|
1981
|
+
totalImages: capturedImages.metadata.totalImages,
|
|
1982
|
+
timestamp: new Date().toISOString()
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
956
1985
|
stopDetection() {
|
|
957
1986
|
if (this.animationId) {
|
|
958
1987
|
cancelAnimationFrame(this.animationId);
|
|
959
1988
|
this.animationId = undefined;
|
|
960
1989
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1990
|
+
this.detectionBoxes = [];
|
|
1991
|
+
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
1992
|
+
}
|
|
1993
|
+
toggleCameraSelector() {
|
|
1994
|
+
if (this.isSwitchingCamera)
|
|
1995
|
+
return; // Don't toggle if switching camera
|
|
1996
|
+
this.showCameraSelector = !this.showCameraSelector;
|
|
1997
|
+
}
|
|
1998
|
+
async handleCameraSwitch(cameraId) {
|
|
1999
|
+
if (this.isSwitchingCamera)
|
|
2000
|
+
return; // Prevent multiple simultaneous switches
|
|
2001
|
+
try {
|
|
2002
|
+
// Close the selector immediately when user selects a camera
|
|
2003
|
+
this.showCameraSelector = false;
|
|
2004
|
+
this.isSwitchingCamera = true;
|
|
2005
|
+
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
2006
|
+
from: this.cameraService.getSelectedCameraId(),
|
|
2007
|
+
to: cameraId
|
|
2008
|
+
});
|
|
2009
|
+
// Stop current video stream
|
|
2010
|
+
if (this.videoStream) {
|
|
2011
|
+
this.videoStream.getTracks().forEach(track => track.stop());
|
|
2012
|
+
}
|
|
2013
|
+
// Switch camera in service
|
|
2014
|
+
await this.cameraService.switchCamera(cameraId);
|
|
2015
|
+
// Setup new camera stream
|
|
2016
|
+
const newStream = await this.cameraService.setupCamera();
|
|
2017
|
+
// Update video element
|
|
2018
|
+
await this.initializeVideoStream(newStream);
|
|
2019
|
+
this.logger.state('CAMBIO_CAMARA_EXITOSO', {
|
|
2020
|
+
newCameraId: cameraId,
|
|
2021
|
+
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
catch (error) {
|
|
2025
|
+
this.logger.error('Error al cambiar cámara:', error);
|
|
2026
|
+
// Try to restore previous camera if switch failed
|
|
2027
|
+
try {
|
|
2028
|
+
const fallbackStream = await this.cameraService.setupCamera();
|
|
2029
|
+
await this.initializeVideoStream(fallbackStream);
|
|
2030
|
+
}
|
|
2031
|
+
catch (fallbackError) {
|
|
2032
|
+
this.logger.error('Error al restaurar cámara anterior:', fallbackError);
|
|
2033
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
finally {
|
|
2037
|
+
this.isSwitchingCamera = false;
|
|
965
2038
|
}
|
|
966
|
-
this.debugLog('🛑 Detector de identificación detenido');
|
|
967
2039
|
}
|
|
968
2040
|
resetDetection() {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
this.
|
|
972
|
-
|
|
2041
|
+
const currentCaptureState = this.stateManager.getCaptureState();
|
|
2042
|
+
const wasVideoActive = currentCaptureState.isVideoActive;
|
|
2043
|
+
this.stateManager.reset();
|
|
2044
|
+
if (wasVideoActive) {
|
|
2045
|
+
this.stateManager.updateCaptureState({ isVideoActive: true });
|
|
2046
|
+
}
|
|
973
2047
|
this.hasScreenshotTaken = false;
|
|
974
|
-
this.
|
|
975
|
-
this.capturedCroppedId = null;
|
|
976
|
-
this.capturedBackFullFrame = null;
|
|
977
|
-
this.capturedBackCroppedId = null;
|
|
978
|
-
this.captureStep = 'front';
|
|
979
|
-
this.isCapturing = false;
|
|
980
|
-
this.showFlipAnimation = false;
|
|
981
|
-
this.showSuccessAnimation = false;
|
|
982
|
-
this.isDetectionPaused = false;
|
|
983
|
-
this.isLoading = false;
|
|
984
|
-
this.lastDetectedBox = undefined;
|
|
985
|
-
this.isModelPreloaded = false;
|
|
986
|
-
// Reset performance counters
|
|
2048
|
+
this.startTime = Date.now();
|
|
987
2049
|
this.frameSkipCounter = 0;
|
|
988
2050
|
this.consecutiveFailures = 0;
|
|
989
2051
|
this.lastInferenceTime = 0;
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
2052
|
+
this.detectionBoxes = [];
|
|
2053
|
+
this.alignmentStartTime = undefined;
|
|
2054
|
+
this.hasDocumentDetected = false;
|
|
2055
|
+
if (this.alignmentTimer) {
|
|
2056
|
+
clearTimeout(this.alignmentTimer);
|
|
2057
|
+
this.alignmentTimer = undefined;
|
|
993
2058
|
}
|
|
994
|
-
|
|
995
|
-
|
|
2059
|
+
if (wasVideoActive && this.detectionService.isModelLoaded()) {
|
|
2060
|
+
this.updateStatus('Captura reiniciada', 'Buscando documento en el marco de captura', 'active');
|
|
996
2061
|
this.detectFrame();
|
|
997
2062
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
this.statusMessage = skippedBack ?
|
|
1002
|
-
"Proceso completado (solo frente capturado)" :
|
|
1003
|
-
"Proceso de captura completado exitosamente";
|
|
1004
|
-
this.statusColor = "#28a745";
|
|
1005
|
-
// Detener el detector
|
|
1006
|
-
this.stopDetection();
|
|
1007
|
-
// Emitir evento con las imágenes capturadas
|
|
1008
|
-
const capturedImages = {
|
|
1009
|
-
front: {
|
|
1010
|
-
fullFrame: this.capturedFullFrame,
|
|
1011
|
-
cropped: this.capturedCroppedId
|
|
1012
|
-
},
|
|
1013
|
-
back: {
|
|
1014
|
-
fullFrame: this.capturedBackFullFrame,
|
|
1015
|
-
cropped: this.capturedBackCroppedId
|
|
1016
|
-
},
|
|
1017
|
-
timestamp: new Date().toISOString(),
|
|
1018
|
-
metadata: {
|
|
1019
|
-
totalImages: skippedBack ? 2 : 4,
|
|
1020
|
-
processCompleted: true,
|
|
1021
|
-
backCaptureSkipped: skippedBack
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
this.captureCompleted.emit(capturedImages);
|
|
1025
|
-
// Mostrar animación de éxito después de la captura
|
|
1026
|
-
setTimeout(() => {
|
|
1027
|
-
this.showSuccessAnimation = true;
|
|
1028
|
-
setTimeout(() => {
|
|
1029
|
-
this.showSuccessAnimation = false;
|
|
1030
|
-
}, 3000);
|
|
1031
|
-
}, 800);
|
|
2063
|
+
else {
|
|
2064
|
+
this.updateStatus('Listo para capturar', '', 'ready');
|
|
2065
|
+
}
|
|
1032
2066
|
}
|
|
1033
2067
|
exitSession() {
|
|
1034
2068
|
if (this.videoStream) {
|
|
1035
2069
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
1036
2070
|
this.videoStream = undefined;
|
|
1037
|
-
this.isVideoActive
|
|
1038
|
-
this.statusMessage = "Sesión finalizada.";
|
|
1039
|
-
this.statusColor = "#aaa";
|
|
1040
|
-
}
|
|
1041
|
-
this.isLoading = false;
|
|
1042
|
-
// Limpiar canvas al finalizar sesión
|
|
1043
|
-
if (this.canvasRef) {
|
|
1044
|
-
const ctx = this.canvasRef.getContext("2d");
|
|
1045
|
-
ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
|
|
2071
|
+
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
1046
2072
|
}
|
|
2073
|
+
this.isMaskReady = false;
|
|
2074
|
+
this.updateStatus('Sesión finalizada', '', 'ready');
|
|
2075
|
+
this.detectionBoxes = [];
|
|
1047
2076
|
this.cleanup();
|
|
1048
2077
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
2078
|
+
// PERFORMANCE MONITORING METHODS
|
|
2079
|
+
initializePerformanceMonitor() {
|
|
2080
|
+
this.performanceMetrics.lastUpdateTime = performance.now();
|
|
2081
|
+
// Update performance metrics every 500ms
|
|
2082
|
+
this.performanceUpdateInterval = window.setInterval(() => {
|
|
2083
|
+
this.updatePerformanceMetrics();
|
|
2084
|
+
}, 500);
|
|
2085
|
+
this.logger.debug('Monitor de performance inicializado');
|
|
2086
|
+
}
|
|
2087
|
+
updatePerformanceMetrics() {
|
|
2088
|
+
const currentTime = performance.now();
|
|
2089
|
+
const deltaTime = currentTime - this.performanceMetrics.lastUpdateTime;
|
|
2090
|
+
// Calculate FPS
|
|
2091
|
+
if (deltaTime > 0) {
|
|
2092
|
+
this.performanceMetrics.fps = Math.round(1000 / (deltaTime / this.frameSkipCounter || 1));
|
|
2093
|
+
}
|
|
2094
|
+
// Get memory usage if available
|
|
2095
|
+
if ('memory' in performance) {
|
|
2096
|
+
const memInfo = performance.memory;
|
|
2097
|
+
this.performanceMetrics.memoryUsage = Math.round(memInfo.usedJSHeapSize / 1048576); // MB
|
|
2098
|
+
}
|
|
2099
|
+
// Calculate detection success rate
|
|
2100
|
+
if (this.performanceMetrics.totalDetections > 0) {
|
|
2101
|
+
this.performanceMetrics.successfulDetections = this.performanceMetrics.successfulDetections;
|
|
2102
|
+
const detectionRate = (this.performanceMetrics.successfulDetections / this.performanceMetrics.totalDetections) * 100;
|
|
2103
|
+
this.performanceMetrics.detectionRate = Math.round(detectionRate);
|
|
2104
|
+
}
|
|
2105
|
+
// Update state for rendering
|
|
2106
|
+
this.performanceData = {
|
|
2107
|
+
fps: this.performanceMetrics.fps,
|
|
2108
|
+
inferenceTime: this.performanceMetrics.inferenceTime,
|
|
2109
|
+
memoryUsage: this.performanceMetrics.memoryUsage,
|
|
2110
|
+
onnxLoadTime: this.performanceMetrics.onnxLoadTime,
|
|
2111
|
+
frameProcessingTime: this.performanceMetrics.frameProcessingTime,
|
|
2112
|
+
totalDetections: this.performanceMetrics.totalDetections,
|
|
2113
|
+
successfulDetections: this.performanceMetrics.successfulDetections,
|
|
2114
|
+
detectionRate: this.performanceMetrics.detectionRate
|
|
2115
|
+
};
|
|
2116
|
+
this.performanceMetrics.lastUpdateTime = currentTime;
|
|
2117
|
+
}
|
|
2118
|
+
recordOnnxPerformance(loadTime, inferenceTime) {
|
|
2119
|
+
this.performanceMetrics.onnxLoadTime = Math.round(loadTime);
|
|
2120
|
+
this.performanceMetrics.inferenceTime = Math.round(inferenceTime);
|
|
2121
|
+
}
|
|
2122
|
+
recordFrameProcessing(processingTime, detectionsFound) {
|
|
2123
|
+
this.performanceMetrics.frameProcessingTime = Math.round(processingTime);
|
|
2124
|
+
this.performanceMetrics.totalDetections++;
|
|
2125
|
+
if (detectionsFound > 0) {
|
|
2126
|
+
this.performanceMetrics.successfulDetections++;
|
|
2127
|
+
}
|
|
1051
2128
|
}
|
|
1052
2129
|
};
|
|
1053
2130
|
JaakStamps.style = myComponentCss;
|