truescene-face-id-capture-sdk 1.0.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/README.md +77 -0
- package/dist/_redirects +3 -0
- package/dist/components/FaceAndIdCapture.js +628 -0
- package/dist/index.js +26005 -0
- package/dist/sdk/CaptureExperience.js +241 -0
- package/dist/sdk/element.js +221 -0
- package/dist/sdk/react/index.js +125 -0
- package/dist/sdk/styles.js +2 -0
- package/dist/sdk/types.js +1 -0
- package/dist/types/components/FaceAndIdCapture.d.ts +47 -0
- package/dist/types/sdk/CaptureExperience.d.ts +26 -0
- package/dist/types/sdk/element.d.ts +27 -0
- package/dist/types/sdk/index.d.ts +4 -0
- package/dist/types/sdk/react/index.d.ts +42 -0
- package/dist/types/sdk/styles.d.ts +1 -0
- package/dist/types/sdk/types.d.ts +13 -0
- package/dist/types/utils/config.d.ts +52 -0
- package/dist/types/utils/faceAnalysis.d.ts +26 -0
- package/dist/types/utils/faceChecks.d.ts +19 -0
- package/dist/types/utils/idPlacement.d.ts +13 -0
- package/dist/types/utils/overlayDraw.d.ts +15 -0
- package/dist/utils/config.js +44 -0
- package/dist/utils/faceAnalysis.js +144 -0
- package/dist/utils/faceChecks.js +84 -0
- package/dist/utils/idPlacement.js +66 -0
- package/dist/utils/overlayDraw.js +96 -0
- package/dist/verification-login-svgrepo-com.svg +45 -0
- package/dist/vite.svg +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import FaceAndIdCapture, {} from '../components/FaceAndIdCapture';
|
|
4
|
+
const stripDataUrl = (value) => {
|
|
5
|
+
if (!value)
|
|
6
|
+
return null;
|
|
7
|
+
const commaIndex = value.indexOf(',');
|
|
8
|
+
return commaIndex >= 0 ? value.slice(commaIndex + 1) : value;
|
|
9
|
+
};
|
|
10
|
+
const formatNumber = (value, digits) => {
|
|
11
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
12
|
+
return '--';
|
|
13
|
+
}
|
|
14
|
+
return value.toFixed(digits);
|
|
15
|
+
};
|
|
16
|
+
const CaptureExperience = ({ sessionToken, compareUrl = '/comparev2', tokenHeader = 'Authorization', tokenPrefix = 'Bearer', tokenField = 'session_token', autoStart = false, initialShowDebug = false, showDebugToggle = false, className, onReadyChange, onStepChange, onMetricsChange, onCapture, onCompareResult, onCompareError, onError, }) => {
|
|
17
|
+
const [ready, setReady] = useState({
|
|
18
|
+
faceReady: false,
|
|
19
|
+
idReady: false,
|
|
20
|
+
});
|
|
21
|
+
const [step, setStep] = useState('FACE_ALIGN');
|
|
22
|
+
const [metrics, setMetrics] = useState(null);
|
|
23
|
+
const [showDebug, setShowDebug] = useState(initialShowDebug);
|
|
24
|
+
const [started, setStarted] = useState(false);
|
|
25
|
+
const [captures, setCaptures] = useState({
|
|
26
|
+
faceImage: null,
|
|
27
|
+
idImage: null,
|
|
28
|
+
fullImage: null,
|
|
29
|
+
});
|
|
30
|
+
const [compareResult, setCompareResult] = useState(null);
|
|
31
|
+
const [compareError, setCompareError] = useState(null);
|
|
32
|
+
const [compareLoading, setCompareLoading] = useState(false);
|
|
33
|
+
const lastCompareRef = useRef(null);
|
|
34
|
+
const trimmedToken = sessionToken?.trim() ?? '';
|
|
35
|
+
const tokenMissing = trimmedToken.length === 0;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!autoStart || started || tokenMissing) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setStarted(true);
|
|
41
|
+
}, [autoStart, started, tokenMissing]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!captures.faceImage || !captures.idImage) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (tokenMissing) {
|
|
47
|
+
const message = 'Session token is required to compare results.';
|
|
48
|
+
setCompareError(message);
|
|
49
|
+
setCompareResult(null);
|
|
50
|
+
setCompareLoading(false);
|
|
51
|
+
onCompareError?.(message);
|
|
52
|
+
onError?.(message);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const last = lastCompareRef.current;
|
|
56
|
+
if (last &&
|
|
57
|
+
last.faceImage === captures.faceImage &&
|
|
58
|
+
last.idImage === captures.idImage &&
|
|
59
|
+
last.sessionToken === trimmedToken) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const facePayload = stripDataUrl(captures.faceImage);
|
|
63
|
+
const idPayload = stripDataUrl(captures.idImage);
|
|
64
|
+
if (!facePayload || !idPayload) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
lastCompareRef.current = {
|
|
68
|
+
faceImage: captures.faceImage,
|
|
69
|
+
idImage: captures.idImage,
|
|
70
|
+
sessionToken: trimmedToken,
|
|
71
|
+
};
|
|
72
|
+
let isActive = true;
|
|
73
|
+
setCompareLoading(true);
|
|
74
|
+
setCompareError(null);
|
|
75
|
+
setCompareResult(null);
|
|
76
|
+
onCompareResult?.(null);
|
|
77
|
+
const headers = {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
};
|
|
80
|
+
if (tokenHeader) {
|
|
81
|
+
headers[tokenHeader] = tokenPrefix
|
|
82
|
+
? `${tokenPrefix} ${trimmedToken}`
|
|
83
|
+
: trimmedToken;
|
|
84
|
+
}
|
|
85
|
+
const payload = {
|
|
86
|
+
image_face: facePayload,
|
|
87
|
+
image_idcard: idPayload,
|
|
88
|
+
};
|
|
89
|
+
if (tokenField) {
|
|
90
|
+
payload[tokenField] = trimmedToken;
|
|
91
|
+
}
|
|
92
|
+
fetch(compareUrl, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers,
|
|
95
|
+
body: JSON.stringify(payload),
|
|
96
|
+
})
|
|
97
|
+
.then(async (res) => {
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
throw new Error(`Server error (${res.status})`);
|
|
100
|
+
}
|
|
101
|
+
return res.json();
|
|
102
|
+
})
|
|
103
|
+
.then((data) => {
|
|
104
|
+
if (!isActive) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (typeof data?.error === 'string' && data.error.trim().length > 0) {
|
|
108
|
+
setCompareError(data.error);
|
|
109
|
+
setCompareResult(null);
|
|
110
|
+
onCompareError?.(data.error);
|
|
111
|
+
onError?.(data.error);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setCompareResult(data);
|
|
115
|
+
onCompareResult?.(data);
|
|
116
|
+
})
|
|
117
|
+
.catch((error) => {
|
|
118
|
+
if (!isActive) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const message = error instanceof Error ? error.message : 'Compare failed';
|
|
122
|
+
setCompareError(message);
|
|
123
|
+
onCompareError?.(message);
|
|
124
|
+
onError?.(message);
|
|
125
|
+
})
|
|
126
|
+
.finally(() => {
|
|
127
|
+
if (!isActive) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
setCompareLoading(false);
|
|
131
|
+
});
|
|
132
|
+
return () => {
|
|
133
|
+
isActive = false;
|
|
134
|
+
};
|
|
135
|
+
}, [
|
|
136
|
+
captures.faceImage,
|
|
137
|
+
captures.idImage,
|
|
138
|
+
compareUrl,
|
|
139
|
+
tokenField,
|
|
140
|
+
tokenHeader,
|
|
141
|
+
tokenMissing,
|
|
142
|
+
tokenPrefix,
|
|
143
|
+
trimmedToken,
|
|
144
|
+
onCompareError,
|
|
145
|
+
onCompareResult,
|
|
146
|
+
onError,
|
|
147
|
+
]);
|
|
148
|
+
const statusLabel = ready.idReady ? 'Ready to continue' : 'Not ready';
|
|
149
|
+
const statusClass = ready.idReady
|
|
150
|
+
? 'status-pill status-pill--ready'
|
|
151
|
+
: 'status-pill status-pill--not';
|
|
152
|
+
const stepLabel = step === 'FACE_ALIGN' ? 'Step 1 of 2: Face align' : 'Step 2 of 2: ID align';
|
|
153
|
+
const metricsRows = useMemo(() => {
|
|
154
|
+
if (!metrics) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [
|
|
158
|
+
{ label: 'Step', value: metrics.step },
|
|
159
|
+
{ label: 'Face ready', value: metrics.ready.faceReady ? 'Yes' : 'No' },
|
|
160
|
+
{ label: 'ID ready', value: metrics.ready.idReady ? 'Yes' : 'No' },
|
|
161
|
+
{ label: 'Face count', value: `${metrics.face.faceCount}` },
|
|
162
|
+
{
|
|
163
|
+
label: 'Face box',
|
|
164
|
+
value: metrics.face.faceBoxNorm
|
|
165
|
+
? `${metrics.face.faceBoxNorm.x.toFixed(2)}, ${metrics.face.faceBoxNorm.y.toFixed(2)}, ${metrics.face.faceBoxNorm.w.toFixed(2)}, ${metrics.face.faceBoxNorm.h.toFixed(2)}`
|
|
166
|
+
: '--',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
label: 'Face yaw',
|
|
170
|
+
value: metrics.face.poseValid
|
|
171
|
+
? `${metrics.face.yawDeg.toFixed(1)} deg`
|
|
172
|
+
: '--',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: 'Face pitch',
|
|
176
|
+
value: metrics.face.poseValid
|
|
177
|
+
? `${metrics.face.pitchDeg.toFixed(1)} deg`
|
|
178
|
+
: '--',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
label: 'Face roll',
|
|
182
|
+
value: metrics.face.poseValid
|
|
183
|
+
? `${metrics.face.rollDeg.toFixed(1)} deg`
|
|
184
|
+
: '--',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: 'ID rect',
|
|
188
|
+
value: metrics.id.rectNorm
|
|
189
|
+
? `${metrics.id.rectNorm.x.toFixed(2)}, ${metrics.id.rectNorm.y.toFixed(2)}, ${metrics.id.rectNorm.w.toFixed(2)}, ${metrics.id.rectNorm.h.toFixed(2)}`
|
|
190
|
+
: '--',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
label: 'ID ROI face count',
|
|
194
|
+
value: `${metrics.id.roiFaceCount}`,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: 'ID ROI face box',
|
|
198
|
+
value: metrics.id.roiLargestFaceBoxNorm
|
|
199
|
+
? `${metrics.id.roiLargestFaceBoxNorm.x.toFixed(2)}, ${metrics.id.roiLargestFaceBoxNorm.y.toFixed(2)}, ${metrics.id.roiLargestFaceBoxNorm.w.toFixed(2)}, ${metrics.id.roiLargestFaceBoxNorm.h.toFixed(2)}`
|
|
200
|
+
: '--',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: 'ID ROI face size',
|
|
204
|
+
value: metrics.id.roiLargestFaceSizeRatio.toFixed(3),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
label: 'ID ROI mean lum',
|
|
208
|
+
value: metrics.id.meanLumROI.toFixed(0),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
label: 'ID ROI blur',
|
|
212
|
+
value: metrics.id.blurScoreROI.toFixed(0),
|
|
213
|
+
},
|
|
214
|
+
{ label: 'Hint', value: metrics.hint },
|
|
215
|
+
];
|
|
216
|
+
}, [metrics]);
|
|
217
|
+
const handleReadyChange = (next) => {
|
|
218
|
+
setReady(next);
|
|
219
|
+
onReadyChange?.(next);
|
|
220
|
+
};
|
|
221
|
+
const handleStepChange = (next) => {
|
|
222
|
+
setStep(next);
|
|
223
|
+
onStepChange?.(next);
|
|
224
|
+
};
|
|
225
|
+
const handleMetricsChange = (next) => {
|
|
226
|
+
setMetrics(next);
|
|
227
|
+
onMetricsChange?.(next);
|
|
228
|
+
};
|
|
229
|
+
const handleCapture = (images) => {
|
|
230
|
+
setCaptures(images);
|
|
231
|
+
onCapture?.(images);
|
|
232
|
+
};
|
|
233
|
+
return (_jsxs("div", { className: `ts-capture${className ? ` ${className}` : ''}`, children: [_jsxs("header", { className: "app__header", children: [_jsxs("div", { children: [_jsx("div", { className: "app__eyebrow", children: "TrueScene Capture" }), _jsxs("div", { children: [_jsx("h4", { className: "app__title", children: "Fast & Deepfake Resistant" }), _jsx("h4", { className: "app__title", children: "ID Verification" })] }), _jsx("p", { className: "app__subtitle", children: "Press Start Verification. Align your face in the oval, then place your ID under your nose so it covers your mouth. Keep your eyes visible." })] }), started && (_jsxs("div", { className: "app__status", children: [_jsxs("div", { className: "app__status-stack", children: [_jsx("span", { className: "status-pill status-pill--step", children: stepLabel }), _jsxs("span", { className: ready.faceReady
|
|
234
|
+
? 'status-pill status-pill--ready'
|
|
235
|
+
: 'status-pill status-pill--pending', children: ["Face ", ready.faceReady ? 'ready' : 'not ready'] }), _jsxs("span", { className: ready.idReady
|
|
236
|
+
? 'status-pill status-pill--ready'
|
|
237
|
+
: 'status-pill status-pill--pending', children: ["ID ", ready.idReady ? 'ready' : 'not ready'] })] }), _jsx("span", { className: statusClass, children: statusLabel })] }))] }), _jsxs("main", { className: "app__grid", children: [_jsxs("section", { className: "app__camera", children: [!started ? (_jsxs("div", { className: "app__start", children: [_jsx("button", { className: "btn btn--primary", type: "button", disabled: tokenMissing, onClick: () => setStarted(true), children: "Start Verification" }), tokenMissing && (_jsx("p", { className: "app__token-hint", children: "Provide a session token before starting." }))] })) : captures.idImage ? (_jsxs("div", { className: "app__captures", children: [_jsxs("div", { className: "capture-card", children: [_jsx("span", { children: "Face capture" }), _jsx("img", { src: captures.faceImage ?? '', alt: "Face capture" })] }), _jsxs("div", { className: "capture-card", children: [_jsx("span", { children: "ID capture (cropped)" }), _jsx("img", { src: captures.idImage, alt: "ID capture" })] }), _jsxs("div", { className: "capture-card", children: [_jsx("span", { children: "Full frame" }), _jsx("img", { src: captures.fullImage ?? '', alt: "Full frame capture" })] })] })) : (_jsx(FaceAndIdCapture, { onReadyChange: handleReadyChange, onMetricsChange: handleMetricsChange, onStepChange: handleStepChange, onCapture: handleCapture })), started ? null : _jsx("br", {}), started && showDebugToggle && (_jsx("div", { className: "app__actions", children: _jsx("button", { className: "btn btn--ghost", type: "button", onClick: () => setShowDebug((prev) => !prev), children: showDebug ? 'Hide Capture debug' : 'Show Capture Debug' }) }))] }), _jsxs("aside", { className: "app__panel", children: [captures.idImage && (_jsxs("div", { className: "panel-card panel-card--accent", children: [_jsx("h2", { children: "Match result" }), compareLoading && (_jsxs("div", { className: "metrics-row", children: [_jsx("span", { children: "Comparing face to ID..." }), _jsx("span", { className: "spinner", "aria-hidden": "true" })] })), compareError && _jsx("p", { children: compareError }), compareResult && (_jsxs("div", { className: "metrics-grid", children: [_jsxs("div", { className: "metrics-row", children: [_jsx("span", { children: "Match" }), _jsx("span", { className: compareResult.match
|
|
238
|
+
? 'match-result match-result--yes'
|
|
239
|
+
: 'match-result match-result--no', children: compareResult.match ? 'YES' : 'NO' })] }), _jsxs("details", { className: "metrics-details", children: [_jsxs("summary", { className: "metrics-summary", children: [_jsxs("span", { className: "metrics-summary__top", children: [_jsx("span", { children: "Match %" }), _jsxs("span", { className: "metrics-summary__value", children: [formatNumber(compareResult.match_percentage, 2), "%"] })] }), _jsx("span", { className: "metrics-summary__arrow", "aria-hidden": "true" })] }), _jsxs("div", { className: "metrics-row", children: [_jsx("span", { children: "Cosine distance" }), _jsx("span", { children: formatNumber(compareResult.cosine_distance, 4) })] }), _jsxs("div", { className: "metrics-row", children: [_jsx("span", { children: "Cosine similarity" }), _jsx("span", { children: formatNumber(compareResult.cosine_similarity, 4) })] })] })] }))] })), captures.idImage && _jsx("br", {}), showDebug && metrics && (_jsxs("div", { className: "panel-card panel-card--metrics", children: [_jsx("h2", { children: "Debug metrics" }), _jsx("div", { className: "metrics-grid", children: metricsRows.map((row) => (_jsxs("div", { className: "metrics-row", children: [_jsx("span", { children: row.label }), _jsx("span", { children: row.value })] }, row.label))) })] })), showDebug && metrics && (_jsx("div", { children: _jsx("br", {}) })), !captures.idImage && (_jsxs("div", { className: "panel-card", children: [_jsx("h2", { children: "Quick checklist" }), _jsxs("ul", { children: [_jsx("li", { children: "Fill the oval, but avoid getting too close." }), _jsx("li", { children: "Place the ID under your nose and keep eyes visible." }), _jsx("li", { children: "Face a soft, even light source and avoid glare." }), _jsx("li", { children: "Hold steady until the status turns ready." })] })] })), _jsx("br", {}), _jsxs("div", { className: "app__cta-row", children: [_jsx("p", { children: "Already tried verification? Let's start integrating it into your app." }), _jsx("a", { className: "btn btn--ghost", href: "https://truescene.site/", target: "_blank", rel: "noreferrer", children: "Start Building" })] })] })] })] }));
|
|
240
|
+
};
|
|
241
|
+
export default CaptureExperience;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import CaptureExperience, {} from './CaptureExperience';
|
|
4
|
+
import { captureStyles } from './styles';
|
|
5
|
+
const parseBoolean = (value) => {
|
|
6
|
+
if (value === null)
|
|
7
|
+
return false;
|
|
8
|
+
if (value === '')
|
|
9
|
+
return true;
|
|
10
|
+
const normalized = value.toLowerCase();
|
|
11
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
};
|
|
19
|
+
export class TrueSceneCaptureElement extends HTMLElement {
|
|
20
|
+
static observedAttributes = [
|
|
21
|
+
'session-token',
|
|
22
|
+
'compare-url',
|
|
23
|
+
'token-header',
|
|
24
|
+
'token-prefix',
|
|
25
|
+
'token-field',
|
|
26
|
+
'auto-start',
|
|
27
|
+
'show-debug',
|
|
28
|
+
'debug-toggle',
|
|
29
|
+
];
|
|
30
|
+
root = null;
|
|
31
|
+
mount = null;
|
|
32
|
+
styleEl = null;
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
console.count('truescene-capture connected');
|
|
35
|
+
if (!this.shadowRoot) {
|
|
36
|
+
this.attachShadow({ mode: 'open' });
|
|
37
|
+
}
|
|
38
|
+
const shadow = this.shadowRoot;
|
|
39
|
+
if (!shadow) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!this.styleEl) {
|
|
43
|
+
const style = document.createElement('style');
|
|
44
|
+
style.textContent = captureStyles;
|
|
45
|
+
shadow.appendChild(style);
|
|
46
|
+
this.styleEl = style;
|
|
47
|
+
}
|
|
48
|
+
if (!this.mount) {
|
|
49
|
+
this.mount = document.createElement('div');
|
|
50
|
+
shadow.appendChild(this.mount);
|
|
51
|
+
}
|
|
52
|
+
if (!this.root) {
|
|
53
|
+
this.root = createRoot(this.mount);
|
|
54
|
+
}
|
|
55
|
+
this.renderReact();
|
|
56
|
+
}
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
console.count('truescene-capture disconnected');
|
|
59
|
+
this.root?.unmount();
|
|
60
|
+
this.root = null;
|
|
61
|
+
}
|
|
62
|
+
attributeChangedCallback() {
|
|
63
|
+
this.renderReact();
|
|
64
|
+
}
|
|
65
|
+
set sessionToken(value) {
|
|
66
|
+
if (value === null || value === undefined) {
|
|
67
|
+
this.removeAttribute('session-token');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.setAttribute('session-token', value);
|
|
71
|
+
}
|
|
72
|
+
get sessionToken() {
|
|
73
|
+
return this.getAttribute('session-token') ?? '';
|
|
74
|
+
}
|
|
75
|
+
set compareUrl(value) {
|
|
76
|
+
if (!value) {
|
|
77
|
+
this.removeAttribute('compare-url');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.setAttribute('compare-url', value);
|
|
81
|
+
}
|
|
82
|
+
get compareUrl() {
|
|
83
|
+
return this.getAttribute('compare-url') ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
set tokenHeader(value) {
|
|
86
|
+
if (!value) {
|
|
87
|
+
this.removeAttribute('token-header');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.setAttribute('token-header', value);
|
|
91
|
+
}
|
|
92
|
+
get tokenHeader() {
|
|
93
|
+
return this.getAttribute('token-header') ?? undefined;
|
|
94
|
+
}
|
|
95
|
+
set tokenPrefix(value) {
|
|
96
|
+
if (!value) {
|
|
97
|
+
this.removeAttribute('token-prefix');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.setAttribute('token-prefix', value);
|
|
101
|
+
}
|
|
102
|
+
get tokenPrefix() {
|
|
103
|
+
return this.getAttribute('token-prefix') ?? undefined;
|
|
104
|
+
}
|
|
105
|
+
set tokenField(value) {
|
|
106
|
+
if (!value) {
|
|
107
|
+
this.removeAttribute('token-field');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.setAttribute('token-field', value);
|
|
111
|
+
}
|
|
112
|
+
get tokenField() {
|
|
113
|
+
return this.getAttribute('token-field') ?? undefined;
|
|
114
|
+
}
|
|
115
|
+
set autoStart(value) {
|
|
116
|
+
if (value) {
|
|
117
|
+
this.setAttribute('auto-start', '');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.removeAttribute('auto-start');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
get autoStart() {
|
|
124
|
+
return parseBoolean(this.getAttribute('auto-start'));
|
|
125
|
+
}
|
|
126
|
+
set initialShowDebug(value) {
|
|
127
|
+
if (value) {
|
|
128
|
+
this.setAttribute('show-debug', '');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.removeAttribute('show-debug');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
get initialShowDebug() {
|
|
135
|
+
return parseBoolean(this.getAttribute('show-debug'));
|
|
136
|
+
}
|
|
137
|
+
set showDebugToggle(value) {
|
|
138
|
+
if (value) {
|
|
139
|
+
this.setAttribute('debug-toggle', '');
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.removeAttribute('debug-toggle');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
get showDebugToggle() {
|
|
146
|
+
return parseBoolean(this.getAttribute('debug-toggle'));
|
|
147
|
+
}
|
|
148
|
+
renderReact() {
|
|
149
|
+
if (!this.root) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const props = {
|
|
153
|
+
sessionToken: this.sessionToken,
|
|
154
|
+
compareUrl: this.compareUrl,
|
|
155
|
+
tokenHeader: this.tokenHeader,
|
|
156
|
+
tokenPrefix: this.tokenPrefix,
|
|
157
|
+
tokenField: this.tokenField,
|
|
158
|
+
autoStart: this.autoStart,
|
|
159
|
+
initialShowDebug: this.initialShowDebug,
|
|
160
|
+
showDebugToggle: this.showDebugToggle,
|
|
161
|
+
onReadyChange: (ready) => {
|
|
162
|
+
this.dispatchEvent(new CustomEvent('ready-change', {
|
|
163
|
+
detail: ready,
|
|
164
|
+
bubbles: true,
|
|
165
|
+
composed: true,
|
|
166
|
+
}));
|
|
167
|
+
},
|
|
168
|
+
onStepChange: (step) => {
|
|
169
|
+
this.dispatchEvent(new CustomEvent('step-change', {
|
|
170
|
+
detail: step,
|
|
171
|
+
bubbles: true,
|
|
172
|
+
composed: true,
|
|
173
|
+
}));
|
|
174
|
+
},
|
|
175
|
+
onMetricsChange: (metrics) => {
|
|
176
|
+
this.dispatchEvent(new CustomEvent('metrics-change', {
|
|
177
|
+
detail: metrics,
|
|
178
|
+
bubbles: true,
|
|
179
|
+
composed: true,
|
|
180
|
+
}));
|
|
181
|
+
},
|
|
182
|
+
onCapture: (images) => {
|
|
183
|
+
this.dispatchEvent(new CustomEvent('capture', {
|
|
184
|
+
detail: images,
|
|
185
|
+
bubbles: true,
|
|
186
|
+
composed: true,
|
|
187
|
+
}));
|
|
188
|
+
},
|
|
189
|
+
onCompareResult: (result) => {
|
|
190
|
+
this.dispatchEvent(new CustomEvent('compare-result', {
|
|
191
|
+
detail: result,
|
|
192
|
+
bubbles: true,
|
|
193
|
+
composed: true,
|
|
194
|
+
}));
|
|
195
|
+
},
|
|
196
|
+
onCompareError: (message) => {
|
|
197
|
+
this.dispatchEvent(new CustomEvent('compare-error', {
|
|
198
|
+
detail: message,
|
|
199
|
+
bubbles: true,
|
|
200
|
+
composed: true,
|
|
201
|
+
}));
|
|
202
|
+
},
|
|
203
|
+
onError: (message) => {
|
|
204
|
+
this.dispatchEvent(new CustomEvent('error', {
|
|
205
|
+
detail: message,
|
|
206
|
+
bubbles: true,
|
|
207
|
+
composed: true,
|
|
208
|
+
}));
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
this.root.render(_jsx(CaptureExperience, { ...props }));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export const defineTrueSceneCapture = (tagName = 'truescene-capture') => {
|
|
215
|
+
if (typeof window === 'undefined') {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!window.customElements?.get(tagName)) {
|
|
219
|
+
window.customElements.define(tagName, TrueSceneCaptureElement);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createElement, useEffect, useRef } from 'react';
|
|
2
|
+
const TrueSceneCapture = ({ sessionToken, compareUrl, tokenHeader, tokenPrefix, tokenField, autoStart, initialShowDebug, showDebugToggle, className, style, onReadyChange, onStepChange, onMetricsChange, onCapture, onCompareResult, onCompareError, onError, }) => {
|
|
3
|
+
const elementRef = useRef(null);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (typeof window === 'undefined') {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (window.customElements?.get('truescene-capture')) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const packageName = 'truescene-face-capture';
|
|
12
|
+
void import(packageName);
|
|
13
|
+
}, []);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const element = elementRef.current;
|
|
16
|
+
if (!element)
|
|
17
|
+
return;
|
|
18
|
+
element.sessionToken = sessionToken ?? '';
|
|
19
|
+
}, [sessionToken]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const element = elementRef.current;
|
|
22
|
+
if (!element)
|
|
23
|
+
return;
|
|
24
|
+
element.compareUrl = compareUrl ?? '';
|
|
25
|
+
}, [compareUrl]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const element = elementRef.current;
|
|
28
|
+
if (!element)
|
|
29
|
+
return;
|
|
30
|
+
element.tokenHeader = tokenHeader ?? '';
|
|
31
|
+
}, [tokenHeader]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const element = elementRef.current;
|
|
34
|
+
if (!element)
|
|
35
|
+
return;
|
|
36
|
+
element.tokenPrefix = tokenPrefix ?? '';
|
|
37
|
+
}, [tokenPrefix]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const element = elementRef.current;
|
|
40
|
+
if (!element)
|
|
41
|
+
return;
|
|
42
|
+
element.tokenField = tokenField ?? '';
|
|
43
|
+
}, [tokenField]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const element = elementRef.current;
|
|
46
|
+
if (!element)
|
|
47
|
+
return;
|
|
48
|
+
element.autoStart = Boolean(autoStart);
|
|
49
|
+
}, [autoStart]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const element = elementRef.current;
|
|
52
|
+
if (!element)
|
|
53
|
+
return;
|
|
54
|
+
element.initialShowDebug = Boolean(initialShowDebug);
|
|
55
|
+
}, [initialShowDebug]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const element = elementRef.current;
|
|
58
|
+
if (!element)
|
|
59
|
+
return;
|
|
60
|
+
element.showDebugToggle = Boolean(showDebugToggle);
|
|
61
|
+
}, [showDebugToggle]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const element = elementRef.current;
|
|
64
|
+
if (!element)
|
|
65
|
+
return;
|
|
66
|
+
const readyHandler = (event) => {
|
|
67
|
+
const detail = event.detail;
|
|
68
|
+
onReadyChange?.(detail);
|
|
69
|
+
};
|
|
70
|
+
const stepHandler = (event) => {
|
|
71
|
+
const detail = event.detail;
|
|
72
|
+
onStepChange?.(detail);
|
|
73
|
+
};
|
|
74
|
+
const metricsHandler = (event) => {
|
|
75
|
+
const detail = event.detail;
|
|
76
|
+
onMetricsChange?.(detail);
|
|
77
|
+
};
|
|
78
|
+
const captureHandler = (event) => {
|
|
79
|
+
const detail = event.detail;
|
|
80
|
+
onCapture?.(detail);
|
|
81
|
+
};
|
|
82
|
+
const compareResultHandler = (event) => {
|
|
83
|
+
const detail = event.detail;
|
|
84
|
+
onCompareResult?.(detail);
|
|
85
|
+
};
|
|
86
|
+
const compareErrorHandler = (event) => {
|
|
87
|
+
const detail = event.detail;
|
|
88
|
+
onCompareError?.(detail);
|
|
89
|
+
};
|
|
90
|
+
const errorHandler = (event) => {
|
|
91
|
+
const detail = event.detail;
|
|
92
|
+
onError?.(detail);
|
|
93
|
+
};
|
|
94
|
+
element.addEventListener('ready-change', readyHandler);
|
|
95
|
+
element.addEventListener('step-change', stepHandler);
|
|
96
|
+
element.addEventListener('metrics-change', metricsHandler);
|
|
97
|
+
element.addEventListener('capture', captureHandler);
|
|
98
|
+
element.addEventListener('compare-result', compareResultHandler);
|
|
99
|
+
element.addEventListener('compare-error', compareErrorHandler);
|
|
100
|
+
element.addEventListener('error', errorHandler);
|
|
101
|
+
return () => {
|
|
102
|
+
element.removeEventListener('ready-change', readyHandler);
|
|
103
|
+
element.removeEventListener('step-change', stepHandler);
|
|
104
|
+
element.removeEventListener('metrics-change', metricsHandler);
|
|
105
|
+
element.removeEventListener('capture', captureHandler);
|
|
106
|
+
element.removeEventListener('compare-result', compareResultHandler);
|
|
107
|
+
element.removeEventListener('compare-error', compareErrorHandler);
|
|
108
|
+
element.removeEventListener('error', errorHandler);
|
|
109
|
+
};
|
|
110
|
+
}, [
|
|
111
|
+
onReadyChange,
|
|
112
|
+
onStepChange,
|
|
113
|
+
onMetricsChange,
|
|
114
|
+
onCapture,
|
|
115
|
+
onCompareResult,
|
|
116
|
+
onCompareError,
|
|
117
|
+
onError,
|
|
118
|
+
]);
|
|
119
|
+
return createElement('truescene-capture', {
|
|
120
|
+
ref: elementRef,
|
|
121
|
+
className,
|
|
122
|
+
style,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
export default TrueSceneCapture;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type NormalizedBox } from '../utils/faceAnalysis';
|
|
2
|
+
import { type NormalizedRect } from '../utils/idPlacement';
|
|
3
|
+
export type CaptureStep = 'FACE_ALIGN' | 'ID_ALIGN';
|
|
4
|
+
export type FaceMetrics = {
|
|
5
|
+
faceCount: number;
|
|
6
|
+
faceBoxNorm: NormalizedBox | null;
|
|
7
|
+
yawDeg: number;
|
|
8
|
+
pitchDeg: number;
|
|
9
|
+
rollDeg: number;
|
|
10
|
+
poseValid: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type IdMetrics = {
|
|
13
|
+
rectNorm: NormalizedRect | null;
|
|
14
|
+
roiFaceCount: number;
|
|
15
|
+
roiLargestFaceBoxNorm: NormalizedBox | null;
|
|
16
|
+
roiLargestFaceSizeRatio: number;
|
|
17
|
+
meanLumROI: number;
|
|
18
|
+
blurScoreROI: number;
|
|
19
|
+
};
|
|
20
|
+
export type FaceAndIdCaptureMetrics = {
|
|
21
|
+
step: CaptureStep;
|
|
22
|
+
face: FaceMetrics;
|
|
23
|
+
id: IdMetrics;
|
|
24
|
+
hint: string;
|
|
25
|
+
ready: {
|
|
26
|
+
faceReady: boolean;
|
|
27
|
+
idReady: boolean;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
type FaceAndIdCaptureProps = {
|
|
31
|
+
onReadyChange: (ready: {
|
|
32
|
+
faceReady: boolean;
|
|
33
|
+
idReady: boolean;
|
|
34
|
+
}) => void;
|
|
35
|
+
onStepChange?: (step: CaptureStep) => void;
|
|
36
|
+
onMetricsChange?: (metrics: FaceAndIdCaptureMetrics) => void;
|
|
37
|
+
onCapture?: (images: {
|
|
38
|
+
faceImage: string | null;
|
|
39
|
+
idImage: string | null;
|
|
40
|
+
fullImage: string | null;
|
|
41
|
+
}) => void;
|
|
42
|
+
width?: number;
|
|
43
|
+
height?: number;
|
|
44
|
+
showBackButton?: boolean;
|
|
45
|
+
};
|
|
46
|
+
declare const FaceAndIdCapture: ({ onReadyChange, onStepChange, onMetricsChange, onCapture, width, height, showBackButton, }: FaceAndIdCaptureProps) => import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
export default FaceAndIdCapture;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CaptureStep, type FaceAndIdCaptureMetrics } from '../components/FaceAndIdCapture';
|
|
2
|
+
import type { CaptureImages, CompareResult } from './types';
|
|
3
|
+
type ReadyState = {
|
|
4
|
+
faceReady: boolean;
|
|
5
|
+
idReady: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type CaptureExperienceProps = {
|
|
8
|
+
sessionToken: string;
|
|
9
|
+
compareUrl?: string;
|
|
10
|
+
tokenHeader?: string;
|
|
11
|
+
tokenPrefix?: string;
|
|
12
|
+
tokenField?: string | null;
|
|
13
|
+
autoStart?: boolean;
|
|
14
|
+
initialShowDebug?: boolean;
|
|
15
|
+
showDebugToggle?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
onReadyChange?: (ready: ReadyState) => void;
|
|
18
|
+
onStepChange?: (step: CaptureStep) => void;
|
|
19
|
+
onMetricsChange?: (metrics: FaceAndIdCaptureMetrics) => void;
|
|
20
|
+
onCapture?: (images: CaptureImages) => void;
|
|
21
|
+
onCompareResult?: (result: CompareResult | null) => void;
|
|
22
|
+
onCompareError?: (message: string | null) => void;
|
|
23
|
+
onError?: (message: string) => void;
|
|
24
|
+
};
|
|
25
|
+
declare const CaptureExperience: ({ sessionToken, compareUrl, tokenHeader, tokenPrefix, tokenField, autoStart, initialShowDebug, showDebugToggle, className, onReadyChange, onStepChange, onMetricsChange, onCapture, onCompareResult, onCompareError, onError, }: CaptureExperienceProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export default CaptureExperience;
|