@daboss2003/liveness-web 1.0.4 → 1.0.5
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 +2 -1
- package/dist/engine.js +26 -21
- package/package.json +1 -1
- package/src/engine.ts +26 -22
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;
|
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) ───────────────────────────────
|
|
@@ -264,6 +264,7 @@ export class LivenessEngine {
|
|
|
264
264
|
async start() {
|
|
265
265
|
this.stopDetectionOnly();
|
|
266
266
|
this.running = true;
|
|
267
|
+
this.steps = shuffleArray(STEP_LABELS).map((label, index) => ({ index, label }));
|
|
267
268
|
this.stepIndex = 0;
|
|
268
269
|
this.lastDetectTs = -1;
|
|
269
270
|
this.lastOvalState = null;
|
|
@@ -276,7 +277,9 @@ export class LivenessEngine {
|
|
|
276
277
|
if (this.running)
|
|
277
278
|
this.fail("Timed out. Please try again.");
|
|
278
279
|
}, config.sessionTimeoutMs);
|
|
279
|
-
this.
|
|
280
|
+
if (this.steps.length > 0) {
|
|
281
|
+
this.opts.callbacks?.onChallengeChanged?.(this.steps[0].index, this.steps[0].label);
|
|
282
|
+
}
|
|
280
283
|
await this.ensureVideo();
|
|
281
284
|
this.landmarker = await createLandmarkerWithRetry(this.opts, MAX_CDN_RETRIES);
|
|
282
285
|
this.loop();
|
|
@@ -360,12 +363,12 @@ export class LivenessEngine {
|
|
|
360
363
|
}
|
|
361
364
|
this.opts.callbacks?.onDebugFrame?.({
|
|
362
365
|
hasFace: true, metrics,
|
|
363
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
366
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
364
367
|
});
|
|
365
368
|
if (inside) {
|
|
366
|
-
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < steps.length) {
|
|
369
|
+
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < this.steps.length) {
|
|
367
370
|
this.stepSoundPlayedForCurrentStep = true;
|
|
368
|
-
this.playStepSound(steps[this.stepIndex].label);
|
|
371
|
+
this.playStepSound(this.steps[this.stepIndex].label);
|
|
369
372
|
}
|
|
370
373
|
if (this.updateState(metrics, now) === "passed") {
|
|
371
374
|
this.scheduleCapture();
|
|
@@ -380,14 +383,14 @@ export class LivenessEngine {
|
|
|
380
383
|
}
|
|
381
384
|
this.opts.callbacks?.onDebugFrame?.({
|
|
382
385
|
hasFace: false, metrics: null,
|
|
383
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
386
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
384
387
|
});
|
|
385
388
|
}
|
|
386
389
|
this.rafId = requestAnimationFrame(() => this.loop());
|
|
387
390
|
}
|
|
388
391
|
// ── Oval check ─────────────────────────────────────────────────────────────
|
|
389
392
|
checkFaceInOval(m) {
|
|
390
|
-
const isHeadTurn = config.headTurnSteps.has(steps[this.stepIndex]?.label ?? "");
|
|
393
|
+
const isHeadTurn = config.headTurnSteps.has(this.steps[this.stepIndex]?.label ?? "");
|
|
391
394
|
const mx = 1 - m.faceCx; // mirror x to match CSS scaleX(-1)
|
|
392
395
|
const dy = (m.faceCy - config.ovalCy) / config.ovalRy;
|
|
393
396
|
const dx = (mx - config.ovalCx) / config.ovalRx;
|
|
@@ -426,7 +429,7 @@ export class LivenessEngine {
|
|
|
426
429
|
const bPitch = this.baselinePitch ?? metrics.pitch;
|
|
427
430
|
const dYaw = metrics.yaw - bYaw;
|
|
428
431
|
const dPitch = metrics.pitch - bPitch;
|
|
429
|
-
switch (steps[this.stepIndex]
|
|
432
|
+
switch (this.steps[this.stepIndex]?.label) {
|
|
430
433
|
// ── LEFT turn (negative yaw delta = turning left from rest) ─────────────
|
|
431
434
|
case "Turn your head LEFT": {
|
|
432
435
|
if (dYaw > config.yawWrongDirDelta) {
|
|
@@ -531,16 +534,18 @@ export class LivenessEngine {
|
|
|
531
534
|
advanceStep(now) {
|
|
532
535
|
this.stopStepSound();
|
|
533
536
|
this.stepIndex += 1;
|
|
534
|
-
if (this.stepIndex >= steps.length) {
|
|
537
|
+
if (this.stepIndex >= this.steps.length) {
|
|
535
538
|
this.playGoodSound();
|
|
536
539
|
return "passed";
|
|
537
540
|
}
|
|
538
541
|
this.stepStart = now + config.readyMs;
|
|
539
542
|
this.resetStepState();
|
|
540
543
|
this.stepSoundPlayedForCurrentStep = true;
|
|
541
|
-
const step = steps[this.stepIndex];
|
|
542
|
-
|
|
543
|
-
|
|
544
|
+
const step = this.steps[this.stepIndex];
|
|
545
|
+
if (step) {
|
|
546
|
+
this.opts.callbacks?.onChallengeChanged?.(step.index, step.label);
|
|
547
|
+
this.playGoodSound(() => this.playStepSound(step.label));
|
|
548
|
+
}
|
|
544
549
|
return "none";
|
|
545
550
|
}
|
|
546
551
|
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
|
|
|
@@ -350,6 +349,7 @@ export class LivenessEngine {
|
|
|
350
349
|
async start(): Promise<void> {
|
|
351
350
|
this.stopDetectionOnly();
|
|
352
351
|
this.running = true;
|
|
352
|
+
this.steps = shuffleArray(STEP_LABELS).map((label, index) => ({ index, label }));
|
|
353
353
|
this.stepIndex = 0;
|
|
354
354
|
this.lastDetectTs = -1;
|
|
355
355
|
this.lastOvalState = null;
|
|
@@ -361,7 +361,9 @@ export class LivenessEngine {
|
|
|
361
361
|
this.sessionTimeoutId = setTimeout(() => {
|
|
362
362
|
if (this.running) this.fail("Timed out. Please try again.");
|
|
363
363
|
}, config.sessionTimeoutMs);
|
|
364
|
-
this.
|
|
364
|
+
if (this.steps.length > 0) {
|
|
365
|
+
this.opts.callbacks?.onChallengeChanged?.(this.steps[0].index, this.steps[0].label);
|
|
366
|
+
}
|
|
365
367
|
await this.ensureVideo();
|
|
366
368
|
this.landmarker = await createLandmarkerWithRetry(this.opts, MAX_CDN_RETRIES);
|
|
367
369
|
this.loop();
|
|
@@ -455,13 +457,13 @@ export class LivenessEngine {
|
|
|
455
457
|
|
|
456
458
|
this.opts.callbacks?.onDebugFrame?.({
|
|
457
459
|
hasFace: true, metrics,
|
|
458
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
460
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
459
461
|
});
|
|
460
462
|
|
|
461
463
|
if (inside) {
|
|
462
|
-
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < steps.length) {
|
|
464
|
+
if (!this.stepSoundPlayedForCurrentStep && this.stepIndex < this.steps.length) {
|
|
463
465
|
this.stepSoundPlayedForCurrentStep = true;
|
|
464
|
-
this.playStepSound(steps[this.stepIndex].label);
|
|
466
|
+
this.playStepSound(this.steps[this.stepIndex].label);
|
|
465
467
|
}
|
|
466
468
|
if (this.updateState(metrics, now) === "passed") {
|
|
467
469
|
this.scheduleCapture();
|
|
@@ -475,7 +477,7 @@ export class LivenessEngine {
|
|
|
475
477
|
}
|
|
476
478
|
this.opts.callbacks?.onDebugFrame?.({
|
|
477
479
|
hasFace: false, metrics: null,
|
|
478
|
-
step: steps[this.stepIndex]?.label ?? "done",
|
|
480
|
+
step: this.steps[this.stepIndex]?.label ?? "done",
|
|
479
481
|
});
|
|
480
482
|
}
|
|
481
483
|
|
|
@@ -485,7 +487,7 @@ export class LivenessEngine {
|
|
|
485
487
|
// ── Oval check ─────────────────────────────────────────────────────────────
|
|
486
488
|
|
|
487
489
|
private checkFaceInOval(m: Metrics): { inside: boolean; reason?: string } {
|
|
488
|
-
const isHeadTurn = config.headTurnSteps.has(steps[this.stepIndex]?.label ?? "");
|
|
490
|
+
const isHeadTurn = config.headTurnSteps.has(this.steps[this.stepIndex]?.label ?? "");
|
|
489
491
|
const mx = 1 - m.faceCx; // mirror x to match CSS scaleX(-1)
|
|
490
492
|
const dy = (m.faceCy - config.ovalCy) / config.ovalRy;
|
|
491
493
|
const dx = (mx - config.ovalCx) / config.ovalRx;
|
|
@@ -529,7 +531,7 @@ export class LivenessEngine {
|
|
|
529
531
|
const dYaw = metrics.yaw - bYaw;
|
|
530
532
|
const dPitch = metrics.pitch - bPitch;
|
|
531
533
|
|
|
532
|
-
switch (steps[this.stepIndex]
|
|
534
|
+
switch (this.steps[this.stepIndex]?.label) {
|
|
533
535
|
|
|
534
536
|
// ── LEFT turn (negative yaw delta = turning left from rest) ─────────────
|
|
535
537
|
case "Turn your head LEFT": {
|
|
@@ -629,16 +631,18 @@ export class LivenessEngine {
|
|
|
629
631
|
private advanceStep(now: number): "passed" | "none" {
|
|
630
632
|
this.stopStepSound();
|
|
631
633
|
this.stepIndex += 1;
|
|
632
|
-
if (this.stepIndex >= steps.length) {
|
|
634
|
+
if (this.stepIndex >= this.steps.length) {
|
|
633
635
|
this.playGoodSound();
|
|
634
636
|
return "passed";
|
|
635
637
|
}
|
|
636
638
|
this.stepStart = now + config.readyMs;
|
|
637
639
|
this.resetStepState();
|
|
638
640
|
this.stepSoundPlayedForCurrentStep = true;
|
|
639
|
-
const step = steps[this.stepIndex];
|
|
640
|
-
|
|
641
|
-
|
|
641
|
+
const step = this.steps[this.stepIndex];
|
|
642
|
+
if (step) {
|
|
643
|
+
this.opts.callbacks?.onChallengeChanged?.(step.index, step.label);
|
|
644
|
+
this.playGoodSound(() => this.playStepSound(step.label));
|
|
645
|
+
}
|
|
642
646
|
return "none";
|
|
643
647
|
}
|
|
644
648
|
|