@moveris/shared 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +460 -109
- package/dist/index.d.ts +460 -109
- package/dist/index.js +1082 -205
- package/dist/index.mjs +1016 -200
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,67 +20,158 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ALIGNMENT_THRESHOLD_CAPTURE: () => ALIGNMENT_THRESHOLD_CAPTURE,
|
|
24
|
+
ALIGNMENT_THRESHOLD_GOOD: () => ALIGNMENT_THRESHOLD_GOOD,
|
|
25
|
+
ALIGNMENT_THRESHOLD_PERFECT: () => ALIGNMENT_THRESHOLD_PERFECT,
|
|
26
|
+
ALIGNMENT_THRESHOLD_POOR: () => ALIGNMENT_THRESHOLD_POOR,
|
|
23
27
|
API_ENDPOINTS: () => API_ENDPOINTS,
|
|
28
|
+
API_PATHS: () => API_PATHS,
|
|
24
29
|
AUTH_CONFIG: () => AUTH_CONFIG,
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
BACKLIT_RATIO_THRESHOLD: () => BACKLIT_RATIO_THRESHOLD,
|
|
31
|
+
BLUR_THRESHOLD_MOBILE: () => BLUR_THRESHOLD_MOBILE,
|
|
32
|
+
BaseFrameCollector: () => BaseFrameCollector,
|
|
33
|
+
DEFAULT_BLUR_THRESHOLD: () => DEFAULT_BLUR_THRESHOLD,
|
|
27
34
|
DEFAULT_ENDPOINT: () => DEFAULT_ENDPOINT,
|
|
35
|
+
DEFAULT_LIVENESS_CONFIG: () => DEFAULT_LIVENESS_CONFIG,
|
|
36
|
+
DEFAULT_LOCALE: () => DEFAULT_LOCALE,
|
|
37
|
+
DEFAULT_OVAL_REGION: () => DEFAULT_OVAL_REGION,
|
|
38
|
+
DEFAULT_STATUS_MESSAGES: () => DEFAULT_STATUS_MESSAGES,
|
|
39
|
+
ES_LOCALE: () => ES_LOCALE,
|
|
40
|
+
FACE_CENTER_VERTICAL_OFFSET: () => FACE_CENTER_VERTICAL_OFFSET,
|
|
41
|
+
FACE_CROP_OUTPUT_SIZE: () => FACE_CROP_OUTPUT_SIZE,
|
|
42
|
+
FEEDBACK_MESSAGES: () => FEEDBACK_MESSAGES,
|
|
28
43
|
FRAME_BUFFER_CONFIG: () => FRAME_BUFFER_CONFIG,
|
|
29
44
|
FRAME_CONFIG: () => FRAME_CONFIG,
|
|
30
45
|
FrameBuffer: () => FrameBuffer,
|
|
31
46
|
FrameQueue: () => FrameQueue,
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
GOOD_ALIGNMENT: () => GOOD_ALIGNMENT,
|
|
48
|
+
HIGH_ALIGNMENT: () => HIGH_ALIGNMENT,
|
|
49
|
+
HYBRID_MODEL_CONFIGS: () => HYBRID_MODEL_CONFIGS,
|
|
50
|
+
IDEAL_CROP_MULTIPLIER: () => IDEAL_CROP_MULTIPLIER,
|
|
51
|
+
LOW_LIGHT_THRESHOLD: () => LOW_LIGHT_THRESHOLD,
|
|
52
|
+
LivenessApiError: () => LivenessApiError,
|
|
53
|
+
LivenessClient: () => LivenessClient,
|
|
54
|
+
MAX_CROP_MULTIPLIER: () => MAX_CROP_MULTIPLIER,
|
|
55
|
+
MAX_FACE_PERCENTAGE_IN_CROP: () => MAX_FACE_PERCENTAGE_IN_CROP,
|
|
56
|
+
MAX_FACE_RATIO: () => MAX_FACE_RATIO,
|
|
57
|
+
MIN_CAPTURE_ALIGNMENT: () => MIN_CAPTURE_ALIGNMENT,
|
|
58
|
+
MIN_CROP_MULTIPLIER: () => MIN_CROP_MULTIPLIER,
|
|
59
|
+
MIN_FACE_BOTTOM_MARGIN: () => MIN_FACE_BOTTOM_MARGIN,
|
|
60
|
+
MIN_FACE_RATIO: () => MIN_FACE_RATIO,
|
|
61
|
+
MIN_FACE_SIDE_MARGIN: () => MIN_FACE_SIDE_MARGIN,
|
|
62
|
+
MIN_FACE_TOP_MARGIN: () => MIN_FACE_TOP_MARGIN,
|
|
63
|
+
MODEL_CONFIGS: () => MODEL_CONFIGS,
|
|
64
|
+
OVAL_GUIDE_COLORS: () => OVAL_GUIDE_COLORS,
|
|
65
|
+
OVAL_GUIDE_STYLES: () => OVAL_GUIDE_STYLES,
|
|
66
|
+
RETRY_CONFIG: () => RETRY_CONFIG,
|
|
67
|
+
TARGET_FACE_PERCENTAGE_IN_CROP: () => TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
68
|
+
analyzeBlur: () => analyzeBlur,
|
|
69
|
+
analyzeLighting: () => analyzeLighting,
|
|
70
|
+
calculateAdaptiveCropMultiplier: () => calculateAdaptiveCropMultiplier,
|
|
71
|
+
calculateBrightness: () => calculateBrightness,
|
|
72
|
+
calculateFaceAlignment: () => calculateFaceAlignment,
|
|
73
|
+
calculateFaceCropRegion: () => calculateFaceCropRegion,
|
|
74
|
+
canCaptureFrame: () => canCaptureFrame,
|
|
75
|
+
checkFrameQuality: () => checkFrameQuality,
|
|
34
76
|
decodeBase64: () => decodeBase64,
|
|
35
77
|
encodeBase64: () => encodeBase64,
|
|
78
|
+
generateSessionId: () => generateSessionId,
|
|
79
|
+
getCaptureQualityFeedback: () => getCaptureQualityFeedback,
|
|
80
|
+
getFeedbackMessage: () => getFeedbackMessage,
|
|
81
|
+
getMinFramesForModel: () => getMinFramesForModel,
|
|
82
|
+
getOvalGuideState: () => getOvalGuideState,
|
|
83
|
+
getStatusMessage: () => getStatusMessage,
|
|
84
|
+
hasEnoughFrames: () => hasEnoughFrames,
|
|
85
|
+
isFaceCropFullyInFrame: () => isFaceCropFullyInFrame,
|
|
86
|
+
isFaceFullyVisible: () => isFaceFullyVisible,
|
|
87
|
+
isFaceInOval: () => isFaceInOval,
|
|
36
88
|
retryWithBackoff: () => retryWithBackoff,
|
|
89
|
+
rgbaToGrayscale: () => rgbaToGrayscale,
|
|
90
|
+
sleep: () => sleep,
|
|
91
|
+
toFrameData: () => toFrameData,
|
|
92
|
+
toHybridFrameData: () => toHybridFrameData,
|
|
93
|
+
toLivenessResult: () => toLivenessResult,
|
|
37
94
|
validateApiKey: () => validateApiKey,
|
|
95
|
+
validateFrameCount: () => validateFrameCount,
|
|
38
96
|
validateFrameData: () => validateFrameData,
|
|
39
|
-
|
|
97
|
+
validateFrameIndex: () => validateFrameIndex,
|
|
98
|
+
validateTimestamp: () => validateTimestamp,
|
|
99
|
+
validateUUID: () => validateUUID,
|
|
100
|
+
validateUrl: () => validateUrl
|
|
40
101
|
});
|
|
41
102
|
module.exports = __toCommonJS(index_exports);
|
|
42
103
|
|
|
43
104
|
// src/constants/config.ts
|
|
44
105
|
var API_ENDPOINTS = {
|
|
45
|
-
production: "
|
|
46
|
-
staging: "
|
|
47
|
-
development: "
|
|
106
|
+
production: "https://api.moveris.com",
|
|
107
|
+
staging: "https://moveris-api-v2-staging.fly.dev",
|
|
108
|
+
development: "http://localhost:8000"
|
|
48
109
|
};
|
|
49
110
|
var DEFAULT_ENDPOINT = API_ENDPOINTS.production;
|
|
50
|
-
var
|
|
51
|
-
|
|
111
|
+
var API_PATHS = {
|
|
112
|
+
health: "/health",
|
|
113
|
+
fastCheck: "/api/v1/fast-check",
|
|
114
|
+
fastCheckCrops: "/api/v1/fast-check-crops",
|
|
115
|
+
verify: "/api/v1/verify",
|
|
116
|
+
hybridCheck: "/api/v1/hybrid-check",
|
|
117
|
+
hybrid50: "/api/v1/hybrid-50",
|
|
118
|
+
hybrid150: "/api/v1/hybrid-150",
|
|
119
|
+
jobResult: "/api/v1/result",
|
|
120
|
+
queueStats: "/api/v1/queue/stats"
|
|
121
|
+
};
|
|
122
|
+
var RETRY_CONFIG = {
|
|
123
|
+
maxAttempts: 3,
|
|
52
124
|
initialDelay: 1e3,
|
|
53
125
|
// 1 second
|
|
54
|
-
maxDelay:
|
|
55
|
-
//
|
|
126
|
+
maxDelay: 1e4,
|
|
127
|
+
// 10 seconds
|
|
56
128
|
backoffMultiplier: 2
|
|
57
129
|
};
|
|
58
130
|
var FRAME_BUFFER_CONFIG = {
|
|
59
131
|
maxSize: 10,
|
|
60
|
-
// Maximum
|
|
132
|
+
// Maximum frames to keep in buffer
|
|
61
133
|
maxMemory: 4 * 1024 * 1024
|
|
62
134
|
// 4 MB
|
|
63
135
|
};
|
|
64
136
|
var AUTH_CONFIG = {
|
|
65
|
-
timeout:
|
|
66
|
-
//
|
|
67
|
-
|
|
137
|
+
timeout: 3e4,
|
|
138
|
+
// 30 seconds for API requests
|
|
139
|
+
apiKeyHeader: "X-API-Key"
|
|
68
140
|
};
|
|
69
141
|
var FRAME_CONFIG = {
|
|
70
142
|
targetFPS: 30,
|
|
71
143
|
frameInterval: 33,
|
|
72
144
|
// milliseconds (1000 / 30)
|
|
73
|
-
jpegQuality: 0.8
|
|
145
|
+
jpegQuality: 0.8,
|
|
74
146
|
// 80%
|
|
147
|
+
defaultModel: "10",
|
|
148
|
+
defaultSource: "live"
|
|
149
|
+
};
|
|
150
|
+
var DEFAULT_LIVENESS_CONFIG = {
|
|
151
|
+
model: FRAME_CONFIG.defaultModel,
|
|
152
|
+
source: FRAME_CONFIG.defaultSource,
|
|
153
|
+
fps: FRAME_CONFIG.targetFPS,
|
|
154
|
+
timeout: AUTH_CONFIG.timeout
|
|
75
155
|
};
|
|
76
156
|
|
|
77
157
|
// src/utils/retry.ts
|
|
158
|
+
function defaultIsRetryable(error) {
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
if ("statusCode" in error) {
|
|
161
|
+
const statusCode = error.statusCode;
|
|
162
|
+
return statusCode === 0 || statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
163
|
+
}
|
|
164
|
+
return error.name === "AbortError" || error.message.includes("network") || error.message.includes("fetch");
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
78
168
|
async function retryWithBackoff(fn, options = {}) {
|
|
79
169
|
const {
|
|
80
|
-
maxAttempts =
|
|
81
|
-
initialDelay =
|
|
82
|
-
maxDelay =
|
|
83
|
-
backoffMultiplier =
|
|
170
|
+
maxAttempts = RETRY_CONFIG.maxAttempts,
|
|
171
|
+
initialDelay = RETRY_CONFIG.initialDelay,
|
|
172
|
+
maxDelay = RETRY_CONFIG.maxDelay,
|
|
173
|
+
backoffMultiplier = RETRY_CONFIG.backoffMultiplier,
|
|
174
|
+
isRetryable = defaultIsRetryable
|
|
84
175
|
} = options;
|
|
85
176
|
let lastError;
|
|
86
177
|
let delay = initialDelay;
|
|
@@ -89,6 +180,9 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
89
180
|
return await fn();
|
|
90
181
|
} catch (error) {
|
|
91
182
|
lastError = error;
|
|
183
|
+
if (!isRetryable(error)) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
92
186
|
if (attempt < maxAttempts - 1) {
|
|
93
187
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
94
188
|
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
|
@@ -97,189 +191,294 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
97
191
|
}
|
|
98
192
|
throw lastError;
|
|
99
193
|
}
|
|
194
|
+
async function sleep(ms) {
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
100
197
|
|
|
101
|
-
// src/client/
|
|
102
|
-
var
|
|
103
|
-
constructor(
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
await retryWithBackoff(
|
|
113
|
-
async () => {
|
|
114
|
-
await this.config.onReconnect();
|
|
115
|
-
this.reconnectAttempts = 0;
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
maxAttempts: this.config.maxAttempts ?? 10
|
|
119
|
-
}
|
|
120
|
-
);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
this.reconnectAttempts++;
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
reset() {
|
|
127
|
-
this.reconnectAttempts = 0;
|
|
198
|
+
// src/client/LivenessClient.ts
|
|
199
|
+
var LivenessApiError = class extends Error {
|
|
200
|
+
constructor(message, code, statusCode, required, received) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "LivenessApiError";
|
|
203
|
+
this.code = code;
|
|
204
|
+
this.statusCode = statusCode;
|
|
205
|
+
this.required = required;
|
|
206
|
+
this.received = received;
|
|
128
207
|
}
|
|
129
208
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
return AUTH_CONFIG.apiKeyPattern.test(apiKey);
|
|
209
|
+
function toFrameData(frames) {
|
|
210
|
+
return frames.map((frame) => ({
|
|
211
|
+
index: frame.index,
|
|
212
|
+
timestamp_ms: frame.timestampMs,
|
|
213
|
+
pixels: frame.pixels
|
|
214
|
+
}));
|
|
137
215
|
}
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return base64Pattern.test(data) && data.length > 0;
|
|
216
|
+
function toHybridFrameData(frames) {
|
|
217
|
+
return frames.map((frame) => ({
|
|
218
|
+
timestamp_ms: frame.timestampMs,
|
|
219
|
+
pixels: frame.pixels
|
|
220
|
+
}));
|
|
144
221
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// src/types/models.ts
|
|
150
|
-
var MODEL_CONFIGS = {
|
|
151
|
-
"50-frame": {
|
|
152
|
-
type: "50-frame",
|
|
153
|
-
fileName: "model_50.h5",
|
|
154
|
-
frameCount: 50
|
|
155
|
-
},
|
|
156
|
-
"250-frame": {
|
|
157
|
-
type: "250-frame",
|
|
158
|
-
fileName: "model_250.h5",
|
|
159
|
-
frameCount: 250
|
|
222
|
+
function toLivenessResult(response) {
|
|
223
|
+
if (!response.verdict) {
|
|
224
|
+
throw new LivenessApiError(response.error ?? "No verdict received", "no_verdict", 500);
|
|
160
225
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return new Promise((resolve, reject) => {
|
|
174
|
-
const timeout = setTimeout(() => {
|
|
175
|
-
reject(new Error("Authentication timeout"));
|
|
176
|
-
}, AUTH_CONFIG.timeout);
|
|
177
|
-
const messageHandler = (event) => {
|
|
178
|
-
try {
|
|
179
|
-
const message = JSON.parse(event.data);
|
|
180
|
-
if (message.type === "auth_success") {
|
|
181
|
-
clearTimeout(timeout);
|
|
182
|
-
ws.removeEventListener("message", messageHandler);
|
|
183
|
-
resolve(message.request_id);
|
|
184
|
-
} else if (message.type === "auth_error") {
|
|
185
|
-
clearTimeout(timeout);
|
|
186
|
-
ws.removeEventListener("message", messageHandler);
|
|
187
|
-
reject(new Error(message.error));
|
|
188
|
-
}
|
|
189
|
-
} catch (error) {
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
ws.addEventListener("message", messageHandler);
|
|
193
|
-
ws.send(
|
|
194
|
-
JSON.stringify({
|
|
195
|
-
type: "auth",
|
|
196
|
-
token: apiKey,
|
|
197
|
-
model_file_name: MODEL_CONFIGS[modelType].fileName
|
|
198
|
-
})
|
|
199
|
-
);
|
|
200
|
-
});
|
|
226
|
+
return {
|
|
227
|
+
verdict: response.verdict,
|
|
228
|
+
confidence: response.confidence ?? 0,
|
|
229
|
+
score: response.score ?? 0,
|
|
230
|
+
sessionId: response.session_id,
|
|
231
|
+
processingMs: response.processing_ms,
|
|
232
|
+
framesProcessed: "frames_processed" in response ? response.frames_processed ?? 0 : 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function generateSessionId() {
|
|
236
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
237
|
+
return crypto.randomUUID();
|
|
201
238
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
240
|
+
const r = Math.random() * 16 | 0;
|
|
241
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
242
|
+
return v.toString(16);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
var LivenessClient = class {
|
|
206
246
|
constructor(config) {
|
|
207
|
-
this.
|
|
208
|
-
this.
|
|
209
|
-
this.
|
|
210
|
-
this.
|
|
211
|
-
|
|
212
|
-
});
|
|
213
|
-
this.authManager = new AuthManager();
|
|
247
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
248
|
+
this.apiKey = config.apiKey;
|
|
249
|
+
this.timeout = config.timeout ?? AUTH_CONFIG.timeout;
|
|
250
|
+
this.enableRetry = config.enableRetry ?? true;
|
|
251
|
+
this.fetchFn = config.customFetch ?? (typeof window !== "undefined" ? fetch.bind(window) : fetch);
|
|
214
252
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.
|
|
253
|
+
/**
|
|
254
|
+
* Make an authenticated API request
|
|
255
|
+
*/
|
|
256
|
+
async request(path, options = {}) {
|
|
257
|
+
const url = `${this.baseUrl}${path}`;
|
|
258
|
+
const controller = new AbortController();
|
|
259
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
220
260
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
this.ws.onmessage = (event) => {
|
|
231
|
-
try {
|
|
232
|
-
const message = JSON.parse(event.data);
|
|
233
|
-
this.handleMessage(message);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
this.handleError(error instanceof Error ? error : new Error("Failed to parse message"));
|
|
261
|
+
const response = await this.fetchFn(url, {
|
|
262
|
+
...options,
|
|
263
|
+
signal: controller.signal,
|
|
264
|
+
headers: {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
[AUTH_CONFIG.apiKeyHeader]: this.apiKey,
|
|
267
|
+
...options.headers
|
|
236
268
|
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
269
|
+
});
|
|
270
|
+
clearTimeout(timeoutId);
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
const errorData = await response.json();
|
|
273
|
+
throw new LivenessApiError(
|
|
274
|
+
errorData.message,
|
|
275
|
+
errorData.error,
|
|
276
|
+
response.status,
|
|
277
|
+
errorData.required,
|
|
278
|
+
errorData.received
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return await response.json();
|
|
245
282
|
} catch (error) {
|
|
246
|
-
|
|
283
|
+
clearTimeout(timeoutId);
|
|
284
|
+
if (error instanceof LivenessApiError) {
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
288
|
+
throw new LivenessApiError("Request timeout", "timeout", 408);
|
|
289
|
+
}
|
|
290
|
+
throw new LivenessApiError(
|
|
291
|
+
error instanceof Error ? error.message : "Network error",
|
|
292
|
+
"network_error",
|
|
293
|
+
0
|
|
294
|
+
);
|
|
247
295
|
}
|
|
248
296
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Make a request with optional retry
|
|
299
|
+
*/
|
|
300
|
+
async requestWithRetry(path, options = {}) {
|
|
301
|
+
if (this.enableRetry) {
|
|
302
|
+
return retryWithBackoff(() => this.request(path, options), {
|
|
303
|
+
maxAttempts: RETRY_CONFIG.maxAttempts,
|
|
304
|
+
initialDelay: RETRY_CONFIG.initialDelay,
|
|
305
|
+
maxDelay: RETRY_CONFIG.maxDelay,
|
|
306
|
+
backoffMultiplier: RETRY_CONFIG.backoffMultiplier
|
|
307
|
+
});
|
|
253
308
|
}
|
|
254
|
-
this.
|
|
309
|
+
return this.request(path, options);
|
|
255
310
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
311
|
+
// ===========================================================================
|
|
312
|
+
// Health & Status
|
|
313
|
+
// ===========================================================================
|
|
314
|
+
/**
|
|
315
|
+
* Check API health status
|
|
316
|
+
*/
|
|
317
|
+
async health() {
|
|
318
|
+
return this.request(API_PATHS.health);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get queue statistics
|
|
322
|
+
*/
|
|
323
|
+
async queueStats() {
|
|
324
|
+
return this.request(API_PATHS.queueStats);
|
|
325
|
+
}
|
|
326
|
+
// ===========================================================================
|
|
327
|
+
// Fast Check Endpoints
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
/**
|
|
330
|
+
* Perform fast liveness check with server-side face detection
|
|
331
|
+
*
|
|
332
|
+
* @param frames - Captured frames to analyze
|
|
333
|
+
* @param options - Additional options
|
|
334
|
+
* @returns Liveness result
|
|
335
|
+
*/
|
|
336
|
+
async fastCheck(frames, options = {}) {
|
|
337
|
+
const request = {
|
|
338
|
+
session_id: options.sessionId ?? generateSessionId(),
|
|
339
|
+
model: options.model ?? "10",
|
|
340
|
+
source: options.source ?? "live",
|
|
341
|
+
frames: toFrameData(frames)
|
|
342
|
+
};
|
|
343
|
+
const response = await this.requestWithRetry(API_PATHS.fastCheck, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
body: JSON.stringify(request)
|
|
346
|
+
});
|
|
347
|
+
return toLivenessResult(response);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Perform fast liveness check with pre-cropped face images
|
|
351
|
+
*
|
|
352
|
+
* @param crops - Pre-cropped 224x224 face images
|
|
353
|
+
* @param options - Additional options
|
|
354
|
+
* @returns Liveness result
|
|
355
|
+
*/
|
|
356
|
+
async fastCheckCrops(crops, options = {}) {
|
|
357
|
+
const request = {
|
|
358
|
+
session_id: options.sessionId ?? generateSessionId(),
|
|
359
|
+
model: options.model ?? "10",
|
|
360
|
+
source: options.source ?? "live",
|
|
361
|
+
crops
|
|
265
362
|
};
|
|
266
|
-
this.
|
|
363
|
+
const response = await this.requestWithRetry(API_PATHS.fastCheckCrops, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
body: JSON.stringify(request)
|
|
366
|
+
});
|
|
367
|
+
return toLivenessResult(response);
|
|
267
368
|
}
|
|
268
|
-
|
|
269
|
-
|
|
369
|
+
// ===========================================================================
|
|
370
|
+
// Verify Endpoint (Spatial Features)
|
|
371
|
+
// ===========================================================================
|
|
372
|
+
/**
|
|
373
|
+
* Verify liveness using spatial feature-based detection
|
|
374
|
+
* Requires minimum 50 frames
|
|
375
|
+
*
|
|
376
|
+
* @param frames - Captured frames to analyze
|
|
377
|
+
* @param options - Additional options
|
|
378
|
+
* @returns Liveness result
|
|
379
|
+
*/
|
|
380
|
+
async verify(frames, options = {}) {
|
|
381
|
+
const request = {
|
|
382
|
+
session_id: options.sessionId ?? generateSessionId(),
|
|
383
|
+
capture_start_ms: options.captureStartMs ?? 0,
|
|
384
|
+
frame_width: options.frameWidth ?? 640,
|
|
385
|
+
frame_height: options.frameHeight ?? 480,
|
|
386
|
+
fps: options.fps ?? 10,
|
|
387
|
+
frames: toFrameData(frames)
|
|
388
|
+
};
|
|
389
|
+
const response = await this.requestWithRetry(API_PATHS.verify, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
body: JSON.stringify(request)
|
|
392
|
+
});
|
|
393
|
+
return toLivenessResult(response);
|
|
270
394
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
395
|
+
// ===========================================================================
|
|
396
|
+
// Hybrid Check Endpoints
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
/**
|
|
399
|
+
* Perform hybrid liveness check with CNN + physiological features
|
|
400
|
+
* Requires minimum 50 frames
|
|
401
|
+
*
|
|
402
|
+
* @param frames - Captured frames to analyze
|
|
403
|
+
* @param options - Additional options
|
|
404
|
+
* @returns Liveness result
|
|
405
|
+
*/
|
|
406
|
+
async hybridCheck(frames, options = {}) {
|
|
407
|
+
const request = {
|
|
408
|
+
session_id: options.sessionId ?? generateSessionId(),
|
|
409
|
+
fps: options.fps ?? 30,
|
|
410
|
+
frames: toHybridFrameData(frames)
|
|
411
|
+
};
|
|
412
|
+
const response = await this.requestWithRetry(API_PATHS.hybridCheck, {
|
|
413
|
+
method: "POST",
|
|
414
|
+
body: JSON.stringify(request)
|
|
415
|
+
});
|
|
416
|
+
return toLivenessResult(response);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Perform 50-frame hybrid liveness check
|
|
420
|
+
* Achieves 93.8% balanced accuracy
|
|
421
|
+
*
|
|
422
|
+
* @param frames - Captured frames to analyze (minimum 50)
|
|
423
|
+
* @param options - Additional options
|
|
424
|
+
* @returns Liveness result
|
|
425
|
+
*/
|
|
426
|
+
async hybrid50(frames, options = {}) {
|
|
427
|
+
const request = {
|
|
428
|
+
session_id: options.sessionId ?? generateSessionId(),
|
|
429
|
+
fps: options.fps ?? 30,
|
|
430
|
+
frames: toHybridFrameData(frames)
|
|
431
|
+
};
|
|
432
|
+
const response = await this.requestWithRetry(API_PATHS.hybrid50, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
body: JSON.stringify(request)
|
|
435
|
+
});
|
|
436
|
+
return toLivenessResult(response);
|
|
276
437
|
}
|
|
277
|
-
|
|
278
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Perform 150-frame hybrid liveness check
|
|
440
|
+
* Achieves 96.2% balanced accuracy
|
|
441
|
+
*
|
|
442
|
+
* @param frames - Captured frames to analyze (minimum 150)
|
|
443
|
+
* @param options - Additional options
|
|
444
|
+
* @returns Liveness result
|
|
445
|
+
*/
|
|
446
|
+
async hybrid150(frames, options = {}) {
|
|
447
|
+
const request = {
|
|
448
|
+
session_id: options.sessionId ?? generateSessionId(),
|
|
449
|
+
fps: options.fps ?? 30,
|
|
450
|
+
frames: toHybridFrameData(frames)
|
|
451
|
+
};
|
|
452
|
+
const response = await this.requestWithRetry(API_PATHS.hybrid150, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
body: JSON.stringify(request)
|
|
455
|
+
});
|
|
456
|
+
return toLivenessResult(response);
|
|
279
457
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
458
|
+
// ===========================================================================
|
|
459
|
+
// Job Polling Endpoints
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
/**
|
|
462
|
+
* Get job status and result
|
|
463
|
+
*
|
|
464
|
+
* @param jobId - Job identifier
|
|
465
|
+
* @returns Job status response
|
|
466
|
+
*/
|
|
467
|
+
async getJobResult(jobId) {
|
|
468
|
+
return this.request(`${API_PATHS.jobResult}/${jobId}`);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Long-poll for job result
|
|
472
|
+
* Waits up to timeout seconds for the job to complete
|
|
473
|
+
*
|
|
474
|
+
* @param jobId - Job identifier
|
|
475
|
+
* @param timeout - Max seconds to wait (default 30, max 120)
|
|
476
|
+
* @returns Job status response
|
|
477
|
+
*/
|
|
478
|
+
async waitForJobResult(jobId, timeout = 30) {
|
|
479
|
+
return this.request(
|
|
480
|
+
`${API_PATHS.jobResult}/${jobId}/wait?timeout=${timeout}`
|
|
481
|
+
);
|
|
283
482
|
}
|
|
284
483
|
};
|
|
285
484
|
|
|
@@ -287,54 +486,103 @@ var WebSocketClient = class {
|
|
|
287
486
|
var FrameBuffer = class {
|
|
288
487
|
constructor(maxSize = FRAME_BUFFER_CONFIG.maxSize, maxMemory = FRAME_BUFFER_CONFIG.maxMemory) {
|
|
289
488
|
this.buffer = /* @__PURE__ */ new Map();
|
|
290
|
-
this.lastAcknowledged = 0;
|
|
291
489
|
this.currentMemoryUsage = 0;
|
|
292
490
|
this.maxSize = maxSize;
|
|
293
491
|
this.maxMemory = maxMemory;
|
|
294
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Add a frame to the buffer
|
|
495
|
+
*/
|
|
295
496
|
add(frame) {
|
|
296
497
|
if (this.currentMemoryUsage + frame.size > this.maxMemory) {
|
|
297
498
|
this.flushOldest();
|
|
298
499
|
}
|
|
299
|
-
this.buffer.set(frame.
|
|
500
|
+
this.buffer.set(frame.index, frame);
|
|
300
501
|
this.currentMemoryUsage += frame.size;
|
|
301
502
|
if (this.buffer.size > this.maxSize) {
|
|
302
503
|
const oldest = Math.min(...this.buffer.keys());
|
|
303
504
|
this.remove(oldest);
|
|
304
505
|
}
|
|
305
506
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
}
|
|
507
|
+
/**
|
|
508
|
+
* Get a frame by index
|
|
509
|
+
*/
|
|
510
|
+
get(index) {
|
|
511
|
+
return this.buffer.get(index);
|
|
313
512
|
}
|
|
314
|
-
|
|
315
|
-
|
|
513
|
+
/**
|
|
514
|
+
* Get all frames sorted by index
|
|
515
|
+
*/
|
|
516
|
+
getAll() {
|
|
517
|
+
return Array.from(this.buffer.values()).sort((a, b) => a.index - b.index);
|
|
316
518
|
}
|
|
317
|
-
|
|
318
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Get frames within a range
|
|
521
|
+
*/
|
|
522
|
+
getRange(startIndex, endIndex) {
|
|
523
|
+
return this.getAll().filter((f) => f.index >= startIndex && f.index <= endIndex);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get the latest N frames
|
|
527
|
+
*/
|
|
528
|
+
getLatest(count) {
|
|
529
|
+
const all = this.getAll();
|
|
530
|
+
return all.slice(-count);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Remove frames up to and including the given index
|
|
534
|
+
*/
|
|
535
|
+
removeUpTo(index) {
|
|
536
|
+
for (const [frameIndex] of this.buffer) {
|
|
537
|
+
if (frameIndex <= index) {
|
|
538
|
+
this.remove(frameIndex);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
319
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Get current buffer size (number of frames)
|
|
544
|
+
*/
|
|
320
545
|
size() {
|
|
321
546
|
return this.buffer.size;
|
|
322
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Get current memory usage in bytes
|
|
550
|
+
*/
|
|
323
551
|
getMemoryUsage() {
|
|
324
552
|
return this.currentMemoryUsage;
|
|
325
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Check if buffer is empty
|
|
556
|
+
*/
|
|
557
|
+
isEmpty() {
|
|
558
|
+
return this.buffer.size === 0;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Check if buffer is at capacity
|
|
562
|
+
*/
|
|
563
|
+
isFull() {
|
|
564
|
+
return this.buffer.size >= this.maxSize;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Clear all frames from the buffer
|
|
568
|
+
*/
|
|
326
569
|
clear() {
|
|
327
570
|
this.buffer.clear();
|
|
328
571
|
this.currentMemoryUsage = 0;
|
|
329
|
-
this.lastAcknowledged = 0;
|
|
330
572
|
}
|
|
331
|
-
|
|
332
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Remove a specific frame by index
|
|
575
|
+
*/
|
|
576
|
+
remove(index) {
|
|
577
|
+
const frame = this.buffer.get(index);
|
|
333
578
|
if (frame) {
|
|
334
579
|
this.currentMemoryUsage -= frame.size;
|
|
335
|
-
this.buffer.delete(
|
|
580
|
+
this.buffer.delete(index);
|
|
336
581
|
}
|
|
337
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Remove the oldest frame
|
|
585
|
+
*/
|
|
338
586
|
flushOldest() {
|
|
339
587
|
if (this.buffer.size === 0) {
|
|
340
588
|
return;
|
|
@@ -372,6 +620,273 @@ var FrameQueue = class {
|
|
|
372
620
|
}
|
|
373
621
|
};
|
|
374
622
|
|
|
623
|
+
// src/types/models.ts
|
|
624
|
+
var MODEL_CONFIGS = {
|
|
625
|
+
"10": {
|
|
626
|
+
type: "10",
|
|
627
|
+
minFrames: 10,
|
|
628
|
+
recommendedFrames: 10,
|
|
629
|
+
description: "Fast model - 10 frames, quick verification"
|
|
630
|
+
},
|
|
631
|
+
"50": {
|
|
632
|
+
type: "50",
|
|
633
|
+
minFrames: 50,
|
|
634
|
+
recommendedFrames: 50,
|
|
635
|
+
description: "Balanced model - 50 frames, good accuracy"
|
|
636
|
+
},
|
|
637
|
+
"250": {
|
|
638
|
+
type: "250",
|
|
639
|
+
minFrames: 250,
|
|
640
|
+
recommendedFrames: 250,
|
|
641
|
+
description: "High-accuracy model - 250 frames, best accuracy"
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
var HYBRID_MODEL_CONFIGS = {
|
|
645
|
+
"hybrid-50": {
|
|
646
|
+
type: "hybrid-50",
|
|
647
|
+
minFrames: 50,
|
|
648
|
+
recommendedFps: 30,
|
|
649
|
+
captureDurationSeconds: 1.7,
|
|
650
|
+
description: "Hybrid 50-frame model - 93.8% balanced accuracy"
|
|
651
|
+
},
|
|
652
|
+
"hybrid-150": {
|
|
653
|
+
type: "hybrid-150",
|
|
654
|
+
minFrames: 150,
|
|
655
|
+
recommendedFps: 30,
|
|
656
|
+
captureDurationSeconds: 5,
|
|
657
|
+
description: "Hybrid 150-frame model - 96.2% balanced accuracy"
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
function getMinFramesForModel(model) {
|
|
661
|
+
return MODEL_CONFIGS[model].minFrames;
|
|
662
|
+
}
|
|
663
|
+
function hasEnoughFrames(model, frameCount) {
|
|
664
|
+
return frameCount >= MODEL_CONFIGS[model].minFrames;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/constants/feedback.ts
|
|
668
|
+
var ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
|
|
669
|
+
var ALIGNMENT_THRESHOLD_POOR = 0.5;
|
|
670
|
+
var ALIGNMENT_THRESHOLD_GOOD = 0.5;
|
|
671
|
+
var ALIGNMENT_THRESHOLD_PERFECT = 0.85;
|
|
672
|
+
var OVAL_GUIDE_COLORS = {
|
|
673
|
+
no_face: "#ef4444",
|
|
674
|
+
// Red - no face detected
|
|
675
|
+
poor: "#f97316",
|
|
676
|
+
// Orange - face detected but alignment < 50%
|
|
677
|
+
good: "#eab308",
|
|
678
|
+
// Yellow - alignment 50-85%
|
|
679
|
+
perfect: "#22c55e"
|
|
680
|
+
// Green - alignment >= 85%, ready to capture
|
|
681
|
+
};
|
|
682
|
+
var OVAL_GUIDE_STYLES = {
|
|
683
|
+
no_face: { color: "#ef4444", dashed: true, pulse: false },
|
|
684
|
+
poor: { color: "#f97316", dashed: false, pulse: false },
|
|
685
|
+
good: { color: "#eab308", dashed: false, pulse: false },
|
|
686
|
+
perfect: { color: "#22c55e", dashed: false, pulse: true }
|
|
687
|
+
};
|
|
688
|
+
function getOvalGuideState(hasFace, alignment) {
|
|
689
|
+
if (!hasFace) return "no_face";
|
|
690
|
+
if (alignment < ALIGNMENT_THRESHOLD_POOR) return "poor";
|
|
691
|
+
if (alignment < ALIGNMENT_THRESHOLD_PERFECT) return "good";
|
|
692
|
+
return "perfect";
|
|
693
|
+
}
|
|
694
|
+
var DEFAULT_STATUS_MESSAGES = {
|
|
695
|
+
idle: "Ready to start",
|
|
696
|
+
capturing: "Hold still...",
|
|
697
|
+
uploading: "Processing...",
|
|
698
|
+
processing: "Analyzing...",
|
|
699
|
+
complete: "Complete!",
|
|
700
|
+
error: "Error occurred"
|
|
701
|
+
};
|
|
702
|
+
var FEEDBACK_MESSAGES = {
|
|
703
|
+
// Initial / No face (Red state)
|
|
704
|
+
position_face: "Position your face in the oval",
|
|
705
|
+
no_face: "No face detected - move into frame",
|
|
706
|
+
multiple_faces: "Multiple people detected - please verify alone",
|
|
707
|
+
// Poor alignment (Orange state) - alignment < 50%
|
|
708
|
+
move_closer_to_center: "Move closer to center",
|
|
709
|
+
center_face: "Center your face",
|
|
710
|
+
// Good alignment (Yellow state) - alignment 50-85%
|
|
711
|
+
almost_there: "Almost there...",
|
|
712
|
+
keep_steady: "Keep steady...",
|
|
713
|
+
// Perfect alignment (Green state) - alignment >= 85%
|
|
714
|
+
perfect: "Perfect! Hold still",
|
|
715
|
+
scanning: "Scanning...",
|
|
716
|
+
// Distance issues
|
|
717
|
+
move_closer: "Move closer - face too far",
|
|
718
|
+
move_back: "Move back - face too close",
|
|
719
|
+
too_close: "Move back - face too close",
|
|
720
|
+
too_far: "Move closer - face too far",
|
|
721
|
+
// Visibility issues
|
|
722
|
+
face_not_visible: "Center your face - edges cut off",
|
|
723
|
+
partial_face: "Center your face - edges cut off",
|
|
724
|
+
// Quality issues
|
|
725
|
+
hold_still: "Hold still - image blurry",
|
|
726
|
+
blurry: "Hold still - image blurry",
|
|
727
|
+
// Lighting issues
|
|
728
|
+
poor_lighting: "Improve lighting",
|
|
729
|
+
too_dark: "Low lighting - move to a brighter area",
|
|
730
|
+
backlit: "Backlit - try facing the light source",
|
|
731
|
+
// Hand occlusion
|
|
732
|
+
hand_detected: "Remove hand from face",
|
|
733
|
+
// Progress messages
|
|
734
|
+
capturing: "Capturing...",
|
|
735
|
+
almost_done: "Almost done...",
|
|
736
|
+
verification_complete: "Verification complete",
|
|
737
|
+
// Alignment-based messages (used by getAlignmentFeedback)
|
|
738
|
+
align_face: "Align your face"
|
|
739
|
+
};
|
|
740
|
+
var DEFAULT_LOCALE = {
|
|
741
|
+
status: DEFAULT_STATUS_MESSAGES,
|
|
742
|
+
feedback: FEEDBACK_MESSAGES
|
|
743
|
+
};
|
|
744
|
+
var ES_LOCALE = {
|
|
745
|
+
status: {
|
|
746
|
+
idle: "Listo para comenzar",
|
|
747
|
+
capturing: "Mantente quieto...",
|
|
748
|
+
uploading: "Procesando...",
|
|
749
|
+
processing: "Analizando...",
|
|
750
|
+
complete: "\xA1Completado!",
|
|
751
|
+
error: "Ocurri\xF3 un error"
|
|
752
|
+
},
|
|
753
|
+
feedback: {
|
|
754
|
+
// Initial / No face (Red state)
|
|
755
|
+
position_face: "Posiciona tu rostro en el \xF3valo",
|
|
756
|
+
no_face: "No se detecta rostro - mu\xE9vete al marco",
|
|
757
|
+
multiple_faces: "Varias personas detectadas - verifica solo",
|
|
758
|
+
// Poor alignment (Orange state)
|
|
759
|
+
move_closer_to_center: "Mu\xE9vete al centro",
|
|
760
|
+
center_face: "Centra tu rostro",
|
|
761
|
+
// Good alignment (Yellow state)
|
|
762
|
+
almost_there: "Casi listo...",
|
|
763
|
+
keep_steady: "Mantente quieto...",
|
|
764
|
+
// Perfect alignment (Green state)
|
|
765
|
+
perfect: "\xA1Perfecto! No te muevas",
|
|
766
|
+
scanning: "Escaneando...",
|
|
767
|
+
// Distance issues
|
|
768
|
+
move_closer: "Ac\xE9rcate - rostro muy lejos",
|
|
769
|
+
move_back: "Al\xE9jate - rostro muy cerca",
|
|
770
|
+
too_close: "Al\xE9jate - rostro muy cerca",
|
|
771
|
+
too_far: "Ac\xE9rcate - rostro muy lejos",
|
|
772
|
+
// Visibility issues
|
|
773
|
+
face_not_visible: "Centra tu rostro - bordes cortados",
|
|
774
|
+
partial_face: "Centra tu rostro - bordes cortados",
|
|
775
|
+
// Quality issues
|
|
776
|
+
hold_still: "Mantente quieto - imagen borrosa",
|
|
777
|
+
blurry: "Mantente quieto - imagen borrosa",
|
|
778
|
+
// Lighting issues
|
|
779
|
+
poor_lighting: "Mejora la iluminaci\xF3n",
|
|
780
|
+
too_dark: "Poca luz - mu\xE9vete a un \xE1rea m\xE1s iluminada",
|
|
781
|
+
backlit: "Contraluz - intenta mirar hacia la fuente de luz",
|
|
782
|
+
// Hand occlusion
|
|
783
|
+
hand_detected: "Retira la mano del rostro",
|
|
784
|
+
// Progress messages
|
|
785
|
+
capturing: "Capturando...",
|
|
786
|
+
almost_done: "Casi listo...",
|
|
787
|
+
verification_complete: "Verificaci\xF3n completada",
|
|
788
|
+
// Alignment-based
|
|
789
|
+
align_face: "Alinea tu rostro"
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
function getFeedbackMessage(key, locale = DEFAULT_LOCALE) {
|
|
793
|
+
return locale.feedback[key] ?? FEEDBACK_MESSAGES[key];
|
|
794
|
+
}
|
|
795
|
+
function getStatusMessage(key, locale = DEFAULT_LOCALE) {
|
|
796
|
+
return locale.status[key] ?? DEFAULT_STATUS_MESSAGES[key];
|
|
797
|
+
}
|
|
798
|
+
function getCaptureQualityFeedback(state) {
|
|
799
|
+
const {
|
|
800
|
+
hasFace,
|
|
801
|
+
alignment,
|
|
802
|
+
isCapturing,
|
|
803
|
+
tooClose,
|
|
804
|
+
tooFar,
|
|
805
|
+
isBlurry,
|
|
806
|
+
isPartialFace,
|
|
807
|
+
framesCaptured,
|
|
808
|
+
targetFrames
|
|
809
|
+
} = state;
|
|
810
|
+
if (!hasFace) {
|
|
811
|
+
return FEEDBACK_MESSAGES.no_face;
|
|
812
|
+
}
|
|
813
|
+
if (tooClose) {
|
|
814
|
+
return FEEDBACK_MESSAGES.too_close;
|
|
815
|
+
}
|
|
816
|
+
if (tooFar) {
|
|
817
|
+
return FEEDBACK_MESSAGES.too_far;
|
|
818
|
+
}
|
|
819
|
+
if (isPartialFace) {
|
|
820
|
+
return FEEDBACK_MESSAGES.partial_face;
|
|
821
|
+
}
|
|
822
|
+
if (isBlurry) {
|
|
823
|
+
return FEEDBACK_MESSAGES.blurry;
|
|
824
|
+
}
|
|
825
|
+
if (alignment < ALIGNMENT_THRESHOLD_POOR) {
|
|
826
|
+
return FEEDBACK_MESSAGES.move_closer_to_center;
|
|
827
|
+
}
|
|
828
|
+
if (alignment < ALIGNMENT_THRESHOLD_PERFECT) {
|
|
829
|
+
return FEEDBACK_MESSAGES.almost_there;
|
|
830
|
+
}
|
|
831
|
+
if (isCapturing && framesCaptured > 0) {
|
|
832
|
+
if (framesCaptured >= targetFrames - 2) {
|
|
833
|
+
return FEEDBACK_MESSAGES.almost_done;
|
|
834
|
+
}
|
|
835
|
+
return FEEDBACK_MESSAGES.scanning;
|
|
836
|
+
}
|
|
837
|
+
return FEEDBACK_MESSAGES.perfect;
|
|
838
|
+
}
|
|
839
|
+
function canCaptureFrame(state) {
|
|
840
|
+
const { hasFace, alignment, tooClose, tooFar, isBlurry, isPartialFace } = state;
|
|
841
|
+
return hasFace && alignment >= ALIGNMENT_THRESHOLD_CAPTURE && !tooClose && !tooFar && !isBlurry && !isPartialFace;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/utils/validators.ts
|
|
845
|
+
function validateApiKey(apiKey) {
|
|
846
|
+
return typeof apiKey === "string" && apiKey.length > 0;
|
|
847
|
+
}
|
|
848
|
+
function validateFrameData(data) {
|
|
849
|
+
if (!data || typeof data !== "string") {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
if (data.startsWith("data:")) {
|
|
853
|
+
const base64Part = data.split(",")[1];
|
|
854
|
+
return base64Part !== void 0 && base64Part.length > 0;
|
|
855
|
+
}
|
|
856
|
+
const base64Pattern = /^[A-Za-z0-9+/=]+$/;
|
|
857
|
+
return base64Pattern.test(data) && data.length > 0;
|
|
858
|
+
}
|
|
859
|
+
function validateFrameIndex(index) {
|
|
860
|
+
return Number.isInteger(index) && index >= 0;
|
|
861
|
+
}
|
|
862
|
+
function validateTimestamp(timestamp) {
|
|
863
|
+
return typeof timestamp === "number" && timestamp >= 0 && isFinite(timestamp);
|
|
864
|
+
}
|
|
865
|
+
function validateFrameCount(model, frameCount) {
|
|
866
|
+
const config = MODEL_CONFIGS[model];
|
|
867
|
+
const required = config.minFrames;
|
|
868
|
+
return {
|
|
869
|
+
valid: frameCount >= required,
|
|
870
|
+
required,
|
|
871
|
+
received: frameCount
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function validateUUID(uuid) {
|
|
875
|
+
if (!uuid || typeof uuid !== "string") {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
879
|
+
return uuidPattern.test(uuid);
|
|
880
|
+
}
|
|
881
|
+
function validateUrl(url) {
|
|
882
|
+
try {
|
|
883
|
+
new URL(url);
|
|
884
|
+
return true;
|
|
885
|
+
} catch {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
375
890
|
// src/utils/encoders.ts
|
|
376
891
|
function encodeBase64(buffer) {
|
|
377
892
|
if (buffer instanceof ArrayBuffer) {
|
|
@@ -388,23 +903,385 @@ function decodeBase64(base64) {
|
|
|
388
903
|
}
|
|
389
904
|
return bytes;
|
|
390
905
|
}
|
|
906
|
+
|
|
907
|
+
// src/utils/frameAnalysis.ts
|
|
908
|
+
var DEFAULT_BLUR_THRESHOLD = 100;
|
|
909
|
+
var BLUR_THRESHOLD_MOBILE = 150;
|
|
910
|
+
var BACKLIT_RATIO_THRESHOLD = 0.6;
|
|
911
|
+
var LOW_LIGHT_THRESHOLD = 50;
|
|
912
|
+
var MIN_FACE_TOP_MARGIN = 0.1;
|
|
913
|
+
var MIN_FACE_BOTTOM_MARGIN = 0.08;
|
|
914
|
+
var MIN_FACE_SIDE_MARGIN = 0.05;
|
|
915
|
+
var MIN_CAPTURE_ALIGNMENT = 0.6;
|
|
916
|
+
var HIGH_ALIGNMENT = 0.85;
|
|
917
|
+
var GOOD_ALIGNMENT = 0.5;
|
|
918
|
+
var IDEAL_CROP_MULTIPLIER = 3.33;
|
|
919
|
+
var MIN_CROP_MULTIPLIER = 1.5;
|
|
920
|
+
var MAX_CROP_MULTIPLIER = 4;
|
|
921
|
+
var FACE_CENTER_VERTICAL_OFFSET = 0.15;
|
|
922
|
+
var MIN_FACE_RATIO = 0.03;
|
|
923
|
+
var MAX_FACE_RATIO = 0.7;
|
|
924
|
+
var FACE_CROP_OUTPUT_SIZE = 224;
|
|
925
|
+
var MAX_FACE_PERCENTAGE_IN_CROP = 0.5;
|
|
926
|
+
var TARGET_FACE_PERCENTAGE_IN_CROP = 0.3;
|
|
927
|
+
function analyzeBlur(grayscalePixels, width, height, threshold = DEFAULT_BLUR_THRESHOLD) {
|
|
928
|
+
const laplacian = [];
|
|
929
|
+
for (let y = 1; y < height - 1; y++) {
|
|
930
|
+
for (let x = 1; x < width - 1; x++) {
|
|
931
|
+
const idx = y * width + x;
|
|
932
|
+
const top = grayscalePixels[idx - width] ?? 0;
|
|
933
|
+
const left = grayscalePixels[idx - 1] ?? 0;
|
|
934
|
+
const center = grayscalePixels[idx] ?? 0;
|
|
935
|
+
const right = grayscalePixels[idx + 1] ?? 0;
|
|
936
|
+
const bottom = grayscalePixels[idx + width] ?? 0;
|
|
937
|
+
const value = top + left + center * -4 + right + bottom;
|
|
938
|
+
laplacian.push(value);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const n = laplacian.length;
|
|
942
|
+
if (n === 0) {
|
|
943
|
+
return { variance: 0, isBlurry: true, threshold };
|
|
944
|
+
}
|
|
945
|
+
const mean = laplacian.reduce((a, b) => a + b, 0) / n;
|
|
946
|
+
const variance = laplacian.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / n;
|
|
947
|
+
return {
|
|
948
|
+
variance,
|
|
949
|
+
isBlurry: variance < threshold,
|
|
950
|
+
threshold
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function rgbaToGrayscale(rgbaPixels) {
|
|
954
|
+
const grayscale = [];
|
|
955
|
+
for (let i = 0; i < rgbaPixels.length; i += 4) {
|
|
956
|
+
const r = rgbaPixels[i] ?? 0;
|
|
957
|
+
const g = rgbaPixels[i + 1] ?? 0;
|
|
958
|
+
const b = rgbaPixels[i + 2] ?? 0;
|
|
959
|
+
grayscale.push(0.299 * r + 0.587 * g + 0.114 * b);
|
|
960
|
+
}
|
|
961
|
+
return grayscale;
|
|
962
|
+
}
|
|
963
|
+
function calculateBrightness(rgbaPixels) {
|
|
964
|
+
let totalLuminance = 0;
|
|
965
|
+
const pixelCount = rgbaPixels.length / 4;
|
|
966
|
+
for (let i = 0; i < rgbaPixels.length; i += 4) {
|
|
967
|
+
const r = rgbaPixels[i] ?? 0;
|
|
968
|
+
const g = rgbaPixels[i + 1] ?? 0;
|
|
969
|
+
const b = rgbaPixels[i + 2] ?? 0;
|
|
970
|
+
totalLuminance += 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
971
|
+
}
|
|
972
|
+
return pixelCount > 0 ? totalLuminance / pixelCount : 0;
|
|
973
|
+
}
|
|
974
|
+
function analyzeLighting(faceBrightness, backgroundBrightness) {
|
|
975
|
+
const ratio = backgroundBrightness > 0 ? faceBrightness / backgroundBrightness : 1;
|
|
976
|
+
let status = "good";
|
|
977
|
+
let warning;
|
|
978
|
+
if (faceBrightness < LOW_LIGHT_THRESHOLD && backgroundBrightness < LOW_LIGHT_THRESHOLD) {
|
|
979
|
+
status = "low_light";
|
|
980
|
+
warning = "Low lighting - move to a brighter area";
|
|
981
|
+
} else if (ratio < BACKLIT_RATIO_THRESHOLD) {
|
|
982
|
+
status = "backlit";
|
|
983
|
+
warning = "Backlit - try facing the light source";
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
faceBrightness,
|
|
987
|
+
backgroundBrightness,
|
|
988
|
+
ratio,
|
|
989
|
+
status,
|
|
990
|
+
warning
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function isFaceFullyVisible(boundingBox, frameWidth, frameHeight) {
|
|
994
|
+
const faceTop = boundingBox.originY / frameHeight;
|
|
995
|
+
const faceBottom = (boundingBox.originY + boundingBox.height) / frameHeight;
|
|
996
|
+
const faceLeft = boundingBox.originX / frameWidth;
|
|
997
|
+
const faceRight = (boundingBox.originX + boundingBox.width) / frameWidth;
|
|
998
|
+
if (faceTop < MIN_FACE_TOP_MARGIN) {
|
|
999
|
+
return { visible: false, reason: "Face too close to top edge" };
|
|
1000
|
+
}
|
|
1001
|
+
if (faceBottom > 1 - MIN_FACE_BOTTOM_MARGIN) {
|
|
1002
|
+
return { visible: false, reason: "Face too close to bottom edge" };
|
|
1003
|
+
}
|
|
1004
|
+
if (faceLeft < MIN_FACE_SIDE_MARGIN) {
|
|
1005
|
+
return { visible: false, reason: "Face too close to left edge" };
|
|
1006
|
+
}
|
|
1007
|
+
if (faceRight > 1 - MIN_FACE_SIDE_MARGIN) {
|
|
1008
|
+
return { visible: false, reason: "Face too close to right edge" };
|
|
1009
|
+
}
|
|
1010
|
+
return { visible: true };
|
|
1011
|
+
}
|
|
1012
|
+
var DEFAULT_OVAL_REGION = {
|
|
1013
|
+
centerX: 0.5,
|
|
1014
|
+
centerY: 0.5,
|
|
1015
|
+
width: 0.3,
|
|
1016
|
+
// 30% of frame width
|
|
1017
|
+
height: 0.4
|
|
1018
|
+
// 30% * (4/3) = 40% of frame height (3:4 aspect ratio)
|
|
1019
|
+
};
|
|
1020
|
+
function isFaceInOval(faceBox, frameWidth, frameHeight, oval = DEFAULT_OVAL_REGION, tolerance = 0.3) {
|
|
1021
|
+
const faceCenterX = (faceBox.originX + faceBox.width / 2) / frameWidth;
|
|
1022
|
+
const faceCenterY = (faceBox.originY + faceBox.height / 2) / frameHeight;
|
|
1023
|
+
const faceWidth = faceBox.width / frameWidth;
|
|
1024
|
+
const faceHeight = faceBox.height / frameHeight;
|
|
1025
|
+
const ovalRadiusX = oval.width / 2;
|
|
1026
|
+
const ovalRadiusY = oval.height / 2;
|
|
1027
|
+
const dx = faceCenterX - oval.centerX;
|
|
1028
|
+
const dy = faceCenterY - oval.centerY;
|
|
1029
|
+
const normalizedDist = dx * dx / (ovalRadiusX * ovalRadiusX) + dy * dy / (ovalRadiusY * ovalRadiusY);
|
|
1030
|
+
const centerInOval = normalizedDist <= 1;
|
|
1031
|
+
const expectedFaceWidth = oval.width * 0.7;
|
|
1032
|
+
const expectedFaceHeight = oval.height * 0.7;
|
|
1033
|
+
const widthRatio = faceWidth / expectedFaceWidth;
|
|
1034
|
+
const heightRatio = faceHeight / expectedFaceHeight;
|
|
1035
|
+
const tooSmall = widthRatio < 1 - tolerance || heightRatio < 1 - tolerance;
|
|
1036
|
+
const tooBig = widthRatio > 1 + tolerance || heightRatio > 1 + tolerance;
|
|
1037
|
+
const sizeMatch = !tooSmall && !tooBig;
|
|
1038
|
+
let feedback;
|
|
1039
|
+
if (!centerInOval) {
|
|
1040
|
+
if (dy < -0.1) {
|
|
1041
|
+
feedback = "Move face down";
|
|
1042
|
+
} else if (dy > 0.1) {
|
|
1043
|
+
feedback = "Move face up";
|
|
1044
|
+
} else if (dx < -0.1) {
|
|
1045
|
+
feedback = "Move face right";
|
|
1046
|
+
} else if (dx > 0.1) {
|
|
1047
|
+
feedback = "Move face left";
|
|
1048
|
+
} else {
|
|
1049
|
+
feedback = "Center your face in the oval";
|
|
1050
|
+
}
|
|
1051
|
+
} else if (tooSmall) {
|
|
1052
|
+
feedback = "Move closer";
|
|
1053
|
+
} else if (tooBig) {
|
|
1054
|
+
feedback = "Move back";
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
isInside: centerInOval && sizeMatch,
|
|
1058
|
+
centerInOval,
|
|
1059
|
+
sizeMatch,
|
|
1060
|
+
feedback
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function calculateFaceAlignment(boundingBox, frameWidth, frameHeight) {
|
|
1064
|
+
const faceCenterX = boundingBox.originX + boundingBox.width / 2;
|
|
1065
|
+
const faceCenterY = boundingBox.originY + boundingBox.height / 2;
|
|
1066
|
+
const targetX = frameWidth / 2;
|
|
1067
|
+
const targetY = frameHeight / 2;
|
|
1068
|
+
const distX = Math.abs(faceCenterX - targetX) / (frameWidth / 2);
|
|
1069
|
+
const distY = Math.abs(faceCenterY - targetY) / (frameHeight / 2);
|
|
1070
|
+
const faceRatio = Math.max(boundingBox.width, boundingBox.height) / Math.min(frameWidth, frameHeight);
|
|
1071
|
+
const score = Math.max(0, 1 - Math.max(distX, distY));
|
|
1072
|
+
return {
|
|
1073
|
+
score,
|
|
1074
|
+
tooClose: faceRatio > MAX_FACE_RATIO,
|
|
1075
|
+
tooFar: faceRatio < MIN_FACE_RATIO
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function calculateAdaptiveCropMultiplier(faceBox, frameWidth, frameHeight) {
|
|
1079
|
+
const faceSize = Math.max(faceBox.width, faceBox.height);
|
|
1080
|
+
const idealCropSize = faceSize * IDEAL_CROP_MULTIPLIER;
|
|
1081
|
+
const maxCropSize = Math.min(frameWidth, frameHeight);
|
|
1082
|
+
if (idealCropSize <= maxCropSize) {
|
|
1083
|
+
return IDEAL_CROP_MULTIPLIER;
|
|
1084
|
+
}
|
|
1085
|
+
const adaptiveMultiplier = maxCropSize / faceSize;
|
|
1086
|
+
return Math.max(MIN_CROP_MULTIPLIER, Math.min(MAX_CROP_MULTIPLIER, adaptiveMultiplier));
|
|
1087
|
+
}
|
|
1088
|
+
function isFaceCropFullyInFrame(faceBox, frameWidth, frameHeight) {
|
|
1089
|
+
const marginMultiplier = calculateAdaptiveCropMultiplier(faceBox, frameWidth, frameHeight);
|
|
1090
|
+
const marginX = faceBox.width * (marginMultiplier - 1) / 2;
|
|
1091
|
+
const marginY = faceBox.height * (marginMultiplier - 1) / 2;
|
|
1092
|
+
const faceLeft = faceBox.originX - marginX;
|
|
1093
|
+
const faceTop = faceBox.originY - marginY;
|
|
1094
|
+
const faceRight = faceBox.originX + faceBox.width + marginX;
|
|
1095
|
+
const faceBottom = faceBox.originY + faceBox.height + marginY;
|
|
1096
|
+
return faceLeft >= 0 && faceTop >= 0 && faceRight <= frameWidth && faceBottom <= frameHeight;
|
|
1097
|
+
}
|
|
1098
|
+
function calculateFaceCropRegion(faceBox, frameWidth, frameHeight) {
|
|
1099
|
+
const cropMultiplier = calculateAdaptiveCropMultiplier(faceBox, frameWidth, frameHeight);
|
|
1100
|
+
const expandedWidth = faceBox.width * cropMultiplier;
|
|
1101
|
+
const expandedHeight = faceBox.height * cropMultiplier;
|
|
1102
|
+
const cropSize = Math.max(expandedWidth, expandedHeight);
|
|
1103
|
+
const centerX = faceBox.originX + faceBox.width / 2;
|
|
1104
|
+
const verticalShift = faceBox.height * FACE_CENTER_VERTICAL_OFFSET;
|
|
1105
|
+
const centerY = faceBox.originY + faceBox.height / 2 + verticalShift;
|
|
1106
|
+
let cropX = centerX - cropSize / 2;
|
|
1107
|
+
let cropY = centerY - cropSize / 2;
|
|
1108
|
+
cropX = Math.max(0, Math.min(frameWidth - cropSize, cropX));
|
|
1109
|
+
cropY = Math.max(0, Math.min(frameHeight - cropSize, cropY));
|
|
1110
|
+
const finalSize = Math.min(cropSize, frameWidth - cropX, frameHeight - cropY);
|
|
1111
|
+
return { x: cropX, y: cropY, size: finalSize };
|
|
1112
|
+
}
|
|
1113
|
+
function checkFrameQuality(options) {
|
|
1114
|
+
const {
|
|
1115
|
+
faceBox,
|
|
1116
|
+
frameWidth,
|
|
1117
|
+
frameHeight,
|
|
1118
|
+
blurAnalysis,
|
|
1119
|
+
lightingAnalysis,
|
|
1120
|
+
minAlignment = MIN_CAPTURE_ALIGNMENT
|
|
1121
|
+
} = options;
|
|
1122
|
+
const result = { passed: true };
|
|
1123
|
+
if (!faceBox) {
|
|
1124
|
+
result.passed = false;
|
|
1125
|
+
result.rejectionReason = "no_face";
|
|
1126
|
+
return result;
|
|
1127
|
+
}
|
|
1128
|
+
result.visibility = isFaceFullyVisible(faceBox, frameWidth, frameHeight);
|
|
1129
|
+
if (!result.visibility.visible) {
|
|
1130
|
+
result.passed = false;
|
|
1131
|
+
result.rejectionReason = "partial_face";
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
result.alignment = calculateFaceAlignment(faceBox, frameWidth, frameHeight);
|
|
1135
|
+
if (result.alignment.tooClose) {
|
|
1136
|
+
result.passed = false;
|
|
1137
|
+
result.rejectionReason = "too_close";
|
|
1138
|
+
return result;
|
|
1139
|
+
}
|
|
1140
|
+
if (result.alignment.tooFar) {
|
|
1141
|
+
result.passed = false;
|
|
1142
|
+
result.rejectionReason = "too_far";
|
|
1143
|
+
return result;
|
|
1144
|
+
}
|
|
1145
|
+
if (result.alignment.score < minAlignment) {
|
|
1146
|
+
result.passed = false;
|
|
1147
|
+
result.rejectionReason = "misaligned";
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
if (blurAnalysis) {
|
|
1151
|
+
result.blur = blurAnalysis;
|
|
1152
|
+
if (blurAnalysis.isBlurry) {
|
|
1153
|
+
result.passed = false;
|
|
1154
|
+
result.rejectionReason = "blur";
|
|
1155
|
+
return result;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (lightingAnalysis) {
|
|
1159
|
+
result.lighting = lightingAnalysis;
|
|
1160
|
+
if (lightingAnalysis.status === "backlit") {
|
|
1161
|
+
result.passed = false;
|
|
1162
|
+
result.rejectionReason = "backlit";
|
|
1163
|
+
return result;
|
|
1164
|
+
}
|
|
1165
|
+
if (lightingAnalysis.status === "low_light") {
|
|
1166
|
+
result.passed = false;
|
|
1167
|
+
result.rejectionReason = "low_light";
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
var BaseFrameCollector = class {
|
|
1174
|
+
constructor(maxFrames = 10) {
|
|
1175
|
+
this.frames = [];
|
|
1176
|
+
this.maxFrames = maxFrames;
|
|
1177
|
+
this.startTime = Date.now();
|
|
1178
|
+
}
|
|
1179
|
+
setMaxFrames(max) {
|
|
1180
|
+
this.maxFrames = max;
|
|
1181
|
+
}
|
|
1182
|
+
addFrame(frame) {
|
|
1183
|
+
if (!this.isComplete()) {
|
|
1184
|
+
this.frames.push(frame);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
getFrames() {
|
|
1188
|
+
return [...this.frames];
|
|
1189
|
+
}
|
|
1190
|
+
getCount() {
|
|
1191
|
+
return this.frames.length;
|
|
1192
|
+
}
|
|
1193
|
+
isComplete() {
|
|
1194
|
+
return this.frames.length >= this.maxFrames;
|
|
1195
|
+
}
|
|
1196
|
+
reset() {
|
|
1197
|
+
this.frames = [];
|
|
1198
|
+
this.startTime = Date.now();
|
|
1199
|
+
}
|
|
1200
|
+
getStartTime() {
|
|
1201
|
+
return this.startTime;
|
|
1202
|
+
}
|
|
1203
|
+
getNextIndex() {
|
|
1204
|
+
return this.frames.length;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
391
1207
|
// Annotate the CommonJS export names for ESM import in node:
|
|
392
1208
|
0 && (module.exports = {
|
|
1209
|
+
ALIGNMENT_THRESHOLD_CAPTURE,
|
|
1210
|
+
ALIGNMENT_THRESHOLD_GOOD,
|
|
1211
|
+
ALIGNMENT_THRESHOLD_PERFECT,
|
|
1212
|
+
ALIGNMENT_THRESHOLD_POOR,
|
|
393
1213
|
API_ENDPOINTS,
|
|
1214
|
+
API_PATHS,
|
|
394
1215
|
AUTH_CONFIG,
|
|
395
|
-
|
|
396
|
-
|
|
1216
|
+
BACKLIT_RATIO_THRESHOLD,
|
|
1217
|
+
BLUR_THRESHOLD_MOBILE,
|
|
1218
|
+
BaseFrameCollector,
|
|
1219
|
+
DEFAULT_BLUR_THRESHOLD,
|
|
397
1220
|
DEFAULT_ENDPOINT,
|
|
1221
|
+
DEFAULT_LIVENESS_CONFIG,
|
|
1222
|
+
DEFAULT_LOCALE,
|
|
1223
|
+
DEFAULT_OVAL_REGION,
|
|
1224
|
+
DEFAULT_STATUS_MESSAGES,
|
|
1225
|
+
ES_LOCALE,
|
|
1226
|
+
FACE_CENTER_VERTICAL_OFFSET,
|
|
1227
|
+
FACE_CROP_OUTPUT_SIZE,
|
|
1228
|
+
FEEDBACK_MESSAGES,
|
|
398
1229
|
FRAME_BUFFER_CONFIG,
|
|
399
1230
|
FRAME_CONFIG,
|
|
400
1231
|
FrameBuffer,
|
|
401
1232
|
FrameQueue,
|
|
402
|
-
|
|
403
|
-
|
|
1233
|
+
GOOD_ALIGNMENT,
|
|
1234
|
+
HIGH_ALIGNMENT,
|
|
1235
|
+
HYBRID_MODEL_CONFIGS,
|
|
1236
|
+
IDEAL_CROP_MULTIPLIER,
|
|
1237
|
+
LOW_LIGHT_THRESHOLD,
|
|
1238
|
+
LivenessApiError,
|
|
1239
|
+
LivenessClient,
|
|
1240
|
+
MAX_CROP_MULTIPLIER,
|
|
1241
|
+
MAX_FACE_PERCENTAGE_IN_CROP,
|
|
1242
|
+
MAX_FACE_RATIO,
|
|
1243
|
+
MIN_CAPTURE_ALIGNMENT,
|
|
1244
|
+
MIN_CROP_MULTIPLIER,
|
|
1245
|
+
MIN_FACE_BOTTOM_MARGIN,
|
|
1246
|
+
MIN_FACE_RATIO,
|
|
1247
|
+
MIN_FACE_SIDE_MARGIN,
|
|
1248
|
+
MIN_FACE_TOP_MARGIN,
|
|
1249
|
+
MODEL_CONFIGS,
|
|
1250
|
+
OVAL_GUIDE_COLORS,
|
|
1251
|
+
OVAL_GUIDE_STYLES,
|
|
1252
|
+
RETRY_CONFIG,
|
|
1253
|
+
TARGET_FACE_PERCENTAGE_IN_CROP,
|
|
1254
|
+
analyzeBlur,
|
|
1255
|
+
analyzeLighting,
|
|
1256
|
+
calculateAdaptiveCropMultiplier,
|
|
1257
|
+
calculateBrightness,
|
|
1258
|
+
calculateFaceAlignment,
|
|
1259
|
+
calculateFaceCropRegion,
|
|
1260
|
+
canCaptureFrame,
|
|
1261
|
+
checkFrameQuality,
|
|
404
1262
|
decodeBase64,
|
|
405
1263
|
encodeBase64,
|
|
1264
|
+
generateSessionId,
|
|
1265
|
+
getCaptureQualityFeedback,
|
|
1266
|
+
getFeedbackMessage,
|
|
1267
|
+
getMinFramesForModel,
|
|
1268
|
+
getOvalGuideState,
|
|
1269
|
+
getStatusMessage,
|
|
1270
|
+
hasEnoughFrames,
|
|
1271
|
+
isFaceCropFullyInFrame,
|
|
1272
|
+
isFaceFullyVisible,
|
|
1273
|
+
isFaceInOval,
|
|
406
1274
|
retryWithBackoff,
|
|
1275
|
+
rgbaToGrayscale,
|
|
1276
|
+
sleep,
|
|
1277
|
+
toFrameData,
|
|
1278
|
+
toHybridFrameData,
|
|
1279
|
+
toLivenessResult,
|
|
407
1280
|
validateApiKey,
|
|
1281
|
+
validateFrameCount,
|
|
408
1282
|
validateFrameData,
|
|
409
|
-
|
|
1283
|
+
validateFrameIndex,
|
|
1284
|
+
validateTimestamp,
|
|
1285
|
+
validateUUID,
|
|
1286
|
+
validateUrl
|
|
410
1287
|
});
|