@smileid/web-components 11.3.0 → 11.4.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-N-0o7eE5.js → DocumentCaptureScreens-DbU8ZxMx.js} +4 -4
- package/dist/esm/{DocumentCaptureScreens-N-0o7eE5.js.map → DocumentCaptureScreens-DbU8ZxMx.js.map} +1 -1
- package/dist/esm/{EndUserConsent-BgKCCMMn.js → EndUserConsent-BGO3oZ-m.js} +3 -3
- package/dist/esm/{EndUserConsent-BgKCCMMn.js.map → EndUserConsent-BGO3oZ-m.js.map} +1 -1
- package/dist/esm/{Navigation-DOFYqTZt.js → Navigation-DH44dkMT.js} +2 -2
- package/dist/esm/{Navigation-DOFYqTZt.js.map → Navigation-DH44dkMT.js.map} +1 -1
- package/dist/esm/{SelfieCaptureScreens-DKd0f7K8.js → SelfieCaptureScreens-bmwnmeS9.js} +2169 -2142
- package/dist/esm/SelfieCaptureScreens-bmwnmeS9.js.map +1 -0
- package/dist/esm/{TotpConsent-BQm8j4-u.js → TotpConsent-V3_Ip2Kw.js} +2 -2
- package/dist/esm/{TotpConsent-BQm8j4-u.js.map → TotpConsent-V3_Ip2Kw.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-Cjzs1eA_.js → index-Dnpp-kwk.js} +41 -41
- package/dist/esm/{index-Cjzs1eA_.js.map → index-Dnpp-kwk.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-KQ2l43v1.js → package-7J5h4EOW.js} +3 -3
- package/dist/esm/{package-KQ2l43v1.js.map → package-7J5h4EOW.js.map} +1 -1
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +5 -5
- package/dist/esm/totp-consent.js +1 -1
- package/dist/smart-camera-web.js +38 -38
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/document-auto-capture.d.ts +34 -0
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +3 -15
- package/lib/components/selfie/src/smartselfie-capture/components/CameraPreview.tsx +6 -10
- package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +7 -1
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +33 -19
- package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +0 -1
- package/lib/components/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +127 -30
- package/lib/components/signature-pad/package-lock.json +15 -12
- package/lib/components/signature-pad/package.json +1 -1
- package/package.json +2 -1
- package/dist/esm/SelfieCaptureScreens-DKd0f7K8.js.map +0 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
declare const SmartDocumentCapture: FunctionComponent<SmartDocumentCaptureProps>;
|
|
4
|
+
export default SmartDocumentCapture;
|
|
5
|
+
|
|
6
|
+
declare interface SmartDocumentCaptureProps {
|
|
7
|
+
'document-type'?: string;
|
|
8
|
+
'side-of-id'?: string;
|
|
9
|
+
'show-navigation'?: string | boolean;
|
|
10
|
+
'theme-color'?: string;
|
|
11
|
+
'hide-attribution'?: string | boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { }
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
declare module 'signature_pad' {
|
|
19
|
+
export default class SignaturePad {
|
|
20
|
+
constructor(canvas: HTMLCanvasElement, options?: any);
|
|
21
|
+
|
|
22
|
+
clear(): void;
|
|
23
|
+
|
|
24
|
+
toDataURL(type?: string): string;
|
|
25
|
+
|
|
26
|
+
fromDataURL(dataURL: string): void;
|
|
27
|
+
|
|
28
|
+
isEmpty(): boolean;
|
|
29
|
+
|
|
30
|
+
on(event: string, callback: (...args: unknown[]) => void): void;
|
|
31
|
+
|
|
32
|
+
off(event: string, callback: (...args: unknown[]) => void): void;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { useRef, useEffect } from 'preact/hooks';
|
|
2
|
-
import { useSignal } from '@preact/signals';
|
|
3
2
|
import register from 'preact-custom-element';
|
|
4
3
|
import type { FunctionComponent } from 'preact';
|
|
5
|
-
import throttle from 'lodash/throttle';
|
|
6
4
|
|
|
7
5
|
import { getBoolProp } from '../../../../utils/props';
|
|
8
6
|
import { useFaceCapture, useCamera } from './hooks';
|
|
@@ -50,13 +48,6 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
50
48
|
const initialFacingMode = allowAgentMode ? 'environment' : 'user';
|
|
51
49
|
const camera = useCamera(initialFacingMode);
|
|
52
50
|
|
|
53
|
-
const throttledMultipleFaces = useSignal(false);
|
|
54
|
-
const updateMultipleFacesUI = useRef(
|
|
55
|
-
throttle((value: boolean) => {
|
|
56
|
-
throttledMultipleFaces.value = value;
|
|
57
|
-
}, 100),
|
|
58
|
-
).current;
|
|
59
|
-
|
|
60
51
|
const faceCapture = useFaceCapture({
|
|
61
52
|
videoRef: camera.videoRef,
|
|
62
53
|
canvasRef,
|
|
@@ -106,14 +97,9 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
106
97
|
faceCapture.stopDetectionLoop();
|
|
107
98
|
camera.stopCamera();
|
|
108
99
|
faceCapture.cleanup();
|
|
109
|
-
updateMultipleFacesUI.cancel();
|
|
110
100
|
};
|
|
111
101
|
}, []);
|
|
112
102
|
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
updateMultipleFacesUI(faceCapture.multipleFaces.value);
|
|
115
|
-
}, [faceCapture.multipleFaces.value]);
|
|
116
|
-
|
|
117
103
|
useEffect(() => {
|
|
118
104
|
const navigation = navigationRef.current;
|
|
119
105
|
|
|
@@ -160,7 +146,6 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
160
146
|
videoRef={camera.videoRef}
|
|
161
147
|
canvasRef={canvasRef}
|
|
162
148
|
facingMode={camera.facingMode}
|
|
163
|
-
multipleFaces={throttledMultipleFaces.value}
|
|
164
149
|
progress={
|
|
165
150
|
faceCapture.capturesTaken.value > 0
|
|
166
151
|
? faceCapture.capturesTaken.value / faceCapture.totalCaptures.value
|
|
@@ -178,6 +163,9 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
178
163
|
isCapturing={faceCapture.isCapturing.value}
|
|
179
164
|
hasFinishedCapture={faceCapture.hasFinishedCapture.value}
|
|
180
165
|
isReadyToCapture={faceCapture.isReadyToCapture.value}
|
|
166
|
+
captureButtonFallbackEnabled={
|
|
167
|
+
faceCapture.captureButtonFallbackEnabled.value
|
|
168
|
+
}
|
|
181
169
|
allowAgentMode={allowAgentMode}
|
|
182
170
|
agentSupported={camera.agentSupported}
|
|
183
171
|
showAgentModeForTests={showAgentModeForTests}
|
|
@@ -5,7 +5,6 @@ interface CameraPreviewProps {
|
|
|
5
5
|
videoRef: Ref<HTMLVideoElement>;
|
|
6
6
|
canvasRef: Ref<HTMLCanvasElement>;
|
|
7
7
|
facingMode: 'user' | 'environment';
|
|
8
|
-
multipleFaces: boolean;
|
|
9
8
|
progress: number;
|
|
10
9
|
interval: number;
|
|
11
10
|
themeColor: string;
|
|
@@ -15,7 +14,6 @@ export const CameraPreview: FunctionComponent<CameraPreviewProps> = ({
|
|
|
15
14
|
videoRef,
|
|
16
15
|
canvasRef,
|
|
17
16
|
facingMode,
|
|
18
|
-
multipleFaces,
|
|
19
17
|
progress,
|
|
20
18
|
interval,
|
|
21
19
|
themeColor,
|
|
@@ -25,7 +23,7 @@ export const CameraPreview: FunctionComponent<CameraPreviewProps> = ({
|
|
|
25
23
|
<div
|
|
26
24
|
className="video-wrapper"
|
|
27
25
|
style={{
|
|
28
|
-
clipPath:
|
|
26
|
+
clipPath: 'url(#selfie-clip-path)',
|
|
29
27
|
}}
|
|
30
28
|
>
|
|
31
29
|
<div className="video-container">
|
|
@@ -42,13 +40,11 @@ export const CameraPreview: FunctionComponent<CameraPreviewProps> = ({
|
|
|
42
40
|
/>
|
|
43
41
|
</div>
|
|
44
42
|
</div>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/>
|
|
51
|
-
)}
|
|
43
|
+
<OvalProgress
|
|
44
|
+
progress={progress}
|
|
45
|
+
duration={interval}
|
|
46
|
+
themeColor={themeColor}
|
|
47
|
+
/>
|
|
52
48
|
</div>
|
|
53
49
|
|
|
54
50
|
<style>{`
|
|
@@ -5,6 +5,7 @@ interface CaptureControlsProps {
|
|
|
5
5
|
isCapturing: boolean;
|
|
6
6
|
hasFinishedCapture: boolean;
|
|
7
7
|
isReadyToCapture: boolean;
|
|
8
|
+
captureButtonFallbackEnabled: boolean;
|
|
8
9
|
allowAgentMode: boolean;
|
|
9
10
|
agentSupported: boolean;
|
|
10
11
|
showAgentModeForTests: boolean;
|
|
@@ -18,6 +19,7 @@ export const CaptureControls: FunctionComponent<CaptureControlsProps> = ({
|
|
|
18
19
|
isCapturing,
|
|
19
20
|
hasFinishedCapture,
|
|
20
21
|
isReadyToCapture,
|
|
22
|
+
captureButtonFallbackEnabled,
|
|
21
23
|
allowAgentMode,
|
|
22
24
|
agentSupported,
|
|
23
25
|
showAgentModeForTests,
|
|
@@ -32,7 +34,11 @@ export const CaptureControls: FunctionComponent<CaptureControlsProps> = ({
|
|
|
32
34
|
id="start-image-capture"
|
|
33
35
|
class="btn-primary"
|
|
34
36
|
onClick={onStartCapture}
|
|
35
|
-
disabled={
|
|
37
|
+
disabled={
|
|
38
|
+
isCapturing ||
|
|
39
|
+
hasFinishedCapture ||
|
|
40
|
+
(!isReadyToCapture && !captureButtonFallbackEnabled)
|
|
41
|
+
}
|
|
36
42
|
>
|
|
37
43
|
{t('selfie.capture.button.startCapture')}
|
|
38
44
|
</button>
|
|
@@ -51,11 +51,11 @@ export const useFaceCapture = ({
|
|
|
51
51
|
const animationFrameRef = useRef<number | null>(null);
|
|
52
52
|
const captureTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
53
53
|
const resumeCaptureRef = useRef<(() => void) | null>(null);
|
|
54
|
+
const fallbackTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
54
55
|
|
|
55
56
|
const faceDetected = useSignal(false);
|
|
56
57
|
const faceInBounds = useSignal(false);
|
|
57
58
|
const faceProximity = useSignal<'too-close' | 'too-far' | 'good'>('good');
|
|
58
|
-
const multipleFaces = useSignal(false);
|
|
59
59
|
const videoAspectRatio = useSignal(16 / 9);
|
|
60
60
|
const faceLandmarks = useSignal<any[]>([]);
|
|
61
61
|
const currentSmileScore = useSignal(0);
|
|
@@ -64,6 +64,7 @@ export const useFaceCapture = ({
|
|
|
64
64
|
const lastSmileTime = useSignal(0);
|
|
65
65
|
const alertTitle = useSignal('');
|
|
66
66
|
const isInitializing = useSignal(true);
|
|
67
|
+
const captureButtonFallbackEnabled = useSignal(false);
|
|
67
68
|
|
|
68
69
|
const isCapturing = useSignal(false);
|
|
69
70
|
const isPaused = useSignal(false);
|
|
@@ -83,8 +84,7 @@ export const useFaceCapture = ({
|
|
|
83
84
|
() =>
|
|
84
85
|
faceDetected.value &&
|
|
85
86
|
faceInBounds.value &&
|
|
86
|
-
faceProximity.value === 'good'
|
|
87
|
-
!multipleFaces.value,
|
|
87
|
+
faceProximity.value === 'good',
|
|
88
88
|
);
|
|
89
89
|
|
|
90
90
|
const updateAlertImmediate = (messageKey: MessageKey | null) => {
|
|
@@ -101,6 +101,19 @@ export const useFaceCapture = ({
|
|
|
101
101
|
}, 600),
|
|
102
102
|
).current;
|
|
103
103
|
|
|
104
|
+
const CAPTURE_FALLBACK_TIMEOUT_MS = 10000;
|
|
105
|
+
|
|
106
|
+
const startFallbackTimer = () => {
|
|
107
|
+
if (fallbackTimerRef.current) {
|
|
108
|
+
clearTimeout(fallbackTimerRef.current);
|
|
109
|
+
}
|
|
110
|
+
fallbackTimerRef.current = setTimeout(() => {
|
|
111
|
+
if (!isReadyToCapture.value) {
|
|
112
|
+
captureButtonFallbackEnabled.value = true;
|
|
113
|
+
}
|
|
114
|
+
}, CAPTURE_FALLBACK_TIMEOUT_MS);
|
|
115
|
+
};
|
|
116
|
+
|
|
104
117
|
const initializeFaceLandmarker = async () => {
|
|
105
118
|
try {
|
|
106
119
|
const isAlreadyLoaded =
|
|
@@ -114,10 +127,15 @@ export const useFaceCapture = ({
|
|
|
114
127
|
|
|
115
128
|
faceLandmarkerRef.current = await getMediapipeInstance();
|
|
116
129
|
isInitializing.value = false;
|
|
130
|
+
startFallbackTimer();
|
|
117
131
|
} catch (error) {
|
|
118
132
|
console.error('Failed to initialize MediaPipe:', error);
|
|
119
133
|
isInitializing.value = false;
|
|
134
|
+
// MediaPipe failed — start the fallback timer so the button eventually
|
|
135
|
+
// enables and the user isn't permanently stuck.
|
|
136
|
+
startFallbackTimer();
|
|
120
137
|
}
|
|
138
|
+
startFallbackTimer();
|
|
121
139
|
};
|
|
122
140
|
|
|
123
141
|
const setupCanvas = () => {
|
|
@@ -165,8 +183,6 @@ export const useFaceCapture = ({
|
|
|
165
183
|
const updateAlerts = () => {
|
|
166
184
|
if (isInitializing.value) {
|
|
167
185
|
updateAlertImmediate('initializing');
|
|
168
|
-
} else if (multipleFaces.value) {
|
|
169
|
-
updateAlert('multiple-faces');
|
|
170
186
|
} else if (!faceDetected.value) {
|
|
171
187
|
updateAlert('no-face');
|
|
172
188
|
} else if (faceProximity.value === 'too-close') {
|
|
@@ -260,13 +276,12 @@ export const useFaceCapture = ({
|
|
|
260
276
|
|
|
261
277
|
// Check number of faces
|
|
262
278
|
const numFaces = results.faceLandmarks ? results.faceLandmarks.length : 0;
|
|
263
|
-
multipleFaces.value = numFaces > 1;
|
|
264
279
|
|
|
265
280
|
// Check if face is detected
|
|
266
281
|
const hasFace =
|
|
267
282
|
results.faceBlendshapes &&
|
|
268
283
|
results.faceBlendshapes.length > 0 &&
|
|
269
|
-
numFaces
|
|
284
|
+
numFaces >= 1;
|
|
270
285
|
faceDetected.value = hasFace;
|
|
271
286
|
|
|
272
287
|
if (hasFace && results.faceLandmarks) {
|
|
@@ -322,7 +337,7 @@ export const useFaceCapture = ({
|
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
} else {
|
|
325
|
-
// No face detected
|
|
340
|
+
// No face detected - reset values
|
|
326
341
|
currentSmileScore.value = 0;
|
|
327
342
|
currentFaceSize.value = 0;
|
|
328
343
|
currentMouthOpen.value = 0;
|
|
@@ -334,7 +349,6 @@ export const useFaceCapture = ({
|
|
|
334
349
|
} catch {
|
|
335
350
|
faceDetected.value = false;
|
|
336
351
|
faceInBounds.value = false;
|
|
337
|
-
multipleFaces.value = false;
|
|
338
352
|
faceProximity.value = 'good';
|
|
339
353
|
currentMouthOpen.value = 0;
|
|
340
354
|
|
|
@@ -417,7 +431,6 @@ export const useFaceCapture = ({
|
|
|
417
431
|
isPaused.value = true;
|
|
418
432
|
|
|
419
433
|
if (
|
|
420
|
-
!multipleFaces.value &&
|
|
421
434
|
faceDetected.value &&
|
|
422
435
|
faceInBounds.value &&
|
|
423
436
|
faceProximity.value === 'good'
|
|
@@ -437,11 +450,6 @@ export const useFaceCapture = ({
|
|
|
437
450
|
return;
|
|
438
451
|
}
|
|
439
452
|
|
|
440
|
-
if (multipleFaces.value) {
|
|
441
|
-
pauseCapture();
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
453
|
if (!faceDetected.value) {
|
|
446
454
|
return;
|
|
447
455
|
}
|
|
@@ -474,8 +482,7 @@ export const useFaceCapture = ({
|
|
|
474
482
|
if (
|
|
475
483
|
faceDetected.value &&
|
|
476
484
|
faceProximity.value === 'good' &&
|
|
477
|
-
faceInBounds.value
|
|
478
|
-
!multipleFaces.value
|
|
485
|
+
faceInBounds.value
|
|
479
486
|
) {
|
|
480
487
|
const isInSmileZone = capturesTaken.value >= smileCheckpoint.value;
|
|
481
488
|
if (isInSmileZone) {
|
|
@@ -556,6 +563,9 @@ export const useFaceCapture = ({
|
|
|
556
563
|
if (captureTimerRef.current) {
|
|
557
564
|
clearInterval(captureTimerRef.current);
|
|
558
565
|
}
|
|
566
|
+
if (fallbackTimerRef.current) {
|
|
567
|
+
clearTimeout(fallbackTimerRef.current);
|
|
568
|
+
}
|
|
559
569
|
stopDetectionLoop();
|
|
560
570
|
updateAlert.cancel();
|
|
561
571
|
};
|
|
@@ -564,12 +574,16 @@ export const useFaceCapture = ({
|
|
|
564
574
|
faceDetected.value = false;
|
|
565
575
|
faceInBounds.value = false;
|
|
566
576
|
faceProximity.value = 'good';
|
|
567
|
-
multipleFaces.value = false;
|
|
568
577
|
faceLandmarks.value = [];
|
|
569
578
|
currentSmileScore.value = 0;
|
|
570
579
|
currentFaceSize.value = 0;
|
|
571
580
|
currentMouthOpen.value = 0;
|
|
572
581
|
lastSmileTime.value = 0;
|
|
582
|
+
captureButtonFallbackEnabled.value = false;
|
|
583
|
+
if (fallbackTimerRef.current) {
|
|
584
|
+
clearTimeout(fallbackTimerRef.current);
|
|
585
|
+
fallbackTimerRef.current = null;
|
|
586
|
+
}
|
|
573
587
|
|
|
574
588
|
if (canvasRef.current) {
|
|
575
589
|
clearCanvas(canvasRef.current);
|
|
@@ -580,7 +594,6 @@ export const useFaceCapture = ({
|
|
|
580
594
|
faceDetected,
|
|
581
595
|
faceInBounds,
|
|
582
596
|
faceProximity,
|
|
583
|
-
multipleFaces,
|
|
584
597
|
videoAspectRatio,
|
|
585
598
|
faceLandmarks,
|
|
586
599
|
currentSmileScore,
|
|
@@ -590,6 +603,7 @@ export const useFaceCapture = ({
|
|
|
590
603
|
alertTitle,
|
|
591
604
|
isInitializing,
|
|
592
605
|
isReadyToCapture,
|
|
606
|
+
captureButtonFallbackEnabled,
|
|
593
607
|
|
|
594
608
|
isCapturing,
|
|
595
609
|
isPaused,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { t } from '../../../../../domain/localisation';
|
|
2
2
|
|
|
3
3
|
export const MESSAGES = {
|
|
4
|
-
'multiple-faces': () => t('selfie.smart.alert.multipleFaces'),
|
|
5
4
|
'no-face': () => t('selfie.smart.alert.noFace'),
|
|
6
5
|
'out-of-bounds': () => t('selfie.smart.alert.outOfBounds'),
|
|
7
6
|
'too-close': () => t('selfie.smart.alert.tooClose'),
|
|
@@ -1,7 +1,65 @@
|
|
|
1
1
|
import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
};
|
|
5
63
|
|
|
6
64
|
declare global {
|
|
7
65
|
interface Window {
|
|
@@ -14,38 +72,70 @@ declare global {
|
|
|
14
72
|
}
|
|
15
73
|
|
|
16
74
|
/**
|
|
17
|
-
* @description
|
|
18
|
-
*
|
|
19
|
-
* @returns {Promise<boolean>} - True if the device model is in the exclusion list.
|
|
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.
|
|
20
77
|
*/
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Request the 'model' high-entropy value and destructure it directly
|
|
26
|
-
const { model } = await navigator.userAgentData.getHighEntropyValues([
|
|
27
|
-
'model',
|
|
28
|
-
]);
|
|
78
|
+
const getSystemArchitectureHints = async (): Promise<string | null> => {
|
|
79
|
+
if (typeof navigator === 'undefined' || !(navigator as any).userAgentData) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
29
82
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
113
|
|
|
34
|
-
|
|
114
|
+
// Secondary check: UA-CH hints (may contain GPU info in some browsers)
|
|
115
|
+
const hintString = await getSystemArchitectureHints();
|
|
116
|
+
const hasUaHints = !!hintString;
|
|
35
117
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.
|
|
41
|
-
|
|
42
|
-
error,
|
|
43
|
-
);
|
|
44
|
-
return false;
|
|
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';
|
|
45
124
|
}
|
|
46
125
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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';
|
|
49
139
|
};
|
|
50
140
|
|
|
51
141
|
// this was added because devices (mostly older) that do not support FP16 will fail to load the model.
|
|
@@ -91,12 +181,14 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
|
91
181
|
'https://web-models.smileidentity.com/mediapipe-tasks-vision-wasm',
|
|
92
182
|
);
|
|
93
183
|
|
|
94
|
-
const
|
|
184
|
+
const gpuDelegate = await getDelegateFromGpuDetection();
|
|
185
|
+
const delegate =
|
|
186
|
+
gpuDelegate === 'CPU' || !hasFP16Support() ? 'CPU' : 'GPU';
|
|
95
187
|
|
|
96
188
|
const faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
|
|
97
189
|
baseOptions: {
|
|
98
190
|
modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
|
|
99
|
-
delegate
|
|
191
|
+
delegate,
|
|
100
192
|
},
|
|
101
193
|
outputFaceBlendshapes: true,
|
|
102
194
|
runningMode: 'VIDEO',
|
|
@@ -116,3 +208,8 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
|
116
208
|
|
|
117
209
|
return mediapipeGlobal.loading;
|
|
118
210
|
};
|
|
211
|
+
|
|
212
|
+
export const __testUtils = {
|
|
213
|
+
matchesExcludedGpu,
|
|
214
|
+
getDelegateFromGpuDetection,
|
|
215
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/signature-pad",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@smileid/signature-pad",
|
|
9
|
-
"version": "11.0
|
|
9
|
+
"version": "11.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"signature_pad": "^5.0.2"
|
|
12
12
|
},
|
|
@@ -224,21 +224,23 @@
|
|
|
224
224
|
}
|
|
225
225
|
},
|
|
226
226
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
|
227
|
-
"version": "2.0.
|
|
228
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.
|
|
229
|
-
"integrity": "sha512-
|
|
227
|
+
"version": "2.0.2",
|
|
228
|
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
229
|
+
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
230
230
|
"dev": true,
|
|
231
|
+
"license": "MIT",
|
|
231
232
|
"dependencies": {
|
|
232
233
|
"balanced-match": "^1.0.0"
|
|
233
234
|
}
|
|
234
235
|
},
|
|
235
236
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
|
236
|
-
"version": "9.0.
|
|
237
|
-
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.
|
|
238
|
-
"integrity": "sha512-
|
|
237
|
+
"version": "9.0.9",
|
|
238
|
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
|
239
|
+
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
|
239
240
|
"dev": true,
|
|
241
|
+
"license": "ISC",
|
|
240
242
|
"dependencies": {
|
|
241
|
-
"brace-expansion": "^2.0.
|
|
243
|
+
"brace-expansion": "^2.0.2"
|
|
242
244
|
},
|
|
243
245
|
"engines": {
|
|
244
246
|
"node": ">=16 || 14 >=14.17"
|
|
@@ -2073,10 +2075,11 @@
|
|
|
2073
2075
|
}
|
|
2074
2076
|
},
|
|
2075
2077
|
"node_modules/minimatch": {
|
|
2076
|
-
"version": "3.1.
|
|
2077
|
-
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.
|
|
2078
|
-
"integrity": "sha512-
|
|
2078
|
+
"version": "3.1.5",
|
|
2079
|
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
|
2080
|
+
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
|
2079
2081
|
"dev": true,
|
|
2082
|
+
"license": "ISC",
|
|
2080
2083
|
"dependencies": {
|
|
2081
2084
|
"brace-expansion": "^1.1.7"
|
|
2082
2085
|
},
|