@jaak.ai/stamps 2.1.0-dev.6 → 2.1.0-dev.8

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 (28) hide show
  1. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
  2. package/dist/cjs/jaak-stamps.cjs.entry.js +623 -31
  3. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  4. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  5. package/dist/cjs/loader.cjs.js +1 -1
  6. package/dist/collection/components/my-component/my-component.css +36 -11
  7. package/dist/collection/components/my-component/my-component.js +484 -24
  8. package/dist/collection/components/my-component/my-component.js.map +1 -1
  9. package/dist/collection/services/DetectionService.js +176 -9
  10. package/dist/collection/services/DetectionService.js.map +1 -1
  11. package/dist/collection/services/interfaces/IDetectionService.js.map +1 -1
  12. package/dist/components/jaak-stamps.js +627 -32
  13. package/dist/components/jaak-stamps.js.map +1 -1
  14. package/dist/esm/jaak-stamps-webcomponent.js +1 -1
  15. package/dist/esm/jaak-stamps.entry.js +623 -31
  16. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  17. package/dist/esm/loader.js +1 -1
  18. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  19. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  20. package/dist/jaak-stamps-webcomponent/p-8e25497e.entry.js +2 -0
  21. package/dist/jaak-stamps-webcomponent/p-8e25497e.entry.js.map +1 -0
  22. package/dist/types/components/my-component/my-component.d.ts +32 -0
  23. package/dist/types/components.d.ts +3 -1
  24. package/dist/types/services/DetectionService.d.ts +9 -1
  25. package/dist/types/services/interfaces/IDetectionService.d.ts +6 -0
  26. package/package.json +1 -1
  27. package/dist/jaak-stamps-webcomponent/p-d05dc382.entry.js +0 -2
  28. package/dist/jaak-stamps-webcomponent/p-d05dc382.entry.js.map +0 -1
@@ -650,6 +650,10 @@ class DetectionService {
650
650
  // Static canvas pool for reuse across instances
651
651
  static canvasPool = new Map();
652
652
  static MAX_POOL_SIZE = 5;
653
+ // Resource management for optimization
654
+ modelLoadController;
655
+ classificationLoadController;
656
+ modelBlobUrls = new Set();
653
657
  constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
654
658
  this.debug = debug;
655
659
  this.useDocumentClassification = useDocumentClassification;
@@ -667,12 +671,32 @@ class DetectionService {
667
671
  return;
668
672
  }
669
673
  try {
674
+ // Create AbortController for this load operation
675
+ this.modelLoadController = new AbortController();
670
676
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
671
677
  try {
672
- this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
678
+ // Load model with abort signal support
679
+ const response = await fetch(this.MODEL_PATH, {
680
+ signal: this.modelLoadController.signal
681
+ });
682
+ if (!response.ok) {
683
+ throw new Error(`Failed to fetch model: ${response.status}`);
684
+ }
685
+ const modelBuffer = await response.arrayBuffer();
686
+ this.session = await window.ort.InferenceSession.create(modelBuffer, sessionOptions);
673
687
  }
674
688
  catch (error) {
675
- if (error.message.includes('failed to allocate a buffer')) {
689
+ // Handle abort signal
690
+ if (error.name === 'AbortError') {
691
+ console.log('Model loading was cancelled');
692
+ return;
693
+ }
694
+ // Múltiples tipos de errores de memoria que pueden ocurrir
695
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
696
+ error.message.includes('Out of memory') ||
697
+ error.message.includes('RangeError: Out of memory') ||
698
+ error.message.includes('no available backend found');
699
+ if (isMemoryError) {
676
700
  const fallbackOptions = {
677
701
  executionProviders: ['wasm'],
678
702
  graphOptimizationLevel: 'disabled',
@@ -682,8 +706,25 @@ class DetectionService {
682
706
  executionMode: 'sequential',
683
707
  interOpNumThreads: 1,
684
708
  intraOpNumThreads: 1,
709
+ // Configuraciones adicionales para dispositivos con poca memoria
710
+ enableProfiling: false,
711
+ sessionLogSeverityLevel: 4,
712
+ sessionLogVerbosityLevel: 0,
685
713
  };
686
- this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
714
+ try {
715
+ // Retry with fallback options and abort signal
716
+ const fallbackResponse = await fetch(this.MODEL_PATH, {
717
+ signal: this.modelLoadController.signal
718
+ });
719
+ if (!fallbackResponse.ok) {
720
+ throw new Error(`Failed to fetch model with fallback: ${fallbackResponse.status}`);
721
+ }
722
+ const fallbackBuffer = await fallbackResponse.arrayBuffer();
723
+ this.session = await window.ort.InferenceSession.create(fallbackBuffer, fallbackOptions);
724
+ }
725
+ catch (fallbackError) {
726
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
727
+ }
687
728
  }
688
729
  else {
689
730
  throw error;
@@ -700,9 +741,13 @@ class DetectionService {
700
741
  return;
701
742
  }
702
743
  try {
744
+ // Create AbortController for classification model loading
745
+ this.classificationLoadController = new AbortController();
703
746
  // Try to load class map (optional - SqueezeNet may not need it for basic classification)
704
747
  try {
705
- const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
748
+ const classResponse = await fetch(this.MOBILENET_CLASSES_PATH, {
749
+ signal: this.classificationLoadController.signal
750
+ });
706
751
  if (classResponse.ok) {
707
752
  this.mobileNetClassMap = await classResponse.json();
708
753
  }
@@ -720,10 +765,28 @@ class DetectionService {
720
765
  // Load model
721
766
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
722
767
  try {
723
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
768
+ // Load classification model with abort signal support
769
+ const response = await fetch(this.MOBILENET_MODEL_PATH, {
770
+ signal: this.classificationLoadController.signal
771
+ });
772
+ if (!response.ok) {
773
+ throw new Error(`Failed to fetch classification model: ${response.status}`);
774
+ }
775
+ const modelBuffer = await response.arrayBuffer();
776
+ this.mobileNetSession = await window.ort.InferenceSession.create(modelBuffer, sessionOptions);
724
777
  }
725
778
  catch (error) {
726
- if (error.message.includes('failed to allocate a buffer')) {
779
+ // Handle abort signal
780
+ if (error.name === 'AbortError') {
781
+ console.log('Classification model loading was cancelled');
782
+ return;
783
+ }
784
+ // Múltiples tipos de errores de memoria que pueden ocurrir
785
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
786
+ error.message.includes('Out of memory') ||
787
+ error.message.includes('RangeError: Out of memory') ||
788
+ error.message.includes('no available backend found');
789
+ if (isMemoryError) {
727
790
  const fallbackOptions = {
728
791
  executionProviders: ['wasm'],
729
792
  graphOptimizationLevel: 'disabled',
@@ -733,8 +796,25 @@ class DetectionService {
733
796
  executionMode: 'sequential',
734
797
  interOpNumThreads: 1,
735
798
  intraOpNumThreads: 1,
799
+ // Configuraciones adicionales para dispositivos con poca memoria
800
+ enableProfiling: false,
801
+ sessionLogSeverityLevel: 4,
802
+ sessionLogVerbosityLevel: 0,
736
803
  };
737
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
804
+ try {
805
+ // Retry with fallback options and abort signal
806
+ const fallbackResponse = await fetch(this.MOBILENET_MODEL_PATH, {
807
+ signal: this.classificationLoadController.signal
808
+ });
809
+ if (!fallbackResponse.ok) {
810
+ throw new Error(`Failed to fetch classification model with fallback: ${fallbackResponse.status}`);
811
+ }
812
+ const fallbackBuffer = await fallbackResponse.arrayBuffer();
813
+ this.mobileNetSession = await window.ort.InferenceSession.create(fallbackBuffer, fallbackOptions);
814
+ }
815
+ catch (fallbackError) {
816
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
817
+ }
738
818
  }
739
819
  else {
740
820
  throw error;
@@ -920,6 +1000,9 @@ class DetectionService {
920
1000
  return alignment.top && alignment.right && alignment.bottom && alignment.left;
921
1001
  }
922
1002
  cleanup() {
1003
+ // Cancel any ongoing requests
1004
+ this.abortPendingRequests();
1005
+ // Cleanup ONNX resources
923
1006
  this.cleanupCanvasPool();
924
1007
  if (this.session) {
925
1008
  this.session.release?.();
@@ -931,6 +1014,8 @@ class DetectionService {
931
1014
  }
932
1015
  this.mobileNetClassMap = undefined;
933
1016
  this.modelLoaded = false;
1017
+ // Cleanup blob URLs
1018
+ this.revokeModelBlobUrls();
934
1019
  }
935
1020
  initializeCanvasPool() {
936
1021
  this.preprocessCanvas = document.createElement('canvas');
@@ -1028,11 +1113,93 @@ class DetectionService {
1028
1113
  const pool = DetectionService.canvasPool.get(key);
1029
1114
  // Only return to pool if not at max capacity
1030
1115
  if (pool.length < DetectionService.MAX_POOL_SIZE) {
1116
+ const ctx = canvas.getContext('2d');
1117
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1031
1118
  pool.push(canvas);
1032
1119
  }
1120
+ // If pool is full, let the canvas be garbage collected
1121
+ }
1122
+ // Resource management methods for cache and browser optimization
1123
+ async clearModelCache() {
1124
+ try {
1125
+ // Clear cache specific to models
1126
+ const cacheNames = await caches.keys();
1127
+ const modelCaches = cacheNames.filter(name => name.includes('onnx') ||
1128
+ name.includes('jaak-static') ||
1129
+ name.includes('squeezenet') ||
1130
+ name.includes('ddmyp'));
1131
+ await Promise.all(modelCaches.map(cacheName => caches.delete(cacheName)));
1132
+ if (this.debug) {
1133
+ console.log(`🧹 Cleared ${modelCaches.length} model caches`);
1134
+ }
1135
+ }
1136
+ catch (error) {
1137
+ console.warn('Error clearing model cache:', error);
1138
+ }
1139
+ }
1140
+ abortPendingRequests() {
1141
+ // Cancel any fetch in progress
1142
+ if (this.modelLoadController) {
1143
+ this.modelLoadController.abort();
1144
+ this.modelLoadController = undefined;
1145
+ }
1146
+ if (this.classificationLoadController) {
1147
+ this.classificationLoadController.abort();
1148
+ this.classificationLoadController = undefined;
1149
+ }
1150
+ }
1151
+ revokeModelBlobUrls() {
1152
+ // Revoke any blob URLs created for models
1153
+ this.modelBlobUrls.forEach(url => URL.revokeObjectURL(url));
1154
+ this.modelBlobUrls.clear();
1155
+ }
1156
+ // Progressive resource release for optimization
1157
+ async releaseMobileNetResources() {
1158
+ if (this.mobileNetSession) {
1159
+ this.mobileNetSession.release?.();
1160
+ this.mobileNetSession = undefined;
1161
+ }
1162
+ this.mobileNetClassMap = undefined;
1163
+ if (this.classificationLoadController) {
1164
+ this.classificationLoadController.abort();
1165
+ this.classificationLoadController = undefined;
1166
+ }
1167
+ if (this.debug) {
1168
+ console.log('🧹 Released MobileNet classification resources');
1169
+ }
1033
1170
  }
1034
- // Static method to clear all canvas pools (useful for memory management)
1035
- static clearCanvasPools() {
1171
+ // Optimize canvas pool by reducing to specific size
1172
+ optimizeCanvasPool(maxSize) {
1173
+ DetectionService.canvasPool.forEach((pool, key) => {
1174
+ while (pool.length > maxSize) {
1175
+ const canvas = pool.pop();
1176
+ // Explicitly clear and nullify for garbage collection
1177
+ if (canvas) {
1178
+ const ctx = canvas.getContext('2d');
1179
+ if (ctx) {
1180
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1181
+ }
1182
+ }
1183
+ }
1184
+ if (pool.length === 0) {
1185
+ DetectionService.canvasPool.delete(key);
1186
+ }
1187
+ });
1188
+ if (this.debug) {
1189
+ console.log(`🧹 Optimized canvas pool to max size: ${maxSize}`);
1190
+ }
1191
+ }
1192
+ // Clear all static canvas pools
1193
+ static clearAllCanvasPools() {
1194
+ DetectionService.canvasPool.forEach((pool) => {
1195
+ pool.forEach(canvas => {
1196
+ const ctx = canvas.getContext('2d');
1197
+ if (ctx) {
1198
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1199
+ }
1200
+ });
1201
+ pool.length = 0;
1202
+ });
1036
1203
  DetectionService.canvasPool.clear();
1037
1204
  }
1038
1205
  // Method to get pool statistics for debugging
@@ -1091,7 +1258,7 @@ class ServiceContainer {
1091
1258
  }
1092
1259
  }
1093
1260
 
1094
- const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.back-capture-section{bottom:16px;max-width:300px}.back-capture-buttons{flex-direction:column;gap:8px;width:100%}.capture-button,.back-capture-buttons .skip-button{width:100%;max-width:200px;padding:10px 20px;font-size:13px}}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.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)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}@keyframes slideInFromTopCentered{0%{opacity:0;transform:translate(-50%, -50%) translateY(-8px) scale(0.95)}100%{opacity:1;transform:translate(-50%, -50%) translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:flex-start;align-items:center;gap:8px;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:0;font-weight:400;display:flex;align-items:center;gap:6px}.camera-label .autofocus-icon{font-size:12px;color:#ffffff !important;opacity:0.8;flex-shrink:0}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9;flex-shrink:0}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}.guide-text{font-size:11px;padding:4px 8px;border-radius:8px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromTop 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}.manual-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.manual-capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.manual-capture-button:hover{background:#f8f9fa}.manual-capture-button:active{background:#e9ecef;transform:translateY(1px)}.manual-capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}}";
1261
+ const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled,.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}}";
1095
1262
 
1096
1263
  const JaakStamps = class {
1097
1264
  constructor(hostRef) {
@@ -1150,6 +1317,7 @@ const JaakStamps = class {
1150
1317
  performanceDegradedMode = false; // Auto-switched to manual mode due to performance
1151
1318
  showPerformanceMessage = false; // Show performance message temporarily
1152
1319
  captureStateVersion = 0; // Triggers re-render when StateManager changes
1320
+ processingButton = null; // Track which button is processing
1153
1321
  // Services
1154
1322
  serviceContainer;
1155
1323
  eventBus;
@@ -1201,14 +1369,19 @@ const JaakStamps = class {
1201
1369
  canvasPool = [];
1202
1370
  MAX_CANVAS_POOL_SIZE = 3;
1203
1371
  async componentDidLoad() {
1204
- this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1205
- await this.initializeServices();
1206
- this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1207
- await this.setupEventListeners();
1208
- this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1209
- await this.initializeComponent();
1210
- if (this.debug) {
1211
- this.initializePerformanceMonitor();
1372
+ try {
1373
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1374
+ await this.initializeServices();
1375
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1376
+ await this.setupEventListeners();
1377
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1378
+ await this.initializeComponent();
1379
+ if (this.debug) {
1380
+ this.initializePerformanceMonitor();
1381
+ }
1382
+ }
1383
+ catch (error) {
1384
+ this.updateStatus('Error de inicialización', error.message, 'error');
1212
1385
  }
1213
1386
  }
1214
1387
  async initializeServices() {
@@ -1503,7 +1676,13 @@ const JaakStamps = class {
1503
1676
  }
1504
1677
  try {
1505
1678
  this.exitSession();
1506
- return { success: true };
1679
+ // Aggressive cleanup when stopping capture
1680
+ const cleanupResult = await this.aggressiveResourceCleanup();
1681
+ return {
1682
+ success: true,
1683
+ resourcesFreed: cleanupResult.freed,
1684
+ cleanupErrors: cleanupResult.errors
1685
+ };
1507
1686
  }
1508
1687
  catch (error) {
1509
1688
  return { success: false, error: error.message };
@@ -1523,8 +1702,14 @@ const JaakStamps = class {
1523
1702
  }
1524
1703
  }
1525
1704
  async skipBackCapture() {
1705
+ console.log('🟡 Botón clicked: Saltar reverso');
1706
+ // Set processing state immediately
1707
+ this.processingButton = 'skip-back';
1708
+ // Add small delay to show spinner
1709
+ await new Promise(resolve => setTimeout(resolve, 100));
1526
1710
  const readyCheck = this.isComponentReady();
1527
1711
  if (!readyCheck.ready) {
1712
+ this.processingButton = null;
1528
1713
  return { success: false, error: readyCheck.message };
1529
1714
  }
1530
1715
  try {
@@ -1541,6 +1726,10 @@ const JaakStamps = class {
1541
1726
  catch (error) {
1542
1727
  return { success: false, error: error.message };
1543
1728
  }
1729
+ finally {
1730
+ // Always clear processing state when done
1731
+ this.processingButton = null;
1732
+ }
1544
1733
  }
1545
1734
  async getStatus() {
1546
1735
  const readyCheck = this.isComponentReady();
@@ -1718,9 +1907,76 @@ const JaakStamps = class {
1718
1907
  // Always allow getting capture delay, even during initialization
1719
1908
  return this.captureDelay;
1720
1909
  }
1910
+ async getResourceReport() {
1911
+ // Public method to get resource usage report
1912
+ return this.getResourceReportInternal();
1913
+ }
1914
+ async forceResourceCleanup() {
1915
+ // Public method to force aggressive resource cleanup
1916
+ const readyCheck = this.isComponentReady();
1917
+ if (!readyCheck.ready) {
1918
+ return { success: false, error: readyCheck.message };
1919
+ }
1920
+ try {
1921
+ const cleanupResult = await this.aggressiveResourceCleanup();
1922
+ return {
1923
+ success: true,
1924
+ resourcesFreed: cleanupResult.freed,
1925
+ errors: cleanupResult.errors,
1926
+ timestamp: new Date().toISOString()
1927
+ };
1928
+ }
1929
+ catch (error) {
1930
+ return { success: false, error: error.message };
1931
+ }
1932
+ }
1933
+ // Memory and device capability detection
1934
+ checkDeviceCapabilities() {
1935
+ const deviceMemory = navigator.deviceMemory;
1936
+ const hardwareConcurrency = navigator.hardwareConcurrency || 1;
1937
+ const isLowEndDevice = hardwareConcurrency <= 2;
1938
+ const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
1939
+ const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
1940
+ // Si tenemos info de memoria del dispositivo
1941
+ if (deviceMemory && deviceMemory < 4) {
1942
+ return {
1943
+ canUseML: false,
1944
+ reason: `Memoria del dispositivo (${deviceMemory}GB) insuficiente para detección automática. Modo manual activado para mejor rendimiento.`,
1945
+ memoryMB: deviceMemory * 1024
1946
+ };
1947
+ }
1948
+ // Dispositivos móviles de gama baja
1949
+ if (isMobile && isLowEndDevice) {
1950
+ return {
1951
+ canUseML: false,
1952
+ reason: 'Dispositivo móvil optimizado detectado. Modo manual activado para mejor rendimiento y duración de batería.',
1953
+ memoryMB: null
1954
+ };
1955
+ }
1956
+ // Safari tiene limitaciones conocidas con WASM
1957
+ if (isSafari) {
1958
+ return {
1959
+ canUseML: false,
1960
+ reason: 'Safari detectado. Modo manual activado por compatibilidad optimizada con este navegador.',
1961
+ memoryMB: null
1962
+ };
1963
+ }
1964
+ return {
1965
+ canUseML: true,
1966
+ reason: 'Dispositivo compatible con detección automática.',
1967
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
1968
+ };
1969
+ }
1721
1970
  // DETECTION METHODS
1722
1971
  async startDetection() {
1723
1972
  try {
1973
+ // Verificar capacidades del dispositivo antes de cargar modelos
1974
+ const capabilities = this.checkDeviceCapabilities();
1975
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
1976
+ if (!capabilities.canUseML && this.useDocumentDetector) {
1977
+ this.switchToManualModeWithWarning(capabilities.reason);
1978
+ return;
1979
+ }
1724
1980
  // Paso 1: Verificar y cargar modelos si es necesario
1725
1981
  if (!this.detectionService.isModelLoaded()) {
1726
1982
  const loadStartTime = performance.now();
@@ -1730,16 +1986,32 @@ const JaakStamps = class {
1730
1986
  await this.detectionService.loadModel();
1731
1987
  }
1732
1988
  catch (modelError) {
1733
- this.switchToManualModeWithError('Error al cargar modelo de detección');
1734
- throw modelError;
1989
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1990
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
1991
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1992
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
1993
+ return;
1994
+ }
1995
+ else {
1996
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1997
+ throw modelError;
1998
+ }
1735
1999
  }
1736
2000
  this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1737
2001
  try {
1738
2002
  await this.detectionService.loadClassificationModel();
1739
2003
  }
1740
2004
  catch (classificationError) {
1741
- this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1742
- throw classificationError;
2005
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
2006
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
2007
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
2008
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
2009
+ return;
2010
+ }
2011
+ else {
2012
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
2013
+ throw classificationError;
2014
+ }
1743
2015
  }
1744
2016
  const loadEndTime = performance.now();
1745
2017
  const loadTime = loadEndTime - loadStartTime;
@@ -1750,11 +2022,27 @@ const JaakStamps = class {
1750
2022
  }
1751
2023
  // Paso 2: Detectar y configurar dispositivos
1752
2024
  this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1753
- await this.cameraService.enumerateDevices();
1754
- await this.updateCameraInfoWithAutofocus();
2025
+ try {
2026
+ await this.cameraService.enumerateDevices();
2027
+ }
2028
+ catch (enumerateError) {
2029
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
2030
+ }
2031
+ try {
2032
+ await this.updateCameraInfoWithAutofocus();
2033
+ }
2034
+ catch (cameraInfoError) {
2035
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
2036
+ }
1755
2037
  // Paso 3: Configurar cámara seleccionada
1756
2038
  this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1757
- const stream = await this.cameraService.setupCamera();
2039
+ let stream;
2040
+ try {
2041
+ stream = await this.cameraService.setupCamera();
2042
+ }
2043
+ catch (setupError) {
2044
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
2045
+ }
1758
2046
  // Paso 4: Inicializar video y mostrar estado de análisis inicial
1759
2047
  if (this.useDocumentDetector) {
1760
2048
  this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
@@ -1762,7 +2050,12 @@ const JaakStamps = class {
1762
2050
  else {
1763
2051
  this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
1764
2052
  }
1765
- await this.initializeVideoStream(stream);
2053
+ try {
2054
+ await this.initializeVideoStream(stream);
2055
+ }
2056
+ catch (videoError) {
2057
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
2058
+ }
1766
2059
  // Paso 5: Calibrar máscara
1767
2060
  if (this.detectionContainer) {
1768
2061
  const container = this.detectionContainer.parentElement;
@@ -1972,11 +2265,182 @@ const JaakStamps = class {
1972
2265
  }
1973
2266
  // Clear canvas pool
1974
2267
  this.canvasPool = [];
2268
+ // Clean DOM references and memory leaks
2269
+ this.cleanupDOMReferences();
1975
2270
  this.detectionBoxes = [];
1976
2271
  this.alignmentStartTime = undefined;
1977
2272
  this.hasDocumentDetected = false;
1978
2273
  this.serviceContainer?.cleanup();
1979
2274
  }
2275
+ // AGGRESSIVE RESOURCE CLEANUP METHODS
2276
+ // Clean DOM references and memory leaks
2277
+ cleanupDOMReferences() {
2278
+ // Remove event listeners that could maintain references
2279
+ if (this.videoRef) {
2280
+ // Remove common event listeners (if any were added)
2281
+ this.videoRef.srcObject = null;
2282
+ this.videoRef = undefined;
2283
+ }
2284
+ // Clean detection container
2285
+ if (this.detectionContainer) {
2286
+ this.detectionContainer = undefined;
2287
+ }
2288
+ // Clean references to images in the DOM and revoke blob URLs
2289
+ const images = this.el.shadowRoot?.querySelectorAll('img');
2290
+ images?.forEach(img => {
2291
+ if (img.src.startsWith('blob:')) {
2292
+ URL.revokeObjectURL(img.src);
2293
+ }
2294
+ img.src = '';
2295
+ });
2296
+ }
2297
+ // Clear browser cache and storage
2298
+ async clearBrowserCache() {
2299
+ try {
2300
+ // 1. Clear Service Worker cache if exists
2301
+ if ('serviceWorker' in navigator) {
2302
+ const registrations = await navigator.serviceWorker.getRegistrations();
2303
+ await Promise.all(registrations.map(registration => registration.unregister()));
2304
+ }
2305
+ // 2. Clear Cache API specific to the component
2306
+ const cacheNames = await caches.keys();
2307
+ const componentCaches = cacheNames.filter(name => name.includes('jaak') || name.includes('stamps'));
2308
+ await Promise.all(componentCaches.map(cacheName => caches.delete(cacheName)));
2309
+ // 3. Force garbage collection if available (only in dev)
2310
+ if (this.debug && window.gc) {
2311
+ window.gc();
2312
+ }
2313
+ }
2314
+ catch (error) {
2315
+ console.warn('Error clearing browser cache:', error);
2316
+ }
2317
+ }
2318
+ // Clear local storage specific to the component
2319
+ async clearLocalStorage() {
2320
+ try {
2321
+ // Clear localStorage specific to the component
2322
+ const keysToRemove = [];
2323
+ for (let i = 0; i < localStorage.length; i++) {
2324
+ const key = localStorage.key(i);
2325
+ if (key && (key.includes('jaak') || key.includes('onnx') || key.includes('stamps'))) {
2326
+ keysToRemove.push(key);
2327
+ }
2328
+ }
2329
+ keysToRemove.forEach(key => localStorage.removeItem(key));
2330
+ // Clear sessionStorage
2331
+ const sessionKeysToRemove = [];
2332
+ for (let i = 0; i < sessionStorage.length; i++) {
2333
+ const key = sessionStorage.key(i);
2334
+ if (key && (key.includes('jaak') || key.includes('onnx'))) {
2335
+ sessionKeysToRemove.push(key);
2336
+ }
2337
+ }
2338
+ sessionKeysToRemove.forEach(key => sessionStorage.removeItem(key));
2339
+ }
2340
+ catch (error) {
2341
+ console.warn('Error clearing local storage:', error);
2342
+ }
2343
+ }
2344
+ // Aggressive resource cleanup combining all strategies
2345
+ async aggressiveResourceCleanup() {
2346
+ const freed = [];
2347
+ const errors = [];
2348
+ try {
2349
+ // 1. Release ONNX resources
2350
+ this.releaseOnnxResources();
2351
+ freed.push('ONNX models');
2352
+ // 2. Clear model cache in detection service
2353
+ if (this.detectionService) {
2354
+ await this.detectionService.clearModelCache();
2355
+ freed.push('Model cache');
2356
+ }
2357
+ // 3. Clear browser cache
2358
+ await this.clearBrowserCache();
2359
+ freed.push('Browser cache');
2360
+ // 4. Clear local storage
2361
+ await this.clearLocalStorage();
2362
+ freed.push('Local storage');
2363
+ // 5. Clean DOM references
2364
+ this.cleanupDOMReferences();
2365
+ freed.push('DOM references');
2366
+ // 6. Optimize canvas pools
2367
+ if (this.detectionService) {
2368
+ this.detectionService.optimizeCanvasPool(0);
2369
+ freed.push('Canvas pools');
2370
+ }
2371
+ // 7. Clear component canvas pool
2372
+ this.canvasPool = [];
2373
+ freed.push('Component canvas pool');
2374
+ // 8. Force garbage collection (if available)
2375
+ if (this.debug && window.gc) {
2376
+ window.gc();
2377
+ freed.push('Garbage collection');
2378
+ }
2379
+ }
2380
+ catch (error) {
2381
+ errors.push(error.message);
2382
+ }
2383
+ return { freed, errors };
2384
+ }
2385
+ // Progressive resource release after front capture
2386
+ async onFrontCaptureComplete() {
2387
+ try {
2388
+ // Release classification model if not needed for back
2389
+ if (this.detectionService && !this.useDocumentClassification) {
2390
+ await this.detectionService.releaseMobileNetResources();
2391
+ }
2392
+ // Optimize canvas pool to minimum needed for back capture
2393
+ if (this.detectionService) {
2394
+ this.detectionService.optimizeCanvasPool(1);
2395
+ }
2396
+ // Reduce component canvas pool
2397
+ while (this.canvasPool.length > 1) {
2398
+ const canvas = this.canvasPool.pop();
2399
+ if (canvas) {
2400
+ const ctx = canvas.getContext('2d');
2401
+ if (ctx) {
2402
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2403
+ }
2404
+ }
2405
+ }
2406
+ if (this.debug) {
2407
+ console.log('🧹 Progressive cleanup after front capture completed');
2408
+ }
2409
+ }
2410
+ catch (error) {
2411
+ console.warn('Error in progressive cleanup:', error);
2412
+ }
2413
+ }
2414
+ // Resource reporting for debugging
2415
+ getResourceReportInternal() {
2416
+ const report = {
2417
+ timestamp: new Date().toISOString(),
2418
+ component: {
2419
+ hasVideoStream: !!this.videoStream,
2420
+ hasAnimationFrame: !!this.animationId,
2421
+ canvasPoolSize: this.canvasPool.length,
2422
+ detectionBoxesCount: this.detectionBoxes.length,
2423
+ performanceMonitorActive: !!this.performanceUpdateInterval
2424
+ }
2425
+ };
2426
+ // Add detection service stats if available
2427
+ if (this.detectionService) {
2428
+ report.detectionService = {
2429
+ isModelLoaded: this.detectionService.isModelLoaded(),
2430
+ canvasPoolStats: this.detectionService.getPoolStats()
2431
+ };
2432
+ }
2433
+ // Add memory info if available
2434
+ if ('memory' in performance) {
2435
+ const memInfo = performance.memory;
2436
+ report.memory = {
2437
+ usedJSHeapSize: Math.round(memInfo.usedJSHeapSize / 1048576), // MB
2438
+ totalJSHeapSize: Math.round(memInfo.totalJSHeapSize / 1048576), // MB
2439
+ jsHeapSizeLimit: Math.round(memInfo.jsHeapSizeLimit / 1048576) // MB
2440
+ };
2441
+ }
2442
+ return report;
2443
+ }
1980
2444
  render() {
1981
2445
  const captureState = this.stateManager?.getCaptureState() || {
1982
2446
  isVideoActive: false,
@@ -1986,7 +2450,7 @@ const JaakStamps = class {
1986
2450
  isCapturing: false
1987
2451
  };
1988
2452
  const cameraInfo = this.cameraInfoWithAutofocus;
1989
- return (index.h("div", { key: '42958507f3463e8506bdd240b3f478f6e3bf1746', class: "detector-container" }, index.h("div", { key: '775a9c1df2075236c0b0bfe4a39eace38d099b09', class: "video-container" }, index.h("video", { key: '035d92f2c167087e95689dd7997ac20b8404ac1e', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), index.h("div", { key: 'e7fee77cba28b3e4149319ce7998b06af5c4075c', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index$1) => (index.h("div", { key: index$1, class: "detection-box", style: {
2453
+ return (index.h("div", { key: 'ca44b9eacbde9f688f2c41aa21ad35a2b882002b', class: "detector-container" }, index.h("div", { key: '86c7b5d31ed69276b62fa43e01c92b0c0b9f298d', class: "video-container" }, index.h("video", { key: 'bb310544c17ae734d8b42ed2617748c2f69743ff', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), index.h("div", { key: '078a79a0c6386ca6c040e629acbf4f52f12d8e29', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index$1) => (index.h("div", { key: index$1, class: "detection-box", style: {
1990
2454
  position: 'absolute',
1991
2455
  left: `${box.x}px`,
1992
2456
  top: `${box.y}px`,
@@ -1995,9 +2459,9 @@ const JaakStamps = class {
1995
2459
  border: '2px solid #32406C',
1996
2460
  pointerEvents: 'none',
1997
2461
  boxSizing: 'border-box'
1998
- } })))), this.isMaskReady && (index.h("div", { key: '3d7ab5706479922c4321953a18c0026f4be2ba07', class: "overlay-mask" }, index.h("div", { key: '410d98aa7ed52604760e12a1d8c8dc5a94a4e0d8', class: "card-outline" }, index.h("div", { key: 'b51b8b7df3ef5c5d3a9bfa43d704d06aef14b2cf', class: "side side-top" }), index.h("div", { key: 'e2c8f24094d2a9356248234c8a7a2d8aca07f3e6', class: "side side-right" }), index.h("div", { key: 'e4f92a79ac4c8af8d1e51e03d743b4814ffd2c00', class: "side side-bottom" }), index.h("div", { key: '400c294159a44f6211eb84a4e457b1406f541f44', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '69c4ed1c4d5bd21bdaf0795a49cf3dd9a59502f4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '92164199e91c9cbf40a94a87b0457e76c566e252', class: "back-capture-section" }, index.h("div", { key: '22ad40233923be0909529927779f09a6917895ee', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (index.h("button", { key: '3c65c3bd5687d709a5f4d1c9fe2293cfd320f269', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Reverso')), index.h("button", { key: 'ad6cb7eb1ce70af2fb3d5a8d031801a850605baa', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
2462
+ } })))), this.isMaskReady && (index.h("div", { key: '990015ff5b2ded89405dd2b891c027aad8788f3e', class: "overlay-mask" }, index.h("div", { key: 'dd9145140987addb1b6e4cc2119b8e9f89003726', class: "card-outline" }, index.h("div", { key: 'e8f4ec692a8b80c7a98075e4f708b79a272db67b', class: "side side-top" }), index.h("div", { key: '2577253b40425ecdcb3dd51b2f4a43c15c83ca79', class: "side side-right" }), index.h("div", { key: '30f4454020c00cf4fb92308af5ecf1843ec24005', class: "side side-bottom" }), index.h("div", { key: '3459f634987b8396106fff23fa1c99b295ee7d0a', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '84f4d70947e211a9c6b296531d53420223d2ca34', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '7c28e86e8de6c33bf7d7baab3083c5f6a0ab060f', class: "back-capture-section" }, index.h("div", { key: '27bdcf9cfe9790c4bbad484423f6f4c353ccf0b3', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (index.h("button", { key: 'ed0320b1df412bf53cbe4e4119f6d769926eb2c5', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (index.h("span", { key: '0bba706c8ebda59c33faca8e97b7536146462d21', class: "button-spinner" })), "Capturar Reverso")), index.h("button", { key: 'b072c46b25d16a04c5e0a8f3975d8aa8d24576d5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (index.h("span", { key: 'efd0827d0f19c9072d49d1b3c0046ef6c95a842c', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1999
2463
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
2000
- : 'Saltar reverso')))), captureState.isVideoActive && (index.h("div", { key: '9b87c7e4f5ab646ec35e5f81833a6c9e9ac5d5c8', class: "camera-controls" }, index.h("button", { key: 'c9c741f3885b56162de343b18efa8764f0c2c001', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (index.h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (index.h("div", { key: 'b4f5c63464085c135dda068c929b147cfd84238f', class: "camera-selector-dropdown" }, index.h("div", { key: '06072cad9c132f1926b4070c0c3d714b76cc6636', class: "camera-selector-header" }, index.h("span", { key: '85a813b383f6cf1fd505239781d8a5ea60cbae58' }, "Seleccionar C\u00E1mara"), index.h("button", { key: 'c1588cea255bcf7151ca2e9d0583d149122784d7', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: '62864eaea7aac898d27d8995fcede3788383918f', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (index.h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, index.h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (index.h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (index.h("span", { class: "selected-indicator" }, "\u2713")))))), index.h("div", { key: '1288fb31a8f3290bf5a280390c52af5b1dce75b5', class: "device-info" }, index.h("small", { key: 'efdf5a337cd07fc14112d3bd883298cb586f67f5' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '6b18401230001a923e139f3f2a704ac44b8f52f4', class: "manual-capture-section" }, index.h("button", { key: '8269121d4822b8d7cb01b966d3b701cc297b8856', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Frente'))))), captureState.isCapturing && (index.h("div", { key: '8920362bb45d0f024149440a914a2144e16783b0', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: '644baeaf145477d31439e7f28a5c262064f4db95', class: "flip-animation" }, index.h("div", { key: '467c703d5ac600f12f7456e2c51bf0d1ec94a638', class: "id-card-icon" }), index.h("div", { key: '8f2cef3855202d959a6e7884f1d74d78e3ef66ac', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: '651898dc58e10886d0b4696f2036af7bf68640ef', class: "success-animation" }, index.h("div", { key: '7b3e1626aa02131af0f860dc5ea8c8f0e4d6655b', class: "check-icon" }), index.h("div", { key: '571dcad0d407c275dd4ceaa04d2de4bbed7a9181', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: 'f2efae53af3ca6a38735f7fb06708101a811e1fe', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: '6f9268bfcc01b305c2d415b2bbbd3197b228d487', class: "status-spinner" })), index.h("div", { key: '17f0f0c8c4debb57408d1bed95dd8a6c180affb5', class: "status-content" }, index.h("div", { key: '6af3696c54d05bea334c24fe7713e658ef1c9d3e', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: '99d8fe46dd0c4e3f779c8d3a7fc6314b551f8424', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: '4b25e47a42a9f20713167c0f89f8adb07090a6e5', class: "performance-monitor" }, index.h("div", { key: 'b323c4cb466bfe50d105ea165d361b4240e97cbf', class: "performance-expanded" }, index.h("div", { key: '001d7d5cc4024162d24a9f46e2bc4b978ad0166b', class: "metrics-row" }, index.h("div", { key: 'd3f8281b92075b48f4fff22a6cbb020e98055f74', class: "metric-compact" }, index.h("span", { key: 'c140d8d5596ed3a3f176daf3e7a74bc12ef9fa56', class: "metric-label" }, "FPS"), index.h("span", { key: 'f88d7a211bc516757ef6842c5a9acb4bb9c3ff9b', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: '6f4b8aac4f1f1f086d0038b42ddd12b4bdff15ea', class: "metric-compact" }, index.h("span", { key: '8a222b05e739012a67f98f73bbba0ba042420b08', class: "metric-label" }, "MEM"), index.h("span", { key: '1a2f785d57ba926900eca868b890c677ce0adafc', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: 'e2dc13638b3cffa1245ed6b1485d85982215a23d', class: "metrics-row" }, index.h("div", { key: '3356b3dc77e2c2adc33f0483b46958495ea963a6', class: "metric-compact" }, index.h("span", { key: '9d6a01153605f15dd13a46fc6de7e43bcec3c4d7', class: "metric-label" }, "INF"), index.h("span", { key: '6db223f93179706ab9f9d19807247f69dc2555bf', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: '920f5f8a7d98d4c30644584ee4177b7f0b9d4ce6', class: "metric-compact" }, index.h("span", { key: 'e27e9a6410f9e9ccb789500e23a0a6140d5cc56e', class: "metric-label" }, "FRAME"), index.h("span", { key: 'e2229a1d7c05c6503a6aaf85951dfbdb93d8922f', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: '33bc285a3ece3bc0887249fc158f63a34fbd84fd', class: "metrics-row" }, index.h("div", { key: '9904884ea06b18561e6996fc996c8522f01138b8', class: "metric-compact" }, index.h("span", { key: 'df4bc26fe4af5e1203cbc1c82d104ac1c64a0d31', class: "metric-label" }, "DET"), index.h("span", { key: '760931f01f32761f80bedb579652018cedeb6edb', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: '9f1e5b50979e54fdb58ade2df5e1a990b8ddd1a3', class: "metric-compact" }, index.h("span", { key: '698e460b3b7b13747cdc27743348c580eab337ad', class: "metric-label" }, "RATE"), index.h("span", { key: '466b3b790ce6d3c680bb7629592b67e57be3cee5', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: '2c093210e7b929967b690c60cfc0943274db2409', class: "watermark" }, index.h("img", { key: '9e30dc6b9cd294e5dc3059ea98c61a45a50c4255', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
2464
+ : 'Saltar reverso')))), captureState.isVideoActive && (index.h("div", { key: 'bd0303e8ce85211e4c68fc3cd79545db7c74288b', class: "camera-controls" }, index.h("button", { key: 'e4327d260b988b67e0de3654c72f24455cd3bbbd', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (index.h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (index.h("div", { key: '35da16baf4c62466c2a66650209a1daf842c0db2', class: "camera-selector-dropdown" }, index.h("div", { key: '7035dd0e22f614cf239a015c6d59531b68fa74a9', class: "camera-selector-header" }, index.h("span", { key: 'ae026f31931f52ea0daf72391a9de4ff9e4b88f1' }, "Seleccionar C\u00E1mara"), index.h("button", { key: '40f2ba0770bafea71fedbf849938010e743be776', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: '6f26927094a724a35ebb07ef701caf14f296a837', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (index.h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, index.h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (index.h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (index.h("span", { class: "selected-indicator" }, "\u2713")))))), index.h("div", { key: '082236981c369f8fd487f036439b16e6560514be', class: "device-info" }, index.h("small", { key: 'ccbbfa0bca96395f574bda71103338ae9226f709' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '6baa487409503777e90453b39fd30649941cbbe4', class: "manual-capture-section" }, index.h("button", { key: '893f4715ca3315613f779603f178459955f686f0', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (index.h("span", { key: '7c30b680fe84f4c8227beba45d8416f1edb8348f', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (index.h("div", { key: 'd8ef052cf67d7bda4b3534febf96efa2cdcc6bf7', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: '3dae7634e770dda2655b1042ec8fb997b3cb2220', class: "flip-animation" }, index.h("div", { key: 'a977fc5e889369121bacc06cf5213dfee39d858d', class: "id-card-icon" }), index.h("div", { key: '4f052cdc8c7b493cddce90e108e976325c9bab8e', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: '5117cd6e63e1e0763c1503f6f60f3c7ef60dd665', class: "success-animation" }, index.h("div", { key: 'd11128c45c7118915cc0dd9c25845bb55b4b3eb2', class: "check-icon" }), index.h("div", { key: '693e197bf8a4c91ac7ddedb457d15aa3aa46a5d5', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: '89caaa9d78eb522e0fadfd1c0e844e05d29b8286', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: '3f41d40b05b71e13e7e1d0c0e980b48308cba9d6', class: "status-spinner" })), index.h("div", { key: '8674480928f4f4a77099c0600ed84f0e27903379', class: "status-content" }, index.h("div", { key: 'e88f4044ab8115a6a5cbb78cc4030975eef9149a', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: 'ad581e42c30c304b837711549148c11b801dee5e', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: '1b371c1b47f3ba699885db00fef52eb448f9e245', class: "performance-monitor" }, index.h("div", { key: '8b9ac60449a15e9fb966c329b2e6c9285c0228c0', class: "performance-expanded" }, index.h("div", { key: '760c11a3ed28e248a144d53a92fd8adee6822ff2', class: "metrics-row" }, index.h("div", { key: '858ab8b106f1d12210e4e15c423ab3bab11bdb60', class: "metric-compact" }, index.h("span", { key: '65fe2479c3822d99452e7e5faa93c25a80765024', class: "metric-label" }, "FPS"), index.h("span", { key: '9faf7763d1284a6f3c1e15fb0f3c99f1d45bb4dc', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: 'c95ed8fa8101f42bfba5cc8ca7debbad028caf42', class: "metric-compact" }, index.h("span", { key: '3555b3778cb71e959d132d6aa563f58f757915d8', class: "metric-label" }, "MEM"), index.h("span", { key: 'd72b0491147a6f80dcf33f5d72dd8dae5898f44a', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: 'cd4dc81f28f9e59a36a0295dde7bfee89f50d3e2', class: "metrics-row" }, index.h("div", { key: 'c43d8624c5da256ce3e97f67b022bdd98fb04e07', class: "metric-compact" }, index.h("span", { key: 'adb402d83809a90580b81544f31b5331bdff59dd', class: "metric-label" }, "INF"), index.h("span", { key: 'e48c9814c08acbe321460a29e42cb49b0eab38ff', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: '71517f9279e9a6aa0d5b02e2169db868ca3fb676', class: "metric-compact" }, index.h("span", { key: '3ddbcc56b137aea702a40f900178e8e16efd146e', class: "metric-label" }, "FRAME"), index.h("span", { key: 'd3a70afb511d3f72b485b3a1cb922876faa62b11', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: 'a3b62cdbe830b1024ab96f7fb726825df24fecdc', class: "metrics-row" }, index.h("div", { key: '95471e5cdde748d1ce6bdee3540740dd3aa66865', class: "metric-compact" }, index.h("span", { key: 'c93e01c0bdc67621c3a666357d2477b7a5275d52', class: "metric-label" }, "DET"), index.h("span", { key: 'ef33c86a83101b0989b14c7f1ab0ce0b331b397f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: 'e14d56128bf96337e4c3859f8e4003e001e8e953', class: "metric-compact" }, index.h("span", { key: '8aa828632f0a977a27ed377f103bb8d5e7e0ed1a', class: "metric-label" }, "RATE"), index.h("span", { key: '59035c473b5a747a27f774e2554322a084b6d19f', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: '4d0c2de00954416faac30ab622fb21f36c932aca', class: "watermark" }, index.h("img", { key: 'd8f775428ebb3ba86a943ca73b074db2180d0c29', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
2001
2465
  }
2002
2466
  // Utility methods
2003
2467
  updateDetectionBoxes(boxes) {
@@ -2113,9 +2577,24 @@ const JaakStamps = class {
2113
2577
  }
2114
2578
  }
2115
2579
  async takeManualScreenshot() {
2116
- if (!this.videoRef)
2580
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
2581
+ // Set processing state immediately
2582
+ const captureState = this.stateManager?.getCaptureState();
2583
+ if (captureState?.step === 'front') {
2584
+ this.processingButton = 'capture-front';
2585
+ }
2586
+ else if (captureState?.step === 'back') {
2587
+ this.processingButton = 'capture-back';
2588
+ }
2589
+ console.log('🔵 processingButton set to:', this.processingButton);
2590
+ // Add small delay to show spinner
2591
+ await new Promise(resolve => setTimeout(resolve, 100));
2592
+ if (!this.videoRef) {
2593
+ this.processingButton = null;
2117
2594
  return;
2595
+ }
2118
2596
  // When using manual capture, use mask coordinates for cropping
2597
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
2119
2598
  await this.takeScreenshotWithMaskCoordinates();
2120
2599
  }
2121
2600
  async takeScreenshotWithMaskCoordinates() {
@@ -2191,6 +2670,8 @@ const JaakStamps = class {
2191
2670
  this.completeProcess(true);
2192
2671
  this.returnCanvasToPool(captureCanvas);
2193
2672
  this.returnCanvasToPool(croppedCanvas);
2673
+ // Clear processing button when passport is detected
2674
+ this.processingButton = null;
2194
2675
  return;
2195
2676
  }
2196
2677
  }
@@ -2204,6 +2685,8 @@ const JaakStamps = class {
2204
2685
  showFlipAnimation: true
2205
2686
  });
2206
2687
  this.triggerRerender();
2688
+ // Progressive resource cleanup after front capture
2689
+ await this.onFrontCaptureComplete();
2207
2690
  setTimeout(() => {
2208
2691
  this.stateManager.updateCaptureState({
2209
2692
  showFlipAnimation: false,
@@ -2211,6 +2694,8 @@ const JaakStamps = class {
2211
2694
  });
2212
2695
  this.triggerRerender();
2213
2696
  this.startBackDocumentTimer();
2697
+ // Clear processing button after animation completes
2698
+ this.processingButton = null;
2214
2699
  }, 3000);
2215
2700
  }
2216
2701
  else if (captureState.step === 'back') {
@@ -2221,6 +2706,8 @@ const JaakStamps = class {
2221
2706
  }
2222
2707
  });
2223
2708
  this.completeProcess(false);
2709
+ // Clear processing button after back capture completes
2710
+ this.processingButton = null;
2224
2711
  }
2225
2712
  // Return canvases to pool after use
2226
2713
  this.returnCanvasToPool(captureCanvas);
@@ -2275,6 +2762,8 @@ const JaakStamps = class {
2275
2762
  isDetectionPaused: true,
2276
2763
  showFlipAnimation: true
2277
2764
  });
2765
+ // Progressive resource cleanup after front capture
2766
+ await this.onFrontCaptureComplete();
2278
2767
  setTimeout(() => {
2279
2768
  this.stateManager.updateCaptureState({
2280
2769
  showFlipAnimation: false,
@@ -2314,6 +2803,9 @@ const JaakStamps = class {
2314
2803
  this.stopPerformanceMonitoring();
2315
2804
  // Liberar recursos de ONNX
2316
2805
  this.releaseOnnxResources();
2806
+ // Resetear máscara a blanco y limpiar detecciones
2807
+ this.resetMaskToWhite();
2808
+ this.detectionBoxes = [];
2317
2809
  this.hasAutoSwitchedToManual = true;
2318
2810
  this.performanceDegradedMode = true;
2319
2811
  this.showManualCaptureButton = true;
@@ -2324,6 +2816,75 @@ const JaakStamps = class {
2324
2816
  }, 5000);
2325
2817
  }
2326
2818
  // Método para cambiar a modo manual por errores de ONNX
2819
+ switchToManualModeWithWarning(warningMessage) {
2820
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2821
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2822
+ return;
2823
+ }
2824
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
2825
+ // Resetear máscara a blanco y limpiar detecciones
2826
+ this.resetMaskToWhite();
2827
+ this.detectionBoxes = [];
2828
+ // Cambiar a modo manual
2829
+ this.hasAutoSwitchedToManual = true;
2830
+ this.performanceDegradedMode = true;
2831
+ this.showManualCaptureButton = true;
2832
+ this.useDocumentDetector = false; // Desactivar detector automático
2833
+ // Actualizar el estado con mensaje informativo (no error)
2834
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
2835
+ // Continuar con la configuración de cámara
2836
+ this.continueWithCameraSetup();
2837
+ }
2838
+ async continueWithCameraSetup() {
2839
+ try {
2840
+ // Paso 2: Detectar y configurar dispositivos
2841
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
2842
+ try {
2843
+ await this.cameraService.enumerateDevices();
2844
+ }
2845
+ catch (enumerateError) {
2846
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
2847
+ }
2848
+ try {
2849
+ await this.updateCameraInfoWithAutofocus();
2850
+ }
2851
+ catch (cameraInfoError) {
2852
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
2853
+ }
2854
+ // Paso 3: Configurar cámara seleccionada
2855
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
2856
+ let stream;
2857
+ try {
2858
+ stream = await this.cameraService.setupCamera();
2859
+ }
2860
+ catch (setupError) {
2861
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
2862
+ }
2863
+ // Paso 4: Inicializar video
2864
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
2865
+ try {
2866
+ await this.initializeVideoStream(stream);
2867
+ }
2868
+ catch (videoError) {
2869
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
2870
+ }
2871
+ // Paso 5: Calibrar máscara
2872
+ if (this.detectionContainer) {
2873
+ const container = this.detectionContainer.parentElement;
2874
+ const rect = container.getBoundingClientRect();
2875
+ this.updateMaskDimensions(rect);
2876
+ }
2877
+ // Finalizar
2878
+ this.startTime = Date.now();
2879
+ this.stateManager.updateCaptureState({ isLoading: false });
2880
+ // Mostrar botón manual (ya debe estar habilitado)
2881
+ this.showManualCaptureButton = true;
2882
+ }
2883
+ catch (error) {
2884
+ this.updateStatus('Error de cámara', error.message, 'error');
2885
+ throw error;
2886
+ }
2887
+ }
2327
2888
  switchToManualModeWithError(errorMessage) {
2328
2889
  // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2329
2890
  if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
@@ -2331,6 +2892,9 @@ const JaakStamps = class {
2331
2892
  }
2332
2893
  // Liberar recursos de ONNX inmediatamente
2333
2894
  this.releaseOnnxResources();
2895
+ // Resetear máscara a blanco y limpiar detecciones
2896
+ this.resetMaskToWhite();
2897
+ this.detectionBoxes = [];
2334
2898
  // Cambiar a modo manual
2335
2899
  this.hasAutoSwitchedToManual = true;
2336
2900
  this.performanceDegradedMode = true;
@@ -2377,6 +2941,16 @@ const JaakStamps = class {
2377
2941
  setTimeout(() => {
2378
2942
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
2379
2943
  }, 3000);
2944
+ // Aggressive cleanup after process completion with delay
2945
+ setTimeout(async () => {
2946
+ const cleanupResult = await this.aggressiveResourceCleanup();
2947
+ if (this.debug) {
2948
+ console.log('🧹 Post-completion cleanup:', cleanupResult.freed);
2949
+ if (cleanupResult.errors.length > 0) {
2950
+ console.warn('⚠️ Cleanup errors:', cleanupResult.errors);
2951
+ }
2952
+ }
2953
+ }, 1000); // Wait 1 second after completion
2380
2954
  }
2381
2955
  stopDetection() {
2382
2956
  if (this.animationId) {
@@ -2581,6 +3155,24 @@ const JaakStamps = class {
2581
3155
  // Performance is good, use base frame skip
2582
3156
  return this.BASE_FRAME_SKIP;
2583
3157
  }
3158
+ // Método para resetear la máscara a color blanco
3159
+ resetMaskToWhite() {
3160
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
3161
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
3162
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
3163
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
3164
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
3165
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
3166
+ // Remover todas las clases de alineación y estado perfecto
3167
+ cardOutline?.classList.remove('perfect-match');
3168
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
3169
+ topSide?.classList.remove('aligned');
3170
+ rightSide?.classList.remove('aligned');
3171
+ bottomSide?.classList.remove('aligned');
3172
+ leftSide?.classList.remove('aligned');
3173
+ // Resetear estado de alineación
3174
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
3175
+ }
2584
3176
  // Canvas pool management for screenshots
2585
3177
  getPooledCanvas(width, height) {
2586
3178
  // Try to find a canvas with matching dimensions