@tn3w/openage 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/LICENSE +201 -0
- package/README.md +151 -0
- package/dist/openage.esm.js +3383 -0
- package/dist/openage.esm.js.map +1 -0
- package/dist/openage.min.js +2 -0
- package/dist/openage.min.js.map +1 -0
- package/dist/openage.umd.js +3408 -0
- package/dist/openage.umd.js.map +1 -0
- package/package.json +56 -0
- package/src/age-estimator.d.ts +17 -0
- package/src/age-estimator.js +77 -0
- package/src/challenge.d.ts +7 -0
- package/src/challenge.js +493 -0
- package/src/constants.d.ts +24 -0
- package/src/constants.js +35 -0
- package/src/face-tracker.d.ts +39 -0
- package/src/face-tracker.js +132 -0
- package/src/index.d.ts +91 -0
- package/src/index.js +303 -0
- package/src/liveness.d.ts +62 -0
- package/src/liveness.js +220 -0
- package/src/positioning.d.ts +13 -0
- package/src/positioning.js +39 -0
- package/src/transport.d.ts +61 -0
- package/src/transport.js +305 -0
- package/src/ui.d.ts +37 -0
- package/src/ui.js +1048 -0
- package/src/vm-client.d.ts +62 -0
- package/src/vm-client.js +219 -0
- package/src/widget.d.ts +58 -0
- package/src/widget.js +617 -0
package/src/challenge.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { loadVision, loadModel, initTracker, track, destroyTracker } from './face-tracker.js';
|
|
2
|
+
import { startPositioning } from './positioning.js';
|
|
3
|
+
import { initAgeEstimator, estimateAgeBurst } from './age-estimator.js';
|
|
4
|
+
import {
|
|
5
|
+
createSession as createLivenessSession,
|
|
6
|
+
processFrame,
|
|
7
|
+
isComplete,
|
|
8
|
+
isPassed,
|
|
9
|
+
currentInstruction,
|
|
10
|
+
currentTaskId,
|
|
11
|
+
progress,
|
|
12
|
+
} from './liveness.js';
|
|
13
|
+
import { createTransport } from './transport.js';
|
|
14
|
+
import {
|
|
15
|
+
initVM,
|
|
16
|
+
setFaceData,
|
|
17
|
+
setChallengeParams,
|
|
18
|
+
executeChallenge as execVM,
|
|
19
|
+
registerBridge,
|
|
20
|
+
unregisterBridge,
|
|
21
|
+
destroyVM,
|
|
22
|
+
toBase64,
|
|
23
|
+
ensureModels,
|
|
24
|
+
getMediaPipeModelBuffer,
|
|
25
|
+
clearModelCache,
|
|
26
|
+
} from './vm-client.js';
|
|
27
|
+
import {
|
|
28
|
+
BURST_FRAMES,
|
|
29
|
+
BURST_INTERVAL_MS,
|
|
30
|
+
MAX_RETRIES,
|
|
31
|
+
MOTION_CAPTURE_MS,
|
|
32
|
+
MOTION_SAMPLE_MS,
|
|
33
|
+
} from './constants.js';
|
|
34
|
+
|
|
35
|
+
let stream = null;
|
|
36
|
+
let videoElement = null;
|
|
37
|
+
|
|
38
|
+
const CAMERA_CONSTRAINTS = {
|
|
39
|
+
video: {
|
|
40
|
+
facingMode: 'user',
|
|
41
|
+
width: { ideal: 640 },
|
|
42
|
+
height: { ideal: 480 },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function startCamera(video) {
|
|
47
|
+
return navigator.mediaDevices.getUserMedia(CAMERA_CONSTRAINTS).then((s) => {
|
|
48
|
+
stream = s;
|
|
49
|
+
video.srcObject = stream;
|
|
50
|
+
videoElement = video;
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
video.onloadedmetadata = () => {
|
|
54
|
+
video.play();
|
|
55
|
+
resolve({
|
|
56
|
+
width: video.videoWidth,
|
|
57
|
+
height: video.videoHeight,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function captureFrame() {
|
|
65
|
+
if (!videoElement) return null;
|
|
66
|
+
|
|
67
|
+
const canvas = document.createElement('canvas');
|
|
68
|
+
canvas.width = videoElement.videoWidth;
|
|
69
|
+
canvas.height = videoElement.videoHeight;
|
|
70
|
+
|
|
71
|
+
const context = canvas.getContext('2d');
|
|
72
|
+
context.drawImage(videoElement, 0, 0);
|
|
73
|
+
return canvas;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function stopCamera() {
|
|
77
|
+
if (stream) {
|
|
78
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
79
|
+
stream = null;
|
|
80
|
+
}
|
|
81
|
+
if (videoElement) {
|
|
82
|
+
videoElement.srcObject = null;
|
|
83
|
+
videoElement = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isCameraActive() {
|
|
88
|
+
return stream !== null && videoElement !== null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function computeAge(ageResults) {
|
|
92
|
+
if (!ageResults || ageResults.length === 0) return null;
|
|
93
|
+
|
|
94
|
+
const ages = ageResults.map((r) => r.age).sort((a, b) => a - b);
|
|
95
|
+
|
|
96
|
+
const trimmed = ages.length >= 3 ? ages.slice(1, -1) : ages;
|
|
97
|
+
|
|
98
|
+
return trimmed.reduce((s, a) => s + a, 0) / trimmed.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const TASK_LABELS = {
|
|
102
|
+
'turn-left': 'Turn your head left',
|
|
103
|
+
'turn-right': 'Turn your head right',
|
|
104
|
+
nod: 'Nod your head',
|
|
105
|
+
'blink-twice': 'Blink twice',
|
|
106
|
+
'move-closer': 'Move closer then back',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function sleep(ms) {
|
|
110
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function runChallenge(widget, emitter) {
|
|
114
|
+
const mode = widget.params.mode || 'serverless';
|
|
115
|
+
|
|
116
|
+
if (mode !== 'serverless' && !widget.params.sitekey) {
|
|
117
|
+
const err = new Error('Configuration error');
|
|
118
|
+
widget.showResult?.('fail', 'Verification failed');
|
|
119
|
+
emitter.emit('error', err, widget.id);
|
|
120
|
+
widget.params.errorCallback?.(err);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (mode === 'serverless') {
|
|
125
|
+
return runServerless(widget, emitter);
|
|
126
|
+
}
|
|
127
|
+
return runServer(widget, emitter);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runServerless(widget, emitter) {
|
|
131
|
+
const params = widget.params;
|
|
132
|
+
let retryCount = 0;
|
|
133
|
+
let modelBuffer = null;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
widget.openPopup();
|
|
137
|
+
widget.setHeroStatus('Loading…');
|
|
138
|
+
|
|
139
|
+
await Promise.all([loadVision(), initAgeEstimator()]);
|
|
140
|
+
|
|
141
|
+
modelBuffer = await loadModel();
|
|
142
|
+
widget.showReady();
|
|
143
|
+
|
|
144
|
+
await waitForStart(widget);
|
|
145
|
+
await startCameraFlow(widget, modelBuffer);
|
|
146
|
+
|
|
147
|
+
const transport = createTransport('serverless', params);
|
|
148
|
+
|
|
149
|
+
const attempt = async () => {
|
|
150
|
+
widget.showLiveness();
|
|
151
|
+
widget.setInstruction('');
|
|
152
|
+
widget.setVideoStatus('Verifying…');
|
|
153
|
+
|
|
154
|
+
const session = createLivenessSession();
|
|
155
|
+
await runLivenessLoop(widget.popupElements.video, session, widget);
|
|
156
|
+
|
|
157
|
+
if (session.failed || !isPassed(session)) {
|
|
158
|
+
return { outcome: 'retry' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
widget.setInstruction('Hold still…');
|
|
162
|
+
widget.setVideoStatus('Processing…');
|
|
163
|
+
const frames = await captureFrameBurst(BURST_FRAMES, BURST_INTERVAL_MS);
|
|
164
|
+
|
|
165
|
+
const ageResults = await estimateAgeBurst(frames);
|
|
166
|
+
const estimatedAge = computeAge(ageResults);
|
|
167
|
+
|
|
168
|
+
const result = await transport.verify({
|
|
169
|
+
estimatedAge,
|
|
170
|
+
livenessOk: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
outcome: result.token ? 'pass' : 'fail',
|
|
175
|
+
token: result.token,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let result = await attempt();
|
|
180
|
+
|
|
181
|
+
while (result.outcome === 'retry' && retryCount < MAX_RETRIES) {
|
|
182
|
+
retryCount++;
|
|
183
|
+
widget.showResult('retry', 'Please try again');
|
|
184
|
+
await waitForStart(widget);
|
|
185
|
+
await startCameraFlow(widget, modelBuffer);
|
|
186
|
+
result = await attempt();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
cleanupLocal();
|
|
190
|
+
emitResult(widget, emitter, result);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
cleanupLocal();
|
|
193
|
+
widget.showResult('fail', 'Verification failed');
|
|
194
|
+
emitter.emit('error', error, widget.id);
|
|
195
|
+
params.errorCallback?.(error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function runServer(widget, emitter) {
|
|
200
|
+
const params = widget.params;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
widget.openPopup();
|
|
204
|
+
widget.setHeroStatus('Connecting…');
|
|
205
|
+
|
|
206
|
+
const transport = createTransport(params.mode, params);
|
|
207
|
+
const session = await transport.createSession();
|
|
208
|
+
|
|
209
|
+
widget.setHeroStatus('Loading…');
|
|
210
|
+
await initVM(session);
|
|
211
|
+
|
|
212
|
+
widget.setHeroStatus('Preparing…');
|
|
213
|
+
await ensureModels(session, () => {});
|
|
214
|
+
|
|
215
|
+
await loadVision();
|
|
216
|
+
const buf = await getMediaPipeModelBuffer(session);
|
|
217
|
+
await initTracker(buf);
|
|
218
|
+
|
|
219
|
+
registerBridge({
|
|
220
|
+
trackFace: () => {
|
|
221
|
+
const video = widget.popupElements?.video;
|
|
222
|
+
if (!video) return 'null';
|
|
223
|
+
const r = track(video, performance.now());
|
|
224
|
+
if (!r) return 'null';
|
|
225
|
+
return JSON.stringify({
|
|
226
|
+
ts: r.timestampMs ?? performance.now(),
|
|
227
|
+
faceCount: r.faceCount,
|
|
228
|
+
headPose: r.headPose || null,
|
|
229
|
+
blendshapes: r.blendshapes || null,
|
|
230
|
+
boundingBox: r.boundingBox || null,
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
captureFrame: () => (captureFrame() ? 'true' : 'null'),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
widget.showReady();
|
|
237
|
+
await waitForStart(widget);
|
|
238
|
+
|
|
239
|
+
const video = widget.showCamera();
|
|
240
|
+
widget.setVideoStatus('Requesting camera…');
|
|
241
|
+
await startCamera(video);
|
|
242
|
+
exposeMirrorVideo(video);
|
|
243
|
+
|
|
244
|
+
widget.setVideoStatus('Position your face');
|
|
245
|
+
await waitForPositioning(video, widget);
|
|
246
|
+
|
|
247
|
+
widget.showLiveness();
|
|
248
|
+
transport.openChannel();
|
|
249
|
+
|
|
250
|
+
let challenge = await transport.receive();
|
|
251
|
+
const rounds = session.rounds;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < rounds; i++) {
|
|
254
|
+
if (!challenge) {
|
|
255
|
+
cleanupVM(transport);
|
|
256
|
+
widget.showResult('fail', 'Verification failed');
|
|
257
|
+
emitter.emit('error', 'failed', widget.id);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (challenge.type === 'verdict') {
|
|
262
|
+
cleanupVM(transport);
|
|
263
|
+
emitVerdict(widget, emitter, challenge);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (challenge.type === 'timeout') {
|
|
268
|
+
cleanupVM(transport);
|
|
269
|
+
widget.showResult('fail', 'Verification failed');
|
|
270
|
+
emitter.emit('error', 'failed', widget.id);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
widget.setVideoStatus(`Step ${i + 1} of ${rounds}`);
|
|
275
|
+
|
|
276
|
+
const task = challenge.token?.task;
|
|
277
|
+
widget.setInstruction(TASK_LABELS[task] ?? 'Look at the camera');
|
|
278
|
+
widget.setTask(task);
|
|
279
|
+
widget.setProgress(i / rounds);
|
|
280
|
+
|
|
281
|
+
const faceData = await captureMotion(widget);
|
|
282
|
+
setFaceData(faceData);
|
|
283
|
+
setChallengeParams(challenge.token);
|
|
284
|
+
|
|
285
|
+
let vmOut;
|
|
286
|
+
try {
|
|
287
|
+
vmOut = execVM();
|
|
288
|
+
} catch {
|
|
289
|
+
cleanupVM(transport);
|
|
290
|
+
widget.showResult('fail', 'Verification failed');
|
|
291
|
+
emitter.emit('error', 'failed', widget.id);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const payload = {
|
|
296
|
+
token: challenge.token,
|
|
297
|
+
tokenSignature: challenge.tokenSignature,
|
|
298
|
+
response: toBase64(vmOut),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = await transport.sendAndReceive(payload);
|
|
302
|
+
|
|
303
|
+
if (!result) {
|
|
304
|
+
cleanupVM(transport);
|
|
305
|
+
widget.showResult('fail', 'Verification failed');
|
|
306
|
+
emitter.emit('error', 'failed', widget.id);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (result.complete) {
|
|
311
|
+
cleanupVM(transport);
|
|
312
|
+
emitVerdict(widget, emitter, result);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (result.hint) {
|
|
317
|
+
widget.setVideoStatus(result.hint);
|
|
318
|
+
await sleep(1000);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
challenge = result.nextChallenge || null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
cleanupVM(transport);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
cleanupVM();
|
|
327
|
+
widget.showResult('fail', 'Verification failed');
|
|
328
|
+
emitter.emit('error', error, widget.id);
|
|
329
|
+
params.errorCallback?.(error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function emitVerdict(widget, emitter, response) {
|
|
334
|
+
const verdict = response?.verdict || response;
|
|
335
|
+
const token = verdict?.token || null;
|
|
336
|
+
const params = widget.params;
|
|
337
|
+
|
|
338
|
+
if (token) {
|
|
339
|
+
widget.token = token;
|
|
340
|
+
widget.showResult('pass', 'Verified');
|
|
341
|
+
emitter.emit('verified', token, widget.id);
|
|
342
|
+
params.callback?.(token);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
widget.showResult('fail', 'Verification failed');
|
|
347
|
+
emitter.emit('error', 'failed', widget.id);
|
|
348
|
+
params.errorCallback?.('failed');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function captureMotion(widget) {
|
|
352
|
+
const video = widget.popupElements?.video;
|
|
353
|
+
const history = [];
|
|
354
|
+
const start = performance.now();
|
|
355
|
+
|
|
356
|
+
while (performance.now() - start < MOTION_CAPTURE_MS) {
|
|
357
|
+
const r = track(video, performance.now());
|
|
358
|
+
if (r && r.faceCount === 1) {
|
|
359
|
+
history.push({
|
|
360
|
+
ts: r.timestampMs,
|
|
361
|
+
headPose: r.headPose,
|
|
362
|
+
blendshapes: r.blendshapes,
|
|
363
|
+
boundingBox: r.boundingBox,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
await sleep(MOTION_SAMPLE_MS);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
faceCount: history.length > 0 ? 1 : 0,
|
|
371
|
+
motionHistory: history,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function emitResult(widget, emitter, result) {
|
|
376
|
+
const params = widget.params;
|
|
377
|
+
|
|
378
|
+
if (result.outcome === 'pass') {
|
|
379
|
+
widget.token = result.token || null;
|
|
380
|
+
widget.showResult('pass', 'Verified');
|
|
381
|
+
emitter.emit('verified', result.token, widget.id);
|
|
382
|
+
params.callback?.(result.token);
|
|
383
|
+
} else {
|
|
384
|
+
widget.showResult('fail', 'Verification failed');
|
|
385
|
+
emitter.emit('error', 'failed', widget.id);
|
|
386
|
+
params.errorCallback?.('failed');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function waitForStart(widget) {
|
|
391
|
+
return new Promise((resolve) => {
|
|
392
|
+
widget.onStartClick = () => {
|
|
393
|
+
widget.onStartClick = null;
|
|
394
|
+
resolve();
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function startCameraFlow(widget, modelBuffer) {
|
|
400
|
+
const video = widget.showCamera();
|
|
401
|
+
widget.setVideoStatus('Requesting camera…');
|
|
402
|
+
await startCamera(video);
|
|
403
|
+
|
|
404
|
+
widget.setVideoStatus('Preparing…');
|
|
405
|
+
await initTracker(modelBuffer);
|
|
406
|
+
|
|
407
|
+
widget.setVideoStatus('Position your face');
|
|
408
|
+
await waitForPositioning(video, widget);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function waitForPositioning(video, widget) {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const handle = startPositioning(video, {
|
|
414
|
+
onStatus: (text) => widget.setVideoStatus(text),
|
|
415
|
+
onReady: () => resolve(),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
setTimeout(() => {
|
|
419
|
+
handle.cancel();
|
|
420
|
+
reject(new Error('Positioning timeout'));
|
|
421
|
+
}, 30000);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function runLivenessLoop(video, session, widget) {
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
const loop = () => {
|
|
428
|
+
const tracking = track(video, performance.now());
|
|
429
|
+
if (tracking) processFrame(session, tracking);
|
|
430
|
+
|
|
431
|
+
widget.setInstruction(currentInstruction(session) || 'Done');
|
|
432
|
+
widget.setTask(currentTaskId(session));
|
|
433
|
+
widget.setProgress(progress(session));
|
|
434
|
+
widget.setVideoStatus(
|
|
435
|
+
`Check ` +
|
|
436
|
+
`${Math.min(session.currentIndex + 1, session.tasks.length)}` +
|
|
437
|
+
` of ${session.tasks.length}`
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (session.failed || isComplete(session)) {
|
|
441
|
+
resolve();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
requestAnimationFrame(loop);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
requestAnimationFrame(loop);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function captureFrameBurst(count, interval) {
|
|
453
|
+
const frames = [];
|
|
454
|
+
for (let i = 0; i < count; i++) {
|
|
455
|
+
const frame = captureFrame();
|
|
456
|
+
if (frame) frames.push(frame);
|
|
457
|
+
if (i < count - 1) await sleep(interval);
|
|
458
|
+
}
|
|
459
|
+
return frames;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function cleanupLocal() {
|
|
463
|
+
stopCamera();
|
|
464
|
+
destroyTracker();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function cleanupVM(transport) {
|
|
468
|
+
stopCamera();
|
|
469
|
+
removeMirrorVideo();
|
|
470
|
+
destroyTracker();
|
|
471
|
+
unregisterBridge();
|
|
472
|
+
destroyVM();
|
|
473
|
+
clearModelCache();
|
|
474
|
+
transport?.close();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function exposeMirrorVideo(source) {
|
|
478
|
+
removeMirrorVideo();
|
|
479
|
+
if (!source?.srcObject) return;
|
|
480
|
+
const mirror = document.createElement('video');
|
|
481
|
+
mirror.id = '__openage_mirror';
|
|
482
|
+
mirror.srcObject = source.srcObject;
|
|
483
|
+
mirror.autoplay = true;
|
|
484
|
+
mirror.muted = true;
|
|
485
|
+
mirror.playsInline = true;
|
|
486
|
+
mirror.style.cssText =
|
|
487
|
+
'position:fixed;width:1px;height:1px;' + 'opacity:0;pointer-events:none;z-index:-1;';
|
|
488
|
+
document.body.appendChild(mirror);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function removeMirrorVideo() {
|
|
492
|
+
document.getElementById('__openage_mirror')?.remove();
|
|
493
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
|
2
|
+
export declare const MEDIAPIPE_CDN: string;
|
|
3
|
+
export declare const MEDIAPIPE_WASM: string;
|
|
4
|
+
export declare const MEDIAPIPE_VISION: string;
|
|
5
|
+
export declare const MEDIAPIPE_MODEL: string;
|
|
6
|
+
export declare const FACEAPI_CDN: string;
|
|
7
|
+
export declare const FACEAPI_MODEL_CDN: string;
|
|
8
|
+
export declare const DEFAULT_MIN_AGE: number;
|
|
9
|
+
export declare const FAIL_FLOOR: number;
|
|
10
|
+
export declare const MAX_RETRIES: number;
|
|
11
|
+
export declare const BURST_FRAMES: number;
|
|
12
|
+
export declare const BURST_INTERVAL_MS: number;
|
|
13
|
+
export declare const POSITION_CHECK_MS: number;
|
|
14
|
+
export declare const MOTION_CAPTURE_MS: number;
|
|
15
|
+
export declare const MOTION_SAMPLE_MS: number;
|
|
16
|
+
export declare const TOKEN_EXPIRY_S: number;
|
|
17
|
+
export declare const STABLE_FRAMES_REQUIRED: number;
|
|
18
|
+
export declare const TASK_TIMEOUT_MS: number;
|
|
19
|
+
export declare const MIN_TASK_TIME_MS: number;
|
|
20
|
+
export declare const TASK_COUNT: number;
|
|
21
|
+
export declare const REQUIRED_TASK_PASSES: number;
|
|
22
|
+
export declare const POPUP_MIN_WIDTH: number;
|
|
23
|
+
export declare const POPUP_MIN_HEIGHT: number;
|
|
24
|
+
export declare const POPUP_MARGIN: number;
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const VERSION = '1.0.0';
|
|
2
|
+
|
|
3
|
+
export const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/' + '@mediapipe/tasks-vision@0.10.17';
|
|
4
|
+
|
|
5
|
+
export const MEDIAPIPE_WASM = `${MEDIAPIPE_CDN}/wasm`;
|
|
6
|
+
|
|
7
|
+
export const MEDIAPIPE_VISION = `${MEDIAPIPE_CDN}/vision_bundle.mjs`;
|
|
8
|
+
|
|
9
|
+
export const MEDIAPIPE_MODEL =
|
|
10
|
+
'https://storage.googleapis.com/mediapipe-models/' +
|
|
11
|
+
'face_landmarker/face_landmarker/float16/latest/' +
|
|
12
|
+
'face_landmarker.task';
|
|
13
|
+
|
|
14
|
+
export const FACEAPI_CDN =
|
|
15
|
+
'https://cdn.jsdelivr.net/npm/' + 'face-api.js@0.22.2/dist/face-api.min.js';
|
|
16
|
+
|
|
17
|
+
export const FACEAPI_MODEL_CDN = 'https://cdn.jsdelivr.net/npm/' + 'face-api.js@0.22.2/weights';
|
|
18
|
+
|
|
19
|
+
export const MAX_RETRIES = 3;
|
|
20
|
+
export const BURST_FRAMES = 5;
|
|
21
|
+
export const BURST_INTERVAL_MS = 200;
|
|
22
|
+
export const POSITION_CHECK_MS = 100;
|
|
23
|
+
export const MOTION_CAPTURE_MS = 3000;
|
|
24
|
+
export const MOTION_SAMPLE_MS = 100;
|
|
25
|
+
export const TOKEN_EXPIRY_S = 300;
|
|
26
|
+
export const STABLE_FRAMES_REQUIRED = 10;
|
|
27
|
+
|
|
28
|
+
export const TASK_TIMEOUT_MS = 8000;
|
|
29
|
+
export const MIN_TASK_TIME_MS = 500;
|
|
30
|
+
export const TASK_COUNT = 3;
|
|
31
|
+
export const REQUIRED_TASK_PASSES = 2;
|
|
32
|
+
|
|
33
|
+
export const POPUP_MIN_WIDTH = 340;
|
|
34
|
+
export const POPUP_MIN_HEIGHT = 520;
|
|
35
|
+
export const POPUP_MARGIN = 12;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface HeadPose {
|
|
2
|
+
yaw: number;
|
|
3
|
+
pitch: number;
|
|
4
|
+
roll: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface BoundingBox {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
area: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TrackingResult {
|
|
16
|
+
faceCount: number;
|
|
17
|
+
timestampMs: number;
|
|
18
|
+
landmarks?: unknown[];
|
|
19
|
+
blendshapes?: Record<string, number>;
|
|
20
|
+
headPose?: HeadPose;
|
|
21
|
+
boundingBox?: BoundingBox;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare function loadVision(): Promise<unknown>;
|
|
25
|
+
|
|
26
|
+
export declare function loadModel(): Promise<Uint8Array>;
|
|
27
|
+
|
|
28
|
+
export declare function initTracker(
|
|
29
|
+
modelBuffer: ArrayBuffer | Uint8Array
|
|
30
|
+
): Promise<void>;
|
|
31
|
+
|
|
32
|
+
export declare function track(
|
|
33
|
+
video: HTMLVideoElement,
|
|
34
|
+
timestampMs: number
|
|
35
|
+
): TrackingResult | null;
|
|
36
|
+
|
|
37
|
+
export declare function destroyTracker(): void;
|
|
38
|
+
|
|
39
|
+
export declare function isTrackerReady(): boolean;
|