@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 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: number;
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 steps = shuffleArray(STEP_LABELS).map((label, index) => ({ index, label }));
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: 4, // chin must DROP by this many degrees from baseline
118
- nodReturnFraction: 0.75, // return to 75% of peak nod depth to complete
119
- nodReturnMaxDelta: 9, // cap: never require returning past 9° from baseline
120
- maxYawDuringNod: 32,
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.35, // blendshape score = eyes closed
123
- blinkOpenThreshold: 0.20, // blendshape score = eyes open
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: 25,
128
- maxPitchDuringBlink: 25,
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.opts.callbacks?.onChallengeChanged?.(steps[0].index, steps[0].label);
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].label) {
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
- this.opts.callbacks?.onChallengeChanged?.(step.index, step.label);
543
- this.playGoodSound(() => this.playStepSound(step.label));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daboss2003/liveness-web",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Web liveness detection using MediaPipe Face Landmarker (CDN)",
5
5
  "keywords": [
6
6
  "liveness",
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 steps: LivenessStep[] = shuffleArray(STEP_LABELS).map((label, index) => ({ index, label }));
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: 4, // chin must DROP by this many degrees from baseline
197
- nodReturnFraction: 0.75, // return to 75% of peak nod depth to complete
198
- nodReturnMaxDelta: 9, // cap: never require returning past 9° from baseline
199
- maxYawDuringNod: 32,
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.35, // blendshape score = eyes closed
203
- blinkOpenThreshold: 0.20, // blendshape score = eyes open
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: 25,
208
- maxPitchDuringBlink: 25,
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.opts.callbacks?.onChallengeChanged?.(steps[0].index, steps[0].label);
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].label) {
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
- this.opts.callbacks?.onChallengeChanged?.(step.index, step.label);
641
- this.playGoodSound(() => this.playStepSound(step.label));
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