@daboss2003/liveness-web 1.0.4 → 1.0.6
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/engine.d.ts +6 -4
- package/dist/engine.js +63 -61
- package/package.json +1 -1
- package/src/engine.ts +60 -60
package/dist/engine.d.ts
CHANGED
|
@@ -50,13 +50,14 @@ export type Metrics = {
|
|
|
50
50
|
faceCy: number;
|
|
51
51
|
faceSize: number;
|
|
52
52
|
};
|
|
53
|
-
export declare const LIVENESS_STEP_COUNT:
|
|
53
|
+
export declare const LIVENESS_STEP_COUNT: 5;
|
|
54
54
|
export declare class LivenessEngine {
|
|
55
55
|
private opts;
|
|
56
56
|
private landmarker;
|
|
57
57
|
private running;
|
|
58
58
|
private rafId;
|
|
59
59
|
private stream;
|
|
60
|
+
private steps;
|
|
60
61
|
private stepIndex;
|
|
61
62
|
private stepStart;
|
|
62
63
|
private baselineYaw;
|
|
@@ -71,14 +72,15 @@ export declare class LivenessEngine {
|
|
|
71
72
|
private lastDetectTs;
|
|
72
73
|
private lastOvalState;
|
|
73
74
|
private stepSoundPlayedForCurrentStep;
|
|
74
|
-
private
|
|
75
|
-
private
|
|
75
|
+
private currentAudio;
|
|
76
|
+
private currentAudioCleanup;
|
|
77
|
+
private currentAudioToken;
|
|
76
78
|
private sessionTimeoutId;
|
|
77
79
|
constructor(opts: LivenessOptions);
|
|
78
80
|
private playSound;
|
|
79
81
|
private getSoundUrl;
|
|
80
82
|
private playStepSound;
|
|
81
|
-
private
|
|
83
|
+
private stopAllSounds;
|
|
82
84
|
private clearSessionTimeout;
|
|
83
85
|
private playGoodSound;
|
|
84
86
|
private playCaptureSound;
|
package/dist/engine.js
CHANGED
|
@@ -84,8 +84,7 @@ function shuffleArray(array) {
|
|
|
84
84
|
}
|
|
85
85
|
return out;
|
|
86
86
|
}
|
|
87
|
-
const
|
|
88
|
-
export const LIVENESS_STEP_COUNT = steps.length;
|
|
87
|
+
export const LIVENESS_STEP_COUNT = STEP_LABELS.length;
|
|
89
88
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
89
|
// KEY DESIGN: RELATIVE MEASUREMENT
|
|
91
90
|
//
|
|
@@ -114,18 +113,18 @@ const config = {
|
|
|
114
113
|
yawWrongDirDelta: 16, // block if turned clearly the WRONG way
|
|
115
114
|
headTurnHoldMs: 80, // sustain the turned pose for this long
|
|
116
115
|
// ── Nod (relative to baseline) ────────────────────────────────────────────
|
|
117
|
-
nodDownDelta:
|
|
118
|
-
nodReturnFraction: 0.
|
|
119
|
-
nodReturnMaxDelta:
|
|
120
|
-
maxYawDuringNod:
|
|
116
|
+
nodDownDelta: 3, // chin must DROP by this many degrees from baseline
|
|
117
|
+
nodReturnFraction: 0.85, // return to 85% of peak nod depth to complete
|
|
118
|
+
nodReturnMaxDelta: 12, // cap: never require returning past 12° from baseline
|
|
119
|
+
maxYawDuringNod: 40,
|
|
121
120
|
// ── Blink ──────────────────────────────────────────────────────────────────
|
|
122
|
-
blinkClosedThreshold: 0.
|
|
123
|
-
blinkOpenThreshold: 0.
|
|
121
|
+
blinkClosedThreshold: 0.30, // blendshape score = eyes closed
|
|
122
|
+
blinkOpenThreshold: 0.25, // blendshape score = eyes open
|
|
124
123
|
earClosedThreshold: 0.20,
|
|
125
124
|
earOpenThreshold: 0.25,
|
|
126
125
|
blinkMaxDurationMs: 4000,
|
|
127
|
-
maxYawDuringBlink:
|
|
128
|
-
maxPitchDuringBlink:
|
|
126
|
+
maxYawDuringBlink: 30,
|
|
127
|
+
maxPitchDuringBlink: 30,
|
|
129
128
|
// ── Mouth ──────────────────────────────────────────────────────────────────
|
|
130
129
|
mouthOpenThreshold: 0.20, // jawOpen blendshape
|
|
131
130
|
mouthOpenMarThreshold: 0.20,
|
|
@@ -158,6 +157,7 @@ export class LivenessEngine {
|
|
|
158
157
|
this.running = false;
|
|
159
158
|
this.rafId = null;
|
|
160
159
|
this.stream = null;
|
|
160
|
+
this.steps = [];
|
|
161
161
|
this.stepIndex = 0;
|
|
162
162
|
this.stepStart = 0;
|
|
163
163
|
// ── Baseline (sampled during readyMs window) ───────────────────────────────
|
|
@@ -174,22 +174,36 @@ export class LivenessEngine {
|
|
|
174
174
|
this.lastDetectTs = -1;
|
|
175
175
|
this.lastOvalState = null;
|
|
176
176
|
this.stepSoundPlayedForCurrentStep = false;
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
177
|
+
this.currentAudio = null;
|
|
178
|
+
this.currentAudioCleanup = null;
|
|
179
|
+
this.currentAudioToken = 0;
|
|
179
180
|
this.sessionTimeoutId = null;
|
|
180
181
|
}
|
|
181
182
|
playSound(url, onEnded) {
|
|
183
|
+
this.stopAllSounds();
|
|
184
|
+
const token = ++this.currentAudioToken;
|
|
182
185
|
const a = new Audio(url);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
a.preload = "auto";
|
|
187
|
+
const done = () => {
|
|
188
|
+
if (token !== this.currentAudioToken)
|
|
189
|
+
return;
|
|
190
|
+
a.removeEventListener("ended", done);
|
|
191
|
+
a.removeEventListener("error", done);
|
|
192
|
+
if (this.currentAudio === a)
|
|
193
|
+
this.currentAudio = null;
|
|
194
|
+
if (this.currentAudioCleanup === cleanup)
|
|
195
|
+
this.currentAudioCleanup = null;
|
|
196
|
+
onEnded?.();
|
|
197
|
+
};
|
|
198
|
+
const cleanup = () => {
|
|
199
|
+
a.removeEventListener("ended", done);
|
|
200
|
+
a.removeEventListener("error", done);
|
|
201
|
+
};
|
|
202
|
+
this.currentAudio = a;
|
|
203
|
+
this.currentAudioCleanup = cleanup;
|
|
204
|
+
a.addEventListener("ended", done);
|
|
205
|
+
a.addEventListener("error", done);
|
|
206
|
+
a.play().catch(() => done());
|
|
193
207
|
}
|
|
194
208
|
getSoundUrl(key) {
|
|
195
209
|
const s = this.opts.sounds;
|
|
@@ -211,34 +225,17 @@ export class LivenessEngine {
|
|
|
211
225
|
const url = this.getSoundUrl(key);
|
|
212
226
|
if (!url)
|
|
213
227
|
return;
|
|
214
|
-
this.
|
|
215
|
-
const a = new Audio(url);
|
|
216
|
-
const done = () => {
|
|
217
|
-
a.removeEventListener("ended", done);
|
|
218
|
-
a.removeEventListener("error", done);
|
|
219
|
-
if (this.currentStepAudio === a)
|
|
220
|
-
this.currentStepAudio = null;
|
|
221
|
-
if (this.currentStepAudioCleanup === cleanup)
|
|
222
|
-
this.currentStepAudioCleanup = null;
|
|
223
|
-
};
|
|
224
|
-
const cleanup = () => {
|
|
225
|
-
a.removeEventListener("ended", done);
|
|
226
|
-
a.removeEventListener("error", done);
|
|
227
|
-
};
|
|
228
|
-
this.currentStepAudio = a;
|
|
229
|
-
this.currentStepAudioCleanup = cleanup;
|
|
230
|
-
a.addEventListener("ended", done);
|
|
231
|
-
a.addEventListener("error", done);
|
|
232
|
-
a.play().catch(() => done());
|
|
228
|
+
this.playSound(url);
|
|
233
229
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.
|
|
230
|
+
stopAllSounds() {
|
|
231
|
+
this.currentAudioToken += 1;
|
|
232
|
+
if (this.currentAudio) {
|
|
233
|
+
this.currentAudio.pause();
|
|
234
|
+
this.currentAudio.currentTime = 0;
|
|
238
235
|
}
|
|
239
|
-
this.
|
|
240
|
-
this.
|
|
241
|
-
this.
|
|
236
|
+
this.currentAudioCleanup?.();
|
|
237
|
+
this.currentAudioCleanup = null;
|
|
238
|
+
this.currentAudio = null;
|
|
242
239
|
}
|
|
243
240
|
clearSessionTimeout() {
|
|
244
241
|
if (this.sessionTimeoutId != null) {
|
|
@@ -264,6 +261,7 @@ export class LivenessEngine {
|
|
|
264
261
|
async start() {
|
|
265
262
|
this.stopDetectionOnly();
|
|
266
263
|
this.running = true;
|
|
264
|
+
this.steps = shuffleArray(STEP_LABELS).map((label, index) => ({ index, label }));
|
|
267
265
|
this.stepIndex = 0;
|
|
268
266
|
this.lastDetectTs = -1;
|
|
269
267
|
this.lastOvalState = null;
|
|
@@ -276,7 +274,9 @@ export class LivenessEngine {
|
|
|
276
274
|
if (this.running)
|
|
277
275
|
this.fail("Timed out. Please try again.");
|
|
278
276
|
}, config.sessionTimeoutMs);
|
|
279
|
-
this.
|
|
277
|
+
if (this.steps.length > 0) {
|
|
278
|
+
this.opts.callbacks?.onChallengeChanged?.(this.steps[0].index, this.steps[0].label);
|
|
279
|
+
}
|
|
280
280
|
await this.ensureVideo();
|
|
281
281
|
this.landmarker = await createLandmarkerWithRetry(this.opts, MAX_CDN_RETRIES);
|
|
282
282
|
this.loop();
|
|
@@ -289,7 +289,7 @@ export class LivenessEngine {
|
|
|
289
289
|
stopDetectionOnly() {
|
|
290
290
|
this.running = false;
|
|
291
291
|
this.clearSessionTimeout();
|
|
292
|
-
this.
|
|
292
|
+
this.stopAllSounds();
|
|
293
293
|
if (this.rafId != null) {
|
|
294
294
|
cancelAnimationFrame(this.rafId);
|
|
295
295
|
this.rafId = null;
|
|
@@ -360,12 +360,12 @@ export class LivenessEngine {
|
|
|
360
360
|
}
|
|
361
361
|
this.opts.callbacks?.onDebugFrame?.({
|
|
362
362
|
hasFace: true, metrics,
|
|
363
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
363
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
364
364
|
});
|
|
365
365
|
if (inside) {
|
|
366
|
-
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < steps.length) {
|
|
366
|
+
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < this.steps.length) {
|
|
367
367
|
this.stepSoundPlayedForCurrentStep = true;
|
|
368
|
-
this.playStepSound(steps[this.stepIndex].label);
|
|
368
|
+
this.playStepSound(this.steps[this.stepIndex].label);
|
|
369
369
|
}
|
|
370
370
|
if (this.updateState(metrics, now) === "passed") {
|
|
371
371
|
this.scheduleCapture();
|
|
@@ -380,14 +380,14 @@ export class LivenessEngine {
|
|
|
380
380
|
}
|
|
381
381
|
this.opts.callbacks?.onDebugFrame?.({
|
|
382
382
|
hasFace: false, metrics: null,
|
|
383
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
383
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
386
|
this.rafId = requestAnimationFrame(() => this.loop());
|
|
387
387
|
}
|
|
388
388
|
// ── Oval check ─────────────────────────────────────────────────────────────
|
|
389
389
|
checkFaceInOval(m) {
|
|
390
|
-
const isHeadTurn = config.headTurnSteps.has(steps[this.stepIndex]?.label ?? "");
|
|
390
|
+
const isHeadTurn = config.headTurnSteps.has(this.steps[this.stepIndex]?.label ?? "");
|
|
391
391
|
const mx = 1 - m.faceCx; // mirror x to match CSS scaleX(-1)
|
|
392
392
|
const dy = (m.faceCy - config.ovalCy) / config.ovalRy;
|
|
393
393
|
const dx = (mx - config.ovalCx) / config.ovalRx;
|
|
@@ -426,7 +426,7 @@ export class LivenessEngine {
|
|
|
426
426
|
const bPitch = this.baselinePitch ?? metrics.pitch;
|
|
427
427
|
const dYaw = metrics.yaw - bYaw;
|
|
428
428
|
const dPitch = metrics.pitch - bPitch;
|
|
429
|
-
switch (steps[this.stepIndex]
|
|
429
|
+
switch (this.steps[this.stepIndex]?.label) {
|
|
430
430
|
// ── LEFT turn (negative yaw delta = turning left from rest) ─────────────
|
|
431
431
|
case "Turn your head LEFT": {
|
|
432
432
|
if (dYaw > config.yawWrongDirDelta) {
|
|
@@ -529,18 +529,20 @@ export class LivenessEngine {
|
|
|
529
529
|
return "none";
|
|
530
530
|
}
|
|
531
531
|
advanceStep(now) {
|
|
532
|
-
this.
|
|
532
|
+
this.stopAllSounds();
|
|
533
533
|
this.stepIndex += 1;
|
|
534
|
-
if (this.stepIndex >= steps.length) {
|
|
534
|
+
if (this.stepIndex >= this.steps.length) {
|
|
535
535
|
this.playGoodSound();
|
|
536
536
|
return "passed";
|
|
537
537
|
}
|
|
538
538
|
this.stepStart = now + config.readyMs;
|
|
539
539
|
this.resetStepState();
|
|
540
540
|
this.stepSoundPlayedForCurrentStep = true;
|
|
541
|
-
const step = steps[this.stepIndex];
|
|
542
|
-
|
|
543
|
-
|
|
541
|
+
const step = this.steps[this.stepIndex];
|
|
542
|
+
if (step) {
|
|
543
|
+
this.opts.callbacks?.onChallengeChanged?.(step.index, step.label);
|
|
544
|
+
this.playGoodSound(() => this.playStepSound(step.label));
|
|
545
|
+
}
|
|
544
546
|
return "none";
|
|
545
547
|
}
|
|
546
548
|
fail(reason) {
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -158,9 +158,7 @@ function shuffleArray<T>(array: readonly T[]): T[] {
|
|
|
158
158
|
return out;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
export const LIVENESS_STEP_COUNT = steps.length;
|
|
161
|
+
export const LIVENESS_STEP_COUNT = STEP_LABELS.length;
|
|
164
162
|
|
|
165
163
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
164
|
// KEY DESIGN: RELATIVE MEASUREMENT
|
|
@@ -193,19 +191,19 @@ const config = {
|
|
|
193
191
|
headTurnHoldMs: 80, // sustain the turned pose for this long
|
|
194
192
|
|
|
195
193
|
// ── Nod (relative to baseline) ────────────────────────────────────────────
|
|
196
|
-
nodDownDelta:
|
|
197
|
-
nodReturnFraction: 0.
|
|
198
|
-
nodReturnMaxDelta:
|
|
199
|
-
maxYawDuringNod:
|
|
194
|
+
nodDownDelta: 3, // chin must DROP by this many degrees from baseline
|
|
195
|
+
nodReturnFraction: 0.85, // return to 85% of peak nod depth to complete
|
|
196
|
+
nodReturnMaxDelta: 12, // cap: never require returning past 12° from baseline
|
|
197
|
+
maxYawDuringNod: 40,
|
|
200
198
|
|
|
201
199
|
// ── Blink ──────────────────────────────────────────────────────────────────
|
|
202
|
-
blinkClosedThreshold: 0.
|
|
203
|
-
blinkOpenThreshold: 0.
|
|
200
|
+
blinkClosedThreshold: 0.30, // blendshape score = eyes closed
|
|
201
|
+
blinkOpenThreshold: 0.25, // blendshape score = eyes open
|
|
204
202
|
earClosedThreshold: 0.20,
|
|
205
203
|
earOpenThreshold: 0.25,
|
|
206
204
|
blinkMaxDurationMs: 4000,
|
|
207
|
-
maxYawDuringBlink:
|
|
208
|
-
maxPitchDuringBlink:
|
|
205
|
+
maxYawDuringBlink: 30,
|
|
206
|
+
maxPitchDuringBlink: 30,
|
|
209
207
|
|
|
210
208
|
// ── Mouth ──────────────────────────────────────────────────────────────────
|
|
211
209
|
mouthOpenThreshold: 0.20, // jawOpen blendshape
|
|
@@ -242,6 +240,7 @@ export class LivenessEngine {
|
|
|
242
240
|
private rafId: number | null = null;
|
|
243
241
|
private stream: MediaStream | null = null;
|
|
244
242
|
|
|
243
|
+
private steps: LivenessStep[] = [];
|
|
245
244
|
private stepIndex = 0;
|
|
246
245
|
private stepStart = 0;
|
|
247
246
|
|
|
@@ -261,24 +260,35 @@ export class LivenessEngine {
|
|
|
261
260
|
private lastDetectTs = -1;
|
|
262
261
|
private lastOvalState: boolean | null = null;
|
|
263
262
|
private stepSoundPlayedForCurrentStep = false;
|
|
264
|
-
private
|
|
265
|
-
private
|
|
263
|
+
private currentAudio: HTMLAudioElement | null = null;
|
|
264
|
+
private currentAudioCleanup: (() => void) | null = null;
|
|
265
|
+
private currentAudioToken = 0;
|
|
266
266
|
private sessionTimeoutId: number | null = null;
|
|
267
267
|
|
|
268
268
|
constructor(private opts: LivenessOptions) {}
|
|
269
269
|
|
|
270
270
|
private playSound(url: string, onEnded?: () => void): void {
|
|
271
|
+
this.stopAllSounds();
|
|
272
|
+
const token = ++this.currentAudioToken;
|
|
271
273
|
const a = new Audio(url);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
274
|
+
a.preload = "auto";
|
|
275
|
+
const done = () => {
|
|
276
|
+
if (token !== this.currentAudioToken) return;
|
|
277
|
+
a.removeEventListener("ended", done);
|
|
278
|
+
a.removeEventListener("error", done);
|
|
279
|
+
if (this.currentAudio === a) this.currentAudio = null;
|
|
280
|
+
if (this.currentAudioCleanup === cleanup) this.currentAudioCleanup = null;
|
|
281
|
+
onEnded?.();
|
|
282
|
+
};
|
|
283
|
+
const cleanup = () => {
|
|
284
|
+
a.removeEventListener("ended", done);
|
|
285
|
+
a.removeEventListener("error", done);
|
|
286
|
+
};
|
|
287
|
+
this.currentAudio = a;
|
|
288
|
+
this.currentAudioCleanup = cleanup;
|
|
289
|
+
a.addEventListener("ended", done);
|
|
290
|
+
a.addEventListener("error", done);
|
|
291
|
+
a.play().catch(() => done());
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
private getSoundUrl(key: string): string | undefined {
|
|
@@ -297,33 +307,18 @@ export class LivenessEngine {
|
|
|
297
307
|
if (!key) return;
|
|
298
308
|
const url = this.getSoundUrl(key);
|
|
299
309
|
if (!url) return;
|
|
300
|
-
this.
|
|
301
|
-
const a = new Audio(url);
|
|
302
|
-
const done = () => {
|
|
303
|
-
a.removeEventListener("ended", done);
|
|
304
|
-
a.removeEventListener("error", done);
|
|
305
|
-
if (this.currentStepAudio === a) this.currentStepAudio = null;
|
|
306
|
-
if (this.currentStepAudioCleanup === cleanup) this.currentStepAudioCleanup = null;
|
|
307
|
-
};
|
|
308
|
-
const cleanup = () => {
|
|
309
|
-
a.removeEventListener("ended", done);
|
|
310
|
-
a.removeEventListener("error", done);
|
|
311
|
-
};
|
|
312
|
-
this.currentStepAudio = a;
|
|
313
|
-
this.currentStepAudioCleanup = cleanup;
|
|
314
|
-
a.addEventListener("ended", done);
|
|
315
|
-
a.addEventListener("error", done);
|
|
316
|
-
a.play().catch(() => done());
|
|
310
|
+
this.playSound(url);
|
|
317
311
|
}
|
|
318
312
|
|
|
319
|
-
private
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.
|
|
313
|
+
private stopAllSounds(): void {
|
|
314
|
+
this.currentAudioToken += 1;
|
|
315
|
+
if (this.currentAudio) {
|
|
316
|
+
this.currentAudio.pause();
|
|
317
|
+
this.currentAudio.currentTime = 0;
|
|
323
318
|
}
|
|
324
|
-
this.
|
|
325
|
-
this.
|
|
326
|
-
this.
|
|
319
|
+
this.currentAudioCleanup?.();
|
|
320
|
+
this.currentAudioCleanup = null;
|
|
321
|
+
this.currentAudio = null;
|
|
327
322
|
}
|
|
328
323
|
|
|
329
324
|
private clearSessionTimeout(): void {
|
|
@@ -350,6 +345,7 @@ export class LivenessEngine {
|
|
|
350
345
|
async start(): Promise<void> {
|
|
351
346
|
this.stopDetectionOnly();
|
|
352
347
|
this.running = true;
|
|
348
|
+
this.steps = shuffleArray(STEP_LABELS).map((label, index) => ({ index, label }));
|
|
353
349
|
this.stepIndex = 0;
|
|
354
350
|
this.lastDetectTs = -1;
|
|
355
351
|
this.lastOvalState = null;
|
|
@@ -361,7 +357,9 @@ export class LivenessEngine {
|
|
|
361
357
|
this.sessionTimeoutId = setTimeout(() => {
|
|
362
358
|
if (this.running) this.fail("Timed out. Please try again.");
|
|
363
359
|
}, config.sessionTimeoutMs);
|
|
364
|
-
this.
|
|
360
|
+
if (this.steps.length > 0) {
|
|
361
|
+
this.opts.callbacks?.onChallengeChanged?.(this.steps[0].index, this.steps[0].label);
|
|
362
|
+
}
|
|
365
363
|
await this.ensureVideo();
|
|
366
364
|
this.landmarker = await createLandmarkerWithRetry(this.opts, MAX_CDN_RETRIES);
|
|
367
365
|
this.loop();
|
|
@@ -376,7 +374,7 @@ export class LivenessEngine {
|
|
|
376
374
|
private stopDetectionOnly(): void {
|
|
377
375
|
this.running = false;
|
|
378
376
|
this.clearSessionTimeout();
|
|
379
|
-
this.
|
|
377
|
+
this.stopAllSounds();
|
|
380
378
|
if (this.rafId != null) { cancelAnimationFrame(this.rafId); this.rafId = null; }
|
|
381
379
|
if (this.landmarker) { this.landmarker.close(); this.landmarker = null; }
|
|
382
380
|
}
|
|
@@ -455,13 +453,13 @@ export class LivenessEngine {
|
|
|
455
453
|
|
|
456
454
|
this.opts.callbacks?.onDebugFrame?.({
|
|
457
455
|
hasFace: true, metrics,
|
|
458
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
456
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
459
457
|
});
|
|
460
458
|
|
|
461
459
|
if (inside) {
|
|
462
|
-
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < steps.length) {
|
|
460
|
+
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < this.steps.length) {
|
|
463
461
|
this.stepSoundPlayedForCurrentStep = true;
|
|
464
|
-
this.playStepSound(steps[this.stepIndex].label);
|
|
462
|
+
this.playStepSound(this.steps[this.stepIndex].label);
|
|
465
463
|
}
|
|
466
464
|
if (this.updateState(metrics, now) === "passed") {
|
|
467
465
|
this.scheduleCapture();
|
|
@@ -475,7 +473,7 @@ export class LivenessEngine {
|
|
|
475
473
|
}
|
|
476
474
|
this.opts.callbacks?.onDebugFrame?.({
|
|
477
475
|
hasFace: false, metrics: null,
|
|
478
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
476
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
479
477
|
});
|
|
480
478
|
}
|
|
481
479
|
|
|
@@ -485,7 +483,7 @@ export class LivenessEngine {
|
|
|
485
483
|
// ── Oval check ─────────────────────────────────────────────────────────────
|
|
486
484
|
|
|
487
485
|
private checkFaceInOval(m: Metrics): { inside: boolean; reason?: string } {
|
|
488
|
-
const isHeadTurn = config.headTurnSteps.has(steps[this.stepIndex]?.label ?? "");
|
|
486
|
+
const isHeadTurn = config.headTurnSteps.has(this.steps[this.stepIndex]?.label ?? "");
|
|
489
487
|
const mx = 1 - m.faceCx; // mirror x to match CSS scaleX(-1)
|
|
490
488
|
const dy = (m.faceCy - config.ovalCy) / config.ovalRy;
|
|
491
489
|
const dx = (mx - config.ovalCx) / config.ovalRx;
|
|
@@ -529,7 +527,7 @@ export class LivenessEngine {
|
|
|
529
527
|
const dYaw = metrics.yaw - bYaw;
|
|
530
528
|
const dPitch = metrics.pitch - bPitch;
|
|
531
529
|
|
|
532
|
-
switch (steps[this.stepIndex]
|
|
530
|
+
switch (this.steps[this.stepIndex]?.label) {
|
|
533
531
|
|
|
534
532
|
// ── LEFT turn (negative yaw delta = turning left from rest) ─────────────
|
|
535
533
|
case "Turn your head LEFT": {
|
|
@@ -627,18 +625,20 @@ export class LivenessEngine {
|
|
|
627
625
|
}
|
|
628
626
|
|
|
629
627
|
private advanceStep(now: number): "passed" | "none" {
|
|
630
|
-
this.
|
|
628
|
+
this.stopAllSounds();
|
|
631
629
|
this.stepIndex += 1;
|
|
632
|
-
if (this.stepIndex >= steps.length) {
|
|
630
|
+
if (this.stepIndex >= this.steps.length) {
|
|
633
631
|
this.playGoodSound();
|
|
634
632
|
return "passed";
|
|
635
633
|
}
|
|
636
634
|
this.stepStart = now + config.readyMs;
|
|
637
635
|
this.resetStepState();
|
|
638
636
|
this.stepSoundPlayedForCurrentStep = true;
|
|
639
|
-
const step = steps[this.stepIndex];
|
|
640
|
-
|
|
641
|
-
|
|
637
|
+
const step = this.steps[this.stepIndex];
|
|
638
|
+
if (step) {
|
|
639
|
+
this.opts.callbacks?.onChallengeChanged?.(step.index, step.label);
|
|
640
|
+
this.playGoodSound(() => this.playStepSound(step.label));
|
|
641
|
+
}
|
|
642
642
|
return "none";
|
|
643
643
|
}
|
|
644
644
|
|