@jaak.ai/stamps 2.1.0-dev.2 → 2.1.0-dev.20

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 (53) hide show
  1. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  3. package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
  4. package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
  5. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
  6. package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
  7. package/dist/cjs/jaak-stamps.cjs.entry.js +542 -68
  8. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  9. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  10. package/dist/cjs/loader.cjs.js +4 -3
  11. package/dist/cjs/loader.cjs.js.map +1 -1
  12. package/dist/collection/collection-manifest.json +1 -1
  13. package/dist/collection/components/my-component/my-component.css +56 -12
  14. package/dist/collection/components/my-component/my-component.js +429 -67
  15. package/dist/collection/components/my-component/my-component.js.map +1 -1
  16. package/dist/collection/services/CameraService.js +84 -2
  17. package/dist/collection/services/CameraService.js.map +1 -1
  18. package/dist/collection/services/DetectionService.js +32 -4
  19. package/dist/collection/services/DetectionService.js.map +1 -1
  20. package/dist/components/index.js +165 -113
  21. package/dist/components/index.js.map +1 -1
  22. package/dist/components/jaak-stamps.js +546 -68
  23. package/dist/components/jaak-stamps.js.map +1 -1
  24. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  25. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  26. package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
  27. package/dist/esm/index-Drbzcuq-.js.map +1 -0
  28. package/dist/esm/jaak-stamps-webcomponent.js +5 -4
  29. package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
  30. package/dist/esm/jaak-stamps.entry.js +542 -68
  31. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  32. package/dist/esm/loader.js +4 -3
  33. package/dist/esm/loader.js.map +1 -1
  34. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  35. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
  36. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  37. package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
  38. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
  39. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
  40. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
  41. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
  42. package/dist/jaak-stamps-webcomponent/p-c06e5d7b.entry.js +2 -0
  43. package/dist/jaak-stamps-webcomponent/p-c06e5d7b.entry.js.map +1 -0
  44. package/dist/types/components/my-component/my-component.d.ts +19 -0
  45. package/dist/types/services/CameraService.d.ts +1 -0
  46. package/dist/types/stencil-public-runtime.d.ts +12 -5
  47. package/package.json +3 -1
  48. package/dist/cjs/index-BfhtOB0D.js.map +0 -1
  49. package/dist/esm/index-BP1Q4KOg.js.map +0 -1
  50. package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js +0 -2
  51. package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js.map +0 -1
  52. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
  53. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { r as registerInstance, c as createEvent, a as getElement, h } from './index-BP1Q4KOg.js';
1
+ import { r as registerInstance, c as createEvent, g as getElement, h } from './index-Drbzcuq-.js';
2
2
 
3
3
  class EventBusService {
4
4
  events = new Map();
@@ -204,7 +204,21 @@ class CameraService {
204
204
  tempStream.getTracks().forEach(track => track.stop());
205
205
  }
206
206
  const devices = await navigator.mediaDevices.enumerateDevices();
207
- this.availableCameras = devices.filter(device => device.kind === 'videoinput');
207
+ const allCameras = devices.filter(device => device.kind === 'videoinput');
208
+ // Filter out virtual cameras
209
+ this.availableCameras = allCameras.filter(camera => {
210
+ const isVirtual = this.isVirtualCamera(camera);
211
+ if (isVirtual) {
212
+ console.log(`Virtual camera detected and filtered: ${camera.label}`);
213
+ }
214
+ return !isVirtual;
215
+ });
216
+ // Check if there are physical cameras available
217
+ if (this.availableCameras.length === 0) {
218
+ console.log('No physical cameras available');
219
+ this.eventBus.emit('error', new Error('Cámaras virtuales no están permitidas. Use una cámara física.'));
220
+ return [];
221
+ }
208
222
  // Cache the results for optimization
209
223
  CameraService.deviceEnumerationCache = {
210
224
  devices: this.availableCameras,
@@ -254,6 +268,21 @@ class CameraService {
254
268
  });
255
269
  this.lastStreamConstraints = finalConstraints;
256
270
  const stream = await this.currentStreamPromise;
271
+ // Validate that active stream is not from a virtual camera
272
+ const videoTrack = stream.getVideoTracks()[0];
273
+ if (videoTrack) {
274
+ const settings = videoTrack.getSettings();
275
+ const deviceId = settings.deviceId;
276
+ if (deviceId) {
277
+ const devices = await navigator.mediaDevices.enumerateDevices();
278
+ const currentDevice = devices.find(device => device.deviceId === deviceId);
279
+ if (currentDevice && this.isVirtualCamera(currentDevice)) {
280
+ console.log(`Virtual camera detected in active stream: ${currentDevice.label}`);
281
+ stream.getTracks().forEach(track => track.stop());
282
+ throw new Error('Cámaras virtuales no están permitidas. Use una cámara física.');
283
+ }
284
+ }
285
+ }
257
286
  return stream;
258
287
  }
259
288
  catch (error) {
@@ -271,7 +300,23 @@ class CameraService {
271
300
  audio: false
272
301
  });
273
302
  this.lastStreamConstraints = basicConstraints;
274
- return await this.currentStreamPromise;
303
+ const stream = await this.currentStreamPromise;
304
+ // Validate fallback stream as well
305
+ const videoTrack = stream.getVideoTracks()[0];
306
+ if (videoTrack) {
307
+ const settings = videoTrack.getSettings();
308
+ const deviceId = settings.deviceId;
309
+ if (deviceId) {
310
+ const devices = await navigator.mediaDevices.enumerateDevices();
311
+ const currentDevice = devices.find(device => device.deviceId === deviceId);
312
+ if (currentDevice && this.isVirtualCamera(currentDevice)) {
313
+ console.log(`Virtual camera detected in fallback stream: ${currentDevice.label}`);
314
+ stream.getTracks().forEach(track => track.stop());
315
+ throw new Error('Cámaras virtuales no están permitidas. Use una cámara física.');
316
+ }
317
+ }
318
+ }
319
+ return stream;
275
320
  }
276
321
  }
277
322
  async switchCamera(cameraId) {
@@ -280,6 +325,12 @@ class CameraService {
280
325
  await this.enumerateDevices();
281
326
  return;
282
327
  }
328
+ // Validate that the target camera is not virtual
329
+ if (this.isVirtualCamera(selectedCamera)) {
330
+ console.log(`Attempted to switch to virtual camera: ${selectedCamera.label}`);
331
+ this.eventBus.emit('error', new Error('No se puede cambiar a cámara virtual'));
332
+ return;
333
+ }
283
334
  await this.setSelectedCamera(cameraId);
284
335
  }
285
336
  isRearCamera(stream) {
@@ -551,6 +602,37 @@ class CameraService {
551
602
  invalidateDeviceCache() {
552
603
  CameraService.deviceEnumerationCache = null;
553
604
  }
605
+ isVirtualCamera(device) {
606
+ const label = device.label.toLowerCase();
607
+ const virtualCameraIndicators = [
608
+ 'obs',
609
+ 'virtual',
610
+ 'snap camera',
611
+ 'manycam',
612
+ 'xsplit',
613
+ 'camtwist',
614
+ 'ecamm',
615
+ 'nvidia broadcast',
616
+ 'droidcam',
617
+ 'iriun',
618
+ 'elgato',
619
+ 'streamlabs',
620
+ 'wirecast',
621
+ 'zoom virtual',
622
+ 'teams virtual',
623
+ 'mmhmm',
624
+ 'camo',
625
+ 'reincubate camo',
626
+ 'webcamoid',
627
+ 'splitcam',
628
+ 'cheese',
629
+ 'guvcview',
630
+ 'yawcam',
631
+ 'webcam 7',
632
+ 'altserver'
633
+ ];
634
+ return virtualCameraIndicators.some(indicator => label.includes(indicator));
635
+ }
554
636
  }
555
637
 
556
638
  class LowMemoryDeviceStrategy {
@@ -670,7 +752,12 @@ class DetectionService {
670
752
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
671
753
  }
672
754
  catch (error) {
673
- if (error.message.includes('failed to allocate a buffer')) {
755
+ // Múltiples tipos de errores de memoria que pueden ocurrir
756
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
757
+ error.message.includes('Out of memory') ||
758
+ error.message.includes('RangeError: Out of memory') ||
759
+ error.message.includes('no available backend found');
760
+ if (isMemoryError) {
674
761
  const fallbackOptions = {
675
762
  executionProviders: ['wasm'],
676
763
  graphOptimizationLevel: 'disabled',
@@ -680,8 +767,17 @@ class DetectionService {
680
767
  executionMode: 'sequential',
681
768
  interOpNumThreads: 1,
682
769
  intraOpNumThreads: 1,
770
+ // Configuraciones adicionales para dispositivos con poca memoria
771
+ enableProfiling: false,
772
+ sessionLogSeverityLevel: 4,
773
+ sessionLogVerbosityLevel: 0,
683
774
  };
684
- this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
775
+ try {
776
+ this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
777
+ }
778
+ catch (fallbackError) {
779
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
780
+ }
685
781
  }
686
782
  else {
687
783
  throw error;
@@ -721,7 +817,12 @@ class DetectionService {
721
817
  this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
722
818
  }
723
819
  catch (error) {
724
- if (error.message.includes('failed to allocate a buffer')) {
820
+ // Múltiples tipos de errores de memoria que pueden ocurrir
821
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
822
+ error.message.includes('Out of memory') ||
823
+ error.message.includes('RangeError: Out of memory') ||
824
+ error.message.includes('no available backend found');
825
+ if (isMemoryError) {
725
826
  const fallbackOptions = {
726
827
  executionProviders: ['wasm'],
727
828
  graphOptimizationLevel: 'disabled',
@@ -731,8 +832,17 @@ class DetectionService {
731
832
  executionMode: 'sequential',
732
833
  interOpNumThreads: 1,
733
834
  intraOpNumThreads: 1,
835
+ // Configuraciones adicionales para dispositivos con poca memoria
836
+ enableProfiling: false,
837
+ sessionLogSeverityLevel: 4,
838
+ sessionLogVerbosityLevel: 0,
734
839
  };
735
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
840
+ try {
841
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
842
+ }
843
+ catch (fallbackError) {
844
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
845
+ }
736
846
  }
737
847
  else {
738
848
  throw error;
@@ -1089,7 +1199,7 @@ class ServiceContainer {
1089
1199
  }
1090
1200
  }
1091
1201
 
1092
- 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%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{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}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.back-capture-section{bottom:16px;max-width:300px}.back-capture-buttons{flex-direction:column;gap:8px;width:100%}.capture-button,.back-capture-buttons .skip-button{width:100%;max-width:200px;padding:10px 20px;font-size:13px}}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.skip-button{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:translateY(1px)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}.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)}}@keyframes slideInFromTopCentered{0%{opacity:0;transform:translate(-50%, -50%) translateY(-8px) scale(0.95)}100%{opacity:1;transform:translate(-50%, -50%) 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:flex-start;align-items:center;gap:8px;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:0;font-weight:400;display:flex;align-items:center;gap:6px}.camera-label .autofocus-icon{font-size:12px;color:#ffffff !important;opacity:0.8;flex-shrink:0}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9;flex-shrink:0}.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}.guide-text{font-size:11px;padding:4px 8px;border-radius:8px}}.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:slideInFromTop 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}.manual-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.manual-capture-button{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}.manual-capture-button:hover{background:#f8f9fa}.manual-capture-button:active{background:#e9ecef;transform:translateY(1px)}.manual-capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}}";
1202
+ 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%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);}.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-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{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}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled,.capture-button.primary-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}@media (max-width: 480px){.back-capture-section{bottom:16px;max-width:300px}.back-capture-buttons{flex-direction:column;gap:8px;width:100%}.capture-button,.back-capture-buttons .skip-button{width:100%;max-width:200px;padding:10px 20px;font-size:13px}.capture-button:disabled,.capture-button.primary-button:disabled,.skip-button:disabled{opacity:0.7 !important;cursor:not-allowed !important}}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.guide-text.performance-warning-text{animation:gentlePulse 2s ease-in-out infinite;background:rgba(255, 107, 107, 0.3);border:1px solid rgba(255, 107, 107, 0.5)}@keyframes gentlePulse{0%,100%{transform:translate(-50%, -50%) scale(1);background:rgba(255, 107, 107, 0.3)}50%{transform:translate(-50%, -50%) scale(1.02);background:rgba(255, 107, 107, 0.4)}}.skip-button{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:translateY(1px)}.skip-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}.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(0, 0, 0, 0.2);border-top:2px solid #333333;border-radius:50%;animation:spin 1s linear infinite;display:inline-block;margin-right:8px;vertical-align:middle}@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)}}@keyframes slideInFromTopCentered{0%{opacity:0;transform:translate(-50%, -50%) translateY(-8px) scale(0.95)}100%{opacity:1;transform:translate(-50%, -50%) 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:flex-start;align-items:center;gap:8px;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:0;font-weight:400;display:flex;align-items:center;gap:6px}.camera-label .autofocus-icon{font-size:12px;color:#ffffff !important;opacity:0.8;flex-shrink:0}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9;flex-shrink:0}.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}.guide-text{font-size:11px;padding:4px 8px;border-radius:8px}}.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:slideInFromTop 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}.manual-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.manual-capture-button{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}.manual-capture-button:hover{background:#f8f9fa}.manual-capture-button:active{background:#e9ecef;transform:translateY(1px)}.manual-capture-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}.manual-capture-button:disabled{opacity:0.7 !important;cursor:not-allowed !important}}";
1093
1203
 
1094
1204
  const JaakStamps = class {
1095
1205
  constructor(hostRef) {
@@ -1145,6 +1255,10 @@ const JaakStamps = class {
1145
1255
  };
1146
1256
  backDocumentTimerRemaining = 0;
1147
1257
  showManualCaptureButton = false;
1258
+ performanceDegradedMode = false; // Auto-switched to manual mode due to performance
1259
+ showPerformanceMessage = false; // Show performance message temporarily
1260
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
1261
+ processingButton = null; // Track which button is processing
1148
1262
  // Services
1149
1263
  serviceContainer;
1150
1264
  eventBus;
@@ -1163,12 +1277,12 @@ const JaakStamps = class {
1163
1277
  alignmentStartTime;
1164
1278
  alignmentTimer;
1165
1279
  backDocumentTimer;
1280
+ orientationTimer;
1166
1281
  // Performance monitoring
1167
1282
  performanceMetrics = {
1168
1283
  fps: 0,
1169
1284
  inferenceTime: 0,
1170
1285
  memoryUsage: 0,
1171
- cpuUsage: 0,
1172
1286
  onnxLoadTime: 0,
1173
1287
  frameProcessingTime: 0,
1174
1288
  totalDetections: 0,
@@ -1187,18 +1301,29 @@ const JaakStamps = class {
1187
1301
  MIN_INFERENCE_INTERVAL = 50;
1188
1302
  performanceHistory = [];
1189
1303
  PERFORMANCE_HISTORY_SIZE = 10;
1304
+ // Sistema simplificado de monitoreo de rendimiento
1305
+ PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
1306
+ SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
1307
+ slowFrameCount = 0; // Contador de frames consecutivos lentos
1308
+ processedFramesCount = 0; // Contador total de frames procesados
1309
+ hasAutoSwitchedToManual = false;
1190
1310
  // Canvas pool for optimized screenshot capture
1191
1311
  canvasPool = [];
1192
1312
  MAX_CANVAS_POOL_SIZE = 3;
1193
1313
  async componentDidLoad() {
1194
- this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1195
- await this.initializeServices();
1196
- this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1197
- await this.setupEventListeners();
1198
- this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1199
- await this.initializeComponent();
1200
- if (this.debug) {
1201
- this.initializePerformanceMonitor();
1314
+ try {
1315
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1316
+ await this.initializeServices();
1317
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1318
+ await this.setupEventListeners();
1319
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1320
+ await this.initializeComponent();
1321
+ if (this.debug) {
1322
+ this.initializePerformanceMonitor();
1323
+ }
1324
+ }
1325
+ catch (error) {
1326
+ this.updateStatus('Error de inicialización', error.message, 'error');
1202
1327
  }
1203
1328
  }
1204
1329
  async initializeServices() {
@@ -1219,14 +1344,8 @@ const JaakStamps = class {
1219
1344
  this.detectionService = this.serviceContainer.getDetectionService();
1220
1345
  }
1221
1346
  async setupEventListeners() {
1222
- this.eventBus.on('state-changed', (data) => {
1223
- this.handleStateChange(data);
1224
- });
1225
- this.eventBus.on('camera-changed', () => {
1226
- // Camera changed event
1227
- });
1228
- this.eventBus.on('error', () => {
1229
- // Handle service errors
1347
+ this.eventBus.on('state-changed', () => {
1348
+ this.handleStateChange();
1230
1349
  });
1231
1350
  }
1232
1351
  async initializeComponent() {
@@ -1262,7 +1381,7 @@ const JaakStamps = class {
1262
1381
  if (!window.ort) {
1263
1382
  this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
1264
1383
  const script = document.createElement('script');
1265
- script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
1384
+ script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.0/dist/ort.min.js';
1266
1385
  document.head.appendChild(script);
1267
1386
  await new Promise((resolve) => {
1268
1387
  script.onload = () => {
@@ -1333,7 +1452,8 @@ const JaakStamps = class {
1333
1452
  return false;
1334
1453
  }
1335
1454
  }
1336
- handleStateChange(data) {
1455
+ handleStateChange() {
1456
+ // Handle state changes from StateManager if needed
1337
1457
  }
1338
1458
  initializeResizeObserver() {
1339
1459
  if ('ResizeObserver' in window && this.detectionContainer) {
@@ -1342,6 +1462,18 @@ const JaakStamps = class {
1342
1462
  });
1343
1463
  resizeObserver.observe(this.detectionContainer.parentElement);
1344
1464
  }
1465
+ // Handle mobile device orientation changes
1466
+ window.addEventListener('orientationchange', () => {
1467
+ // Clear any existing orientation timer
1468
+ if (this.orientationTimer) {
1469
+ clearTimeout(this.orientationTimer);
1470
+ }
1471
+ // Add delay to ensure video dimensions are updated after rotation
1472
+ this.orientationTimer = window.setTimeout(() => {
1473
+ this.handleResize();
1474
+ this.orientationTimer = undefined;
1475
+ }, 300);
1476
+ });
1345
1477
  }
1346
1478
  handleResize() {
1347
1479
  if (this.detectionContainer) {
@@ -1350,10 +1482,17 @@ const JaakStamps = class {
1350
1482
  this.updateMaskDimensions(rect);
1351
1483
  }
1352
1484
  }
1485
+ triggerRerender() {
1486
+ this.captureStateVersion = this.captureStateVersion + 1;
1487
+ }
1353
1488
  getGuideText(step) {
1354
1489
  if (step === 'completed') {
1355
1490
  return 'Proceso completado';
1356
1491
  }
1492
+ // Check if we should show performance message
1493
+ if (this.showPerformanceMessage && step === 'front') {
1494
+ return 'Modo manual activado por rendimiento. Use el botón para capturar.';
1495
+ }
1357
1496
  if (step === 'front') {
1358
1497
  // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
1359
1498
  if (this.showManualCaptureButton) {
@@ -1511,8 +1650,14 @@ const JaakStamps = class {
1511
1650
  }
1512
1651
  }
1513
1652
  async skipBackCapture() {
1653
+ console.log('🟡 Botón clicked: Saltar reverso');
1654
+ // Set processing state immediately
1655
+ this.processingButton = 'skip-back';
1656
+ // Add small delay to show spinner
1657
+ await new Promise(resolve => setTimeout(resolve, 100));
1514
1658
  const readyCheck = this.isComponentReady();
1515
1659
  if (!readyCheck.ready) {
1660
+ this.processingButton = null;
1516
1661
  return { success: false, error: readyCheck.message };
1517
1662
  }
1518
1663
  try {
@@ -1529,6 +1674,10 @@ const JaakStamps = class {
1529
1674
  catch (error) {
1530
1675
  return { success: false, error: error.message };
1531
1676
  }
1677
+ finally {
1678
+ // Always clear processing state when done
1679
+ this.processingButton = null;
1680
+ }
1532
1681
  }
1533
1682
  async getStatus() {
1534
1683
  const readyCheck = this.isComponentReady();
@@ -1583,9 +1732,21 @@ const JaakStamps = class {
1583
1732
  if (this.stateManager) {
1584
1733
  this.stateManager.updateCaptureState({ isLoading: true });
1585
1734
  }
1586
- await this.detectionService.loadModel();
1735
+ try {
1736
+ await this.detectionService.loadModel();
1737
+ }
1738
+ catch (modelError) {
1739
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1740
+ throw modelError;
1741
+ }
1587
1742
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1588
- await this.detectionService.loadClassificationModel();
1743
+ try {
1744
+ await this.detectionService.loadClassificationModel();
1745
+ }
1746
+ catch (classificationError) {
1747
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1748
+ throw classificationError;
1749
+ }
1589
1750
  const loadEndTime = performance.now();
1590
1751
  const loadTime = loadEndTime - loadStartTime;
1591
1752
  // Record ONNX load time
@@ -1694,17 +1855,63 @@ const JaakStamps = class {
1694
1855
  // Always allow getting capture delay, even during initialization
1695
1856
  return this.captureDelay;
1696
1857
  }
1858
+ // Memory and device capability detection
1859
+ checkDeviceCapabilities() {
1860
+ const deviceMemory = navigator.deviceMemory;
1861
+ // Siempre intentar usar ML, independientemente del dispositivo
1862
+ // Las restricciones anteriores no eran confiables
1863
+ return {
1864
+ canUseML: true,
1865
+ reason: 'Dispositivo compatible con detección automática.',
1866
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
1867
+ };
1868
+ }
1697
1869
  // DETECTION METHODS
1698
1870
  async startDetection() {
1699
1871
  try {
1872
+ // Verificar capacidades del dispositivo antes de cargar modelos
1873
+ const capabilities = this.checkDeviceCapabilities();
1874
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
1875
+ if (!capabilities.canUseML && this.useDocumentDetector) {
1876
+ this.switchToManualModeWithWarning(capabilities.reason);
1877
+ return;
1878
+ }
1700
1879
  // Paso 1: Verificar y cargar modelos si es necesario
1701
1880
  if (!this.detectionService.isModelLoaded()) {
1702
1881
  const loadStartTime = performance.now();
1703
1882
  this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1704
1883
  this.stateManager.updateCaptureState({ isLoading: true });
1705
- await this.detectionService.loadModel();
1884
+ try {
1885
+ await this.detectionService.loadModel();
1886
+ }
1887
+ catch (modelError) {
1888
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1889
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
1890
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1891
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
1892
+ return;
1893
+ }
1894
+ else {
1895
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1896
+ throw modelError;
1897
+ }
1898
+ }
1706
1899
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1707
- await this.detectionService.loadClassificationModel();
1900
+ try {
1901
+ await this.detectionService.loadClassificationModel();
1902
+ }
1903
+ catch (classificationError) {
1904
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1905
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
1906
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1907
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
1908
+ return;
1909
+ }
1910
+ else {
1911
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1912
+ throw classificationError;
1913
+ }
1914
+ }
1708
1915
  const loadEndTime = performance.now();
1709
1916
  const loadTime = loadEndTime - loadStartTime;
1710
1917
  // Record ONNX load time
@@ -1714,19 +1921,40 @@ const JaakStamps = class {
1714
1921
  }
1715
1922
  // Paso 2: Detectar y configurar dispositivos
1716
1923
  this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1717
- await this.cameraService.enumerateDevices();
1718
- await this.updateCameraInfoWithAutofocus();
1924
+ try {
1925
+ await this.cameraService.enumerateDevices();
1926
+ }
1927
+ catch (enumerateError) {
1928
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
1929
+ }
1930
+ try {
1931
+ await this.updateCameraInfoWithAutofocus();
1932
+ }
1933
+ catch (cameraInfoError) {
1934
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
1935
+ }
1719
1936
  // Paso 3: Configurar cámara seleccionada
1720
1937
  this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1721
- const stream = await this.cameraService.setupCamera();
1722
- // Paso 4: Inicializar video y ocultar estado inmediatamente
1938
+ let stream;
1939
+ try {
1940
+ stream = await this.cameraService.setupCamera();
1941
+ }
1942
+ catch (setupError) {
1943
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
1944
+ }
1945
+ // Paso 4: Inicializar video y mostrar estado de análisis inicial
1723
1946
  if (this.useDocumentDetector) {
1724
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
1947
+ this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
1725
1948
  }
1726
1949
  else {
1727
1950
  this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
1728
1951
  }
1729
- await this.initializeVideoStream(stream);
1952
+ try {
1953
+ await this.initializeVideoStream(stream);
1954
+ }
1955
+ catch (videoError) {
1956
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
1957
+ }
1730
1958
  // Paso 5: Calibrar máscara
1731
1959
  if (this.detectionContainer) {
1732
1960
  const container = this.detectionContainer.parentElement;
@@ -1757,6 +1985,12 @@ const JaakStamps = class {
1757
1985
  this.videoRef.onloadedmetadata = async () => {
1758
1986
  await this.videoRef.play();
1759
1987
  this.stateManager.updateCaptureState({ isVideoActive: true });
1988
+ // Recalculate mask dimensions when new camera stream loads
1989
+ if (this.detectionContainer) {
1990
+ const container = this.detectionContainer.parentElement;
1991
+ const rect = container.getBoundingClientRect();
1992
+ this.updateMaskDimensions(rect);
1993
+ }
1760
1994
  resolve();
1761
1995
  };
1762
1996
  });
@@ -1771,8 +2005,8 @@ const JaakStamps = class {
1771
2005
  const captureState = this.stateManager.getCaptureState();
1772
2006
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
1773
2007
  return;
1774
- // Show manual capture button when document detector is disabled
1775
- if (!this.useDocumentDetector) {
2008
+ // Show manual capture button when document detector is disabled or performance is degraded
2009
+ if (!this.useDocumentDetector || this.performanceDegradedMode) {
1776
2010
  this.showManualCaptureButton = true;
1777
2011
  // Continue the loop for UI updates but skip detection logic
1778
2012
  if (captureState.step !== 'completed') {
@@ -1780,7 +2014,7 @@ const JaakStamps = class {
1780
2014
  }
1781
2015
  return;
1782
2016
  }
1783
- else {
2017
+ else if (!this.performanceDegradedMode) {
1784
2018
  this.showManualCaptureButton = false;
1785
2019
  }
1786
2020
  if (captureState.isDetectionPaused) {
@@ -1810,10 +2044,27 @@ const JaakStamps = class {
1810
2044
  this.lastInferenceTime = currentTime;
1811
2045
  // Measure preprocessing and inference time
1812
2046
  const inferenceStartTime = performance.now();
1813
- const inputTensor = this.detectionService.preprocess(this.videoRef);
1814
- // Standard detection without quality validation
1815
- const detections = await this.detectionService.runInference(inputTensor);
1816
- const inferenceTime = performance.now() - inferenceStartTime;
2047
+ let inputTensor;
2048
+ let detections;
2049
+ let inferenceTime;
2050
+ try {
2051
+ inputTensor = this.detectionService.preprocess(this.videoRef);
2052
+ }
2053
+ catch (preprocessError) {
2054
+ this.switchToManualModeWithError('Error al procesar imagen');
2055
+ return;
2056
+ }
2057
+ try {
2058
+ // Standard detection without timeout (timeout is handled by frame counting)
2059
+ detections = await this.detectionService.runInference(inputTensor);
2060
+ inferenceTime = performance.now() - inferenceStartTime;
2061
+ }
2062
+ catch (inferenceError) {
2063
+ this.switchToManualModeWithError('Error en detección automática');
2064
+ return;
2065
+ }
2066
+ // Increment frame count
2067
+ this.processedFramesCount++;
1817
2068
  // Record ONNX performance metrics
1818
2069
  if (this.debug) {
1819
2070
  this.recordOnnxPerformance(0, inferenceTime);
@@ -1852,9 +2103,30 @@ const JaakStamps = class {
1852
2103
  }
1853
2104
  this.updateMaskColor(detections);
1854
2105
  // Record frame processing metrics
2106
+ const frameEndTime = performance.now();
2107
+ const totalFrameTime = frameEndTime - frameStartTime;
2108
+ // Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
2109
+ if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
2110
+ // Verificar si el frame es lento
2111
+ if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
2112
+ this.slowFrameCount++;
2113
+ // Si tenemos suficientes frames lentos consecutivos, cambiar a manual
2114
+ if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
2115
+ this.switchToManualMode();
2116
+ }
2117
+ }
2118
+ else {
2119
+ // Frame rápido, reiniciar contador
2120
+ if (this.slowFrameCount > 0) {
2121
+ this.slowFrameCount = 0;
2122
+ }
2123
+ }
2124
+ // Cambiar mensaje de estado después de analizar los primeros 2 frames
2125
+ if (this.processedFramesCount === 2) {
2126
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
2127
+ }
2128
+ }
1855
2129
  if (this.debug) {
1856
- const frameEndTime = performance.now();
1857
- const totalFrameTime = frameEndTime - frameStartTime;
1858
2130
  this.recordFrameProcessing(totalFrameTime, detections.length);
1859
2131
  }
1860
2132
  // Continue detection loop
@@ -1890,6 +2162,12 @@ const JaakStamps = class {
1890
2162
  clearTimeout(this.alignmentTimer);
1891
2163
  this.alignmentTimer = undefined;
1892
2164
  }
2165
+ if (this.orientationTimer) {
2166
+ clearTimeout(this.orientationTimer);
2167
+ this.orientationTimer = undefined;
2168
+ }
2169
+ // Liberar recursos de ONNX al limpiar el componente
2170
+ this.releaseOnnxResources();
1893
2171
  if (this.performanceUpdateInterval) {
1894
2172
  clearInterval(this.performanceUpdateInterval);
1895
2173
  this.performanceUpdateInterval = undefined;
@@ -1910,7 +2188,7 @@ const JaakStamps = class {
1910
2188
  isCapturing: false
1911
2189
  };
1912
2190
  const cameraInfo = this.cameraInfoWithAutofocus;
1913
- return (h("div", { key: 'e0b32b1b41b58e3a58b552199384080e8f2c069b', class: "detector-container" }, h("div", { key: '03c77af311ff12d936994d629d58550cbaae190c', class: "video-container" }, h("video", { key: '94f15b34f34367a10d17816a31cd147adb57bc90', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '982be192792dfaf644eb173bfee9b26ebd229288', 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: {
2191
+ return (h("div", { key: 'f9cbca2fd6b8548a29ace7fcb972b53f402f61af', class: "detector-container" }, h("div", { key: '4f3cac734eb6bfd44a8de644eadca17e7893e11a', class: "video-container" }, h("video", { key: '5e15a38c989a72d5274281272d41ba064447ca6f', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '38c8ad8b691db60e9b5a2a8b641fe6febbeacfca', 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: {
1914
2192
  position: 'absolute',
1915
2193
  left: `${box.x}px`,
1916
2194
  top: `${box.y}px`,
@@ -1919,9 +2197,9 @@ const JaakStamps = class {
1919
2197
  border: '2px solid #32406C',
1920
2198
  pointerEvents: 'none',
1921
2199
  boxSizing: 'border-box'
1922
- } })))), this.isMaskReady && (h("div", { key: 'f5212ff70db65d2b836a3ac456a1701701630660', class: "overlay-mask" }, h("div", { key: '5080217f4962c2c7152def39cc978ba1c40a3a1c', class: "card-outline" }, h("div", { key: 'd37f1ca8723d79b10d1c6a3afaf54e2f2657ae79', class: "side side-top" }), h("div", { key: 'c5bb4f7d646a991f43598688ffa909c04697bb06', class: "side side-right" }), h("div", { key: '4a5040b4a236367dcdf72f4e72d497b8b6dee6c0', class: "side side-bottom" }), h("div", { key: 'c74ecef56eee8e92cd9491d693fa89a6c01bdbbd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '2a2932b0c03835193ebf86ddb5f13173b1dba367', class: "guide-text" }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '41fcf2d79106d3ed928fc2842cb6a6725ba4d4f6', class: "back-capture-section" }, h("div", { key: '98dbe1bbe454bad42c3e155247f25a9151aaf1f9', class: "back-capture-buttons" }, !this.useDocumentDetector && (h("button", { key: '01693bc0d6915a4d02f318268020dfbcf3a70b94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Reverso')), h("button", { key: '0a4ba55cbbea867b936a83a16a622a3c36a225f9', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
2200
+ } })))), this.isMaskReady && (h("div", { key: 'c56fde56b7a86a4e8a4cf558745291ea5cde12fd', class: "overlay-mask" }, h("div", { key: '04ed3f01f271dc84eed594cb4b60c2453645bd41', class: "card-outline" }, h("div", { key: '648bd423a5e886d4768115237de5896c7223b30b', class: "side side-top" }), h("div", { key: 'b8878059493ca61961b9b3c82ecc18b50b79ac85', class: "side side-right" }), h("div", { key: '09e5ad64ff33dbd5c112bf52b71e14409ab6aa65', class: "side side-bottom" }), h("div", { key: 'b8ba3d83c9bc1b4a0da859d7ed4b9a15eaddcc2a', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '3279701a240f77ac96e68a34ad95c099340c9a6a', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '2adf96d0a57e482372fd9586229dac2301b64fd4', class: "back-capture-section" }, h("div", { key: '305a147c9cfee854c60ac946a48a1d21b2792725', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode || this.showManualCaptureButton) && (h("button", { key: '43270a0647f39307229c540dd5130fb58b1ff4fb', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (h("span", { key: 'eb4666603774564eef95a120ac13faeb9e10c8b4', class: "button-spinner" })), "Capturar Reverso")), h("button", { key: 'cac667d9548ae2314ad5a677c5ed90f25184f728', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (h("span", { key: '0af7aa4106ce189ae2d79bec78ede01abe25b1e7', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1923
2201
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
1924
- : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: '1db208a53dea37211bdd88254e575142b7abf91f', class: "camera-controls" }, h("button", { key: '90755839358f997c3ce2f4c36a8d17d39ea5f8ad', 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: 'ef7889ab82f9eaabcabec3e3dc3ebb7850818dd4', class: "camera-selector-dropdown" }, h("div", { key: 'e28fb005316a742318be3cf8582f520392b71bba', class: "camera-selector-header" }, h("span", { key: '97338030b1aedb50c5f5a588ea03c0f19bcb0ddd' }, "Seleccionar C\u00E1mara"), h("button", { key: '1537c907dfab17c232a256f9fc9210cbd0125cc6', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '509178119e6eed5fe0a6ea287904a53bd9fbecbc', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '485f7e1a97c43ed280a106bde3c6ee910625025c', class: "device-info" }, h("small", { key: 'd76edba943f4e06c14024923496555fa1fe6d845' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'c5184dd93aa41cff61b88ea2046b481efc87c1f2', class: "manual-capture-section" }, h("button", { key: '6e45d9a0fec60469eb926ff353cb1a440d59f5fe', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Frente'))))), captureState.isCapturing && (h("div", { key: '9e296a942ba51cc1fd152e76ae406620c84ed721', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '517db50f7273788d94d5e6db6c40f65e00e18784', class: "flip-animation" }, h("div", { key: 'fbf6a26c4cc0539bd16b45899d19984af3449610', class: "id-card-icon" }), h("div", { key: 'fb3a201864d04531a078074458aa6e7f6580d565', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '10c694e233d26523d538ff0d2edbf6dcfb8e37c8', class: "success-animation" }, h("div", { key: '87e545650825ab805e887667281ecdf8bdb34c45', class: "check-icon" }), h("div", { key: '359da71f391d68e9437b749b949ee90685b96b3f', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '7c5533bb875eb3d9cdcc6cfdfc20e4d5bd98d103', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'f32df46ab83279387de606eadf53cd4a4009f449', class: "status-spinner" })), h("div", { key: 'dd640de5a3b64f5f99363318e2fc70e32ccbc7db', class: "status-content" }, h("div", { key: '618c0ab9cb0a24b0af3f6946052beab0d660fe6d', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'e52c2a4945c7f5a190a446ac588aa1668c21f26d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '61cea10bc3438fb74aa176d71c4238b070cb0f09', class: "performance-monitor" }, h("div", { key: '44f6b1accb56d99a236b22c0769f870638d9c768', class: "performance-expanded" }, h("div", { key: '6febc85e319752b0aebc0a4e4050ea6d597375a7', class: "metrics-row" }, h("div", { key: '96b81895ad3cd742c96308ed53a2126af76eb619', class: "metric-compact" }, h("span", { key: '774a11c6fed6a87db7673b018b3cdc378c5252ef', class: "metric-label" }, "FPS"), h("span", { key: 'cb866dcbea661e5f715b82c4dd760f786fd5bb2a', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'a935aaadb83ea400bfaa984d1d845bdfd87becc8', class: "metric-compact" }, h("span", { key: 'ffacaa2cb7838403a297145b70586e255cc845c7', class: "metric-label" }, "MEM"), h("span", { key: 'a1d7a7abec97dc0544d50b11b372f699f750b76f', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'bb3d156a46569a8c290bc518c42ec7957bc5b94e', class: "metrics-row" }, h("div", { key: '4cadd8f845e4c12120ed549e4a73e02c6cbd9257', class: "metric-compact" }, h("span", { key: '1fc5661222f495b79fd7eb126e04f52eecab6fb3', class: "metric-label" }, "INF"), h("span", { key: 'eca1f59d1c41ab99158eec81592bc7150bfeabe1', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'f708e2757adcb03e633111d96bd6786fb6b80ea6', class: "metric-compact" }, h("span", { key: 'a13099ded64180996d6f65bf215483ba83daccbc', class: "metric-label" }, "FRAME"), h("span", { key: '2765cd1f7c1a49df9de64639cc1b777c79f89f4c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '13aaf85a1b4b66294c4204f02d956b64665aa766', class: "metrics-row" }, h("div", { key: '40012f3f646d70c535a606ebbe581a2fe6fd2e50', class: "metric-compact" }, h("span", { key: '03f8b17746fe404485820d06a50173678db2fd25', class: "metric-label" }, "DET"), h("span", { key: 'c4f87430f142355054d9361330d6a5d508974b70', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'cf366a41d7f7cda6046c32cc7c514eb7b46b05d1', class: "metric-compact" }, h("span", { key: '925e7de15b96ba4dbbcdbf8a9f0e3173bae49573', class: "metric-label" }, "RATE"), h("span", { key: '9701e60c7793f2c205f3c9ffd254b6a115b8a68c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '636aeb9b2b8dd992d8a28a8db44b213e12514096', class: "watermark" }, h("img", { key: '0407b550d962a6113e05ad75e25a1a1bbd76d9ce', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
2202
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'de6e5c4fad4d4d996b894457c9af52bc71e533fb', class: "camera-controls" }, h("button", { key: 'cfddcf2420d5f5923a6b02ca446bbe1b8e9fc5e4', 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: '5fb73f958b9a5a6f9242032c4530b0bdab84f6c5', class: "camera-selector-dropdown" }, h("div", { key: '8ee2d26b1001e571e7b6c326e927926fa08287fd', class: "camera-selector-header" }, h("span", { key: '78350b0fb1bd933f7c8c59d8dab0b7122ffbe363' }, "Seleccionar C\u00E1mara"), h("button", { key: 'bb750e0ddd5a1ee5ff96bd57805a7f90b8821e9f', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: 'd0f6343cf1b7367f3da33ec8cf64891109d923b8', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: 'c9642fbfb0acea766d3f0542f4cc9f17f4af4d7b', class: "device-info" }, h("small", { key: 'b6e9507ff7addf011f339f6c22a88a96b40db68a' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '04ae9262409284782850b90945eb7038673e2097', class: "manual-capture-section" }, h("button", { key: '27c315e503ccba468b89ebd58d6b70f40ddd06ba', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (h("span", { key: '8f2dcd2df9a218b37eb0b8204a8724362cfeaa53', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (h("div", { key: '6fd6fe1d0213b01c8119a67fb8108f73227feee1', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'fba3816b48b99ca64510ecbefc0d41431569c9ae', class: "flip-animation" }, h("div", { key: 'beea1c8245b5d81f1008efc6d1b079a61c821e7f', class: "id-card-icon" }), h("div", { key: 'f0fbc34f619a653e2c0ade764ca99e7aa582d157', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: 'e4a0e6d31112e3a50024d1d0430b4ad997049fe7', class: "success-animation" }, h("div", { key: '5dd3f781f55630bc904c81207548ae35bc79b452', class: "check-icon" }), h("div", { key: 'a10ebc557582325091efb0abd12d62b27a43b98c', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '975865955dee1803b5215ecf049701c98b324855', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '84f6f67202e26ce80f7fc5d5f321e23b5bc55c57', class: "status-spinner" })), h("div", { key: 'a1ae56ed156e8feb279a1bf160f749c5d0b5ce45', class: "status-content" }, h("div", { key: 'd62932554cea257ce19763df26e0ba702e50eeca', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'c5506fa12dd0d37d26dc26c9f2381c843330451f', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '2097eda63185f336f7319014fae2ef6db1e41761', class: "performance-monitor" }, h("div", { key: 'd611c494c6b1a5930ef64aebdcba41e5d94c9ee6', class: "performance-expanded" }, h("div", { key: '800d46831e7b9bec260d9c5e67ea1a3eb9c2933e', class: "metrics-row" }, h("div", { key: 'dd0db1faa9050d98e71da4b5cd26a4a484c37c64', class: "metric-compact" }, h("span", { key: 'f0e6ea61a35ddb2ae5c2a6aff8c255a02c5c1219', class: "metric-label" }, "FPS"), h("span", { key: 'cbf4df4d142f6f01e45f566a8ede8fbbf29ad673', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '83861ad31fa46c8199a5583e4b3f8eefd44bacd6', class: "metric-compact" }, h("span", { key: '41b8de4cda47959dfc9df393e95fdadde534dd21', class: "metric-label" }, "MEM"), h("span", { key: 'b268f13cc78066e240ed158f1443113a94827aff', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '9f9a9d0690906b186492af23b900c5fa0f5cff89', class: "metrics-row" }, h("div", { key: '84fa6e453dc36e4a74e8b573447b17230766229e', class: "metric-compact" }, h("span", { key: 'f17d52899090c89f532d74c552dd8c5338fc2d83', class: "metric-label" }, "INF"), h("span", { key: '2beca0d7ce55ed574f6b01efc086c5846983e024', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '981e300cf076e81bfe946974f6cffd63619eacde', class: "metric-compact" }, h("span", { key: '3dfb0c996a69eb87b0f711322675cad69c471146', class: "metric-label" }, "FRAME"), h("span", { key: '3ae2ac6e90cda77f23e559f3197c3b761f9bdc40', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '2c51216838d87728d9d7764e5ba8a50fa02eb493', class: "metrics-row" }, h("div", { key: 'eda810ffb80ec38ab84583f59b26706f90a50fc9', class: "metric-compact" }, h("span", { key: '978ba9c0c67d6da6b29b3535af33a314bab9276a', class: "metric-label" }, "DET"), h("span", { key: '3e5e169fd4a3906283d8f918b5bbc7c5df6ccd0f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'b4b915c745e35c09470a4dc5edfc112d32cfb8b6', class: "metric-compact" }, h("span", { key: '3e2a7b88a4446813c955efb0dfa9e2958e8e1448', class: "metric-label" }, "RATE"), h("span", { key: 'f4f073f7ee622167a241c6a1c535ef580c13d9ab', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '76c556e92f6d7980539f1a30efe8751f665164fb', class: "watermark" }, h("img", { key: 'af23e115d6b4e92a72e55d6d98fde0e4b1d09e9b', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1925
2203
  }
1926
2204
  // Utility methods
1927
2205
  updateDetectionBoxes(boxes) {
@@ -2037,9 +2315,24 @@ const JaakStamps = class {
2037
2315
  }
2038
2316
  }
2039
2317
  async takeManualScreenshot() {
2040
- if (!this.videoRef)
2318
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
2319
+ // Set processing state immediately
2320
+ const captureState = this.stateManager?.getCaptureState();
2321
+ if (captureState?.step === 'front') {
2322
+ this.processingButton = 'capture-front';
2323
+ }
2324
+ else if (captureState?.step === 'back') {
2325
+ this.processingButton = 'capture-back';
2326
+ }
2327
+ console.log('🔵 processingButton set to:', this.processingButton);
2328
+ // Add small delay to show spinner
2329
+ await new Promise(resolve => setTimeout(resolve, 100));
2330
+ if (!this.videoRef) {
2331
+ this.processingButton = null;
2041
2332
  return;
2333
+ }
2042
2334
  // When using manual capture, use mask coordinates for cropping
2335
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
2043
2336
  await this.takeScreenshotWithMaskCoordinates();
2044
2337
  }
2045
2338
  async takeScreenshotWithMaskCoordinates() {
@@ -2103,18 +2396,25 @@ const JaakStamps = class {
2103
2396
  if (captureState.step === 'front') {
2104
2397
  this.stateManager.setCapturedImages({
2105
2398
  front: {
2106
- fullFrame: captureCanvas.toDataURL('image/png'),
2107
- cropped: croppedCanvas.toDataURL('image/png')
2399
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2400
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2108
2401
  }
2109
2402
  });
2110
2403
  // Check if document classification is enabled (independent of detector)
2111
2404
  if (this.useDocumentClassification) {
2112
- const classification = await this.detectionService.classifyDocument(croppedCanvas);
2113
- if (classification && classification.class === 'passport') {
2114
- this.completeProcess(true);
2115
- this.returnCanvasToPool(captureCanvas);
2116
- this.returnCanvasToPool(croppedCanvas);
2117
- return;
2405
+ try {
2406
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2407
+ if (classification && classification.class === 'passport') {
2408
+ this.completeProcess(true);
2409
+ this.returnCanvasToPool(captureCanvas);
2410
+ this.returnCanvasToPool(croppedCanvas);
2411
+ // Clear processing button when passport is detected
2412
+ this.processingButton = null;
2413
+ return;
2414
+ }
2415
+ }
2416
+ catch (classifyError) {
2417
+ // No cambiar a manual aquí, solo registrar el error y continuar
2118
2418
  }
2119
2419
  }
2120
2420
  this.stateManager.updateCaptureState({
@@ -2122,22 +2422,28 @@ const JaakStamps = class {
2122
2422
  isDetectionPaused: true,
2123
2423
  showFlipAnimation: true
2124
2424
  });
2425
+ this.triggerRerender();
2125
2426
  setTimeout(() => {
2126
2427
  this.stateManager.updateCaptureState({
2127
2428
  showFlipAnimation: false,
2128
2429
  isDetectionPaused: false
2129
2430
  });
2431
+ this.triggerRerender();
2130
2432
  this.startBackDocumentTimer();
2433
+ // Clear processing button after animation completes
2434
+ this.processingButton = null;
2131
2435
  }, 3000);
2132
2436
  }
2133
2437
  else if (captureState.step === 'back') {
2134
2438
  this.stateManager.setCapturedImages({
2135
2439
  back: {
2136
- fullFrame: captureCanvas.toDataURL('image/png'),
2137
- cropped: croppedCanvas.toDataURL('image/png')
2440
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2441
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2138
2442
  }
2139
2443
  });
2140
2444
  this.completeProcess(false);
2445
+ // Clear processing button after back capture completes
2446
+ this.processingButton = null;
2141
2447
  }
2142
2448
  // Return canvases to pool after use
2143
2449
  this.returnCanvasToPool(captureCanvas);
@@ -2170,16 +2476,21 @@ const JaakStamps = class {
2170
2476
  if (captureState.step === 'front') {
2171
2477
  this.stateManager.setCapturedImages({
2172
2478
  front: {
2173
- fullFrame: captureCanvas.toDataURL('image/png'),
2174
- cropped: croppedCanvas.toDataURL('image/png')
2479
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2480
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2175
2481
  }
2176
2482
  });
2177
2483
  // Check if document classification is enabled
2178
2484
  if (this.useDocumentClassification) {
2179
- const classification = await this.detectionService.classifyDocument(croppedCanvas);
2180
- if (classification && classification.class === 'passport') {
2181
- this.completeProcess(true);
2182
- return;
2485
+ try {
2486
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2487
+ if (classification && classification.class === 'passport') {
2488
+ this.completeProcess(true);
2489
+ return;
2490
+ }
2491
+ }
2492
+ catch (classifyError) {
2493
+ // No cambiar a manual aquí, solo registrar el error y continuar
2183
2494
  }
2184
2495
  }
2185
2496
  this.stateManager.updateCaptureState({
@@ -2198,8 +2509,8 @@ const JaakStamps = class {
2198
2509
  else if (captureState.step === 'back') {
2199
2510
  this.stateManager.setCapturedImages({
2200
2511
  back: {
2201
- fullFrame: captureCanvas.toDataURL('image/png'),
2202
- cropped: croppedCanvas.toDataURL('image/png')
2512
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2513
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2203
2514
  }
2204
2515
  });
2205
2516
  this.completeProcess(false);
@@ -2216,6 +2527,138 @@ const JaakStamps = class {
2216
2527
  cardOutline?.classList.remove('capturing');
2217
2528
  }, 600);
2218
2529
  }
2530
+ // Método simplificado para cambio a modo manual
2531
+ switchToManualMode() {
2532
+ // Only switch if detector is enabled and not already switched
2533
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2534
+ return;
2535
+ }
2536
+ // Detener todo el monitoreo de performance
2537
+ this.stopPerformanceMonitoring();
2538
+ // Liberar recursos de ONNX
2539
+ this.releaseOnnxResources();
2540
+ // Resetear máscara a blanco y limpiar detecciones
2541
+ this.resetMaskToWhite();
2542
+ this.detectionBoxes = [];
2543
+ this.hasAutoSwitchedToManual = true;
2544
+ this.performanceDegradedMode = true;
2545
+ this.showManualCaptureButton = true;
2546
+ this.showPerformanceMessage = true;
2547
+ // Hide performance message after 5 seconds
2548
+ setTimeout(() => {
2549
+ this.showPerformanceMessage = false;
2550
+ }, 5000);
2551
+ }
2552
+ // Método para cambiar a modo manual por errores de ONNX
2553
+ switchToManualModeWithWarning(warningMessage) {
2554
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2555
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2556
+ return;
2557
+ }
2558
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
2559
+ // Resetear máscara a blanco y limpiar detecciones
2560
+ this.resetMaskToWhite();
2561
+ this.detectionBoxes = [];
2562
+ // Cambiar a modo manual
2563
+ this.hasAutoSwitchedToManual = true;
2564
+ this.performanceDegradedMode = true;
2565
+ this.showManualCaptureButton = true;
2566
+ this.useDocumentDetector = false; // Desactivar detector automático
2567
+ // Actualizar el estado con mensaje informativo (no error)
2568
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
2569
+ // Continuar con la configuración de cámara
2570
+ this.continueWithCameraSetup();
2571
+ }
2572
+ async continueWithCameraSetup() {
2573
+ try {
2574
+ // Paso 2: Detectar y configurar dispositivos
2575
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
2576
+ try {
2577
+ await this.cameraService.enumerateDevices();
2578
+ }
2579
+ catch (enumerateError) {
2580
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
2581
+ }
2582
+ try {
2583
+ await this.updateCameraInfoWithAutofocus();
2584
+ }
2585
+ catch (cameraInfoError) {
2586
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
2587
+ }
2588
+ // Paso 3: Configurar cámara seleccionada
2589
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
2590
+ let stream;
2591
+ try {
2592
+ stream = await this.cameraService.setupCamera();
2593
+ }
2594
+ catch (setupError) {
2595
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
2596
+ }
2597
+ // Paso 4: Inicializar video
2598
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
2599
+ try {
2600
+ await this.initializeVideoStream(stream);
2601
+ }
2602
+ catch (videoError) {
2603
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
2604
+ }
2605
+ // Paso 5: Calibrar máscara
2606
+ if (this.detectionContainer) {
2607
+ const container = this.detectionContainer.parentElement;
2608
+ const rect = container.getBoundingClientRect();
2609
+ this.updateMaskDimensions(rect);
2610
+ }
2611
+ // Finalizar
2612
+ this.startTime = Date.now();
2613
+ this.stateManager.updateCaptureState({ isLoading: false });
2614
+ // Mostrar botón manual (ya debe estar habilitado)
2615
+ this.showManualCaptureButton = true;
2616
+ }
2617
+ catch (error) {
2618
+ this.updateStatus('Error de cámara', error.message, 'error');
2619
+ throw error;
2620
+ }
2621
+ }
2622
+ switchToManualModeWithError(errorMessage) {
2623
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2624
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2625
+ return;
2626
+ }
2627
+ // Liberar recursos de ONNX inmediatamente
2628
+ this.releaseOnnxResources();
2629
+ // Resetear máscara a blanco y limpiar detecciones
2630
+ this.resetMaskToWhite();
2631
+ this.detectionBoxes = [];
2632
+ // Cambiar a modo manual
2633
+ this.hasAutoSwitchedToManual = true;
2634
+ this.performanceDegradedMode = true;
2635
+ this.showManualCaptureButton = true;
2636
+ this.useDocumentDetector = false; // Desactivar detector automático
2637
+ // Actualizar el estado con mensaje de error
2638
+ this.updateStatus('Modo manual activado', errorMessage, 'error');
2639
+ // Continuar con la configuración de cámara para mostrar el video
2640
+ this.continueWithCameraSetup().catch(() => {
2641
+ // Si falla la configuración de cámara, mantener el mensaje de error pero cambiar el estado
2642
+ setTimeout(() => {
2643
+ this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
2644
+ }, 5000);
2645
+ });
2646
+ }
2647
+ stopPerformanceMonitoring() {
2648
+ // Nota: No necesitamos limpiar más variables porque ya no se usarán
2649
+ // El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
2650
+ }
2651
+ // Método para liberar recursos de ONNX cuando se cambia a modo manual
2652
+ releaseOnnxResources() {
2653
+ try {
2654
+ // Liberar recursos del servicio de detección
2655
+ if (this.detectionService) {
2656
+ this.detectionService.cleanup();
2657
+ }
2658
+ }
2659
+ catch (error) {
2660
+ }
2661
+ }
2219
2662
  completeProcess(skippedBack = false) {
2220
2663
  this.stateManager.updateCaptureState({
2221
2664
  step: 'completed',
@@ -2286,6 +2729,12 @@ const JaakStamps = class {
2286
2729
  try {
2287
2730
  // Close the selector immediately when user selects a camera
2288
2731
  this.showCameraSelector = false;
2732
+ // Check if user selected the same camera that's already active
2733
+ const currentCameraId = this.cameraService.getSelectedCameraId();
2734
+ if (currentCameraId === cameraId) {
2735
+ // Same camera selected, just close the selector without switching
2736
+ return;
2737
+ }
2289
2738
  this.isSwitchingCamera = true;
2290
2739
  // Stop current video stream
2291
2740
  if (this.videoStream) {
@@ -2329,6 +2778,13 @@ const JaakStamps = class {
2329
2778
  this.detectionBoxes = [];
2330
2779
  this.alignmentStartTime = undefined;
2331
2780
  this.hasDocumentDetected = false;
2781
+ // Reset sistema simplificado
2782
+ this.processedFramesCount = 0;
2783
+ this.slowFrameCount = 0;
2784
+ // Reset performance degradation state
2785
+ this.hasAutoSwitchedToManual = false;
2786
+ this.performanceDegradedMode = false;
2787
+ this.showPerformanceMessage = false;
2332
2788
  if (this.alignmentTimer) {
2333
2789
  clearTimeout(this.alignmentTimer);
2334
2790
  this.alignmentTimer = undefined;
@@ -2432,6 +2888,24 @@ const JaakStamps = class {
2432
2888
  // Performance is good, use base frame skip
2433
2889
  return this.BASE_FRAME_SKIP;
2434
2890
  }
2891
+ // Método para resetear la máscara a color blanco
2892
+ resetMaskToWhite() {
2893
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
2894
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
2895
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
2896
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
2897
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
2898
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
2899
+ // Remover todas las clases de alineación y estado perfecto
2900
+ cardOutline?.classList.remove('perfect-match');
2901
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
2902
+ topSide?.classList.remove('aligned');
2903
+ rightSide?.classList.remove('aligned');
2904
+ bottomSide?.classList.remove('aligned');
2905
+ leftSide?.classList.remove('aligned');
2906
+ // Resetear estado de alineación
2907
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
2908
+ }
2435
2909
  // Canvas pool management for screenshots
2436
2910
  getPooledCanvas(width, height) {
2437
2911
  // Try to find a canvas with matching dimensions