@moveris/shared 2.2.0 → 2.4.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/README.md +62 -0
- package/dist/index.d.mts +56 -2
- package/dist/index.d.ts +56 -2
- package/dist/index.js +270 -23
- package/dist/index.mjs +261 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -426,6 +426,11 @@ const status = getStatusMessage('capturing', DEFAULT_LOCALE);
|
|
|
426
426
|
| `processing` | "Processing..." | API verification in progress |
|
|
427
427
|
| `success` | "Verification complete" | Successful completion |
|
|
428
428
|
| `failed` | "Verification failed" | Failed verification |
|
|
429
|
+
| `eyes_not_visible` | "Eyes not clearly visible" | Eye region featureless |
|
|
430
|
+
| `eyes_shadowed` | "Eyes are in shadow…" | Eye region too dark |
|
|
431
|
+
| `eyes_overexposed` | "Eye region overexposed…" | Eye region too bright |
|
|
432
|
+
| `glasses_glare` | "Glare detected…" | Specular highlights on eyes |
|
|
433
|
+
| `eye_quality_poor` | "Eye region quality is poor" | Generic eye quality failure |
|
|
429
434
|
|
|
430
435
|
---
|
|
431
436
|
|
|
@@ -452,6 +457,59 @@ const color = OVAL_GUIDE_COLORS[state];
|
|
|
452
457
|
|
|
453
458
|
---
|
|
454
459
|
|
|
460
|
+
### Eye Region Quality Analysis
|
|
461
|
+
|
|
462
|
+
Pre-request eye-region quality gate. Platform-agnostic functions that analyze RGBA pixel data from eye regions to detect shadows, glare, occlusion, and poor visibility.
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import {
|
|
466
|
+
checkEyeRegionQuality,
|
|
467
|
+
analyzeEyeRegionBrightness,
|
|
468
|
+
analyzeEyeRegionContrast,
|
|
469
|
+
detectSpecularHighlights,
|
|
470
|
+
EYE_QUALITY_THRESHOLDS,
|
|
471
|
+
} from '@moveris/shared';
|
|
472
|
+
|
|
473
|
+
// Combined quality check (recommended)
|
|
474
|
+
const quality = checkEyeRegionQuality(eyeRegionPixels);
|
|
475
|
+
console.log(quality.passed); // boolean
|
|
476
|
+
console.log(quality.brightness); // 0-255
|
|
477
|
+
console.log(quality.contrast); // standard deviation of luminance
|
|
478
|
+
console.log(quality.hasGlare); // boolean
|
|
479
|
+
console.log(quality.glareRatio); // 0-1
|
|
480
|
+
console.log(quality.message); // user-facing feedback or null
|
|
481
|
+
|
|
482
|
+
// Individual checks
|
|
483
|
+
const brightness = analyzeEyeRegionBrightness(pixels);
|
|
484
|
+
const contrast = analyzeEyeRegionContrast(pixels);
|
|
485
|
+
const glareRatio = detectSpecularHighlights(pixels);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
| Check | Threshold | Condition |
|
|
489
|
+
| ----------- | --------------------- | --------------------------------------- |
|
|
490
|
+
| Shadowed | `minBrightness: 40` | Average luminance too low |
|
|
491
|
+
| Overexposed | `maxBrightness: 230` | Average luminance too high |
|
|
492
|
+
| Glare | `maxGlareRatio: 0.15` | >15% of pixels are specular highlights |
|
|
493
|
+
| Occluded | `minContrast: 12` | Standard deviation of luminance too low |
|
|
494
|
+
|
|
495
|
+
Custom thresholds can be passed as a second argument to `checkEyeRegionQuality()`.
|
|
496
|
+
|
|
497
|
+
### Eye Region Landmarks
|
|
498
|
+
|
|
499
|
+
Extract bounding boxes for left and right eye regions from MediaPipe 468-point face mesh landmarks.
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { getEyeRegionBounds, EYE_LANDMARK_INDICES, validateFaceLandmarks } from '@moveris/shared';
|
|
503
|
+
|
|
504
|
+
const bounds = getEyeRegionBounds(landmarks);
|
|
505
|
+
if (bounds) {
|
|
506
|
+
console.log(bounds.leftEye); // { x, y, width, height } (normalized 0-1)
|
|
507
|
+
console.log(bounds.rightEye); // { x, y, width, height } (normalized 0-1)
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
455
513
|
### Frame Analysis Utilities
|
|
456
514
|
|
|
457
515
|
Utilities for assessing frame quality before submission.
|
|
@@ -527,6 +585,10 @@ import type {
|
|
|
527
585
|
HeadPose,
|
|
528
586
|
FaceLandmarkPoint,
|
|
529
587
|
LandmarkValidationResult,
|
|
588
|
+
EyeRegionQuality,
|
|
589
|
+
EyeQualityThresholds,
|
|
590
|
+
EyeRegionBounds,
|
|
591
|
+
EyeRegionsBounds,
|
|
530
592
|
} from '@moveris/shared';
|
|
531
593
|
```
|
|
532
594
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
type Verdict = 'live' | 'fake';
|
|
2
|
-
|
|
2
|
+
interface ModelEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description: string;
|
|
6
|
+
min_frames: number;
|
|
7
|
+
deprecated: boolean;
|
|
8
|
+
}
|
|
9
|
+
type ModelsResponse = ModelEntry[];
|
|
10
|
+
type FastCheckModel = '10' | '50' | '250' | 'hybrid-v2-10' | 'hybrid-v2-30' | 'hybrid-v2-50' | 'hybrid-v2-60' | 'hybrid-v2-90' | 'hybrid-v2-100' | 'hybrid-v2-125' | 'hybrid-v2-150' | 'hybrid-v2-250' | 'mixed-10' | 'mixed-30' | 'mixed-60' | 'mixed-90' | 'mixed-120' | 'mixed-150' | 'mixed-250' | 'mixed-10-v2' | 'mixed-30-v2' | 'mixed-60-v2' | 'mixed-90-v2' | 'mixed-120-v2' | (string & object);
|
|
3
11
|
type StreamingStatus = 'buffering' | 'complete';
|
|
4
12
|
type FrameSource = 'media' | 'live';
|
|
5
13
|
type JobStatus = 'queued' | 'processing' | 'complete' | 'failed';
|
|
@@ -172,6 +180,7 @@ interface ModelConfig {
|
|
|
172
180
|
minFrames: number;
|
|
173
181
|
recommendedFrames: number;
|
|
174
182
|
description: string;
|
|
183
|
+
deprecated: boolean;
|
|
175
184
|
}
|
|
176
185
|
declare const MODEL_CONFIGS: Record<FastCheckModel, ModelConfig>;
|
|
177
186
|
interface HybridModelConfig {
|
|
@@ -184,6 +193,8 @@ interface HybridModelConfig {
|
|
|
184
193
|
declare const HYBRID_MODEL_CONFIGS: Record<string, HybridModelConfig>;
|
|
185
194
|
declare function getMinFramesForModel(model: FastCheckModel): number;
|
|
186
195
|
declare function hasEnoughFrames(model: FastCheckModel, frameCount: number): boolean;
|
|
196
|
+
declare function isDeprecatedModel(model: FastCheckModel): boolean;
|
|
197
|
+
declare function getActiveModels(): ModelConfig[];
|
|
187
198
|
|
|
188
199
|
interface Frame {
|
|
189
200
|
index: number;
|
|
@@ -322,6 +333,21 @@ declare const LANDMARK_INDEX: {
|
|
|
322
333
|
readonly UPPER_LIP: 13;
|
|
323
334
|
readonly LOWER_LIP: 14;
|
|
324
335
|
};
|
|
336
|
+
declare const EYE_LANDMARK_INDICES: {
|
|
337
|
+
readonly RIGHT_EYE: readonly [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246];
|
|
338
|
+
readonly LEFT_EYE: readonly [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398];
|
|
339
|
+
};
|
|
340
|
+
interface EyeRegionBounds {
|
|
341
|
+
x: number;
|
|
342
|
+
y: number;
|
|
343
|
+
width: number;
|
|
344
|
+
height: number;
|
|
345
|
+
}
|
|
346
|
+
interface EyeRegionsBounds {
|
|
347
|
+
leftEye: EyeRegionBounds;
|
|
348
|
+
rightEye: EyeRegionBounds;
|
|
349
|
+
}
|
|
350
|
+
declare function getEyeRegionBounds(landmarks: FaceLandmarkPoint[]): EyeRegionsBounds | null;
|
|
325
351
|
declare const LANDMARK_MIN_BOUND = 0.1;
|
|
326
352
|
declare const LANDMARK_MAX_BOUND = 0.9;
|
|
327
353
|
declare const MIN_LANDMARK_COUNT = 15;
|
|
@@ -435,6 +461,7 @@ declare class LivenessClient {
|
|
|
435
461
|
private static extractCodeFromText;
|
|
436
462
|
private requestWithRetry;
|
|
437
463
|
health(): Promise<HealthResponse>;
|
|
464
|
+
getModels(): Promise<ModelEntry[]>;
|
|
438
465
|
queueStats(): Promise<QueueStatsResponse>;
|
|
439
466
|
fastCheck(frames: CapturedFrame[], options?: {
|
|
440
467
|
sessionId?: string;
|
|
@@ -531,6 +558,7 @@ declare const API_ENDPOINTS: {
|
|
|
531
558
|
declare const DEFAULT_ENDPOINT: "https://api.moveris.com";
|
|
532
559
|
declare const API_PATHS: {
|
|
533
560
|
readonly health: "/health";
|
|
561
|
+
readonly models: "/api/v1/models";
|
|
534
562
|
readonly fastCheck: "/api/v1/fast-check";
|
|
535
563
|
readonly fastCheckCrops: "/api/v1/fast-check-crops";
|
|
536
564
|
readonly fastCheckStream: "/api/v1/fast-check-stream";
|
|
@@ -655,6 +683,11 @@ declare const FEEDBACK_MESSAGES: {
|
|
|
655
683
|
readonly too_dark: "Low lighting - move to a brighter area";
|
|
656
684
|
readonly backlit: "Backlit - try facing the light source";
|
|
657
685
|
readonly hand_detected: "Remove hand from face";
|
|
686
|
+
readonly eyes_not_visible: "Eyes not clearly visible";
|
|
687
|
+
readonly eyes_shadowed: "Eyes are in shadow - improve lighting";
|
|
688
|
+
readonly eyes_overexposed: "Eye region overexposed - reduce lighting";
|
|
689
|
+
readonly glasses_glare: "Glare detected - adjust angle or remove glasses";
|
|
690
|
+
readonly eye_quality_poor: "Eye region quality is poor";
|
|
658
691
|
readonly capturing: "Capturing...";
|
|
659
692
|
readonly almost_done: "Almost done...";
|
|
660
693
|
readonly verification_complete: "Verification complete";
|
|
@@ -708,4 +741,25 @@ interface RetryOptions {
|
|
|
708
741
|
declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
709
742
|
declare function sleep(ms: number): Promise<void>;
|
|
710
743
|
|
|
711
|
-
|
|
744
|
+
interface EyeRegionQuality {
|
|
745
|
+
passed: boolean;
|
|
746
|
+
brightness: number;
|
|
747
|
+
contrast: number;
|
|
748
|
+
hasGlare: boolean;
|
|
749
|
+
glareRatio: number;
|
|
750
|
+
message: string | null;
|
|
751
|
+
}
|
|
752
|
+
interface EyeQualityThresholds {
|
|
753
|
+
minBrightness: number;
|
|
754
|
+
maxBrightness: number;
|
|
755
|
+
minContrast: number;
|
|
756
|
+
glarePixelThreshold: number;
|
|
757
|
+
maxGlareRatio: number;
|
|
758
|
+
}
|
|
759
|
+
declare const EYE_QUALITY_THRESHOLDS: EyeQualityThresholds;
|
|
760
|
+
declare function analyzeEyeRegionBrightness(pixels: Uint8Array | Uint8ClampedArray): number;
|
|
761
|
+
declare function analyzeEyeRegionContrast(pixels: Uint8Array | Uint8ClampedArray, meanBrightness?: number): number;
|
|
762
|
+
declare function detectSpecularHighlights(pixels: Uint8Array | Uint8ClampedArray, threshold?: number): number;
|
|
763
|
+
declare function checkEyeRegionQuality(pixels: Uint8Array | Uint8ClampedArray, thresholds?: EyeQualityThresholds): EyeRegionQuality;
|
|
764
|
+
|
|
765
|
+
export { ALIGNMENT_THRESHOLD_CAPTURE, ALIGNMENT_THRESHOLD_GOOD, ALIGNMENT_THRESHOLD_PERFECT, ALIGNMENT_THRESHOLD_POOR, API_ENDPOINTS, API_ERROR_CODES, API_PATHS, AUTH_CONFIG, type ApiErrorCode, BACKLIT_RATIO_THRESHOLD, BLUR_THRESHOLD_MOBILE, BaseFrameCollector, type BlurAnalysis, type CaptureQualityState, type CapturedFrame, type CropData, DEFAULT_BLUR_THRESHOLD, DEFAULT_ENDPOINT, DEFAULT_FACE_DETECTION_TIERS, DEFAULT_GAZE_THRESHOLDS, DEFAULT_HAND_OCCLUSION_CONFIG, DEFAULT_LIVENESS_CONFIG, DEFAULT_LOCALE, DEFAULT_OVAL_REGION, DEFAULT_STABILIZER_CONFIG, DEFAULT_STATUS_MESSAGES, type DetectionResult, type DetectionSummary, type DetectorConfig, ERROR_MESSAGES, ERROR_MESSAGES_ES, ES_LOCALE, EYE_LANDMARK_INDICES, EYE_QUALITY_THRESHOLDS, type ErrorResponse, type EyeQualityThresholds, type EyeRegionBounds, type EyeRegionQuality, type EyeRegionsBounds, FACE_CENTER_VERTICAL_OFFSET, FACE_CROP_OUTPUT_SIZE, FEEDBACK_MESSAGES, FRAME_BUFFER_CONFIG, FRAME_CONFIG, type FaceAlignmentResult, type FaceBoundingBox, type FaceDetectionTiers, type FaceInOvalResult, type FaceLandmarkPoint, type FaceVisibilityResult, type FastCheckCropsRequest, type FastCheckModel, type FastCheckRequest, type FastCheckResponse, type FastCheckStreamRequest, type FastCheckStreamResponse, type FeedbackLocale, type FeedbackMessageKey, type Frame, FrameBuffer, type FrameData, type FrameQualityResult, FrameQueue, type FrameSource, GOOD_ALIGNMENT, type GazeThresholds, HIGH_ALIGNMENT, HYBRID_MODEL_CONFIGS, type HandOcclusionConfig, type HeadPose, type HealthResponse, type Hybrid150CheckRequest, type Hybrid50CheckRequest, type HybridCheckRequest, type HybridCheckResponse, type HybridFrameData, type HybridModelConfig, IDEAL_CROP_MULTIPLIER, type JobStatus, type JobStatusResponse, LANDMARK_INDEX, LANDMARK_MAX_BOUND, LANDMARK_MIN_BOUND, LOW_LIGHT_THRESHOLD, type LandmarkValidationResult, type LightingAnalysis, LivenessApiError, type LivenessCallbacks, LivenessClient, type LivenessClientConfig, type LivenessConfig, type LivenessResult, type LivenessState, MAX_CROP_MULTIPLIER, MAX_FACE_PERCENTAGE_IN_CROP, MAX_FACE_RATIO, MIN_CAPTURE_ALIGNMENT, MIN_CROP_MULTIPLIER, MIN_FACE_BOTTOM_MARGIN, MIN_FACE_RATIO, MIN_FACE_SIDE_MARGIN, MIN_FACE_TOP_MARGIN, MIN_LANDMARK_COUNT, MODEL_CONFIGS, type ModelConfig, type ModelEntry, type ModelType, type ModelsResponse, OVAL_GUIDE_COLORS, OVAL_GUIDE_STYLES, type OnErrorCallback, type OnFrameCapturedCallback, type OnProgressCallback, type OnResultCallback, type OnStateChangeCallback, type OvalGuideState, type OvalRegion, type QueueStatsResponse, RETRY_CONFIG, type RetryOptions, type StabilizationProgress, type StabilizationResult, type StabilizerConfig, type StatusMessageKey, type StreamingStatus, TARGET_FACE_PERCENTAGE_IN_CROP, type Verdict, type VerifyRequest, type VerifyResponse, type VideoFrameMetadata, analyzeBlur, analyzeEyeRegionBrightness, analyzeEyeRegionContrast, analyzeLighting, calculateAdaptiveCropMultiplier, calculateBrightness, calculateFaceAlignment, calculateFaceCropRegion, canCaptureFrame, checkEyeRegionQuality, checkFrameQuality, decodeBase64, detectSpecularHighlights, encodeBase64, generateSessionId, getActiveModels, getApiErrorMessage, getCaptureQualityFeedback, getEyeRegionBounds, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isDeprecatedModel, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, isRetryableError, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
type Verdict = 'live' | 'fake';
|
|
2
|
-
|
|
2
|
+
interface ModelEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description: string;
|
|
6
|
+
min_frames: number;
|
|
7
|
+
deprecated: boolean;
|
|
8
|
+
}
|
|
9
|
+
type ModelsResponse = ModelEntry[];
|
|
10
|
+
type FastCheckModel = '10' | '50' | '250' | 'hybrid-v2-10' | 'hybrid-v2-30' | 'hybrid-v2-50' | 'hybrid-v2-60' | 'hybrid-v2-90' | 'hybrid-v2-100' | 'hybrid-v2-125' | 'hybrid-v2-150' | 'hybrid-v2-250' | 'mixed-10' | 'mixed-30' | 'mixed-60' | 'mixed-90' | 'mixed-120' | 'mixed-150' | 'mixed-250' | 'mixed-10-v2' | 'mixed-30-v2' | 'mixed-60-v2' | 'mixed-90-v2' | 'mixed-120-v2' | (string & object);
|
|
3
11
|
type StreamingStatus = 'buffering' | 'complete';
|
|
4
12
|
type FrameSource = 'media' | 'live';
|
|
5
13
|
type JobStatus = 'queued' | 'processing' | 'complete' | 'failed';
|
|
@@ -172,6 +180,7 @@ interface ModelConfig {
|
|
|
172
180
|
minFrames: number;
|
|
173
181
|
recommendedFrames: number;
|
|
174
182
|
description: string;
|
|
183
|
+
deprecated: boolean;
|
|
175
184
|
}
|
|
176
185
|
declare const MODEL_CONFIGS: Record<FastCheckModel, ModelConfig>;
|
|
177
186
|
interface HybridModelConfig {
|
|
@@ -184,6 +193,8 @@ interface HybridModelConfig {
|
|
|
184
193
|
declare const HYBRID_MODEL_CONFIGS: Record<string, HybridModelConfig>;
|
|
185
194
|
declare function getMinFramesForModel(model: FastCheckModel): number;
|
|
186
195
|
declare function hasEnoughFrames(model: FastCheckModel, frameCount: number): boolean;
|
|
196
|
+
declare function isDeprecatedModel(model: FastCheckModel): boolean;
|
|
197
|
+
declare function getActiveModels(): ModelConfig[];
|
|
187
198
|
|
|
188
199
|
interface Frame {
|
|
189
200
|
index: number;
|
|
@@ -322,6 +333,21 @@ declare const LANDMARK_INDEX: {
|
|
|
322
333
|
readonly UPPER_LIP: 13;
|
|
323
334
|
readonly LOWER_LIP: 14;
|
|
324
335
|
};
|
|
336
|
+
declare const EYE_LANDMARK_INDICES: {
|
|
337
|
+
readonly RIGHT_EYE: readonly [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246];
|
|
338
|
+
readonly LEFT_EYE: readonly [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398];
|
|
339
|
+
};
|
|
340
|
+
interface EyeRegionBounds {
|
|
341
|
+
x: number;
|
|
342
|
+
y: number;
|
|
343
|
+
width: number;
|
|
344
|
+
height: number;
|
|
345
|
+
}
|
|
346
|
+
interface EyeRegionsBounds {
|
|
347
|
+
leftEye: EyeRegionBounds;
|
|
348
|
+
rightEye: EyeRegionBounds;
|
|
349
|
+
}
|
|
350
|
+
declare function getEyeRegionBounds(landmarks: FaceLandmarkPoint[]): EyeRegionsBounds | null;
|
|
325
351
|
declare const LANDMARK_MIN_BOUND = 0.1;
|
|
326
352
|
declare const LANDMARK_MAX_BOUND = 0.9;
|
|
327
353
|
declare const MIN_LANDMARK_COUNT = 15;
|
|
@@ -435,6 +461,7 @@ declare class LivenessClient {
|
|
|
435
461
|
private static extractCodeFromText;
|
|
436
462
|
private requestWithRetry;
|
|
437
463
|
health(): Promise<HealthResponse>;
|
|
464
|
+
getModels(): Promise<ModelEntry[]>;
|
|
438
465
|
queueStats(): Promise<QueueStatsResponse>;
|
|
439
466
|
fastCheck(frames: CapturedFrame[], options?: {
|
|
440
467
|
sessionId?: string;
|
|
@@ -531,6 +558,7 @@ declare const API_ENDPOINTS: {
|
|
|
531
558
|
declare const DEFAULT_ENDPOINT: "https://api.moveris.com";
|
|
532
559
|
declare const API_PATHS: {
|
|
533
560
|
readonly health: "/health";
|
|
561
|
+
readonly models: "/api/v1/models";
|
|
534
562
|
readonly fastCheck: "/api/v1/fast-check";
|
|
535
563
|
readonly fastCheckCrops: "/api/v1/fast-check-crops";
|
|
536
564
|
readonly fastCheckStream: "/api/v1/fast-check-stream";
|
|
@@ -655,6 +683,11 @@ declare const FEEDBACK_MESSAGES: {
|
|
|
655
683
|
readonly too_dark: "Low lighting - move to a brighter area";
|
|
656
684
|
readonly backlit: "Backlit - try facing the light source";
|
|
657
685
|
readonly hand_detected: "Remove hand from face";
|
|
686
|
+
readonly eyes_not_visible: "Eyes not clearly visible";
|
|
687
|
+
readonly eyes_shadowed: "Eyes are in shadow - improve lighting";
|
|
688
|
+
readonly eyes_overexposed: "Eye region overexposed - reduce lighting";
|
|
689
|
+
readonly glasses_glare: "Glare detected - adjust angle or remove glasses";
|
|
690
|
+
readonly eye_quality_poor: "Eye region quality is poor";
|
|
658
691
|
readonly capturing: "Capturing...";
|
|
659
692
|
readonly almost_done: "Almost done...";
|
|
660
693
|
readonly verification_complete: "Verification complete";
|
|
@@ -708,4 +741,25 @@ interface RetryOptions {
|
|
|
708
741
|
declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
709
742
|
declare function sleep(ms: number): Promise<void>;
|
|
710
743
|
|
|
711
|
-
|
|
744
|
+
interface EyeRegionQuality {
|
|
745
|
+
passed: boolean;
|
|
746
|
+
brightness: number;
|
|
747
|
+
contrast: number;
|
|
748
|
+
hasGlare: boolean;
|
|
749
|
+
glareRatio: number;
|
|
750
|
+
message: string | null;
|
|
751
|
+
}
|
|
752
|
+
interface EyeQualityThresholds {
|
|
753
|
+
minBrightness: number;
|
|
754
|
+
maxBrightness: number;
|
|
755
|
+
minContrast: number;
|
|
756
|
+
glarePixelThreshold: number;
|
|
757
|
+
maxGlareRatio: number;
|
|
758
|
+
}
|
|
759
|
+
declare const EYE_QUALITY_THRESHOLDS: EyeQualityThresholds;
|
|
760
|
+
declare function analyzeEyeRegionBrightness(pixels: Uint8Array | Uint8ClampedArray): number;
|
|
761
|
+
declare function analyzeEyeRegionContrast(pixels: Uint8Array | Uint8ClampedArray, meanBrightness?: number): number;
|
|
762
|
+
declare function detectSpecularHighlights(pixels: Uint8Array | Uint8ClampedArray, threshold?: number): number;
|
|
763
|
+
declare function checkEyeRegionQuality(pixels: Uint8Array | Uint8ClampedArray, thresholds?: EyeQualityThresholds): EyeRegionQuality;
|
|
764
|
+
|
|
765
|
+
export { ALIGNMENT_THRESHOLD_CAPTURE, ALIGNMENT_THRESHOLD_GOOD, ALIGNMENT_THRESHOLD_PERFECT, ALIGNMENT_THRESHOLD_POOR, API_ENDPOINTS, API_ERROR_CODES, API_PATHS, AUTH_CONFIG, type ApiErrorCode, BACKLIT_RATIO_THRESHOLD, BLUR_THRESHOLD_MOBILE, BaseFrameCollector, type BlurAnalysis, type CaptureQualityState, type CapturedFrame, type CropData, DEFAULT_BLUR_THRESHOLD, DEFAULT_ENDPOINT, DEFAULT_FACE_DETECTION_TIERS, DEFAULT_GAZE_THRESHOLDS, DEFAULT_HAND_OCCLUSION_CONFIG, DEFAULT_LIVENESS_CONFIG, DEFAULT_LOCALE, DEFAULT_OVAL_REGION, DEFAULT_STABILIZER_CONFIG, DEFAULT_STATUS_MESSAGES, type DetectionResult, type DetectionSummary, type DetectorConfig, ERROR_MESSAGES, ERROR_MESSAGES_ES, ES_LOCALE, EYE_LANDMARK_INDICES, EYE_QUALITY_THRESHOLDS, type ErrorResponse, type EyeQualityThresholds, type EyeRegionBounds, type EyeRegionQuality, type EyeRegionsBounds, FACE_CENTER_VERTICAL_OFFSET, FACE_CROP_OUTPUT_SIZE, FEEDBACK_MESSAGES, FRAME_BUFFER_CONFIG, FRAME_CONFIG, type FaceAlignmentResult, type FaceBoundingBox, type FaceDetectionTiers, type FaceInOvalResult, type FaceLandmarkPoint, type FaceVisibilityResult, type FastCheckCropsRequest, type FastCheckModel, type FastCheckRequest, type FastCheckResponse, type FastCheckStreamRequest, type FastCheckStreamResponse, type FeedbackLocale, type FeedbackMessageKey, type Frame, FrameBuffer, type FrameData, type FrameQualityResult, FrameQueue, type FrameSource, GOOD_ALIGNMENT, type GazeThresholds, HIGH_ALIGNMENT, HYBRID_MODEL_CONFIGS, type HandOcclusionConfig, type HeadPose, type HealthResponse, type Hybrid150CheckRequest, type Hybrid50CheckRequest, type HybridCheckRequest, type HybridCheckResponse, type HybridFrameData, type HybridModelConfig, IDEAL_CROP_MULTIPLIER, type JobStatus, type JobStatusResponse, LANDMARK_INDEX, LANDMARK_MAX_BOUND, LANDMARK_MIN_BOUND, LOW_LIGHT_THRESHOLD, type LandmarkValidationResult, type LightingAnalysis, LivenessApiError, type LivenessCallbacks, LivenessClient, type LivenessClientConfig, type LivenessConfig, type LivenessResult, type LivenessState, MAX_CROP_MULTIPLIER, MAX_FACE_PERCENTAGE_IN_CROP, MAX_FACE_RATIO, MIN_CAPTURE_ALIGNMENT, MIN_CROP_MULTIPLIER, MIN_FACE_BOTTOM_MARGIN, MIN_FACE_RATIO, MIN_FACE_SIDE_MARGIN, MIN_FACE_TOP_MARGIN, MIN_LANDMARK_COUNT, MODEL_CONFIGS, type ModelConfig, type ModelEntry, type ModelType, type ModelsResponse, OVAL_GUIDE_COLORS, OVAL_GUIDE_STYLES, type OnErrorCallback, type OnFrameCapturedCallback, type OnProgressCallback, type OnResultCallback, type OnStateChangeCallback, type OvalGuideState, type OvalRegion, type QueueStatsResponse, RETRY_CONFIG, type RetryOptions, type StabilizationProgress, type StabilizationResult, type StabilizerConfig, type StatusMessageKey, type StreamingStatus, TARGET_FACE_PERCENTAGE_IN_CROP, type Verdict, type VerifyRequest, type VerifyResponse, type VideoFrameMetadata, analyzeBlur, analyzeEyeRegionBrightness, analyzeEyeRegionContrast, analyzeLighting, calculateAdaptiveCropMultiplier, calculateBrightness, calculateFaceAlignment, calculateFaceCropRegion, canCaptureFrame, checkEyeRegionQuality, checkFrameQuality, decodeBase64, detectSpecularHighlights, encodeBase64, generateSessionId, getActiveModels, getApiErrorMessage, getCaptureQualityFeedback, getEyeRegionBounds, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isDeprecatedModel, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, isRetryableError, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,8 @@ __export(index_exports, {
|
|
|
44
44
|
ERROR_MESSAGES: () => ERROR_MESSAGES,
|
|
45
45
|
ERROR_MESSAGES_ES: () => ERROR_MESSAGES_ES,
|
|
46
46
|
ES_LOCALE: () => ES_LOCALE,
|
|
47
|
+
EYE_LANDMARK_INDICES: () => EYE_LANDMARK_INDICES,
|
|
48
|
+
EYE_QUALITY_THRESHOLDS: () => EYE_QUALITY_THRESHOLDS,
|
|
47
49
|
FACE_CENTER_VERTICAL_OFFSET: () => FACE_CENTER_VERTICAL_OFFSET,
|
|
48
50
|
FACE_CROP_OUTPUT_SIZE: () => FACE_CROP_OUTPUT_SIZE,
|
|
49
51
|
FEEDBACK_MESSAGES: () => FEEDBACK_MESSAGES,
|
|
@@ -77,23 +79,30 @@ __export(index_exports, {
|
|
|
77
79
|
RETRY_CONFIG: () => RETRY_CONFIG,
|
|
78
80
|
TARGET_FACE_PERCENTAGE_IN_CROP: () => TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
79
81
|
analyzeBlur: () => analyzeBlur,
|
|
82
|
+
analyzeEyeRegionBrightness: () => analyzeEyeRegionBrightness,
|
|
83
|
+
analyzeEyeRegionContrast: () => analyzeEyeRegionContrast,
|
|
80
84
|
analyzeLighting: () => analyzeLighting,
|
|
81
85
|
calculateAdaptiveCropMultiplier: () => calculateAdaptiveCropMultiplier,
|
|
82
86
|
calculateBrightness: () => calculateBrightness,
|
|
83
87
|
calculateFaceAlignment: () => calculateFaceAlignment,
|
|
84
88
|
calculateFaceCropRegion: () => calculateFaceCropRegion,
|
|
85
89
|
canCaptureFrame: () => canCaptureFrame,
|
|
90
|
+
checkEyeRegionQuality: () => checkEyeRegionQuality,
|
|
86
91
|
checkFrameQuality: () => checkFrameQuality,
|
|
87
92
|
decodeBase64: () => decodeBase64,
|
|
93
|
+
detectSpecularHighlights: () => detectSpecularHighlights,
|
|
88
94
|
encodeBase64: () => encodeBase64,
|
|
89
95
|
generateSessionId: () => generateSessionId,
|
|
96
|
+
getActiveModels: () => getActiveModels,
|
|
90
97
|
getApiErrorMessage: () => getApiErrorMessage,
|
|
91
98
|
getCaptureQualityFeedback: () => getCaptureQualityFeedback,
|
|
99
|
+
getEyeRegionBounds: () => getEyeRegionBounds,
|
|
92
100
|
getFeedbackMessage: () => getFeedbackMessage,
|
|
93
101
|
getMinFramesForModel: () => getMinFramesForModel,
|
|
94
102
|
getOvalGuideState: () => getOvalGuideState,
|
|
95
103
|
getStatusMessage: () => getStatusMessage,
|
|
96
104
|
hasEnoughFrames: () => hasEnoughFrames,
|
|
105
|
+
isDeprecatedModel: () => isDeprecatedModel,
|
|
97
106
|
isFaceCropFullyInFrame: () => isFaceCropFullyInFrame,
|
|
98
107
|
isFaceFullyVisible: () => isFaceFullyVisible,
|
|
99
108
|
isFaceInOval: () => isFaceInOval,
|
|
@@ -125,6 +134,7 @@ var API_ENDPOINTS = {
|
|
|
125
134
|
var DEFAULT_ENDPOINT = API_ENDPOINTS.production;
|
|
126
135
|
var API_PATHS = {
|
|
127
136
|
health: "/health",
|
|
137
|
+
models: "/api/v1/models",
|
|
128
138
|
fastCheck: "/api/v1/fast-check",
|
|
129
139
|
fastCheckCrops: "/api/v1/fast-check-crops",
|
|
130
140
|
fastCheckStream: "/api/v1/fast-check-stream",
|
|
@@ -412,6 +422,14 @@ var LivenessClient = class _LivenessClient {
|
|
|
412
422
|
async health() {
|
|
413
423
|
return this.request(API_PATHS.health);
|
|
414
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Fetch the full model registry from the API.
|
|
427
|
+
* Includes all models with their deprecated status.
|
|
428
|
+
* Use this instead of the local MODEL_CONFIGS for a live list.
|
|
429
|
+
*/
|
|
430
|
+
async getModels() {
|
|
431
|
+
return this.request(API_PATHS.models);
|
|
432
|
+
}
|
|
415
433
|
/**
|
|
416
434
|
* Get queue statistics
|
|
417
435
|
*/
|
|
@@ -861,117 +879,172 @@ var MODEL_CONFIGS = {
|
|
|
861
879
|
type: "10",
|
|
862
880
|
minFrames: 10,
|
|
863
881
|
recommendedFrames: 10,
|
|
864
|
-
description: "Fast model - 10 frames, quick verification"
|
|
882
|
+
description: "Fast model - 10 frames, quick verification",
|
|
883
|
+
deprecated: false
|
|
865
884
|
},
|
|
866
885
|
"50": {
|
|
867
886
|
type: "50",
|
|
868
887
|
minFrames: 50,
|
|
869
888
|
recommendedFrames: 50,
|
|
870
|
-
description: "Balanced model - 50 frames, good accuracy"
|
|
889
|
+
description: "Balanced model - 50 frames, good accuracy",
|
|
890
|
+
deprecated: false
|
|
871
891
|
},
|
|
872
892
|
"250": {
|
|
873
893
|
type: "250",
|
|
874
894
|
minFrames: 250,
|
|
875
895
|
recommendedFrames: 250,
|
|
876
|
-
description: "High-accuracy model - 250 frames, best accuracy"
|
|
896
|
+
description: "High-accuracy model - 250 frames, best accuracy",
|
|
897
|
+
deprecated: false
|
|
877
898
|
},
|
|
878
899
|
// Hybrid V2 models with physiological features
|
|
879
900
|
"hybrid-v2-10": {
|
|
880
901
|
type: "hybrid-v2-10",
|
|
881
902
|
minFrames: 10,
|
|
882
903
|
recommendedFrames: 10,
|
|
883
|
-
description: "Hybrid V2 10-frame model with physio features"
|
|
904
|
+
description: "Hybrid V2 10-frame model with physio features",
|
|
905
|
+
deprecated: false
|
|
884
906
|
},
|
|
885
907
|
"hybrid-v2-30": {
|
|
886
908
|
type: "hybrid-v2-30",
|
|
887
909
|
minFrames: 30,
|
|
888
910
|
recommendedFrames: 30,
|
|
889
|
-
description: "Hybrid V2 30-frame model with physio features"
|
|
911
|
+
description: "Hybrid V2 30-frame model with physio features",
|
|
912
|
+
deprecated: false
|
|
890
913
|
},
|
|
891
914
|
"hybrid-v2-50": {
|
|
892
915
|
type: "hybrid-v2-50",
|
|
893
916
|
minFrames: 50,
|
|
894
917
|
recommendedFrames: 50,
|
|
895
|
-
description: "Hybrid V2 50-frame model with physio features"
|
|
918
|
+
description: "Hybrid V2 50-frame model with physio features",
|
|
919
|
+
deprecated: false
|
|
896
920
|
},
|
|
897
921
|
"hybrid-v2-60": {
|
|
898
922
|
type: "hybrid-v2-60",
|
|
899
923
|
minFrames: 60,
|
|
900
924
|
recommendedFrames: 60,
|
|
901
|
-
description: "Hybrid V2 60-frame model with physio features"
|
|
925
|
+
description: "Hybrid V2 60-frame model with physio features",
|
|
926
|
+
deprecated: false
|
|
902
927
|
},
|
|
903
928
|
"hybrid-v2-90": {
|
|
904
929
|
type: "hybrid-v2-90",
|
|
905
930
|
minFrames: 90,
|
|
906
931
|
recommendedFrames: 90,
|
|
907
|
-
description: "Hybrid V2 90-frame model with physio features"
|
|
932
|
+
description: "Hybrid V2 90-frame model with physio features",
|
|
933
|
+
deprecated: false
|
|
908
934
|
},
|
|
909
935
|
"hybrid-v2-100": {
|
|
910
936
|
type: "hybrid-v2-100",
|
|
911
937
|
minFrames: 100,
|
|
912
938
|
recommendedFrames: 100,
|
|
913
|
-
description: "Hybrid V2 100-frame model with physio features"
|
|
939
|
+
description: "Hybrid V2 100-frame model with physio features",
|
|
940
|
+
deprecated: false
|
|
914
941
|
},
|
|
915
942
|
"hybrid-v2-125": {
|
|
916
943
|
type: "hybrid-v2-125",
|
|
917
944
|
minFrames: 125,
|
|
918
945
|
recommendedFrames: 125,
|
|
919
|
-
description: "Hybrid V2 125-frame model with physio features"
|
|
946
|
+
description: "Hybrid V2 125-frame model with physio features",
|
|
947
|
+
deprecated: false
|
|
920
948
|
},
|
|
921
949
|
"hybrid-v2-150": {
|
|
922
950
|
type: "hybrid-v2-150",
|
|
923
951
|
minFrames: 150,
|
|
924
952
|
recommendedFrames: 150,
|
|
925
|
-
description: "Hybrid V2 150-frame model with physio features"
|
|
953
|
+
description: "Hybrid V2 150-frame model with physio features",
|
|
954
|
+
deprecated: false
|
|
926
955
|
},
|
|
927
956
|
"hybrid-v2-250": {
|
|
928
957
|
type: "hybrid-v2-250",
|
|
929
958
|
minFrames: 250,
|
|
930
959
|
recommendedFrames: 250,
|
|
931
|
-
description: "Hybrid V2 250-frame model with physio features"
|
|
960
|
+
description: "Hybrid V2 250-frame model with physio features",
|
|
961
|
+
deprecated: false
|
|
932
962
|
},
|
|
933
|
-
// Mixed models
|
|
963
|
+
// Mixed V1 models — deprecated, use mixed-*-v2 equivalents instead
|
|
934
964
|
"mixed-10": {
|
|
935
965
|
type: "mixed-10",
|
|
936
966
|
minFrames: 10,
|
|
937
967
|
recommendedFrames: 10,
|
|
938
|
-
description: "Mixed 10-frame model"
|
|
968
|
+
description: "Mixed 10-frame model",
|
|
969
|
+
deprecated: true
|
|
939
970
|
},
|
|
940
971
|
"mixed-30": {
|
|
941
972
|
type: "mixed-30",
|
|
942
973
|
minFrames: 30,
|
|
943
974
|
recommendedFrames: 30,
|
|
944
|
-
description: "Mixed 30-frame model"
|
|
975
|
+
description: "Mixed 30-frame model",
|
|
976
|
+
deprecated: true
|
|
945
977
|
},
|
|
946
978
|
"mixed-60": {
|
|
947
979
|
type: "mixed-60",
|
|
948
980
|
minFrames: 60,
|
|
949
981
|
recommendedFrames: 60,
|
|
950
|
-
description: "Mixed 60-frame model"
|
|
982
|
+
description: "Mixed 60-frame model",
|
|
983
|
+
deprecated: true
|
|
951
984
|
},
|
|
952
985
|
"mixed-90": {
|
|
953
986
|
type: "mixed-90",
|
|
954
987
|
minFrames: 90,
|
|
955
988
|
recommendedFrames: 90,
|
|
956
|
-
description: "Mixed 90-frame model"
|
|
989
|
+
description: "Mixed 90-frame model",
|
|
990
|
+
deprecated: true
|
|
957
991
|
},
|
|
958
992
|
"mixed-120": {
|
|
959
993
|
type: "mixed-120",
|
|
960
994
|
minFrames: 120,
|
|
961
995
|
recommendedFrames: 120,
|
|
962
|
-
description: "Mixed 120-frame model"
|
|
996
|
+
description: "Mixed 120-frame model",
|
|
997
|
+
deprecated: true
|
|
963
998
|
},
|
|
964
999
|
"mixed-150": {
|
|
965
1000
|
type: "mixed-150",
|
|
966
1001
|
minFrames: 150,
|
|
967
1002
|
recommendedFrames: 150,
|
|
968
|
-
description: "Mixed 150-frame model"
|
|
1003
|
+
description: "Mixed 150-frame model",
|
|
1004
|
+
deprecated: true
|
|
969
1005
|
},
|
|
970
1006
|
"mixed-250": {
|
|
971
1007
|
type: "mixed-250",
|
|
972
1008
|
minFrames: 250,
|
|
973
1009
|
recommendedFrames: 250,
|
|
974
|
-
description: "Mixed 250-frame model"
|
|
1010
|
+
description: "Mixed 250-frame model",
|
|
1011
|
+
deprecated: true
|
|
1012
|
+
},
|
|
1013
|
+
// Mixed V2 models (exp_042 checkpoints — EER: 4.0% / AUC: 0.991 at 30f)
|
|
1014
|
+
"mixed-10-v2": {
|
|
1015
|
+
type: "mixed-10-v2",
|
|
1016
|
+
minFrames: 10,
|
|
1017
|
+
recommendedFrames: 10,
|
|
1018
|
+
description: "Mixed V2 10-frame model",
|
|
1019
|
+
deprecated: false
|
|
1020
|
+
},
|
|
1021
|
+
"mixed-30-v2": {
|
|
1022
|
+
type: "mixed-30-v2",
|
|
1023
|
+
minFrames: 30,
|
|
1024
|
+
recommendedFrames: 30,
|
|
1025
|
+
description: "Mixed V2 30-frame model (recommended \u2014 95.7% balanced accuracy)",
|
|
1026
|
+
deprecated: false
|
|
1027
|
+
},
|
|
1028
|
+
"mixed-60-v2": {
|
|
1029
|
+
type: "mixed-60-v2",
|
|
1030
|
+
minFrames: 60,
|
|
1031
|
+
recommendedFrames: 60,
|
|
1032
|
+
description: "Mixed V2 60-frame model",
|
|
1033
|
+
deprecated: false
|
|
1034
|
+
},
|
|
1035
|
+
"mixed-90-v2": {
|
|
1036
|
+
type: "mixed-90-v2",
|
|
1037
|
+
minFrames: 90,
|
|
1038
|
+
recommendedFrames: 90,
|
|
1039
|
+
description: "Mixed V2 90-frame model",
|
|
1040
|
+
deprecated: false
|
|
1041
|
+
},
|
|
1042
|
+
"mixed-120-v2": {
|
|
1043
|
+
type: "mixed-120-v2",
|
|
1044
|
+
minFrames: 120,
|
|
1045
|
+
recommendedFrames: 120,
|
|
1046
|
+
description: "Mixed V2 120-frame model",
|
|
1047
|
+
deprecated: false
|
|
975
1048
|
}
|
|
976
1049
|
};
|
|
977
1050
|
var HYBRID_MODEL_CONFIGS = {
|
|
@@ -991,10 +1064,16 @@ var HYBRID_MODEL_CONFIGS = {
|
|
|
991
1064
|
}
|
|
992
1065
|
};
|
|
993
1066
|
function getMinFramesForModel(model) {
|
|
994
|
-
return MODEL_CONFIGS[model]
|
|
1067
|
+
return MODEL_CONFIGS[model]?.minFrames ?? 10;
|
|
995
1068
|
}
|
|
996
1069
|
function hasEnoughFrames(model, frameCount) {
|
|
997
|
-
return frameCount >=
|
|
1070
|
+
return frameCount >= getMinFramesForModel(model);
|
|
1071
|
+
}
|
|
1072
|
+
function isDeprecatedModel(model) {
|
|
1073
|
+
return MODEL_CONFIGS[model]?.deprecated ?? false;
|
|
1074
|
+
}
|
|
1075
|
+
function getActiveModels() {
|
|
1076
|
+
return Object.values(MODEL_CONFIGS).filter((c) => !c.deprecated);
|
|
998
1077
|
}
|
|
999
1078
|
|
|
1000
1079
|
// src/types/detectors.ts
|
|
@@ -1161,6 +1240,12 @@ var FEEDBACK_MESSAGES = {
|
|
|
1161
1240
|
backlit: "Backlit - try facing the light source",
|
|
1162
1241
|
// Hand occlusion
|
|
1163
1242
|
hand_detected: "Remove hand from face",
|
|
1243
|
+
// Eye region quality
|
|
1244
|
+
eyes_not_visible: "Eyes not clearly visible",
|
|
1245
|
+
eyes_shadowed: "Eyes are in shadow - improve lighting",
|
|
1246
|
+
eyes_overexposed: "Eye region overexposed - reduce lighting",
|
|
1247
|
+
glasses_glare: "Glare detected - adjust angle or remove glasses",
|
|
1248
|
+
eye_quality_poor: "Eye region quality is poor",
|
|
1164
1249
|
// Progress messages
|
|
1165
1250
|
capturing: "Capturing...",
|
|
1166
1251
|
almost_done: "Almost done...",
|
|
@@ -1212,6 +1297,12 @@ var ES_LOCALE = {
|
|
|
1212
1297
|
backlit: "Contraluz - intenta mirar hacia la fuente de luz",
|
|
1213
1298
|
// Hand occlusion
|
|
1214
1299
|
hand_detected: "Retira la mano del rostro",
|
|
1300
|
+
// Eye region quality
|
|
1301
|
+
eyes_not_visible: "Ojos no visibles claramente",
|
|
1302
|
+
eyes_shadowed: "Ojos en sombra - mejora la iluminaci\xF3n",
|
|
1303
|
+
eyes_overexposed: "Regi\xF3n de ojos sobreexpuesta - reduce la iluminaci\xF3n",
|
|
1304
|
+
glasses_glare: "Reflejo detectado - ajusta el \xE1ngulo o retira los lentes",
|
|
1305
|
+
eye_quality_poor: "Calidad de la regi\xF3n de ojos insuficiente",
|
|
1215
1306
|
// Progress messages
|
|
1216
1307
|
capturing: "Capturando...",
|
|
1217
1308
|
almost_done: "Casi listo...",
|
|
@@ -1295,7 +1386,7 @@ function validateTimestamp(timestamp) {
|
|
|
1295
1386
|
}
|
|
1296
1387
|
function validateFrameCount(model, frameCount) {
|
|
1297
1388
|
const config = MODEL_CONFIGS[model];
|
|
1298
|
-
const required = config
|
|
1389
|
+
const required = config?.minFrames ?? 10;
|
|
1299
1390
|
return {
|
|
1300
1391
|
valid: frameCount >= required,
|
|
1301
1392
|
required,
|
|
@@ -1645,6 +1736,44 @@ var LANDMARK_INDEX = {
|
|
|
1645
1736
|
/** Lower lip center */
|
|
1646
1737
|
LOWER_LIP: 14
|
|
1647
1738
|
};
|
|
1739
|
+
var EYE_LANDMARK_INDICES = {
|
|
1740
|
+
/** Right eye contour (viewer's left) */
|
|
1741
|
+
RIGHT_EYE: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
|
|
1742
|
+
/** Left eye contour (viewer's right) */
|
|
1743
|
+
LEFT_EYE: [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398]
|
|
1744
|
+
};
|
|
1745
|
+
function getEyeRegionBounds(landmarks) {
|
|
1746
|
+
if (landmarks.length < 468) return null;
|
|
1747
|
+
const PADDING = 0.2;
|
|
1748
|
+
function computeBounds(indices) {
|
|
1749
|
+
let minX = 1;
|
|
1750
|
+
let minY = 1;
|
|
1751
|
+
let maxX = 0;
|
|
1752
|
+
let maxY = 0;
|
|
1753
|
+
for (const idx of indices) {
|
|
1754
|
+
const lm = landmarks[idx];
|
|
1755
|
+
if (!lm) continue;
|
|
1756
|
+
if (lm.x < minX) minX = lm.x;
|
|
1757
|
+
if (lm.y < minY) minY = lm.y;
|
|
1758
|
+
if (lm.x > maxX) maxX = lm.x;
|
|
1759
|
+
if (lm.y > maxY) maxY = lm.y;
|
|
1760
|
+
}
|
|
1761
|
+
const width = maxX - minX;
|
|
1762
|
+
const height = maxY - minY;
|
|
1763
|
+
const padX = width * PADDING;
|
|
1764
|
+
const padY = height * PADDING;
|
|
1765
|
+
return {
|
|
1766
|
+
x: Math.max(0, minX - padX),
|
|
1767
|
+
y: Math.max(0, minY - padY),
|
|
1768
|
+
width: Math.min(1 - Math.max(0, minX - padX), width + padX * 2),
|
|
1769
|
+
height: Math.min(1 - Math.max(0, minY - padY), height + padY * 2)
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
return {
|
|
1773
|
+
rightEye: computeBounds(EYE_LANDMARK_INDICES.RIGHT_EYE),
|
|
1774
|
+
leftEye: computeBounds(EYE_LANDMARK_INDICES.LEFT_EYE)
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1648
1777
|
var LANDMARK_MIN_BOUND = 0.1;
|
|
1649
1778
|
var LANDMARK_MAX_BOUND = 0.9;
|
|
1650
1779
|
var MIN_LANDMARK_COUNT = 15;
|
|
@@ -1666,6 +1795,115 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1666
1795
|
}
|
|
1667
1796
|
return { valid: true };
|
|
1668
1797
|
}
|
|
1798
|
+
|
|
1799
|
+
// src/utils/eyeRegionAnalysis.ts
|
|
1800
|
+
var EYE_QUALITY_THRESHOLDS = {
|
|
1801
|
+
minBrightness: 40,
|
|
1802
|
+
maxBrightness: 230,
|
|
1803
|
+
minContrast: 12,
|
|
1804
|
+
glarePixelThreshold: 240,
|
|
1805
|
+
maxGlareRatio: 0.15
|
|
1806
|
+
};
|
|
1807
|
+
function rgbaToLuminance(r, g, b) {
|
|
1808
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
1809
|
+
}
|
|
1810
|
+
function analyzeEyeRegionBrightness(pixels) {
|
|
1811
|
+
const pixelCount = pixels.length / 4;
|
|
1812
|
+
if (pixelCount === 0) return 0;
|
|
1813
|
+
let totalLuminance = 0;
|
|
1814
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1815
|
+
totalLuminance += rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1816
|
+
}
|
|
1817
|
+
return totalLuminance / pixelCount;
|
|
1818
|
+
}
|
|
1819
|
+
function analyzeEyeRegionContrast(pixels, meanBrightness) {
|
|
1820
|
+
const pixelCount = pixels.length / 4;
|
|
1821
|
+
if (pixelCount === 0) return 0;
|
|
1822
|
+
const mean = meanBrightness ?? analyzeEyeRegionBrightness(pixels);
|
|
1823
|
+
let sumSqDiff = 0;
|
|
1824
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1825
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1826
|
+
const diff = lum - mean;
|
|
1827
|
+
sumSqDiff += diff * diff;
|
|
1828
|
+
}
|
|
1829
|
+
return Math.sqrt(sumSqDiff / pixelCount);
|
|
1830
|
+
}
|
|
1831
|
+
function detectSpecularHighlights(pixels, threshold = EYE_QUALITY_THRESHOLDS.glarePixelThreshold) {
|
|
1832
|
+
const pixelCount = pixels.length / 4;
|
|
1833
|
+
if (pixelCount === 0) return 0;
|
|
1834
|
+
let glareCount = 0;
|
|
1835
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1836
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1837
|
+
if (lum >= threshold) {
|
|
1838
|
+
glareCount++;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return glareCount / pixelCount;
|
|
1842
|
+
}
|
|
1843
|
+
function checkEyeRegionQuality(pixels, thresholds = EYE_QUALITY_THRESHOLDS) {
|
|
1844
|
+
if (pixels.length < 4) {
|
|
1845
|
+
return {
|
|
1846
|
+
passed: false,
|
|
1847
|
+
brightness: 0,
|
|
1848
|
+
contrast: 0,
|
|
1849
|
+
hasGlare: false,
|
|
1850
|
+
glareRatio: 0,
|
|
1851
|
+
message: "Eyes not clearly visible"
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
const brightness = analyzeEyeRegionBrightness(pixels);
|
|
1855
|
+
const contrast = analyzeEyeRegionContrast(pixels, brightness);
|
|
1856
|
+
const glareRatio = detectSpecularHighlights(pixels, thresholds.glarePixelThreshold);
|
|
1857
|
+
const hasGlare = glareRatio > thresholds.maxGlareRatio;
|
|
1858
|
+
if (brightness < thresholds.minBrightness) {
|
|
1859
|
+
return {
|
|
1860
|
+
passed: false,
|
|
1861
|
+
brightness,
|
|
1862
|
+
contrast,
|
|
1863
|
+
hasGlare,
|
|
1864
|
+
glareRatio,
|
|
1865
|
+
message: "Eyes are in shadow - improve lighting"
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
if (brightness > thresholds.maxBrightness) {
|
|
1869
|
+
return {
|
|
1870
|
+
passed: false,
|
|
1871
|
+
brightness,
|
|
1872
|
+
contrast,
|
|
1873
|
+
hasGlare,
|
|
1874
|
+
glareRatio,
|
|
1875
|
+
message: "Eye region overexposed - reduce lighting"
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
if (hasGlare) {
|
|
1879
|
+
return {
|
|
1880
|
+
passed: false,
|
|
1881
|
+
brightness,
|
|
1882
|
+
contrast,
|
|
1883
|
+
hasGlare,
|
|
1884
|
+
glareRatio,
|
|
1885
|
+
message: "Glare detected - adjust angle or remove glasses"
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
if (contrast < thresholds.minContrast) {
|
|
1889
|
+
return {
|
|
1890
|
+
passed: false,
|
|
1891
|
+
brightness,
|
|
1892
|
+
contrast,
|
|
1893
|
+
hasGlare,
|
|
1894
|
+
glareRatio,
|
|
1895
|
+
message: "Eyes not clearly visible"
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
passed: true,
|
|
1900
|
+
brightness,
|
|
1901
|
+
contrast,
|
|
1902
|
+
hasGlare,
|
|
1903
|
+
glareRatio,
|
|
1904
|
+
message: null
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1669
1907
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1670
1908
|
0 && (module.exports = {
|
|
1671
1909
|
ALIGNMENT_THRESHOLD_CAPTURE,
|
|
@@ -1692,6 +1930,8 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1692
1930
|
ERROR_MESSAGES,
|
|
1693
1931
|
ERROR_MESSAGES_ES,
|
|
1694
1932
|
ES_LOCALE,
|
|
1933
|
+
EYE_LANDMARK_INDICES,
|
|
1934
|
+
EYE_QUALITY_THRESHOLDS,
|
|
1695
1935
|
FACE_CENTER_VERTICAL_OFFSET,
|
|
1696
1936
|
FACE_CROP_OUTPUT_SIZE,
|
|
1697
1937
|
FEEDBACK_MESSAGES,
|
|
@@ -1725,23 +1965,30 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1725
1965
|
RETRY_CONFIG,
|
|
1726
1966
|
TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
1727
1967
|
analyzeBlur,
|
|
1968
|
+
analyzeEyeRegionBrightness,
|
|
1969
|
+
analyzeEyeRegionContrast,
|
|
1728
1970
|
analyzeLighting,
|
|
1729
1971
|
calculateAdaptiveCropMultiplier,
|
|
1730
1972
|
calculateBrightness,
|
|
1731
1973
|
calculateFaceAlignment,
|
|
1732
1974
|
calculateFaceCropRegion,
|
|
1733
1975
|
canCaptureFrame,
|
|
1976
|
+
checkEyeRegionQuality,
|
|
1734
1977
|
checkFrameQuality,
|
|
1735
1978
|
decodeBase64,
|
|
1979
|
+
detectSpecularHighlights,
|
|
1736
1980
|
encodeBase64,
|
|
1737
1981
|
generateSessionId,
|
|
1982
|
+
getActiveModels,
|
|
1738
1983
|
getApiErrorMessage,
|
|
1739
1984
|
getCaptureQualityFeedback,
|
|
1985
|
+
getEyeRegionBounds,
|
|
1740
1986
|
getFeedbackMessage,
|
|
1741
1987
|
getMinFramesForModel,
|
|
1742
1988
|
getOvalGuideState,
|
|
1743
1989
|
getStatusMessage,
|
|
1744
1990
|
hasEnoughFrames,
|
|
1991
|
+
isDeprecatedModel,
|
|
1745
1992
|
isFaceCropFullyInFrame,
|
|
1746
1993
|
isFaceFullyVisible,
|
|
1747
1994
|
isFaceInOval,
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ var API_ENDPOINTS = {
|
|
|
7
7
|
var DEFAULT_ENDPOINT = API_ENDPOINTS.production;
|
|
8
8
|
var API_PATHS = {
|
|
9
9
|
health: "/health",
|
|
10
|
+
models: "/api/v1/models",
|
|
10
11
|
fastCheck: "/api/v1/fast-check",
|
|
11
12
|
fastCheckCrops: "/api/v1/fast-check-crops",
|
|
12
13
|
fastCheckStream: "/api/v1/fast-check-stream",
|
|
@@ -294,6 +295,14 @@ var LivenessClient = class _LivenessClient {
|
|
|
294
295
|
async health() {
|
|
295
296
|
return this.request(API_PATHS.health);
|
|
296
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Fetch the full model registry from the API.
|
|
300
|
+
* Includes all models with their deprecated status.
|
|
301
|
+
* Use this instead of the local MODEL_CONFIGS for a live list.
|
|
302
|
+
*/
|
|
303
|
+
async getModels() {
|
|
304
|
+
return this.request(API_PATHS.models);
|
|
305
|
+
}
|
|
297
306
|
/**
|
|
298
307
|
* Get queue statistics
|
|
299
308
|
*/
|
|
@@ -743,117 +752,172 @@ var MODEL_CONFIGS = {
|
|
|
743
752
|
type: "10",
|
|
744
753
|
minFrames: 10,
|
|
745
754
|
recommendedFrames: 10,
|
|
746
|
-
description: "Fast model - 10 frames, quick verification"
|
|
755
|
+
description: "Fast model - 10 frames, quick verification",
|
|
756
|
+
deprecated: false
|
|
747
757
|
},
|
|
748
758
|
"50": {
|
|
749
759
|
type: "50",
|
|
750
760
|
minFrames: 50,
|
|
751
761
|
recommendedFrames: 50,
|
|
752
|
-
description: "Balanced model - 50 frames, good accuracy"
|
|
762
|
+
description: "Balanced model - 50 frames, good accuracy",
|
|
763
|
+
deprecated: false
|
|
753
764
|
},
|
|
754
765
|
"250": {
|
|
755
766
|
type: "250",
|
|
756
767
|
minFrames: 250,
|
|
757
768
|
recommendedFrames: 250,
|
|
758
|
-
description: "High-accuracy model - 250 frames, best accuracy"
|
|
769
|
+
description: "High-accuracy model - 250 frames, best accuracy",
|
|
770
|
+
deprecated: false
|
|
759
771
|
},
|
|
760
772
|
// Hybrid V2 models with physiological features
|
|
761
773
|
"hybrid-v2-10": {
|
|
762
774
|
type: "hybrid-v2-10",
|
|
763
775
|
minFrames: 10,
|
|
764
776
|
recommendedFrames: 10,
|
|
765
|
-
description: "Hybrid V2 10-frame model with physio features"
|
|
777
|
+
description: "Hybrid V2 10-frame model with physio features",
|
|
778
|
+
deprecated: false
|
|
766
779
|
},
|
|
767
780
|
"hybrid-v2-30": {
|
|
768
781
|
type: "hybrid-v2-30",
|
|
769
782
|
minFrames: 30,
|
|
770
783
|
recommendedFrames: 30,
|
|
771
|
-
description: "Hybrid V2 30-frame model with physio features"
|
|
784
|
+
description: "Hybrid V2 30-frame model with physio features",
|
|
785
|
+
deprecated: false
|
|
772
786
|
},
|
|
773
787
|
"hybrid-v2-50": {
|
|
774
788
|
type: "hybrid-v2-50",
|
|
775
789
|
minFrames: 50,
|
|
776
790
|
recommendedFrames: 50,
|
|
777
|
-
description: "Hybrid V2 50-frame model with physio features"
|
|
791
|
+
description: "Hybrid V2 50-frame model with physio features",
|
|
792
|
+
deprecated: false
|
|
778
793
|
},
|
|
779
794
|
"hybrid-v2-60": {
|
|
780
795
|
type: "hybrid-v2-60",
|
|
781
796
|
minFrames: 60,
|
|
782
797
|
recommendedFrames: 60,
|
|
783
|
-
description: "Hybrid V2 60-frame model with physio features"
|
|
798
|
+
description: "Hybrid V2 60-frame model with physio features",
|
|
799
|
+
deprecated: false
|
|
784
800
|
},
|
|
785
801
|
"hybrid-v2-90": {
|
|
786
802
|
type: "hybrid-v2-90",
|
|
787
803
|
minFrames: 90,
|
|
788
804
|
recommendedFrames: 90,
|
|
789
|
-
description: "Hybrid V2 90-frame model with physio features"
|
|
805
|
+
description: "Hybrid V2 90-frame model with physio features",
|
|
806
|
+
deprecated: false
|
|
790
807
|
},
|
|
791
808
|
"hybrid-v2-100": {
|
|
792
809
|
type: "hybrid-v2-100",
|
|
793
810
|
minFrames: 100,
|
|
794
811
|
recommendedFrames: 100,
|
|
795
|
-
description: "Hybrid V2 100-frame model with physio features"
|
|
812
|
+
description: "Hybrid V2 100-frame model with physio features",
|
|
813
|
+
deprecated: false
|
|
796
814
|
},
|
|
797
815
|
"hybrid-v2-125": {
|
|
798
816
|
type: "hybrid-v2-125",
|
|
799
817
|
minFrames: 125,
|
|
800
818
|
recommendedFrames: 125,
|
|
801
|
-
description: "Hybrid V2 125-frame model with physio features"
|
|
819
|
+
description: "Hybrid V2 125-frame model with physio features",
|
|
820
|
+
deprecated: false
|
|
802
821
|
},
|
|
803
822
|
"hybrid-v2-150": {
|
|
804
823
|
type: "hybrid-v2-150",
|
|
805
824
|
minFrames: 150,
|
|
806
825
|
recommendedFrames: 150,
|
|
807
|
-
description: "Hybrid V2 150-frame model with physio features"
|
|
826
|
+
description: "Hybrid V2 150-frame model with physio features",
|
|
827
|
+
deprecated: false
|
|
808
828
|
},
|
|
809
829
|
"hybrid-v2-250": {
|
|
810
830
|
type: "hybrid-v2-250",
|
|
811
831
|
minFrames: 250,
|
|
812
832
|
recommendedFrames: 250,
|
|
813
|
-
description: "Hybrid V2 250-frame model with physio features"
|
|
833
|
+
description: "Hybrid V2 250-frame model with physio features",
|
|
834
|
+
deprecated: false
|
|
814
835
|
},
|
|
815
|
-
// Mixed models
|
|
836
|
+
// Mixed V1 models — deprecated, use mixed-*-v2 equivalents instead
|
|
816
837
|
"mixed-10": {
|
|
817
838
|
type: "mixed-10",
|
|
818
839
|
minFrames: 10,
|
|
819
840
|
recommendedFrames: 10,
|
|
820
|
-
description: "Mixed 10-frame model"
|
|
841
|
+
description: "Mixed 10-frame model",
|
|
842
|
+
deprecated: true
|
|
821
843
|
},
|
|
822
844
|
"mixed-30": {
|
|
823
845
|
type: "mixed-30",
|
|
824
846
|
minFrames: 30,
|
|
825
847
|
recommendedFrames: 30,
|
|
826
|
-
description: "Mixed 30-frame model"
|
|
848
|
+
description: "Mixed 30-frame model",
|
|
849
|
+
deprecated: true
|
|
827
850
|
},
|
|
828
851
|
"mixed-60": {
|
|
829
852
|
type: "mixed-60",
|
|
830
853
|
minFrames: 60,
|
|
831
854
|
recommendedFrames: 60,
|
|
832
|
-
description: "Mixed 60-frame model"
|
|
855
|
+
description: "Mixed 60-frame model",
|
|
856
|
+
deprecated: true
|
|
833
857
|
},
|
|
834
858
|
"mixed-90": {
|
|
835
859
|
type: "mixed-90",
|
|
836
860
|
minFrames: 90,
|
|
837
861
|
recommendedFrames: 90,
|
|
838
|
-
description: "Mixed 90-frame model"
|
|
862
|
+
description: "Mixed 90-frame model",
|
|
863
|
+
deprecated: true
|
|
839
864
|
},
|
|
840
865
|
"mixed-120": {
|
|
841
866
|
type: "mixed-120",
|
|
842
867
|
minFrames: 120,
|
|
843
868
|
recommendedFrames: 120,
|
|
844
|
-
description: "Mixed 120-frame model"
|
|
869
|
+
description: "Mixed 120-frame model",
|
|
870
|
+
deprecated: true
|
|
845
871
|
},
|
|
846
872
|
"mixed-150": {
|
|
847
873
|
type: "mixed-150",
|
|
848
874
|
minFrames: 150,
|
|
849
875
|
recommendedFrames: 150,
|
|
850
|
-
description: "Mixed 150-frame model"
|
|
876
|
+
description: "Mixed 150-frame model",
|
|
877
|
+
deprecated: true
|
|
851
878
|
},
|
|
852
879
|
"mixed-250": {
|
|
853
880
|
type: "mixed-250",
|
|
854
881
|
minFrames: 250,
|
|
855
882
|
recommendedFrames: 250,
|
|
856
|
-
description: "Mixed 250-frame model"
|
|
883
|
+
description: "Mixed 250-frame model",
|
|
884
|
+
deprecated: true
|
|
885
|
+
},
|
|
886
|
+
// Mixed V2 models (exp_042 checkpoints — EER: 4.0% / AUC: 0.991 at 30f)
|
|
887
|
+
"mixed-10-v2": {
|
|
888
|
+
type: "mixed-10-v2",
|
|
889
|
+
minFrames: 10,
|
|
890
|
+
recommendedFrames: 10,
|
|
891
|
+
description: "Mixed V2 10-frame model",
|
|
892
|
+
deprecated: false
|
|
893
|
+
},
|
|
894
|
+
"mixed-30-v2": {
|
|
895
|
+
type: "mixed-30-v2",
|
|
896
|
+
minFrames: 30,
|
|
897
|
+
recommendedFrames: 30,
|
|
898
|
+
description: "Mixed V2 30-frame model (recommended \u2014 95.7% balanced accuracy)",
|
|
899
|
+
deprecated: false
|
|
900
|
+
},
|
|
901
|
+
"mixed-60-v2": {
|
|
902
|
+
type: "mixed-60-v2",
|
|
903
|
+
minFrames: 60,
|
|
904
|
+
recommendedFrames: 60,
|
|
905
|
+
description: "Mixed V2 60-frame model",
|
|
906
|
+
deprecated: false
|
|
907
|
+
},
|
|
908
|
+
"mixed-90-v2": {
|
|
909
|
+
type: "mixed-90-v2",
|
|
910
|
+
minFrames: 90,
|
|
911
|
+
recommendedFrames: 90,
|
|
912
|
+
description: "Mixed V2 90-frame model",
|
|
913
|
+
deprecated: false
|
|
914
|
+
},
|
|
915
|
+
"mixed-120-v2": {
|
|
916
|
+
type: "mixed-120-v2",
|
|
917
|
+
minFrames: 120,
|
|
918
|
+
recommendedFrames: 120,
|
|
919
|
+
description: "Mixed V2 120-frame model",
|
|
920
|
+
deprecated: false
|
|
857
921
|
}
|
|
858
922
|
};
|
|
859
923
|
var HYBRID_MODEL_CONFIGS = {
|
|
@@ -873,10 +937,16 @@ var HYBRID_MODEL_CONFIGS = {
|
|
|
873
937
|
}
|
|
874
938
|
};
|
|
875
939
|
function getMinFramesForModel(model) {
|
|
876
|
-
return MODEL_CONFIGS[model]
|
|
940
|
+
return MODEL_CONFIGS[model]?.minFrames ?? 10;
|
|
877
941
|
}
|
|
878
942
|
function hasEnoughFrames(model, frameCount) {
|
|
879
|
-
return frameCount >=
|
|
943
|
+
return frameCount >= getMinFramesForModel(model);
|
|
944
|
+
}
|
|
945
|
+
function isDeprecatedModel(model) {
|
|
946
|
+
return MODEL_CONFIGS[model]?.deprecated ?? false;
|
|
947
|
+
}
|
|
948
|
+
function getActiveModels() {
|
|
949
|
+
return Object.values(MODEL_CONFIGS).filter((c) => !c.deprecated);
|
|
880
950
|
}
|
|
881
951
|
|
|
882
952
|
// src/types/detectors.ts
|
|
@@ -1043,6 +1113,12 @@ var FEEDBACK_MESSAGES = {
|
|
|
1043
1113
|
backlit: "Backlit - try facing the light source",
|
|
1044
1114
|
// Hand occlusion
|
|
1045
1115
|
hand_detected: "Remove hand from face",
|
|
1116
|
+
// Eye region quality
|
|
1117
|
+
eyes_not_visible: "Eyes not clearly visible",
|
|
1118
|
+
eyes_shadowed: "Eyes are in shadow - improve lighting",
|
|
1119
|
+
eyes_overexposed: "Eye region overexposed - reduce lighting",
|
|
1120
|
+
glasses_glare: "Glare detected - adjust angle or remove glasses",
|
|
1121
|
+
eye_quality_poor: "Eye region quality is poor",
|
|
1046
1122
|
// Progress messages
|
|
1047
1123
|
capturing: "Capturing...",
|
|
1048
1124
|
almost_done: "Almost done...",
|
|
@@ -1094,6 +1170,12 @@ var ES_LOCALE = {
|
|
|
1094
1170
|
backlit: "Contraluz - intenta mirar hacia la fuente de luz",
|
|
1095
1171
|
// Hand occlusion
|
|
1096
1172
|
hand_detected: "Retira la mano del rostro",
|
|
1173
|
+
// Eye region quality
|
|
1174
|
+
eyes_not_visible: "Ojos no visibles claramente",
|
|
1175
|
+
eyes_shadowed: "Ojos en sombra - mejora la iluminaci\xF3n",
|
|
1176
|
+
eyes_overexposed: "Regi\xF3n de ojos sobreexpuesta - reduce la iluminaci\xF3n",
|
|
1177
|
+
glasses_glare: "Reflejo detectado - ajusta el \xE1ngulo o retira los lentes",
|
|
1178
|
+
eye_quality_poor: "Calidad de la regi\xF3n de ojos insuficiente",
|
|
1097
1179
|
// Progress messages
|
|
1098
1180
|
capturing: "Capturando...",
|
|
1099
1181
|
almost_done: "Casi listo...",
|
|
@@ -1177,7 +1259,7 @@ function validateTimestamp(timestamp) {
|
|
|
1177
1259
|
}
|
|
1178
1260
|
function validateFrameCount(model, frameCount) {
|
|
1179
1261
|
const config = MODEL_CONFIGS[model];
|
|
1180
|
-
const required = config
|
|
1262
|
+
const required = config?.minFrames ?? 10;
|
|
1181
1263
|
return {
|
|
1182
1264
|
valid: frameCount >= required,
|
|
1183
1265
|
required,
|
|
@@ -1527,6 +1609,44 @@ var LANDMARK_INDEX = {
|
|
|
1527
1609
|
/** Lower lip center */
|
|
1528
1610
|
LOWER_LIP: 14
|
|
1529
1611
|
};
|
|
1612
|
+
var EYE_LANDMARK_INDICES = {
|
|
1613
|
+
/** Right eye contour (viewer's left) */
|
|
1614
|
+
RIGHT_EYE: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
|
|
1615
|
+
/** Left eye contour (viewer's right) */
|
|
1616
|
+
LEFT_EYE: [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398]
|
|
1617
|
+
};
|
|
1618
|
+
function getEyeRegionBounds(landmarks) {
|
|
1619
|
+
if (landmarks.length < 468) return null;
|
|
1620
|
+
const PADDING = 0.2;
|
|
1621
|
+
function computeBounds(indices) {
|
|
1622
|
+
let minX = 1;
|
|
1623
|
+
let minY = 1;
|
|
1624
|
+
let maxX = 0;
|
|
1625
|
+
let maxY = 0;
|
|
1626
|
+
for (const idx of indices) {
|
|
1627
|
+
const lm = landmarks[idx];
|
|
1628
|
+
if (!lm) continue;
|
|
1629
|
+
if (lm.x < minX) minX = lm.x;
|
|
1630
|
+
if (lm.y < minY) minY = lm.y;
|
|
1631
|
+
if (lm.x > maxX) maxX = lm.x;
|
|
1632
|
+
if (lm.y > maxY) maxY = lm.y;
|
|
1633
|
+
}
|
|
1634
|
+
const width = maxX - minX;
|
|
1635
|
+
const height = maxY - minY;
|
|
1636
|
+
const padX = width * PADDING;
|
|
1637
|
+
const padY = height * PADDING;
|
|
1638
|
+
return {
|
|
1639
|
+
x: Math.max(0, minX - padX),
|
|
1640
|
+
y: Math.max(0, minY - padY),
|
|
1641
|
+
width: Math.min(1 - Math.max(0, minX - padX), width + padX * 2),
|
|
1642
|
+
height: Math.min(1 - Math.max(0, minY - padY), height + padY * 2)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
rightEye: computeBounds(EYE_LANDMARK_INDICES.RIGHT_EYE),
|
|
1647
|
+
leftEye: computeBounds(EYE_LANDMARK_INDICES.LEFT_EYE)
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1530
1650
|
var LANDMARK_MIN_BOUND = 0.1;
|
|
1531
1651
|
var LANDMARK_MAX_BOUND = 0.9;
|
|
1532
1652
|
var MIN_LANDMARK_COUNT = 15;
|
|
@@ -1548,6 +1668,115 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1548
1668
|
}
|
|
1549
1669
|
return { valid: true };
|
|
1550
1670
|
}
|
|
1671
|
+
|
|
1672
|
+
// src/utils/eyeRegionAnalysis.ts
|
|
1673
|
+
var EYE_QUALITY_THRESHOLDS = {
|
|
1674
|
+
minBrightness: 40,
|
|
1675
|
+
maxBrightness: 230,
|
|
1676
|
+
minContrast: 12,
|
|
1677
|
+
glarePixelThreshold: 240,
|
|
1678
|
+
maxGlareRatio: 0.15
|
|
1679
|
+
};
|
|
1680
|
+
function rgbaToLuminance(r, g, b) {
|
|
1681
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
1682
|
+
}
|
|
1683
|
+
function analyzeEyeRegionBrightness(pixels) {
|
|
1684
|
+
const pixelCount = pixels.length / 4;
|
|
1685
|
+
if (pixelCount === 0) return 0;
|
|
1686
|
+
let totalLuminance = 0;
|
|
1687
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1688
|
+
totalLuminance += rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1689
|
+
}
|
|
1690
|
+
return totalLuminance / pixelCount;
|
|
1691
|
+
}
|
|
1692
|
+
function analyzeEyeRegionContrast(pixels, meanBrightness) {
|
|
1693
|
+
const pixelCount = pixels.length / 4;
|
|
1694
|
+
if (pixelCount === 0) return 0;
|
|
1695
|
+
const mean = meanBrightness ?? analyzeEyeRegionBrightness(pixels);
|
|
1696
|
+
let sumSqDiff = 0;
|
|
1697
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1698
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1699
|
+
const diff = lum - mean;
|
|
1700
|
+
sumSqDiff += diff * diff;
|
|
1701
|
+
}
|
|
1702
|
+
return Math.sqrt(sumSqDiff / pixelCount);
|
|
1703
|
+
}
|
|
1704
|
+
function detectSpecularHighlights(pixels, threshold = EYE_QUALITY_THRESHOLDS.glarePixelThreshold) {
|
|
1705
|
+
const pixelCount = pixels.length / 4;
|
|
1706
|
+
if (pixelCount === 0) return 0;
|
|
1707
|
+
let glareCount = 0;
|
|
1708
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1709
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1710
|
+
if (lum >= threshold) {
|
|
1711
|
+
glareCount++;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return glareCount / pixelCount;
|
|
1715
|
+
}
|
|
1716
|
+
function checkEyeRegionQuality(pixels, thresholds = EYE_QUALITY_THRESHOLDS) {
|
|
1717
|
+
if (pixels.length < 4) {
|
|
1718
|
+
return {
|
|
1719
|
+
passed: false,
|
|
1720
|
+
brightness: 0,
|
|
1721
|
+
contrast: 0,
|
|
1722
|
+
hasGlare: false,
|
|
1723
|
+
glareRatio: 0,
|
|
1724
|
+
message: "Eyes not clearly visible"
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
const brightness = analyzeEyeRegionBrightness(pixels);
|
|
1728
|
+
const contrast = analyzeEyeRegionContrast(pixels, brightness);
|
|
1729
|
+
const glareRatio = detectSpecularHighlights(pixels, thresholds.glarePixelThreshold);
|
|
1730
|
+
const hasGlare = glareRatio > thresholds.maxGlareRatio;
|
|
1731
|
+
if (brightness < thresholds.minBrightness) {
|
|
1732
|
+
return {
|
|
1733
|
+
passed: false,
|
|
1734
|
+
brightness,
|
|
1735
|
+
contrast,
|
|
1736
|
+
hasGlare,
|
|
1737
|
+
glareRatio,
|
|
1738
|
+
message: "Eyes are in shadow - improve lighting"
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
if (brightness > thresholds.maxBrightness) {
|
|
1742
|
+
return {
|
|
1743
|
+
passed: false,
|
|
1744
|
+
brightness,
|
|
1745
|
+
contrast,
|
|
1746
|
+
hasGlare,
|
|
1747
|
+
glareRatio,
|
|
1748
|
+
message: "Eye region overexposed - reduce lighting"
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
if (hasGlare) {
|
|
1752
|
+
return {
|
|
1753
|
+
passed: false,
|
|
1754
|
+
brightness,
|
|
1755
|
+
contrast,
|
|
1756
|
+
hasGlare,
|
|
1757
|
+
glareRatio,
|
|
1758
|
+
message: "Glare detected - adjust angle or remove glasses"
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
if (contrast < thresholds.minContrast) {
|
|
1762
|
+
return {
|
|
1763
|
+
passed: false,
|
|
1764
|
+
brightness,
|
|
1765
|
+
contrast,
|
|
1766
|
+
hasGlare,
|
|
1767
|
+
glareRatio,
|
|
1768
|
+
message: "Eyes not clearly visible"
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
passed: true,
|
|
1773
|
+
brightness,
|
|
1774
|
+
contrast,
|
|
1775
|
+
hasGlare,
|
|
1776
|
+
glareRatio,
|
|
1777
|
+
message: null
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1551
1780
|
export {
|
|
1552
1781
|
ALIGNMENT_THRESHOLD_CAPTURE,
|
|
1553
1782
|
ALIGNMENT_THRESHOLD_GOOD,
|
|
@@ -1573,6 +1802,8 @@ export {
|
|
|
1573
1802
|
ERROR_MESSAGES,
|
|
1574
1803
|
ERROR_MESSAGES_ES,
|
|
1575
1804
|
ES_LOCALE,
|
|
1805
|
+
EYE_LANDMARK_INDICES,
|
|
1806
|
+
EYE_QUALITY_THRESHOLDS,
|
|
1576
1807
|
FACE_CENTER_VERTICAL_OFFSET,
|
|
1577
1808
|
FACE_CROP_OUTPUT_SIZE,
|
|
1578
1809
|
FEEDBACK_MESSAGES,
|
|
@@ -1606,23 +1837,30 @@ export {
|
|
|
1606
1837
|
RETRY_CONFIG,
|
|
1607
1838
|
TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
1608
1839
|
analyzeBlur,
|
|
1840
|
+
analyzeEyeRegionBrightness,
|
|
1841
|
+
analyzeEyeRegionContrast,
|
|
1609
1842
|
analyzeLighting,
|
|
1610
1843
|
calculateAdaptiveCropMultiplier,
|
|
1611
1844
|
calculateBrightness,
|
|
1612
1845
|
calculateFaceAlignment,
|
|
1613
1846
|
calculateFaceCropRegion,
|
|
1614
1847
|
canCaptureFrame,
|
|
1848
|
+
checkEyeRegionQuality,
|
|
1615
1849
|
checkFrameQuality,
|
|
1616
1850
|
decodeBase64,
|
|
1851
|
+
detectSpecularHighlights,
|
|
1617
1852
|
encodeBase64,
|
|
1618
1853
|
generateSessionId,
|
|
1854
|
+
getActiveModels,
|
|
1619
1855
|
getApiErrorMessage,
|
|
1620
1856
|
getCaptureQualityFeedback,
|
|
1857
|
+
getEyeRegionBounds,
|
|
1621
1858
|
getFeedbackMessage,
|
|
1622
1859
|
getMinFramesForModel,
|
|
1623
1860
|
getOvalGuideState,
|
|
1624
1861
|
getStatusMessage,
|
|
1625
1862
|
hasEnoughFrames,
|
|
1863
|
+
isDeprecatedModel,
|
|
1626
1864
|
isFaceCropFullyInFrame,
|
|
1627
1865
|
isFaceFullyVisible,
|
|
1628
1866
|
isFaceInOval,
|