@jaak.ai/stamps 2.0.0-dev.40 → 2.0.0-dev.41

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 (43) hide show
  1. package/README.md +4 -1
  2. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
  3. package/dist/cjs/jaak-stamps.cjs.entry.js +868 -11
  4. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  5. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  6. package/dist/cjs/loader.cjs.js +1 -1
  7. package/dist/collection/components/my-component/my-component.css +66 -0
  8. package/dist/collection/components/my-component/my-component.js +494 -8
  9. package/dist/collection/components/my-component/my-component.js.map +1 -1
  10. package/dist/collection/services/CameraService.js +161 -0
  11. package/dist/collection/services/CameraService.js.map +1 -1
  12. package/dist/collection/services/DetectionService.js +164 -1
  13. package/dist/collection/services/DetectionService.js.map +1 -1
  14. package/dist/collection/services/ImageQualityService.js +329 -0
  15. package/dist/collection/services/ImageQualityService.js.map +1 -0
  16. package/dist/collection/services/ServiceContainer.js +6 -1
  17. package/dist/collection/services/ServiceContainer.js.map +1 -1
  18. package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
  19. package/dist/collection/services/interfaces/IDetectionService.js.map +1 -1
  20. package/dist/collection/services/interfaces/IImageQualityService.js +2 -0
  21. package/dist/collection/services/interfaces/IImageQualityService.js.map +1 -0
  22. package/dist/components/jaak-stamps.js +885 -12
  23. package/dist/components/jaak-stamps.js.map +1 -1
  24. package/dist/esm/jaak-stamps-webcomponent.js +1 -1
  25. package/dist/esm/jaak-stamps.entry.js +868 -11
  26. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  27. package/dist/esm/loader.js +1 -1
  28. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  29. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  30. package/dist/jaak-stamps-webcomponent/p-47f37982.entry.js +2 -0
  31. package/dist/jaak-stamps-webcomponent/p-47f37982.entry.js.map +1 -0
  32. package/dist/types/components/my-component/my-component.d.ts +48 -0
  33. package/dist/types/components.d.ts +65 -0
  34. package/dist/types/services/CameraService.d.ts +5 -0
  35. package/dist/types/services/DetectionService.d.ts +22 -2
  36. package/dist/types/services/ImageQualityService.d.ts +31 -0
  37. package/dist/types/services/ServiceContainer.d.ts +7 -0
  38. package/dist/types/services/interfaces/ICameraService.d.ts +2 -0
  39. package/dist/types/services/interfaces/IDetectionService.d.ts +13 -0
  40. package/dist/types/services/interfaces/IImageQualityService.d.ts +58 -0
  41. package/package.json +1 -1
  42. package/dist/jaak-stamps-webcomponent/p-c30c7b47.entry.js +0 -2
  43. package/dist/jaak-stamps-webcomponent/p-c30c7b47.entry.js.map +0 -1
@@ -449,6 +449,8 @@ class CameraService {
449
449
  const capabilities = videoTrack.getCapabilities();
450
450
  tempStream.getTracks().forEach(track => track.stop());
451
451
  const constraints = { ...videoConstraints };
452
+ // Enhanced focus and image quality settings
453
+ this.applyAdvancedCameraSettings(constraints, capabilities);
452
454
  if (capabilities.width && capabilities.height) {
453
455
  const maxWidth = Math.min(capabilities.width.max, 1920);
454
456
  const maxHeight = Math.min(capabilities.height.max, 1080);
@@ -462,6 +464,10 @@ class CameraService {
462
464
  constraints.height = { ideal: maxHeight };
463
465
  }
464
466
  }
467
+ this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
468
+ constraints,
469
+ capabilities: this.getCapabilitiesSummary(capabilities)
470
+ });
465
471
  return constraints;
466
472
  }
467
473
  catch (err) {
@@ -471,6 +477,8 @@ class CameraService {
471
477
  width: { ideal: isTablet ? 1280 : 1920 },
472
478
  height: { ideal: isTablet ? 720 : 1080 }
473
479
  };
480
+ // Apply basic focus settings even for fallback
481
+ this.applyBasicFocusSettings(fallbackConstraints);
474
482
  if (this.selectedCameraId) {
475
483
  fallbackConstraints.deviceId = { exact: this.selectedCameraId };
476
484
  }
@@ -483,6 +491,159 @@ class CameraService {
483
491
  return fallbackConstraints;
484
492
  }
485
493
  }
494
+ applyAdvancedCameraSettings(constraints, capabilities) {
495
+ // Apply standard camera settings that are widely supported
496
+ // Frame rate optimization
497
+ if (capabilities.frameRate) {
498
+ constraints.frameRate = {
499
+ ideal: 30,
500
+ min: 15,
501
+ max: 60
502
+ };
503
+ }
504
+ // Width and height constraints for optimal resolution
505
+ if (capabilities.width && capabilities.height) {
506
+ constraints.width = {
507
+ ideal: Math.min(1920, capabilities.width.max || 1920),
508
+ min: 640
509
+ };
510
+ constraints.height = {
511
+ ideal: Math.min(1080, capabilities.height.max || 1080),
512
+ min: 480
513
+ };
514
+ }
515
+ // Apply enhanced constraints through advanced array for better browser compatibility
516
+ const advancedConstraints = [];
517
+ // Try to set optimal settings through advanced constraints
518
+ const advanced = {};
519
+ // Focus mode - use 'any' type to bypass TypeScript limitations
520
+ if (capabilities.focusMode) {
521
+ if (capabilities.focusMode.includes('continuous')) {
522
+ advanced.focusMode = 'continuous';
523
+ }
524
+ else if (capabilities.focusMode.includes('single-shot')) {
525
+ advanced.focusMode = 'single-shot';
526
+ }
527
+ }
528
+ // Focus distance for close objects
529
+ if (capabilities.focusDistance) {
530
+ advanced.focusDistance = {
531
+ ideal: 0.4, // 40cm - optimal for document capture
532
+ min: 0.2,
533
+ max: 1.0
534
+ };
535
+ }
536
+ // Zoom control
537
+ if (capabilities.zoom) {
538
+ advanced.zoom = {
539
+ ideal: 1.0,
540
+ min: capabilities.zoom.min,
541
+ max: Math.min(capabilities.zoom.max, 3.0)
542
+ };
543
+ }
544
+ // Add advanced constraints if any were set
545
+ if (Object.keys(advanced).length > 0) {
546
+ advancedConstraints.push(advanced);
547
+ }
548
+ if (advancedConstraints.length > 0) {
549
+ constraints.advanced = advancedConstraints;
550
+ }
551
+ }
552
+ applyBasicFocusSettings(constraints) {
553
+ // Apply basic focus settings when capabilities are not available
554
+ // Use advanced constraints for non-standard properties
555
+ const advanced = {
556
+ focusMode: 'continuous',
557
+ exposureMode: 'continuous',
558
+ whiteBalanceMode: 'continuous'
559
+ };
560
+ if (!constraints.advanced) {
561
+ constraints.advanced = [];
562
+ }
563
+ constraints.advanced.push(advanced);
564
+ constraints.frameRate = { ideal: 30 };
565
+ }
566
+ getCapabilitiesSummary(capabilities) {
567
+ return {
568
+ hasAutoFocus: !!capabilities.focusMode,
569
+ hasFocusDistance: !!capabilities.focusDistance,
570
+ hasExposureControl: !!capabilities.exposureMode,
571
+ hasWhiteBalance: !!capabilities.whiteBalanceMode,
572
+ hasZoom: !!capabilities.zoom,
573
+ hasTorch: capabilities.torch !== undefined,
574
+ hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
575
+ resolutionRange: capabilities.width && capabilities.height ?
576
+ `${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
577
+ };
578
+ }
579
+ // New method to enable torch/flash for better illumination
580
+ async setTorchEnabled(enabled, stream) {
581
+ try {
582
+ if (!stream) {
583
+ this.logger.warn('No hay stream disponible para controlar el flash');
584
+ return false;
585
+ }
586
+ const videoTrack = stream.getVideoTracks()[0];
587
+ if (!videoTrack) {
588
+ this.logger.warn('No hay track de video disponible');
589
+ return false;
590
+ }
591
+ const capabilities = videoTrack.getCapabilities();
592
+ if (capabilities.torch === undefined) {
593
+ this.logger.warn('El dispositivo no soporta control de flash');
594
+ return false;
595
+ }
596
+ await videoTrack.applyConstraints({
597
+ advanced: [{ torch: enabled }]
598
+ });
599
+ this.logger.state('FLASH_CONTROLADO', { enabled });
600
+ return true;
601
+ }
602
+ catch (error) {
603
+ this.logger.error('Error al controlar el flash:', error);
604
+ return false;
605
+ }
606
+ }
607
+ // Method to manually trigger focus on a specific point
608
+ async focusAtPoint(x, y, stream) {
609
+ try {
610
+ if (!stream) {
611
+ this.logger.warn('No hay stream disponible para enfocar');
612
+ return false;
613
+ }
614
+ const videoTrack = stream.getVideoTracks()[0];
615
+ if (!videoTrack) {
616
+ this.logger.warn('No hay track de video disponible');
617
+ return false;
618
+ }
619
+ const capabilities = videoTrack.getCapabilities();
620
+ if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
621
+ this.logger.warn('El dispositivo no soporta enfoque manual');
622
+ return false;
623
+ }
624
+ // Trigger single-shot focus
625
+ await videoTrack.applyConstraints({
626
+ advanced: [{ focusMode: 'single-shot' }]
627
+ });
628
+ // Return to continuous focus after a short delay
629
+ setTimeout(async () => {
630
+ try {
631
+ await videoTrack.applyConstraints({
632
+ advanced: [{ focusMode: 'continuous' }]
633
+ });
634
+ }
635
+ catch (error) {
636
+ this.logger.warn('Error al volver a enfoque continuo:', error);
637
+ }
638
+ }, 1000);
639
+ this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
640
+ return true;
641
+ }
642
+ catch (error) {
643
+ this.logger.error('Error al enfocar manualmente:', error);
644
+ return false;
645
+ }
646
+ }
486
647
  }
487
648
 
488
649
  class LowMemoryDeviceStrategy {
@@ -559,15 +720,346 @@ class DeviceStrategyFactory {
559
720
  }
560
721
  }
561
722
 
723
+ class ImageQualityService {
724
+ logger;
725
+ // Thresholds for quality assessment
726
+ BLUR_THRESHOLD = 80; // Reduced from 100 to 80 for better acceptance
727
+ FOCUS_THRESHOLD = 40; // Reduced from 50 to 40 for better acceptance
728
+ REFLECTION_THRESHOLD = 240; // Pixel brightness threshold for reflections
729
+ MIN_BRIGHTNESS = 50; // Minimum acceptable brightness
730
+ MAX_BRIGHTNESS = 200; // Maximum acceptable brightness
731
+ MIN_CONTRAST = 30; // Minimum acceptable contrast
732
+ constructor(logger) {
733
+ this.logger = logger;
734
+ }
735
+ analyzeImageQuality(canvas, documentBounds) {
736
+ const ctx = canvas.getContext('2d');
737
+ if (!ctx) {
738
+ throw new Error('Unable to get canvas context');
739
+ }
740
+ // Get image data for the document area or full canvas
741
+ let imageData;
742
+ if (documentBounds) {
743
+ imageData = ctx.getImageData(documentBounds.x, documentBounds.y, documentBounds.w, documentBounds.h);
744
+ }
745
+ else {
746
+ imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
747
+ }
748
+ // Perform all quality assessments
749
+ const blur = this.detectBlur(imageData);
750
+ const reflection = this.detectReflections(imageData);
751
+ const focus = this.measureFocus(imageData);
752
+ const brightness = this.measureBrightness(imageData);
753
+ const contrast = this.measureContrast(imageData);
754
+ // Calculate overall quality score (0-100)
755
+ const qualityScore = this.calculateOverallQuality(blur, reflection, focus, brightness, contrast);
756
+ const overallQuality = this.getQualityLevel(qualityScore);
757
+ // Identify issues and recommendations
758
+ const issues = this.identifyIssues(blur, reflection, focus, brightness, contrast);
759
+ const recommendations = this.getQualityRecommendations({
760
+ blur, reflection, focus, brightness, contrast,
761
+ overallQuality, qualityScore, issues, recommendations: []
762
+ });
763
+ const result = {
764
+ blur,
765
+ reflection,
766
+ focus,
767
+ brightness,
768
+ contrast,
769
+ overallQuality,
770
+ qualityScore,
771
+ issues,
772
+ recommendations
773
+ };
774
+ this.logger.state('CALIDAD_IMAGEN_ANALIZADA', {
775
+ qualityScore,
776
+ overallQuality,
777
+ issues: issues.length,
778
+ hasBlur: !blur.isAcceptable,
779
+ hasReflection: reflection.hasReflection,
780
+ isFocused: focus.isFocused
781
+ });
782
+ return result;
783
+ }
784
+ detectBlur(imageData) {
785
+ const { data, width, height } = imageData;
786
+ let totalVariance = 0;
787
+ let pixelCount = 0;
788
+ // Calculate Laplacian variance for blur detection
789
+ for (let y = 1; y < height - 1; y++) {
790
+ for (let x = 1; x < width - 1; x++) {
791
+ const idx = (y * width + x) * 4;
792
+ // Convert to grayscale
793
+ const gray = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];
794
+ // Calculate Laplacian (edge detection)
795
+ const neighbors = [
796
+ this.getGrayscale(data, x - 1, y - 1, width),
797
+ this.getGrayscale(data, x, y - 1, width),
798
+ this.getGrayscale(data, x + 1, y - 1, width),
799
+ this.getGrayscale(data, x - 1, y, width),
800
+ this.getGrayscale(data, x + 1, y, width),
801
+ this.getGrayscale(data, x - 1, y + 1, width),
802
+ this.getGrayscale(data, x, y + 1, width),
803
+ this.getGrayscale(data, x + 1, y + 1, width)
804
+ ];
805
+ const laplacian = 8 * gray - neighbors.reduce((sum, val) => sum + val, 0);
806
+ totalVariance += laplacian * laplacian;
807
+ pixelCount++;
808
+ }
809
+ }
810
+ const blurScore = Math.sqrt(totalVariance / pixelCount);
811
+ const isAcceptable = blurScore > this.BLUR_THRESHOLD;
812
+ return {
813
+ blurScore,
814
+ isAcceptable,
815
+ threshold: this.BLUR_THRESHOLD
816
+ };
817
+ }
818
+ detectReflections(imageData) {
819
+ const { data, width, height } = imageData;
820
+ let reflectionPixels = 0;
821
+ const totalPixels = width * height;
822
+ const reflectionAreas = [];
823
+ // Track bright regions that could be reflections
824
+ const brightRegions = Array(height).fill(null).map(() => Array(width).fill(false));
825
+ for (let y = 0; y < height; y++) {
826
+ for (let x = 0; x < width; x++) {
827
+ const idx = (y * width + x) * 4;
828
+ const brightness = (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
829
+ if (brightness > this.REFLECTION_THRESHOLD) {
830
+ brightRegions[y][x] = true;
831
+ reflectionPixels++;
832
+ }
833
+ }
834
+ }
835
+ // Find connected bright regions (potential reflections)
836
+ const visited = Array(height).fill(null).map(() => Array(width).fill(false));
837
+ for (let y = 0; y < height; y++) {
838
+ for (let x = 0; x < width; x++) {
839
+ if (brightRegions[y][x] && !visited[y][x]) {
840
+ const region = this.findConnectedRegion(brightRegions, visited, x, y, width, height);
841
+ if (region.size > 100) { // Minimum size for a reflection
842
+ reflectionAreas.push({
843
+ x: region.minX,
844
+ y: region.minY,
845
+ width: region.maxX - region.minX,
846
+ height: region.maxY - region.minY
847
+ });
848
+ }
849
+ }
850
+ }
851
+ }
852
+ const reflectionScore = (reflectionPixels / totalPixels) * 100;
853
+ const hasReflection = reflectionScore > 5 || reflectionAreas.length > 0; // 5% threshold
854
+ return {
855
+ reflectionScore,
856
+ hasReflection,
857
+ threshold: 5,
858
+ reflectionAreas
859
+ };
860
+ }
861
+ measureFocus(imageData) {
862
+ const { data, width, height } = imageData;
863
+ let edgeStrength = 0;
864
+ let edgeCount = 0;
865
+ // Sobel edge detection for focus measurement
866
+ for (let y = 1; y < height - 1; y++) {
867
+ for (let x = 1; x < width - 1; x++) {
868
+ const gx = this.sobelX(data, x, y, width);
869
+ const gy = this.sobelY(data, x, y, width);
870
+ const magnitude = Math.sqrt(gx * gx + gy * gy);
871
+ edgeStrength += magnitude;
872
+ edgeCount++;
873
+ }
874
+ }
875
+ const focusScore = edgeCount > 0 ? edgeStrength / edgeCount : 0;
876
+ const isFocused = focusScore > this.FOCUS_THRESHOLD;
877
+ return {
878
+ focusScore,
879
+ isFocused,
880
+ threshold: this.FOCUS_THRESHOLD,
881
+ edgeStrength
882
+ };
883
+ }
884
+ measureBrightness(imageData) {
885
+ const { data } = imageData;
886
+ let totalBrightness = 0;
887
+ const pixelCount = data.length / 4;
888
+ for (let i = 0; i < data.length; i += 4) {
889
+ const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
890
+ totalBrightness += brightness;
891
+ }
892
+ const brightness = totalBrightness / pixelCount;
893
+ const isAcceptable = brightness >= this.MIN_BRIGHTNESS && brightness <= this.MAX_BRIGHTNESS;
894
+ return {
895
+ brightness,
896
+ isAcceptable,
897
+ minThreshold: this.MIN_BRIGHTNESS,
898
+ maxThreshold: this.MAX_BRIGHTNESS
899
+ };
900
+ }
901
+ measureContrast(imageData) {
902
+ const { data } = imageData;
903
+ let min = 255;
904
+ let max = 0;
905
+ for (let i = 0; i < data.length; i += 4) {
906
+ const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
907
+ min = Math.min(min, brightness);
908
+ max = Math.max(max, brightness);
909
+ }
910
+ const contrast = max - min;
911
+ const isAcceptable = contrast > this.MIN_CONTRAST;
912
+ return {
913
+ contrast,
914
+ isAcceptable,
915
+ threshold: this.MIN_CONTRAST
916
+ };
917
+ }
918
+ getQualityRecommendations(result) {
919
+ const recommendations = [];
920
+ if (!result.blur.isAcceptable) {
921
+ recommendations.push('Mantenga el dispositivo estable para reducir el desenfoque');
922
+ recommendations.push('Acérquese al documento para mejorar la nitidez');
923
+ }
924
+ if (result.reflection.hasReflection) {
925
+ recommendations.push('Cambie el ángulo del dispositivo para evitar reflejos');
926
+ recommendations.push('Mejore la iluminación indirecta del área');
927
+ }
928
+ if (!result.focus.isFocused) {
929
+ recommendations.push('Permita que la cámara enfoque automáticamente');
930
+ recommendations.push('Toque la pantalla sobre el documento para enfocar');
931
+ }
932
+ if (!result.brightness.isAcceptable) {
933
+ if (result.brightness.brightness < result.brightness.minThreshold) {
934
+ recommendations.push('Aumente la iluminación del área');
935
+ recommendations.push('Muévase a un lugar con mejor luz');
936
+ }
937
+ else {
938
+ recommendations.push('Reduzca la iluminación directa sobre el documento');
939
+ recommendations.push('Evite la luz solar directa');
940
+ }
941
+ }
942
+ if (!result.contrast.isAcceptable) {
943
+ recommendations.push('Mejore la iluminación para aumentar el contraste');
944
+ recommendations.push('Asegúrese de que el fondo contraste con el documento');
945
+ }
946
+ if (recommendations.length === 0) {
947
+ recommendations.push('La calidad de la imagen es óptima para la captura');
948
+ }
949
+ return recommendations;
950
+ }
951
+ calculateOverallQuality(blur, reflection, focus, brightness, contrast) {
952
+ let score = 0;
953
+ // Blur contribution (25%)
954
+ score += blur.isAcceptable ? 25 : Math.max(0, (blur.blurScore / this.BLUR_THRESHOLD) * 25);
955
+ // Reflection contribution (20%)
956
+ score += reflection.hasReflection ? Math.max(0, 20 - reflection.reflectionScore * 2) : 20;
957
+ // Focus contribution (25%)
958
+ score += focus.isFocused ? 25 : Math.max(0, (focus.focusScore / this.FOCUS_THRESHOLD) * 25);
959
+ // Brightness contribution (15%)
960
+ if (brightness.isAcceptable) {
961
+ score += 15;
962
+ }
963
+ else {
964
+ const brightnessPenalty = Math.abs(brightness.brightness - (brightness.minThreshold + brightness.maxThreshold) / 2);
965
+ score += Math.max(0, 15 - brightnessPenalty / 10);
966
+ }
967
+ // Contrast contribution (15%)
968
+ score += contrast.isAcceptable ? 15 : Math.max(0, (contrast.contrast / this.MIN_CONTRAST) * 15);
969
+ return Math.round(Math.max(0, Math.min(100, score)));
970
+ }
971
+ getQualityLevel(score) {
972
+ if (score >= 85)
973
+ return 'excellent';
974
+ if (score >= 70)
975
+ return 'good';
976
+ if (score >= 50)
977
+ return 'acceptable';
978
+ return 'poor';
979
+ }
980
+ identifyIssues(blur, reflection, focus, brightness, contrast) {
981
+ const issues = [];
982
+ if (!blur.isAcceptable) {
983
+ issues.push('Imagen desenfocada por movimiento');
984
+ }
985
+ if (reflection.hasReflection) {
986
+ issues.push('Reflejos detectados en el documento');
987
+ }
988
+ if (!focus.isFocused) {
989
+ issues.push('Documento fuera de foco');
990
+ }
991
+ if (!brightness.isAcceptable) {
992
+ if (brightness.brightness < brightness.minThreshold) {
993
+ issues.push('Iluminación insuficiente');
994
+ }
995
+ else {
996
+ issues.push('Imagen sobreexpuesta');
997
+ }
998
+ }
999
+ if (!contrast.isAcceptable) {
1000
+ issues.push('Contraste insuficiente');
1001
+ }
1002
+ return issues;
1003
+ }
1004
+ getGrayscale(data, x, y, width) {
1005
+ const idx = (y * width + x) * 4;
1006
+ return 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2];
1007
+ }
1008
+ findConnectedRegion(brightRegions, visited, startX, startY, width, height) {
1009
+ const stack = [{ x: startX, y: startY }];
1010
+ let size = 0;
1011
+ let minX = startX, maxX = startX, minY = startY, maxY = startY;
1012
+ while (stack.length > 0) {
1013
+ const { x, y } = stack.pop();
1014
+ if (x < 0 || x >= width || y < 0 || y >= height || visited[y][x] || !brightRegions[y][x]) {
1015
+ continue;
1016
+ }
1017
+ visited[y][x] = true;
1018
+ size++;
1019
+ minX = Math.min(minX, x);
1020
+ maxX = Math.max(maxX, x);
1021
+ minY = Math.min(minY, y);
1022
+ maxY = Math.max(maxY, y);
1023
+ // Add adjacent pixels
1024
+ stack.push({ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 });
1025
+ }
1026
+ return { size, minX, maxX, minY, maxY };
1027
+ }
1028
+ sobelX(data, x, y, width) {
1029
+ const kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
1030
+ let sum = 0;
1031
+ for (let ky = -1; ky <= 1; ky++) {
1032
+ for (let kx = -1; kx <= 1; kx++) {
1033
+ const gray = this.getGrayscale(data, x + kx, y + ky, width);
1034
+ sum += gray * kernel[ky + 1][kx + 1];
1035
+ }
1036
+ }
1037
+ return sum;
1038
+ }
1039
+ sobelY(data, x, y, width) {
1040
+ const kernel = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
1041
+ let sum = 0;
1042
+ for (let ky = -1; ky <= 1; ky++) {
1043
+ for (let kx = -1; kx <= 1; kx++) {
1044
+ const gray = this.getGrayscale(data, x + kx, y + ky, width);
1045
+ sum += gray * kernel[ky + 1][kx + 1];
1046
+ }
1047
+ }
1048
+ return sum;
1049
+ }
1050
+ }
1051
+
562
1052
  class DetectionService {
563
1053
  logger;
564
1054
  debug;
565
1055
  useDocumentClassification;
1056
+ qualityThresholds;
566
1057
  session;
567
1058
  mobileNetSession;
568
1059
  mobileNetClassMap;
569
1060
  modelLoaded = false;
570
1061
  deviceStrategy;
1062
+ imageQualityService;
571
1063
  MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/ddmyp-v2.onnx";
572
1064
  MOBILENET_MODEL_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.onnx";
573
1065
  MOBILENET_CLASSES_PATH = "https://storage.googleapis.com/jaak-static/web/component/stamps/cdmmp-v1.json";
@@ -577,11 +1069,13 @@ class DetectionService {
577
1069
  preprocessCanvas;
578
1070
  preprocessCtx;
579
1071
  captureCanvas;
580
- constructor(logger, debug = false, useDocumentClassification = false) {
1072
+ constructor(logger, debug = false, useDocumentClassification = false, qualityThresholds = {}) {
581
1073
  this.logger = logger;
582
1074
  this.debug = debug;
583
1075
  this.useDocumentClassification = useDocumentClassification;
1076
+ this.qualityThresholds = qualityThresholds;
584
1077
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
1078
+ this.imageQualityService = new ImageQualityService(logger);
585
1079
  this.initializeCanvasPool();
586
1080
  }
587
1081
  async loadModel() {
@@ -925,6 +1419,164 @@ class DetectionService {
925
1419
  }
926
1420
  return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
927
1421
  }
1422
+ // New methods for image quality validation
1423
+ validateImageQuality(video, detectedBox) {
1424
+ // Create a canvas to capture the current frame
1425
+ const canvas = document.createElement('canvas');
1426
+ canvas.width = video.videoWidth;
1427
+ canvas.height = video.videoHeight;
1428
+ const ctx = canvas.getContext('2d');
1429
+ ctx.drawImage(video, 0, 0);
1430
+ // If we have a detected document, focus quality analysis on that area
1431
+ let documentBounds;
1432
+ if (detectedBox) {
1433
+ const scaleX = video.videoWidth / this.INPUT_SIZE;
1434
+ const scaleY = video.videoHeight / this.INPUT_SIZE;
1435
+ documentBounds = {
1436
+ x: Math.max(0, detectedBox.x * scaleX),
1437
+ y: Math.max(0, detectedBox.y * scaleY),
1438
+ w: Math.min(detectedBox.w * scaleX, video.videoWidth),
1439
+ h: Math.min(detectedBox.h * scaleY, video.videoHeight)
1440
+ };
1441
+ }
1442
+ return this.imageQualityService.analyzeImageQuality(canvas, documentBounds);
1443
+ }
1444
+ isImageQualityAcceptable(qualityResult) {
1445
+ // Use configurable thresholds with fallback defaults
1446
+ const minQualityScore = this.qualityThresholds.minQualityScore ?? 45;
1447
+ const minFocusScore = this.qualityThresholds.minFocusScore ?? 16;
1448
+ const minBlurScore = this.qualityThresholds.minBlurScore ?? 22;
1449
+ const maxReflectionScore = this.qualityThresholds.maxReflectionScore ?? 15;
1450
+ // Check overall quality score - primary metric
1451
+ if (qualityResult.qualityScore < minQualityScore) {
1452
+ this.logger.state('QUALITY_REJECTION_SCORE', {
1453
+ qualityScore: qualityResult.qualityScore,
1454
+ minQualityScore,
1455
+ reason: 'score_too_low'
1456
+ });
1457
+ return false;
1458
+ }
1459
+ // Check focus score
1460
+ if (qualityResult.focus.focusScore < minFocusScore) {
1461
+ this.logger.state('QUALITY_REJECTION_FOCUS', {
1462
+ focusScore: qualityResult.focus.focusScore,
1463
+ minFocusScore,
1464
+ reason: 'severe_focus_issues'
1465
+ });
1466
+ return false;
1467
+ }
1468
+ // Check blur score
1469
+ if (qualityResult.blur.blurScore < minBlurScore) {
1470
+ this.logger.state('QUALITY_REJECTION_BLUR', {
1471
+ blurScore: qualityResult.blur.blurScore,
1472
+ minBlurScore,
1473
+ reason: 'severe_blur_issues'
1474
+ });
1475
+ return false;
1476
+ }
1477
+ // Check reflection score
1478
+ if (qualityResult.reflection.hasReflection && qualityResult.reflection.reflectionScore > maxReflectionScore) {
1479
+ this.logger.state('QUALITY_REJECTION_REFLECTION', {
1480
+ hasReflection: qualityResult.reflection.hasReflection,
1481
+ reflectionScore: qualityResult.reflection.reflectionScore,
1482
+ maxReflectionScore,
1483
+ reason: 'severe_reflection_issues'
1484
+ });
1485
+ return false;
1486
+ }
1487
+ this.logger.state('QUALITY_ACCEPTANCE', {
1488
+ qualityScore: qualityResult.qualityScore,
1489
+ focusScore: qualityResult.focus.focusScore,
1490
+ blurScore: qualityResult.blur.blurScore,
1491
+ reflectionScore: qualityResult.reflection.reflectionScore,
1492
+ issues: qualityResult.issues,
1493
+ reason: 'quality_acceptable'
1494
+ });
1495
+ return true;
1496
+ }
1497
+ updateQualityThresholds(thresholds) {
1498
+ this.qualityThresholds = { ...this.qualityThresholds, ...thresholds };
1499
+ this.logger.state('QUALITY_THRESHOLDS_UPDATED', {
1500
+ newThresholds: this.qualityThresholds
1501
+ });
1502
+ }
1503
+ getQualityThresholds() {
1504
+ return { ...this.qualityThresholds };
1505
+ }
1506
+ getQualityFeedback(qualityResult) {
1507
+ if (qualityResult.qualityScore >= 85) {
1508
+ return 'Calidad excelente - listo para capturar';
1509
+ }
1510
+ else if (qualityResult.qualityScore >= 70) {
1511
+ return 'Buena calidad - puede capturar';
1512
+ }
1513
+ else if (qualityResult.qualityScore >= 50) {
1514
+ const primaryIssue = qualityResult.issues[0];
1515
+ return primaryIssue ? `Calidad aceptable - ${primaryIssue.toLowerCase()}` : 'Calidad aceptable';
1516
+ }
1517
+ else {
1518
+ const mainIssues = qualityResult.issues.slice(0, 2).join(', ').toLowerCase();
1519
+ return `Calidad insuficiente - ${mainIssues}`;
1520
+ }
1521
+ }
1522
+ // Enhanced detection method that includes quality validation
1523
+ async runInferenceWithQuality(inputTensor, video) {
1524
+ const detections = await this.runInference(inputTensor);
1525
+ // Get the best detection for quality analysis
1526
+ const bestDetection = detections.length > 0 ?
1527
+ detections.reduce((best, current) => current.score > best.score ? current : best) :
1528
+ undefined;
1529
+ const qualityResult = this.validateImageQuality(video, bestDetection);
1530
+ const canCapture = this.isImageQualityAcceptable(qualityResult);
1531
+ const feedback = this.getQualityFeedback(qualityResult);
1532
+ this.logger.state('DETECCION_CON_CALIDAD', {
1533
+ detectionsCount: detections.length,
1534
+ qualityScore: qualityResult.qualityScore,
1535
+ canCapture,
1536
+ issues: qualityResult.issues.length,
1537
+ bestDetectionScore: bestDetection?.score || 0
1538
+ });
1539
+ return {
1540
+ detections,
1541
+ qualityResult,
1542
+ canCapture,
1543
+ feedback
1544
+ };
1545
+ }
1546
+ // Method to get real-time quality feedback without full analysis
1547
+ getQuickQualityFeedback(video) {
1548
+ const canvas = document.createElement('canvas');
1549
+ canvas.width = Math.min(video.videoWidth, 640); // Reduced resolution for performance
1550
+ canvas.height = Math.min(video.videoHeight, 480);
1551
+ const ctx = canvas.getContext('2d');
1552
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
1553
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1554
+ // Quick blur detection
1555
+ const blurMetrics = this.imageQualityService.detectBlur(imageData);
1556
+ // Quick reflection detection
1557
+ const reflectionMetrics = this.imageQualityService.detectReflections(imageData);
1558
+ // Quick focus measurement
1559
+ const focusMetrics = this.imageQualityService.measureFocus(imageData);
1560
+ let feedback = 'Analizando calidad...';
1561
+ if (!blurMetrics.isAcceptable) {
1562
+ feedback = 'Estabilice el dispositivo';
1563
+ }
1564
+ else if (reflectionMetrics.hasReflection) {
1565
+ feedback = 'Evite reflejos en el documento';
1566
+ }
1567
+ else if (!focusMetrics.isFocused) {
1568
+ feedback = 'Permitiendo enfoque automático...';
1569
+ }
1570
+ else {
1571
+ feedback = 'Calidad óptima';
1572
+ }
1573
+ return {
1574
+ hasBlur: !blurMetrics.isAcceptable,
1575
+ hasReflections: reflectionMetrics.hasReflection,
1576
+ isFocused: focusMetrics.isFocused,
1577
+ feedback
1578
+ };
1579
+ }
928
1580
  }
929
1581
 
930
1582
  class ServiceContainer {
@@ -938,7 +1590,12 @@ class ServiceContainer {
938
1590
  const logger = new LoggerService(config.debug);
939
1591
  const stateManager = new StateManagerService(eventBus);
940
1592
  const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
941
- const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
1593
+ const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification, {
1594
+ minQualityScore: config.minQualityScore,
1595
+ minFocusScore: config.minFocusScore,
1596
+ minBlurScore: config.minBlurScore,
1597
+ maxReflectionScore: config.maxReflectionScore
1598
+ });
942
1599
  // Register services
943
1600
  this.services.set('eventBus', eventBus);
944
1601
  this.services.set('logger', logger);
@@ -982,7 +1639,7 @@ class ServiceContainer {
982
1639
  }
983
1640
  }
984
1641
 
985
- const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#fff;font-size:14px;font-weight:600;text-align:center;white-space:normal;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;z-index:20}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-button{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateX(-50%) translateY(0)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:space-between;align-items:center;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:8px;font-weight:400}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}";
1642
+ const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#fff;font-size:14px;font-weight:600;text-align:center;white-space:normal;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;z-index:20}.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-button{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateX(-50%) translateY(0)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:space-between;align-items:center;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:8px;font-weight:400}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}";
986
1643
 
987
1644
  const JaakStamps = class {
988
1645
  constructor(hostRef) {
@@ -997,6 +1654,13 @@ const JaakStamps = class {
997
1654
  cropMargin = 0;
998
1655
  useDocumentClassification = false;
999
1656
  preferredCamera = 'auto';
1657
+ captureDelay = 1000;
1658
+ enableQualityValidation = true;
1659
+ qualityThreshold = 60;
1660
+ minQualityScore = 45;
1661
+ minFocusScore = 16;
1662
+ minBlurScore = 22;
1663
+ maxReflectionScore = 15;
1000
1664
  captureCompleted;
1001
1665
  isReady;
1002
1666
  // State derived from services
@@ -1008,12 +1672,19 @@ const JaakStamps = class {
1008
1672
  shouldMirrorVideo = true;
1009
1673
  showCameraSelector = false;
1010
1674
  isSwitchingCamera = false;
1675
+ hasDocumentDetected = false;
1011
1676
  currentStatus = {
1012
1677
  message: 'Inicializando componente...',
1013
1678
  description: 'Configurando servicios y cargando recursos',
1014
1679
  type: 'initializing',
1015
1680
  isInitialized: false
1016
1681
  };
1682
+ qualityFeedback = {
1683
+ message: 'Analizando calidad...',
1684
+ score: 0,
1685
+ hasIssues: false,
1686
+ canCapture: false
1687
+ };
1017
1688
  performanceData = {
1018
1689
  fps: 0,
1019
1690
  inferenceTime: 0,
@@ -1081,7 +1752,14 @@ const JaakStamps = class {
1081
1752
  maskSize: this.maskSize,
1082
1753
  cropMargin: this.cropMargin,
1083
1754
  useDocumentClassification: this.useDocumentClassification,
1084
- preferredCamera: this.preferredCamera
1755
+ preferredCamera: this.preferredCamera,
1756
+ captureDelay: this.captureDelay,
1757
+ enableQualityValidation: this.enableQualityValidation,
1758
+ qualityThreshold: this.qualityThreshold,
1759
+ minQualityScore: this.minQualityScore,
1760
+ minFocusScore: this.minFocusScore,
1761
+ minBlurScore: this.minBlurScore,
1762
+ maxReflectionScore: this.maxReflectionScore
1085
1763
  };
1086
1764
  this.serviceContainer = new ServiceContainer(config);
1087
1765
  this.logger = this.serviceContainer.getLogger();
@@ -1108,7 +1786,8 @@ const JaakStamps = class {
1108
1786
  maskSize: this.maskSize,
1109
1787
  cropMargin: this.cropMargin,
1110
1788
  useDocumentClassification: this.useDocumentClassification,
1111
- preferredCamera: this.preferredCamera
1789
+ preferredCamera: this.preferredCamera,
1790
+ captureDelay: this.captureDelay
1112
1791
  });
1113
1792
  this.validateProps();
1114
1793
  if (this.debug) {
@@ -1136,6 +1815,30 @@ const JaakStamps = class {
1136
1815
  this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
1137
1816
  this.preferredCamera = 'auto';
1138
1817
  }
1818
+ if (this.captureDelay < 0 || this.captureDelay > 10000) {
1819
+ this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1000`);
1820
+ this.captureDelay = 1000;
1821
+ }
1822
+ if (this.qualityThreshold < 0 || this.qualityThreshold > 100) {
1823
+ this.logger.warn(`Propiedad qualityThreshold inválida. Valor: ${this.qualityThreshold}, esperado: 0-100. Usando valor por defecto: 60`);
1824
+ this.qualityThreshold = 60;
1825
+ }
1826
+ if (this.minQualityScore < 0 || this.minQualityScore > 100) {
1827
+ this.logger.warn(`Propiedad minQualityScore inválida. Valor: ${this.minQualityScore}, esperado: 0-100. Usando valor por defecto: 45`);
1828
+ this.minQualityScore = 45;
1829
+ }
1830
+ if (this.minFocusScore < 0 || this.minFocusScore > 100) {
1831
+ this.logger.warn(`Propiedad minFocusScore inválida. Valor: ${this.minFocusScore}, esperado: 0-100. Usando valor por defecto: 16`);
1832
+ this.minFocusScore = 16;
1833
+ }
1834
+ if (this.minBlurScore < 0 || this.minBlurScore > 200) {
1835
+ this.logger.warn(`Propiedad minBlurScore inválida. Valor: ${this.minBlurScore}, esperado: 0-200. Usando valor por defecto: 22`);
1836
+ this.minBlurScore = 22;
1837
+ }
1838
+ if (this.maxReflectionScore < 0 || this.maxReflectionScore > 100) {
1839
+ this.logger.warn(`Propiedad maxReflectionScore inválida. Valor: ${this.maxReflectionScore}, esperado: 0-100. Usando valor por defecto: 15`);
1840
+ this.maxReflectionScore = 15;
1841
+ }
1139
1842
  }
1140
1843
  async loadOnnxRuntime() {
1141
1844
  if (!window.ort) {
@@ -1354,6 +2057,91 @@ const JaakStamps = class {
1354
2057
  availableCameras: this.cameraService.getAvailableCameras().length
1355
2058
  };
1356
2059
  }
2060
+ async setCaptureDelay(delay) {
2061
+ if (delay < 0 || delay > 10000) {
2062
+ throw new Error('Capture delay must be between 0 and 10000 milliseconds');
2063
+ }
2064
+ this.captureDelay = delay;
2065
+ this.serviceContainer.updateConfig({ captureDelay: delay });
2066
+ return {
2067
+ success: true,
2068
+ captureDelay: this.captureDelay
2069
+ };
2070
+ }
2071
+ async getCaptureDelay() {
2072
+ return this.captureDelay;
2073
+ }
2074
+ async setTorchEnabled(enabled) {
2075
+ const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
2076
+ return {
2077
+ success: torchEnabled,
2078
+ enabled: torchEnabled ? enabled : false
2079
+ };
2080
+ }
2081
+ async focusAtPoint(x, y) {
2082
+ const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
2083
+ return {
2084
+ success: focused,
2085
+ coordinates: { x, y }
2086
+ };
2087
+ }
2088
+ async getImageQuality() {
2089
+ if (!this.videoRef || !this.detectionService.isModelLoaded()) {
2090
+ return null;
2091
+ }
2092
+ try {
2093
+ const qualityResult = this.detectionService.validateImageQuality(this.videoRef);
2094
+ return {
2095
+ qualityScore: qualityResult.qualityScore,
2096
+ overallQuality: qualityResult.overallQuality,
2097
+ issues: qualityResult.issues,
2098
+ recommendations: qualityResult.recommendations,
2099
+ canCapture: this.detectionService.isImageQualityAcceptable(qualityResult)
2100
+ };
2101
+ }
2102
+ catch (error) {
2103
+ this.logger.error('Error al analizar calidad de imagen:', error);
2104
+ return null;
2105
+ }
2106
+ }
2107
+ async setQualityThresholds(thresholds) {
2108
+ // Validate thresholds
2109
+ if (thresholds.minQualityScore !== undefined && (thresholds.minQualityScore < 0 || thresholds.minQualityScore > 100)) {
2110
+ throw new Error('minQualityScore must be between 0 and 100');
2111
+ }
2112
+ if (thresholds.minFocusScore !== undefined && (thresholds.minFocusScore < 0 || thresholds.minFocusScore > 100)) {
2113
+ throw new Error('minFocusScore must be between 0 and 100');
2114
+ }
2115
+ if (thresholds.minBlurScore !== undefined && (thresholds.minBlurScore < 0 || thresholds.minBlurScore > 200)) {
2116
+ throw new Error('minBlurScore must be between 0 and 200');
2117
+ }
2118
+ if (thresholds.maxReflectionScore !== undefined && (thresholds.maxReflectionScore < 0 || thresholds.maxReflectionScore > 100)) {
2119
+ throw new Error('maxReflectionScore must be between 0 and 100');
2120
+ }
2121
+ // Update component properties
2122
+ if (thresholds.minQualityScore !== undefined)
2123
+ this.minQualityScore = thresholds.minQualityScore;
2124
+ if (thresholds.minFocusScore !== undefined)
2125
+ this.minFocusScore = thresholds.minFocusScore;
2126
+ if (thresholds.minBlurScore !== undefined)
2127
+ this.minBlurScore = thresholds.minBlurScore;
2128
+ if (thresholds.maxReflectionScore !== undefined)
2129
+ this.maxReflectionScore = thresholds.maxReflectionScore;
2130
+ // Update detection service
2131
+ this.detectionService.updateQualityThresholds(thresholds);
2132
+ return {
2133
+ success: true,
2134
+ thresholds: this.detectionService.getQualityThresholds()
2135
+ };
2136
+ }
2137
+ async getQualityThresholds() {
2138
+ return {
2139
+ minQualityScore: this.minQualityScore,
2140
+ minFocusScore: this.minFocusScore,
2141
+ minBlurScore: this.minBlurScore,
2142
+ maxReflectionScore: this.maxReflectionScore
2143
+ };
2144
+ }
1357
2145
  // DETECTION METHODS
1358
2146
  async startDetection() {
1359
2147
  this.logger.state('INICIANDO_DETECCION');
@@ -1455,7 +2243,36 @@ const JaakStamps = class {
1455
2243
  // Measure preprocessing and inference time
1456
2244
  const inferenceStartTime = performance.now();
1457
2245
  const inputTensor = this.detectionService.preprocess(this.videoRef);
1458
- const detections = await this.detectionService.runInference(inputTensor);
2246
+ // Enhanced detection with quality validation if enabled
2247
+ let detections;
2248
+ let qualityResult = null;
2249
+ let canCapture = true;
2250
+ if (this.enableQualityValidation) {
2251
+ // Use enhanced detection method with quality validation
2252
+ const enhancedResult = await this.detectionService.runInferenceWithQuality(inputTensor, this.videoRef);
2253
+ detections = enhancedResult.detections;
2254
+ qualityResult = enhancedResult.qualityResult;
2255
+ canCapture = enhancedResult.canCapture;
2256
+ // Update quality feedback state
2257
+ this.qualityFeedback = {
2258
+ message: enhancedResult.feedback,
2259
+ score: qualityResult.qualityScore,
2260
+ hasIssues: qualityResult.issues.length > 0,
2261
+ canCapture: canCapture
2262
+ };
2263
+ }
2264
+ else {
2265
+ // Standard detection without quality validation
2266
+ detections = await this.detectionService.runInference(inputTensor);
2267
+ // Quick quality feedback for real-time display
2268
+ const quickFeedback = this.detectionService.getQuickQualityFeedback(this.videoRef);
2269
+ this.qualityFeedback = {
2270
+ message: quickFeedback.feedback,
2271
+ score: 0, // Not calculated in quick mode
2272
+ hasIssues: quickFeedback.hasBlur || quickFeedback.hasReflections || !quickFeedback.isFocused,
2273
+ canCapture: true // Allow capture without strict validation
2274
+ };
2275
+ }
1459
2276
  const inferenceTime = performance.now() - inferenceStartTime;
1460
2277
  // Record ONNX performance metrics
1461
2278
  if (this.debug) {
@@ -1471,6 +2288,8 @@ const JaakStamps = class {
1471
2288
  }
1472
2289
  });
1473
2290
  }
2291
+ // Update document detection state
2292
+ this.hasDocumentDetected = detections.length > 0;
1474
2293
  // Adaptive frame rate
1475
2294
  if (detections.length === 0) {
1476
2295
  this.consecutiveFailures++;
@@ -1532,6 +2351,7 @@ const JaakStamps = class {
1532
2351
  }
1533
2352
  this.detectionBoxes = [];
1534
2353
  this.alignmentStartTime = undefined;
2354
+ this.hasDocumentDetected = false;
1535
2355
  this.serviceContainer?.cleanup();
1536
2356
  }
1537
2357
  render() {
@@ -1546,7 +2366,7 @@ const JaakStamps = class {
1546
2366
  availableCameras: [],
1547
2367
  selectedCameraId: null,
1548
2368
  deviceType: 'desktop'};
1549
- return (index.h("div", { key: '91be2bd01dfa62ef439ff4b3d029539248b69364', class: "detector-container" }, index.h("div", { key: '7dbfbe2b782a8d06e2303ec3edff4b5c68ba4444', class: "video-container" }, index.h("video", { key: 'a2b7ec84a2f49935a94f1afb12deb1137f472fcd', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), index.h("div", { key: '63cf0b7a05fc6d604f3a273fd902717542ba47f5', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index$1) => (index.h("div", { key: index$1, class: "detection-box", style: {
2369
+ return (index.h("div", { key: 'd71fc140c7fc87160ceaf479f9b9beae7685e9bc', class: "detector-container" }, index.h("div", { key: 'd83347ab6ccacfe940fb66d17adc21cff126d875', class: "video-container" }, index.h("video", { key: 'fb69de65e8133c7936cfb03ec86acdf89d2a8112', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), index.h("div", { key: 'c957341c1a750a1732afd6bb1045c1ba9bb2993d', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index$1) => (index.h("div", { key: index$1, class: "detection-box", style: {
1550
2370
  position: 'absolute',
1551
2371
  left: `${box.x}px`,
1552
2372
  top: `${box.y}px`,
@@ -1555,7 +2375,9 @@ const JaakStamps = class {
1555
2375
  border: '2px solid #32406C',
1556
2376
  pointerEvents: 'none',
1557
2377
  boxSizing: 'border-box'
1558
- } })))), this.isMaskReady && (index.h("div", { key: '089227de9415e523e99261eeeca5dcb981816a83', class: "overlay-mask" }, index.h("div", { key: '846bd914025e8da92ca85381a566c6d93caea2ea', class: "card-outline" }, index.h("div", { key: '1f49ae76e9066a65e848e55e19839547d8b9bef0', class: "side side-top" }), index.h("div", { key: '5b69c6bcf604042f86383e9bf122204d4fe1aeb1', class: "side side-right" }), index.h("div", { key: '8aa4c1f09c8b7046a369ed8d7fae1a96a525f778', class: "side side-bottom" }), index.h("div", { key: 'd8826c0347a219c154703d165ee5b4ced33f7a3f', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '916e038d0d0f80971629916189ef39c2689e4e43', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("button", { key: '02b01dff08bdbf042c6df1403f955601964bff28', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")), captureState.isVideoActive && (index.h("div", { key: 'cd88fd0567e5b792ddc623c281616ea35616f8f6', class: "camera-controls" }, index.h("button", { key: '9d2a63a53926d0c987e551bd49caa8143b4e15d4', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (index.h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (index.h("div", { key: '43e9ea946f88498082339de62cf54cba352e765e', class: "camera-selector-dropdown" }, index.h("div", { key: '9fe39e8fa32ee458e4261d3f0d8fd40d50fefcf4', class: "camera-selector-header" }, index.h("span", { key: 'edf45c7bd8c5a43df9aba3ff6310cc5d06bb1aa2' }, "Seleccionar C\u00E1mara"), index.h("button", { key: 'dd5d417bb5eadd3f3b488ecd901d8dfa075c29c8', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: '8edcd693f137ddd4ada9654c01615a17c90cb9c3', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (index.h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, index.h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`), cameraInfo.selectedCameraId === camera.id && (index.h("span", { class: "selected-indicator" }, "\u2713")))))), index.h("div", { key: 'b7f050513f25af03ca72a735bd8745f71a456d76', class: "device-info" }, index.h("small", { key: '34305a7ca3418d46bceb2a9923a664fd5a0062fa' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (index.h("div", { key: '54ec511bd6d22c8c25eb41717abcebd89ed315f8', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: '9392bccfd3576b1858610b543c0ac0b747cef153', class: "flip-animation" }, index.h("div", { key: '030dc63ea455afa24fd0b370425aae14ab322613', class: "id-card-icon" }), index.h("div", { key: '3f7d8985af2d6eaec21699b87d5aeb0b27d2d55e', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: '0da0a234e87013fb2ccc652cb920318cf00b6733', class: "success-animation" }, index.h("div", { key: 'f6b62ff21e147c8b365ab8c88e42dcb106cb8743', class: "check-icon" }), index.h("div", { key: '8c8b56561234e140cd658c5fad252cd6a1e0251c', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: '684119ec540276cc28a49724d6177c72aff74b02', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: 'abc9a89349b9d8d76e03c765aa0084ec364f73a8', class: "status-spinner" })), index.h("div", { key: '6903b83962ee971a95a18e9ac793d872a7ce4551', class: "status-content" }, index.h("div", { key: 'c01dd0268a223ee269b616cea19a9cdb69cc048f', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: '1f6ebecc7a1d058c34aca5b4572ace8a81b5ab69', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: 'eff03d22449aafac6c986e71af2aafd487b1776d', class: "performance-monitor" }, index.h("div", { key: '66dfd57c92f7ee67e7c52293b3ad3f1105793e70', class: "performance-expanded" }, index.h("div", { key: '0d952f89275e551a1e4cdb9b215e1ba48776ebd4', class: "metrics-row" }, index.h("div", { key: '36fab2101078e4da16a3a3ac50d927077d9e0874', class: "metric-compact" }, index.h("span", { key: 'a694bf26ff69522a6462963d735ffa145513d84c', class: "metric-label" }, "FPS"), index.h("span", { key: '02a250b3863f8f158a4103e0817cf14accdb7b30', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: 'd6e849779f13ae0ee3641d98148f0ab151877157', class: "metric-compact" }, index.h("span", { key: '983411baa6e79e3bfe7ad277879361c023dc6d48', class: "metric-label" }, "MEM"), index.h("span", { key: 'eea7060b9cef780ed47a534b7bc69c94162b67d9', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: 'e0e5e80c208f1eadc606c20df844efcc66f6599c', class: "metrics-row" }, index.h("div", { key: 'd27ae06d877097c99c1796c36284427861ae0be3', class: "metric-compact" }, index.h("span", { key: '229837e3037ce64fc5930a5732ad6a0e323371e9', class: "metric-label" }, "INF"), index.h("span", { key: '82002332976cddf7dbb2aa6346ad9da0ce0901df', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: '243afc7933e169084eda28cca270198420a4341d', class: "metric-compact" }, index.h("span", { key: '834e9a69791abc42545dddc1c0de7372a3292d60', class: "metric-label" }, "FRAME"), index.h("span", { key: '9106f8989818aa9111d7571006224befe7837406', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: 'da911fc95ce6151fb5fb64b8d5df53d6b693ffef', class: "metrics-row" }, index.h("div", { key: 'c813a7152d51bd2402b9c2bdf5bd1fe09b6df7db', class: "metric-compact" }, index.h("span", { key: 'b5a7807764a897663ce9d37111188c26bcf2cb6d', class: "metric-label" }, "DET"), index.h("span", { key: '17ec09407ac24800e0ec965c7b3a037730a6afd4', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: '5aa0b6f4d1643e3ec224d1a9d89d4d37407d3c58', class: "metric-compact" }, index.h("span", { key: '6b8efdcf07e9eac7d5036a508804b017ccedaea9', class: "metric-label" }, "RATE"), index.h("span", { key: '038a5c3377185784c806c614f0c098e417360acf', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: '3317e18916b62bce70cc02540cfc274d588b466a', class: "watermark" }, index.h("img", { key: 'adf070f99c17c3fdfc3e2c69b1a63dfef66ad71e', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
2378
+ } })))), this.isMaskReady && (index.h("div", { key: 'cf81504f7f129a2bb2789dd7addfe8d365fd7973', class: "overlay-mask" }, index.h("div", { key: 'b0f8ab1e02d1731295b95cbca4b44be430c6104d', class: "card-outline" }, index.h("div", { key: 'fadb33b8dc3c2cbfbb78aa0ff4d273b2b26a3565', class: "side side-top" }), index.h("div", { key: '3f3ca187195c78f2c3daffa481cc275f970c68a5', class: "side side-right" }), index.h("div", { key: '1e6bac145a85bbee2dab24da81b8064bc4ac282b', class: "side side-bottom" }), index.h("div", { key: '9bef1cd7afcef6cb1975a1103ec57d33d89f7640', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '9198d8b8fb1f4a17ec615943a09844f47a639048', class: "guide-text" }, this.enableQualityValidation && this.hasDocumentDetected && this.qualityFeedback.message ?
2379
+ this.qualityFeedback.message :
2380
+ 'Alinee su identificación con el marco')), this.enableQualityValidation && this.hasDocumentDetected && captureState.isVideoActive && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '04afa661c1d988d9bedb48c78672de233dd49f9b', class: `quality-indicator ${this.qualityFeedback.hasIssues ? 'warning' : 'good'}` }, index.h("div", { key: 'bc789e19894de1d3dfd4a232e05e612d804e0e6c', class: "quality-score" }, this.qualityFeedback.score > 0 ? `${this.qualityFeedback.score}%` : ''), index.h("div", { key: '0750f5e5dfdb7a7c054d01965146d34a5bb4485a', class: `quality-status ${this.qualityFeedback.canCapture ? 'ready' : 'not-ready'}` }, this.qualityFeedback.canCapture ? '✓' : '!')))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("button", { key: 'cbe391726b5d62acbb466c36b135e0556fc488f7', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, "Saltar reverso")), captureState.isVideoActive && (index.h("div", { key: '929f8fe054b30e3c30bf066319e957a633fa587c', class: "camera-controls" }, index.h("button", { key: 'b4f68dd3a6865cba49c3f5d38c43874b7b9b364d', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (index.h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (index.h("div", { key: 'c9315ff70704fdc265af07bf765b6247814563e6', class: "camera-selector-dropdown" }, index.h("div", { key: 'f4950e7e3c3e279c48310a4049550f3143fb4d3f', class: "camera-selector-header" }, index.h("span", { key: '4cfacf909549b0e18bd8ac6e54e127ce09b75335' }, "Seleccionar C\u00E1mara"), index.h("button", { key: '13d7679c5235f41f0dbc38b9f397483cf4ec9ffd', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: '6c97cb8ba9486c33a633901d9dd49765f87d92ed', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (index.h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, index.h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`), cameraInfo.selectedCameraId === camera.id && (index.h("span", { class: "selected-indicator" }, "\u2713")))))), index.h("div", { key: '35796316fc950b8491f6167303b549b098abbf58', class: "device-info" }, index.h("small", { key: '83c59a8cb0c0b67a37163fe75c1f1f383fcb00f4' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (index.h("div", { key: 'be5b316abb21c06cf5bb69ed090d43d45537a9e6', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: '43e64fcc325c7c96871a99838c5fa094c6774af2', class: "flip-animation" }, index.h("div", { key: '39223d9b593716e76912ade58b556c7212a7e05f', class: "id-card-icon" }), index.h("div", { key: '79463b43724c3c56c45bd5b41337117d7d415618', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: '7be8ca4d0bd59b1e4127254ff68e41321aa387cc', class: "success-animation" }, index.h("div", { key: '03a3e625f622c546a3016f85e029f9ea165f65c3', class: "check-icon" }), index.h("div", { key: '9c63faad517dad4b2ede8ec5a0998ad0b5c807d5', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: '94aa38c67fdaf328e74a2c1ec476987bf8e483d5', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: '52a460f2fe11505158331cc06d7121db80bdfed9', class: "status-spinner" })), index.h("div", { key: '1eabfed00df31ff30fdf818f3d33cd837a7f31a5', class: "status-content" }, index.h("div", { key: '4bdbadf18fc48f2fad5dafd9ddf4a78f0a81dbf5', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: '14c41346fc9fc306315cdda1c54ec1e828c38f62', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: '21fb0ca08547143648097f2aa4f3eb3ade73bce1', class: "performance-monitor" }, index.h("div", { key: '88c990f42b5c87c9bc971acd228fc84bacec861b', class: "performance-expanded" }, index.h("div", { key: '351215f4b8ab202b42a4c9b0bfa8665485469f64', class: "metrics-row" }, index.h("div", { key: '28a2c5386ba99850f7dc41add6fb4ef073a6b8cb', class: "metric-compact" }, index.h("span", { key: '7a2ab758f9baf1264cd4ad70ff30f42a35c41065', class: "metric-label" }, "FPS"), index.h("span", { key: '1c4370d016f2bdcf61275b4869bddbd8109a7b2a', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: 'e79c27b4cbad3e70f2df917a5147dd125d51304d', class: "metric-compact" }, index.h("span", { key: 'aee4c178ae751cfe94735ef9fcb78f273f9cb885', class: "metric-label" }, "MEM"), index.h("span", { key: '823e98dec413968ee5080b29839729e6ac9f91d2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: 'e29037f684c82579a711aff022060c97ab9d93cf', class: "metrics-row" }, index.h("div", { key: '2917f3c4b026e3106fad741944e582aff36e10e0', class: "metric-compact" }, index.h("span", { key: 'e8783e8e2b18b29b37342eea376a75f257a46850', class: "metric-label" }, "INF"), index.h("span", { key: '30cdc1db3189135bc772aab5c31e081294ebd431', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: 'c481ec26394e35efe38047e5fa1b835e520f49b8', class: "metric-compact" }, index.h("span", { key: 'c806cae91f3fcad896bd6053b6a0391ab518ab6f', class: "metric-label" }, "FRAME"), index.h("span", { key: 'af9996e2093c918ea903d036e637905baeef1aa2', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: '48422447a6d00f78a9f861bfc50de4ef5a628a57', class: "metrics-row" }, index.h("div", { key: 'c88a03fbff56f68796df33187256fd5a5a68f763', class: "metric-compact" }, index.h("span", { key: '5e366c35865f8bf45aecac126ce98e0e17be0825', class: "metric-label" }, "DET"), index.h("span", { key: '123570ac9ae59cf5ef1b992ea6c4563b753990c3', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: 'dea3a496f84e80f9d6f27173c8fa2e4149bf8e0b', class: "metric-compact" }, index.h("span", { key: 'fe47d6c16ca84fff8824bef92d8ce9efe50dfa38', class: "metric-label" }, "RATE"), index.h("span", { key: '873846eb42c1ed97f4e9493871e51741766c852f', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: 'b485b1d8fafee0314ffbf1c7934ffc9a3d4c6902', class: "watermark" }, index.h("img", { key: 'f5fae340c3e96ecafe26aa819dbc2411038aa953', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1559
2381
  }
1560
2382
  // Utility methods
1561
2383
  updateDetectionBoxes(boxes) {
@@ -1627,15 +2449,38 @@ const JaakStamps = class {
1627
2449
  if (allSidesAligned && bestBox) {
1628
2450
  cardOutline?.classList.add('perfect-match');
1629
2451
  corners?.forEach(corner => corner.classList.add('perfect-match'));
1630
- if (!this.hasScreenshotTaken) {
2452
+ // Check quality validation before allowing capture
2453
+ const qualityCheckPassed = !this.enableQualityValidation || this.qualityFeedback.canCapture;
2454
+ // Debug logging
2455
+ this.logger.state('CAPTURE_EVALUATION', {
2456
+ allSidesAligned: true,
2457
+ hasScreenshotTaken: this.hasScreenshotTaken,
2458
+ qualityCheckPassed,
2459
+ enableQualityValidation: this.enableQualityValidation,
2460
+ qualityFeedback: this.qualityFeedback,
2461
+ captureDelay: this.captureDelay,
2462
+ alignmentStartTime: this.alignmentStartTime
2463
+ });
2464
+ if (!this.hasScreenshotTaken && qualityCheckPassed) {
1631
2465
  const currentTime = Date.now();
1632
2466
  // Initialize alignment start time if not set
1633
2467
  if (!this.alignmentStartTime) {
1634
2468
  this.alignmentStartTime = currentTime;
2469
+ this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
1635
2470
  }
1636
- // Check if document has been aligned for 1 second
2471
+ // Check if document has been aligned for the configured delay
1637
2472
  const alignmentDuration = currentTime - this.alignmentStartTime;
1638
- if (alignmentDuration >= 1000) {
2473
+ this.logger.state('ALIGNMENT_DURATION_CHECK', {
2474
+ alignmentDuration,
2475
+ captureDelay: this.captureDelay,
2476
+ readyToCapture: alignmentDuration >= this.captureDelay
2477
+ });
2478
+ if (alignmentDuration >= this.captureDelay) {
2479
+ this.logger.state('TRIGGERING_CAPTURE', {
2480
+ alignmentDuration,
2481
+ captureDelay: this.captureDelay,
2482
+ qualityScore: this.qualityFeedback.score
2483
+ });
1639
2484
  this.lastDetectedBox = bestBox;
1640
2485
  this.takeScreenshot().catch(error => {
1641
2486
  this.logger.error('Error al tomar captura de pantalla:', error);
@@ -1647,6 +2492,17 @@ const JaakStamps = class {
1647
2492
  }, 2000);
1648
2493
  }
1649
2494
  }
2495
+ else if (!qualityCheckPassed) {
2496
+ // Reset alignment timer if quality check fails
2497
+ this.logger.state('QUALITY_CHECK_FAILED', {
2498
+ enableQualityValidation: this.enableQualityValidation,
2499
+ canCapture: this.qualityFeedback.canCapture,
2500
+ qualityScore: this.qualityFeedback.score,
2501
+ hasIssues: this.qualityFeedback.hasIssues
2502
+ });
2503
+ this.alignmentStartTime = undefined;
2504
+ cardOutline?.classList.add('quality-warning');
2505
+ }
1650
2506
  }
1651
2507
  else {
1652
2508
  cardOutline?.classList.remove('perfect-match');
@@ -1835,6 +2691,7 @@ const JaakStamps = class {
1835
2691
  this.lastInferenceTime = 0;
1836
2692
  this.detectionBoxes = [];
1837
2693
  this.alignmentStartTime = undefined;
2694
+ this.hasDocumentDetected = false;
1838
2695
  if (this.alignmentTimer) {
1839
2696
  clearTimeout(this.alignmentTimer);
1840
2697
  this.alignmentTimer = undefined;