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