@smileid/web-components 11.4.4 → 11.5.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-bLFW-yEM.js → DocumentCaptureScreens-ucJDu5nH.js} +555 -2470
- package/dist/esm/DocumentCaptureScreens-ucJDu5nH.js.map +1 -0
- package/dist/esm/{EndUserConsent-D26UoVk5.js → EndUserConsent-CsiwoThZ.js} +3 -3
- package/dist/esm/{EndUserConsent-D26UoVk5.js.map → EndUserConsent-CsiwoThZ.js.map} +1 -1
- package/dist/esm/{Navigation-nvehze1F.js → Navigation-Xg565kcu.js} +28 -22
- package/dist/esm/Navigation-Xg565kcu.js.map +1 -0
- package/dist/esm/SelfieCaptureScreens-D3KuMzZA.js +11471 -0
- package/dist/esm/SelfieCaptureScreens-D3KuMzZA.js.map +1 -0
- package/dist/esm/{TotpConsent-owUOdKzP.js → TotpConsent-CRtmtudl.js} +2 -2
- package/dist/esm/{TotpConsent-owUOdKzP.js.map → TotpConsent-CRtmtudl.js.map} +1 -1
- package/dist/esm/combobox.js +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/index-CUwa6MPI.js +1363 -0
- package/dist/esm/{index-5Nn2kzHI.js.map → index-CUwa6MPI.js.map} +1 -1
- package/dist/esm/localisation.js +1 -1
- package/dist/esm/main.js +6 -6
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/package-BmVbDNny.js +2535 -0
- package/dist/esm/package-BmVbDNny.js.map +1 -0
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +67 -40
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/totp-consent.js +1 -1
- package/dist/smart-camera-web.js +877 -122
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/main.d.ts +13 -0
- package/lib/components/navigation/src/Navigation.js +27 -8
- package/lib/components/selfie/src/SelfieCaptureScreens.js +139 -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 +163 -17
- 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/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +145 -9
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/smart-camera-web/src/SmartCameraWeb.js +70 -11
- package/lib/domain/localisation/index.js +2 -2
- package/package.json +3 -3
- package/dist/esm/DocumentCaptureScreens-bLFW-yEM.js.map +0 -1
- package/dist/esm/Navigation-nvehze1F.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-BXIs6_tl.js +0 -7522
- package/dist/esm/SelfieCaptureScreens-BXIs6_tl.js.map +0 -1
- package/dist/esm/index-5Nn2kzHI.js +0 -1360
- package/dist/esm/package-DmH-I6GW.js +0 -565
- package/dist/esm/package-DmH-I6GW.js.map +0 -1
|
@@ -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
|
+
};
|