@moveris/shared 1.0.2 → 2.1.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/dist/index.d.mts +248 -106
- package/dist/index.d.ts +248 -106
- package/dist/index.js +340 -11
- package/dist/index.mjs +330 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,9 +32,13 @@ __export(index_exports, {
|
|
|
32
32
|
BaseFrameCollector: () => BaseFrameCollector,
|
|
33
33
|
DEFAULT_BLUR_THRESHOLD: () => DEFAULT_BLUR_THRESHOLD,
|
|
34
34
|
DEFAULT_ENDPOINT: () => DEFAULT_ENDPOINT,
|
|
35
|
+
DEFAULT_FACE_DETECTION_TIERS: () => DEFAULT_FACE_DETECTION_TIERS,
|
|
36
|
+
DEFAULT_GAZE_THRESHOLDS: () => DEFAULT_GAZE_THRESHOLDS,
|
|
37
|
+
DEFAULT_HAND_OCCLUSION_CONFIG: () => DEFAULT_HAND_OCCLUSION_CONFIG,
|
|
35
38
|
DEFAULT_LIVENESS_CONFIG: () => DEFAULT_LIVENESS_CONFIG,
|
|
36
39
|
DEFAULT_LOCALE: () => DEFAULT_LOCALE,
|
|
37
40
|
DEFAULT_OVAL_REGION: () => DEFAULT_OVAL_REGION,
|
|
41
|
+
DEFAULT_STABILIZER_CONFIG: () => DEFAULT_STABILIZER_CONFIG,
|
|
38
42
|
DEFAULT_STATUS_MESSAGES: () => DEFAULT_STATUS_MESSAGES,
|
|
39
43
|
ES_LOCALE: () => ES_LOCALE,
|
|
40
44
|
FACE_CENTER_VERTICAL_OFFSET: () => FACE_CENTER_VERTICAL_OFFSET,
|
|
@@ -48,6 +52,9 @@ __export(index_exports, {
|
|
|
48
52
|
HIGH_ALIGNMENT: () => HIGH_ALIGNMENT,
|
|
49
53
|
HYBRID_MODEL_CONFIGS: () => HYBRID_MODEL_CONFIGS,
|
|
50
54
|
IDEAL_CROP_MULTIPLIER: () => IDEAL_CROP_MULTIPLIER,
|
|
55
|
+
LANDMARK_INDEX: () => LANDMARK_INDEX,
|
|
56
|
+
LANDMARK_MAX_BOUND: () => LANDMARK_MAX_BOUND,
|
|
57
|
+
LANDMARK_MIN_BOUND: () => LANDMARK_MIN_BOUND,
|
|
51
58
|
LOW_LIGHT_THRESHOLD: () => LOW_LIGHT_THRESHOLD,
|
|
52
59
|
LivenessApiError: () => LivenessApiError,
|
|
53
60
|
LivenessClient: () => LivenessClient,
|
|
@@ -60,6 +67,7 @@ __export(index_exports, {
|
|
|
60
67
|
MIN_FACE_RATIO: () => MIN_FACE_RATIO,
|
|
61
68
|
MIN_FACE_SIDE_MARGIN: () => MIN_FACE_SIDE_MARGIN,
|
|
62
69
|
MIN_FACE_TOP_MARGIN: () => MIN_FACE_TOP_MARGIN,
|
|
70
|
+
MIN_LANDMARK_COUNT: () => MIN_LANDMARK_COUNT,
|
|
63
71
|
MODEL_CONFIGS: () => MODEL_CONFIGS,
|
|
64
72
|
OVAL_GUIDE_COLORS: () => OVAL_GUIDE_COLORS,
|
|
65
73
|
OVAL_GUIDE_STYLES: () => OVAL_GUIDE_STYLES,
|
|
@@ -91,7 +99,9 @@ __export(index_exports, {
|
|
|
91
99
|
toFrameData: () => toFrameData,
|
|
92
100
|
toHybridFrameData: () => toHybridFrameData,
|
|
93
101
|
toLivenessResult: () => toLivenessResult,
|
|
102
|
+
toLivenessResultFromStream: () => toLivenessResultFromStream,
|
|
94
103
|
validateApiKey: () => validateApiKey,
|
|
104
|
+
validateFaceLandmarks: () => validateFaceLandmarks,
|
|
95
105
|
validateFrameCount: () => validateFrameCount,
|
|
96
106
|
validateFrameData: () => validateFrameData,
|
|
97
107
|
validateFrameIndex: () => validateFrameIndex,
|
|
@@ -112,6 +122,7 @@ var API_PATHS = {
|
|
|
112
122
|
health: "/health",
|
|
113
123
|
fastCheck: "/api/v1/fast-check",
|
|
114
124
|
fastCheckCrops: "/api/v1/fast-check-crops",
|
|
125
|
+
fastCheckStream: "/api/v1/fast-check-stream",
|
|
115
126
|
verify: "/api/v1/verify",
|
|
116
127
|
hybridCheck: "/api/v1/hybrid-check",
|
|
117
128
|
hybrid50: "/api/v1/hybrid-50",
|
|
@@ -232,6 +243,22 @@ function toLivenessResult(response) {
|
|
|
232
243
|
framesProcessed: "frames_processed" in response ? response.frames_processed ?? 0 : 0
|
|
233
244
|
};
|
|
234
245
|
}
|
|
246
|
+
function toLivenessResultFromStream(response) {
|
|
247
|
+
if (response.status !== "complete") {
|
|
248
|
+
throw new LivenessApiError("Stream not complete", "stream_incomplete", 400);
|
|
249
|
+
}
|
|
250
|
+
if (!response.verdict) {
|
|
251
|
+
throw new LivenessApiError(response.error ?? "No verdict received", "no_verdict", 500);
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
verdict: response.verdict,
|
|
255
|
+
confidence: response.confidence ?? 0,
|
|
256
|
+
score: response.score ?? 0,
|
|
257
|
+
sessionId: response.session_id,
|
|
258
|
+
processingMs: response.processing_ms ?? 0,
|
|
259
|
+
framesProcessed: response.frames_processed ?? 0
|
|
260
|
+
};
|
|
261
|
+
}
|
|
235
262
|
function generateSessionId() {
|
|
236
263
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
237
264
|
return crypto.randomUUID();
|
|
@@ -366,6 +393,145 @@ var LivenessClient = class {
|
|
|
366
393
|
});
|
|
367
394
|
return toLivenessResult(response);
|
|
368
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Send a single captured frame to the streaming endpoint.
|
|
398
|
+
*
|
|
399
|
+
* Use this to implement **real-time** streaming: call once per captured
|
|
400
|
+
* frame instead of batching all frames first.
|
|
401
|
+
*
|
|
402
|
+
* - While the server is still collecting frames the response will have
|
|
403
|
+
* `status: 'buffering'` with `frames_received` / `frames_required`.
|
|
404
|
+
* - When the last required frame arrives the response will have
|
|
405
|
+
* `status: 'complete'` with the full liveness result.
|
|
406
|
+
*
|
|
407
|
+
* Each frame gets its own retry via {@link retryWithBackoff}.
|
|
408
|
+
*
|
|
409
|
+
* @param frame - A single captured frame
|
|
410
|
+
* @param options - Session, model and source
|
|
411
|
+
* @returns Stream response (buffering or complete)
|
|
412
|
+
*/
|
|
413
|
+
async streamFrame(frame, options) {
|
|
414
|
+
const frameData = {
|
|
415
|
+
index: frame.index,
|
|
416
|
+
timestamp_ms: frame.timestampMs,
|
|
417
|
+
pixels: frame.pixels
|
|
418
|
+
};
|
|
419
|
+
return this.sendStreamFrameInternal(frameData, {
|
|
420
|
+
sessionId: options.sessionId,
|
|
421
|
+
model: options.model ?? "10",
|
|
422
|
+
source: options.source ?? "live"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Send a single FrameData to the streaming endpoint with retry (internal)
|
|
427
|
+
*
|
|
428
|
+
* @param frame - Single frame to send
|
|
429
|
+
* @param options - Session and model options
|
|
430
|
+
* @returns Stream response with status
|
|
431
|
+
*/
|
|
432
|
+
async sendStreamFrameInternal(frameData, options) {
|
|
433
|
+
const request = {
|
|
434
|
+
session_id: options.sessionId,
|
|
435
|
+
model: options.model,
|
|
436
|
+
source: options.source,
|
|
437
|
+
frame: frameData
|
|
438
|
+
};
|
|
439
|
+
return this.requestWithRetry(API_PATHS.fastCheckStream, {
|
|
440
|
+
method: "POST",
|
|
441
|
+
body: JSON.stringify(request)
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Perform streaming liveness check by sending frames one at a time
|
|
446
|
+
*
|
|
447
|
+
* Each frame is sent separately to the streaming endpoint with individual retry.
|
|
448
|
+
* The last frame that completes the set will return the full liveness result.
|
|
449
|
+
*
|
|
450
|
+
* @param frames - Captured frames to analyze
|
|
451
|
+
* @param options - Additional options
|
|
452
|
+
* @param callbacks - Optional callbacks for progress tracking
|
|
453
|
+
* @returns Liveness result
|
|
454
|
+
*/
|
|
455
|
+
async fastCheckStream(frames, options = {}, callbacks) {
|
|
456
|
+
const sessionId = options.sessionId ?? generateSessionId();
|
|
457
|
+
const model = options.model ?? "10";
|
|
458
|
+
const source = options.source ?? "live";
|
|
459
|
+
const frameDataList = toFrameData(frames);
|
|
460
|
+
const total = frameDataList.length;
|
|
461
|
+
let finalResponse = null;
|
|
462
|
+
const sendPromises = frameDataList.map(async (frameData, index) => {
|
|
463
|
+
const response = await this.sendStreamFrameInternal(frameData, {
|
|
464
|
+
sessionId,
|
|
465
|
+
model,
|
|
466
|
+
source
|
|
467
|
+
});
|
|
468
|
+
callbacks?.onFrameSent?.(index + 1, total);
|
|
469
|
+
if (response.status === "buffering") {
|
|
470
|
+
callbacks?.onFrameBuffered?.(response.frames_received, response.frames_required);
|
|
471
|
+
}
|
|
472
|
+
return response;
|
|
473
|
+
});
|
|
474
|
+
const responses = await Promise.all(sendPromises);
|
|
475
|
+
finalResponse = responses.find((r) => r.status === "complete") ?? null;
|
|
476
|
+
if (!finalResponse) {
|
|
477
|
+
const lastResponse = responses[responses.length - 1];
|
|
478
|
+
if (lastResponse) {
|
|
479
|
+
throw new LivenessApiError(
|
|
480
|
+
`Streaming incomplete: received ${lastResponse.frames_received}/${lastResponse.frames_required} frames`,
|
|
481
|
+
"stream_incomplete",
|
|
482
|
+
400
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
throw new LivenessApiError(
|
|
486
|
+
"Streaming failed: no responses received",
|
|
487
|
+
"stream_incomplete",
|
|
488
|
+
400
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return toLivenessResultFromStream(finalResponse);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Perform streaming liveness check by sending frames sequentially
|
|
495
|
+
*
|
|
496
|
+
* Alternative to fastCheckStream that sends frames one by one in order.
|
|
497
|
+
* Useful when parallel uploads are not desired or network is constrained.
|
|
498
|
+
*
|
|
499
|
+
* @param frames - Captured frames to analyze
|
|
500
|
+
* @param options - Additional options
|
|
501
|
+
* @param callbacks - Optional callbacks for progress tracking
|
|
502
|
+
* @returns Liveness result
|
|
503
|
+
*/
|
|
504
|
+
async fastCheckStreamSequential(frames, options = {}, callbacks) {
|
|
505
|
+
const sessionId = options.sessionId ?? generateSessionId();
|
|
506
|
+
const model = options.model ?? "10";
|
|
507
|
+
const source = options.source ?? "live";
|
|
508
|
+
const frameDataList = toFrameData(frames);
|
|
509
|
+
const total = frameDataList.length;
|
|
510
|
+
let finalResponse = null;
|
|
511
|
+
for (const frameData of frameDataList) {
|
|
512
|
+
const response = await this.sendStreamFrameInternal(frameData, {
|
|
513
|
+
sessionId,
|
|
514
|
+
model,
|
|
515
|
+
source
|
|
516
|
+
});
|
|
517
|
+
const currentIndex = frameData.index;
|
|
518
|
+
callbacks?.onFrameSent?.(currentIndex + 1, total);
|
|
519
|
+
if (response.status === "buffering") {
|
|
520
|
+
callbacks?.onFrameBuffered?.(response.frames_received, response.frames_required);
|
|
521
|
+
} else if (response.status === "complete") {
|
|
522
|
+
finalResponse = response;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!finalResponse) {
|
|
527
|
+
throw new LivenessApiError(
|
|
528
|
+
"Streaming did not complete after all frames sent",
|
|
529
|
+
"stream_incomplete",
|
|
530
|
+
400
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return toLivenessResultFromStream(finalResponse);
|
|
534
|
+
}
|
|
369
535
|
// ===========================================================================
|
|
370
536
|
// Verify Endpoint (Spatial Features)
|
|
371
537
|
// ===========================================================================
|
|
@@ -622,6 +788,7 @@ var FrameQueue = class {
|
|
|
622
788
|
|
|
623
789
|
// src/types/models.ts
|
|
624
790
|
var MODEL_CONFIGS = {
|
|
791
|
+
// Standard fast-check models
|
|
625
792
|
"10": {
|
|
626
793
|
type: "10",
|
|
627
794
|
minFrames: 10,
|
|
@@ -639,6 +806,104 @@ var MODEL_CONFIGS = {
|
|
|
639
806
|
minFrames: 250,
|
|
640
807
|
recommendedFrames: 250,
|
|
641
808
|
description: "High-accuracy model - 250 frames, best accuracy"
|
|
809
|
+
},
|
|
810
|
+
// Hybrid V2 models with physiological features
|
|
811
|
+
"hybrid-v2-10": {
|
|
812
|
+
type: "hybrid-v2-10",
|
|
813
|
+
minFrames: 10,
|
|
814
|
+
recommendedFrames: 10,
|
|
815
|
+
description: "Hybrid V2 10-frame model with physio features"
|
|
816
|
+
},
|
|
817
|
+
"hybrid-v2-30": {
|
|
818
|
+
type: "hybrid-v2-30",
|
|
819
|
+
minFrames: 30,
|
|
820
|
+
recommendedFrames: 30,
|
|
821
|
+
description: "Hybrid V2 30-frame model with physio features"
|
|
822
|
+
},
|
|
823
|
+
"hybrid-v2-50": {
|
|
824
|
+
type: "hybrid-v2-50",
|
|
825
|
+
minFrames: 50,
|
|
826
|
+
recommendedFrames: 50,
|
|
827
|
+
description: "Hybrid V2 50-frame model with physio features"
|
|
828
|
+
},
|
|
829
|
+
"hybrid-v2-60": {
|
|
830
|
+
type: "hybrid-v2-60",
|
|
831
|
+
minFrames: 60,
|
|
832
|
+
recommendedFrames: 60,
|
|
833
|
+
description: "Hybrid V2 60-frame model with physio features"
|
|
834
|
+
},
|
|
835
|
+
"hybrid-v2-90": {
|
|
836
|
+
type: "hybrid-v2-90",
|
|
837
|
+
minFrames: 90,
|
|
838
|
+
recommendedFrames: 90,
|
|
839
|
+
description: "Hybrid V2 90-frame model with physio features"
|
|
840
|
+
},
|
|
841
|
+
"hybrid-v2-100": {
|
|
842
|
+
type: "hybrid-v2-100",
|
|
843
|
+
minFrames: 100,
|
|
844
|
+
recommendedFrames: 100,
|
|
845
|
+
description: "Hybrid V2 100-frame model with physio features"
|
|
846
|
+
},
|
|
847
|
+
"hybrid-v2-125": {
|
|
848
|
+
type: "hybrid-v2-125",
|
|
849
|
+
minFrames: 125,
|
|
850
|
+
recommendedFrames: 125,
|
|
851
|
+
description: "Hybrid V2 125-frame model with physio features"
|
|
852
|
+
},
|
|
853
|
+
"hybrid-v2-150": {
|
|
854
|
+
type: "hybrid-v2-150",
|
|
855
|
+
minFrames: 150,
|
|
856
|
+
recommendedFrames: 150,
|
|
857
|
+
description: "Hybrid V2 150-frame model with physio features"
|
|
858
|
+
},
|
|
859
|
+
"hybrid-v2-250": {
|
|
860
|
+
type: "hybrid-v2-250",
|
|
861
|
+
minFrames: 250,
|
|
862
|
+
recommendedFrames: 250,
|
|
863
|
+
description: "Hybrid V2 250-frame model with physio features"
|
|
864
|
+
},
|
|
865
|
+
// Mixed models (FastLivenessDetector with mixed-trained weights)
|
|
866
|
+
"mixed-10": {
|
|
867
|
+
type: "mixed-10",
|
|
868
|
+
minFrames: 10,
|
|
869
|
+
recommendedFrames: 10,
|
|
870
|
+
description: "Mixed 10-frame model"
|
|
871
|
+
},
|
|
872
|
+
"mixed-30": {
|
|
873
|
+
type: "mixed-30",
|
|
874
|
+
minFrames: 30,
|
|
875
|
+
recommendedFrames: 30,
|
|
876
|
+
description: "Mixed 30-frame model"
|
|
877
|
+
},
|
|
878
|
+
"mixed-60": {
|
|
879
|
+
type: "mixed-60",
|
|
880
|
+
minFrames: 60,
|
|
881
|
+
recommendedFrames: 60,
|
|
882
|
+
description: "Mixed 60-frame model"
|
|
883
|
+
},
|
|
884
|
+
"mixed-90": {
|
|
885
|
+
type: "mixed-90",
|
|
886
|
+
minFrames: 90,
|
|
887
|
+
recommendedFrames: 90,
|
|
888
|
+
description: "Mixed 90-frame model"
|
|
889
|
+
},
|
|
890
|
+
"mixed-120": {
|
|
891
|
+
type: "mixed-120",
|
|
892
|
+
minFrames: 120,
|
|
893
|
+
recommendedFrames: 120,
|
|
894
|
+
description: "Mixed 120-frame model"
|
|
895
|
+
},
|
|
896
|
+
"mixed-150": {
|
|
897
|
+
type: "mixed-150",
|
|
898
|
+
minFrames: 150,
|
|
899
|
+
recommendedFrames: 150,
|
|
900
|
+
description: "Mixed 150-frame model"
|
|
901
|
+
},
|
|
902
|
+
"mixed-250": {
|
|
903
|
+
type: "mixed-250",
|
|
904
|
+
minFrames: 250,
|
|
905
|
+
recommendedFrames: 250,
|
|
906
|
+
description: "Mixed 250-frame model"
|
|
642
907
|
}
|
|
643
908
|
};
|
|
644
909
|
var HYBRID_MODEL_CONFIGS = {
|
|
@@ -664,6 +929,29 @@ function hasEnoughFrames(model, frameCount) {
|
|
|
664
929
|
return frameCount >= MODEL_CONFIGS[model].minFrames;
|
|
665
930
|
}
|
|
666
931
|
|
|
932
|
+
// src/types/detectors.ts
|
|
933
|
+
var DEFAULT_GAZE_THRESHOLDS = {
|
|
934
|
+
maxYaw: 25,
|
|
935
|
+
maxPitch: 20
|
|
936
|
+
};
|
|
937
|
+
var DEFAULT_HAND_OCCLUSION_CONFIG = {
|
|
938
|
+
faceExpansionFactor: 0.5,
|
|
939
|
+
minLandmarksForOcclusion: 3,
|
|
940
|
+
faceBoxExpiryMs: 1e3,
|
|
941
|
+
centerRegion: 0.6
|
|
942
|
+
};
|
|
943
|
+
var DEFAULT_FACE_DETECTION_TIERS = {
|
|
944
|
+
primaryConfidence: 0.7,
|
|
945
|
+
secondaryConfidence: 0.4
|
|
946
|
+
};
|
|
947
|
+
var DEFAULT_STABILIZER_CONFIG = {
|
|
948
|
+
stabilityThreshold: 1.5,
|
|
949
|
+
requiredStableFrames: 15,
|
|
950
|
+
maxWaitMs: 4e3,
|
|
951
|
+
checkIntervalMs: 100,
|
|
952
|
+
sampleSize: 64
|
|
953
|
+
};
|
|
954
|
+
|
|
667
955
|
// src/constants/feedback.ts
|
|
668
956
|
var ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
|
|
669
957
|
var ALIGNMENT_THRESHOLD_POOR = 0.5;
|
|
@@ -915,15 +1203,15 @@ var MIN_FACE_SIDE_MARGIN = 0.05;
|
|
|
915
1203
|
var MIN_CAPTURE_ALIGNMENT = 0.6;
|
|
916
1204
|
var HIGH_ALIGNMENT = 0.85;
|
|
917
1205
|
var GOOD_ALIGNMENT = 0.5;
|
|
918
|
-
var IDEAL_CROP_MULTIPLIER =
|
|
919
|
-
var MIN_CROP_MULTIPLIER = 1.
|
|
920
|
-
var MAX_CROP_MULTIPLIER =
|
|
921
|
-
var FACE_CENTER_VERTICAL_OFFSET = 0.
|
|
922
|
-
var MIN_FACE_RATIO = 0.
|
|
1206
|
+
var IDEAL_CROP_MULTIPLIER = 2;
|
|
1207
|
+
var MIN_CROP_MULTIPLIER = 1.8;
|
|
1208
|
+
var MAX_CROP_MULTIPLIER = 2.5;
|
|
1209
|
+
var FACE_CENTER_VERTICAL_OFFSET = 0.05;
|
|
1210
|
+
var MIN_FACE_RATIO = 0.036;
|
|
923
1211
|
var MAX_FACE_RATIO = 0.7;
|
|
924
1212
|
var FACE_CROP_OUTPUT_SIZE = 224;
|
|
925
|
-
var MAX_FACE_PERCENTAGE_IN_CROP = 0.
|
|
926
|
-
var TARGET_FACE_PERCENTAGE_IN_CROP = 0.
|
|
1213
|
+
var MAX_FACE_PERCENTAGE_IN_CROP = 0.6;
|
|
1214
|
+
var TARGET_FACE_PERCENTAGE_IN_CROP = 0.5;
|
|
927
1215
|
function analyzeBlur(grayscalePixels, width, height, threshold = DEFAULT_BLUR_THRESHOLD) {
|
|
928
1216
|
const laplacian = [];
|
|
929
1217
|
for (let y = 1; y < height - 1; y++) {
|
|
@@ -1012,10 +1300,10 @@ function isFaceFullyVisible(boundingBox, frameWidth, frameHeight) {
|
|
|
1012
1300
|
var DEFAULT_OVAL_REGION = {
|
|
1013
1301
|
centerX: 0.5,
|
|
1014
1302
|
centerY: 0.5,
|
|
1015
|
-
width: 0.
|
|
1016
|
-
//
|
|
1017
|
-
height: 0.
|
|
1018
|
-
//
|
|
1303
|
+
width: 0.36,
|
|
1304
|
+
// 36% of frame width (+20%)
|
|
1305
|
+
height: 0.48
|
|
1306
|
+
// 36% * (4/3) = 48% of frame height (+20%)
|
|
1019
1307
|
};
|
|
1020
1308
|
function isFaceInOval(faceBox, frameWidth, frameHeight, oval = DEFAULT_OVAL_REGION, tolerance = 0.3) {
|
|
1021
1309
|
const faceCenterX = (faceBox.originX + faceBox.width / 2) / frameWidth;
|
|
@@ -1204,6 +1492,37 @@ var BaseFrameCollector = class {
|
|
|
1204
1492
|
return this.frames.length;
|
|
1205
1493
|
}
|
|
1206
1494
|
};
|
|
1495
|
+
|
|
1496
|
+
// src/utils/landmarkValidator.ts
|
|
1497
|
+
var LANDMARK_INDEX = {
|
|
1498
|
+
/** Nose tip */
|
|
1499
|
+
NOSE_TIP: 1,
|
|
1500
|
+
/** Upper lip center */
|
|
1501
|
+
UPPER_LIP: 13,
|
|
1502
|
+
/** Lower lip center */
|
|
1503
|
+
LOWER_LIP: 14
|
|
1504
|
+
};
|
|
1505
|
+
var LANDMARK_MIN_BOUND = 0.1;
|
|
1506
|
+
var LANDMARK_MAX_BOUND = 0.9;
|
|
1507
|
+
var MIN_LANDMARK_COUNT = 15;
|
|
1508
|
+
function isInBounds(x, y) {
|
|
1509
|
+
return x >= LANDMARK_MIN_BOUND && x <= LANDMARK_MAX_BOUND && y >= LANDMARK_MIN_BOUND && y <= LANDMARK_MAX_BOUND;
|
|
1510
|
+
}
|
|
1511
|
+
function validateFaceLandmarks(landmarks) {
|
|
1512
|
+
if (!landmarks || landmarks.length < MIN_LANDMARK_COUNT) {
|
|
1513
|
+
return { valid: false, message: "Move closer to the camera" };
|
|
1514
|
+
}
|
|
1515
|
+
const noseTip = landmarks[LANDMARK_INDEX.NOSE_TIP];
|
|
1516
|
+
const upperLip = landmarks[LANDMARK_INDEX.UPPER_LIP];
|
|
1517
|
+
const lowerLip = landmarks[LANDMARK_INDEX.LOWER_LIP];
|
|
1518
|
+
if (!noseTip || !isInBounds(noseTip.x, noseTip.y)) {
|
|
1519
|
+
return { valid: false, message: "Make sure your full face is visible" };
|
|
1520
|
+
}
|
|
1521
|
+
if (!upperLip || !lowerLip || !isInBounds(upperLip.x, upperLip.y) || !isInBounds(lowerLip.x, lowerLip.y)) {
|
|
1522
|
+
return { valid: false, message: "Don't cover your mouth" };
|
|
1523
|
+
}
|
|
1524
|
+
return { valid: true };
|
|
1525
|
+
}
|
|
1207
1526
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1208
1527
|
0 && (module.exports = {
|
|
1209
1528
|
ALIGNMENT_THRESHOLD_CAPTURE,
|
|
@@ -1218,9 +1537,13 @@ var BaseFrameCollector = class {
|
|
|
1218
1537
|
BaseFrameCollector,
|
|
1219
1538
|
DEFAULT_BLUR_THRESHOLD,
|
|
1220
1539
|
DEFAULT_ENDPOINT,
|
|
1540
|
+
DEFAULT_FACE_DETECTION_TIERS,
|
|
1541
|
+
DEFAULT_GAZE_THRESHOLDS,
|
|
1542
|
+
DEFAULT_HAND_OCCLUSION_CONFIG,
|
|
1221
1543
|
DEFAULT_LIVENESS_CONFIG,
|
|
1222
1544
|
DEFAULT_LOCALE,
|
|
1223
1545
|
DEFAULT_OVAL_REGION,
|
|
1546
|
+
DEFAULT_STABILIZER_CONFIG,
|
|
1224
1547
|
DEFAULT_STATUS_MESSAGES,
|
|
1225
1548
|
ES_LOCALE,
|
|
1226
1549
|
FACE_CENTER_VERTICAL_OFFSET,
|
|
@@ -1234,6 +1557,9 @@ var BaseFrameCollector = class {
|
|
|
1234
1557
|
HIGH_ALIGNMENT,
|
|
1235
1558
|
HYBRID_MODEL_CONFIGS,
|
|
1236
1559
|
IDEAL_CROP_MULTIPLIER,
|
|
1560
|
+
LANDMARK_INDEX,
|
|
1561
|
+
LANDMARK_MAX_BOUND,
|
|
1562
|
+
LANDMARK_MIN_BOUND,
|
|
1237
1563
|
LOW_LIGHT_THRESHOLD,
|
|
1238
1564
|
LivenessApiError,
|
|
1239
1565
|
LivenessClient,
|
|
@@ -1246,6 +1572,7 @@ var BaseFrameCollector = class {
|
|
|
1246
1572
|
MIN_FACE_RATIO,
|
|
1247
1573
|
MIN_FACE_SIDE_MARGIN,
|
|
1248
1574
|
MIN_FACE_TOP_MARGIN,
|
|
1575
|
+
MIN_LANDMARK_COUNT,
|
|
1249
1576
|
MODEL_CONFIGS,
|
|
1250
1577
|
OVAL_GUIDE_COLORS,
|
|
1251
1578
|
OVAL_GUIDE_STYLES,
|
|
@@ -1277,7 +1604,9 @@ var BaseFrameCollector = class {
|
|
|
1277
1604
|
toFrameData,
|
|
1278
1605
|
toHybridFrameData,
|
|
1279
1606
|
toLivenessResult,
|
|
1607
|
+
toLivenessResultFromStream,
|
|
1280
1608
|
validateApiKey,
|
|
1609
|
+
validateFaceLandmarks,
|
|
1281
1610
|
validateFrameCount,
|
|
1282
1611
|
validateFrameData,
|
|
1283
1612
|
validateFrameIndex,
|