@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.
- package/README.md +4 -1
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +868 -11
- package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
- package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/my-component/my-component.css +66 -0
- package/dist/collection/components/my-component/my-component.js +494 -8
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/CameraService.js +161 -0
- package/dist/collection/services/CameraService.js.map +1 -1
- package/dist/collection/services/DetectionService.js +164 -1
- package/dist/collection/services/DetectionService.js.map +1 -1
- package/dist/collection/services/ImageQualityService.js +329 -0
- package/dist/collection/services/ImageQualityService.js.map +1 -0
- package/dist/collection/services/ServiceContainer.js +6 -1
- package/dist/collection/services/ServiceContainer.js.map +1 -1
- package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
- package/dist/collection/services/interfaces/IDetectionService.js.map +1 -1
- package/dist/collection/services/interfaces/IImageQualityService.js +2 -0
- package/dist/collection/services/interfaces/IImageQualityService.js.map +1 -0
- package/dist/components/jaak-stamps.js +885 -12
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/jaak-stamps-webcomponent.js +1 -1
- package/dist/esm/jaak-stamps.entry.js +868 -11
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-47f37982.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-47f37982.entry.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +48 -0
- package/dist/types/components.d.ts +65 -0
- package/dist/types/services/CameraService.d.ts +5 -0
- package/dist/types/services/DetectionService.d.ts +22 -2
- package/dist/types/services/ImageQualityService.d.ts +31 -0
- package/dist/types/services/ServiceContainer.d.ts +7 -0
- package/dist/types/services/interfaces/ICameraService.d.ts +2 -0
- package/dist/types/services/interfaces/IDetectionService.d.ts +13 -0
- package/dist/types/services/interfaces/IImageQualityService.d.ts +58 -0
- package/package.json +1 -1
- package/dist/jaak-stamps-webcomponent/p-c30c7b47.entry.js +0 -2
- 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
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
|
2471
|
+
// Check if document has been aligned for the configured delay
|
|
1637
2472
|
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
1638
|
-
|
|
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;
|