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