@moveris/shared 2.1.1 → 2.3.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 +65 -0
- package/dist/index.d.mts +67 -1
- package/dist/index.d.ts +67 -1
- package/dist/index.js +331 -10
- package/dist/index.mjs +319 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ const client = new LivenessClient({
|
|
|
25
25
|
|
|
26
26
|
// Perform a fast liveness check
|
|
27
27
|
const result = await client.fastCheck(frames, {
|
|
28
|
+
sessionId: 'my-session-id', // optional — auto-generated if omitted
|
|
28
29
|
model: '10',
|
|
29
30
|
source: 'live',
|
|
30
31
|
});
|
|
@@ -293,6 +294,8 @@ const sessionId = generateSessionId();
|
|
|
293
294
|
// e.g., 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
|
|
294
295
|
```
|
|
295
296
|
|
|
297
|
+
> **Session ID injection**: Every `LivenessClient` method (`fastCheck`, `fastCheckCrops`, `streamFrame`, `verify`, `hybrid50`, `hybrid150`) accepts an optional `sessionId` in its options object. If provided, that ID is used for the request; if omitted, a new UUID is generated automatically. This is useful for debugging, testing, or correlating client requests with server logs. In `@moveris/react`, the `sessionId` can be injected via the `useLiveness` hook config or the `LivenessView` / `LivenessModal` props.
|
|
298
|
+
|
|
296
299
|
#### `toFrameData(frames)`
|
|
297
300
|
|
|
298
301
|
Convert CapturedFrame array to API format.
|
|
@@ -423,6 +426,11 @@ const status = getStatusMessage('capturing', DEFAULT_LOCALE);
|
|
|
423
426
|
| `processing` | "Processing..." | API verification in progress |
|
|
424
427
|
| `success` | "Verification complete" | Successful completion |
|
|
425
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 |
|
|
426
434
|
|
|
427
435
|
---
|
|
428
436
|
|
|
@@ -449,6 +457,59 @@ const color = OVAL_GUIDE_COLORS[state];
|
|
|
449
457
|
|
|
450
458
|
---
|
|
451
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
|
+
|
|
452
513
|
### Frame Analysis Utilities
|
|
453
514
|
|
|
454
515
|
Utilities for assessing frame quality before submission.
|
|
@@ -524,6 +585,10 @@ import type {
|
|
|
524
585
|
HeadPose,
|
|
525
586
|
FaceLandmarkPoint,
|
|
526
587
|
LandmarkValidationResult,
|
|
588
|
+
EyeRegionQuality,
|
|
589
|
+
EyeQualityThresholds,
|
|
590
|
+
EyeRegionBounds,
|
|
591
|
+
EyeRegionsBounds,
|
|
527
592
|
} from '@moveris/shared';
|
|
528
593
|
```
|
|
529
594
|
|
package/dist/index.d.mts
CHANGED
|
@@ -322,6 +322,21 @@ declare const LANDMARK_INDEX: {
|
|
|
322
322
|
readonly UPPER_LIP: 13;
|
|
323
323
|
readonly LOWER_LIP: 14;
|
|
324
324
|
};
|
|
325
|
+
declare const EYE_LANDMARK_INDICES: {
|
|
326
|
+
readonly RIGHT_EYE: readonly [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246];
|
|
327
|
+
readonly LEFT_EYE: readonly [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398];
|
|
328
|
+
};
|
|
329
|
+
interface EyeRegionBounds {
|
|
330
|
+
x: number;
|
|
331
|
+
y: number;
|
|
332
|
+
width: number;
|
|
333
|
+
height: number;
|
|
334
|
+
}
|
|
335
|
+
interface EyeRegionsBounds {
|
|
336
|
+
leftEye: EyeRegionBounds;
|
|
337
|
+
rightEye: EyeRegionBounds;
|
|
338
|
+
}
|
|
339
|
+
declare function getEyeRegionBounds(landmarks: FaceLandmarkPoint[]): EyeRegionsBounds | null;
|
|
325
340
|
declare const LANDMARK_MIN_BOUND = 0.1;
|
|
326
341
|
declare const LANDMARK_MAX_BOUND = 0.9;
|
|
327
342
|
declare const MIN_LANDMARK_COUNT = 15;
|
|
@@ -429,6 +444,10 @@ declare class LivenessClient {
|
|
|
429
444
|
private readonly fetchFn;
|
|
430
445
|
constructor(config: LivenessClientConfig);
|
|
431
446
|
private request;
|
|
447
|
+
private parseErrorResponse;
|
|
448
|
+
private static unwrapErrorCode;
|
|
449
|
+
private static unwrapErrorMessage;
|
|
450
|
+
private static extractCodeFromText;
|
|
432
451
|
private requestWithRetry;
|
|
433
452
|
health(): Promise<HealthResponse>;
|
|
434
453
|
queueStats(): Promise<QueueStatsResponse>;
|
|
@@ -565,6 +584,27 @@ declare const DEFAULT_LIVENESS_CONFIG: {
|
|
|
565
584
|
readonly timeout: 30000;
|
|
566
585
|
};
|
|
567
586
|
|
|
587
|
+
declare const API_ERROR_CODES: {
|
|
588
|
+
readonly SESSION_EXPIRED: "session_expired";
|
|
589
|
+
readonly INVALID_KEY: "invalid_key";
|
|
590
|
+
readonly INVALID_MODEL: "invalid_model";
|
|
591
|
+
readonly INSUFFICIENT_FRAMES: "insufficient_frames";
|
|
592
|
+
readonly MISSING_FIELD: "missing_field";
|
|
593
|
+
readonly INVALID_FRAME: "invalid_frame";
|
|
594
|
+
readonly INVALID_SESSION: "invalid_session";
|
|
595
|
+
readonly RATE_LIMITED: "rate_limited";
|
|
596
|
+
readonly STREAM_INCOMPLETE: "stream_incomplete";
|
|
597
|
+
readonly NO_VERDICT: "no_verdict";
|
|
598
|
+
readonly TIMEOUT: "timeout";
|
|
599
|
+
readonly NETWORK_ERROR: "network_error";
|
|
600
|
+
readonly SERVER_ERROR: "server_error";
|
|
601
|
+
};
|
|
602
|
+
type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
|
|
603
|
+
declare const ERROR_MESSAGES: Record<string, string>;
|
|
604
|
+
declare const ERROR_MESSAGES_ES: Record<string, string>;
|
|
605
|
+
declare function getApiErrorMessage(code: string | undefined, message?: string, customMessages?: Record<string, string>): string;
|
|
606
|
+
declare function isRetryableError(code: string | undefined): boolean;
|
|
607
|
+
|
|
568
608
|
declare const ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
|
|
569
609
|
declare const ALIGNMENT_THRESHOLD_POOR = 0.5;
|
|
570
610
|
declare const ALIGNMENT_THRESHOLD_GOOD = 0.5;
|
|
@@ -630,6 +670,11 @@ declare const FEEDBACK_MESSAGES: {
|
|
|
630
670
|
readonly too_dark: "Low lighting - move to a brighter area";
|
|
631
671
|
readonly backlit: "Backlit - try facing the light source";
|
|
632
672
|
readonly hand_detected: "Remove hand from face";
|
|
673
|
+
readonly eyes_not_visible: "Eyes not clearly visible";
|
|
674
|
+
readonly eyes_shadowed: "Eyes are in shadow - improve lighting";
|
|
675
|
+
readonly eyes_overexposed: "Eye region overexposed - reduce lighting";
|
|
676
|
+
readonly glasses_glare: "Glare detected - adjust angle or remove glasses";
|
|
677
|
+
readonly eye_quality_poor: "Eye region quality is poor";
|
|
633
678
|
readonly capturing: "Capturing...";
|
|
634
679
|
readonly almost_done: "Almost done...";
|
|
635
680
|
readonly verification_complete: "Verification complete";
|
|
@@ -683,4 +728,25 @@ interface RetryOptions {
|
|
|
683
728
|
declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
684
729
|
declare function sleep(ms: number): Promise<void>;
|
|
685
730
|
|
|
686
|
-
|
|
731
|
+
interface EyeRegionQuality {
|
|
732
|
+
passed: boolean;
|
|
733
|
+
brightness: number;
|
|
734
|
+
contrast: number;
|
|
735
|
+
hasGlare: boolean;
|
|
736
|
+
glareRatio: number;
|
|
737
|
+
message: string | null;
|
|
738
|
+
}
|
|
739
|
+
interface EyeQualityThresholds {
|
|
740
|
+
minBrightness: number;
|
|
741
|
+
maxBrightness: number;
|
|
742
|
+
minContrast: number;
|
|
743
|
+
glarePixelThreshold: number;
|
|
744
|
+
maxGlareRatio: number;
|
|
745
|
+
}
|
|
746
|
+
declare const EYE_QUALITY_THRESHOLDS: EyeQualityThresholds;
|
|
747
|
+
declare function analyzeEyeRegionBrightness(pixels: Uint8Array | Uint8ClampedArray): number;
|
|
748
|
+
declare function analyzeEyeRegionContrast(pixels: Uint8Array | Uint8ClampedArray, meanBrightness?: number): number;
|
|
749
|
+
declare function detectSpecularHighlights(pixels: Uint8Array | Uint8ClampedArray, threshold?: number): number;
|
|
750
|
+
declare function checkEyeRegionQuality(pixels: Uint8Array | Uint8ClampedArray, thresholds?: EyeQualityThresholds): EyeRegionQuality;
|
|
751
|
+
|
|
752
|
+
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 ModelType, 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, getApiErrorMessage, getCaptureQualityFeedback, getEyeRegionBounds, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, 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
|
@@ -322,6 +322,21 @@ declare const LANDMARK_INDEX: {
|
|
|
322
322
|
readonly UPPER_LIP: 13;
|
|
323
323
|
readonly LOWER_LIP: 14;
|
|
324
324
|
};
|
|
325
|
+
declare const EYE_LANDMARK_INDICES: {
|
|
326
|
+
readonly RIGHT_EYE: readonly [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246];
|
|
327
|
+
readonly LEFT_EYE: readonly [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398];
|
|
328
|
+
};
|
|
329
|
+
interface EyeRegionBounds {
|
|
330
|
+
x: number;
|
|
331
|
+
y: number;
|
|
332
|
+
width: number;
|
|
333
|
+
height: number;
|
|
334
|
+
}
|
|
335
|
+
interface EyeRegionsBounds {
|
|
336
|
+
leftEye: EyeRegionBounds;
|
|
337
|
+
rightEye: EyeRegionBounds;
|
|
338
|
+
}
|
|
339
|
+
declare function getEyeRegionBounds(landmarks: FaceLandmarkPoint[]): EyeRegionsBounds | null;
|
|
325
340
|
declare const LANDMARK_MIN_BOUND = 0.1;
|
|
326
341
|
declare const LANDMARK_MAX_BOUND = 0.9;
|
|
327
342
|
declare const MIN_LANDMARK_COUNT = 15;
|
|
@@ -429,6 +444,10 @@ declare class LivenessClient {
|
|
|
429
444
|
private readonly fetchFn;
|
|
430
445
|
constructor(config: LivenessClientConfig);
|
|
431
446
|
private request;
|
|
447
|
+
private parseErrorResponse;
|
|
448
|
+
private static unwrapErrorCode;
|
|
449
|
+
private static unwrapErrorMessage;
|
|
450
|
+
private static extractCodeFromText;
|
|
432
451
|
private requestWithRetry;
|
|
433
452
|
health(): Promise<HealthResponse>;
|
|
434
453
|
queueStats(): Promise<QueueStatsResponse>;
|
|
@@ -565,6 +584,27 @@ declare const DEFAULT_LIVENESS_CONFIG: {
|
|
|
565
584
|
readonly timeout: 30000;
|
|
566
585
|
};
|
|
567
586
|
|
|
587
|
+
declare const API_ERROR_CODES: {
|
|
588
|
+
readonly SESSION_EXPIRED: "session_expired";
|
|
589
|
+
readonly INVALID_KEY: "invalid_key";
|
|
590
|
+
readonly INVALID_MODEL: "invalid_model";
|
|
591
|
+
readonly INSUFFICIENT_FRAMES: "insufficient_frames";
|
|
592
|
+
readonly MISSING_FIELD: "missing_field";
|
|
593
|
+
readonly INVALID_FRAME: "invalid_frame";
|
|
594
|
+
readonly INVALID_SESSION: "invalid_session";
|
|
595
|
+
readonly RATE_LIMITED: "rate_limited";
|
|
596
|
+
readonly STREAM_INCOMPLETE: "stream_incomplete";
|
|
597
|
+
readonly NO_VERDICT: "no_verdict";
|
|
598
|
+
readonly TIMEOUT: "timeout";
|
|
599
|
+
readonly NETWORK_ERROR: "network_error";
|
|
600
|
+
readonly SERVER_ERROR: "server_error";
|
|
601
|
+
};
|
|
602
|
+
type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
|
|
603
|
+
declare const ERROR_MESSAGES: Record<string, string>;
|
|
604
|
+
declare const ERROR_MESSAGES_ES: Record<string, string>;
|
|
605
|
+
declare function getApiErrorMessage(code: string | undefined, message?: string, customMessages?: Record<string, string>): string;
|
|
606
|
+
declare function isRetryableError(code: string | undefined): boolean;
|
|
607
|
+
|
|
568
608
|
declare const ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
|
|
569
609
|
declare const ALIGNMENT_THRESHOLD_POOR = 0.5;
|
|
570
610
|
declare const ALIGNMENT_THRESHOLD_GOOD = 0.5;
|
|
@@ -630,6 +670,11 @@ declare const FEEDBACK_MESSAGES: {
|
|
|
630
670
|
readonly too_dark: "Low lighting - move to a brighter area";
|
|
631
671
|
readonly backlit: "Backlit - try facing the light source";
|
|
632
672
|
readonly hand_detected: "Remove hand from face";
|
|
673
|
+
readonly eyes_not_visible: "Eyes not clearly visible";
|
|
674
|
+
readonly eyes_shadowed: "Eyes are in shadow - improve lighting";
|
|
675
|
+
readonly eyes_overexposed: "Eye region overexposed - reduce lighting";
|
|
676
|
+
readonly glasses_glare: "Glare detected - adjust angle or remove glasses";
|
|
677
|
+
readonly eye_quality_poor: "Eye region quality is poor";
|
|
633
678
|
readonly capturing: "Capturing...";
|
|
634
679
|
readonly almost_done: "Almost done...";
|
|
635
680
|
readonly verification_complete: "Verification complete";
|
|
@@ -683,4 +728,25 @@ interface RetryOptions {
|
|
|
683
728
|
declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
684
729
|
declare function sleep(ms: number): Promise<void>;
|
|
685
730
|
|
|
686
|
-
|
|
731
|
+
interface EyeRegionQuality {
|
|
732
|
+
passed: boolean;
|
|
733
|
+
brightness: number;
|
|
734
|
+
contrast: number;
|
|
735
|
+
hasGlare: boolean;
|
|
736
|
+
glareRatio: number;
|
|
737
|
+
message: string | null;
|
|
738
|
+
}
|
|
739
|
+
interface EyeQualityThresholds {
|
|
740
|
+
minBrightness: number;
|
|
741
|
+
maxBrightness: number;
|
|
742
|
+
minContrast: number;
|
|
743
|
+
glarePixelThreshold: number;
|
|
744
|
+
maxGlareRatio: number;
|
|
745
|
+
}
|
|
746
|
+
declare const EYE_QUALITY_THRESHOLDS: EyeQualityThresholds;
|
|
747
|
+
declare function analyzeEyeRegionBrightness(pixels: Uint8Array | Uint8ClampedArray): number;
|
|
748
|
+
declare function analyzeEyeRegionContrast(pixels: Uint8Array | Uint8ClampedArray, meanBrightness?: number): number;
|
|
749
|
+
declare function detectSpecularHighlights(pixels: Uint8Array | Uint8ClampedArray, threshold?: number): number;
|
|
750
|
+
declare function checkEyeRegionQuality(pixels: Uint8Array | Uint8ClampedArray, thresholds?: EyeQualityThresholds): EyeRegionQuality;
|
|
751
|
+
|
|
752
|
+
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 ModelType, 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, getApiErrorMessage, getCaptureQualityFeedback, getEyeRegionBounds, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, isRetryableError, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(index_exports, {
|
|
|
25
25
|
ALIGNMENT_THRESHOLD_PERFECT: () => ALIGNMENT_THRESHOLD_PERFECT,
|
|
26
26
|
ALIGNMENT_THRESHOLD_POOR: () => ALIGNMENT_THRESHOLD_POOR,
|
|
27
27
|
API_ENDPOINTS: () => API_ENDPOINTS,
|
|
28
|
+
API_ERROR_CODES: () => API_ERROR_CODES,
|
|
28
29
|
API_PATHS: () => API_PATHS,
|
|
29
30
|
AUTH_CONFIG: () => AUTH_CONFIG,
|
|
30
31
|
BACKLIT_RATIO_THRESHOLD: () => BACKLIT_RATIO_THRESHOLD,
|
|
@@ -40,7 +41,11 @@ __export(index_exports, {
|
|
|
40
41
|
DEFAULT_OVAL_REGION: () => DEFAULT_OVAL_REGION,
|
|
41
42
|
DEFAULT_STABILIZER_CONFIG: () => DEFAULT_STABILIZER_CONFIG,
|
|
42
43
|
DEFAULT_STATUS_MESSAGES: () => DEFAULT_STATUS_MESSAGES,
|
|
44
|
+
ERROR_MESSAGES: () => ERROR_MESSAGES,
|
|
45
|
+
ERROR_MESSAGES_ES: () => ERROR_MESSAGES_ES,
|
|
43
46
|
ES_LOCALE: () => ES_LOCALE,
|
|
47
|
+
EYE_LANDMARK_INDICES: () => EYE_LANDMARK_INDICES,
|
|
48
|
+
EYE_QUALITY_THRESHOLDS: () => EYE_QUALITY_THRESHOLDS,
|
|
44
49
|
FACE_CENTER_VERTICAL_OFFSET: () => FACE_CENTER_VERTICAL_OFFSET,
|
|
45
50
|
FACE_CROP_OUTPUT_SIZE: () => FACE_CROP_OUTPUT_SIZE,
|
|
46
51
|
FEEDBACK_MESSAGES: () => FEEDBACK_MESSAGES,
|
|
@@ -74,17 +79,23 @@ __export(index_exports, {
|
|
|
74
79
|
RETRY_CONFIG: () => RETRY_CONFIG,
|
|
75
80
|
TARGET_FACE_PERCENTAGE_IN_CROP: () => TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
76
81
|
analyzeBlur: () => analyzeBlur,
|
|
82
|
+
analyzeEyeRegionBrightness: () => analyzeEyeRegionBrightness,
|
|
83
|
+
analyzeEyeRegionContrast: () => analyzeEyeRegionContrast,
|
|
77
84
|
analyzeLighting: () => analyzeLighting,
|
|
78
85
|
calculateAdaptiveCropMultiplier: () => calculateAdaptiveCropMultiplier,
|
|
79
86
|
calculateBrightness: () => calculateBrightness,
|
|
80
87
|
calculateFaceAlignment: () => calculateFaceAlignment,
|
|
81
88
|
calculateFaceCropRegion: () => calculateFaceCropRegion,
|
|
82
89
|
canCaptureFrame: () => canCaptureFrame,
|
|
90
|
+
checkEyeRegionQuality: () => checkEyeRegionQuality,
|
|
83
91
|
checkFrameQuality: () => checkFrameQuality,
|
|
84
92
|
decodeBase64: () => decodeBase64,
|
|
93
|
+
detectSpecularHighlights: () => detectSpecularHighlights,
|
|
85
94
|
encodeBase64: () => encodeBase64,
|
|
86
95
|
generateSessionId: () => generateSessionId,
|
|
96
|
+
getApiErrorMessage: () => getApiErrorMessage,
|
|
87
97
|
getCaptureQualityFeedback: () => getCaptureQualityFeedback,
|
|
98
|
+
getEyeRegionBounds: () => getEyeRegionBounds,
|
|
88
99
|
getFeedbackMessage: () => getFeedbackMessage,
|
|
89
100
|
getMinFramesForModel: () => getMinFramesForModel,
|
|
90
101
|
getOvalGuideState: () => getOvalGuideState,
|
|
@@ -93,6 +104,7 @@ __export(index_exports, {
|
|
|
93
104
|
isFaceCropFullyInFrame: () => isFaceCropFullyInFrame,
|
|
94
105
|
isFaceFullyVisible: () => isFaceFullyVisible,
|
|
95
106
|
isFaceInOval: () => isFaceInOval,
|
|
107
|
+
isRetryableError: () => isRetryableError,
|
|
96
108
|
retryWithBackoff: () => retryWithBackoff,
|
|
97
109
|
rgbaToGrayscale: () => rgbaToGrayscale,
|
|
98
110
|
sleep: () => sleep,
|
|
@@ -248,7 +260,8 @@ function toLivenessResultFromStream(response) {
|
|
|
248
260
|
throw new LivenessApiError("Stream not complete", "stream_incomplete", 400);
|
|
249
261
|
}
|
|
250
262
|
if (!response.verdict) {
|
|
251
|
-
|
|
263
|
+
const errorCode = response.error ?? "no_verdict";
|
|
264
|
+
throw new LivenessApiError(response.error ?? "No verdict received", errorCode, 500);
|
|
252
265
|
}
|
|
253
266
|
return {
|
|
254
267
|
verdict: response.verdict,
|
|
@@ -269,7 +282,7 @@ function generateSessionId() {
|
|
|
269
282
|
return v.toString(16);
|
|
270
283
|
});
|
|
271
284
|
}
|
|
272
|
-
var LivenessClient = class {
|
|
285
|
+
var LivenessClient = class _LivenessClient {
|
|
273
286
|
constructor(config) {
|
|
274
287
|
this.baseUrl = (config.baseUrl ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
275
288
|
this.apiKey = config.apiKey;
|
|
@@ -296,14 +309,7 @@ var LivenessClient = class {
|
|
|
296
309
|
});
|
|
297
310
|
clearTimeout(timeoutId);
|
|
298
311
|
if (!response.ok) {
|
|
299
|
-
|
|
300
|
-
throw new LivenessApiError(
|
|
301
|
-
errorData.message,
|
|
302
|
-
errorData.error,
|
|
303
|
-
response.status,
|
|
304
|
-
errorData.required,
|
|
305
|
-
errorData.received
|
|
306
|
-
);
|
|
312
|
+
throw await this.parseErrorResponse(response);
|
|
307
313
|
}
|
|
308
314
|
return await response.json();
|
|
309
315
|
} catch (error) {
|
|
@@ -321,6 +327,75 @@ var LivenessClient = class {
|
|
|
321
327
|
);
|
|
322
328
|
}
|
|
323
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Parse an error response body, handling both JSON and non-JSON bodies.
|
|
332
|
+
*
|
|
333
|
+
* The backend sometimes wraps the real error in a generic envelope:
|
|
334
|
+
* ```json
|
|
335
|
+
* { "error": "http_error", "message": "{'error': 'session_expired', ...}" }
|
|
336
|
+
* ```
|
|
337
|
+
* This method unwraps such envelopes to extract the real error code.
|
|
338
|
+
*/
|
|
339
|
+
async parseErrorResponse(response) {
|
|
340
|
+
const status = response.status;
|
|
341
|
+
let body;
|
|
342
|
+
try {
|
|
343
|
+
body = await response.text();
|
|
344
|
+
} catch {
|
|
345
|
+
return new LivenessApiError(`Request failed with status ${status}`, "server_error", status);
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const data = JSON.parse(body);
|
|
349
|
+
if (data.error) {
|
|
350
|
+
const realCode = _LivenessClient.unwrapErrorCode(data.error, data.message);
|
|
351
|
+
const realMessage = _LivenessClient.unwrapErrorMessage(data.message);
|
|
352
|
+
return new LivenessApiError(
|
|
353
|
+
realMessage ?? `Request failed (${realCode})`,
|
|
354
|
+
realCode,
|
|
355
|
+
status,
|
|
356
|
+
data.required,
|
|
357
|
+
data.received
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
const code = _LivenessClient.extractCodeFromText(body) ?? "server_error";
|
|
363
|
+
return new LivenessApiError(`Request failed with status ${status}`, code, status);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* When the outer error code is a generic wrapper (e.g. "http_error"),
|
|
367
|
+
* try to extract the real error code from the message text.
|
|
368
|
+
*/
|
|
369
|
+
static unwrapErrorCode(outerCode, message) {
|
|
370
|
+
const WRAPPER_CODES = /* @__PURE__ */ new Set([
|
|
371
|
+
"http_error",
|
|
372
|
+
"upstream_error",
|
|
373
|
+
"proxy_error",
|
|
374
|
+
"internal_error"
|
|
375
|
+
]);
|
|
376
|
+
if (!WRAPPER_CODES.has(outerCode) || !message) {
|
|
377
|
+
return outerCode;
|
|
378
|
+
}
|
|
379
|
+
return _LivenessClient.extractCodeFromText(message) ?? outerCode;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Try to extract a human-readable message from a nested Python dict string.
|
|
383
|
+
* E.g.: "{'error': 'session_expired', 'message': \"Create a new session...\"}"
|
|
384
|
+
* → "Create a new session..."
|
|
385
|
+
*/
|
|
386
|
+
static unwrapErrorMessage(message) {
|
|
387
|
+
if (!message) return void 0;
|
|
388
|
+
const msgMatch = /'message'\s*:\s*"([^"]+)"/.exec(message) ?? /'message'\s*:\s*'([^']+)'/.exec(message) ?? /"message"\s*:\s*"([^"]+)"/.exec(message);
|
|
389
|
+
return msgMatch?.[1] ?? void 0;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Extract an error code from raw text using regex.
|
|
393
|
+
* Handles both Python dict (`'error': 'code'`) and JSON (`"error": "code"`).
|
|
394
|
+
*/
|
|
395
|
+
static extractCodeFromText(text) {
|
|
396
|
+
const match = /'error'\s*:\s*'([^']+)'/.exec(text) ?? /"error"\s*:\s*"([^"]+)"/.exec(text);
|
|
397
|
+
return match?.[1] ?? null;
|
|
398
|
+
}
|
|
324
399
|
/**
|
|
325
400
|
* Make a request with optional retry
|
|
326
401
|
*/
|
|
@@ -952,6 +1027,81 @@ var DEFAULT_STABILIZER_CONFIG = {
|
|
|
952
1027
|
sampleSize: 64
|
|
953
1028
|
};
|
|
954
1029
|
|
|
1030
|
+
// src/constants/errors.ts
|
|
1031
|
+
var API_ERROR_CODES = {
|
|
1032
|
+
SESSION_EXPIRED: "session_expired",
|
|
1033
|
+
INVALID_KEY: "invalid_key",
|
|
1034
|
+
INVALID_MODEL: "invalid_model",
|
|
1035
|
+
INSUFFICIENT_FRAMES: "insufficient_frames",
|
|
1036
|
+
MISSING_FIELD: "missing_field",
|
|
1037
|
+
INVALID_FRAME: "invalid_frame",
|
|
1038
|
+
INVALID_SESSION: "invalid_session",
|
|
1039
|
+
RATE_LIMITED: "rate_limited",
|
|
1040
|
+
STREAM_INCOMPLETE: "stream_incomplete",
|
|
1041
|
+
NO_VERDICT: "no_verdict",
|
|
1042
|
+
TIMEOUT: "timeout",
|
|
1043
|
+
NETWORK_ERROR: "network_error",
|
|
1044
|
+
SERVER_ERROR: "server_error"
|
|
1045
|
+
};
|
|
1046
|
+
var ERROR_MESSAGES = {
|
|
1047
|
+
[API_ERROR_CODES.SESSION_EXPIRED]: "Session expired \u2014 please restart the verification",
|
|
1048
|
+
[API_ERROR_CODES.INVALID_KEY]: "Invalid API key \u2014 contact your administrator",
|
|
1049
|
+
[API_ERROR_CODES.INVALID_MODEL]: "Invalid model configuration",
|
|
1050
|
+
[API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Not enough frames captured \u2014 try again",
|
|
1051
|
+
[API_ERROR_CODES.MISSING_FIELD]: "Missing required data \u2014 please try again",
|
|
1052
|
+
[API_ERROR_CODES.INVALID_FRAME]: "Invalid frame data \u2014 please try again",
|
|
1053
|
+
[API_ERROR_CODES.INVALID_SESSION]: "Invalid session \u2014 please restart",
|
|
1054
|
+
[API_ERROR_CODES.RATE_LIMITED]: "Too many requests \u2014 wait a moment and try again",
|
|
1055
|
+
[API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incomplete \u2014 please try again",
|
|
1056
|
+
[API_ERROR_CODES.NO_VERDICT]: "No result received \u2014 please try again",
|
|
1057
|
+
[API_ERROR_CODES.TIMEOUT]: "Request timed out \u2014 check your connection",
|
|
1058
|
+
[API_ERROR_CODES.NETWORK_ERROR]: "Network error \u2014 check your connection",
|
|
1059
|
+
[API_ERROR_CODES.SERVER_ERROR]: "Server error \u2014 please try again later"
|
|
1060
|
+
};
|
|
1061
|
+
var ERROR_MESSAGES_ES = {
|
|
1062
|
+
[API_ERROR_CODES.SESSION_EXPIRED]: "Sesi\xF3n expirada \u2014 reinicie la verificaci\xF3n",
|
|
1063
|
+
[API_ERROR_CODES.INVALID_KEY]: "Clave API inv\xE1lida \u2014 contacte al administrador",
|
|
1064
|
+
[API_ERROR_CODES.INVALID_MODEL]: "Configuraci\xF3n de modelo inv\xE1lida",
|
|
1065
|
+
[API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Frames insuficientes \u2014 intente de nuevo",
|
|
1066
|
+
[API_ERROR_CODES.MISSING_FIELD]: "Datos requeridos faltantes \u2014 intente de nuevo",
|
|
1067
|
+
[API_ERROR_CODES.INVALID_FRAME]: "Frame inv\xE1lido \u2014 intente de nuevo",
|
|
1068
|
+
[API_ERROR_CODES.INVALID_SESSION]: "Sesi\xF3n inv\xE1lida \u2014 reinicie",
|
|
1069
|
+
[API_ERROR_CODES.RATE_LIMITED]: "Demasiadas solicitudes \u2014 espere un momento",
|
|
1070
|
+
[API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incompleto \u2014 intente de nuevo",
|
|
1071
|
+
[API_ERROR_CODES.NO_VERDICT]: "Sin resultado \u2014 intente de nuevo",
|
|
1072
|
+
[API_ERROR_CODES.TIMEOUT]: "Tiempo de espera agotado \u2014 verifique su conexi\xF3n",
|
|
1073
|
+
[API_ERROR_CODES.NETWORK_ERROR]: "Error de red \u2014 verifique su conexi\xF3n",
|
|
1074
|
+
[API_ERROR_CODES.SERVER_ERROR]: "Error del servidor \u2014 intente m\xE1s tarde"
|
|
1075
|
+
};
|
|
1076
|
+
function looksLikeRawData(text) {
|
|
1077
|
+
const trimmed = text.trim();
|
|
1078
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return true;
|
|
1079
|
+
if (/'error'\s*:/.test(trimmed)) return true;
|
|
1080
|
+
if (/"error"\s*:/.test(trimmed)) return true;
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
var GENERIC_FALLBACK = "An unexpected error occurred \u2014 please try again";
|
|
1084
|
+
function getApiErrorMessage(code, message, customMessages) {
|
|
1085
|
+
if (code) {
|
|
1086
|
+
const custom = customMessages?.[code];
|
|
1087
|
+
if (custom) return custom;
|
|
1088
|
+
const known = ERROR_MESSAGES[code];
|
|
1089
|
+
if (known) return known;
|
|
1090
|
+
}
|
|
1091
|
+
if (message && !looksLikeRawData(message)) {
|
|
1092
|
+
return message;
|
|
1093
|
+
}
|
|
1094
|
+
return GENERIC_FALLBACK;
|
|
1095
|
+
}
|
|
1096
|
+
function isRetryableError(code) {
|
|
1097
|
+
if (!code) return true;
|
|
1098
|
+
const nonRetryable = /* @__PURE__ */ new Set([
|
|
1099
|
+
API_ERROR_CODES.INVALID_KEY,
|
|
1100
|
+
API_ERROR_CODES.INVALID_MODEL
|
|
1101
|
+
]);
|
|
1102
|
+
return !nonRetryable.has(code);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
955
1105
|
// src/constants/feedback.ts
|
|
956
1106
|
var ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
|
|
957
1107
|
var ALIGNMENT_THRESHOLD_POOR = 0.5;
|
|
@@ -1018,6 +1168,12 @@ var FEEDBACK_MESSAGES = {
|
|
|
1018
1168
|
backlit: "Backlit - try facing the light source",
|
|
1019
1169
|
// Hand occlusion
|
|
1020
1170
|
hand_detected: "Remove hand from face",
|
|
1171
|
+
// Eye region quality
|
|
1172
|
+
eyes_not_visible: "Eyes not clearly visible",
|
|
1173
|
+
eyes_shadowed: "Eyes are in shadow - improve lighting",
|
|
1174
|
+
eyes_overexposed: "Eye region overexposed - reduce lighting",
|
|
1175
|
+
glasses_glare: "Glare detected - adjust angle or remove glasses",
|
|
1176
|
+
eye_quality_poor: "Eye region quality is poor",
|
|
1021
1177
|
// Progress messages
|
|
1022
1178
|
capturing: "Capturing...",
|
|
1023
1179
|
almost_done: "Almost done...",
|
|
@@ -1069,6 +1225,12 @@ var ES_LOCALE = {
|
|
|
1069
1225
|
backlit: "Contraluz - intenta mirar hacia la fuente de luz",
|
|
1070
1226
|
// Hand occlusion
|
|
1071
1227
|
hand_detected: "Retira la mano del rostro",
|
|
1228
|
+
// Eye region quality
|
|
1229
|
+
eyes_not_visible: "Ojos no visibles claramente",
|
|
1230
|
+
eyes_shadowed: "Ojos en sombra - mejora la iluminaci\xF3n",
|
|
1231
|
+
eyes_overexposed: "Regi\xF3n de ojos sobreexpuesta - reduce la iluminaci\xF3n",
|
|
1232
|
+
glasses_glare: "Reflejo detectado - ajusta el \xE1ngulo o retira los lentes",
|
|
1233
|
+
eye_quality_poor: "Calidad de la regi\xF3n de ojos insuficiente",
|
|
1072
1234
|
// Progress messages
|
|
1073
1235
|
capturing: "Capturando...",
|
|
1074
1236
|
almost_done: "Casi listo...",
|
|
@@ -1502,6 +1664,44 @@ var LANDMARK_INDEX = {
|
|
|
1502
1664
|
/** Lower lip center */
|
|
1503
1665
|
LOWER_LIP: 14
|
|
1504
1666
|
};
|
|
1667
|
+
var EYE_LANDMARK_INDICES = {
|
|
1668
|
+
/** Right eye contour (viewer's left) */
|
|
1669
|
+
RIGHT_EYE: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
|
|
1670
|
+
/** Left eye contour (viewer's right) */
|
|
1671
|
+
LEFT_EYE: [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398]
|
|
1672
|
+
};
|
|
1673
|
+
function getEyeRegionBounds(landmarks) {
|
|
1674
|
+
if (landmarks.length < 468) return null;
|
|
1675
|
+
const PADDING = 0.2;
|
|
1676
|
+
function computeBounds(indices) {
|
|
1677
|
+
let minX = 1;
|
|
1678
|
+
let minY = 1;
|
|
1679
|
+
let maxX = 0;
|
|
1680
|
+
let maxY = 0;
|
|
1681
|
+
for (const idx of indices) {
|
|
1682
|
+
const lm = landmarks[idx];
|
|
1683
|
+
if (!lm) continue;
|
|
1684
|
+
if (lm.x < minX) minX = lm.x;
|
|
1685
|
+
if (lm.y < minY) minY = lm.y;
|
|
1686
|
+
if (lm.x > maxX) maxX = lm.x;
|
|
1687
|
+
if (lm.y > maxY) maxY = lm.y;
|
|
1688
|
+
}
|
|
1689
|
+
const width = maxX - minX;
|
|
1690
|
+
const height = maxY - minY;
|
|
1691
|
+
const padX = width * PADDING;
|
|
1692
|
+
const padY = height * PADDING;
|
|
1693
|
+
return {
|
|
1694
|
+
x: Math.max(0, minX - padX),
|
|
1695
|
+
y: Math.max(0, minY - padY),
|
|
1696
|
+
width: Math.min(1 - Math.max(0, minX - padX), width + padX * 2),
|
|
1697
|
+
height: Math.min(1 - Math.max(0, minY - padY), height + padY * 2)
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1701
|
+
rightEye: computeBounds(EYE_LANDMARK_INDICES.RIGHT_EYE),
|
|
1702
|
+
leftEye: computeBounds(EYE_LANDMARK_INDICES.LEFT_EYE)
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1505
1705
|
var LANDMARK_MIN_BOUND = 0.1;
|
|
1506
1706
|
var LANDMARK_MAX_BOUND = 0.9;
|
|
1507
1707
|
var MIN_LANDMARK_COUNT = 15;
|
|
@@ -1523,6 +1723,115 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1523
1723
|
}
|
|
1524
1724
|
return { valid: true };
|
|
1525
1725
|
}
|
|
1726
|
+
|
|
1727
|
+
// src/utils/eyeRegionAnalysis.ts
|
|
1728
|
+
var EYE_QUALITY_THRESHOLDS = {
|
|
1729
|
+
minBrightness: 40,
|
|
1730
|
+
maxBrightness: 230,
|
|
1731
|
+
minContrast: 12,
|
|
1732
|
+
glarePixelThreshold: 240,
|
|
1733
|
+
maxGlareRatio: 0.15
|
|
1734
|
+
};
|
|
1735
|
+
function rgbaToLuminance(r, g, b) {
|
|
1736
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
1737
|
+
}
|
|
1738
|
+
function analyzeEyeRegionBrightness(pixels) {
|
|
1739
|
+
const pixelCount = pixels.length / 4;
|
|
1740
|
+
if (pixelCount === 0) return 0;
|
|
1741
|
+
let totalLuminance = 0;
|
|
1742
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1743
|
+
totalLuminance += rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1744
|
+
}
|
|
1745
|
+
return totalLuminance / pixelCount;
|
|
1746
|
+
}
|
|
1747
|
+
function analyzeEyeRegionContrast(pixels, meanBrightness) {
|
|
1748
|
+
const pixelCount = pixels.length / 4;
|
|
1749
|
+
if (pixelCount === 0) return 0;
|
|
1750
|
+
const mean = meanBrightness ?? analyzeEyeRegionBrightness(pixels);
|
|
1751
|
+
let sumSqDiff = 0;
|
|
1752
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1753
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1754
|
+
const diff = lum - mean;
|
|
1755
|
+
sumSqDiff += diff * diff;
|
|
1756
|
+
}
|
|
1757
|
+
return Math.sqrt(sumSqDiff / pixelCount);
|
|
1758
|
+
}
|
|
1759
|
+
function detectSpecularHighlights(pixels, threshold = EYE_QUALITY_THRESHOLDS.glarePixelThreshold) {
|
|
1760
|
+
const pixelCount = pixels.length / 4;
|
|
1761
|
+
if (pixelCount === 0) return 0;
|
|
1762
|
+
let glareCount = 0;
|
|
1763
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1764
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1765
|
+
if (lum >= threshold) {
|
|
1766
|
+
glareCount++;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return glareCount / pixelCount;
|
|
1770
|
+
}
|
|
1771
|
+
function checkEyeRegionQuality(pixels, thresholds = EYE_QUALITY_THRESHOLDS) {
|
|
1772
|
+
if (pixels.length < 4) {
|
|
1773
|
+
return {
|
|
1774
|
+
passed: false,
|
|
1775
|
+
brightness: 0,
|
|
1776
|
+
contrast: 0,
|
|
1777
|
+
hasGlare: false,
|
|
1778
|
+
glareRatio: 0,
|
|
1779
|
+
message: "Eyes not clearly visible"
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
const brightness = analyzeEyeRegionBrightness(pixels);
|
|
1783
|
+
const contrast = analyzeEyeRegionContrast(pixels, brightness);
|
|
1784
|
+
const glareRatio = detectSpecularHighlights(pixels, thresholds.glarePixelThreshold);
|
|
1785
|
+
const hasGlare = glareRatio > thresholds.maxGlareRatio;
|
|
1786
|
+
if (brightness < thresholds.minBrightness) {
|
|
1787
|
+
return {
|
|
1788
|
+
passed: false,
|
|
1789
|
+
brightness,
|
|
1790
|
+
contrast,
|
|
1791
|
+
hasGlare,
|
|
1792
|
+
glareRatio,
|
|
1793
|
+
message: "Eyes are in shadow - improve lighting"
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
if (brightness > thresholds.maxBrightness) {
|
|
1797
|
+
return {
|
|
1798
|
+
passed: false,
|
|
1799
|
+
brightness,
|
|
1800
|
+
contrast,
|
|
1801
|
+
hasGlare,
|
|
1802
|
+
glareRatio,
|
|
1803
|
+
message: "Eye region overexposed - reduce lighting"
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (hasGlare) {
|
|
1807
|
+
return {
|
|
1808
|
+
passed: false,
|
|
1809
|
+
brightness,
|
|
1810
|
+
contrast,
|
|
1811
|
+
hasGlare,
|
|
1812
|
+
glareRatio,
|
|
1813
|
+
message: "Glare detected - adjust angle or remove glasses"
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
if (contrast < thresholds.minContrast) {
|
|
1817
|
+
return {
|
|
1818
|
+
passed: false,
|
|
1819
|
+
brightness,
|
|
1820
|
+
contrast,
|
|
1821
|
+
hasGlare,
|
|
1822
|
+
glareRatio,
|
|
1823
|
+
message: "Eyes not clearly visible"
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
passed: true,
|
|
1828
|
+
brightness,
|
|
1829
|
+
contrast,
|
|
1830
|
+
hasGlare,
|
|
1831
|
+
glareRatio,
|
|
1832
|
+
message: null
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1526
1835
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1527
1836
|
0 && (module.exports = {
|
|
1528
1837
|
ALIGNMENT_THRESHOLD_CAPTURE,
|
|
@@ -1530,6 +1839,7 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1530
1839
|
ALIGNMENT_THRESHOLD_PERFECT,
|
|
1531
1840
|
ALIGNMENT_THRESHOLD_POOR,
|
|
1532
1841
|
API_ENDPOINTS,
|
|
1842
|
+
API_ERROR_CODES,
|
|
1533
1843
|
API_PATHS,
|
|
1534
1844
|
AUTH_CONFIG,
|
|
1535
1845
|
BACKLIT_RATIO_THRESHOLD,
|
|
@@ -1545,7 +1855,11 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1545
1855
|
DEFAULT_OVAL_REGION,
|
|
1546
1856
|
DEFAULT_STABILIZER_CONFIG,
|
|
1547
1857
|
DEFAULT_STATUS_MESSAGES,
|
|
1858
|
+
ERROR_MESSAGES,
|
|
1859
|
+
ERROR_MESSAGES_ES,
|
|
1548
1860
|
ES_LOCALE,
|
|
1861
|
+
EYE_LANDMARK_INDICES,
|
|
1862
|
+
EYE_QUALITY_THRESHOLDS,
|
|
1549
1863
|
FACE_CENTER_VERTICAL_OFFSET,
|
|
1550
1864
|
FACE_CROP_OUTPUT_SIZE,
|
|
1551
1865
|
FEEDBACK_MESSAGES,
|
|
@@ -1579,17 +1893,23 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1579
1893
|
RETRY_CONFIG,
|
|
1580
1894
|
TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
1581
1895
|
analyzeBlur,
|
|
1896
|
+
analyzeEyeRegionBrightness,
|
|
1897
|
+
analyzeEyeRegionContrast,
|
|
1582
1898
|
analyzeLighting,
|
|
1583
1899
|
calculateAdaptiveCropMultiplier,
|
|
1584
1900
|
calculateBrightness,
|
|
1585
1901
|
calculateFaceAlignment,
|
|
1586
1902
|
calculateFaceCropRegion,
|
|
1587
1903
|
canCaptureFrame,
|
|
1904
|
+
checkEyeRegionQuality,
|
|
1588
1905
|
checkFrameQuality,
|
|
1589
1906
|
decodeBase64,
|
|
1907
|
+
detectSpecularHighlights,
|
|
1590
1908
|
encodeBase64,
|
|
1591
1909
|
generateSessionId,
|
|
1910
|
+
getApiErrorMessage,
|
|
1592
1911
|
getCaptureQualityFeedback,
|
|
1912
|
+
getEyeRegionBounds,
|
|
1593
1913
|
getFeedbackMessage,
|
|
1594
1914
|
getMinFramesForModel,
|
|
1595
1915
|
getOvalGuideState,
|
|
@@ -1598,6 +1918,7 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1598
1918
|
isFaceCropFullyInFrame,
|
|
1599
1919
|
isFaceFullyVisible,
|
|
1600
1920
|
isFaceInOval,
|
|
1921
|
+
isRetryableError,
|
|
1601
1922
|
retryWithBackoff,
|
|
1602
1923
|
rgbaToGrayscale,
|
|
1603
1924
|
sleep,
|
package/dist/index.mjs
CHANGED
|
@@ -135,7 +135,8 @@ function toLivenessResultFromStream(response) {
|
|
|
135
135
|
throw new LivenessApiError("Stream not complete", "stream_incomplete", 400);
|
|
136
136
|
}
|
|
137
137
|
if (!response.verdict) {
|
|
138
|
-
|
|
138
|
+
const errorCode = response.error ?? "no_verdict";
|
|
139
|
+
throw new LivenessApiError(response.error ?? "No verdict received", errorCode, 500);
|
|
139
140
|
}
|
|
140
141
|
return {
|
|
141
142
|
verdict: response.verdict,
|
|
@@ -156,7 +157,7 @@ function generateSessionId() {
|
|
|
156
157
|
return v.toString(16);
|
|
157
158
|
});
|
|
158
159
|
}
|
|
159
|
-
var LivenessClient = class {
|
|
160
|
+
var LivenessClient = class _LivenessClient {
|
|
160
161
|
constructor(config) {
|
|
161
162
|
this.baseUrl = (config.baseUrl ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
162
163
|
this.apiKey = config.apiKey;
|
|
@@ -183,14 +184,7 @@ var LivenessClient = class {
|
|
|
183
184
|
});
|
|
184
185
|
clearTimeout(timeoutId);
|
|
185
186
|
if (!response.ok) {
|
|
186
|
-
|
|
187
|
-
throw new LivenessApiError(
|
|
188
|
-
errorData.message,
|
|
189
|
-
errorData.error,
|
|
190
|
-
response.status,
|
|
191
|
-
errorData.required,
|
|
192
|
-
errorData.received
|
|
193
|
-
);
|
|
187
|
+
throw await this.parseErrorResponse(response);
|
|
194
188
|
}
|
|
195
189
|
return await response.json();
|
|
196
190
|
} catch (error) {
|
|
@@ -208,6 +202,75 @@ var LivenessClient = class {
|
|
|
208
202
|
);
|
|
209
203
|
}
|
|
210
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Parse an error response body, handling both JSON and non-JSON bodies.
|
|
207
|
+
*
|
|
208
|
+
* The backend sometimes wraps the real error in a generic envelope:
|
|
209
|
+
* ```json
|
|
210
|
+
* { "error": "http_error", "message": "{'error': 'session_expired', ...}" }
|
|
211
|
+
* ```
|
|
212
|
+
* This method unwraps such envelopes to extract the real error code.
|
|
213
|
+
*/
|
|
214
|
+
async parseErrorResponse(response) {
|
|
215
|
+
const status = response.status;
|
|
216
|
+
let body;
|
|
217
|
+
try {
|
|
218
|
+
body = await response.text();
|
|
219
|
+
} catch {
|
|
220
|
+
return new LivenessApiError(`Request failed with status ${status}`, "server_error", status);
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const data = JSON.parse(body);
|
|
224
|
+
if (data.error) {
|
|
225
|
+
const realCode = _LivenessClient.unwrapErrorCode(data.error, data.message);
|
|
226
|
+
const realMessage = _LivenessClient.unwrapErrorMessage(data.message);
|
|
227
|
+
return new LivenessApiError(
|
|
228
|
+
realMessage ?? `Request failed (${realCode})`,
|
|
229
|
+
realCode,
|
|
230
|
+
status,
|
|
231
|
+
data.required,
|
|
232
|
+
data.received
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
const code = _LivenessClient.extractCodeFromText(body) ?? "server_error";
|
|
238
|
+
return new LivenessApiError(`Request failed with status ${status}`, code, status);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* When the outer error code is a generic wrapper (e.g. "http_error"),
|
|
242
|
+
* try to extract the real error code from the message text.
|
|
243
|
+
*/
|
|
244
|
+
static unwrapErrorCode(outerCode, message) {
|
|
245
|
+
const WRAPPER_CODES = /* @__PURE__ */ new Set([
|
|
246
|
+
"http_error",
|
|
247
|
+
"upstream_error",
|
|
248
|
+
"proxy_error",
|
|
249
|
+
"internal_error"
|
|
250
|
+
]);
|
|
251
|
+
if (!WRAPPER_CODES.has(outerCode) || !message) {
|
|
252
|
+
return outerCode;
|
|
253
|
+
}
|
|
254
|
+
return _LivenessClient.extractCodeFromText(message) ?? outerCode;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Try to extract a human-readable message from a nested Python dict string.
|
|
258
|
+
* E.g.: "{'error': 'session_expired', 'message': \"Create a new session...\"}"
|
|
259
|
+
* → "Create a new session..."
|
|
260
|
+
*/
|
|
261
|
+
static unwrapErrorMessage(message) {
|
|
262
|
+
if (!message) return void 0;
|
|
263
|
+
const msgMatch = /'message'\s*:\s*"([^"]+)"/.exec(message) ?? /'message'\s*:\s*'([^']+)'/.exec(message) ?? /"message"\s*:\s*"([^"]+)"/.exec(message);
|
|
264
|
+
return msgMatch?.[1] ?? void 0;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Extract an error code from raw text using regex.
|
|
268
|
+
* Handles both Python dict (`'error': 'code'`) and JSON (`"error": "code"`).
|
|
269
|
+
*/
|
|
270
|
+
static extractCodeFromText(text) {
|
|
271
|
+
const match = /'error'\s*:\s*'([^']+)'/.exec(text) ?? /"error"\s*:\s*"([^"]+)"/.exec(text);
|
|
272
|
+
return match?.[1] ?? null;
|
|
273
|
+
}
|
|
211
274
|
/**
|
|
212
275
|
* Make a request with optional retry
|
|
213
276
|
*/
|
|
@@ -839,6 +902,81 @@ var DEFAULT_STABILIZER_CONFIG = {
|
|
|
839
902
|
sampleSize: 64
|
|
840
903
|
};
|
|
841
904
|
|
|
905
|
+
// src/constants/errors.ts
|
|
906
|
+
var API_ERROR_CODES = {
|
|
907
|
+
SESSION_EXPIRED: "session_expired",
|
|
908
|
+
INVALID_KEY: "invalid_key",
|
|
909
|
+
INVALID_MODEL: "invalid_model",
|
|
910
|
+
INSUFFICIENT_FRAMES: "insufficient_frames",
|
|
911
|
+
MISSING_FIELD: "missing_field",
|
|
912
|
+
INVALID_FRAME: "invalid_frame",
|
|
913
|
+
INVALID_SESSION: "invalid_session",
|
|
914
|
+
RATE_LIMITED: "rate_limited",
|
|
915
|
+
STREAM_INCOMPLETE: "stream_incomplete",
|
|
916
|
+
NO_VERDICT: "no_verdict",
|
|
917
|
+
TIMEOUT: "timeout",
|
|
918
|
+
NETWORK_ERROR: "network_error",
|
|
919
|
+
SERVER_ERROR: "server_error"
|
|
920
|
+
};
|
|
921
|
+
var ERROR_MESSAGES = {
|
|
922
|
+
[API_ERROR_CODES.SESSION_EXPIRED]: "Session expired \u2014 please restart the verification",
|
|
923
|
+
[API_ERROR_CODES.INVALID_KEY]: "Invalid API key \u2014 contact your administrator",
|
|
924
|
+
[API_ERROR_CODES.INVALID_MODEL]: "Invalid model configuration",
|
|
925
|
+
[API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Not enough frames captured \u2014 try again",
|
|
926
|
+
[API_ERROR_CODES.MISSING_FIELD]: "Missing required data \u2014 please try again",
|
|
927
|
+
[API_ERROR_CODES.INVALID_FRAME]: "Invalid frame data \u2014 please try again",
|
|
928
|
+
[API_ERROR_CODES.INVALID_SESSION]: "Invalid session \u2014 please restart",
|
|
929
|
+
[API_ERROR_CODES.RATE_LIMITED]: "Too many requests \u2014 wait a moment and try again",
|
|
930
|
+
[API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incomplete \u2014 please try again",
|
|
931
|
+
[API_ERROR_CODES.NO_VERDICT]: "No result received \u2014 please try again",
|
|
932
|
+
[API_ERROR_CODES.TIMEOUT]: "Request timed out \u2014 check your connection",
|
|
933
|
+
[API_ERROR_CODES.NETWORK_ERROR]: "Network error \u2014 check your connection",
|
|
934
|
+
[API_ERROR_CODES.SERVER_ERROR]: "Server error \u2014 please try again later"
|
|
935
|
+
};
|
|
936
|
+
var ERROR_MESSAGES_ES = {
|
|
937
|
+
[API_ERROR_CODES.SESSION_EXPIRED]: "Sesi\xF3n expirada \u2014 reinicie la verificaci\xF3n",
|
|
938
|
+
[API_ERROR_CODES.INVALID_KEY]: "Clave API inv\xE1lida \u2014 contacte al administrador",
|
|
939
|
+
[API_ERROR_CODES.INVALID_MODEL]: "Configuraci\xF3n de modelo inv\xE1lida",
|
|
940
|
+
[API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Frames insuficientes \u2014 intente de nuevo",
|
|
941
|
+
[API_ERROR_CODES.MISSING_FIELD]: "Datos requeridos faltantes \u2014 intente de nuevo",
|
|
942
|
+
[API_ERROR_CODES.INVALID_FRAME]: "Frame inv\xE1lido \u2014 intente de nuevo",
|
|
943
|
+
[API_ERROR_CODES.INVALID_SESSION]: "Sesi\xF3n inv\xE1lida \u2014 reinicie",
|
|
944
|
+
[API_ERROR_CODES.RATE_LIMITED]: "Demasiadas solicitudes \u2014 espere un momento",
|
|
945
|
+
[API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incompleto \u2014 intente de nuevo",
|
|
946
|
+
[API_ERROR_CODES.NO_VERDICT]: "Sin resultado \u2014 intente de nuevo",
|
|
947
|
+
[API_ERROR_CODES.TIMEOUT]: "Tiempo de espera agotado \u2014 verifique su conexi\xF3n",
|
|
948
|
+
[API_ERROR_CODES.NETWORK_ERROR]: "Error de red \u2014 verifique su conexi\xF3n",
|
|
949
|
+
[API_ERROR_CODES.SERVER_ERROR]: "Error del servidor \u2014 intente m\xE1s tarde"
|
|
950
|
+
};
|
|
951
|
+
function looksLikeRawData(text) {
|
|
952
|
+
const trimmed = text.trim();
|
|
953
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return true;
|
|
954
|
+
if (/'error'\s*:/.test(trimmed)) return true;
|
|
955
|
+
if (/"error"\s*:/.test(trimmed)) return true;
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
var GENERIC_FALLBACK = "An unexpected error occurred \u2014 please try again";
|
|
959
|
+
function getApiErrorMessage(code, message, customMessages) {
|
|
960
|
+
if (code) {
|
|
961
|
+
const custom = customMessages?.[code];
|
|
962
|
+
if (custom) return custom;
|
|
963
|
+
const known = ERROR_MESSAGES[code];
|
|
964
|
+
if (known) return known;
|
|
965
|
+
}
|
|
966
|
+
if (message && !looksLikeRawData(message)) {
|
|
967
|
+
return message;
|
|
968
|
+
}
|
|
969
|
+
return GENERIC_FALLBACK;
|
|
970
|
+
}
|
|
971
|
+
function isRetryableError(code) {
|
|
972
|
+
if (!code) return true;
|
|
973
|
+
const nonRetryable = /* @__PURE__ */ new Set([
|
|
974
|
+
API_ERROR_CODES.INVALID_KEY,
|
|
975
|
+
API_ERROR_CODES.INVALID_MODEL
|
|
976
|
+
]);
|
|
977
|
+
return !nonRetryable.has(code);
|
|
978
|
+
}
|
|
979
|
+
|
|
842
980
|
// src/constants/feedback.ts
|
|
843
981
|
var ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
|
|
844
982
|
var ALIGNMENT_THRESHOLD_POOR = 0.5;
|
|
@@ -905,6 +1043,12 @@ var FEEDBACK_MESSAGES = {
|
|
|
905
1043
|
backlit: "Backlit - try facing the light source",
|
|
906
1044
|
// Hand occlusion
|
|
907
1045
|
hand_detected: "Remove hand from face",
|
|
1046
|
+
// Eye region quality
|
|
1047
|
+
eyes_not_visible: "Eyes not clearly visible",
|
|
1048
|
+
eyes_shadowed: "Eyes are in shadow - improve lighting",
|
|
1049
|
+
eyes_overexposed: "Eye region overexposed - reduce lighting",
|
|
1050
|
+
glasses_glare: "Glare detected - adjust angle or remove glasses",
|
|
1051
|
+
eye_quality_poor: "Eye region quality is poor",
|
|
908
1052
|
// Progress messages
|
|
909
1053
|
capturing: "Capturing...",
|
|
910
1054
|
almost_done: "Almost done...",
|
|
@@ -956,6 +1100,12 @@ var ES_LOCALE = {
|
|
|
956
1100
|
backlit: "Contraluz - intenta mirar hacia la fuente de luz",
|
|
957
1101
|
// Hand occlusion
|
|
958
1102
|
hand_detected: "Retira la mano del rostro",
|
|
1103
|
+
// Eye region quality
|
|
1104
|
+
eyes_not_visible: "Ojos no visibles claramente",
|
|
1105
|
+
eyes_shadowed: "Ojos en sombra - mejora la iluminaci\xF3n",
|
|
1106
|
+
eyes_overexposed: "Regi\xF3n de ojos sobreexpuesta - reduce la iluminaci\xF3n",
|
|
1107
|
+
glasses_glare: "Reflejo detectado - ajusta el \xE1ngulo o retira los lentes",
|
|
1108
|
+
eye_quality_poor: "Calidad de la regi\xF3n de ojos insuficiente",
|
|
959
1109
|
// Progress messages
|
|
960
1110
|
capturing: "Capturando...",
|
|
961
1111
|
almost_done: "Casi listo...",
|
|
@@ -1389,6 +1539,44 @@ var LANDMARK_INDEX = {
|
|
|
1389
1539
|
/** Lower lip center */
|
|
1390
1540
|
LOWER_LIP: 14
|
|
1391
1541
|
};
|
|
1542
|
+
var EYE_LANDMARK_INDICES = {
|
|
1543
|
+
/** Right eye contour (viewer's left) */
|
|
1544
|
+
RIGHT_EYE: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
|
|
1545
|
+
/** Left eye contour (viewer's right) */
|
|
1546
|
+
LEFT_EYE: [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398]
|
|
1547
|
+
};
|
|
1548
|
+
function getEyeRegionBounds(landmarks) {
|
|
1549
|
+
if (landmarks.length < 468) return null;
|
|
1550
|
+
const PADDING = 0.2;
|
|
1551
|
+
function computeBounds(indices) {
|
|
1552
|
+
let minX = 1;
|
|
1553
|
+
let minY = 1;
|
|
1554
|
+
let maxX = 0;
|
|
1555
|
+
let maxY = 0;
|
|
1556
|
+
for (const idx of indices) {
|
|
1557
|
+
const lm = landmarks[idx];
|
|
1558
|
+
if (!lm) continue;
|
|
1559
|
+
if (lm.x < minX) minX = lm.x;
|
|
1560
|
+
if (lm.y < minY) minY = lm.y;
|
|
1561
|
+
if (lm.x > maxX) maxX = lm.x;
|
|
1562
|
+
if (lm.y > maxY) maxY = lm.y;
|
|
1563
|
+
}
|
|
1564
|
+
const width = maxX - minX;
|
|
1565
|
+
const height = maxY - minY;
|
|
1566
|
+
const padX = width * PADDING;
|
|
1567
|
+
const padY = height * PADDING;
|
|
1568
|
+
return {
|
|
1569
|
+
x: Math.max(0, minX - padX),
|
|
1570
|
+
y: Math.max(0, minY - padY),
|
|
1571
|
+
width: Math.min(1 - Math.max(0, minX - padX), width + padX * 2),
|
|
1572
|
+
height: Math.min(1 - Math.max(0, minY - padY), height + padY * 2)
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
rightEye: computeBounds(EYE_LANDMARK_INDICES.RIGHT_EYE),
|
|
1577
|
+
leftEye: computeBounds(EYE_LANDMARK_INDICES.LEFT_EYE)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1392
1580
|
var LANDMARK_MIN_BOUND = 0.1;
|
|
1393
1581
|
var LANDMARK_MAX_BOUND = 0.9;
|
|
1394
1582
|
var MIN_LANDMARK_COUNT = 15;
|
|
@@ -1410,12 +1598,122 @@ function validateFaceLandmarks(landmarks) {
|
|
|
1410
1598
|
}
|
|
1411
1599
|
return { valid: true };
|
|
1412
1600
|
}
|
|
1601
|
+
|
|
1602
|
+
// src/utils/eyeRegionAnalysis.ts
|
|
1603
|
+
var EYE_QUALITY_THRESHOLDS = {
|
|
1604
|
+
minBrightness: 40,
|
|
1605
|
+
maxBrightness: 230,
|
|
1606
|
+
minContrast: 12,
|
|
1607
|
+
glarePixelThreshold: 240,
|
|
1608
|
+
maxGlareRatio: 0.15
|
|
1609
|
+
};
|
|
1610
|
+
function rgbaToLuminance(r, g, b) {
|
|
1611
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
1612
|
+
}
|
|
1613
|
+
function analyzeEyeRegionBrightness(pixels) {
|
|
1614
|
+
const pixelCount = pixels.length / 4;
|
|
1615
|
+
if (pixelCount === 0) return 0;
|
|
1616
|
+
let totalLuminance = 0;
|
|
1617
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1618
|
+
totalLuminance += rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1619
|
+
}
|
|
1620
|
+
return totalLuminance / pixelCount;
|
|
1621
|
+
}
|
|
1622
|
+
function analyzeEyeRegionContrast(pixels, meanBrightness) {
|
|
1623
|
+
const pixelCount = pixels.length / 4;
|
|
1624
|
+
if (pixelCount === 0) return 0;
|
|
1625
|
+
const mean = meanBrightness ?? analyzeEyeRegionBrightness(pixels);
|
|
1626
|
+
let sumSqDiff = 0;
|
|
1627
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1628
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1629
|
+
const diff = lum - mean;
|
|
1630
|
+
sumSqDiff += diff * diff;
|
|
1631
|
+
}
|
|
1632
|
+
return Math.sqrt(sumSqDiff / pixelCount);
|
|
1633
|
+
}
|
|
1634
|
+
function detectSpecularHighlights(pixels, threshold = EYE_QUALITY_THRESHOLDS.glarePixelThreshold) {
|
|
1635
|
+
const pixelCount = pixels.length / 4;
|
|
1636
|
+
if (pixelCount === 0) return 0;
|
|
1637
|
+
let glareCount = 0;
|
|
1638
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1639
|
+
const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
|
|
1640
|
+
if (lum >= threshold) {
|
|
1641
|
+
glareCount++;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return glareCount / pixelCount;
|
|
1645
|
+
}
|
|
1646
|
+
function checkEyeRegionQuality(pixels, thresholds = EYE_QUALITY_THRESHOLDS) {
|
|
1647
|
+
if (pixels.length < 4) {
|
|
1648
|
+
return {
|
|
1649
|
+
passed: false,
|
|
1650
|
+
brightness: 0,
|
|
1651
|
+
contrast: 0,
|
|
1652
|
+
hasGlare: false,
|
|
1653
|
+
glareRatio: 0,
|
|
1654
|
+
message: "Eyes not clearly visible"
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
const brightness = analyzeEyeRegionBrightness(pixels);
|
|
1658
|
+
const contrast = analyzeEyeRegionContrast(pixels, brightness);
|
|
1659
|
+
const glareRatio = detectSpecularHighlights(pixels, thresholds.glarePixelThreshold);
|
|
1660
|
+
const hasGlare = glareRatio > thresholds.maxGlareRatio;
|
|
1661
|
+
if (brightness < thresholds.minBrightness) {
|
|
1662
|
+
return {
|
|
1663
|
+
passed: false,
|
|
1664
|
+
brightness,
|
|
1665
|
+
contrast,
|
|
1666
|
+
hasGlare,
|
|
1667
|
+
glareRatio,
|
|
1668
|
+
message: "Eyes are in shadow - improve lighting"
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
if (brightness > thresholds.maxBrightness) {
|
|
1672
|
+
return {
|
|
1673
|
+
passed: false,
|
|
1674
|
+
brightness,
|
|
1675
|
+
contrast,
|
|
1676
|
+
hasGlare,
|
|
1677
|
+
glareRatio,
|
|
1678
|
+
message: "Eye region overexposed - reduce lighting"
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
if (hasGlare) {
|
|
1682
|
+
return {
|
|
1683
|
+
passed: false,
|
|
1684
|
+
brightness,
|
|
1685
|
+
contrast,
|
|
1686
|
+
hasGlare,
|
|
1687
|
+
glareRatio,
|
|
1688
|
+
message: "Glare detected - adjust angle or remove glasses"
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
if (contrast < thresholds.minContrast) {
|
|
1692
|
+
return {
|
|
1693
|
+
passed: false,
|
|
1694
|
+
brightness,
|
|
1695
|
+
contrast,
|
|
1696
|
+
hasGlare,
|
|
1697
|
+
glareRatio,
|
|
1698
|
+
message: "Eyes not clearly visible"
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
return {
|
|
1702
|
+
passed: true,
|
|
1703
|
+
brightness,
|
|
1704
|
+
contrast,
|
|
1705
|
+
hasGlare,
|
|
1706
|
+
glareRatio,
|
|
1707
|
+
message: null
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1413
1710
|
export {
|
|
1414
1711
|
ALIGNMENT_THRESHOLD_CAPTURE,
|
|
1415
1712
|
ALIGNMENT_THRESHOLD_GOOD,
|
|
1416
1713
|
ALIGNMENT_THRESHOLD_PERFECT,
|
|
1417
1714
|
ALIGNMENT_THRESHOLD_POOR,
|
|
1418
1715
|
API_ENDPOINTS,
|
|
1716
|
+
API_ERROR_CODES,
|
|
1419
1717
|
API_PATHS,
|
|
1420
1718
|
AUTH_CONFIG,
|
|
1421
1719
|
BACKLIT_RATIO_THRESHOLD,
|
|
@@ -1431,7 +1729,11 @@ export {
|
|
|
1431
1729
|
DEFAULT_OVAL_REGION,
|
|
1432
1730
|
DEFAULT_STABILIZER_CONFIG,
|
|
1433
1731
|
DEFAULT_STATUS_MESSAGES,
|
|
1732
|
+
ERROR_MESSAGES,
|
|
1733
|
+
ERROR_MESSAGES_ES,
|
|
1434
1734
|
ES_LOCALE,
|
|
1735
|
+
EYE_LANDMARK_INDICES,
|
|
1736
|
+
EYE_QUALITY_THRESHOLDS,
|
|
1435
1737
|
FACE_CENTER_VERTICAL_OFFSET,
|
|
1436
1738
|
FACE_CROP_OUTPUT_SIZE,
|
|
1437
1739
|
FEEDBACK_MESSAGES,
|
|
@@ -1465,17 +1767,23 @@ export {
|
|
|
1465
1767
|
RETRY_CONFIG,
|
|
1466
1768
|
TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
1467
1769
|
analyzeBlur,
|
|
1770
|
+
analyzeEyeRegionBrightness,
|
|
1771
|
+
analyzeEyeRegionContrast,
|
|
1468
1772
|
analyzeLighting,
|
|
1469
1773
|
calculateAdaptiveCropMultiplier,
|
|
1470
1774
|
calculateBrightness,
|
|
1471
1775
|
calculateFaceAlignment,
|
|
1472
1776
|
calculateFaceCropRegion,
|
|
1473
1777
|
canCaptureFrame,
|
|
1778
|
+
checkEyeRegionQuality,
|
|
1474
1779
|
checkFrameQuality,
|
|
1475
1780
|
decodeBase64,
|
|
1781
|
+
detectSpecularHighlights,
|
|
1476
1782
|
encodeBase64,
|
|
1477
1783
|
generateSessionId,
|
|
1784
|
+
getApiErrorMessage,
|
|
1478
1785
|
getCaptureQualityFeedback,
|
|
1786
|
+
getEyeRegionBounds,
|
|
1479
1787
|
getFeedbackMessage,
|
|
1480
1788
|
getMinFramesForModel,
|
|
1481
1789
|
getOvalGuideState,
|
|
@@ -1484,6 +1792,7 @@ export {
|
|
|
1484
1792
|
isFaceCropFullyInFrame,
|
|
1485
1793
|
isFaceFullyVisible,
|
|
1486
1794
|
isFaceInOval,
|
|
1795
|
+
isRetryableError,
|
|
1487
1796
|
retryWithBackoff,
|
|
1488
1797
|
rgbaToGrayscale,
|
|
1489
1798
|
sleep,
|