@jaak.ai/stamps 2.0.0-dev.58 → 2.1.0-beta.1

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 (56) hide show
  1. package/README.md +11 -1
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  3. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  4. package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
  5. package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
  6. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
  7. package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
  8. package/dist/cjs/jaak-stamps.cjs.entry.js +644 -60
  9. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  10. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  11. package/dist/cjs/loader.cjs.js +4 -3
  12. package/dist/cjs/loader.cjs.js.map +1 -1
  13. package/dist/collection/collection-manifest.json +1 -1
  14. package/dist/collection/components/my-component/my-component.css +211 -12
  15. package/dist/collection/components/my-component/my-component.js +618 -58
  16. package/dist/collection/components/my-component/my-component.js.map +1 -1
  17. package/dist/collection/services/DetectionService.js +48 -5
  18. package/dist/collection/services/DetectionService.js.map +1 -1
  19. package/dist/collection/services/ServiceContainer.js +3 -2
  20. package/dist/collection/services/ServiceContainer.js.map +1 -1
  21. package/dist/components/index.js +165 -113
  22. package/dist/components/index.js.map +1 -1
  23. package/dist/components/jaak-stamps.js +650 -60
  24. package/dist/components/jaak-stamps.js.map +1 -1
  25. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  26. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  27. package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
  28. package/dist/esm/index-Drbzcuq-.js.map +1 -0
  29. package/dist/esm/jaak-stamps-webcomponent.js +5 -4
  30. package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
  31. package/dist/esm/jaak-stamps.entry.js +644 -60
  32. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  33. package/dist/esm/loader.js +4 -3
  34. package/dist/esm/loader.js.map +1 -1
  35. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  36. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
  37. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  38. package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
  39. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
  40. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
  41. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
  42. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
  43. package/dist/jaak-stamps-webcomponent/p-a76ae84d.entry.js +2 -0
  44. package/dist/jaak-stamps-webcomponent/p-a76ae84d.entry.js.map +1 -0
  45. package/dist/types/components/my-component/my-component.d.ts +25 -0
  46. package/dist/types/components.d.ts +8 -0
  47. package/dist/types/services/DetectionService.d.ts +2 -1
  48. package/dist/types/services/ServiceContainer.d.ts +1 -0
  49. package/dist/types/stencil-public-runtime.d.ts +12 -5
  50. package/package.json +3 -1
  51. package/dist/cjs/index-BfhtOB0D.js.map +0 -1
  52. package/dist/esm/index-BP1Q4KOg.js.map +0 -1
  53. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js +0 -2
  54. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js.map +0 -1
  55. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
  56. 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();
@@ -630,6 +630,7 @@ class DeviceStrategyFactory {
630
630
  class DetectionService {
631
631
  debug;
632
632
  useDocumentClassification;
633
+ useDocumentDetector;
633
634
  session;
634
635
  mobileNetSession;
635
636
  mobileNetClassMap;
@@ -647,9 +648,10 @@ class DetectionService {
647
648
  // Static canvas pool for reuse across instances
648
649
  static canvasPool = new Map();
649
650
  static MAX_POOL_SIZE = 5;
650
- constructor(debug = false, useDocumentClassification = false) {
651
+ constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
651
652
  this.debug = debug;
652
653
  this.useDocumentClassification = useDocumentClassification;
654
+ this.useDocumentDetector = useDocumentDetector;
653
655
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
654
656
  this.initializeCanvasPool();
655
657
  }
@@ -657,13 +659,23 @@ class DetectionService {
657
659
  if (this.modelLoaded || this.session) {
658
660
  return;
659
661
  }
662
+ if (!this.useDocumentDetector) {
663
+ // Skip loading the detection model but mark as "loaded" for compatibility
664
+ this.modelLoaded = true;
665
+ return;
666
+ }
660
667
  try {
661
668
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
662
669
  try {
663
670
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
664
671
  }
665
672
  catch (error) {
666
- if (error.message.includes('failed to allocate a buffer')) {
673
+ // Múltiples tipos de errores de memoria que pueden ocurrir
674
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
675
+ error.message.includes('Out of memory') ||
676
+ error.message.includes('RangeError: Out of memory') ||
677
+ error.message.includes('no available backend found');
678
+ if (isMemoryError) {
667
679
  const fallbackOptions = {
668
680
  executionProviders: ['wasm'],
669
681
  graphOptimizationLevel: 'disabled',
@@ -673,8 +685,17 @@ class DetectionService {
673
685
  executionMode: 'sequential',
674
686
  interOpNumThreads: 1,
675
687
  intraOpNumThreads: 1,
688
+ // Configuraciones adicionales para dispositivos con poca memoria
689
+ enableProfiling: false,
690
+ sessionLogSeverityLevel: 4,
691
+ sessionLogVerbosityLevel: 0,
676
692
  };
677
- this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
693
+ try {
694
+ this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
695
+ }
696
+ catch (fallbackError) {
697
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
698
+ }
678
699
  }
679
700
  else {
680
701
  throw error;
@@ -714,7 +735,12 @@ class DetectionService {
714
735
  this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
715
736
  }
716
737
  catch (error) {
717
- if (error.message.includes('failed to allocate a buffer')) {
738
+ // Múltiples tipos de errores de memoria que pueden ocurrir
739
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
740
+ error.message.includes('Out of memory') ||
741
+ error.message.includes('RangeError: Out of memory') ||
742
+ error.message.includes('no available backend found');
743
+ if (isMemoryError) {
718
744
  const fallbackOptions = {
719
745
  executionProviders: ['wasm'],
720
746
  graphOptimizationLevel: 'disabled',
@@ -724,8 +750,17 @@ class DetectionService {
724
750
  executionMode: 'sequential',
725
751
  interOpNumThreads: 1,
726
752
  intraOpNumThreads: 1,
753
+ // Configuraciones adicionales para dispositivos con poca memoria
754
+ enableProfiling: false,
755
+ sessionLogSeverityLevel: 4,
756
+ sessionLogVerbosityLevel: 0,
727
757
  };
728
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
758
+ try {
759
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
760
+ }
761
+ catch (fallbackError) {
762
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
763
+ }
729
764
  }
730
765
  else {
731
766
  throw error;
@@ -737,6 +772,10 @@ class DetectionService {
737
772
  }
738
773
  }
739
774
  isModelLoaded() {
775
+ if (!this.useDocumentDetector) {
776
+ // If detector is disabled, consider it "loaded" for compatibility
777
+ return this.modelLoaded;
778
+ }
740
779
  return this.modelLoaded && !!this.session;
741
780
  }
742
781
  preprocess(video) {
@@ -761,6 +800,10 @@ class DetectionService {
761
800
  return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
762
801
  }
763
802
  async runInference(inputTensor) {
803
+ if (!this.useDocumentDetector) {
804
+ // Return empty array when document detector is disabled
805
+ return [];
806
+ }
764
807
  if (!this.session) {
765
808
  throw new Error('Detection model not loaded');
766
809
  }
@@ -1038,7 +1081,7 @@ class ServiceContainer {
1038
1081
  const eventBus = new EventBusService();
1039
1082
  const stateManager = new StateManagerService(eventBus);
1040
1083
  const cameraService = new CameraService(eventBus, config.preferredCamera);
1041
- const detectionService = new DetectionService(config.debug, config.useDocumentClassification);
1084
+ const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
1042
1085
  // Register services
1043
1086
  this.services.set('eventBus', eventBus);
1044
1087
  this.services.set('stateManager', stateManager);
@@ -1074,7 +1117,7 @@ class ServiceContainer {
1074
1117
  }
1075
1118
  }
1076
1119
 
1077
- const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#fff;font-size:14px;font-weight:600;text-align:center;white-space:normal;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);padding:12px 20px;border-radius:8px;max-width:300px;z-index:20;border:1px solid rgba(255, 255, 255, 0.1)}.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}.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}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content: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}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}";
1120
+ 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}}";
1078
1121
 
1079
1122
  const JaakStamps = class {
1080
1123
  constructor(hostRef) {
@@ -1088,6 +1131,7 @@ const JaakStamps = class {
1088
1131
  maskSize = 90;
1089
1132
  cropMargin = 20;
1090
1133
  useDocumentClassification = false;
1134
+ useDocumentDetector = true;
1091
1135
  preferredCamera = 'auto';
1092
1136
  captureDelay = 1500;
1093
1137
  enableBackDocumentTimer = false;
@@ -1128,6 +1172,11 @@ const JaakStamps = class {
1128
1172
  detectionRate: 0
1129
1173
  };
1130
1174
  backDocumentTimerRemaining = 0;
1175
+ showManualCaptureButton = false;
1176
+ performanceDegradedMode = false; // Auto-switched to manual mode due to performance
1177
+ showPerformanceMessage = false; // Show performance message temporarily
1178
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
1179
+ processingButton = null; // Track which button is processing
1131
1180
  // Services
1132
1181
  serviceContainer;
1133
1182
  eventBus;
@@ -1146,12 +1195,12 @@ const JaakStamps = class {
1146
1195
  alignmentStartTime;
1147
1196
  alignmentTimer;
1148
1197
  backDocumentTimer;
1198
+ orientationTimer;
1149
1199
  // Performance monitoring
1150
1200
  performanceMetrics = {
1151
1201
  fps: 0,
1152
1202
  inferenceTime: 0,
1153
1203
  memoryUsage: 0,
1154
- cpuUsage: 0,
1155
1204
  onnxLoadTime: 0,
1156
1205
  frameProcessingTime: 0,
1157
1206
  totalDetections: 0,
@@ -1170,18 +1219,29 @@ const JaakStamps = class {
1170
1219
  MIN_INFERENCE_INTERVAL = 50;
1171
1220
  performanceHistory = [];
1172
1221
  PERFORMANCE_HISTORY_SIZE = 10;
1222
+ // Sistema simplificado de monitoreo de rendimiento
1223
+ PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
1224
+ SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
1225
+ slowFrameCount = 0; // Contador de frames consecutivos lentos
1226
+ processedFramesCount = 0; // Contador total de frames procesados
1227
+ hasAutoSwitchedToManual = false;
1173
1228
  // Canvas pool for optimized screenshot capture
1174
1229
  canvasPool = [];
1175
1230
  MAX_CANVAS_POOL_SIZE = 3;
1176
1231
  async componentDidLoad() {
1177
- this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1178
- await this.initializeServices();
1179
- this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1180
- await this.setupEventListeners();
1181
- this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1182
- await this.initializeComponent();
1183
- if (this.debug) {
1184
- this.initializePerformanceMonitor();
1232
+ try {
1233
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1234
+ await this.initializeServices();
1235
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1236
+ await this.setupEventListeners();
1237
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1238
+ await this.initializeComponent();
1239
+ if (this.debug) {
1240
+ this.initializePerformanceMonitor();
1241
+ }
1242
+ }
1243
+ catch (error) {
1244
+ this.updateStatus('Error de inicialización', error.message, 'error');
1185
1245
  }
1186
1246
  }
1187
1247
  async initializeServices() {
@@ -1191,6 +1251,7 @@ const JaakStamps = class {
1191
1251
  maskSize: this.maskSize,
1192
1252
  cropMargin: this.cropMargin,
1193
1253
  useDocumentClassification: this.useDocumentClassification,
1254
+ useDocumentDetector: this.useDocumentDetector,
1194
1255
  preferredCamera: this.preferredCamera,
1195
1256
  captureDelay: this.captureDelay
1196
1257
  };
@@ -1201,14 +1262,8 @@ const JaakStamps = class {
1201
1262
  this.detectionService = this.serviceContainer.getDetectionService();
1202
1263
  }
1203
1264
  async setupEventListeners() {
1204
- this.eventBus.on('state-changed', (data) => {
1205
- this.handleStateChange(data);
1206
- });
1207
- this.eventBus.on('camera-changed', () => {
1208
- // Camera changed event
1209
- });
1210
- this.eventBus.on('error', () => {
1211
- // Handle service errors
1265
+ this.eventBus.on('state-changed', () => {
1266
+ this.handleStateChange();
1212
1267
  });
1213
1268
  }
1214
1269
  async initializeComponent() {
@@ -1248,27 +1303,39 @@ const JaakStamps = class {
1248
1303
  document.head.appendChild(script);
1249
1304
  await new Promise((resolve) => {
1250
1305
  script.onload = () => {
1251
- setTimeout(() => {
1252
- this.finalizeInitialization();
1306
+ setTimeout(async () => {
1307
+ await this.finalizeInitialization();
1253
1308
  resolve(undefined);
1254
1309
  }, 300);
1255
1310
  };
1256
1311
  });
1257
1312
  }
1258
1313
  else {
1259
- setTimeout(() => {
1260
- this.finalizeInitialization();
1314
+ setTimeout(async () => {
1315
+ await this.finalizeInitialization();
1261
1316
  }, 300);
1262
1317
  }
1263
1318
  }
1264
- finalizeInitialization() {
1319
+ async finalizeInitialization() {
1265
1320
  this.stateManager.updateCaptureState({ isLoading: false });
1266
- this.currentStatus = {
1267
- message: 'Listo para capturar',
1268
- description: '',
1269
- type: 'ready',
1270
- isInitialized: true
1271
- };
1321
+ // Check camera permissions before showing "ready" status
1322
+ const hasPermissions = await this.checkCameraPermissions();
1323
+ if (hasPermissions) {
1324
+ this.currentStatus = {
1325
+ message: 'Listo para capturar',
1326
+ description: '',
1327
+ type: 'ready',
1328
+ isInitialized: true
1329
+ };
1330
+ }
1331
+ else {
1332
+ this.currentStatus = {
1333
+ message: 'Permisos de cámara requeridos',
1334
+ description: 'Por favor, otorgue permisos de cámara para continuar',
1335
+ type: 'error',
1336
+ isInitialized: true
1337
+ };
1338
+ }
1272
1339
  this.emitReadyEvent();
1273
1340
  }
1274
1341
  updateStatus(message, description, type = 'loading') {
@@ -1283,7 +1350,28 @@ const JaakStamps = class {
1283
1350
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
1284
1351
  this.isReady.emit(isDocumentReady);
1285
1352
  }
1286
- handleStateChange(data) {
1353
+ async checkCameraPermissions() {
1354
+ try {
1355
+ if (!navigator.permissions) {
1356
+ // Fallback: try to access camera directly
1357
+ try {
1358
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1359
+ stream.getTracks().forEach(track => track.stop());
1360
+ return true;
1361
+ }
1362
+ catch {
1363
+ return false;
1364
+ }
1365
+ }
1366
+ const permission = await navigator.permissions.query({ name: 'camera' });
1367
+ return permission.state === 'granted';
1368
+ }
1369
+ catch (error) {
1370
+ return false;
1371
+ }
1372
+ }
1373
+ handleStateChange() {
1374
+ // Handle state changes from StateManager if needed
1287
1375
  }
1288
1376
  initializeResizeObserver() {
1289
1377
  if ('ResizeObserver' in window && this.detectionContainer) {
@@ -1292,6 +1380,18 @@ const JaakStamps = class {
1292
1380
  });
1293
1381
  resizeObserver.observe(this.detectionContainer.parentElement);
1294
1382
  }
1383
+ // Handle mobile device orientation changes
1384
+ window.addEventListener('orientationchange', () => {
1385
+ // Clear any existing orientation timer
1386
+ if (this.orientationTimer) {
1387
+ clearTimeout(this.orientationTimer);
1388
+ }
1389
+ // Add delay to ensure video dimensions are updated after rotation
1390
+ this.orientationTimer = window.setTimeout(() => {
1391
+ this.handleResize();
1392
+ this.orientationTimer = undefined;
1393
+ }, 300);
1394
+ });
1295
1395
  }
1296
1396
  handleResize() {
1297
1397
  if (this.detectionContainer) {
@@ -1300,6 +1400,36 @@ const JaakStamps = class {
1300
1400
  this.updateMaskDimensions(rect);
1301
1401
  }
1302
1402
  }
1403
+ triggerRerender() {
1404
+ this.captureStateVersion = this.captureStateVersion + 1;
1405
+ }
1406
+ getGuideText(step) {
1407
+ if (step === 'completed') {
1408
+ return 'Proceso completado';
1409
+ }
1410
+ // Check if we should show performance message
1411
+ if (this.showPerformanceMessage && step === 'front') {
1412
+ return 'Modo manual activado por rendimiento. Use el botón para capturar.';
1413
+ }
1414
+ if (step === 'front') {
1415
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
1416
+ if (this.showManualCaptureButton) {
1417
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
1418
+ }
1419
+ // Instrucción básica de alineación
1420
+ return 'Alinee su identificación con el marco';
1421
+ }
1422
+ else if (step === 'back') {
1423
+ // Para el reverso, mostrar instrucciones específicas según el modo
1424
+ if (this.useDocumentDetector) {
1425
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
1426
+ }
1427
+ else {
1428
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
1429
+ }
1430
+ }
1431
+ return 'Alinee su identificación con el marco';
1432
+ }
1303
1433
  updateMaskDimensions(containerRect) {
1304
1434
  if (!this.videoRef)
1305
1435
  return;
@@ -1438,8 +1568,14 @@ const JaakStamps = class {
1438
1568
  }
1439
1569
  }
1440
1570
  async skipBackCapture() {
1571
+ console.log('🟡 Botón clicked: Saltar reverso');
1572
+ // Set processing state immediately
1573
+ this.processingButton = 'skip-back';
1574
+ // Add small delay to show spinner
1575
+ await new Promise(resolve => setTimeout(resolve, 100));
1441
1576
  const readyCheck = this.isComponentReady();
1442
1577
  if (!readyCheck.ready) {
1578
+ this.processingButton = null;
1443
1579
  return { success: false, error: readyCheck.message };
1444
1580
  }
1445
1581
  try {
@@ -1456,6 +1592,10 @@ const JaakStamps = class {
1456
1592
  catch (error) {
1457
1593
  return { success: false, error: error.message };
1458
1594
  }
1595
+ finally {
1596
+ // Always clear processing state when done
1597
+ this.processingButton = null;
1598
+ }
1459
1599
  }
1460
1600
  async getStatus() {
1461
1601
  const readyCheck = this.isComponentReady();
@@ -1510,9 +1650,21 @@ const JaakStamps = class {
1510
1650
  if (this.stateManager) {
1511
1651
  this.stateManager.updateCaptureState({ isLoading: true });
1512
1652
  }
1513
- await this.detectionService.loadModel();
1653
+ try {
1654
+ await this.detectionService.loadModel();
1655
+ }
1656
+ catch (modelError) {
1657
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1658
+ throw modelError;
1659
+ }
1514
1660
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1515
- await this.detectionService.loadClassificationModel();
1661
+ try {
1662
+ await this.detectionService.loadClassificationModel();
1663
+ }
1664
+ catch (classificationError) {
1665
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1666
+ throw classificationError;
1667
+ }
1516
1668
  const loadEndTime = performance.now();
1517
1669
  const loadTime = loadEndTime - loadStartTime;
1518
1670
  // Record ONNX load time
@@ -1621,17 +1773,63 @@ const JaakStamps = class {
1621
1773
  // Always allow getting capture delay, even during initialization
1622
1774
  return this.captureDelay;
1623
1775
  }
1776
+ // Memory and device capability detection
1777
+ checkDeviceCapabilities() {
1778
+ const deviceMemory = navigator.deviceMemory;
1779
+ // Siempre intentar usar ML, independientemente del dispositivo
1780
+ // Las restricciones anteriores no eran confiables
1781
+ return {
1782
+ canUseML: true,
1783
+ reason: 'Dispositivo compatible con detección automática.',
1784
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
1785
+ };
1786
+ }
1624
1787
  // DETECTION METHODS
1625
1788
  async startDetection() {
1626
1789
  try {
1790
+ // Verificar capacidades del dispositivo antes de cargar modelos
1791
+ const capabilities = this.checkDeviceCapabilities();
1792
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
1793
+ if (!capabilities.canUseML && this.useDocumentDetector) {
1794
+ this.switchToManualModeWithWarning(capabilities.reason);
1795
+ return;
1796
+ }
1627
1797
  // Paso 1: Verificar y cargar modelos si es necesario
1628
1798
  if (!this.detectionService.isModelLoaded()) {
1629
1799
  const loadStartTime = performance.now();
1630
1800
  this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1631
1801
  this.stateManager.updateCaptureState({ isLoading: true });
1632
- await this.detectionService.loadModel();
1802
+ try {
1803
+ await this.detectionService.loadModel();
1804
+ }
1805
+ catch (modelError) {
1806
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1807
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
1808
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1809
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
1810
+ return;
1811
+ }
1812
+ else {
1813
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1814
+ throw modelError;
1815
+ }
1816
+ }
1633
1817
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1634
- await this.detectionService.loadClassificationModel();
1818
+ try {
1819
+ await this.detectionService.loadClassificationModel();
1820
+ }
1821
+ catch (classificationError) {
1822
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1823
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
1824
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1825
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
1826
+ return;
1827
+ }
1828
+ else {
1829
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1830
+ throw classificationError;
1831
+ }
1832
+ }
1635
1833
  const loadEndTime = performance.now();
1636
1834
  const loadTime = loadEndTime - loadStartTime;
1637
1835
  // Record ONNX load time
@@ -1641,14 +1839,40 @@ const JaakStamps = class {
1641
1839
  }
1642
1840
  // Paso 2: Detectar y configurar dispositivos
1643
1841
  this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1644
- await this.cameraService.enumerateDevices();
1645
- await this.updateCameraInfoWithAutofocus();
1842
+ try {
1843
+ await this.cameraService.enumerateDevices();
1844
+ }
1845
+ catch (enumerateError) {
1846
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
1847
+ }
1848
+ try {
1849
+ await this.updateCameraInfoWithAutofocus();
1850
+ }
1851
+ catch (cameraInfoError) {
1852
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
1853
+ }
1646
1854
  // Paso 3: Configurar cámara seleccionada
1647
1855
  this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1648
- const stream = await this.cameraService.setupCamera();
1649
- // Paso 4: Inicializar video y ocultar estado inmediatamente
1650
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
1651
- await this.initializeVideoStream(stream);
1856
+ let stream;
1857
+ try {
1858
+ stream = await this.cameraService.setupCamera();
1859
+ }
1860
+ catch (setupError) {
1861
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
1862
+ }
1863
+ // Paso 4: Inicializar video y mostrar estado de análisis inicial
1864
+ if (this.useDocumentDetector) {
1865
+ this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
1866
+ }
1867
+ else {
1868
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
1869
+ }
1870
+ try {
1871
+ await this.initializeVideoStream(stream);
1872
+ }
1873
+ catch (videoError) {
1874
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
1875
+ }
1652
1876
  // Paso 5: Calibrar máscara
1653
1877
  if (this.detectionContainer) {
1654
1878
  const container = this.detectionContainer.parentElement;
@@ -1658,6 +1882,10 @@ const JaakStamps = class {
1658
1882
  // Finalizar e iniciar captura
1659
1883
  this.startTime = Date.now();
1660
1884
  this.stateManager.updateCaptureState({ isLoading: false });
1885
+ // Mostrar botón manual si el detector está deshabilitado
1886
+ if (!this.useDocumentDetector) {
1887
+ this.showManualCaptureButton = true;
1888
+ }
1661
1889
  this.detectFrame();
1662
1890
  }
1663
1891
  catch (err) {
@@ -1675,6 +1903,12 @@ const JaakStamps = class {
1675
1903
  this.videoRef.onloadedmetadata = async () => {
1676
1904
  await this.videoRef.play();
1677
1905
  this.stateManager.updateCaptureState({ isVideoActive: true });
1906
+ // Recalculate mask dimensions when new camera stream loads
1907
+ if (this.detectionContainer) {
1908
+ const container = this.detectionContainer.parentElement;
1909
+ const rect = container.getBoundingClientRect();
1910
+ this.updateMaskDimensions(rect);
1911
+ }
1678
1912
  resolve();
1679
1913
  };
1680
1914
  });
@@ -1689,6 +1923,18 @@ const JaakStamps = class {
1689
1923
  const captureState = this.stateManager.getCaptureState();
1690
1924
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
1691
1925
  return;
1926
+ // Show manual capture button when document detector is disabled or performance is degraded
1927
+ if (!this.useDocumentDetector || this.performanceDegradedMode) {
1928
+ this.showManualCaptureButton = true;
1929
+ // Continue the loop for UI updates but skip detection logic
1930
+ if (captureState.step !== 'completed') {
1931
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
1932
+ }
1933
+ return;
1934
+ }
1935
+ else if (!this.performanceDegradedMode) {
1936
+ this.showManualCaptureButton = false;
1937
+ }
1692
1938
  if (captureState.isDetectionPaused) {
1693
1939
  if (captureState.step !== 'completed') {
1694
1940
  this.animationId = requestAnimationFrame(() => this.detectFrame());
@@ -1716,10 +1962,27 @@ const JaakStamps = class {
1716
1962
  this.lastInferenceTime = currentTime;
1717
1963
  // Measure preprocessing and inference time
1718
1964
  const inferenceStartTime = performance.now();
1719
- const inputTensor = this.detectionService.preprocess(this.videoRef);
1720
- // Standard detection without quality validation
1721
- const detections = await this.detectionService.runInference(inputTensor);
1722
- const inferenceTime = performance.now() - inferenceStartTime;
1965
+ let inputTensor;
1966
+ let detections;
1967
+ let inferenceTime;
1968
+ try {
1969
+ inputTensor = this.detectionService.preprocess(this.videoRef);
1970
+ }
1971
+ catch (preprocessError) {
1972
+ this.switchToManualModeWithError('Error al procesar imagen');
1973
+ return;
1974
+ }
1975
+ try {
1976
+ // Standard detection without timeout (timeout is handled by frame counting)
1977
+ detections = await this.detectionService.runInference(inputTensor);
1978
+ inferenceTime = performance.now() - inferenceStartTime;
1979
+ }
1980
+ catch (inferenceError) {
1981
+ this.switchToManualModeWithError('Error en detección automática');
1982
+ return;
1983
+ }
1984
+ // Increment frame count
1985
+ this.processedFramesCount++;
1723
1986
  // Record ONNX performance metrics
1724
1987
  if (this.debug) {
1725
1988
  this.recordOnnxPerformance(0, inferenceTime);
@@ -1758,9 +2021,30 @@ const JaakStamps = class {
1758
2021
  }
1759
2022
  this.updateMaskColor(detections);
1760
2023
  // Record frame processing metrics
2024
+ const frameEndTime = performance.now();
2025
+ const totalFrameTime = frameEndTime - frameStartTime;
2026
+ // Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
2027
+ if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
2028
+ // Verificar si el frame es lento
2029
+ if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
2030
+ this.slowFrameCount++;
2031
+ // Si tenemos suficientes frames lentos consecutivos, cambiar a manual
2032
+ if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
2033
+ this.switchToManualMode();
2034
+ }
2035
+ }
2036
+ else {
2037
+ // Frame rápido, reiniciar contador
2038
+ if (this.slowFrameCount > 0) {
2039
+ this.slowFrameCount = 0;
2040
+ }
2041
+ }
2042
+ // Cambiar mensaje de estado después de analizar los primeros 2 frames
2043
+ if (this.processedFramesCount === 2) {
2044
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
2045
+ }
2046
+ }
1761
2047
  if (this.debug) {
1762
- const frameEndTime = performance.now();
1763
- const totalFrameTime = frameEndTime - frameStartTime;
1764
2048
  this.recordFrameProcessing(totalFrameTime, detections.length);
1765
2049
  }
1766
2050
  // Continue detection loop
@@ -1796,6 +2080,12 @@ const JaakStamps = class {
1796
2080
  clearTimeout(this.alignmentTimer);
1797
2081
  this.alignmentTimer = undefined;
1798
2082
  }
2083
+ if (this.orientationTimer) {
2084
+ clearTimeout(this.orientationTimer);
2085
+ this.orientationTimer = undefined;
2086
+ }
2087
+ // Liberar recursos de ONNX al limpiar el componente
2088
+ this.releaseOnnxResources();
1799
2089
  if (this.performanceUpdateInterval) {
1800
2090
  clearInterval(this.performanceUpdateInterval);
1801
2091
  this.performanceUpdateInterval = undefined;
@@ -1816,7 +2106,7 @@ const JaakStamps = class {
1816
2106
  isCapturing: false
1817
2107
  };
1818
2108
  const cameraInfo = this.cameraInfoWithAutofocus;
1819
- return (h("div", { key: '69a8df7969288e19088e8809f1fefcc89b237cda', class: "detector-container" }, h("div", { key: '79e59a315d266be9d4fb5eed5740e5ad825d5890', class: "video-container" }, h("video", { key: 'a3312788c739748b774a25acb30c2dcea7f01695', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: 'ddab107500c2dab24c140049bef0b44aafc35dc7', 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: {
2109
+ return (h("div", { key: 'dda3e3054a18dac59fd6dab4bf722f405d1c1b0f', class: "detector-container" }, h("div", { key: 'c260bf52307e072a634cddcc273c1f55037d01ef', class: "video-container" }, h("video", { key: '5f20ce9d95c5ece7ab90a2da30f28034b86a57a0', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '6afd8a46c16592c67e6bb93f1655de1660338f30', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
1820
2110
  position: 'absolute',
1821
2111
  left: `${box.x}px`,
1822
2112
  top: `${box.y}px`,
@@ -1825,9 +2115,9 @@ const JaakStamps = class {
1825
2115
  border: '2px solid #32406C',
1826
2116
  pointerEvents: 'none',
1827
2117
  boxSizing: 'border-box'
1828
- } })))), this.isMaskReady && (h("div", { key: '370337089a3de949b8cb4ae84dc0037e6044b2d5', class: "overlay-mask" }, h("div", { key: 'cb35c2e42bf1729ad02a8fe7e7abaf27cf1a8d7a', class: "card-outline" }, h("div", { key: '637d72ada63805e7f25051cd1ad9093030cb025a', class: "side side-top" }), h("div", { key: 'c7a465aa686afa41f80433d949715b40f6f349dc', class: "side side-right" }), h("div", { key: '384aade0610b5bc7bdd7db350a70d82f9bebd8ab', class: "side side-bottom" }), h("div", { key: '5506cc346f3fd718e3fda4365f69ea780304b6d6', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'f5b9b62937eabab7c096b7417a13bbf6f822ee8a', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '74b122125a42839d2600cf99fb855140042ab761', class: "skip-section" }, h("div", { key: '0b4a39d1599854d6c445e2c35dcc89a0e4970846', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: 'b52b47df390cc099c8847ca8a676270bb154294d', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
2118
+ } })))), this.isMaskReady && (h("div", { key: '178f2022bed334f8c9c8c866a92b44c8cb0369b1', class: "overlay-mask" }, h("div", { key: 'acb1bc996409095afd29ffa5a038c3eeae6c7ed9', class: "card-outline" }, h("div", { key: 'dcc952d9011c5fd13b229f3a759cc74721a1ee11', class: "side side-top" }), h("div", { key: 'addb25f6c41ad10a09b6918e6357052294d14adf', class: "side side-right" }), h("div", { key: 'a1ec08f3bf1b16731a224c034c76dd8c6412597b', class: "side side-bottom" }), h("div", { key: '22f3c158f2c2bb1dc582f796c39ea60bc11505dd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '5b150f7df5b0c5402e42bae4601936d4e5d1f5d4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '17b0a81437eb8691cb4dc5042c72bda01ac92be0', class: "back-capture-section" }, h("div", { key: 'a65ccbd5f53b06ec340b36a0a2348cf04920b3df', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (h("button", { key: '383d7398079dbb8b1adfe695407cc598059b2a94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (h("span", { key: '86bddcfeb6f90da9b18926f4c6e1208296502dcf', class: "button-spinner" })), "Capturar Reverso")), h("button", { key: '5c9d116cbb9b5bc16481b2b90c8451182ad436c5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (h("span", { key: '881f00cfc3a24167bc6b8da5849cb404101b692d', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1829
2119
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
1830
- : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'f04e47749a25197c434b849844c67eadca5c6af3', class: "camera-controls" }, h("button", { key: '82625eef057f59b6dac11c4443ccb315231eac7f', 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: '61cfb89cd9bb549cfff2cb70293db06ee6cdf06b', class: "camera-selector-dropdown" }, h("div", { key: '60f751d8db7b305f2bdfdc089d2110bcff940150', class: "camera-selector-header" }, h("span", { key: '9dae045a34cc06f323ff4850921b495fc845be47' }, "Seleccionar C\u00E1mara"), h("button", { key: '4584d7964d91ddd7f3d980c3e5e90493530d33bd', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '9881f2b4fc6ffbdc76db06a7b49d060cefbe6277', 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: '3dfe9ffc7b4cc9afb199a78026cef6d4b469805e', class: "device-info" }, h("small", { key: 'bddc865a30469bd2724624b2f7c8f641f9d97a07' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2fd963e44f69f80d17c6bbbfa3c9b5e11bc816be', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'ba4872ea5388a0ac3134be61feff5412373465ab', class: "flip-animation" }, h("div", { key: 'e83a92cea79f3e85aefc6964c5d9de7b495d511e', class: "id-card-icon" }), h("div", { key: '1d06b51fd6ad14d9b37aa6bca72bd13c951a386a', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '503c377896d5fdc3ae8476b09f511d571f23ae72', class: "success-animation" }, h("div", { key: 'e1929c245b0d1479a6fb1513b0b37c53070c4291', class: "check-icon" }), h("div", { key: '024c897c70427b205390770fc3dfb999ffcc84d3', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '54e350707628e5c88ae2900e412e6bda2eace943', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '82ea10569ed46285686cf3e63885ca1941bb559c', class: "status-spinner" })), h("div", { key: '1fecf179c10965cccf1be3dc0a54afd377ba38ad', class: "status-content" }, h("div", { key: '1b301c1bbb02e5cac2e6babcb20c190662d45c68', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: '5b6c61308d17176daade02c951a436d114a74100', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'b8104db64208dc87a617a072957c007f28055194', class: "performance-monitor" }, h("div", { key: 'e66aec071f10821be2713858dcc251c04f480e63', class: "performance-expanded" }, h("div", { key: '1ae09a91d0704ab21b9eb51097cf452fd5f44359', class: "metrics-row" }, h("div", { key: '340d4d16d048a5217fe13d5de63618c9ea221165', class: "metric-compact" }, h("span", { key: '20cfda14969ef70d232edd68f2e9902bd44b5d9a', class: "metric-label" }, "FPS"), h("span", { key: '3a089c2814a71ed6513bf05b0120a66aa159daee', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '23f700def988c3220fc5df37aeb57a0a20ff56b3', class: "metric-compact" }, h("span", { key: '441e8490a871469813a6d832114e03583d375e91', class: "metric-label" }, "MEM"), h("span", { key: '51f7c49164c9d5e699141c92c88375fc99b312ed', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '6547fee80e2a1db7c9ff889a1c478fc8248159e6', class: "metrics-row" }, h("div", { key: '7b2d1642fd122ee0fbed36387afb23152afe10c2', class: "metric-compact" }, h("span", { key: 'a9137119bf7da366f18feaf8f28807ee40ad7a25', class: "metric-label" }, "INF"), h("span", { key: '5cbc0c040102688f1039cdcd4bb58020248cf700', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'cb4a86f5da46e4081d997c8b8bb6a899f06dfcd2', class: "metric-compact" }, h("span", { key: '88ad5e1d842eb156cf6813ecaa9d2bec76a5999e', class: "metric-label" }, "FRAME"), h("span", { key: 'a9b14cd98b64dcfc058d3b68a3c168a9e092fb3c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '86e01f3596682cb8ad2ff5464748e6ecb7db374a', class: "metrics-row" }, h("div", { key: 'e73b9d39e9bcbc5d46fa0ba0a737700e3e5566d9', class: "metric-compact" }, h("span", { key: '8ecfe969dfe77c87d9da76d7390706014b4872f3', class: "metric-label" }, "DET"), h("span", { key: 'bd1a6a52e0bd1fb4d377413d7515e4e0f00da10f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '36539ae98787fd60642082db1bf89031003c4bef', class: "metric-compact" }, h("span", { key: '7f3a81629c09053e3beeca97f485da0f60412052', class: "metric-label" }, "RATE"), h("span", { key: '72399e0ac39270c82b265ce64e402f84cda9fb2c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '4e3a914ba2f5993f85de7b9dd6dc93d76b41c96d', class: "watermark" }, h("img", { key: 'aa634c2ba2090f64ee9b41aa1d75a9114d46fd63', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
2120
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'd929fc0eeeeee944f3d722fde98c52642443e9a5', class: "camera-controls" }, h("button", { key: '1afcfae9d4a23aaede153f27ba6895a33182ada1', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: 'f37feefd9def935417467dffb52b9fe8d8acea79', class: "camera-selector-dropdown" }, h("div", { key: 'a8479ec6a6c4076b5dc40677e1988295100e4d0d', class: "camera-selector-header" }, h("span", { key: '074f02967354293a1e5df4e3ac8a126faee33d04' }, "Seleccionar C\u00E1mara"), h("button", { key: 'd6ba0252fb269c3a04967fb0e5c075166f202d71', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '63f5f9be2b05da71f4fe5a4e38b11ab3e39fb1e0', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '4ca46fdebe0eaa078f1dae4d2f1af03321577c7f', class: "device-info" }, h("small", { key: '10c54ddc5a196b11a69077b045119187ed09f4be' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '25393018948798f4d9d5f44738e61c7a0a6be990', class: "manual-capture-section" }, h("button", { key: '7a6c8686c0db3e352f9689bc41f7fd3f612c800c', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (h("span", { key: '1916b97da3f8dce7d06b86b21f0361dd7ce10b95', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (h("div", { key: 'aed05a3280dc96121564ffea87945f441d4286f6', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '63d262e5e10325d145fada8e91a878c47305f2b9', class: "flip-animation" }, h("div", { key: '2a3699f47e595c5e4ab00dfd11e1e66ab2e13039', class: "id-card-icon" }), h("div", { key: 'df195fe9a6db4ec3e14b04b4e932d55adb7c3f3c', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '670341c302a31c8017a9083ebf35e2988122ac3f', class: "success-animation" }, h("div", { key: '27b0b0d91de6a882182b6ccdebecc7c440997e36', class: "check-icon" }), h("div", { key: '71ee5327faf48ea26c2ae36a8a6906c448c3a389', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '39c92aa0b82f6169058b1092894845fcba3f27b4', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '74b02ba81756f0e8bfb6e2feafdb2c44fb80afad', class: "status-spinner" })), h("div", { key: 'c7419efb2c6e2760019ecce619534e24ae3d7351', class: "status-content" }, h("div", { key: '57f315384630f17f6332eddfea628b333299e831', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'ac8632fdd68c24ed00192cb04a13026ada1f953d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'e51286a839795b18cfac2874e7e1f6e9f8e3bb4b', class: "performance-monitor" }, h("div", { key: 'f8cd289d260af9df2daf23bdfb8db70fe8bf6f45', class: "performance-expanded" }, h("div", { key: 'bf2becb2ee3ad3bffb82b0e90246a4385a19f567', class: "metrics-row" }, h("div", { key: 'f334c2b5de45df73aec6e06cd371151110789a45', class: "metric-compact" }, h("span", { key: 'cbf32f72eb3aaf1bf770d651c32123a43b81035c', class: "metric-label" }, "FPS"), h("span", { key: '9d2d2f46ba3fdb23c010f781044f00b1dd1250e6', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'f29406e8a5b00f5838a144de9fda77ce53177d43', class: "metric-compact" }, h("span", { key: '8f44e29f6134c340f0a58f8aa321612c8a9bbfb8', class: "metric-label" }, "MEM"), h("span", { key: '7a58e164d292329d95054b2679a90e031f6fe4c2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'd0284a0e65fc2879557d73fc2e03295c29f512ae', class: "metrics-row" }, h("div", { key: '78dcbce552e65c95fcd01158f3aa8916ef0e7175', class: "metric-compact" }, h("span", { key: '673cfbc8c9dabb020a3d3c18ee07558a61da9b51', class: "metric-label" }, "INF"), h("span", { key: '833761b7a875c931a8b31913930c3b132f71f618', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '324801e3e69a8de181755367201d8e8c643e8d72', class: "metric-compact" }, h("span", { key: 'f928d9b2dfbf81f5738ab08a42ebb76ff4a483de', class: "metric-label" }, "FRAME"), h("span", { key: 'dc9f2e4a1e848be4d240d89c89126203555ae203', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '7ca0a7b44719b1884334edab586267f021cf86c1', class: "metrics-row" }, h("div", { key: '262667c4320d337dae2220b77f142cf293542562', class: "metric-compact" }, h("span", { key: '578b53d251e58933f654a5afe558931e4333f022', class: "metric-label" }, "DET"), h("span", { key: '4fb36eaebebd73e5cb06d582fa3b4ece61463761', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'c17192e3964164ad603427de3befa1615b027022', class: "metric-compact" }, h("span", { key: 'd004e74797b9468e94c2af996bb62c1a3ca8d58e', class: "metric-label" }, "RATE"), h("span", { key: '0960e8760fd0ac2456a9e76598e5392c24e42d3b', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '3588fba1fe51af1335d643107ba0355aca6f4f8c', class: "watermark" }, h("img", { key: 'd53ec6b0ee9f97f602f4f8857ec605427b2ee478', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1831
2121
  }
1832
2122
  // Utility methods
1833
2123
  updateDetectionBoxes(boxes) {
@@ -1942,6 +2232,141 @@ const JaakStamps = class {
1942
2232
  }
1943
2233
  }
1944
2234
  }
2235
+ async takeManualScreenshot() {
2236
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
2237
+ // Set processing state immediately
2238
+ const captureState = this.stateManager?.getCaptureState();
2239
+ if (captureState?.step === 'front') {
2240
+ this.processingButton = 'capture-front';
2241
+ }
2242
+ else if (captureState?.step === 'back') {
2243
+ this.processingButton = 'capture-back';
2244
+ }
2245
+ console.log('🔵 processingButton set to:', this.processingButton);
2246
+ // Add small delay to show spinner
2247
+ await new Promise(resolve => setTimeout(resolve, 100));
2248
+ if (!this.videoRef) {
2249
+ this.processingButton = null;
2250
+ return;
2251
+ }
2252
+ // When using manual capture, use mask coordinates for cropping
2253
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
2254
+ await this.takeScreenshotWithMaskCoordinates();
2255
+ }
2256
+ async takeScreenshotWithMaskCoordinates() {
2257
+ if (!this.videoRef || !this.detectionContainer)
2258
+ return;
2259
+ this.stateManager.updateCaptureState({ isCapturing: true });
2260
+ this.triggerCaptureAnimation();
2261
+ // Use pooled canvas for optimization
2262
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2263
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2264
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2265
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2266
+ // Calculate mask coordinates for cropping
2267
+ const container = this.detectionContainer.parentElement;
2268
+ const containerRect = container.getBoundingClientRect();
2269
+ // Get mask dimensions from CSS properties
2270
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
2271
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
2272
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
2273
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
2274
+ // Convert percentages to actual video coordinates
2275
+ const videoWidth = this.videoRef.videoWidth;
2276
+ const videoHeight = this.videoRef.videoHeight;
2277
+ const videoAspectRatio = videoWidth / videoHeight;
2278
+ const containerAspectRatio = containerRect.width / containerRect.height;
2279
+ let displayedVideoWidth, displayedVideoHeight;
2280
+ let videoOffsetX = 0, videoOffsetY = 0;
2281
+ if (videoAspectRatio > containerAspectRatio) {
2282
+ displayedVideoWidth = containerRect.width;
2283
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
2284
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
2285
+ }
2286
+ else {
2287
+ displayedVideoHeight = containerRect.height;
2288
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
2289
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
2290
+ }
2291
+ // Calculate mask coordinates in video space
2292
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
2293
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
2294
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
2295
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
2296
+ // Convert to video coordinates
2297
+ const scaleX = videoWidth / displayedVideoWidth;
2298
+ const scaleY = videoHeight / displayedVideoHeight;
2299
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
2300
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
2301
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
2302
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
2303
+ // Calculate crop coordinates
2304
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
2305
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
2306
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
2307
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
2308
+ // Use pooled canvas for cropped version
2309
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2310
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2311
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2312
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2313
+ const captureState = this.stateManager.getCaptureState();
2314
+ if (captureState.step === 'front') {
2315
+ this.stateManager.setCapturedImages({
2316
+ front: {
2317
+ fullFrame: captureCanvas.toDataURL('image/png'),
2318
+ cropped: croppedCanvas.toDataURL('image/png')
2319
+ }
2320
+ });
2321
+ // Check if document classification is enabled (independent of detector)
2322
+ if (this.useDocumentClassification) {
2323
+ try {
2324
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2325
+ if (classification && classification.class === 'passport') {
2326
+ this.completeProcess(true);
2327
+ this.returnCanvasToPool(captureCanvas);
2328
+ this.returnCanvasToPool(croppedCanvas);
2329
+ // Clear processing button when passport is detected
2330
+ this.processingButton = null;
2331
+ return;
2332
+ }
2333
+ }
2334
+ catch (classifyError) {
2335
+ // No cambiar a manual aquí, solo registrar el error y continuar
2336
+ }
2337
+ }
2338
+ this.stateManager.updateCaptureState({
2339
+ step: 'back',
2340
+ isDetectionPaused: true,
2341
+ showFlipAnimation: true
2342
+ });
2343
+ this.triggerRerender();
2344
+ setTimeout(() => {
2345
+ this.stateManager.updateCaptureState({
2346
+ showFlipAnimation: false,
2347
+ isDetectionPaused: false
2348
+ });
2349
+ this.triggerRerender();
2350
+ this.startBackDocumentTimer();
2351
+ // Clear processing button after animation completes
2352
+ this.processingButton = null;
2353
+ }, 3000);
2354
+ }
2355
+ else if (captureState.step === 'back') {
2356
+ this.stateManager.setCapturedImages({
2357
+ back: {
2358
+ fullFrame: captureCanvas.toDataURL('image/png'),
2359
+ cropped: croppedCanvas.toDataURL('image/png')
2360
+ }
2361
+ });
2362
+ this.completeProcess(false);
2363
+ // Clear processing button after back capture completes
2364
+ this.processingButton = null;
2365
+ }
2366
+ // Return canvases to pool after use
2367
+ this.returnCanvasToPool(captureCanvas);
2368
+ this.returnCanvasToPool(croppedCanvas);
2369
+ }
1945
2370
  async takeScreenshot() {
1946
2371
  if (!this.videoRef || !this.lastDetectedBox)
1947
2372
  return;
@@ -1975,10 +2400,15 @@ const JaakStamps = class {
1975
2400
  });
1976
2401
  // Check if document classification is enabled
1977
2402
  if (this.useDocumentClassification) {
1978
- const classification = await this.detectionService.classifyDocument(croppedCanvas);
1979
- if (classification && classification.class === 'passport') {
1980
- this.completeProcess(true);
1981
- return;
2403
+ try {
2404
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2405
+ if (classification && classification.class === 'passport') {
2406
+ this.completeProcess(true);
2407
+ return;
2408
+ }
2409
+ }
2410
+ catch (classifyError) {
2411
+ // No cambiar a manual aquí, solo registrar el error y continuar
1982
2412
  }
1983
2413
  }
1984
2414
  this.stateManager.updateCaptureState({
@@ -2015,6 +2445,135 @@ const JaakStamps = class {
2015
2445
  cardOutline?.classList.remove('capturing');
2016
2446
  }, 600);
2017
2447
  }
2448
+ // Método simplificado para cambio a modo manual
2449
+ switchToManualMode() {
2450
+ // Only switch if detector is enabled and not already switched
2451
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2452
+ return;
2453
+ }
2454
+ // Detener todo el monitoreo de performance
2455
+ this.stopPerformanceMonitoring();
2456
+ // Liberar recursos de ONNX
2457
+ this.releaseOnnxResources();
2458
+ // Resetear máscara a blanco y limpiar detecciones
2459
+ this.resetMaskToWhite();
2460
+ this.detectionBoxes = [];
2461
+ this.hasAutoSwitchedToManual = true;
2462
+ this.performanceDegradedMode = true;
2463
+ this.showManualCaptureButton = true;
2464
+ this.showPerformanceMessage = true;
2465
+ // Hide performance message after 5 seconds
2466
+ setTimeout(() => {
2467
+ this.showPerformanceMessage = false;
2468
+ }, 5000);
2469
+ }
2470
+ // Método para cambiar a modo manual por errores de ONNX
2471
+ switchToManualModeWithWarning(warningMessage) {
2472
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2473
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2474
+ return;
2475
+ }
2476
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
2477
+ // Resetear máscara a blanco y limpiar detecciones
2478
+ this.resetMaskToWhite();
2479
+ this.detectionBoxes = [];
2480
+ // Cambiar a modo manual
2481
+ this.hasAutoSwitchedToManual = true;
2482
+ this.performanceDegradedMode = true;
2483
+ this.showManualCaptureButton = true;
2484
+ this.useDocumentDetector = false; // Desactivar detector automático
2485
+ // Actualizar el estado con mensaje informativo (no error)
2486
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
2487
+ // Continuar con la configuración de cámara
2488
+ this.continueWithCameraSetup();
2489
+ }
2490
+ async continueWithCameraSetup() {
2491
+ try {
2492
+ // Paso 2: Detectar y configurar dispositivos
2493
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
2494
+ try {
2495
+ await this.cameraService.enumerateDevices();
2496
+ }
2497
+ catch (enumerateError) {
2498
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
2499
+ }
2500
+ try {
2501
+ await this.updateCameraInfoWithAutofocus();
2502
+ }
2503
+ catch (cameraInfoError) {
2504
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
2505
+ }
2506
+ // Paso 3: Configurar cámara seleccionada
2507
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
2508
+ let stream;
2509
+ try {
2510
+ stream = await this.cameraService.setupCamera();
2511
+ }
2512
+ catch (setupError) {
2513
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
2514
+ }
2515
+ // Paso 4: Inicializar video
2516
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
2517
+ try {
2518
+ await this.initializeVideoStream(stream);
2519
+ }
2520
+ catch (videoError) {
2521
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
2522
+ }
2523
+ // Paso 5: Calibrar máscara
2524
+ if (this.detectionContainer) {
2525
+ const container = this.detectionContainer.parentElement;
2526
+ const rect = container.getBoundingClientRect();
2527
+ this.updateMaskDimensions(rect);
2528
+ }
2529
+ // Finalizar
2530
+ this.startTime = Date.now();
2531
+ this.stateManager.updateCaptureState({ isLoading: false });
2532
+ // Mostrar botón manual (ya debe estar habilitado)
2533
+ this.showManualCaptureButton = true;
2534
+ }
2535
+ catch (error) {
2536
+ this.updateStatus('Error de cámara', error.message, 'error');
2537
+ throw error;
2538
+ }
2539
+ }
2540
+ switchToManualModeWithError(errorMessage) {
2541
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2542
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2543
+ return;
2544
+ }
2545
+ // Liberar recursos de ONNX inmediatamente
2546
+ this.releaseOnnxResources();
2547
+ // Resetear máscara a blanco y limpiar detecciones
2548
+ this.resetMaskToWhite();
2549
+ this.detectionBoxes = [];
2550
+ // Cambiar a modo manual
2551
+ this.hasAutoSwitchedToManual = true;
2552
+ this.performanceDegradedMode = true;
2553
+ this.showManualCaptureButton = true;
2554
+ this.useDocumentDetector = false; // Desactivar detector automático
2555
+ // Actualizar el estado con mensaje de error
2556
+ this.updateStatus('Modo manual activado', errorMessage, 'error');
2557
+ // Ocultar mensaje después de 5 segundos
2558
+ setTimeout(() => {
2559
+ this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
2560
+ }, 5000);
2561
+ }
2562
+ stopPerformanceMonitoring() {
2563
+ // Nota: No necesitamos limpiar más variables porque ya no se usarán
2564
+ // El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
2565
+ }
2566
+ // Método para liberar recursos de ONNX cuando se cambia a modo manual
2567
+ releaseOnnxResources() {
2568
+ try {
2569
+ // Liberar recursos del servicio de detección
2570
+ if (this.detectionService) {
2571
+ this.detectionService.cleanup();
2572
+ }
2573
+ }
2574
+ catch (error) {
2575
+ }
2576
+ }
2018
2577
  completeProcess(skippedBack = false) {
2019
2578
  this.stateManager.updateCaptureState({
2020
2579
  step: 'completed',
@@ -2128,6 +2687,13 @@ const JaakStamps = class {
2128
2687
  this.detectionBoxes = [];
2129
2688
  this.alignmentStartTime = undefined;
2130
2689
  this.hasDocumentDetected = false;
2690
+ // Reset sistema simplificado
2691
+ this.processedFramesCount = 0;
2692
+ this.slowFrameCount = 0;
2693
+ // Reset performance degradation state
2694
+ this.hasAutoSwitchedToManual = false;
2695
+ this.performanceDegradedMode = false;
2696
+ this.showPerformanceMessage = false;
2131
2697
  if (this.alignmentTimer) {
2132
2698
  clearTimeout(this.alignmentTimer);
2133
2699
  this.alignmentTimer = undefined;
@@ -2231,6 +2797,24 @@ const JaakStamps = class {
2231
2797
  // Performance is good, use base frame skip
2232
2798
  return this.BASE_FRAME_SKIP;
2233
2799
  }
2800
+ // Método para resetear la máscara a color blanco
2801
+ resetMaskToWhite() {
2802
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
2803
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
2804
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
2805
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
2806
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
2807
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
2808
+ // Remover todas las clases de alineación y estado perfecto
2809
+ cardOutline?.classList.remove('perfect-match');
2810
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
2811
+ topSide?.classList.remove('aligned');
2812
+ rightSide?.classList.remove('aligned');
2813
+ bottomSide?.classList.remove('aligned');
2814
+ leftSide?.classList.remove('aligned');
2815
+ // Resetear estado de alineación
2816
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
2817
+ }
2234
2818
  // Canvas pool management for screenshots
2235
2819
  getPooledCanvas(width, height) {
2236
2820
  // Try to find a canvas with matching dimensions