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