@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.
- package/README.md +4 -1
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +875 -13
- 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 +501 -10
- 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 +892 -14
- 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 +875 -13
- 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-043b1cfd.entry.js +0 -2
- 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
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
|
2469
|
+
// Check if document has been aligned for the configured delay
|
|
1635
2470
|
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
1636
|
-
|
|
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
|
-
|
|
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();
|