@smileid/web-components 11.4.5 → 11.6.0
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/dist/esm/DocumentCaptureScreens-DjSTdVP-.js +5398 -0
- package/dist/esm/DocumentCaptureScreens-DjSTdVP-.js.map +1 -0
- package/dist/esm/{Navigation-Bb7MPLE8.js → Navigation-6DH3vF4-.js} +28 -22
- package/dist/esm/Navigation-6DH3vF4-.js.map +1 -0
- package/dist/esm/{PoweredBySmileId-CxbaihMu.js → PoweredBySmileId-DoKwoPUd.js} +424 -6
- package/dist/esm/PoweredBySmileId-DoKwoPUd.js.map +1 -0
- package/dist/esm/SelfieCaptureScreens-CtX-4Tco.js +11470 -0
- package/dist/esm/SelfieCaptureScreens-CtX-4Tco.js.map +1 -0
- package/dist/esm/combobox.js +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +713 -2
- package/dist/esm/end-user-consent.js.map +1 -1
- package/dist/esm/index-BqyuTk9f.js +1366 -0
- package/dist/esm/{index-C4RTMbgw.js.map → index-BqyuTk9f.js.map} +1 -1
- package/dist/esm/localisation.js +1 -1
- package/dist/esm/main.js +14 -14
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/package-CjZI-cNQ.js +2540 -0
- package/dist/esm/package-CjZI-cNQ.js.map +1 -0
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +81 -37
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/totp-consent.js +731 -2
- package/dist/esm/totp-consent.js.map +1 -1
- package/dist/esm/validate.js +31 -0
- package/dist/esm/validate.js.map +1 -0
- package/dist/smart-camera-web.js +1513 -383
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/main.d.ts +18 -1
- package/dist/types/validate.d.ts +21 -0
- package/lib/components/document/src/DocumentCaptureScreens.js +97 -18
- package/lib/components/document/src/assets/lottie.d.ts +12 -0
- package/lib/components/document/src/assets/svg-inline.d.ts +8 -0
- package/lib/components/document/src/document-auto-capture/DocumentAutoCapture.stories.js +75 -0
- package/lib/components/document/src/document-auto-capture/DocumentAutoCapture.tsx +1458 -0
- package/lib/components/document/src/document-auto-capture/README.md +73 -0
- package/lib/components/document/src/document-auto-capture/assets/Greenbook_Shimmer.svg +42 -0
- package/lib/components/document/src/document-auto-capture/assets/ID_Back_Shimmer.svg +8 -0
- package/lib/components/document/src/document-auto-capture/assets/ID_Front_Shimmer.svg +20 -0
- package/lib/components/document/src/document-auto-capture/assets/Passport-Shimmer.svg +143 -0
- package/lib/components/document/src/document-auto-capture/assets/shimmers.ts +21 -0
- package/lib/components/document/src/document-auto-capture/assets/svg-raw.d.ts +4 -0
- package/lib/components/document/src/document-auto-capture/components/CaptureButton.tsx +122 -0
- package/lib/components/document/src/document-auto-capture/components/Overlay.tsx +167 -0
- package/lib/components/document/src/document-auto-capture/components/TuningPanel.tsx +856 -0
- package/lib/components/document/src/document-auto-capture/constants/captureLayout.ts +58 -0
- package/lib/components/document/src/document-auto-capture/detection/cvErrorRecovery.ts +40 -0
- package/lib/components/document/src/document-auto-capture/detection/documentAspect.ts +20 -0
- package/lib/components/document/src/document-auto-capture/detection/qualityScoring.ts +35 -0
- package/lib/components/document/src/document-auto-capture/detection/seamRejection.ts +209 -0
- package/lib/components/document/src/document-auto-capture/detection/synthesisTiming.ts +10 -0
- package/lib/components/document/src/document-auto-capture/hooks/useCamera.ts +117 -0
- package/lib/components/document/src/document-auto-capture/hooks/useCardDetection.ts +3059 -0
- package/lib/components/document/src/document-auto-capture/index.ts +4 -0
- package/lib/components/document/src/document-auto-capture/theme.ts +40 -0
- package/lib/components/document/src/document-auto-capture/utils/debug.ts +25 -0
- package/lib/components/document/src/document-auto-capture/utils/opencvLoader.ts +86 -0
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.tsx +327 -244
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +153 -189
- package/lib/components/document/src/document-capture-submission/DocumentCaptureSubmission.tsx +432 -0
- package/lib/components/document/src/document-capture-submission/index.js +3 -0
- package/lib/components/navigation/src/Navigation.js +27 -8
- package/lib/components/selfie/README.md +13 -0
- package/lib/components/selfie/src/SelfieCaptureScreens.js +56 -8
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieCapture.tsx +684 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieConsent.tsx +71 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieSubmission.tsx +181 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/OvalProgress.tsx +87 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/Icon.svg +8 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/accessories.svg +77 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/active_liveness_animation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device.svg +12 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device_orientation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/good.svg +52 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/id-card.svg +9 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/illustrations.tsx +852 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/instructions-img.svg +3 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/multiple-faces.svg +69 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/person.svg +6 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/phone.svg +8 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/poor-lighting.svg +53 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/too_dark_animation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ActiveLivenessOverlay.tsx +226 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/AlertDisplay.tsx +38 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/BackNavigation.tsx +45 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CameraPreview.tsx +96 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureControls.tsx +97 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureGuidelines.tsx +374 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ConsentView.tsx +460 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/SubmissionView.tsx +426 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/index.ts +3 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/constants.ts +23 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/index.ts +2 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useCamera.ts +238 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useFaceCapture.ts +1075 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/index.ts +1 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/alertMessages.ts +20 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/canvas.ts +108 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/faceDetection.ts +545 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageCapture.ts +66 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageQuality.ts +151 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/index.ts +5 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/mediapipeManager.ts +215 -0
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +24 -1
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +15 -7
- package/lib/components/selfie/src/smartselfie-capture/utils/canvas.ts +4 -6
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/smart-camera-web/src/README.md +11 -0
- package/lib/components/smart-camera-web/src/SmartCameraWeb.js +89 -8
- package/lib/components/totp-consent/src/TotpConsent.js +1 -1
- package/lib/domain/localisation/index.js +2 -2
- package/package.json +9 -5
- package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js +0 -4147
- package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js.map +0 -1
- package/dist/esm/EndUserConsent-uHfA3txP.js +0 -717
- package/dist/esm/EndUserConsent-uHfA3txP.js.map +0 -1
- package/dist/esm/Navigation-Bb7MPLE8.js.map +0 -1
- package/dist/esm/PoweredBySmileId-CxbaihMu.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js +0 -7651
- package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js.map +0 -1
- package/dist/esm/TotpConsent-Depzg0ti.js +0 -734
- package/dist/esm/TotpConsent-Depzg0ti.js.map +0 -1
- package/dist/esm/index-C4RTMbgw.js +0 -1360
- package/dist/esm/package-D6YrpMcO.js +0 -565
- package/dist/esm/package-D6YrpMcO.js.map +0 -1
- package/dist/esm/styles-BTEClL7R.js +0 -419
- package/dist/esm/styles-BTEClL7R.js.map +0 -1
- /package/lib/components/document/src/assets/lottie/{taking photo of green book passport.lottie → greenbook.lottie} +0 -0
- /package/lib/components/document/src/assets/lottie/{taking photo of ID FLIP 2D.lottie → id-card-flip.lottie} +0 -0
- /package/lib/components/document/src/assets/lottie/{taking photo of ID.lottie → id-card.lottie} +0 -0
- /package/lib/components/document/src/assets/lottie/{taking photo of passport 2.lottie → passport.lottie} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { JPEG_QUALITY } from '../../../../../domain/constants/src/Constants';
|
|
2
|
+
|
|
3
|
+
export const captureImageFromVideo = (
|
|
4
|
+
videoElement: HTMLVideoElement,
|
|
5
|
+
isReference: boolean = false,
|
|
6
|
+
): string | null => {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
const ctx = canvas.getContext('2d');
|
|
9
|
+
if (!ctx) return null;
|
|
10
|
+
|
|
11
|
+
const isPortrait = videoElement.videoHeight > videoElement.videoWidth;
|
|
12
|
+
|
|
13
|
+
if (isReference) {
|
|
14
|
+
if (isPortrait) {
|
|
15
|
+
canvas.width = 480;
|
|
16
|
+
canvas.height = Math.max(
|
|
17
|
+
640,
|
|
18
|
+
(canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
|
|
19
|
+
);
|
|
20
|
+
} else {
|
|
21
|
+
canvas.width = 640;
|
|
22
|
+
canvas.height = Math.max(
|
|
23
|
+
480,
|
|
24
|
+
(canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
} else if (isPortrait) {
|
|
28
|
+
canvas.width = 240;
|
|
29
|
+
canvas.height = Math.max(
|
|
30
|
+
320,
|
|
31
|
+
(canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
|
|
32
|
+
);
|
|
33
|
+
} else {
|
|
34
|
+
canvas.width = 320;
|
|
35
|
+
canvas.height = Math.max(
|
|
36
|
+
240,
|
|
37
|
+
(canvas.width * videoElement.videoHeight) / videoElement.videoWidth,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// capture more of the user's head and avoid clipping
|
|
42
|
+
const zoomOutFactor = 1;
|
|
43
|
+
const sourceWidth = videoElement.videoWidth * zoomOutFactor;
|
|
44
|
+
const sourceHeight = videoElement.videoHeight * zoomOutFactor;
|
|
45
|
+
|
|
46
|
+
// center the zoomed out area
|
|
47
|
+
const offsetX = (sourceWidth - videoElement.videoWidth) / 2;
|
|
48
|
+
const offsetY = (sourceHeight - videoElement.videoHeight) / 2;
|
|
49
|
+
|
|
50
|
+
// vertical offset to shift up and capture full head
|
|
51
|
+
const verticalOffset = 0;
|
|
52
|
+
|
|
53
|
+
ctx.drawImage(
|
|
54
|
+
videoElement,
|
|
55
|
+
-offsetX,
|
|
56
|
+
-offsetY - verticalOffset,
|
|
57
|
+
sourceWidth,
|
|
58
|
+
sourceHeight,
|
|
59
|
+
0,
|
|
60
|
+
0,
|
|
61
|
+
canvas.width,
|
|
62
|
+
canvas.height,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return canvas.toDataURL('image/jpeg', JPEG_QUALITY);
|
|
66
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight image quality checks used by the Enhanced SmartSelfie active
|
|
3
|
+
* liveness flow. Both routines work on small ROIs so they can run inside the
|
|
4
|
+
* face detection loop without noticeable cost.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SAMPLE_STEP = 4; // Sample every 4th pixel — keeps work to ~6% of full ROI.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Module-scoped scratch canvases reused across frames. Allocating a fresh
|
|
11
|
+
* `<canvas>` per call during the capture loop (~10 fps × 2 routines) creates
|
|
12
|
+
* meaningful GC churn and re-enters the `willReadFrequently` software path on
|
|
13
|
+
* every frame. We keep one canvas per routine and only resize when the
|
|
14
|
+
* computed downsample target changes — assigning `width`/`height` also clears
|
|
15
|
+
* the bitmap, so no manual clear is required.
|
|
16
|
+
*/
|
|
17
|
+
let luminanceCanvas: HTMLCanvasElement | null = null;
|
|
18
|
+
let luminanceCtx: CanvasRenderingContext2D | null = null;
|
|
19
|
+
let blurCanvas: HTMLCanvasElement | null = null;
|
|
20
|
+
let blurCtx: CanvasRenderingContext2D | null = null;
|
|
21
|
+
|
|
22
|
+
const getScratchContext = (
|
|
23
|
+
which: 'luminance' | 'blur',
|
|
24
|
+
dw: number,
|
|
25
|
+
dh: number,
|
|
26
|
+
): CanvasRenderingContext2D | null => {
|
|
27
|
+
let canvas = which === 'luminance' ? luminanceCanvas : blurCanvas;
|
|
28
|
+
let ctx = which === 'luminance' ? luminanceCtx : blurCtx;
|
|
29
|
+
|
|
30
|
+
if (!canvas) {
|
|
31
|
+
canvas = document.createElement('canvas');
|
|
32
|
+
ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
33
|
+
if (which === 'luminance') {
|
|
34
|
+
luminanceCanvas = canvas;
|
|
35
|
+
luminanceCtx = ctx;
|
|
36
|
+
} else {
|
|
37
|
+
blurCanvas = canvas;
|
|
38
|
+
blurCtx = ctx;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!ctx) return null;
|
|
42
|
+
|
|
43
|
+
if (canvas.width !== dw) canvas.width = dw;
|
|
44
|
+
if (canvas.height !== dh) canvas.height = dh;
|
|
45
|
+
return ctx;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Average BT.601 luma (0–255) over a region of the video frame.
|
|
50
|
+
*
|
|
51
|
+
* @param video live video element
|
|
52
|
+
* @param region optional normalised ROI (defaults to centre 60%)
|
|
53
|
+
*/
|
|
54
|
+
export const calculateLuminance = (
|
|
55
|
+
video: HTMLVideoElement,
|
|
56
|
+
region?: { x: number; y: number; width: number; height: number },
|
|
57
|
+
): number => {
|
|
58
|
+
if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
|
|
59
|
+
|
|
60
|
+
const w = video.videoWidth;
|
|
61
|
+
const h = video.videoHeight;
|
|
62
|
+
const r = region ?? { x: 0.2, y: 0.2, width: 0.6, height: 0.6 };
|
|
63
|
+
|
|
64
|
+
const sx = Math.max(0, Math.floor(r.x * w));
|
|
65
|
+
const sy = Math.max(0, Math.floor(r.y * h));
|
|
66
|
+
const sw = Math.min(w - sx, Math.floor(r.width * w));
|
|
67
|
+
const sh = Math.min(h - sy, Math.floor(r.height * h));
|
|
68
|
+
if (sw <= 0 || sh <= 0) return 0;
|
|
69
|
+
|
|
70
|
+
// Downsample heavily — 64x64 max — to keep this near-free.
|
|
71
|
+
const targetSize = 64;
|
|
72
|
+
const scale = Math.min(1, targetSize / Math.max(sw, sh));
|
|
73
|
+
const dw = Math.max(1, Math.round(sw * scale));
|
|
74
|
+
const dh = Math.max(1, Math.round(sh * scale));
|
|
75
|
+
|
|
76
|
+
const ctx = getScratchContext('luminance', dw, dh);
|
|
77
|
+
if (!ctx) return 0;
|
|
78
|
+
|
|
79
|
+
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, dw, dh);
|
|
80
|
+
const { data } = ctx.getImageData(0, 0, dw, dh);
|
|
81
|
+
|
|
82
|
+
let total = 0;
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (let i = 0; i < data.length; i += 4 * SAMPLE_STEP) {
|
|
85
|
+
// BT.601 luma — same approximation OpenCV uses for cvtColor RGB→GRAY.
|
|
86
|
+
total += 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
87
|
+
count += 1;
|
|
88
|
+
}
|
|
89
|
+
return count === 0 ? 0 : total / count;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Estimate sharpness as the variance of a 3x3 Laplacian filter applied to the
|
|
94
|
+
* grayscaled ROI. Higher = sharper. Empirically values < ~30 indicate
|
|
95
|
+
* meaningful motion blur for a downsampled webcam frame.
|
|
96
|
+
*/
|
|
97
|
+
export const calculateBlurScore = (
|
|
98
|
+
video: HTMLVideoElement,
|
|
99
|
+
region?: { x: number; y: number; width: number; height: number },
|
|
100
|
+
): number => {
|
|
101
|
+
if (!video || video.videoWidth === 0 || video.videoHeight === 0) return 0;
|
|
102
|
+
|
|
103
|
+
const w = video.videoWidth;
|
|
104
|
+
const h = video.videoHeight;
|
|
105
|
+
const r = region ?? { x: 0.25, y: 0.25, width: 0.5, height: 0.5 };
|
|
106
|
+
|
|
107
|
+
const sx = Math.max(0, Math.floor(r.x * w));
|
|
108
|
+
const sy = Math.max(0, Math.floor(r.y * h));
|
|
109
|
+
const sw = Math.min(w - sx, Math.floor(r.width * w));
|
|
110
|
+
const sh = Math.min(h - sy, Math.floor(r.height * h));
|
|
111
|
+
if (sw <= 0 || sh <= 0) return 0;
|
|
112
|
+
|
|
113
|
+
const targetSize = 96;
|
|
114
|
+
const scale = Math.min(1, targetSize / Math.max(sw, sh));
|
|
115
|
+
const dw = Math.max(3, Math.round(sw * scale));
|
|
116
|
+
const dh = Math.max(3, Math.round(sh * scale));
|
|
117
|
+
|
|
118
|
+
const ctx = getScratchContext('blur', dw, dh);
|
|
119
|
+
if (!ctx) return 0;
|
|
120
|
+
|
|
121
|
+
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, dw, dh);
|
|
122
|
+
const { data } = ctx.getImageData(0, 0, dw, dh);
|
|
123
|
+
|
|
124
|
+
// Convert to grayscale once.
|
|
125
|
+
const gray = new Float32Array(dw * dh);
|
|
126
|
+
for (let i = 0; i < gray.length; i += 1) {
|
|
127
|
+
const o = i * 4;
|
|
128
|
+
gray[i] = 0.299 * data[o] + 0.587 * data[o + 1] + 0.114 * data[o + 2];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Variance of 3x3 Laplacian (skip 1-px border).
|
|
132
|
+
let sum = 0;
|
|
133
|
+
let sumSq = 0;
|
|
134
|
+
let n = 0;
|
|
135
|
+
for (let y = 1; y < dh - 1; y += 1) {
|
|
136
|
+
for (let x = 1; x < dw - 1; x += 1) {
|
|
137
|
+
const i = y * dw + x;
|
|
138
|
+
const lap =
|
|
139
|
+
4 * gray[i] - gray[i - 1] - gray[i + 1] - gray[i - dw] - gray[i + dw];
|
|
140
|
+
sum += lap;
|
|
141
|
+
sumSq += lap * lap;
|
|
142
|
+
n += 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (n === 0) return 0;
|
|
146
|
+
const mean = sum / n;
|
|
147
|
+
return sumSq / n - mean * mean;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const DEFAULT_LUMINANCE_MIN = 85; // average luma below this = "too dark"
|
|
151
|
+
export const DEFAULT_BLUR_MIN = 80; // Laplacian variance below this = "too blurry"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
|
|
2
|
+
|
|
3
|
+
const EXCLUDED_GPUS = [
|
|
4
|
+
'adreno-830',
|
|
5
|
+
'adreno-8xx',
|
|
6
|
+
'adreno-9xx',
|
|
7
|
+
'adreno-840',
|
|
8
|
+
'adreno-810',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const normalizeGpuText = (value: string): string =>
|
|
12
|
+
value
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/\(tm\)|\btm\b/g, '')
|
|
15
|
+
.replace(/[^a-z0-9]/g, '');
|
|
16
|
+
|
|
17
|
+
const matchesExcludedGpu = (value: string): boolean => {
|
|
18
|
+
const normalizedValue = normalizeGpuText(value);
|
|
19
|
+
|
|
20
|
+
return EXCLUDED_GPUS.some((gpuPattern) => {
|
|
21
|
+
const normalizedPattern = normalizeGpuText(gpuPattern);
|
|
22
|
+
|
|
23
|
+
if (normalizedPattern.endsWith('xx')) {
|
|
24
|
+
const familyPrefix = normalizedPattern.slice(0, -2);
|
|
25
|
+
return new RegExp(`${familyPrefix}\\d{2}`).test(normalizedValue);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizedValue.includes(normalizedPattern);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @description Gets the GPU renderer string using WebGL debug info extension.
|
|
34
|
+
* @returns {string | null} The GPU renderer string or null if unavailable.
|
|
35
|
+
*/
|
|
36
|
+
const getGpuRenderer = (): string | null => {
|
|
37
|
+
try {
|
|
38
|
+
const canvas = document.createElement('canvas');
|
|
39
|
+
const gl =
|
|
40
|
+
canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
41
|
+
if (!gl || !(gl instanceof WebGLRenderingContext)) return null;
|
|
42
|
+
|
|
43
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
44
|
+
if (!ext) return null;
|
|
45
|
+
|
|
46
|
+
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) as string | null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @description Checks if the GPU renderer matches any excluded GPU.
|
|
54
|
+
* @param {string | null} [renderer] Optional GPU renderer string to use. If not provided, it will be fetched via WebGL.
|
|
55
|
+
* @returns {boolean} True if the GPU is excluded.
|
|
56
|
+
*/
|
|
57
|
+
const isExcludedGpuFromWebGL = (renderer?: string | null): boolean => {
|
|
58
|
+
const rendererString = (renderer ?? getGpuRenderer())?.toLowerCase() ?? '';
|
|
59
|
+
if (!rendererString) return false;
|
|
60
|
+
|
|
61
|
+
return matchesExcludedGpu(rendererString);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
declare global {
|
|
65
|
+
interface Window {
|
|
66
|
+
__smileIdentityMediapipe?: {
|
|
67
|
+
instance: FaceLandmarker | null;
|
|
68
|
+
loading: Promise<FaceLandmarker> | null;
|
|
69
|
+
loaded: boolean;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @description Reads system architecture hints from User-Agent Client Hints.
|
|
76
|
+
* @returns {Promise<string | null>} Lower-cased hint string or null when hints are unavailable.
|
|
77
|
+
*/
|
|
78
|
+
const getSystemArchitectureHints = async (): Promise<string | null> => {
|
|
79
|
+
if (typeof navigator === 'undefined' || !(navigator as any).userAgentData) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const hints = await (navigator as any).userAgentData.getHighEntropyValues([
|
|
85
|
+
'architecture',
|
|
86
|
+
'model',
|
|
87
|
+
'platform',
|
|
88
|
+
'platformVersion',
|
|
89
|
+
'fullVersionList',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
return JSON.stringify(hints).toLowerCase();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn('UA-CH architecture hints fetch failed.', error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @description Determines the MediaPipe delegate based on WebGL renderer info and UA-CH hints.
|
|
101
|
+
* Uses WebGL renderer as primary detection, UA-CH hints as secondary.
|
|
102
|
+
* @returns {Promise<'CPU' | 'GPU'>} CPU when excluded GPU is detected; otherwise GPU.
|
|
103
|
+
*/
|
|
104
|
+
const getDelegateFromGpuDetection = async (): Promise<'CPU' | 'GPU'> => {
|
|
105
|
+
const renderer = getGpuRenderer();
|
|
106
|
+
const hasWebGlRendererInfo = !!renderer;
|
|
107
|
+
|
|
108
|
+
// Primary check: WebGL renderer info (most reliable for GPU detection)
|
|
109
|
+
if (isExcludedGpuFromWebGL(renderer)) {
|
|
110
|
+
console.info(`[SmileID] Excluded GPU via WebGL: ${renderer}. Using CPU.`);
|
|
111
|
+
return 'CPU';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Secondary check: UA-CH hints (may contain GPU info in some browsers)
|
|
115
|
+
const hintString = await getSystemArchitectureHints();
|
|
116
|
+
const hasUaHints = !!hintString;
|
|
117
|
+
|
|
118
|
+
if (hintString) {
|
|
119
|
+
const hasExcludedGpuInHints = matchesExcludedGpu(hintString);
|
|
120
|
+
|
|
121
|
+
if (hasExcludedGpuInHints) {
|
|
122
|
+
console.info(`[SmileID] Excluded GPU via UA-CH hints. Using CPU.`);
|
|
123
|
+
return 'CPU';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!hasWebGlRendererInfo && !hasUaHints) {
|
|
128
|
+
console.info(
|
|
129
|
+
'[SmileID] No WebGL renderer or UA-CH hints available. Using CPU.',
|
|
130
|
+
);
|
|
131
|
+
return 'CPU';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Default to GPU when no exclusion is detected
|
|
135
|
+
console.info(
|
|
136
|
+
`[SmileID] No excluded GPU detected. WebGL renderer: ${renderer ?? 'unavailable'}. Using GPU.`,
|
|
137
|
+
);
|
|
138
|
+
return 'GPU';
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// this was added because devices (mostly older) that do not support FP16 will fail to load the model.
|
|
142
|
+
const hasFP16Support = () => {
|
|
143
|
+
const canvas = document.createElement('canvas');
|
|
144
|
+
const gl =
|
|
145
|
+
canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
146
|
+
if (!gl) return false;
|
|
147
|
+
|
|
148
|
+
const hasHalfFloatExt = (gl as any).getExtension('OES_texture_half_float');
|
|
149
|
+
const hasHalfFloatLinear = (gl as any).getExtension(
|
|
150
|
+
'OES_texture_half_float_linear',
|
|
151
|
+
);
|
|
152
|
+
const hasColorBufferHalfFloat = (gl as any).getExtension(
|
|
153
|
+
'EXT_color_buffer_half_float',
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return !!(hasHalfFloatExt && hasColorBufferHalfFloat && hasHalfFloatLinear);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
160
|
+
if (!window.__smileIdentityMediapipe) {
|
|
161
|
+
window.__smileIdentityMediapipe = {
|
|
162
|
+
instance: null,
|
|
163
|
+
loading: null,
|
|
164
|
+
loaded: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mediapipeGlobal = window.__smileIdentityMediapipe;
|
|
169
|
+
|
|
170
|
+
if (mediapipeGlobal.loaded && mediapipeGlobal.instance) {
|
|
171
|
+
return mediapipeGlobal.instance;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (mediapipeGlobal.loading) {
|
|
175
|
+
return mediapipeGlobal.loading;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
mediapipeGlobal.loading = (async () => {
|
|
179
|
+
try {
|
|
180
|
+
const vision = await FilesetResolver.forVisionTasks(
|
|
181
|
+
'https://web-models.smileidentity.com/mediapipe-tasks-vision-wasm',
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const gpuDelegate = await getDelegateFromGpuDetection();
|
|
185
|
+
const delegate =
|
|
186
|
+
gpuDelegate === 'CPU' || !hasFP16Support() ? 'CPU' : 'GPU';
|
|
187
|
+
|
|
188
|
+
const faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
|
|
189
|
+
baseOptions: {
|
|
190
|
+
modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
|
|
191
|
+
delegate,
|
|
192
|
+
},
|
|
193
|
+
outputFaceBlendshapes: true,
|
|
194
|
+
runningMode: 'VIDEO',
|
|
195
|
+
numFaces: 2,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
mediapipeGlobal.instance = faceLandmarker;
|
|
199
|
+
mediapipeGlobal.loaded = true;
|
|
200
|
+
mediapipeGlobal.loading = null;
|
|
201
|
+
|
|
202
|
+
return faceLandmarker;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
mediapipeGlobal.loading = null;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
|
|
209
|
+
return mediapipeGlobal.loading;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const __testUtils = {
|
|
213
|
+
matchesExcludedGpu,
|
|
214
|
+
getDelegateFromGpuDetection,
|
|
215
|
+
};
|
|
@@ -6,6 +6,7 @@ import type { FunctionComponent } from 'preact';
|
|
|
6
6
|
import { getBoolProp } from '../../../../utils/props';
|
|
7
7
|
import { translate, translateHtml } from '../../../../domain/localisation';
|
|
8
8
|
import SmartSelfieCapture from '../smartselfie-capture/SmartSelfieCapture';
|
|
9
|
+
import EnhancedSmartSelfieCapture from '../enhanced-smartselfie-capture/EnhancedSmartSelfieCapture';
|
|
9
10
|
// Legacy web component fallback (used when Mediapipe isn't available)
|
|
10
11
|
import '../selfie-capture/SelfieCapture';
|
|
11
12
|
// Mediapipe loader/manager used by SmartSelfieCapture
|
|
@@ -40,6 +41,8 @@ interface Props {
|
|
|
40
41
|
'show-agent-mode-for-tests'?: string | boolean;
|
|
41
42
|
'hide-attribution'?: string | boolean;
|
|
42
43
|
'disable-image-tests'?: string | boolean;
|
|
44
|
+
'use-strict-mode'?: string | boolean;
|
|
45
|
+
'show-back-on-guidelines'?: string | boolean;
|
|
43
46
|
key?: string;
|
|
44
47
|
'start-countdown'?: string | boolean;
|
|
45
48
|
hidden?: string | boolean;
|
|
@@ -73,6 +76,7 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
73
76
|
timeout,
|
|
74
77
|
'start-countdown': startCountdownProp = false,
|
|
75
78
|
'allow-legacy-selfie-fallback': allowLegacySelfieFallbackProp = false,
|
|
79
|
+
'use-strict-mode': useStrictModeProp = false,
|
|
76
80
|
hidden: hiddenProp = false,
|
|
77
81
|
...props
|
|
78
82
|
}) => {
|
|
@@ -106,7 +110,11 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
106
110
|
|
|
107
111
|
const hidden = getBoolProp(hiddenProp);
|
|
108
112
|
const startCountdown = getBoolProp(startCountdownProp);
|
|
109
|
-
const
|
|
113
|
+
const useStrictMode = getBoolProp(useStrictModeProp);
|
|
114
|
+
// Strict mode (Enhanced SmartSelfie) requires Mediapipe head-pose detection,
|
|
115
|
+
// so the legacy fallback is force-disabled regardless of the partner setting.
|
|
116
|
+
const allowLegacySelfieFallback =
|
|
117
|
+
!useStrictMode && getBoolProp(allowLegacySelfieFallbackProp);
|
|
110
118
|
|
|
111
119
|
// Resolve how long we'll wait for Mediapipe before the hard deadline fires.
|
|
112
120
|
// Precedence:
|
|
@@ -463,6 +471,19 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
463
471
|
return null;
|
|
464
472
|
}
|
|
465
473
|
|
|
474
|
+
// Strict mode (Enhanced SmartSelfie) owns its own loading UX. Mount it
|
|
475
|
+
// immediately — the guidelines screen doesn't need Mediapipe, and by the
|
|
476
|
+
// time the user reaches the capture screen the background load started by
|
|
477
|
+
// `useFaceCapture.initializeFaceLandmarker()` will normally have resolved.
|
|
478
|
+
if (useStrictMode) {
|
|
479
|
+
return (
|
|
480
|
+
<>
|
|
481
|
+
<style>{`:host { display: block; height: 100%; }`}</style>
|
|
482
|
+
<EnhancedSmartSelfieCapture {...(props as any)} />
|
|
483
|
+
</>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
466
487
|
// on retakes, prefer SmartSelfieCapture if Mediapipe is ready
|
|
467
488
|
if (initialSessionCompleted && mediapipeReady && !usingSelfieCapture) {
|
|
468
489
|
return <SmartSelfieCapture {...props} />;
|
|
@@ -613,6 +634,8 @@ if (!customElements.get('selfie-capture-wrapper')) {
|
|
|
613
634
|
'show-agent-mode-for-tests',
|
|
614
635
|
'hide-attribution',
|
|
615
636
|
'disable-image-tests',
|
|
637
|
+
'use-strict-mode',
|
|
638
|
+
'show-back-on-guidelines',
|
|
616
639
|
'key',
|
|
617
640
|
'start-countdown',
|
|
618
641
|
'hidden',
|
|
@@ -237,11 +237,11 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
237
237
|
font-family: "DM Sans", sans-serif;
|
|
238
238
|
cursor: pointer;
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
button.btn-primary:hover {
|
|
242
242
|
background-color: #2d2b2a;
|
|
243
243
|
}
|
|
244
|
-
|
|
244
|
+
|
|
245
245
|
button.btn-primary:disabled {
|
|
246
246
|
background-color: #666;
|
|
247
247
|
cursor: not-allowed;
|
|
@@ -336,6 +336,17 @@ export const useFaceCapture = ({
|
|
|
336
336
|
}, 0);
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
+
|
|
340
|
+
// Before the smile zone, resume automatically when face returns to a
|
|
341
|
+
// valid position — no smile is needed yet.
|
|
342
|
+
if (
|
|
343
|
+
isPaused.value &&
|
|
344
|
+
isCapturing.value &&
|
|
345
|
+
capturesTaken.value < smileCheckpoint.value &&
|
|
346
|
+
resumeCaptureRef.current
|
|
347
|
+
) {
|
|
348
|
+
resumeCaptureRef.current();
|
|
349
|
+
}
|
|
339
350
|
} else {
|
|
340
351
|
// No face detected - reset values
|
|
341
352
|
currentSmileScore.value = 0;
|
|
@@ -414,9 +425,7 @@ export const useFaceCapture = ({
|
|
|
414
425
|
};
|
|
415
426
|
|
|
416
427
|
window.dispatchEvent(
|
|
417
|
-
new CustomEvent('selfie-capture.publish', {
|
|
418
|
-
detail: eventDetail,
|
|
419
|
-
}),
|
|
428
|
+
new CustomEvent('selfie-capture.publish', { detail: eventDetail }),
|
|
420
429
|
);
|
|
421
430
|
|
|
422
431
|
hasFinishedCapture.value = true;
|
|
@@ -515,10 +524,9 @@ export const useFaceCapture = ({
|
|
|
515
524
|
smartCameraWeb?.dispatchEvent(
|
|
516
525
|
new CustomEvent('metadata.selfie-origin', {
|
|
517
526
|
detail: {
|
|
518
|
-
imageOrigin: {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}[getFacingMode()],
|
|
527
|
+
imageOrigin: { environment: 'back_camera', user: 'front_camera' }[
|
|
528
|
+
getFacingMode()
|
|
529
|
+
],
|
|
522
530
|
},
|
|
523
531
|
}),
|
|
524
532
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { DrawingUtils, FaceLandmarker } from '@mediapipe/tasks-vision';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Create a cropped square canvas from video for face detection
|
|
5
3
|
*/
|
|
4
|
+
import { DrawingUtils, FaceLandmarker } from '@mediapipe/tasks-vision';
|
|
5
|
+
|
|
6
6
|
export const createCroppedVideoFrame = (
|
|
7
7
|
videoElement: HTMLVideoElement,
|
|
8
8
|
): HTMLCanvasElement | null => {
|
|
@@ -51,16 +51,14 @@ export const drawFaceMesh = (
|
|
|
51
51
|
const canvasHeight = canvas.height;
|
|
52
52
|
|
|
53
53
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
54
|
-
const drawingUtils = new DrawingUtils(ctx);
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
// const scaleFactor = Math.sqrt(canvasWidth * canvasHeight) / 500;
|
|
55
|
+
const drawingUtils = new DrawingUtils(ctx);
|
|
58
56
|
|
|
59
57
|
landmarks.forEach((landmark) => {
|
|
60
58
|
if (!landmark || landmark.length === 0) return;
|
|
61
59
|
|
|
62
60
|
const outlineColor = 'rgba(162, 155, 254,0.4)';
|
|
63
|
-
const lineWidth = 2;
|
|
61
|
+
const lineWidth = 2;
|
|
64
62
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
|
65
63
|
ctx.lineWidth = lineWidth;
|
|
66
64
|
ctx.lineCap = 'round';
|
|
@@ -193,6 +193,17 @@ After installation and necessary imports:
|
|
|
193
193
|
|
|
194
194
|
This approach can also be achieved using other Server to Server libraries.
|
|
195
195
|
|
|
196
|
+
### Enhanced SmartSelfie
|
|
197
|
+
|
|
198
|
+
For web-component integrations, Enhanced SmartSelfie is enabled with the
|
|
199
|
+
`use-strict-mode` attribute on the component.
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<smart-camera-web use-strict-mode="true"></smart-camera-web>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- Set `use-strict-mode="true"` to enable strict-mode selfie capture.
|
|
206
|
+
|
|
196
207
|
## Compatibility
|
|
197
208
|
|
|
198
209
|
`SmartCameraWeb` is compatible with most JavaScript frameworks and libraries. For integration with [ReactJS](https://reactjs.org), refer to this [tutorial](https://www.robinwieruch.de/react-web-components) due to React-WebComponents compatibility issues.
|