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