@jaak.ai/stamps 2.0.0-dev.30 → 2.0.0-dev.32
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/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +1493 -1254
- 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 +1 -1
- package/dist/collection/components/my-component/my-component.css +325 -126
- package/dist/collection/components/my-component/my-component.js +645 -1389
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/CameraService.js +292 -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/components/jaak-stamps.js +1501 -1275
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/jaak-stamps-webcomponent.js +1 -1
- package/dist/esm/jaak-stamps.entry.js +1493 -1254
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- 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-9d1c45a9.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-9d1c45a9.entry.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +68 -100
- package/dist/types/components.d.ts +3 -3
- package/dist/types/services/CameraService.d.ts +41 -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 +26 -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 +32 -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/package.json +3 -3
- package/dist/jaak-stamps-webcomponent/p-b62b53f8.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-b62b53f8.entry.js.map +0 -1
|
@@ -1,414 +1,120 @@
|
|
|
1
1
|
import { h } from "@stencil/core";
|
|
2
|
+
import { ServiceContainer } from "../../services/ServiceContainer";
|
|
2
3
|
export class JaakStamps {
|
|
3
4
|
el;
|
|
4
5
|
debug = false;
|
|
5
|
-
alignmentTolerance = 10;
|
|
6
|
-
maskSize = 90;
|
|
7
|
-
cropMargin = 0;
|
|
8
|
-
useDocumentClassification = false;
|
|
9
|
-
preferredCamera = 'auto';
|
|
6
|
+
alignmentTolerance = 10;
|
|
7
|
+
maskSize = 90;
|
|
8
|
+
cropMargin = 0;
|
|
9
|
+
useDocumentClassification = false;
|
|
10
|
+
preferredCamera = 'auto';
|
|
10
11
|
captureCompleted;
|
|
11
12
|
isReady;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
statusColor = '#aaa';
|
|
15
|
-
bestScore = 0;
|
|
16
|
-
capturedFullFrame = null;
|
|
17
|
-
capturedCroppedId = null;
|
|
18
|
-
capturedBackFullFrame = null;
|
|
19
|
-
capturedBackCroppedId = null;
|
|
20
|
-
captureStep = 'front';
|
|
21
|
-
isCapturing = false;
|
|
22
|
-
showFlipAnimation = false;
|
|
23
|
-
showSuccessAnimation = false;
|
|
24
|
-
shouldMirrorVideo = true;
|
|
13
|
+
// State derived from services
|
|
14
|
+
detectionBoxes = [];
|
|
25
15
|
sideAlignment = {
|
|
26
|
-
top: false,
|
|
27
|
-
right: false,
|
|
28
|
-
bottom: false,
|
|
29
|
-
left: false
|
|
16
|
+
top: false, right: false, bottom: false, left: false
|
|
30
17
|
};
|
|
31
|
-
isDetectionPaused = false;
|
|
32
|
-
isLoading = false;
|
|
33
|
-
isModelPreloaded = false;
|
|
34
18
|
isMaskReady = false;
|
|
35
|
-
|
|
36
|
-
selectedCameraId = null;
|
|
19
|
+
shouldMirrorVideo = true;
|
|
37
20
|
showCameraSelector = false;
|
|
38
|
-
|
|
21
|
+
isSwitchingCamera = false;
|
|
22
|
+
currentStatus = {
|
|
23
|
+
message: 'Inicializando componente...',
|
|
24
|
+
description: 'Configurando servicios y cargando recursos',
|
|
25
|
+
type: 'initializing',
|
|
26
|
+
isInitialized: false
|
|
27
|
+
};
|
|
28
|
+
performanceData = {
|
|
29
|
+
fps: 0,
|
|
30
|
+
inferenceTime: 0,
|
|
31
|
+
memoryUsage: 0,
|
|
32
|
+
onnxLoadTime: 0,
|
|
33
|
+
frameProcessingTime: 0,
|
|
34
|
+
totalDetections: 0,
|
|
35
|
+
successfulDetections: 0,
|
|
36
|
+
detectionRate: 0
|
|
37
|
+
};
|
|
38
|
+
isPerformanceMonitorMinimized = false;
|
|
39
|
+
// Services
|
|
40
|
+
serviceContainer;
|
|
41
|
+
logger;
|
|
42
|
+
eventBus;
|
|
43
|
+
stateManager;
|
|
44
|
+
cameraService;
|
|
45
|
+
detectionService;
|
|
46
|
+
// UI References
|
|
39
47
|
videoRef;
|
|
40
|
-
|
|
41
|
-
session;
|
|
42
|
-
startTime;
|
|
48
|
+
detectionContainer;
|
|
43
49
|
videoStream;
|
|
44
50
|
animationId;
|
|
45
|
-
|
|
51
|
+
// Detection state
|
|
46
52
|
lastDetectedBox;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
startTime;
|
|
54
|
+
hasScreenshotTaken = false;
|
|
55
|
+
alignmentStartTime;
|
|
56
|
+
alignmentTimer;
|
|
57
|
+
// Performance monitoring
|
|
58
|
+
performanceMetrics = {
|
|
59
|
+
fps: 0,
|
|
60
|
+
inferenceTime: 0,
|
|
61
|
+
memoryUsage: 0,
|
|
62
|
+
cpuUsage: 0,
|
|
63
|
+
onnxLoadTime: 0,
|
|
64
|
+
frameProcessingTime: 0,
|
|
65
|
+
totalDetections: 0,
|
|
66
|
+
successfulDetections: 0,
|
|
67
|
+
detectionRate: 0,
|
|
68
|
+
lastUpdateTime: 0
|
|
69
|
+
};
|
|
70
|
+
performanceUpdateInterval;
|
|
71
|
+
// Performance optimization
|
|
53
72
|
frameSkipCounter = 0;
|
|
54
|
-
FRAME_SKIP = 2;
|
|
73
|
+
FRAME_SKIP = 2;
|
|
55
74
|
consecutiveFailures = 0;
|
|
56
|
-
MAX_FAILURES = 30;
|
|
75
|
+
MAX_FAILURES = 30;
|
|
57
76
|
lastInferenceTime = 0;
|
|
58
|
-
MIN_INFERENCE_INTERVAL = 50;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
CONFIDENCE_THRESHOLD = 0.6;
|
|
69
|
-
// ISO/IEC 7810 ID-1 standard dimensions (85.60mm x 53.98mm)
|
|
70
|
-
ID1_ASPECT_RATIO = 85.60 / 53.98; // 1.5863320574...
|
|
71
|
-
logger = {
|
|
72
|
-
info: (...args) => {
|
|
73
|
-
if (this.debug) {
|
|
74
|
-
console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
warn: (...args) => {
|
|
78
|
-
if (this.debug) {
|
|
79
|
-
console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
error: (...args) => {
|
|
83
|
-
if (this.debug) {
|
|
84
|
-
console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
debug: (...args) => {
|
|
88
|
-
if (this.debug) {
|
|
89
|
-
console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
state: (state, data) => {
|
|
93
|
-
if (this.debug) {
|
|
94
|
-
console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
performance: (operation, duration) => {
|
|
98
|
-
if (this.debug) {
|
|
99
|
-
console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
validateMaskSize() {
|
|
104
|
-
if (this.maskSize < 50 || this.maskSize > 100) {
|
|
105
|
-
this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
|
|
106
|
-
this.maskSize = 90;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
validateCropMargin() {
|
|
110
|
-
if (this.cropMargin < 0 || this.cropMargin > 100) {
|
|
111
|
-
this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
|
|
112
|
-
this.cropMargin = 0;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
validatePreferredCamera() {
|
|
116
|
-
const validOptions = ['auto', 'front', 'back'];
|
|
117
|
-
if (!validOptions.includes(this.preferredCamera)) {
|
|
118
|
-
this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
|
|
119
|
-
this.preferredCamera = 'auto';
|
|
77
|
+
MIN_INFERENCE_INTERVAL = 50;
|
|
78
|
+
async componentDidLoad() {
|
|
79
|
+
this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
|
|
80
|
+
await this.initializeServices();
|
|
81
|
+
this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
|
|
82
|
+
await this.setupEventListeners();
|
|
83
|
+
this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
84
|
+
await this.initializeComponent();
|
|
85
|
+
if (this.debug) {
|
|
86
|
+
this.initializePerformanceMonitor();
|
|
120
87
|
}
|
|
121
88
|
}
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
89
|
+
async initializeServices() {
|
|
90
|
+
const config = {
|
|
91
|
+
debug: this.debug,
|
|
92
|
+
alignmentTolerance: this.alignmentTolerance,
|
|
93
|
+
maskSize: this.maskSize,
|
|
94
|
+
cropMargin: this.cropMargin,
|
|
95
|
+
useDocumentClassification: this.useDocumentClassification,
|
|
96
|
+
preferredCamera: this.preferredCamera
|
|
97
|
+
};
|
|
98
|
+
this.serviceContainer = new ServiceContainer(config);
|
|
99
|
+
this.logger = this.serviceContainer.getLogger();
|
|
100
|
+
this.eventBus = this.serviceContainer.getEventBus();
|
|
101
|
+
this.stateManager = this.serviceContainer.getStateManager();
|
|
102
|
+
this.cameraService = this.serviceContainer.getCameraService();
|
|
103
|
+
this.detectionService = this.serviceContainer.getDetectionService();
|
|
104
|
+
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
105
|
+
}
|
|
106
|
+
async setupEventListeners() {
|
|
107
|
+
this.eventBus.on('state-changed', (data) => {
|
|
108
|
+
this.handleStateChange(data);
|
|
129
109
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
133
|
-
if (!videoTrack)
|
|
134
|
-
return false;
|
|
135
|
-
const settings = videoTrack.getSettings();
|
|
136
|
-
return settings.facingMode === 'environment';
|
|
137
|
-
}
|
|
138
|
-
async detectDeviceTypeAndCameras() {
|
|
139
|
-
// Detect device type
|
|
140
|
-
const userAgent = navigator.userAgent;
|
|
141
|
-
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
142
|
-
const isTablet = /iPad|Android/i.test(userAgent) && window.innerWidth >= 768;
|
|
143
|
-
if (isTablet) {
|
|
144
|
-
this.deviceType = 'tablet';
|
|
145
|
-
}
|
|
146
|
-
else if (isMobile) {
|
|
147
|
-
this.deviceType = 'mobile';
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
this.deviceType = 'desktop';
|
|
151
|
-
}
|
|
152
|
-
this.logger.state('DISPOSITIVO_DETECTADO', {
|
|
153
|
-
deviceType: this.deviceType,
|
|
154
|
-
userAgent: navigator.userAgent,
|
|
155
|
-
screenDimensions: { width: window.innerWidth, height: window.innerHeight }
|
|
110
|
+
this.eventBus.on('camera-changed', (cameraId) => {
|
|
111
|
+
this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
|
|
156
112
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Load user preference
|
|
160
|
-
this.loadCameraPreference();
|
|
161
|
-
}
|
|
162
|
-
async enumerateAndDetectCameras() {
|
|
163
|
-
try {
|
|
164
|
-
// First, check if we have permission to enumerate devices
|
|
165
|
-
const permissionStatus = await this.checkCameraPermission();
|
|
166
|
-
if (permissionStatus === 'denied') {
|
|
167
|
-
this.logger.error('Permiso de cámara denegado por el usuario');
|
|
168
|
-
this.statusMessage = "Permiso de cámara denegado";
|
|
169
|
-
this.statusColor = "#ff6b6b";
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
// Request minimal permission to get device labels
|
|
173
|
-
if (permissionStatus === 'prompt') {
|
|
174
|
-
const tempStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
175
|
-
tempStream.getTracks().forEach(track => track.stop());
|
|
176
|
-
}
|
|
177
|
-
// Now enumerate devices with labels
|
|
178
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
179
|
-
this.availableCameras = devices.filter(device => device.kind === 'videoinput');
|
|
180
|
-
this.isMultipleCamerasAvailable = this.availableCameras.length > 1;
|
|
181
|
-
this.logger.state('CAMARAS_DETECTADAS', {
|
|
182
|
-
count: this.availableCameras.length,
|
|
183
|
-
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable,
|
|
184
|
-
cameras: this.availableCameras.map(cam => ({
|
|
185
|
-
id: cam.deviceId,
|
|
186
|
-
label: cam.label || 'Unknown Camera'
|
|
187
|
-
}))
|
|
188
|
-
});
|
|
189
|
-
// Set initial camera preference based on device type
|
|
190
|
-
this.setInitialCameraPreference();
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
this.logger.error('Error al enumerar cámaras disponibles:', error);
|
|
194
|
-
this.handleCameraPermissionError(error);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async checkCameraPermission() {
|
|
198
|
-
try {
|
|
199
|
-
if (!navigator.permissions) {
|
|
200
|
-
return 'prompt'; // Assume we need to prompt on older browsers
|
|
201
|
-
}
|
|
202
|
-
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
203
|
-
return permission.state;
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
this.logger.warn('No se pudo verificar permisos de cámara:', error);
|
|
207
|
-
return 'prompt';
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
handleCameraPermissionError(error) {
|
|
211
|
-
if (error.name === 'NotAllowedError') {
|
|
212
|
-
this.statusMessage = "Permiso de cámara denegado. Active el permiso en configuración.";
|
|
213
|
-
this.statusColor = "#ff6b6b";
|
|
214
|
-
this.availableCameras = [];
|
|
215
|
-
this.isMultipleCamerasAvailable = false;
|
|
216
|
-
}
|
|
217
|
-
else if (error.name === 'NotFoundError') {
|
|
218
|
-
this.statusMessage = "No se encontraron cámaras disponibles.";
|
|
219
|
-
this.statusColor = "#ff6b6b";
|
|
220
|
-
this.availableCameras = [];
|
|
221
|
-
this.isMultipleCamerasAvailable = false;
|
|
222
|
-
}
|
|
223
|
-
else if (error.name === 'NotReadableError') {
|
|
224
|
-
this.statusMessage = "Cámara en uso por otra aplicación.";
|
|
225
|
-
this.statusColor = "#ff6b6b";
|
|
226
|
-
this.availableCameras = [];
|
|
227
|
-
this.isMultipleCamerasAvailable = false;
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
this.statusMessage = "Error al acceder a las cámaras.";
|
|
231
|
-
this.statusColor = "#ff6b6b";
|
|
232
|
-
this.availableCameras = [];
|
|
233
|
-
this.isMultipleCamerasAvailable = false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
setInitialCameraPreference() {
|
|
237
|
-
if (this.availableCameras.length === 0)
|
|
238
|
-
return;
|
|
239
|
-
// Apply user preference for camera selection
|
|
240
|
-
if (this.preferredCamera === 'front') {
|
|
241
|
-
// User explicitly wants front camera
|
|
242
|
-
this.preferredCameraFacing = 'user';
|
|
243
|
-
const frontCamera = this.availableCameras.find(camera => camera.label.toLowerCase().includes('front') ||
|
|
244
|
-
camera.label.toLowerCase().includes('user') ||
|
|
245
|
-
camera.label.toLowerCase().includes('selfie') ||
|
|
246
|
-
!camera.label.toLowerCase().includes('back') && !camera.label.toLowerCase().includes('rear'));
|
|
247
|
-
if (frontCamera) {
|
|
248
|
-
this.selectedCameraId = frontCamera.deviceId;
|
|
249
|
-
this.logger.state('CAMARA_FRONTAL_SELECCIONADA', { label: frontCamera.label, deviceId: frontCamera.deviceId });
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
this.selectedCameraId = this.availableCameras[0].deviceId;
|
|
253
|
-
this.logger.warn('Cámara frontal no encontrada, usando primera disponible:', this.availableCameras[0].label);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else if (this.preferredCamera === 'back') {
|
|
257
|
-
// User explicitly wants back camera
|
|
258
|
-
this.preferredCameraFacing = 'environment';
|
|
259
|
-
const backCamera = this.availableCameras.find(camera => camera.label.toLowerCase().includes('back') ||
|
|
260
|
-
camera.label.toLowerCase().includes('rear') ||
|
|
261
|
-
camera.label.toLowerCase().includes('environment'));
|
|
262
|
-
if (backCamera) {
|
|
263
|
-
this.selectedCameraId = backCamera.deviceId;
|
|
264
|
-
this.logger.state('CAMARA_TRASERA_SELECCIONADA', { label: backCamera.label, deviceId: backCamera.deviceId });
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
this.selectedCameraId = this.availableCameras[0].deviceId;
|
|
268
|
-
this.logger.warn('Cámara trasera no encontrada, usando primera disponible:', this.availableCameras[0].label);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
// Auto mode - use device type to determine best camera
|
|
273
|
-
if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
|
|
274
|
-
// For mobile/tablet, prefer rear camera for document scanning
|
|
275
|
-
this.preferredCameraFacing = 'environment';
|
|
276
|
-
const rearCamera = this.availableCameras.find(camera => camera.label.toLowerCase().includes('back') ||
|
|
277
|
-
camera.label.toLowerCase().includes('rear') ||
|
|
278
|
-
camera.label.toLowerCase().includes('environment'));
|
|
279
|
-
if (rearCamera) {
|
|
280
|
-
this.selectedCameraId = rearCamera.deviceId;
|
|
281
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear', label: rearCamera.label, deviceId: rearCamera.deviceId });
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
this.selectedCameraId = this.availableCameras[0].deviceId;
|
|
285
|
-
this.logger.warn('Cámara trasera no encontrada en mobile, usando primera disponible:', this.availableCameras[0].label);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
// For desktop, use first available camera (usually the only one)
|
|
290
|
-
this.selectedCameraId = this.availableCameras[0].deviceId;
|
|
291
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', { label: this.availableCameras[0].label, deviceId: this.availableCameras[0].deviceId });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
loadCameraPreference() {
|
|
296
|
-
try {
|
|
297
|
-
const saved = localStorage.getItem('jaak-stamps-camera-preference');
|
|
298
|
-
if (saved) {
|
|
299
|
-
const preference = JSON.parse(saved);
|
|
300
|
-
// Validate that the saved camera is still available
|
|
301
|
-
const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
|
|
302
|
-
if (isStillAvailable) {
|
|
303
|
-
this.selectedCameraId = preference.cameraId;
|
|
304
|
-
this.preferredCameraFacing = preference.facing;
|
|
305
|
-
this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
this.logger.warn('Error al cargar preferencia de cámara:', error);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
saveCameraPreference() {
|
|
314
|
-
try {
|
|
315
|
-
const preference = {
|
|
316
|
-
cameraId: this.selectedCameraId,
|
|
317
|
-
facing: this.preferredCameraFacing,
|
|
318
|
-
timestamp: Date.now()
|
|
319
|
-
};
|
|
320
|
-
localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
|
|
321
|
-
this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
this.logger.warn('Error al guardar preferencia de cámara:', error);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
async switchCamera(cameraId) {
|
|
328
|
-
if (!this.isVideoActive || this.selectedCameraId === cameraId)
|
|
329
|
-
return;
|
|
330
|
-
try {
|
|
331
|
-
// Check if the selected camera is still available
|
|
332
|
-
const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
|
|
333
|
-
if (!selectedCamera) {
|
|
334
|
-
this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
|
|
335
|
-
await this.enumerateAndDetectCameras();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
// Show loading state
|
|
339
|
-
if (this.debug) {
|
|
340
|
-
this.statusMessage = "Cambiando cámara...";
|
|
341
|
-
this.statusColor = "#007bff";
|
|
342
|
-
}
|
|
343
|
-
// Stop current stream
|
|
344
|
-
if (this.videoStream) {
|
|
345
|
-
this.videoStream.getTracks().forEach(track => track.stop());
|
|
346
|
-
}
|
|
347
|
-
// Update selected camera
|
|
348
|
-
this.selectedCameraId = cameraId;
|
|
349
|
-
// Update facing mode preference
|
|
350
|
-
const isRearCamera = selectedCamera.label.toLowerCase().includes('back') ||
|
|
351
|
-
selectedCamera.label.toLowerCase().includes('rear') ||
|
|
352
|
-
selectedCamera.label.toLowerCase().includes('environment');
|
|
353
|
-
this.preferredCameraFacing = isRearCamera ? 'environment' : 'user';
|
|
354
|
-
// Save preference
|
|
355
|
-
this.saveCameraPreference();
|
|
356
|
-
// Setup new camera with error handling
|
|
357
|
-
await this.setupCameraWithRetry();
|
|
358
|
-
this.logger.state('CAMARA_CAMBIADA', { label: selectedCamera.label, deviceId: selectedCamera.deviceId });
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
this.logger.error('Error al cambiar de cámara:', error);
|
|
362
|
-
this.handleCameraPermissionError(error);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
async setupCameraWithRetry(maxRetries = 3) {
|
|
366
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
367
|
-
try {
|
|
368
|
-
await this.setupCamera();
|
|
369
|
-
if (this.debug) {
|
|
370
|
-
this.statusMessage = "Cámara configurada correctamente";
|
|
371
|
-
this.statusColor = "#28a745";
|
|
372
|
-
}
|
|
373
|
-
return; // Success
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
this.logger.error(`Intento ${attempt} de configuración de cámara fallido:`, error);
|
|
377
|
-
if (attempt === maxRetries) {
|
|
378
|
-
// Last attempt failed, handle the error
|
|
379
|
-
this.statusMessage = "Error al configurar la cámara";
|
|
380
|
-
this.statusColor = "#ff6b6b";
|
|
381
|
-
this.handleCameraPermissionError(error);
|
|
382
|
-
throw error;
|
|
383
|
-
}
|
|
384
|
-
// Update status for retry attempt
|
|
385
|
-
if (this.debug) {
|
|
386
|
-
this.statusMessage = `Reintentando configuración de cámara... (${attempt}/${maxRetries})`;
|
|
387
|
-
this.statusColor = "#ffb366";
|
|
388
|
-
}
|
|
389
|
-
// Wait before retrying
|
|
390
|
-
await new Promise(resolve => setTimeout(resolve, 500 * attempt));
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
toggleCameraSelector() {
|
|
395
|
-
this.showCameraSelector = !this.showCameraSelector;
|
|
396
|
-
this.logger.state('SELECTOR_CAMARA_TOGGLE', {
|
|
397
|
-
showCameraSelector: this.showCameraSelector,
|
|
398
|
-
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable,
|
|
399
|
-
availableCameras: this.availableCameras.length,
|
|
400
|
-
isVideoActive: this.isVideoActive
|
|
113
|
+
this.eventBus.on('error', (error) => {
|
|
114
|
+
this.logger.error('Error en servicio:', error);
|
|
401
115
|
});
|
|
402
116
|
}
|
|
403
|
-
async
|
|
404
|
-
if (!this.isMultipleCamerasAvailable)
|
|
405
|
-
return;
|
|
406
|
-
const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
|
|
407
|
-
const nextIndex = (currentIndex + 1) % this.availableCameras.length;
|
|
408
|
-
const nextCamera = this.availableCameras[nextIndex];
|
|
409
|
-
await this.switchCamera(nextCamera.deviceId);
|
|
410
|
-
}
|
|
411
|
-
async componentDidLoad() {
|
|
117
|
+
async initializeComponent() {
|
|
412
118
|
this.logger.state('COMPONENTE_INICIALIZANDO', {
|
|
413
119
|
debug: this.debug,
|
|
414
120
|
maskSize: this.maskSize,
|
|
@@ -416,71 +122,103 @@ export class JaakStamps {
|
|
|
416
122
|
useDocumentClassification: this.useDocumentClassification,
|
|
417
123
|
preferredCamera: this.preferredCamera
|
|
418
124
|
});
|
|
125
|
+
this.validateProps();
|
|
419
126
|
if (this.debug) {
|
|
420
|
-
|
|
421
|
-
this.isLoading = true;
|
|
422
|
-
this.statusMessage = 'Inicializando componente...';
|
|
423
|
-
this.statusColor = '#007bff';
|
|
424
|
-
// Small delay to ensure initialization message is visible
|
|
127
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
425
128
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
426
129
|
}
|
|
427
|
-
this.
|
|
428
|
-
this.
|
|
429
|
-
this.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
130
|
+
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
131
|
+
await this.cameraService.detectDeviceType();
|
|
132
|
+
await this.cameraService.enumerateDevices();
|
|
133
|
+
this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
|
|
134
|
+
await this.loadOnnxRuntime();
|
|
135
|
+
this.initializeResizeObserver();
|
|
136
|
+
}
|
|
137
|
+
validateProps() {
|
|
138
|
+
if (this.maskSize < 50 || this.maskSize > 100) {
|
|
139
|
+
this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
|
|
140
|
+
this.maskSize = 90;
|
|
433
141
|
}
|
|
434
|
-
|
|
142
|
+
if (this.cropMargin < 0 || this.cropMargin > 100) {
|
|
143
|
+
this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
|
|
144
|
+
this.cropMargin = 0;
|
|
145
|
+
}
|
|
146
|
+
const validOptions = ['auto', 'front', 'back'];
|
|
147
|
+
if (!validOptions.includes(this.preferredCamera)) {
|
|
148
|
+
this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
|
|
149
|
+
this.preferredCamera = 'auto';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async loadOnnxRuntime() {
|
|
435
153
|
if (!window.ort) {
|
|
436
|
-
|
|
437
|
-
// Update status for ONNX runtime loading
|
|
438
|
-
this.statusMessage = 'Cargando librerías de IA...';
|
|
439
|
-
}
|
|
154
|
+
this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
|
|
440
155
|
const script = document.createElement('script');
|
|
441
156
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
442
157
|
document.head.appendChild(script);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
};
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
script.onload = () => {
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
this.finalizeInitialization();
|
|
162
|
+
resolve(undefined);
|
|
163
|
+
}, 300);
|
|
164
|
+
};
|
|
165
|
+
});
|
|
452
166
|
}
|
|
453
167
|
else {
|
|
454
|
-
// ONNX runtime already loaded
|
|
455
168
|
setTimeout(() => {
|
|
456
|
-
this.
|
|
457
|
-
this.statusColor = '#aaa';
|
|
458
|
-
this.isLoading = false;
|
|
459
|
-
this.emitReadyEvent();
|
|
169
|
+
this.finalizeInitialization();
|
|
460
170
|
}, 300);
|
|
461
171
|
}
|
|
462
|
-
// Initialize canvas pool for better performance
|
|
463
|
-
this.initializeCanvasPool();
|
|
464
|
-
this.setupResizeObserver();
|
|
465
172
|
}
|
|
466
|
-
|
|
467
|
-
|
|
173
|
+
finalizeInitialization() {
|
|
174
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
175
|
+
this.currentStatus = {
|
|
176
|
+
message: 'Listo para capturar',
|
|
177
|
+
description: '',
|
|
178
|
+
type: 'ready',
|
|
179
|
+
isInitialized: true
|
|
180
|
+
};
|
|
181
|
+
this.emitReadyEvent();
|
|
182
|
+
}
|
|
183
|
+
updateStatus(message, description, type = 'loading') {
|
|
184
|
+
this.currentStatus = {
|
|
185
|
+
message,
|
|
186
|
+
description,
|
|
187
|
+
type,
|
|
188
|
+
isInitialized: this.currentStatus.isInitialized
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
emitReadyEvent() {
|
|
192
|
+
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
193
|
+
this.isReady.emit(isDocumentReady);
|
|
194
|
+
this.logger.state('COMPONENTE_LISTO', {
|
|
195
|
+
ortLibraryLoaded: !!window.ort,
|
|
196
|
+
modelPreloaded: this.detectionService.isModelLoaded(),
|
|
197
|
+
isReady: isDocumentReady
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
handleStateChange(data) {
|
|
201
|
+
// React to state changes from StateManager
|
|
202
|
+
// This is where the component updates its internal state based on service state changes
|
|
203
|
+
if (data.changes.isLoading !== undefined) {
|
|
204
|
+
// Force re-render when loading state changes
|
|
205
|
+
// Note: Stencil automatically re-renders when @State changes
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
initializeResizeObserver() {
|
|
209
|
+
if ('ResizeObserver' in window && this.detectionContainer) {
|
|
468
210
|
const resizeObserver = new ResizeObserver(() => {
|
|
469
211
|
this.handleResize();
|
|
470
212
|
});
|
|
471
|
-
resizeObserver.observe(this.
|
|
213
|
+
resizeObserver.observe(this.detectionContainer.parentElement);
|
|
472
214
|
}
|
|
473
215
|
}
|
|
474
216
|
handleResize() {
|
|
475
|
-
if (this.
|
|
476
|
-
const container = this.
|
|
217
|
+
if (this.detectionContainer) {
|
|
218
|
+
const container = this.detectionContainer.parentElement;
|
|
477
219
|
const rect = container.getBoundingClientRect();
|
|
478
|
-
// Set canvas size to match container
|
|
479
|
-
this.canvasRef.width = rect.width;
|
|
480
|
-
this.canvasRef.height = rect.height;
|
|
481
|
-
// Update mask positioning based on container and video dimensions
|
|
482
220
|
this.updateMaskDimensions(rect);
|
|
483
|
-
this.logger.debug('
|
|
221
|
+
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
484
222
|
}
|
|
485
223
|
}
|
|
486
224
|
updateMaskDimensions(containerRect) {
|
|
@@ -497,32 +235,27 @@ export class JaakStamps {
|
|
|
497
235
|
let displayedVideoWidth, displayedVideoHeight;
|
|
498
236
|
let videoOffsetX = 0, videoOffsetY = 0;
|
|
499
237
|
if (videoAspectRatio > containerAspectRatio) {
|
|
500
|
-
// Video is wider - letterboxed (black bars top/bottom)
|
|
501
238
|
displayedVideoWidth = containerRect.width;
|
|
502
239
|
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
503
240
|
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
504
241
|
}
|
|
505
242
|
else {
|
|
506
|
-
// Video is taller - pillarboxed (black bars left/right)
|
|
507
243
|
displayedVideoHeight = containerRect.height;
|
|
508
244
|
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
509
245
|
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
510
246
|
}
|
|
511
|
-
// Calculate
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const maxWidthByHeight = displayedVideoHeight * this.ID1_ASPECT_RATIO;
|
|
247
|
+
// Calculate mask dimensions with ID-1 aspect ratio
|
|
248
|
+
const ID1_ASPECT_RATIO = 85.60 / 53.98;
|
|
249
|
+
const maxWidthByHeight = displayedVideoHeight * ID1_ASPECT_RATIO;
|
|
515
250
|
let maskWidthInVideo, maskHeightInVideo;
|
|
516
251
|
const sizeRatio = this.maskSize / 100;
|
|
517
252
|
if (maxWidthByHeight <= displayedVideoWidth) {
|
|
518
|
-
// Video height is the limiting factor
|
|
519
253
|
maskHeightInVideo = displayedVideoHeight * sizeRatio;
|
|
520
|
-
maskWidthInVideo = maskHeightInVideo *
|
|
254
|
+
maskWidthInVideo = maskHeightInVideo * ID1_ASPECT_RATIO;
|
|
521
255
|
}
|
|
522
256
|
else {
|
|
523
|
-
// Video width is the limiting factor
|
|
524
257
|
maskWidthInVideo = displayedVideoWidth * sizeRatio;
|
|
525
|
-
maskHeightInVideo = maskWidthInVideo /
|
|
258
|
+
maskHeightInVideo = maskWidthInVideo / ID1_ASPECT_RATIO;
|
|
526
259
|
}
|
|
527
260
|
// Convert to percentages of the container
|
|
528
261
|
const maskWidthPercent = (maskWidthInVideo / containerRect.width) * 100;
|
|
@@ -538,7 +271,6 @@ export class JaakStamps {
|
|
|
538
271
|
this.el.style.setProperty('--mask-height', `${maskHeightPercent}%`);
|
|
539
272
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
540
273
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
541
|
-
// Mark mask as ready now that dimensions are calculated
|
|
542
274
|
this.isMaskReady = true;
|
|
543
275
|
this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
|
|
544
276
|
video: { width: videoWidth, height: videoHeight },
|
|
@@ -548,46 +280,16 @@ export class JaakStamps {
|
|
|
548
280
|
offset: { x: videoOffsetX, y: videoOffsetY }
|
|
549
281
|
});
|
|
550
282
|
}
|
|
551
|
-
|
|
552
|
-
// Preprocess canvas - reused for every inference
|
|
553
|
-
this.preprocessCanvas = document.createElement('canvas');
|
|
554
|
-
this.preprocessCanvas.width = this.INPUT_SIZE;
|
|
555
|
-
this.preprocessCanvas.height = this.INPUT_SIZE;
|
|
556
|
-
this.preprocessCtx = this.preprocessCanvas.getContext('2d', {
|
|
557
|
-
alpha: false, // No transparency needed
|
|
558
|
-
willReadFrequently: true // Optimize for frequent getImageData calls
|
|
559
|
-
});
|
|
560
|
-
// Capture canvas - reused for screenshots
|
|
561
|
-
this.captureCanvas = document.createElement('canvas');
|
|
562
|
-
this.captureCtx = this.captureCanvas.getContext('2d', {
|
|
563
|
-
alpha: false
|
|
564
|
-
});
|
|
565
|
-
this.logger.state('CANVAS_POOL_INICIALIZADO', { preprocessCanvasSize: this.INPUT_SIZE });
|
|
566
|
-
}
|
|
567
|
-
disconnectedCallback() {
|
|
568
|
-
this.cleanup();
|
|
569
|
-
}
|
|
283
|
+
// PUBLIC METHODS
|
|
570
284
|
async getCapturedImages() {
|
|
571
|
-
|
|
285
|
+
const state = this.stateManager.getCaptureState();
|
|
286
|
+
if (state.step !== 'completed') {
|
|
572
287
|
throw new Error('El proceso de captura no ha sido completado');
|
|
573
288
|
}
|
|
574
|
-
return
|
|
575
|
-
front: {
|
|
576
|
-
fullFrame: this.capturedFullFrame,
|
|
577
|
-
cropped: this.capturedCroppedId
|
|
578
|
-
},
|
|
579
|
-
back: {
|
|
580
|
-
fullFrame: this.capturedBackFullFrame,
|
|
581
|
-
cropped: this.capturedBackCroppedId
|
|
582
|
-
},
|
|
583
|
-
metadata: {
|
|
584
|
-
hasBackCapture: !!(this.capturedBackFullFrame && this.capturedBackCroppedId),
|
|
585
|
-
totalImages: (this.capturedBackFullFrame && this.capturedBackCroppedId) ? 4 : 2
|
|
586
|
-
}
|
|
587
|
-
};
|
|
289
|
+
return this.stateManager.getCapturedImages();
|
|
588
290
|
}
|
|
589
291
|
async isProcessCompleted() {
|
|
590
|
-
return this.
|
|
292
|
+
return this.stateManager.isProcessCompleted();
|
|
591
293
|
}
|
|
592
294
|
async startCapture() {
|
|
593
295
|
await this.startDetection();
|
|
@@ -598,611 +300,219 @@ export class JaakStamps {
|
|
|
598
300
|
async resetCapture() {
|
|
599
301
|
this.resetDetection();
|
|
600
302
|
}
|
|
303
|
+
async skipBackCapture() {
|
|
304
|
+
const captureState = this.stateManager.getCaptureState();
|
|
305
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
306
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
307
|
+
this.completeProcess(true);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
601
310
|
async getStatus() {
|
|
311
|
+
const captureState = this.stateManager.getCaptureState();
|
|
312
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
602
313
|
return {
|
|
603
|
-
isVideoActive:
|
|
604
|
-
captureStep:
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
isModelPreloaded: this.isModelPreloaded
|
|
314
|
+
isVideoActive: captureState.isVideoActive,
|
|
315
|
+
captureStep: captureState.step,
|
|
316
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
317
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
318
|
+
isModelPreloaded: this.detectionService.isModelLoaded()
|
|
609
319
|
};
|
|
610
320
|
}
|
|
611
321
|
async preloadModel() {
|
|
612
|
-
if (this.
|
|
613
|
-
this.logger.state('MODELO_YA_PRECARGADO'
|
|
322
|
+
if (this.detectionService.isModelLoaded()) {
|
|
323
|
+
this.logger.state('MODELO_YA_PRECARGADO');
|
|
324
|
+
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
614
325
|
return { success: true, message: 'Model already loaded' };
|
|
615
326
|
}
|
|
616
327
|
try {
|
|
617
|
-
|
|
618
|
-
this.
|
|
619
|
-
this.
|
|
620
|
-
|
|
621
|
-
this.
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
catch (error) {
|
|
629
|
-
if (error.message.includes('failed to allocate a buffer')) {
|
|
630
|
-
this.logger.warn('Fallo en asignación de buffer durante precarga, intentando con configuración mínima');
|
|
631
|
-
const fallbackOptions = {
|
|
632
|
-
executionProviders: ['wasm'],
|
|
633
|
-
graphOptimizationLevel: 'disabled',
|
|
634
|
-
logSeverityLevel: 4,
|
|
635
|
-
enableCpuMemArena: false,
|
|
636
|
-
enableMemPattern: false,
|
|
637
|
-
executionMode: 'sequential',
|
|
638
|
-
interOpNumThreads: 1,
|
|
639
|
-
intraOpNumThreads: 1,
|
|
640
|
-
};
|
|
641
|
-
this.session = await window.ort.InferenceSession.create(modelPath, fallbackOptions);
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
throw error;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
// Preload MobileNet model and classes only if classification is enabled
|
|
648
|
-
// For low memory devices, load sequentially to avoid memory pressure
|
|
649
|
-
if (this.useDocumentClassification) {
|
|
650
|
-
if (deviceInfo.isLowMemory) {
|
|
651
|
-
this.logger.state('CARGA_SECUENCIAL_MODELOS', { reason: 'low memory device' });
|
|
652
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
653
|
-
}
|
|
654
|
-
await this.loadMobileNetModel();
|
|
328
|
+
const loadStartTime = performance.now();
|
|
329
|
+
this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
|
|
330
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
331
|
+
await this.detectionService.loadModel();
|
|
332
|
+
this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
|
|
333
|
+
await this.detectionService.loadClassificationModel();
|
|
334
|
+
const loadEndTime = performance.now();
|
|
335
|
+
const loadTime = loadEndTime - loadStartTime;
|
|
336
|
+
// Record ONNX load time
|
|
337
|
+
if (this.debug) {
|
|
338
|
+
this.recordOnnxPerformance(loadTime, 0);
|
|
655
339
|
}
|
|
656
|
-
this.
|
|
657
|
-
|
|
658
|
-
this.
|
|
659
|
-
this.
|
|
340
|
+
this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
|
|
341
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
342
|
+
this.updateStatus('Modelos precargados', '', 'ready');
|
|
343
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
660
344
|
this.emitReadyEvent();
|
|
661
|
-
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', {
|
|
662
|
-
detectionModel: !!this.session,
|
|
663
|
-
classificationModel: !!this.mobileNetSession,
|
|
664
|
-
useClassification: this.useDocumentClassification
|
|
665
|
-
});
|
|
345
|
+
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
666
346
|
return { success: true, message: 'Models preloaded successfully' };
|
|
667
347
|
}
|
|
668
348
|
catch (error) {
|
|
669
349
|
this.logger.error('Error al precargar modelos:', error);
|
|
670
|
-
this.
|
|
671
|
-
this.
|
|
672
|
-
this.statusColor = "#ff6b6b";
|
|
350
|
+
this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
|
|
351
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
673
352
|
return { success: false, error: error.message };
|
|
674
353
|
}
|
|
675
354
|
}
|
|
676
|
-
async skipBackCapture() {
|
|
677
|
-
if (this.captureStep === 'back' && this.capturedFullFrame) {
|
|
678
|
-
this.completeProcess(true);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
355
|
async getCameraInfo() {
|
|
682
|
-
return
|
|
683
|
-
availableCameras: this.availableCameras.map(camera => ({
|
|
684
|
-
id: camera.deviceId,
|
|
685
|
-
label: camera.label || 'Unknown Camera',
|
|
686
|
-
selected: camera.deviceId === this.selectedCameraId
|
|
687
|
-
})),
|
|
688
|
-
selectedCameraId: this.selectedCameraId,
|
|
689
|
-
deviceType: this.deviceType,
|
|
690
|
-
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable,
|
|
691
|
-
preferredFacing: this.preferredCameraFacing,
|
|
692
|
-
userPreferredCamera: this.preferredCamera
|
|
693
|
-
};
|
|
356
|
+
return this.cameraService.getCameraInfo();
|
|
694
357
|
}
|
|
695
358
|
async setPreferredCamera(camera) {
|
|
696
359
|
this.preferredCamera = camera;
|
|
697
|
-
this.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return {
|
|
705
|
-
success: true,
|
|
706
|
-
selectedCamera: this.selectedCameraId,
|
|
707
|
-
availableCameras: this.availableCameras.length
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
async loadMobileNetModel() {
|
|
711
|
-
try {
|
|
712
|
-
this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
|
|
713
|
-
// Load class map
|
|
714
|
-
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
715
|
-
if (!classResponse.ok) {
|
|
716
|
-
throw new Error(`Failed to load class map: ${this.MOBILENET_CLASSES_PATH}`);
|
|
717
|
-
}
|
|
718
|
-
this.mobileNetClassMap = await classResponse.json();
|
|
719
|
-
this.logger.state('CLASES_MOBILENET_CARGADAS', { classCount: Object.keys(this.mobileNetClassMap).length });
|
|
720
|
-
// Load model
|
|
721
|
-
const sessionOptions = this.getSessionOptions();
|
|
722
|
-
try {
|
|
723
|
-
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
|
|
724
|
-
}
|
|
725
|
-
catch (error) {
|
|
726
|
-
if (error.message.includes('failed to allocate a buffer')) {
|
|
727
|
-
this.logger.warn('Fallo en asignación de buffer de MobileNet, intentando con configuración mínima');
|
|
728
|
-
const fallbackOptions = {
|
|
729
|
-
executionProviders: ['wasm'],
|
|
730
|
-
graphOptimizationLevel: 'disabled',
|
|
731
|
-
logSeverityLevel: 4,
|
|
732
|
-
enableCpuMemArena: false,
|
|
733
|
-
enableMemPattern: false,
|
|
734
|
-
executionMode: 'sequential',
|
|
735
|
-
interOpNumThreads: 1,
|
|
736
|
-
intraOpNumThreads: 1,
|
|
737
|
-
};
|
|
738
|
-
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
|
|
739
|
-
}
|
|
740
|
-
else {
|
|
741
|
-
throw error;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.mobileNetSession });
|
|
745
|
-
}
|
|
746
|
-
catch (error) {
|
|
747
|
-
this.logger.error('Error al cargar modelo MobileNet:', error);
|
|
748
|
-
throw error;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
preprocessMobileNet(canvas) {
|
|
752
|
-
// Create a temporary canvas for MobileNet preprocessing (224x224)
|
|
753
|
-
const tempCanvas = document.createElement('canvas');
|
|
754
|
-
tempCanvas.width = 224;
|
|
755
|
-
tempCanvas.height = 224;
|
|
756
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
757
|
-
// Draw the cropped image onto the 224x224 canvas
|
|
758
|
-
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
759
|
-
// Get image data and preprocess for MobileNet
|
|
760
|
-
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
761
|
-
const data = imageData.data;
|
|
762
|
-
const hw = 224 * 224;
|
|
763
|
-
const arr = new Float32Array(3 * hw);
|
|
764
|
-
// Normalize pixels: (pixel/255 - 0.5) / 0.5 = (pixel - 127.5) / 127.5
|
|
765
|
-
for (let i = 0; i < hw; i++) {
|
|
766
|
-
arr[i] = (data[i * 4 + 0] / 255 - 0.5) / 0.5; // R
|
|
767
|
-
arr[hw + i] = (data[i * 4 + 1] / 255 - 0.5) / 0.5; // G
|
|
768
|
-
arr[2 * hw + i] = (data[i * 4 + 2] / 255 - 0.5) / 0.5; // B
|
|
769
|
-
}
|
|
770
|
-
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
771
|
-
}
|
|
772
|
-
async classifyDocument(canvas) {
|
|
773
|
-
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
774
|
-
this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
try {
|
|
778
|
-
this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
|
|
779
|
-
// Preprocess image for MobileNet
|
|
780
|
-
const inputTensor = this.preprocessMobileNet(canvas);
|
|
781
|
-
// Run inference
|
|
782
|
-
const feeds = { input: inputTensor };
|
|
783
|
-
const results = await this.mobileNetSession.run(feeds);
|
|
784
|
-
const output = results[Object.keys(results)[0]].data;
|
|
785
|
-
// Find the class with highest confidence
|
|
786
|
-
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
787
|
-
const confidence = output[maxIdx];
|
|
788
|
-
const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
|
|
789
|
-
this.logger.state('DOCUMENTO_CLASIFICADO', {
|
|
790
|
-
class: className,
|
|
791
|
-
confidence: confidence.toFixed(3),
|
|
792
|
-
classIndex: maxIdx,
|
|
793
|
-
timestamp: Date.now()
|
|
794
|
-
});
|
|
795
|
-
return {
|
|
796
|
-
class: className,
|
|
797
|
-
confidence: confidence,
|
|
798
|
-
classIndex: maxIdx
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
catch (error) {
|
|
802
|
-
this.logger.error('Error al clasificar documento:', error);
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
cleanup() {
|
|
807
|
-
if (this.animationId) {
|
|
808
|
-
cancelAnimationFrame(this.animationId);
|
|
809
|
-
}
|
|
810
|
-
if (this.videoStream) {
|
|
811
|
-
this.videoStream.getTracks().forEach(track => track.stop());
|
|
812
|
-
}
|
|
813
|
-
// Limpiar canvas en cleanup general
|
|
814
|
-
if (this.canvasRef) {
|
|
815
|
-
const ctx = this.canvasRef.getContext("2d");
|
|
816
|
-
if (ctx) {
|
|
817
|
-
ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
|
|
360
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
361
|
+
await this.cameraService.enumerateDevices();
|
|
362
|
+
const captureState = this.stateManager.getCaptureState();
|
|
363
|
+
if (captureState.isVideoActive) {
|
|
364
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
365
|
+
if (selectedCameraId) {
|
|
366
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
818
367
|
}
|
|
819
368
|
}
|
|
820
|
-
// OPTIMIZATION: Clean up canvas pool
|
|
821
|
-
this.cleanupCanvasPool();
|
|
822
|
-
}
|
|
823
|
-
cleanupCanvasPool() {
|
|
824
|
-
// Release canvas references for garbage collection
|
|
825
|
-
if (this.preprocessCanvas) {
|
|
826
|
-
this.preprocessCtx = undefined;
|
|
827
|
-
this.preprocessCanvas = undefined;
|
|
828
|
-
}
|
|
829
|
-
if (this.captureCanvas) {
|
|
830
|
-
this.captureCtx = undefined;
|
|
831
|
-
this.captureCanvas = undefined;
|
|
832
|
-
}
|
|
833
|
-
// Disposed ONNX sessions
|
|
834
|
-
if (this.session) {
|
|
835
|
-
this.session.release?.();
|
|
836
|
-
this.session = undefined;
|
|
837
|
-
}
|
|
838
|
-
if (this.mobileNetSession) {
|
|
839
|
-
this.mobileNetSession.release?.();
|
|
840
|
-
this.mobileNetSession = undefined;
|
|
841
|
-
}
|
|
842
|
-
this.mobileNetClassMap = undefined;
|
|
843
|
-
this.isModelPreloaded = false;
|
|
844
|
-
this.logger.state('CANVAS_POOL_LIMPIADO', { timestamp: Date.now() });
|
|
845
|
-
}
|
|
846
|
-
async getMaxResolution() {
|
|
847
|
-
try {
|
|
848
|
-
// Build constraints with selected camera if available
|
|
849
|
-
const videoConstraints = {};
|
|
850
|
-
if (this.selectedCameraId) {
|
|
851
|
-
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
852
|
-
}
|
|
853
|
-
else if (this.preferredCameraFacing) {
|
|
854
|
-
videoConstraints.facingMode = this.preferredCameraFacing;
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
videoConstraints.facingMode = "environment";
|
|
858
|
-
}
|
|
859
|
-
// Get temporary stream to access capabilities
|
|
860
|
-
const tempStream = await navigator.mediaDevices.getUserMedia({
|
|
861
|
-
video: videoConstraints
|
|
862
|
-
});
|
|
863
|
-
const videoTrack = tempStream.getVideoTracks()[0];
|
|
864
|
-
const capabilities = videoTrack.getCapabilities();
|
|
865
|
-
// Detener el stream temporal
|
|
866
|
-
tempStream.getTracks().forEach(track => track.stop());
|
|
867
|
-
// Build constraints with resolution optimized for tablets
|
|
868
|
-
const constraints = {};
|
|
869
|
-
// Set camera selection constraints
|
|
870
|
-
if (this.selectedCameraId) {
|
|
871
|
-
constraints.deviceId = { exact: this.selectedCameraId };
|
|
872
|
-
}
|
|
873
|
-
else if (this.preferredCameraFacing) {
|
|
874
|
-
constraints.facingMode = this.preferredCameraFacing;
|
|
875
|
-
}
|
|
876
|
-
else {
|
|
877
|
-
constraints.facingMode = "environment";
|
|
878
|
-
}
|
|
879
|
-
;
|
|
880
|
-
if (capabilities.width && capabilities.height) {
|
|
881
|
-
// Limitar resolución máxima para tablets para evitar problemas de rendimiento
|
|
882
|
-
const maxWidth = Math.min(capabilities.width.max, 1920);
|
|
883
|
-
const maxHeight = Math.min(capabilities.height.max, 1080);
|
|
884
|
-
// Para tablets, usar resolución moderada en lugar de máxima
|
|
885
|
-
const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
|
|
886
|
-
if (isTablet) {
|
|
887
|
-
constraints.width = { ideal: Math.min(maxWidth, 1280) };
|
|
888
|
-
constraints.height = { ideal: Math.min(maxHeight, 720) };
|
|
889
|
-
}
|
|
890
|
-
else {
|
|
891
|
-
constraints.width = { ideal: maxWidth };
|
|
892
|
-
constraints.height = { ideal: maxHeight };
|
|
893
|
-
}
|
|
894
|
-
this.logger.state('CAPACIDADES_RESOLUCION_DETECTADAS', {
|
|
895
|
-
maxWidth: capabilities.width.max,
|
|
896
|
-
maxHeight: capabilities.height.max,
|
|
897
|
-
selectedWidth: constraints.width.ideal,
|
|
898
|
-
selectedHeight: constraints.height.ideal,
|
|
899
|
-
isTablet,
|
|
900
|
-
deviceType: this.deviceType
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
return constraints;
|
|
904
|
-
}
|
|
905
|
-
catch (err) {
|
|
906
|
-
this.logger.warn('No se pudieron obtener capacidades de cámara, usando configuración de respaldo');
|
|
907
|
-
// Optimized fallback for tablets
|
|
908
|
-
const isTablet = /iPad|Android/i.test(navigator.userAgent) && window.innerWidth >= 768;
|
|
909
|
-
const fallbackConstraints = {
|
|
910
|
-
width: { ideal: isTablet ? 1280 : 1920 },
|
|
911
|
-
height: { ideal: isTablet ? 720 : 1080 }
|
|
912
|
-
};
|
|
913
|
-
// Add camera selection to fallback
|
|
914
|
-
if (this.selectedCameraId) {
|
|
915
|
-
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
916
|
-
}
|
|
917
|
-
else if (this.preferredCameraFacing) {
|
|
918
|
-
fallbackConstraints.facingMode = this.preferredCameraFacing;
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
fallbackConstraints.facingMode = "environment";
|
|
922
|
-
}
|
|
923
|
-
return fallbackConstraints;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
async setupCamera() {
|
|
927
|
-
try {
|
|
928
|
-
const constraints = await this.getMaxResolution();
|
|
929
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
930
|
-
video: constraints,
|
|
931
|
-
audio: false
|
|
932
|
-
});
|
|
933
|
-
if (this.videoRef) {
|
|
934
|
-
this.videoRef.srcObject = stream;
|
|
935
|
-
this.videoStream = stream;
|
|
936
|
-
// Determine if video should be mirrored
|
|
937
|
-
const isRear = this.isRearCamera(stream);
|
|
938
|
-
this.shouldMirrorVideo = !isRear;
|
|
939
|
-
this.logger.state('CAMARA_CONFIGURADA', {
|
|
940
|
-
isRearCamera: isRear,
|
|
941
|
-
shouldMirrorVideo: this.shouldMirrorVideo,
|
|
942
|
-
videoActive: this.isVideoActive
|
|
943
|
-
});
|
|
944
|
-
return new Promise((resolve) => {
|
|
945
|
-
this.videoRef.onloadedmetadata = async () => {
|
|
946
|
-
await this.videoRef.play();
|
|
947
|
-
this.isVideoActive = true;
|
|
948
|
-
// Update mask dimensions once video is loaded
|
|
949
|
-
if (this.canvasRef) {
|
|
950
|
-
const container = this.canvasRef.parentElement;
|
|
951
|
-
const rect = container.getBoundingClientRect();
|
|
952
|
-
this.updateMaskDimensions(rect);
|
|
953
|
-
}
|
|
954
|
-
resolve();
|
|
955
|
-
};
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
catch (err) {
|
|
960
|
-
this.logger.error('No se pudo acceder a la cámara:', err);
|
|
961
|
-
this.handleCameraPermissionError(err);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
preprocess(video) {
|
|
965
|
-
// OPTIMIZATION: Reuse canvas instead of creating new ones
|
|
966
|
-
if (!this.preprocessCanvas || !this.preprocessCtx) {
|
|
967
|
-
this.initializeCanvasPool();
|
|
968
|
-
}
|
|
969
|
-
// Clear and redraw on reused canvas
|
|
970
|
-
this.preprocessCtx.clearRect(0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
|
|
971
|
-
this.preprocessCtx.drawImage(video, 0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
|
|
972
|
-
const imageData = this.preprocessCtx.getImageData(0, 0, this.INPUT_SIZE, this.INPUT_SIZE);
|
|
973
|
-
const [R, G, B] = [[], [], []];
|
|
974
|
-
const { data } = imageData;
|
|
975
|
-
// Optimized pixel processing
|
|
976
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
977
|
-
R.push(data[i] / 255);
|
|
978
|
-
G.push(data[i + 1] / 255);
|
|
979
|
-
B.push(data[i + 2] / 255);
|
|
980
|
-
}
|
|
981
|
-
const transposedData = new Float32Array(R.concat(G, B));
|
|
982
|
-
// Convert to float16 for the lighter model
|
|
983
|
-
const float16Data = new Uint16Array(transposedData.length);
|
|
984
|
-
for (let i = 0; i < transposedData.length; i++) {
|
|
985
|
-
float16Data[i] = this.float32ToFloat16(transposedData[i]);
|
|
986
|
-
}
|
|
987
|
-
return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
|
|
988
|
-
}
|
|
989
|
-
float32ToFloat16(value) {
|
|
990
|
-
// Convert float32 to float16 using IEEE 754 half precision format
|
|
991
|
-
const buffer = new ArrayBuffer(4);
|
|
992
|
-
const view = new DataView(buffer);
|
|
993
|
-
view.setFloat32(0, value, true);
|
|
994
|
-
const f = view.getUint32(0, true);
|
|
995
|
-
const sign = (f >> 31) & 0x1;
|
|
996
|
-
const exp = (f >> 23) & 0xFF;
|
|
997
|
-
const frac = f & 0x7FFFFF;
|
|
998
|
-
let newExp = exp - 127 + 15;
|
|
999
|
-
if (exp === 0) {
|
|
1000
|
-
newExp = 0;
|
|
1001
|
-
}
|
|
1002
|
-
else if (exp === 0xFF) {
|
|
1003
|
-
newExp = 0x1F;
|
|
1004
|
-
}
|
|
1005
|
-
else if (newExp >= 0x1F) {
|
|
1006
|
-
newExp = 0x1F;
|
|
1007
|
-
return (sign << 15) | (newExp << 10);
|
|
1008
|
-
}
|
|
1009
|
-
else if (newExp <= 0) {
|
|
1010
|
-
return (sign << 15);
|
|
1011
|
-
}
|
|
1012
|
-
return (sign << 15) | (newExp << 10) | (frac >> 13);
|
|
1013
|
-
}
|
|
1014
|
-
getDeviceMemoryInfo() {
|
|
1015
|
-
const nav = navigator;
|
|
1016
|
-
const memory = nav.deviceMemory || nav.hardwareConcurrency || 4;
|
|
1017
|
-
const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
1018
|
-
const isSlowConnection = connection && (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g');
|
|
1019
369
|
return {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
getSessionOptions() {
|
|
1026
|
-
const deviceInfo = this.getDeviceMemoryInfo();
|
|
1027
|
-
if (deviceInfo.isLowMemory) {
|
|
1028
|
-
return {
|
|
1029
|
-
executionProviders: ['wasm'],
|
|
1030
|
-
graphOptimizationLevel: 'basic',
|
|
1031
|
-
logSeverityLevel: 4,
|
|
1032
|
-
logVerbosityLevel: 0,
|
|
1033
|
-
enableCpuMemArena: false,
|
|
1034
|
-
enableMemPattern: false,
|
|
1035
|
-
executionMode: 'sequential',
|
|
1036
|
-
interOpNumThreads: 1,
|
|
1037
|
-
intraOpNumThreads: 1,
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
return {
|
|
1041
|
-
executionProviders: [
|
|
1042
|
-
'webgl',
|
|
1043
|
-
'wasm'
|
|
1044
|
-
],
|
|
1045
|
-
graphOptimizationLevel: 'all',
|
|
1046
|
-
logSeverityLevel: this.debug ? 2 : 4,
|
|
1047
|
-
logVerbosityLevel: 0,
|
|
1048
|
-
enableCpuMemArena: true,
|
|
1049
|
-
enableMemPattern: true,
|
|
1050
|
-
executionMode: 'parallel',
|
|
1051
|
-
interOpNumThreads: 2,
|
|
1052
|
-
intraOpNumThreads: 2,
|
|
370
|
+
success: true,
|
|
371
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
372
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1053
373
|
};
|
|
1054
374
|
}
|
|
375
|
+
// DETECTION METHODS
|
|
1055
376
|
async startDetection() {
|
|
1056
|
-
this.logger.state('INICIANDO_DETECCION'
|
|
1057
|
-
sessionExists: !!this.session,
|
|
1058
|
-
modelPreloaded: this.isModelPreloaded,
|
|
1059
|
-
videoActive: this.isVideoActive,
|
|
1060
|
-
captureStep: this.captureStep
|
|
1061
|
-
});
|
|
377
|
+
this.logger.state('INICIANDO_DETECCION');
|
|
1062
378
|
try {
|
|
1063
|
-
//
|
|
1064
|
-
if (!this.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const modelPath = this.MODEL_PATH;
|
|
1075
|
-
this.logger.state('CARGANDO_MODELO_DETECCION', { modelPath });
|
|
1076
|
-
const sessionOptions = this.getSessionOptions();
|
|
1077
|
-
try {
|
|
1078
|
-
this.session = await window.ort.InferenceSession.create(modelPath, sessionOptions);
|
|
1079
|
-
}
|
|
1080
|
-
catch (error) {
|
|
1081
|
-
if (error.message.includes('failed to allocate a buffer')) {
|
|
1082
|
-
this.logger.warn('Fallo en asignación de buffer, intentando con configuración mínima');
|
|
1083
|
-
const fallbackOptions = {
|
|
1084
|
-
executionProviders: ['wasm'],
|
|
1085
|
-
graphOptimizationLevel: 'disabled',
|
|
1086
|
-
logSeverityLevel: 4,
|
|
1087
|
-
enableCpuMemArena: false,
|
|
1088
|
-
enableMemPattern: false,
|
|
1089
|
-
executionMode: 'sequential',
|
|
1090
|
-
interOpNumThreads: 1,
|
|
1091
|
-
intraOpNumThreads: 1,
|
|
1092
|
-
};
|
|
1093
|
-
this.session = await window.ort.InferenceSession.create(modelPath, fallbackOptions);
|
|
1094
|
-
}
|
|
1095
|
-
else {
|
|
1096
|
-
throw error;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
// Load MobileNet model if classification is enabled and not already loaded
|
|
1100
|
-
if (this.useDocumentClassification && !this.mobileNetSession) {
|
|
1101
|
-
if (this.debug) {
|
|
1102
|
-
this.statusMessage = "Cargando modelo de clasificación...";
|
|
1103
|
-
}
|
|
1104
|
-
await this.loadMobileNetModel();
|
|
1105
|
-
}
|
|
1106
|
-
this.isModelPreloaded = true;
|
|
1107
|
-
if (this.debug) {
|
|
1108
|
-
this.statusMessage = "Modelos cargados exitosamente";
|
|
1109
|
-
}
|
|
1110
|
-
this.emitReadyEvent();
|
|
1111
|
-
}
|
|
1112
|
-
else {
|
|
1113
|
-
this.logger.state('USANDO_MODELOS_PRECARGADOS', { sessionExists: !!this.session, modelPreloaded: this.isModelPreloaded });
|
|
379
|
+
// Paso 1: Verificar y cargar modelos si es necesario
|
|
380
|
+
if (!this.detectionService.isModelLoaded()) {
|
|
381
|
+
const loadStartTime = performance.now();
|
|
382
|
+
this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
|
|
383
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
384
|
+
await this.detectionService.loadModel();
|
|
385
|
+
this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
|
|
386
|
+
await this.detectionService.loadClassificationModel();
|
|
387
|
+
const loadEndTime = performance.now();
|
|
388
|
+
const loadTime = loadEndTime - loadStartTime;
|
|
389
|
+
// Record ONNX load time
|
|
1114
390
|
if (this.debug) {
|
|
1115
|
-
this.
|
|
1116
|
-
this.statusColor = "#007bff";
|
|
391
|
+
this.recordOnnxPerformance(loadTime, 0);
|
|
1117
392
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
await this.
|
|
393
|
+
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
394
|
+
}
|
|
395
|
+
// Paso 2: Detectar y configurar dispositivos
|
|
396
|
+
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
|
|
397
|
+
await this.cameraService.enumerateDevices();
|
|
398
|
+
// Paso 3: Configurar cámara seleccionada
|
|
399
|
+
this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
|
|
400
|
+
const stream = await this.cameraService.setupCamera();
|
|
401
|
+
// Paso 4: Inicializar video y ocultar estado inmediatamente
|
|
402
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
403
|
+
await this.initializeVideoStream(stream);
|
|
404
|
+
// Paso 5: Calibrar máscara
|
|
405
|
+
if (this.detectionContainer) {
|
|
406
|
+
const container = this.detectionContainer.parentElement;
|
|
407
|
+
const rect = container.getBoundingClientRect();
|
|
408
|
+
this.updateMaskDimensions(rect);
|
|
409
|
+
}
|
|
410
|
+
// Finalizar e iniciar captura
|
|
1123
411
|
this.startTime = Date.now();
|
|
1124
|
-
this.isLoading
|
|
1125
|
-
if (this.debug) {
|
|
1126
|
-
this.statusMessage = "Detección activa - Busque su identificación";
|
|
1127
|
-
this.statusColor = "#28a745";
|
|
1128
|
-
}
|
|
412
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1129
413
|
this.detectFrame();
|
|
1130
414
|
}
|
|
1131
415
|
catch (err) {
|
|
1132
416
|
this.logger.error('Error al inicializar detección:', err);
|
|
1133
|
-
this.
|
|
1134
|
-
this.
|
|
1135
|
-
|
|
417
|
+
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
418
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async initializeVideoStream(stream) {
|
|
422
|
+
if (this.videoRef) {
|
|
423
|
+
this.videoRef.srcObject = stream;
|
|
424
|
+
this.videoStream = stream;
|
|
425
|
+
const isRear = this.cameraService.isRearCamera(stream);
|
|
426
|
+
this.shouldMirrorVideo = !isRear;
|
|
427
|
+
this.logger.state('CAMARA_CONFIGURADA', {
|
|
428
|
+
isRearCamera: isRear,
|
|
429
|
+
shouldMirrorVideo: this.shouldMirrorVideo
|
|
430
|
+
});
|
|
431
|
+
return new Promise((resolve) => {
|
|
432
|
+
this.videoRef.onloadedmetadata = async () => {
|
|
433
|
+
await this.videoRef.play();
|
|
434
|
+
this.stateManager.updateCaptureState({ isVideoActive: true });
|
|
435
|
+
resolve();
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
throw new Error('Video element not available');
|
|
1136
441
|
}
|
|
1137
442
|
}
|
|
1138
443
|
async detectFrame() {
|
|
1139
444
|
try {
|
|
1140
|
-
|
|
445
|
+
const frameStartTime = performance.now();
|
|
446
|
+
const captureState = this.stateManager.getCaptureState();
|
|
447
|
+
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
1141
448
|
return;
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
// Solo continuar el bucle sin procesar detección
|
|
1145
|
-
if (this.captureStep !== 'completed') {
|
|
449
|
+
if (captureState.isDetectionPaused) {
|
|
450
|
+
if (captureState.step !== 'completed') {
|
|
1146
451
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1147
452
|
}
|
|
1148
453
|
return;
|
|
1149
454
|
}
|
|
1150
|
-
//
|
|
455
|
+
// Frame skipping for performance
|
|
1151
456
|
this.frameSkipCounter++;
|
|
1152
457
|
if (this.frameSkipCounter <= this.FRAME_SKIP) {
|
|
1153
|
-
if (
|
|
458
|
+
if (captureState.step !== 'completed') {
|
|
1154
459
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1155
460
|
}
|
|
1156
461
|
return;
|
|
1157
462
|
}
|
|
1158
463
|
this.frameSkipCounter = 0;
|
|
1159
|
-
//
|
|
464
|
+
// Throttle inference
|
|
1160
465
|
const currentTime = Date.now();
|
|
1161
466
|
if (currentTime - this.lastInferenceTime < this.MIN_INFERENCE_INTERVAL) {
|
|
1162
|
-
if (
|
|
467
|
+
if (captureState.step !== 'completed') {
|
|
1163
468
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1164
469
|
}
|
|
1165
470
|
return;
|
|
1166
471
|
}
|
|
1167
472
|
this.lastInferenceTime = currentTime;
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1171
|
-
const
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
473
|
+
// Measure preprocessing and inference time
|
|
474
|
+
const inferenceStartTime = performance.now();
|
|
475
|
+
const inputTensor = this.detectionService.preprocess(this.videoRef);
|
|
476
|
+
const detections = await this.detectionService.runInference(inputTensor);
|
|
477
|
+
const inferenceTime = performance.now() - inferenceStartTime;
|
|
478
|
+
// Record ONNX performance metrics
|
|
479
|
+
if (this.debug) {
|
|
480
|
+
this.recordOnnxPerformance(0, inferenceTime);
|
|
481
|
+
}
|
|
482
|
+
// Update best score logic
|
|
483
|
+
if (this.startTime && Date.now() - this.startTime < 5000) {
|
|
484
|
+
detections.forEach(detection => {
|
|
485
|
+
const isWellPositioned = this.detectionService.isCardInFrame(detection);
|
|
486
|
+
const effectiveScore = isWellPositioned ? detection.score * 1.2 : detection.score;
|
|
487
|
+
if (effectiveScore > captureState.bestScore) {
|
|
488
|
+
this.stateManager.updateCaptureState({ bestScore: effectiveScore });
|
|
1185
489
|
}
|
|
1186
|
-
}
|
|
490
|
+
});
|
|
1187
491
|
}
|
|
1188
|
-
//
|
|
1189
|
-
if (
|
|
492
|
+
// Adaptive frame rate
|
|
493
|
+
if (detections.length === 0) {
|
|
1190
494
|
this.consecutiveFailures++;
|
|
1191
495
|
}
|
|
1192
496
|
else {
|
|
1193
497
|
this.consecutiveFailures = 0;
|
|
1194
498
|
}
|
|
1195
|
-
|
|
499
|
+
// Update detection boxes for debug
|
|
500
|
+
if (this.debug) {
|
|
501
|
+
this.updateDetectionBoxes(detections);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
this.detectionBoxes = [];
|
|
505
|
+
}
|
|
506
|
+
this.updateMaskColor(detections);
|
|
507
|
+
// Record frame processing metrics
|
|
1196
508
|
if (this.debug) {
|
|
1197
|
-
|
|
509
|
+
const frameEndTime = performance.now();
|
|
510
|
+
const totalFrameTime = frameEndTime - frameStartTime;
|
|
511
|
+
this.recordFrameProcessing(totalFrameTime, detections.length);
|
|
1198
512
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
// Solo continuar si no hemos completado el proceso
|
|
1202
|
-
if (this.captureStep !== 'completed') {
|
|
1203
|
-
// OPTIMIZATION 4: Reduce frame rate when no detection for better battery life
|
|
513
|
+
// Continue detection loop
|
|
514
|
+
if (captureState.step !== 'completed') {
|
|
1204
515
|
if (this.consecutiveFailures > this.MAX_FAILURES) {
|
|
1205
|
-
// Reduce to ~5fps when no detection for 0.5 seconds
|
|
1206
516
|
setTimeout(() => this.detectFrame(), 200);
|
|
1207
517
|
}
|
|
1208
518
|
else {
|
|
@@ -1212,101 +522,99 @@ export class JaakStamps {
|
|
|
1212
522
|
}
|
|
1213
523
|
catch (e) {
|
|
1214
524
|
this.logger.error('Error en inferencia de modelo:', e);
|
|
1215
|
-
|
|
1216
|
-
if (
|
|
1217
|
-
// On error, wait longer before retrying
|
|
525
|
+
const captureState = this.stateManager.getCaptureState();
|
|
526
|
+
if (captureState.step !== 'completed') {
|
|
1218
527
|
setTimeout(() => this.detectFrame(), 100);
|
|
1219
528
|
}
|
|
1220
529
|
}
|
|
1221
530
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const frameCenterY = this.INPUT_SIZE / 2;
|
|
1227
|
-
const toleranceX = 40;
|
|
1228
|
-
const toleranceY = 30;
|
|
1229
|
-
const isCentered = Math.abs(cardCenterX - frameCenterX) < toleranceX &&
|
|
1230
|
-
Math.abs(cardCenterY - frameCenterY) < toleranceY;
|
|
1231
|
-
const isGoodSize = box.w > 150 && box.w < 300 && box.h > 90 && box.h < 200;
|
|
1232
|
-
return isCentered && isGoodSize;
|
|
531
|
+
// ... (continuing with remaining methods)
|
|
532
|
+
// Due to length constraints, I'll provide the key architectural changes
|
|
533
|
+
disconnectedCallback() {
|
|
534
|
+
this.cleanup();
|
|
1233
535
|
}
|
|
1234
|
-
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1237
|
-
|
|
536
|
+
cleanup() {
|
|
537
|
+
if (this.animationId) {
|
|
538
|
+
cancelAnimationFrame(this.animationId);
|
|
539
|
+
}
|
|
540
|
+
if (this.videoStream) {
|
|
541
|
+
this.videoStream.getTracks().forEach(track => track.stop());
|
|
542
|
+
}
|
|
543
|
+
if (this.alignmentTimer) {
|
|
544
|
+
clearTimeout(this.alignmentTimer);
|
|
545
|
+
this.alignmentTimer = undefined;
|
|
546
|
+
}
|
|
547
|
+
if (this.performanceUpdateInterval) {
|
|
548
|
+
clearInterval(this.performanceUpdateInterval);
|
|
549
|
+
this.performanceUpdateInterval = undefined;
|
|
550
|
+
}
|
|
551
|
+
this.detectionBoxes = [];
|
|
552
|
+
this.alignmentStartTime = undefined;
|
|
553
|
+
this.serviceContainer?.cleanup();
|
|
554
|
+
}
|
|
555
|
+
render() {
|
|
556
|
+
const captureState = this.stateManager?.getCaptureState() || {
|
|
557
|
+
isVideoActive: false,
|
|
558
|
+
isLoading: false,
|
|
559
|
+
showFlipAnimation: false,
|
|
560
|
+
showSuccessAnimation: false,
|
|
561
|
+
step: 'front',
|
|
562
|
+
isCapturing: false
|
|
563
|
+
};
|
|
564
|
+
const cameraInfo = this.cameraService?.getCameraInfo() || {
|
|
565
|
+
availableCameras: [],
|
|
566
|
+
isMultipleCamerasAvailable: false,
|
|
567
|
+
selectedCameraId: null,
|
|
568
|
+
deviceType: 'desktop',
|
|
569
|
+
preferredFacing: null
|
|
570
|
+
};
|
|
571
|
+
return (h("div", { key: '6d83055c7dcfc6c6f77e99d8439866aaca31f323', class: "detector-container" }, h("div", { key: '89d95da0fbee2bf03a455da911103d13f2d55a64', class: "video-container" }, h("video", { key: 'a06b3df2a0c2de21c4c3113ca5357a0518168214', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: 'a11ae04dd825208d069a02368708811a718a5b68', 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: {
|
|
572
|
+
position: 'absolute',
|
|
573
|
+
left: `${box.x}px`,
|
|
574
|
+
top: `${box.y}px`,
|
|
575
|
+
width: `${box.w}px`,
|
|
576
|
+
height: `${box.h}px`,
|
|
577
|
+
border: '2px solid #32406C',
|
|
578
|
+
pointerEvents: 'none',
|
|
579
|
+
boxSizing: 'border-box'
|
|
580
|
+
} })))), this.isMaskReady && (h("div", { key: '58c0a0bdd8088835c677c9b455e01ffd8a28bcc0', class: "overlay-mask" }, h("div", { key: '52f87e8837f82a4ab6fd75bd22f8c178ff5670c4', class: "card-outline" }, h("div", { key: '0f503ec7ab610342e01d2b976fff0a5ff1b01845', class: "side side-top" }), h("div", { key: '305c980c5d638270c348ce2fc7bf762515557ea1', class: "side side-right" }), h("div", { key: '201531d41aa8fab1782bc3045bda313bfac14929', class: "side side-bottom" }), h("div", { key: 'acaa082133217e2b4aa807571d6f7f8e5f225530', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '17246f4264074dfae7fdef963120a2d3756b0f64', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("button", { key: '9f21b022b3601f2b8f9898f72683e33b86f433a6', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")), captureState.isVideoActive && (h("div", { key: '3c9d8e3b2135dd14414463a6ada636b425385583', class: "camera-controls" }, h("button", { key: 'b287e1dab5a98f2eb6e05e924b1af9bb6616f947', 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: 'd8dca3049cbdf827d03cd432ac183a1651f77ab6', class: "camera-selector-dropdown" }, h("div", { key: '3a4200b5624f5d96de4ae8acb821a47d31a34c93', class: "camera-selector-header" }, h("span", { key: '14f8e8618d9404cdfd3f9cc150e54b510507c977' }, "Seleccionar C\u00E1mara"), h("button", { key: '98367e182d227093742e0fc9118f81db32352bdd', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '172471ef3c0d55cb619a061eafe37205c3a0bee4', 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: 'e08ced0c02b5662be31c60b2ba35c83fac46bb80', class: "device-info" }, h("small", { key: '8489f10c13a0a528abc878cc66d71dc769890b90' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '89f6c5a9bc1efb9bd8af190ae0821dabe992a2be', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'ead4d0cf0751097220572a4946bf4dcb3d7f4f33', class: "flip-animation" }, h("div", { key: '357e981fd9b2f616e9b1b90c8454555dceac6fbd', class: "id-card-icon" }), h("div", { key: '4abfecbb5ee0165b720d6ad8398984b25ecec7a1', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: 'cdeedd36903c8467e7ebb406e3414ecd92abf47c', class: "success-animation" }, h("div", { key: '8731f318d490ff67fa7f68edf264acb762632604', class: "check-icon" }), h("div", { key: '2e21ce0921fedfaa0b90c56ec44d289426ea7708', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '6cf678c79f8e57fde12202a932fe855905ae770f', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'f9e888616736c26624bd3de7c2f1eee297d0a2f5', class: "status-spinner" })), h("div", { key: 'b849c5187ed8e66b4cf62113d2f93c006cf9f836', class: "status-content" }, h("div", { key: '64a2dac4cb5e4c17fc2aa161c8faa389c64a0458', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: '64e85a98129f791d6784a98129f2411607360fff', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '1d41d3139d7d486bdf9225f584d6de250faeddf7', class: "performance-monitor" }, h("div", { key: '75bf3d45d869c4b0365f994444292de2245d391c', class: "performance-expanded" }, h("div", { key: '762c977e7125202103aba84c568c0fefee43d6b2', class: "metrics-row" }, h("div", { key: '51b03d882f84e835664af11d409fc01a5363c0ce', class: "metric-compact" }, h("span", { key: '21767c803c2fda9fe8653881cb8dfdcaa992fe3b', class: "metric-label" }, "FPS"), h("span", { key: '5467ba22adb75668a0acadd4ffcbaa25117fd8ae', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'a6c020d500a72f330a7cbb01cfdddc1fff63f1bb', class: "metric-compact" }, h("span", { key: '5034e33c12ad454368e4fa32567c307bce6ffe0e', class: "metric-label" }, "MEM"), h("span", { key: '19f691984f9e6aac29067fc983561b701b9080de', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'c2a11b8dc156b90a4ba18020e5a62ff87c29f624', class: "metrics-row" }, h("div", { key: '7af9c2c1286bb32d6c166d3487c8ea4a02f14490', class: "metric-compact" }, h("span", { key: 'b34e54ac6b4e387ee8155176f999dd2b721a8617', class: "metric-label" }, "INF"), h("span", { key: 'ada2f913ac6c97661a054f9bf596f4ac52eb918e', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'cc2d819a7e75af4e03d2521756c23110785076ad', class: "metric-compact" }, h("span", { key: 'b2f7e528be88eafcc66d801e8f547b4d274e0cf8', class: "metric-label" }, "FRAME"), h("span", { key: '1bcddf221b960018fb5505553e9f8cfd3717c349', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: 'cb80b6198f621fab68b13c4992578ef2e548ae99', class: "metrics-row" }, h("div", { key: 'b752b65906c92eff86cad2d0ffdc1420f03a8932', class: "metric-compact" }, h("span", { key: '96af746b42a101c6bfe4c197b652837ecc66fc06', class: "metric-label" }, "DET"), h("span", { key: 'f2038ae4943ebd9ae2ec1d8b907ae23f04c8fd90', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'c2e6b9380928b30b1a879a6abc43eb7053da9ac1', class: "metric-compact" }, h("span", { key: '096268d19a7ac7471c5dafd5adc3103d30433a55', class: "metric-label" }, "RATE"), h("span", { key: '7f9e9e49c68da719fe43391518514af3cdab004d', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: 'c6546f8ec722c56fa369da6d5d732f29d3afc65f', class: "watermark" }, h("img", { key: 'd195a395051a4c436dec33d7835c565e748cf60a', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
|
|
581
|
+
}
|
|
582
|
+
// Utility methods
|
|
583
|
+
updateDetectionBoxes(boxes) {
|
|
584
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
585
|
+
return;
|
|
1238
586
|
const videoWidth = this.videoRef.videoWidth;
|
|
1239
587
|
const videoHeight = this.videoRef.videoHeight;
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
588
|
+
const container = this.detectionContainer.parentElement;
|
|
589
|
+
const containerRect = container.getBoundingClientRect();
|
|
590
|
+
const containerWidth = containerRect.width;
|
|
591
|
+
const containerHeight = containerRect.height;
|
|
592
|
+
if (videoWidth === 0 || videoHeight === 0)
|
|
593
|
+
return;
|
|
1244
594
|
const videoAspectRatio = videoWidth / videoHeight;
|
|
1245
|
-
|
|
1246
|
-
// the mask should be in this distorted space to match the visual mask
|
|
1247
|
-
// In the visual display, we calculate mask size based on the limiting dimension
|
|
1248
|
-
// We need to replicate this logic but account for the distortion in model space
|
|
1249
|
-
// Calculate the "display" dimensions (what the visual mask calculations use)
|
|
1250
|
-
const containerAspectRatio = 1; // Assume square container for simplicity
|
|
595
|
+
const containerAspectRatio = containerWidth / containerHeight;
|
|
1251
596
|
let displayedVideoWidth, displayedVideoHeight;
|
|
597
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
1252
598
|
if (videoAspectRatio > containerAspectRatio) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
}
|
|
1257
|
-
else {
|
|
1258
|
-
// Video is taller - pillarboxed in display
|
|
1259
|
-
displayedVideoHeight = this.INPUT_SIZE;
|
|
1260
|
-
displayedVideoWidth = this.INPUT_SIZE * videoAspectRatio;
|
|
1261
|
-
}
|
|
1262
|
-
// Calculate mask dimensions using the same logic as updateMaskDimensions
|
|
1263
|
-
const maxWidthByHeight = displayedVideoHeight * this.ID1_ASPECT_RATIO;
|
|
1264
|
-
let visualMaskWidth, visualMaskHeight;
|
|
1265
|
-
const sizeRatio = this.maskSize / 100;
|
|
1266
|
-
if (maxWidthByHeight <= displayedVideoWidth) {
|
|
1267
|
-
// Video height is the limiting factor
|
|
1268
|
-
visualMaskHeight = displayedVideoHeight * sizeRatio;
|
|
1269
|
-
visualMaskWidth = visualMaskHeight * this.ID1_ASPECT_RATIO;
|
|
599
|
+
displayedVideoWidth = containerWidth;
|
|
600
|
+
displayedVideoHeight = containerWidth / videoAspectRatio;
|
|
601
|
+
videoOffsetY = (containerHeight - displayedVideoHeight) / 2;
|
|
1270
602
|
}
|
|
1271
603
|
else {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
// Now map these visual mask dimensions to the distorted model space
|
|
1277
|
-
// The model stretches video to 320x320, so we need to apply the inverse transformation
|
|
1278
|
-
const modelMaskWidth = visualMaskWidth * (this.INPUT_SIZE / displayedVideoWidth);
|
|
1279
|
-
const modelMaskHeight = visualMaskHeight * (this.INPUT_SIZE / displayedVideoHeight);
|
|
1280
|
-
// Calculate mask boundaries in model coordinate system (always centered in 320x320)
|
|
1281
|
-
const maskCenterX = this.INPUT_SIZE / 2;
|
|
1282
|
-
const maskCenterY = this.INPUT_SIZE / 2;
|
|
1283
|
-
const maskLeft = maskCenterX - (modelMaskWidth / 2);
|
|
1284
|
-
const maskRight = maskCenterX + (modelMaskWidth / 2);
|
|
1285
|
-
const maskTop = maskCenterY - (modelMaskHeight / 2);
|
|
1286
|
-
const maskBottom = maskCenterY + (modelMaskHeight / 2);
|
|
1287
|
-
// Obtener las coordenadas del documento detectado
|
|
1288
|
-
let docLeft = box.x;
|
|
1289
|
-
let docRight = box.x + box.w;
|
|
1290
|
-
const docTop = box.y;
|
|
1291
|
-
const docBottom = box.y + box.h;
|
|
1292
|
-
// Si el video está en modo mirror, invertir las coordenadas horizontales
|
|
1293
|
-
if (this.shouldMirrorVideo) {
|
|
1294
|
-
const originalDocLeft = docLeft;
|
|
1295
|
-
const originalDocRight = docRight;
|
|
1296
|
-
docLeft = this.INPUT_SIZE - originalDocRight;
|
|
1297
|
-
docRight = this.INPUT_SIZE - originalDocLeft;
|
|
604
|
+
displayedVideoHeight = containerHeight;
|
|
605
|
+
displayedVideoWidth = containerHeight * videoAspectRatio;
|
|
606
|
+
videoOffsetX = (containerWidth - displayedVideoWidth) / 2;
|
|
1298
607
|
}
|
|
1299
|
-
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
return alignment.top && alignment.right && alignment.bottom && alignment.left;
|
|
608
|
+
const INPUT_SIZE = 320;
|
|
609
|
+
const scaleX = displayedVideoWidth / INPUT_SIZE;
|
|
610
|
+
const scaleY = displayedVideoHeight / INPUT_SIZE;
|
|
611
|
+
this.detectionBoxes = boxes.map(det => ({
|
|
612
|
+
x: det.x * scaleX + videoOffsetX,
|
|
613
|
+
y: det.y * scaleY + videoOffsetY,
|
|
614
|
+
w: det.w * scaleX,
|
|
615
|
+
h: det.h * scaleY,
|
|
616
|
+
score: det.score
|
|
617
|
+
}));
|
|
1310
618
|
}
|
|
1311
619
|
updateMaskColor(boxes) {
|
|
1312
620
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -1319,346 +627,307 @@ export class JaakStamps {
|
|
|
1319
627
|
let currentAlignment = { top: false, right: false, bottom: false, left: false };
|
|
1320
628
|
if (boxes.length > 0) {
|
|
1321
629
|
bestBox = boxes.reduce((best, current) => current.score > best.score ? current : best);
|
|
1322
|
-
|
|
630
|
+
const maskConfig = {
|
|
631
|
+
INPUT_SIZE: 320,
|
|
632
|
+
ID1_ASPECT_RATIO: 85.60 / 53.98,
|
|
633
|
+
shouldMirrorVideo: this.shouldMirrorVideo,
|
|
634
|
+
alignmentTolerance: this.alignmentTolerance,
|
|
635
|
+
maskSize: this.maskSize,
|
|
636
|
+
videoRef: this.videoRef
|
|
637
|
+
};
|
|
638
|
+
currentAlignment = this.detectionService.checkSideAlignment(bestBox, maskConfig);
|
|
1323
639
|
this.sideAlignment = currentAlignment;
|
|
1324
640
|
}
|
|
1325
641
|
else {
|
|
1326
|
-
// Reset alignment when no detection
|
|
1327
642
|
this.sideAlignment = { top: false, right: false, bottom: false, left: false };
|
|
1328
643
|
}
|
|
1329
|
-
// Actualizar colores de cada lado individualmente
|
|
1330
644
|
topSide?.classList.toggle('aligned', currentAlignment.top);
|
|
1331
645
|
rightSide?.classList.toggle('aligned', currentAlignment.right);
|
|
1332
646
|
bottomSide?.classList.toggle('aligned', currentAlignment.bottom);
|
|
1333
647
|
leftSide?.classList.toggle('aligned', currentAlignment.left);
|
|
1334
|
-
|
|
1335
|
-
const allSidesAligned = this.areAllSidesAligned(currentAlignment);
|
|
648
|
+
const allSidesAligned = this.detectionService.areAllSidesAligned(currentAlignment);
|
|
1336
649
|
if (allSidesAligned && bestBox) {
|
|
1337
650
|
cardOutline?.classList.add('perfect-match');
|
|
1338
651
|
corners?.forEach(corner => corner.classList.add('perfect-match'));
|
|
1339
652
|
if (!this.hasScreenshotTaken) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
653
|
+
const currentTime = Date.now();
|
|
654
|
+
// Initialize alignment start time if not set
|
|
655
|
+
if (!this.alignmentStartTime) {
|
|
656
|
+
this.alignmentStartTime = currentTime;
|
|
657
|
+
}
|
|
658
|
+
// Check if document has been aligned for 1 second
|
|
659
|
+
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
660
|
+
if (alignmentDuration >= 1000) {
|
|
661
|
+
this.lastDetectedBox = bestBox;
|
|
662
|
+
this.takeScreenshot().catch(error => {
|
|
663
|
+
this.logger.error('Error al tomar captura de pantalla:', error);
|
|
664
|
+
});
|
|
665
|
+
this.hasScreenshotTaken = true;
|
|
666
|
+
this.alignmentStartTime = undefined;
|
|
667
|
+
setTimeout(() => {
|
|
668
|
+
this.hasScreenshotTaken = false;
|
|
669
|
+
}, 2000);
|
|
670
|
+
}
|
|
1349
671
|
}
|
|
1350
672
|
}
|
|
1351
673
|
else {
|
|
1352
674
|
cardOutline?.classList.remove('perfect-match');
|
|
1353
675
|
corners?.forEach(corner => corner.classList.remove('perfect-match'));
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
if (boxes.length === 0) {
|
|
1358
|
-
this.statusMessage = "Posicione la identificación dentro del marco";
|
|
1359
|
-
this.statusColor = "#ff6b6b";
|
|
1360
|
-
this.logger.debug('Sin detección de documento en el frame');
|
|
1361
|
-
}
|
|
1362
|
-
else {
|
|
1363
|
-
const bestBox = boxes.reduce((best, current) => current.score > best.score ? current : best);
|
|
1364
|
-
const alignment = this.checkSideAlignment(bestBox);
|
|
1365
|
-
const alignedSides = [alignment.top, alignment.right, alignment.bottom, alignment.left].filter(Boolean).length;
|
|
1366
|
-
const allSidesAligned = this.areAllSidesAligned(alignment);
|
|
1367
|
-
if (allSidesAligned) {
|
|
1368
|
-
this.statusMessage = "Identificación perfectamente alineada. Mantenga inmóvil";
|
|
1369
|
-
this.statusColor = "#00ff00";
|
|
1370
|
-
this.logger.state('DOCUMENTO_PERFECTAMENTE_ALINEADO', {
|
|
1371
|
-
score: bestBox.score,
|
|
1372
|
-
boxDimensions: { width: bestBox.w, height: bestBox.h },
|
|
1373
|
-
alignedSides: 4
|
|
1374
|
-
});
|
|
676
|
+
// Reset alignment timer when document moves out of position
|
|
677
|
+
if (this.alignmentStartTime) {
|
|
678
|
+
this.alignmentStartTime = undefined;
|
|
1375
679
|
}
|
|
1376
|
-
|
|
1377
|
-
this.
|
|
1378
|
-
this.
|
|
680
|
+
if (this.alignmentTimer) {
|
|
681
|
+
clearTimeout(this.alignmentTimer);
|
|
682
|
+
this.alignmentTimer = undefined;
|
|
1379
683
|
}
|
|
1380
|
-
else if (bestBox.w < 150) {
|
|
1381
|
-
this.statusMessage = "Identificación detectada. Acerque al marco";
|
|
1382
|
-
this.statusColor = "#ffb366";
|
|
1383
|
-
}
|
|
1384
|
-
else {
|
|
1385
|
-
this.statusMessage = "Identificación detectada. Alinee con el marco";
|
|
1386
|
-
this.statusColor = "#ffb366";
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
drawDetections(ctx, boxes) {
|
|
1391
|
-
if (!this.videoRef)
|
|
1392
|
-
return;
|
|
1393
|
-
// Get video and container dimensions
|
|
1394
|
-
const videoWidth = this.videoRef.videoWidth;
|
|
1395
|
-
const videoHeight = this.videoRef.videoHeight;
|
|
1396
|
-
const containerWidth = this.canvasRef.width;
|
|
1397
|
-
const containerHeight = this.canvasRef.height;
|
|
1398
|
-
if (videoWidth === 0 || videoHeight === 0)
|
|
1399
|
-
return;
|
|
1400
|
-
// Calculate video aspect ratio and container aspect ratio
|
|
1401
|
-
const videoAspectRatio = videoWidth / videoHeight;
|
|
1402
|
-
const containerAspectRatio = containerWidth / containerHeight;
|
|
1403
|
-
// Determine how video fits in container (same logic as updateMaskDimensions)
|
|
1404
|
-
let displayedVideoWidth, displayedVideoHeight;
|
|
1405
|
-
let videoOffsetX = 0, videoOffsetY = 0;
|
|
1406
|
-
if (videoAspectRatio > containerAspectRatio) {
|
|
1407
|
-
// Video is wider - letterboxed (black bars top/bottom)
|
|
1408
|
-
displayedVideoWidth = containerWidth;
|
|
1409
|
-
displayedVideoHeight = containerWidth / videoAspectRatio;
|
|
1410
|
-
videoOffsetY = (containerHeight - displayedVideoHeight) / 2;
|
|
1411
684
|
}
|
|
1412
|
-
else {
|
|
1413
|
-
// Video is taller - pillarboxed (black bars left/right)
|
|
1414
|
-
displayedVideoHeight = containerHeight;
|
|
1415
|
-
displayedVideoWidth = containerHeight * videoAspectRatio;
|
|
1416
|
-
videoOffsetX = (containerWidth - displayedVideoWidth) / 2;
|
|
1417
|
-
}
|
|
1418
|
-
// Scale factor from model coordinates (320x320) to displayed video area
|
|
1419
|
-
const scaleX = displayedVideoWidth / this.INPUT_SIZE;
|
|
1420
|
-
const scaleY = displayedVideoHeight / this.INPUT_SIZE;
|
|
1421
|
-
boxes.forEach(det => {
|
|
1422
|
-
// Convert model coordinates to displayed video coordinates
|
|
1423
|
-
const x = det.x * scaleX + videoOffsetX;
|
|
1424
|
-
const y = det.y * scaleY + videoOffsetY;
|
|
1425
|
-
const w = det.w * scaleX;
|
|
1426
|
-
const h = det.h * scaleY;
|
|
1427
|
-
ctx.strokeStyle = "#32406C";
|
|
1428
|
-
ctx.lineWidth = 2;
|
|
1429
|
-
ctx.strokeRect(x, y, w, h);
|
|
1430
|
-
});
|
|
1431
685
|
}
|
|
1432
686
|
async takeScreenshot() {
|
|
1433
687
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
1434
688
|
return;
|
|
1435
689
|
this.logger.state('INICIANDO_CAPTURA', {
|
|
1436
|
-
captureStep: this.
|
|
690
|
+
captureStep: this.stateManager.getCaptureState().step,
|
|
1437
691
|
detectedBox: this.lastDetectedBox,
|
|
1438
692
|
videoResolution: {
|
|
1439
693
|
width: this.videoRef.videoWidth,
|
|
1440
694
|
height: this.videoRef.videoHeight
|
|
1441
695
|
}
|
|
1442
696
|
});
|
|
1443
|
-
|
|
697
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
1444
698
|
this.triggerCaptureAnimation();
|
|
1445
|
-
//
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
this.captureCanvas.width
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
const
|
|
1455
|
-
const scaleY = this.videoRef.videoHeight / this.INPUT_SIZE;
|
|
699
|
+
// Create capture canvas
|
|
700
|
+
const captureCanvas = document.createElement('canvas');
|
|
701
|
+
captureCanvas.width = this.videoRef.videoWidth;
|
|
702
|
+
captureCanvas.height = this.videoRef.videoHeight;
|
|
703
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
704
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
705
|
+
// Calculate crop coordinates
|
|
706
|
+
const INPUT_SIZE = 320;
|
|
707
|
+
const scaleX = this.videoRef.videoWidth / INPUT_SIZE;
|
|
708
|
+
const scaleY = this.videoRef.videoHeight / INPUT_SIZE;
|
|
1456
709
|
const cropX = Math.max(0, (this.lastDetectedBox.x * scaleX) - this.cropMargin);
|
|
1457
710
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
1458
711
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
1459
712
|
const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
|
|
1460
|
-
//
|
|
1461
|
-
// (We reuse main capture canvas for full frame)
|
|
713
|
+
// Create cropped version
|
|
1462
714
|
const croppedCanvas = document.createElement('canvas');
|
|
1463
715
|
croppedCanvas.width = cropWidth;
|
|
1464
716
|
croppedCanvas.height = cropHeight;
|
|
1465
717
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
718
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
719
|
+
const captureState = this.stateManager.getCaptureState();
|
|
720
|
+
if (captureState.step === 'front') {
|
|
721
|
+
this.stateManager.setCapturedImages({
|
|
722
|
+
front: {
|
|
723
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
724
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
725
|
+
}
|
|
726
|
+
});
|
|
1474
727
|
// Check if document classification is enabled
|
|
1475
728
|
if (this.useDocumentClassification) {
|
|
1476
|
-
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
catch (error) {
|
|
1482
|
-
this.logger.warn('Fallo al cargar modelo de clasificación, continuando sin clasificación:', error);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
// Classify the cropped document if model is available
|
|
1486
|
-
if (this.mobileNetSession) {
|
|
1487
|
-
const classification = await this.classifyDocument(croppedCanvas);
|
|
1488
|
-
if (classification && classification.class === 'passport') {
|
|
1489
|
-
// If it's a passport, skip back capture since passports don't have a back side
|
|
1490
|
-
this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
|
|
1491
|
-
this.completeProcess(true);
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
729
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
730
|
+
if (classification && classification.class === 'passport') {
|
|
731
|
+
this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
|
|
732
|
+
this.completeProcess(true);
|
|
733
|
+
return;
|
|
1494
734
|
}
|
|
1495
735
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
// Pausar detección durante la animación de giro
|
|
1501
|
-
this.isDetectionPaused = true;
|
|
1502
|
-
// Mostrar animación de giro después de la captura
|
|
1503
|
-
setTimeout(() => {
|
|
1504
|
-
this.showFlipAnimation = true;
|
|
1505
|
-
setTimeout(() => {
|
|
1506
|
-
this.showFlipAnimation = false;
|
|
1507
|
-
// Reanudar detección después de que termine la animación
|
|
1508
|
-
this.isDetectionPaused = false;
|
|
1509
|
-
}, 3000);
|
|
1510
|
-
}, 800);
|
|
1511
|
-
this.logger.state('CAPTURA_FRENTE_COMPLETADA', {
|
|
1512
|
-
captureStep: this.captureStep,
|
|
1513
|
-
hasFullFrame: !!this.capturedFullFrame,
|
|
1514
|
-
hasCroppedId: !!this.capturedCroppedId
|
|
736
|
+
this.stateManager.updateCaptureState({
|
|
737
|
+
step: 'back',
|
|
738
|
+
isDetectionPaused: true,
|
|
739
|
+
showFlipAnimation: true
|
|
1515
740
|
});
|
|
741
|
+
setTimeout(() => {
|
|
742
|
+
this.stateManager.updateCaptureState({
|
|
743
|
+
showFlipAnimation: false,
|
|
744
|
+
isDetectionPaused: false
|
|
745
|
+
});
|
|
746
|
+
}, 3000);
|
|
1516
747
|
}
|
|
1517
|
-
else if (
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
captureStep: this.captureStep,
|
|
1524
|
-
hasBackFullFrame: !!this.capturedBackFullFrame,
|
|
1525
|
-
hasBackCroppedId: !!this.capturedBackCroppedId,
|
|
1526
|
-
processCompleted: true
|
|
748
|
+
else if (captureState.step === 'back') {
|
|
749
|
+
this.stateManager.setCapturedImages({
|
|
750
|
+
back: {
|
|
751
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
752
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
753
|
+
}
|
|
1527
754
|
});
|
|
755
|
+
this.completeProcess(false);
|
|
1528
756
|
}
|
|
1529
757
|
}
|
|
1530
758
|
triggerCaptureAnimation() {
|
|
1531
|
-
this.isCapturing = true;
|
|
1532
|
-
// Agregar clase de animación al marco
|
|
1533
759
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
1534
760
|
cardOutline?.classList.add('capturing');
|
|
1535
|
-
// Limpiar animación después de que termine
|
|
1536
761
|
setTimeout(() => {
|
|
1537
|
-
this.isCapturing
|
|
762
|
+
this.stateManager.updateCaptureState({ isCapturing: false });
|
|
1538
763
|
cardOutline?.classList.remove('capturing');
|
|
1539
764
|
}, 600);
|
|
1540
765
|
}
|
|
766
|
+
completeProcess(skippedBack = false) {
|
|
767
|
+
this.stateManager.updateCaptureState({
|
|
768
|
+
step: 'completed',
|
|
769
|
+
showSuccessAnimation: true
|
|
770
|
+
});
|
|
771
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
772
|
+
capturedImages.metadata.processCompleted = true;
|
|
773
|
+
capturedImages.metadata.backCaptureSkipped = skippedBack;
|
|
774
|
+
this.stateManager.setCapturedImages(capturedImages);
|
|
775
|
+
this.stopDetection();
|
|
776
|
+
this.updateStatus('Proceso completado', `${capturedImages.metadata.totalImages} imágenes capturadas exitosamente`, 'ready');
|
|
777
|
+
const finalImages = {
|
|
778
|
+
...capturedImages,
|
|
779
|
+
timestamp: new Date().toISOString()
|
|
780
|
+
};
|
|
781
|
+
this.captureCompleted.emit(finalImages);
|
|
782
|
+
setTimeout(() => {
|
|
783
|
+
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
784
|
+
}, 3000);
|
|
785
|
+
this.logger.state('PROCESO_COMPLETADO', {
|
|
786
|
+
skippedBack,
|
|
787
|
+
totalImages: capturedImages.metadata.totalImages,
|
|
788
|
+
timestamp: new Date().toISOString()
|
|
789
|
+
});
|
|
790
|
+
}
|
|
1541
791
|
stopDetection() {
|
|
1542
792
|
if (this.animationId) {
|
|
1543
793
|
cancelAnimationFrame(this.animationId);
|
|
1544
794
|
this.animationId = undefined;
|
|
1545
795
|
}
|
|
1546
|
-
|
|
1547
|
-
if (this.canvasRef) {
|
|
1548
|
-
const ctx = this.canvasRef.getContext("2d");
|
|
1549
|
-
ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
|
|
1550
|
-
}
|
|
796
|
+
this.detectionBoxes = [];
|
|
1551
797
|
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
1552
798
|
}
|
|
799
|
+
toggleCameraSelector() {
|
|
800
|
+
if (this.isSwitchingCamera)
|
|
801
|
+
return; // Don't toggle if switching camera
|
|
802
|
+
this.showCameraSelector = !this.showCameraSelector;
|
|
803
|
+
}
|
|
804
|
+
async handleCameraSwitch(cameraId) {
|
|
805
|
+
if (this.isSwitchingCamera)
|
|
806
|
+
return; // Prevent multiple simultaneous switches
|
|
807
|
+
try {
|
|
808
|
+
// Close the selector immediately when user selects a camera
|
|
809
|
+
this.showCameraSelector = false;
|
|
810
|
+
this.isSwitchingCamera = true;
|
|
811
|
+
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
812
|
+
from: this.cameraService.getSelectedCameraId(),
|
|
813
|
+
to: cameraId
|
|
814
|
+
});
|
|
815
|
+
// Stop current video stream
|
|
816
|
+
if (this.videoStream) {
|
|
817
|
+
this.videoStream.getTracks().forEach(track => track.stop());
|
|
818
|
+
}
|
|
819
|
+
// Switch camera in service
|
|
820
|
+
await this.cameraService.switchCamera(cameraId);
|
|
821
|
+
// Setup new camera stream
|
|
822
|
+
const newStream = await this.cameraService.setupCamera();
|
|
823
|
+
// Update video element
|
|
824
|
+
await this.initializeVideoStream(newStream);
|
|
825
|
+
this.logger.state('CAMBIO_CAMARA_EXITOSO', {
|
|
826
|
+
newCameraId: cameraId,
|
|
827
|
+
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
this.logger.error('Error al cambiar cámara:', error);
|
|
832
|
+
// Try to restore previous camera if switch failed
|
|
833
|
+
try {
|
|
834
|
+
const fallbackStream = await this.cameraService.setupCamera();
|
|
835
|
+
await this.initializeVideoStream(fallbackStream);
|
|
836
|
+
}
|
|
837
|
+
catch (fallbackError) {
|
|
838
|
+
this.logger.error('Error al restaurar cámara anterior:', fallbackError);
|
|
839
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
finally {
|
|
843
|
+
this.isSwitchingCamera = false;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
1553
846
|
resetDetection() {
|
|
1554
|
-
this.
|
|
1555
|
-
this.startTime = Date.now();
|
|
1556
|
-
this.statusMessage = "Sistema reiniciado. Posicione la identificación";
|
|
1557
|
-
this.statusColor = "#aaa";
|
|
847
|
+
this.stateManager.reset();
|
|
1558
848
|
this.hasScreenshotTaken = false;
|
|
1559
|
-
this.
|
|
1560
|
-
this.capturedCroppedId = null;
|
|
1561
|
-
this.capturedBackFullFrame = null;
|
|
1562
|
-
this.capturedBackCroppedId = null;
|
|
1563
|
-
this.captureStep = 'front';
|
|
1564
|
-
this.isCapturing = false;
|
|
1565
|
-
this.showFlipAnimation = false;
|
|
1566
|
-
this.showSuccessAnimation = false;
|
|
1567
|
-
this.isDetectionPaused = false;
|
|
1568
|
-
this.isLoading = false;
|
|
1569
|
-
this.lastDetectedBox = undefined;
|
|
1570
|
-
this.isModelPreloaded = false;
|
|
1571
|
-
// Reset performance counters
|
|
849
|
+
this.startTime = Date.now();
|
|
1572
850
|
this.frameSkipCounter = 0;
|
|
1573
851
|
this.consecutiveFailures = 0;
|
|
1574
852
|
this.lastInferenceTime = 0;
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
853
|
+
this.detectionBoxes = [];
|
|
854
|
+
this.alignmentStartTime = undefined;
|
|
855
|
+
if (this.alignmentTimer) {
|
|
856
|
+
clearTimeout(this.alignmentTimer);
|
|
857
|
+
this.alignmentTimer = undefined;
|
|
858
|
+
}
|
|
859
|
+
const captureState = this.stateManager.getCaptureState();
|
|
860
|
+
if (captureState.isVideoActive && this.detectionService.isModelLoaded()) {
|
|
861
|
+
this.updateStatus('Captura reiniciada', 'Buscando documento en el marco de captura', 'active');
|
|
1581
862
|
this.detectFrame();
|
|
1582
863
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
this.statusMessage = skippedBack ?
|
|
1587
|
-
"Proceso completado (solo frente capturado)" :
|
|
1588
|
-
"Proceso de captura completado exitosamente";
|
|
1589
|
-
this.statusColor = "#28a745";
|
|
1590
|
-
this.logger.state('PROCESO_COMPLETADO', {
|
|
1591
|
-
skippedBack,
|
|
1592
|
-
hasFrontImages: !!(this.capturedFullFrame && this.capturedCroppedId),
|
|
1593
|
-
hasBackImages: !!(this.capturedBackFullFrame && this.capturedBackCroppedId),
|
|
1594
|
-
totalImages: skippedBack ? 2 : 4,
|
|
1595
|
-
timestamp: new Date().toISOString()
|
|
1596
|
-
});
|
|
1597
|
-
// Detener el detector
|
|
1598
|
-
this.stopDetection();
|
|
1599
|
-
// Emitir evento con las imágenes capturadas
|
|
1600
|
-
const capturedImages = {
|
|
1601
|
-
front: {
|
|
1602
|
-
fullFrame: this.capturedFullFrame,
|
|
1603
|
-
cropped: this.capturedCroppedId
|
|
1604
|
-
},
|
|
1605
|
-
back: {
|
|
1606
|
-
fullFrame: this.capturedBackFullFrame,
|
|
1607
|
-
cropped: this.capturedBackCroppedId
|
|
1608
|
-
},
|
|
1609
|
-
timestamp: new Date().toISOString(),
|
|
1610
|
-
metadata: {
|
|
1611
|
-
totalImages: skippedBack ? 2 : 4,
|
|
1612
|
-
processCompleted: true,
|
|
1613
|
-
backCaptureSkipped: skippedBack
|
|
1614
|
-
}
|
|
1615
|
-
};
|
|
1616
|
-
this.captureCompleted.emit(capturedImages);
|
|
1617
|
-
// Mostrar animación de éxito después de la captura
|
|
1618
|
-
setTimeout(() => {
|
|
1619
|
-
this.showSuccessAnimation = true;
|
|
1620
|
-
setTimeout(() => {
|
|
1621
|
-
this.showSuccessAnimation = false;
|
|
1622
|
-
}, 3000);
|
|
1623
|
-
}, 800);
|
|
864
|
+
else {
|
|
865
|
+
this.updateStatus('Listo para capturar', '', 'ready');
|
|
866
|
+
}
|
|
1624
867
|
}
|
|
1625
868
|
exitSession() {
|
|
1626
869
|
if (this.videoStream) {
|
|
1627
870
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
1628
871
|
this.videoStream = undefined;
|
|
1629
|
-
this.isVideoActive
|
|
1630
|
-
if (this.debug) {
|
|
1631
|
-
this.statusMessage = "Cámara desactivada - Listo para nueva sesión";
|
|
1632
|
-
this.statusColor = "#6c757d";
|
|
1633
|
-
}
|
|
1634
|
-
else {
|
|
1635
|
-
this.statusMessage = "Presione el botón para activar la cámara";
|
|
1636
|
-
this.statusColor = "#aaa";
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
this.isLoading = false;
|
|
1640
|
-
// Limpiar canvas al finalizar sesión
|
|
1641
|
-
if (this.canvasRef) {
|
|
1642
|
-
const ctx = this.canvasRef.getContext("2d");
|
|
1643
|
-
ctx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
|
|
872
|
+
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
1644
873
|
}
|
|
874
|
+
this.updateStatus('Sesión finalizada', '', 'ready');
|
|
875
|
+
this.detectionBoxes = [];
|
|
1645
876
|
this.cleanup();
|
|
1646
877
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
878
|
+
// PERFORMANCE MONITORING METHODS
|
|
879
|
+
initializePerformanceMonitor() {
|
|
880
|
+
this.performanceMetrics.lastUpdateTime = performance.now();
|
|
881
|
+
// Update performance metrics every 500ms
|
|
882
|
+
this.performanceUpdateInterval = window.setInterval(() => {
|
|
883
|
+
this.updatePerformanceMetrics();
|
|
884
|
+
}, 500);
|
|
885
|
+
this.logger.debug('Monitor de performance inicializado');
|
|
886
|
+
}
|
|
887
|
+
updatePerformanceMetrics() {
|
|
888
|
+
const currentTime = performance.now();
|
|
889
|
+
const deltaTime = currentTime - this.performanceMetrics.lastUpdateTime;
|
|
890
|
+
// Calculate FPS
|
|
891
|
+
if (deltaTime > 0) {
|
|
892
|
+
this.performanceMetrics.fps = Math.round(1000 / (deltaTime / this.frameSkipCounter || 1));
|
|
893
|
+
}
|
|
894
|
+
// Get memory usage if available
|
|
895
|
+
if ('memory' in performance) {
|
|
896
|
+
const memInfo = performance.memory;
|
|
897
|
+
this.performanceMetrics.memoryUsage = Math.round(memInfo.usedJSHeapSize / 1048576); // MB
|
|
898
|
+
}
|
|
899
|
+
// Calculate detection success rate
|
|
900
|
+
if (this.performanceMetrics.totalDetections > 0) {
|
|
901
|
+
this.performanceMetrics.successfulDetections = this.performanceMetrics.successfulDetections;
|
|
902
|
+
const detectionRate = (this.performanceMetrics.successfulDetections / this.performanceMetrics.totalDetections) * 100;
|
|
903
|
+
this.performanceMetrics.detectionRate = Math.round(detectionRate);
|
|
904
|
+
}
|
|
905
|
+
// Update state for rendering
|
|
906
|
+
this.performanceData = {
|
|
907
|
+
fps: this.performanceMetrics.fps,
|
|
908
|
+
inferenceTime: this.performanceMetrics.inferenceTime,
|
|
909
|
+
memoryUsage: this.performanceMetrics.memoryUsage,
|
|
910
|
+
onnxLoadTime: this.performanceMetrics.onnxLoadTime,
|
|
911
|
+
frameProcessingTime: this.performanceMetrics.frameProcessingTime,
|
|
912
|
+
totalDetections: this.performanceMetrics.totalDetections,
|
|
913
|
+
successfulDetections: this.performanceMetrics.successfulDetections,
|
|
914
|
+
detectionRate: this.performanceMetrics.detectionRate
|
|
915
|
+
};
|
|
916
|
+
this.performanceMetrics.lastUpdateTime = currentTime;
|
|
917
|
+
}
|
|
918
|
+
recordOnnxPerformance(loadTime, inferenceTime) {
|
|
919
|
+
this.performanceMetrics.onnxLoadTime = Math.round(loadTime);
|
|
920
|
+
this.performanceMetrics.inferenceTime = Math.round(inferenceTime);
|
|
921
|
+
}
|
|
922
|
+
recordFrameProcessing(processingTime, detectionsFound) {
|
|
923
|
+
this.performanceMetrics.frameProcessingTime = Math.round(processingTime);
|
|
924
|
+
this.performanceMetrics.totalDetections++;
|
|
925
|
+
if (detectionsFound > 0) {
|
|
926
|
+
this.performanceMetrics.successfulDetections++;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
togglePerformanceMonitor() {
|
|
930
|
+
this.isPerformanceMonitorMinimized = !this.isPerformanceMonitorMinimized;
|
|
1662
931
|
}
|
|
1663
932
|
static get is() { return "jaak-stamps"; }
|
|
1664
933
|
static get encapsulation() { return "shadow"; }
|
|
@@ -1798,28 +1067,15 @@ export class JaakStamps {
|
|
|
1798
1067
|
}
|
|
1799
1068
|
static get states() {
|
|
1800
1069
|
return {
|
|
1801
|
-
"
|
|
1802
|
-
"statusMessage": {},
|
|
1803
|
-
"statusColor": {},
|
|
1804
|
-
"bestScore": {},
|
|
1805
|
-
"capturedFullFrame": {},
|
|
1806
|
-
"capturedCroppedId": {},
|
|
1807
|
-
"capturedBackFullFrame": {},
|
|
1808
|
-
"capturedBackCroppedId": {},
|
|
1809
|
-
"captureStep": {},
|
|
1810
|
-
"isCapturing": {},
|
|
1811
|
-
"showFlipAnimation": {},
|
|
1812
|
-
"showSuccessAnimation": {},
|
|
1813
|
-
"shouldMirrorVideo": {},
|
|
1070
|
+
"detectionBoxes": {},
|
|
1814
1071
|
"sideAlignment": {},
|
|
1815
|
-
"isDetectionPaused": {},
|
|
1816
|
-
"isLoading": {},
|
|
1817
|
-
"isModelPreloaded": {},
|
|
1818
1072
|
"isMaskReady": {},
|
|
1819
|
-
"
|
|
1820
|
-
"selectedCameraId": {},
|
|
1073
|
+
"shouldMirrorVideo": {},
|
|
1821
1074
|
"showCameraSelector": {},
|
|
1822
|
-
"
|
|
1075
|
+
"isSwitchingCamera": {},
|
|
1076
|
+
"currentStatus": {},
|
|
1077
|
+
"performanceData": {},
|
|
1078
|
+
"isPerformanceMonitorMinimized": {}
|
|
1823
1079
|
};
|
|
1824
1080
|
}
|
|
1825
1081
|
static get events() {
|
|
@@ -1859,7 +1115,7 @@ export class JaakStamps {
|
|
|
1859
1115
|
return {
|
|
1860
1116
|
"getCapturedImages": {
|
|
1861
1117
|
"complexType": {
|
|
1862
|
-
"signature": "() => Promise<
|
|
1118
|
+
"signature": "() => Promise<import(\"/home/runner/work/jaak-stamps-webcomponent/jaak-stamps-webcomponent/src/services/interfaces/IStateManager\").CapturedImages>",
|
|
1863
1119
|
"parameters": [],
|
|
1864
1120
|
"references": {
|
|
1865
1121
|
"Promise": {
|
|
@@ -1867,7 +1123,7 @@ export class JaakStamps {
|
|
|
1867
1123
|
"id": "global::Promise"
|
|
1868
1124
|
}
|
|
1869
1125
|
},
|
|
1870
|
-
"return": "Promise<
|
|
1126
|
+
"return": "Promise<CapturedImages>"
|
|
1871
1127
|
},
|
|
1872
1128
|
"docs": {
|
|
1873
1129
|
"text": "",
|
|
@@ -1942,9 +1198,9 @@ export class JaakStamps {
|
|
|
1942
1198
|
"tags": []
|
|
1943
1199
|
}
|
|
1944
1200
|
},
|
|
1945
|
-
"
|
|
1201
|
+
"skipBackCapture": {
|
|
1946
1202
|
"complexType": {
|
|
1947
|
-
"signature": "() => Promise<
|
|
1203
|
+
"signature": "() => Promise<void>",
|
|
1948
1204
|
"parameters": [],
|
|
1949
1205
|
"references": {
|
|
1950
1206
|
"Promise": {
|
|
@@ -1952,16 +1208,16 @@ export class JaakStamps {
|
|
|
1952
1208
|
"id": "global::Promise"
|
|
1953
1209
|
}
|
|
1954
1210
|
},
|
|
1955
|
-
"return": "Promise<
|
|
1211
|
+
"return": "Promise<void>"
|
|
1956
1212
|
},
|
|
1957
1213
|
"docs": {
|
|
1958
1214
|
"text": "",
|
|
1959
1215
|
"tags": []
|
|
1960
1216
|
}
|
|
1961
1217
|
},
|
|
1962
|
-
"
|
|
1218
|
+
"getStatus": {
|
|
1963
1219
|
"complexType": {
|
|
1964
|
-
"signature": "() => Promise<{
|
|
1220
|
+
"signature": "() => Promise<{ isVideoActive: boolean; captureStep: \"front\" | \"back\" | \"completed\"; hasImages: boolean; isProcessCompleted: boolean; isModelPreloaded: boolean; }>",
|
|
1965
1221
|
"parameters": [],
|
|
1966
1222
|
"references": {
|
|
1967
1223
|
"Promise": {
|
|
@@ -1969,16 +1225,16 @@ export class JaakStamps {
|
|
|
1969
1225
|
"id": "global::Promise"
|
|
1970
1226
|
}
|
|
1971
1227
|
},
|
|
1972
|
-
"return": "Promise<{
|
|
1228
|
+
"return": "Promise<{ isVideoActive: boolean; captureStep: \"front\" | \"back\" | \"completed\"; hasImages: boolean; isProcessCompleted: boolean; isModelPreloaded: boolean; }>"
|
|
1973
1229
|
},
|
|
1974
1230
|
"docs": {
|
|
1975
1231
|
"text": "",
|
|
1976
1232
|
"tags": []
|
|
1977
1233
|
}
|
|
1978
1234
|
},
|
|
1979
|
-
"
|
|
1235
|
+
"preloadModel": {
|
|
1980
1236
|
"complexType": {
|
|
1981
|
-
"signature": "() => Promise<
|
|
1237
|
+
"signature": "() => Promise<{ success: boolean; message: string; error?: undefined; } | { success: boolean; error: any; message?: undefined; }>",
|
|
1982
1238
|
"parameters": [],
|
|
1983
1239
|
"references": {
|
|
1984
1240
|
"Promise": {
|
|
@@ -1986,7 +1242,7 @@ export class JaakStamps {
|
|
|
1986
1242
|
"id": "global::Promise"
|
|
1987
1243
|
}
|
|
1988
1244
|
},
|
|
1989
|
-
"return": "Promise<
|
|
1245
|
+
"return": "Promise<{ success: boolean; message: string; error?: undefined; } | { success: boolean; error: any; message?: undefined; }>"
|
|
1990
1246
|
},
|
|
1991
1247
|
"docs": {
|
|
1992
1248
|
"text": "",
|
|
@@ -1995,7 +1251,7 @@ export class JaakStamps {
|
|
|
1995
1251
|
},
|
|
1996
1252
|
"getCameraInfo": {
|
|
1997
1253
|
"complexType": {
|
|
1998
|
-
"signature": "() => Promise<{ availableCameras:
|
|
1254
|
+
"signature": "() => Promise<{ availableCameras: import(\"/home/runner/work/jaak-stamps-webcomponent/jaak-stamps-webcomponent/src/services/interfaces/ICameraService\").CameraInfo[]; selectedCameraId: string | null; deviceType: string; isMultipleCamerasAvailable: boolean; preferredFacing: \"environment\" | \"user\" | null; }>",
|
|
1999
1255
|
"parameters": [],
|
|
2000
1256
|
"references": {
|
|
2001
1257
|
"Promise": {
|
|
@@ -2003,7 +1259,7 @@ export class JaakStamps {
|
|
|
2003
1259
|
"id": "global::Promise"
|
|
2004
1260
|
}
|
|
2005
1261
|
},
|
|
2006
|
-
"return": "Promise<{ availableCameras:
|
|
1262
|
+
"return": "Promise<{ availableCameras: CameraInfo[]; selectedCameraId: string; deviceType: string; isMultipleCamerasAvailable: boolean; preferredFacing: \"environment\" | \"user\"; }>"
|
|
2007
1263
|
},
|
|
2008
1264
|
"docs": {
|
|
2009
1265
|
"text": "",
|