@simprints/simface-sdk 0.4.1 → 0.6.1

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.
@@ -0,0 +1,69 @@
1
+ import { FaceDetector, FilesetResolver } from '@mediapipe/tasks-vision';
2
+ import { evaluateFaceQuality } from './face-quality.js';
3
+ const WASM_CDN = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm';
4
+ const MODEL_URL = 'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite';
5
+ // Minimum confidence to consider a face detected
6
+ const MIN_CONFIDENCE = 0.7;
7
+ let visionInstance = null;
8
+ let imageDetectorInstance = null;
9
+ let videoDetectorInstance = null;
10
+ function getVision() {
11
+ visionInstance ?? (visionInstance = FilesetResolver.forVisionTasks(WASM_CDN));
12
+ return visionInstance;
13
+ }
14
+ function getImageDetector() {
15
+ imageDetectorInstance ?? (imageDetectorInstance = createDetector('IMAGE'));
16
+ return imageDetectorInstance;
17
+ }
18
+ export function getVideoDetector() {
19
+ videoDetectorInstance ?? (videoDetectorInstance = createDetector('VIDEO'));
20
+ return videoDetectorInstance;
21
+ }
22
+ async function createDetector(runningMode) {
23
+ const vision = await getVision();
24
+ return FaceDetector.createFromOptions(vision, {
25
+ baseOptions: { modelAssetPath: MODEL_URL },
26
+ runningMode,
27
+ minDetectionConfidence: MIN_CONFIDENCE,
28
+ });
29
+ }
30
+ /**
31
+ * Assess face quality from a captured image.
32
+ * Checks: face present, single face, face centered, sufficient size.
33
+ */
34
+ export async function assessFaceQuality(imageElement) {
35
+ const detector = await getImageDetector();
36
+ const result = detector.detect(imageElement);
37
+ return evaluateFaceQuality({
38
+ detections: mapDetections(result.detections),
39
+ width: imageElement.naturalWidth,
40
+ height: imageElement.naturalHeight,
41
+ });
42
+ }
43
+ export async function assessFaceQualityForVideo(videoElement, timestamp) {
44
+ const detector = await getVideoDetector();
45
+ const result = detector.detectForVideo(videoElement, timestamp);
46
+ return evaluateFaceQuality({
47
+ detections: mapDetections(result.detections),
48
+ width: videoElement.videoWidth,
49
+ height: videoElement.videoHeight,
50
+ });
51
+ }
52
+ function mapDetections(detections) {
53
+ return detections.map((detection) => ({
54
+ boundingBox: detection.boundingBox
55
+ ? {
56
+ originX: detection.boundingBox.originX,
57
+ originY: detection.boundingBox.originY,
58
+ width: detection.boundingBox.width,
59
+ height: detection.boundingBox.height,
60
+ }
61
+ : undefined,
62
+ confidence: detection.categories[0]?.score ?? 0,
63
+ keypoints: detection.keypoints.map((keypoint) => ({
64
+ x: keypoint.x,
65
+ y: keypoint.y,
66
+ })),
67
+ }));
68
+ }
69
+ //# sourceMappingURL=face-detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"face-detection.js","sourceRoot":"","sources":["../../src/services/face-detection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { FaceQualityResult } from '../types/index.js';
2
+ export interface FaceBoundingBox {
3
+ originX: number;
4
+ originY: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface FaceKeypoint {
9
+ x: number;
10
+ y: number;
11
+ }
12
+ export interface FaceDetectionSnapshot {
13
+ boundingBox?: FaceBoundingBox;
14
+ confidence: number;
15
+ keypoints: FaceKeypoint[];
16
+ }
17
+ interface FaceQualityInput {
18
+ width: number;
19
+ height: number;
20
+ detections: FaceDetectionSnapshot[];
21
+ }
22
+ export declare function evaluateFaceQuality(input: FaceQualityInput): FaceQualityResult;
23
+ export {};
@@ -0,0 +1,136 @@
1
+ const MIN_FACE_AREA_RATIO = 0.1;
2
+ const MAX_FACE_AREA_RATIO = 0.42;
3
+ const IDEAL_FACE_AREA_RATIO = 0.24;
4
+ const CENTER_TOLERANCE_X = 0.14;
5
+ const CENTER_TOLERANCE_Y = 0.18;
6
+ const MAX_NOSE_OFFSET_RATIO = 0.12;
7
+ const KEYPOINT_RIGHT_EYE = 0;
8
+ const KEYPOINT_LEFT_EYE = 1;
9
+ const KEYPOINT_NOSE = 2;
10
+ export function evaluateFaceQuality(input) {
11
+ const { detections, width, height } = input;
12
+ if (detections.length === 0) {
13
+ return createQualityResult({
14
+ hasFace: false,
15
+ faceCount: 0,
16
+ confidence: 0,
17
+ captureScore: 0,
18
+ isCentered: false,
19
+ passesQualityChecks: false,
20
+ feedback: 'no-face',
21
+ message: 'No face detected. Center your face in the oval and look at the camera.',
22
+ });
23
+ }
24
+ if (detections.length > 1) {
25
+ return createQualityResult({
26
+ hasFace: true,
27
+ faceCount: detections.length,
28
+ confidence: detections[0]?.confidence ?? 0,
29
+ captureScore: 0,
30
+ isCentered: false,
31
+ passesQualityChecks: false,
32
+ feedback: 'multiple-faces',
33
+ message: 'Multiple faces detected. Make sure only one person is in view.',
34
+ });
35
+ }
36
+ const detection = detections[0];
37
+ const bbox = detection.boundingBox;
38
+ if (!bbox) {
39
+ return createQualityResult({
40
+ hasFace: true,
41
+ faceCount: 1,
42
+ confidence: detection.confidence,
43
+ captureScore: 0,
44
+ isCentered: false,
45
+ passesQualityChecks: false,
46
+ feedback: 'face-unclear',
47
+ message: 'Face detected, but the frame is unclear. Hold still and try again.',
48
+ });
49
+ }
50
+ const faceAreaRatio = (bbox.width * bbox.height) / (width * height);
51
+ if (faceAreaRatio < MIN_FACE_AREA_RATIO) {
52
+ return createFrameAdjustmentResult(detection.confidence, 'too-far', 'Move closer to the camera.');
53
+ }
54
+ if (faceAreaRatio > MAX_FACE_AREA_RATIO) {
55
+ return createFrameAdjustmentResult(detection.confidence, 'too-close', 'Move slightly farther away.');
56
+ }
57
+ const faceCenterX = (bbox.originX + bbox.width / 2) / width;
58
+ const faceCenterY = (bbox.originY + bbox.height / 2) / height;
59
+ if (faceCenterX < 0.5 - CENTER_TOLERANCE_X) {
60
+ return createFrameAdjustmentResult(detection.confidence, 'move-right', 'Move your face a little to the right.');
61
+ }
62
+ if (faceCenterX > 0.5 + CENTER_TOLERANCE_X) {
63
+ return createFrameAdjustmentResult(detection.confidence, 'move-left', 'Move your face a little to the left.');
64
+ }
65
+ if (faceCenterY < 0.5 - CENTER_TOLERANCE_Y) {
66
+ return createFrameAdjustmentResult(detection.confidence, 'move-down', 'Move your face slightly down.');
67
+ }
68
+ if (faceCenterY > 0.5 + CENTER_TOLERANCE_Y) {
69
+ return createFrameAdjustmentResult(detection.confidence, 'move-up', 'Move your face slightly up.');
70
+ }
71
+ const turnFeedback = detectTurnFeedback(detection.keypoints);
72
+ if (turnFeedback) {
73
+ return createFrameAdjustmentResult(detection.confidence, turnFeedback.feedback, turnFeedback.message);
74
+ }
75
+ return createQualityResult({
76
+ hasFace: true,
77
+ faceCount: 1,
78
+ confidence: detection.confidence,
79
+ captureScore: calculateCaptureScore(detection.confidence, faceCenterX, faceCenterY, faceAreaRatio),
80
+ isCentered: true,
81
+ passesQualityChecks: true,
82
+ feedback: 'good',
83
+ message: 'Hold still. Capturing automatically...',
84
+ });
85
+ }
86
+ function detectTurnFeedback(keypoints) {
87
+ const rightEye = keypoints[KEYPOINT_RIGHT_EYE];
88
+ const leftEye = keypoints[KEYPOINT_LEFT_EYE];
89
+ const nose = keypoints[KEYPOINT_NOSE];
90
+ if (!rightEye || !leftEye || !nose) {
91
+ return null;
92
+ }
93
+ const eyeMidpointX = (rightEye.x + leftEye.x) / 2;
94
+ const eyeDistance = Math.abs(leftEye.x - rightEye.x);
95
+ if (eyeDistance === 0) {
96
+ return null;
97
+ }
98
+ const noseOffsetRatio = (nose.x - eyeMidpointX) / eyeDistance;
99
+ if (noseOffsetRatio <= -MAX_NOSE_OFFSET_RATIO) {
100
+ return {
101
+ feedback: 'turn-right',
102
+ message: 'Turn slightly right so your face points at the camera.',
103
+ };
104
+ }
105
+ if (noseOffsetRatio >= MAX_NOSE_OFFSET_RATIO) {
106
+ return {
107
+ feedback: 'turn-left',
108
+ message: 'Turn slightly left so your face points at the camera.',
109
+ };
110
+ }
111
+ return null;
112
+ }
113
+ function createFrameAdjustmentResult(confidence, feedback, message) {
114
+ return createQualityResult({
115
+ hasFace: true,
116
+ faceCount: 1,
117
+ confidence,
118
+ captureScore: 0,
119
+ isCentered: false,
120
+ passesQualityChecks: false,
121
+ feedback,
122
+ message,
123
+ });
124
+ }
125
+ function createQualityResult(result) {
126
+ return result;
127
+ }
128
+ function calculateCaptureScore(confidence, faceCenterX, faceCenterY, faceAreaRatio) {
129
+ const xPenalty = Math.abs(faceCenterX - 0.5) / CENTER_TOLERANCE_X;
130
+ const yPenalty = Math.abs(faceCenterY - 0.5) / CENTER_TOLERANCE_Y;
131
+ const centerScore = 1 - Math.min((xPenalty + yPenalty) / 2, 1);
132
+ const maxAreaDistance = Math.max(Math.abs(IDEAL_FACE_AREA_RATIO - MIN_FACE_AREA_RATIO), Math.abs(MAX_FACE_AREA_RATIO - IDEAL_FACE_AREA_RATIO));
133
+ const sizeScore = 1 - Math.min(Math.abs(faceAreaRatio - IDEAL_FACE_AREA_RATIO) / maxAreaDistance, 1);
134
+ return Number((confidence * 0.55 + centerScore * 0.3 + sizeScore * 0.15).toFixed(4));
135
+ }
136
+ //# sourceMappingURL=face-quality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"face-quality.js","sourceRoot":"","sources":["../../src/services/face-quality.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { FaceQualityResult } from '../types/index.js';
2
+ export declare const AUTO_CAPTURE_ANALYSIS_INTERVAL_MS = 180;
3
+ export declare const AUTO_CAPTURE_COUNTDOWN_MS = 5000;
4
+ export declare const CAPTURE_GUIDE_PATH = "M 50 17 C 60 17 68 19.5 74 25 C 79 29.5 81.5 36.5 81.5 46.5 V 54.5 C 81.5 63.5 78.5 71 71.5 76.5 C 65 81.5 57.5 84 50 84 C 42.5 84 35 81.5 28.5 76.5 C 21.5 71 18.5 63.5 18.5 54.5 V 46.5 C 18.5 36.5 21 29.5 26 25 C 32 19.5 40 17 50 17 Z";
5
+ export declare const CAPTURE_GUIDE_MASK_PATH = "M 0 0 H 100 V 100 H 0 Z M 50 17 C 60 17 68 19.5 74 25 C 79 29.5 81.5 36.5 81.5 46.5 V 54.5 C 81.5 63.5 78.5 71 71.5 76.5 C 65 81.5 57.5 84 50 84 C 42.5 84 35 81.5 28.5 76.5 C 21.5 71 18.5 63.5 18.5 54.5 V 46.5 C 18.5 36.5 21 29.5 26 25 C 32 19.5 40 17 50 17 Z";
6
+ export declare function autoCaptureCountdownMessage(timestamp: number, countdownStartedAt: number | null, qualityResult: FaceQualityResult): string;
7
+ export declare function autoCaptureCompleteMessage(bestQualityResult: FaceQualityResult | null): "Best frame captured. Review and confirm this photo." | "Capture complete. Review and confirm this photo.";
@@ -0,0 +1,23 @@
1
+ export const AUTO_CAPTURE_ANALYSIS_INTERVAL_MS = 180;
2
+ export const AUTO_CAPTURE_COUNTDOWN_MS = 5000;
3
+ export const CAPTURE_GUIDE_PATH = 'M 50 17 C 60 17 68 19.5 74 25 C 79 29.5 81.5 36.5 81.5 46.5 V 54.5 C 81.5 63.5 78.5 71 71.5 76.5 C 65 81.5 57.5 84 50 84 C 42.5 84 35 81.5 28.5 76.5 C 21.5 71 18.5 63.5 18.5 54.5 V 46.5 C 18.5 36.5 21 29.5 26 25 C 32 19.5 40 17 50 17 Z';
4
+ export const CAPTURE_GUIDE_MASK_PATH = `M 0 0 H 100 V 100 H 0 Z ${CAPTURE_GUIDE_PATH}`;
5
+ export function autoCaptureCountdownMessage(timestamp, countdownStartedAt, qualityResult) {
6
+ if (countdownStartedAt === null) {
7
+ return 'Center your face in the oval. We will capture automatically when framing looks good.';
8
+ }
9
+ const remainingMs = Math.max(AUTO_CAPTURE_COUNTDOWN_MS - (timestamp - countdownStartedAt), 0);
10
+ const remainingSeconds = Math.max(Math.ceil(remainingMs / 1000), 0);
11
+ if (qualityResult.passesQualityChecks) {
12
+ return `Hold steady. Capturing the best frame in ${remainingSeconds}s.`;
13
+ }
14
+ return `${qualityResult.message} Best frame selection finishes in ${remainingSeconds}s.`;
15
+ }
16
+ export function autoCaptureCompleteMessage(bestQualityResult) {
17
+ const score = bestQualityResult?.captureScore;
18
+ if (typeof score === 'number' && score > 0) {
19
+ return 'Best frame captured. Review and confirm this photo.';
20
+ }
21
+ return 'Capture complete. Review and confirm this photo.';
22
+ }
23
+ //# sourceMappingURL=auto-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-capture.js","sourceRoot":"","sources":["../../src/shared/auto-capture.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iCAAiC,GAAG,GAAG,CAAC;AACrD,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC;AAE9C,MAAM,CAAC,MAAM,kBAAkB,GAAG,6OAA6O,CAAC;AAChR,MAAM,CAAC,MAAM,uBAAuB,GAAG,2BAA2B,kBAAkB,EAAE,CAAC;AAEvF,MAAM,UAAU,2BAA2B,CACzC,SAAiB,EACjB,kBAAiC,EACjC,aAAgC;IAEhC,IAAI,kBAAkB,KAAK,IAAI,EAAE,CAAC;QAChC,OAAO,sFAAsF,CAAC;IAChG,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,yBAAyB,GAAG,CAAC,SAAS,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9F,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACpE,IAAI,aAAa,CAAC,mBAAmB,EAAE,CAAC;QACtC,OAAO,4CAA4C,gBAAgB,IAAI,CAAC;IAC1E,CAAC;IAED,OAAO,GAAG,aAAa,CAAC,OAAO,qCAAqC,gBAAgB,IAAI,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,iBAA2C;IACpF,MAAM,KAAK,GAAG,iBAAiB,EAAE,YAAY,CAAC;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC3C,OAAO,qDAAqD,CAAC;IAC/D,CAAC;IAED,OAAO,kDAAkD,CAAC;AAC5D,CAAC"}