@moveris/shared 2.7.0 → 3.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.mjs CHANGED
@@ -35,7 +35,8 @@ var FRAME_BUFFER_CONFIG = {
35
35
  var AUTH_CONFIG = {
36
36
  timeout: 3e4,
37
37
  // 30 seconds for API requests
38
- apiKeyHeader: "X-API-Key"
38
+ apiKeyHeader: "X-API-Key",
39
+ modelVersionHeader: "X-Model-Version"
39
40
  };
40
41
  var FRAME_CONFIG = {
41
42
  targetFPS: 30,
@@ -164,14 +165,22 @@ var LivenessClient = class _LivenessClient {
164
165
  constructor(config) {
165
166
  this.baseUrl = (config.baseUrl ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
166
167
  this.apiKey = config.apiKey;
168
+ this.modelVersion = config.modelVersion;
167
169
  this.timeout = config.timeout ?? AUTH_CONFIG.timeout;
168
170
  this.enableRetry = config.enableRetry ?? true;
169
171
  this.fetchFn = config.customFetch ?? (typeof window !== "undefined" ? fetch.bind(window) : fetch);
170
172
  }
171
173
  /**
172
- * Make an authenticated API request
174
+ * Make an authenticated API request, returning the parsed body.
173
175
  */
174
176
  async request(path, options = {}) {
177
+ const { data } = await this.requestRaw(path, options);
178
+ return data;
179
+ }
180
+ /**
181
+ * Make an authenticated API request, returning both parsed body and headers.
182
+ */
183
+ async requestRaw(path, options = {}) {
175
184
  const url = `${this.baseUrl}${path}`;
176
185
  const controller = new AbortController();
177
186
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@@ -189,7 +198,8 @@ var LivenessClient = class _LivenessClient {
189
198
  if (!response.ok) {
190
199
  throw await this.parseErrorResponse(response);
191
200
  }
192
- return await response.json();
201
+ const data = await response.json();
202
+ return { data, headers: response.headers };
193
203
  } catch (error) {
194
204
  clearTimeout(timeoutId);
195
205
  if (error instanceof LivenessApiError) {
@@ -274,6 +284,30 @@ var LivenessClient = class _LivenessClient {
274
284
  const match = /'error'\s*:\s*'([^']+)'/.exec(text) ?? /"error"\s*:\s*"([^"]+)"/.exec(text);
275
285
  return match?.[1] ?? null;
276
286
  }
287
+ /**
288
+ * Parse deprecation-related headers from an API response.
289
+ * Returns undefined when no deprecation info is present.
290
+ */
291
+ static parseDeprecationHeaders(headers) {
292
+ const resolved = headers.get("x-moveris-model-resolved");
293
+ if (!resolved) return void 0;
294
+ const deprecated = headers.get("deprecation") === "true";
295
+ return {
296
+ deprecated,
297
+ resolvedModel: resolved,
298
+ deprecatedModel: headers.get("x-moveris-deprecated-model") ?? void 0,
299
+ sunsetDate: headers.get("sunset") ?? void 0,
300
+ suggestedModel: headers.get("x-moveris-suggested-model") ?? void 0
301
+ };
302
+ }
303
+ /**
304
+ * Build extra request headers for model versioning.
305
+ */
306
+ buildModelVersionHeaders(modelVersion) {
307
+ const version = modelVersion ?? this.modelVersion;
308
+ if (!version) return {};
309
+ return { [AUTH_CONFIG.modelVersionHeader]: version };
310
+ }
277
311
  /**
278
312
  * Make a request with optional retry
279
313
  */
@@ -288,6 +322,20 @@ var LivenessClient = class _LivenessClient {
288
322
  }
289
323
  return this.request(path, options);
290
324
  }
325
+ /**
326
+ * Make a request with optional retry, returning both data and headers.
327
+ */
328
+ async requestWithRetryRaw(path, options = {}) {
329
+ if (this.enableRetry) {
330
+ return retryWithBackoff(() => this.requestRaw(path, options), {
331
+ maxAttempts: RETRY_CONFIG.maxAttempts,
332
+ initialDelay: RETRY_CONFIG.initialDelay,
333
+ maxDelay: RETRY_CONFIG.maxDelay,
334
+ backoffMultiplier: RETRY_CONFIG.backoffMultiplier
335
+ });
336
+ }
337
+ return this.requestRaw(path, options);
338
+ }
291
339
  // ===========================================================================
292
340
  // Health & Status
293
341
  // ===========================================================================
@@ -322,18 +370,31 @@ var LivenessClient = class _LivenessClient {
322
370
  * @returns Liveness result
323
371
  */
324
372
  async fastCheck(frames, options = {}) {
373
+ const effectiveVersion = options.modelVersion ?? this.modelVersion;
325
374
  const request = {
326
375
  session_id: options.sessionId ?? generateSessionId(),
327
376
  model: options.model ?? "10",
328
377
  source: options.source ?? "live",
329
378
  frames: toFrameData(frames),
379
+ ...options.frameCount != null ? { frame_count: options.frameCount } : {},
330
380
  ...options.warnings?.length ? { warnings: options.warnings } : {}
331
381
  };
332
- const response = await this.requestWithRetry(API_PATHS.fastCheck, {
333
- method: "POST",
334
- body: JSON.stringify(request)
335
- });
336
- return toLivenessResult(response);
382
+ const { data: response, headers } = await this.requestWithRetryRaw(
383
+ API_PATHS.fastCheck,
384
+ {
385
+ method: "POST",
386
+ body: JSON.stringify(request),
387
+ headers: this.buildModelVersionHeaders(effectiveVersion)
388
+ }
389
+ );
390
+ const result = toLivenessResult(response);
391
+ result.deprecation = _LivenessClient.parseDeprecationHeaders(headers);
392
+ if (result.deprecation?.deprecated) {
393
+ console.warn(
394
+ `[Moveris] Model "${result.deprecation.resolvedModel}" is deprecated.` + (result.deprecation.suggestedModel ? ` Migrate to "${result.deprecation.suggestedModel}".` : "") + (result.deprecation.sunsetDate ? ` Sunset date: ${result.deprecation.sunsetDate}.` : "")
395
+ );
396
+ }
397
+ return result;
337
398
  }
338
399
  /**
339
400
  * Perform fast liveness check with pre-cropped face images
@@ -343,18 +404,32 @@ var LivenessClient = class _LivenessClient {
343
404
  * @returns Liveness result
344
405
  */
345
406
  async fastCheckCrops(crops, options = {}) {
407
+ const effectiveVersion = options.modelVersion ?? this.modelVersion;
346
408
  const request = {
347
409
  session_id: options.sessionId ?? generateSessionId(),
348
410
  model: options.model ?? "10",
349
411
  source: options.source ?? "live",
350
412
  crops,
351
- ...options.warnings?.length ? { warnings: options.warnings } : {}
413
+ ...options.frameCount != null ? { frame_count: options.frameCount } : {},
414
+ ...options.warnings?.length ? { warnings: options.warnings } : {},
415
+ ...options.bgSegmentation !== void 0 ? { bg_segmentation: options.bgSegmentation } : {}
352
416
  };
353
- const response = await this.requestWithRetry(API_PATHS.fastCheckCrops, {
354
- method: "POST",
355
- body: JSON.stringify(request)
356
- });
357
- return toLivenessResult(response);
417
+ const { data: response, headers } = await this.requestWithRetryRaw(
418
+ API_PATHS.fastCheckCrops,
419
+ {
420
+ method: "POST",
421
+ body: JSON.stringify(request),
422
+ headers: this.buildModelVersionHeaders(effectiveVersion)
423
+ }
424
+ );
425
+ const result = toLivenessResult(response);
426
+ result.deprecation = _LivenessClient.parseDeprecationHeaders(headers);
427
+ if (result.deprecation?.deprecated) {
428
+ console.warn(
429
+ `[Moveris] Model "${result.deprecation.resolvedModel}" is deprecated.` + (result.deprecation.suggestedModel ? ` Migrate to "${result.deprecation.suggestedModel}".` : "") + (result.deprecation.sunsetDate ? ` Sunset date: ${result.deprecation.sunsetDate}.` : "")
430
+ );
431
+ }
432
+ return result;
358
433
  }
359
434
  /**
360
435
  * Send a single captured frame to the streaming endpoint.
@@ -382,28 +457,29 @@ var LivenessClient = class _LivenessClient {
382
457
  return this.sendStreamFrameInternal(frameData, {
383
458
  sessionId: options.sessionId,
384
459
  model: options.model ?? "10",
460
+ modelVersion: options.modelVersion,
461
+ frameCount: options.frameCount,
385
462
  source: options.source ?? "live",
386
463
  warnings: options.warnings
387
464
  });
388
465
  }
389
466
  /**
390
467
  * Send a single FrameData to the streaming endpoint with retry (internal)
391
- *
392
- * @param frame - Single frame to send
393
- * @param options - Session and model options
394
- * @returns Stream response with status
395
468
  */
396
469
  async sendStreamFrameInternal(frameData, options) {
470
+ const effectiveVersion = options.modelVersion ?? this.modelVersion;
397
471
  const request = {
398
472
  session_id: options.sessionId,
399
473
  model: options.model,
400
474
  source: options.source,
401
475
  frame: frameData,
476
+ ...options.frameCount != null ? { frame_count: options.frameCount } : {},
402
477
  ...options.warnings?.length ? { warnings: options.warnings } : {}
403
478
  };
404
479
  return this.requestWithRetry(API_PATHS.fastCheckStream, {
405
480
  method: "POST",
406
- body: JSON.stringify(request)
481
+ body: JSON.stringify(request),
482
+ headers: this.buildModelVersionHeaders(effectiveVersion)
407
483
  });
408
484
  }
409
485
  /**
@@ -428,6 +504,8 @@ var LivenessClient = class _LivenessClient {
428
504
  const response = await this.sendStreamFrameInternal(frameData, {
429
505
  sessionId,
430
506
  model,
507
+ modelVersion: options.modelVersion,
508
+ frameCount: options.frameCount,
431
509
  source,
432
510
  warnings: options.warnings
433
511
  });
@@ -478,6 +556,8 @@ var LivenessClient = class _LivenessClient {
478
556
  const response = await this.sendStreamFrameInternal(frameData, {
479
557
  sessionId,
480
558
  model,
559
+ modelVersion: options.modelVersion,
560
+ frameCount: options.frameCount,
481
561
  source,
482
562
  warnings: options.warnings
483
563
  });
@@ -754,6 +834,7 @@ var FrameQueue = class {
754
834
  };
755
835
 
756
836
  // src/types/models.ts
837
+ var VALID_FRAME_COUNTS = [10, 30, 60, 90, 120];
757
838
  var MODEL_CONFIGS = {
758
839
  // Standard fast-check models
759
840
  "10": {
@@ -761,21 +842,24 @@ var MODEL_CONFIGS = {
761
842
  minFrames: 10,
762
843
  recommendedFrames: 10,
763
844
  description: "Fast model - 10 frames, quick verification",
764
- deprecated: false
845
+ deprecated: false,
846
+ aliases: ["fast"]
765
847
  },
766
848
  "50": {
767
849
  type: "50",
768
850
  minFrames: 50,
769
851
  recommendedFrames: 50,
770
852
  description: "Balanced model - 50 frames, good accuracy",
771
- deprecated: false
853
+ deprecated: false,
854
+ aliases: ["spatial"]
772
855
  },
773
856
  "250": {
774
857
  type: "250",
775
858
  minFrames: 250,
776
859
  recommendedFrames: 250,
777
860
  description: "High-accuracy model - 250 frames, best accuracy",
778
- deprecated: false
861
+ deprecated: false,
862
+ aliases: ["spatial"]
779
863
  },
780
864
  // Hybrid V2 models with physiological features
781
865
  "hybrid-v2-10": {
@@ -783,63 +867,72 @@ var MODEL_CONFIGS = {
783
867
  minFrames: 10,
784
868
  recommendedFrames: 10,
785
869
  description: "Hybrid V2 10-frame model with physio features",
786
- deprecated: false
870
+ deprecated: false,
871
+ aliases: ["hybrid"]
787
872
  },
788
873
  "hybrid-v2-30": {
789
874
  type: "hybrid-v2-30",
790
875
  minFrames: 30,
791
876
  recommendedFrames: 30,
792
877
  description: "Hybrid V2 30-frame model with physio features",
793
- deprecated: false
878
+ deprecated: false,
879
+ aliases: ["hybrid"]
794
880
  },
795
881
  "hybrid-v2-50": {
796
882
  type: "hybrid-v2-50",
797
883
  minFrames: 50,
798
884
  recommendedFrames: 50,
799
885
  description: "Hybrid V2 50-frame model with physio features",
800
- deprecated: false
886
+ deprecated: false,
887
+ aliases: ["hybrid"]
801
888
  },
802
889
  "hybrid-v2-60": {
803
890
  type: "hybrid-v2-60",
804
891
  minFrames: 60,
805
892
  recommendedFrames: 60,
806
893
  description: "Hybrid V2 60-frame model with physio features",
807
- deprecated: false
894
+ deprecated: false,
895
+ aliases: ["hybrid"]
808
896
  },
809
897
  "hybrid-v2-90": {
810
898
  type: "hybrid-v2-90",
811
899
  minFrames: 90,
812
900
  recommendedFrames: 90,
813
901
  description: "Hybrid V2 90-frame model with physio features",
814
- deprecated: false
902
+ deprecated: false,
903
+ aliases: ["hybrid"]
815
904
  },
816
905
  "hybrid-v2-100": {
817
906
  type: "hybrid-v2-100",
818
907
  minFrames: 100,
819
908
  recommendedFrames: 100,
820
909
  description: "Hybrid V2 100-frame model with physio features",
821
- deprecated: false
910
+ deprecated: false,
911
+ aliases: ["hybrid"]
822
912
  },
823
913
  "hybrid-v2-125": {
824
914
  type: "hybrid-v2-125",
825
915
  minFrames: 125,
826
916
  recommendedFrames: 125,
827
917
  description: "Hybrid V2 125-frame model with physio features",
828
- deprecated: false
918
+ deprecated: false,
919
+ aliases: ["hybrid"]
829
920
  },
830
921
  "hybrid-v2-150": {
831
922
  type: "hybrid-v2-150",
832
923
  minFrames: 150,
833
924
  recommendedFrames: 150,
834
925
  description: "Hybrid V2 150-frame model with physio features",
835
- deprecated: false
926
+ deprecated: false,
927
+ aliases: ["hybrid"]
836
928
  },
837
929
  "hybrid-v2-250": {
838
930
  type: "hybrid-v2-250",
839
931
  minFrames: 250,
840
932
  recommendedFrames: 250,
841
933
  description: "Hybrid V2 250-frame model with physio features",
842
- deprecated: false
934
+ deprecated: false,
935
+ aliases: ["hybrid"]
843
936
  },
844
937
  // Mixed V1 models — deprecated, use mixed-*-v2 equivalents instead
845
938
  "mixed-10": {
@@ -847,49 +940,68 @@ var MODEL_CONFIGS = {
847
940
  minFrames: 10,
848
941
  recommendedFrames: 10,
849
942
  description: "Mixed 10-frame model",
850
- deprecated: true
943
+ deprecated: true,
944
+ aliases: ["v1"],
945
+ sunsetDate: "2026-09-01",
946
+ replacement: "mixed-10-v2"
851
947
  },
852
948
  "mixed-30": {
853
949
  type: "mixed-30",
854
950
  minFrames: 30,
855
951
  recommendedFrames: 30,
856
952
  description: "Mixed 30-frame model",
857
- deprecated: true
953
+ deprecated: true,
954
+ aliases: ["v1"],
955
+ sunsetDate: "2026-09-01",
956
+ replacement: "mixed-30-v2"
858
957
  },
859
958
  "mixed-60": {
860
959
  type: "mixed-60",
861
960
  minFrames: 60,
862
961
  recommendedFrames: 60,
863
962
  description: "Mixed 60-frame model",
864
- deprecated: true
963
+ deprecated: true,
964
+ aliases: ["v1"],
965
+ sunsetDate: "2026-09-01",
966
+ replacement: "mixed-60-v2"
865
967
  },
866
968
  "mixed-90": {
867
969
  type: "mixed-90",
868
970
  minFrames: 90,
869
971
  recommendedFrames: 90,
870
972
  description: "Mixed 90-frame model",
871
- deprecated: true
973
+ deprecated: true,
974
+ aliases: ["v1"],
975
+ sunsetDate: "2026-09-01",
976
+ replacement: "mixed-90-v2"
872
977
  },
873
978
  "mixed-120": {
874
979
  type: "mixed-120",
875
980
  minFrames: 120,
876
981
  recommendedFrames: 120,
877
982
  description: "Mixed 120-frame model",
878
- deprecated: true
983
+ deprecated: true,
984
+ aliases: ["v1"],
985
+ sunsetDate: "2026-09-01",
986
+ replacement: "mixed-120-v2"
879
987
  },
880
988
  "mixed-150": {
881
989
  type: "mixed-150",
882
990
  minFrames: 150,
883
991
  recommendedFrames: 150,
884
992
  description: "Mixed 150-frame model",
885
- deprecated: true
993
+ deprecated: true,
994
+ aliases: ["v1"],
995
+ sunsetDate: "2026-09-01"
886
996
  },
887
997
  "mixed-250": {
888
998
  type: "mixed-250",
889
999
  minFrames: 250,
890
1000
  recommendedFrames: 250,
891
1001
  description: "Mixed 250-frame model",
892
- deprecated: true
1002
+ deprecated: true,
1003
+ aliases: ["v1"],
1004
+ sunsetDate: "2026-09-01"
893
1005
  },
894
1006
  // Mixed V2 models (exp_042 checkpoints — EER: 4.0% / AUC: 0.991 at 30f)
895
1007
  "mixed-10-v2": {
@@ -897,35 +1009,40 @@ var MODEL_CONFIGS = {
897
1009
  minFrames: 10,
898
1010
  recommendedFrames: 10,
899
1011
  description: "Mixed V2 10-frame model",
900
- deprecated: false
1012
+ deprecated: false,
1013
+ aliases: ["latest", "v2"]
901
1014
  },
902
1015
  "mixed-30-v2": {
903
1016
  type: "mixed-30-v2",
904
1017
  minFrames: 30,
905
1018
  recommendedFrames: 30,
906
1019
  description: "Mixed V2 30-frame model (recommended \u2014 95.7% balanced accuracy)",
907
- deprecated: false
1020
+ deprecated: false,
1021
+ aliases: ["latest", "v2"]
908
1022
  },
909
1023
  "mixed-60-v2": {
910
1024
  type: "mixed-60-v2",
911
1025
  minFrames: 60,
912
1026
  recommendedFrames: 60,
913
1027
  description: "Mixed V2 60-frame model",
914
- deprecated: false
1028
+ deprecated: false,
1029
+ aliases: ["latest", "v2"]
915
1030
  },
916
1031
  "mixed-90-v2": {
917
1032
  type: "mixed-90-v2",
918
1033
  minFrames: 90,
919
1034
  recommendedFrames: 90,
920
1035
  description: "Mixed V2 90-frame model",
921
- deprecated: false
1036
+ deprecated: false,
1037
+ aliases: ["latest", "v2"]
922
1038
  },
923
1039
  "mixed-120-v2": {
924
1040
  type: "mixed-120-v2",
925
1041
  minFrames: 120,
926
1042
  recommendedFrames: 120,
927
1043
  description: "Mixed V2 120-frame model",
928
- deprecated: false
1044
+ deprecated: false,
1045
+ aliases: ["latest", "v2"]
929
1046
  }
930
1047
  };
931
1048
  var HYBRID_MODEL_CONFIGS = {
@@ -1121,9 +1238,10 @@ var FEEDBACK_MESSAGES = {
1121
1238
  poor_lighting: "Improve lighting",
1122
1239
  too_dark: "Low lighting - move to a brighter area",
1123
1240
  backlit: "Backlit - try facing the light source",
1124
- // Phone orientation
1125
- phone_angle_low: "Raise your phone to eye level",
1126
- phone_tilted: "Hold your phone level",
1241
+ // Camera angle (platform-agnostic)
1242
+ camera_angle_low: "Raise camera to eye level",
1243
+ camera_angle_high: "Lower camera to eye level",
1244
+ camera_tilted: "Hold camera level",
1127
1245
  // Hand occlusion
1128
1246
  hand_detected: "Remove hand from face",
1129
1247
  // Eye region quality
@@ -1183,9 +1301,10 @@ var ES_LOCALE = {
1183
1301
  poor_lighting: "Mejora la iluminaci\xF3n",
1184
1302
  too_dark: "Poca luz - mu\xE9vete a un \xE1rea m\xE1s iluminada",
1185
1303
  backlit: "Contraluz - intenta mirar hacia la fuente de luz",
1186
- // Phone orientation
1187
- phone_angle_low: "Levanta el tel\xE9fono a la altura de los ojos",
1188
- phone_tilted: "Mant\xE9n el tel\xE9fono nivelado",
1304
+ // Camera angle (platform-agnostic)
1305
+ camera_angle_low: "Levanta la c\xE1mara a la altura de los ojos",
1306
+ camera_angle_high: "Baja la c\xE1mara a la altura de los ojos",
1307
+ camera_tilted: "Mant\xE9n la c\xE1mara nivelada",
1189
1308
  // Hand occlusion
1190
1309
  hand_detected: "Retira la mano del rostro",
1191
1310
  // Eye region quality
@@ -1221,17 +1340,21 @@ function getCaptureQualityFeedback(state) {
1221
1340
  targetFrames,
1222
1341
  tooFarFromIdeal,
1223
1342
  tooCloseToIdeal,
1224
- phoneAngled,
1225
- phoneTilted
1343
+ cameraAngleLow,
1344
+ cameraAngleHigh,
1345
+ cameraTilted
1226
1346
  } = state;
1227
1347
  if (!hasFace) {
1228
1348
  return FEEDBACK_MESSAGES.no_face;
1229
1349
  }
1230
- if (phoneTilted) {
1231
- return FEEDBACK_MESSAGES.phone_tilted;
1350
+ if (cameraTilted) {
1351
+ return FEEDBACK_MESSAGES.camera_tilted;
1232
1352
  }
1233
- if (phoneAngled) {
1234
- return FEEDBACK_MESSAGES.phone_angle_low;
1353
+ if (cameraAngleLow) {
1354
+ return FEEDBACK_MESSAGES.camera_angle_low;
1355
+ }
1356
+ if (cameraAngleHigh) {
1357
+ return FEEDBACK_MESSAGES.camera_angle_high;
1235
1358
  }
1236
1359
  if (tooClose) {
1237
1360
  return FEEDBACK_MESSAGES.too_close;
@@ -1275,10 +1398,11 @@ function canCaptureFrame(state) {
1275
1398
  isPartialFace,
1276
1399
  tooFarFromIdeal,
1277
1400
  tooCloseToIdeal,
1278
- phoneAngled,
1279
- phoneTilted
1401
+ cameraAngleLow,
1402
+ cameraAngleHigh,
1403
+ cameraTilted
1280
1404
  } = state;
1281
- return hasFace && alignment >= ALIGNMENT_THRESHOLD_CAPTURE && !tooClose && !tooFar && !isBlurry && !isPartialFace && !tooFarFromIdeal && !tooCloseToIdeal && !phoneAngled && !phoneTilted;
1405
+ return hasFace && alignment >= ALIGNMENT_THRESHOLD_CAPTURE && !tooClose && !tooFar && !isBlurry && !isPartialFace && !tooFarFromIdeal && !tooCloseToIdeal && !cameraAngleLow && !cameraAngleHigh && !cameraTilted;
1282
1406
  }
1283
1407
 
1284
1408
  // src/utils/validators.ts
@@ -1345,7 +1469,7 @@ function decodeBase64(base64) {
1345
1469
  }
1346
1470
 
1347
1471
  // src/utils/frameAnalysis.ts
1348
- var DEFAULT_BLUR_THRESHOLD = 100;
1472
+ var DEFAULT_BLUR_THRESHOLD = 110;
1349
1473
  var BLUR_THRESHOLD_MOBILE = 60;
1350
1474
  var BACKLIT_RATIO_THRESHOLD = 0.6;
1351
1475
  var LOW_LIGHT_THRESHOLD = 50;
@@ -1646,6 +1770,30 @@ function detectFaceRoll(landmarks) {
1646
1770
  tooTilted: roll > MAX_FACE_ROLL_DEGREES
1647
1771
  };
1648
1772
  }
1773
+ var CAMERA_ANGLE_HIGH_RATIO = 1.35;
1774
+ var CAMERA_ANGLE_LOW_RATIO = 0.75;
1775
+ function detectCameraAngle(landmarks) {
1776
+ if (landmarks.length < 153) {
1777
+ return { ratio: 1, cameraAbove: false, cameraBelow: false };
1778
+ }
1779
+ const forehead = landmarks[10];
1780
+ const noseTip = landmarks[1];
1781
+ const chin = landmarks[152];
1782
+ if (!forehead || !noseTip || !chin) {
1783
+ return { ratio: 1, cameraAbove: false, cameraBelow: false };
1784
+ }
1785
+ const foreheadToNose = Math.abs(forehead.y - noseTip.y);
1786
+ const noseToChin = Math.abs(noseTip.y - chin.y);
1787
+ if (noseToChin < 1e-3) {
1788
+ return { ratio: 1, cameraAbove: false, cameraBelow: false };
1789
+ }
1790
+ const ratio = foreheadToNose / noseToChin;
1791
+ return {
1792
+ ratio,
1793
+ cameraAbove: ratio > CAMERA_ANGLE_HIGH_RATIO,
1794
+ cameraBelow: ratio < CAMERA_ANGLE_LOW_RATIO
1795
+ };
1796
+ }
1649
1797
  var BaseFrameCollector = class {
1650
1798
  constructor(maxFrames = 10) {
1651
1799
  this.frames = [];
@@ -1862,6 +2010,8 @@ export {
1862
2010
  BACKLIT_RATIO_THRESHOLD,
1863
2011
  BLUR_THRESHOLD_MOBILE,
1864
2012
  BaseFrameCollector,
2013
+ CAMERA_ANGLE_HIGH_RATIO,
2014
+ CAMERA_ANGLE_LOW_RATIO,
1865
2015
  DEFAULT_BLUR_THRESHOLD,
1866
2016
  DEFAULT_ENDPOINT,
1867
2017
  DEFAULT_FACE_DETECTION_TIERS,
@@ -1914,6 +2064,7 @@ export {
1914
2064
  OVAL_REGION_MOBILE,
1915
2065
  RETRY_CONFIG,
1916
2066
  TARGET_FACE_PERCENTAGE_IN_CROP,
2067
+ VALID_FRAME_COUNTS,
1917
2068
  analyzeBlur,
1918
2069
  analyzeEyeRegionBrightness,
1919
2070
  analyzeEyeRegionContrast,
@@ -1926,6 +2077,7 @@ export {
1926
2077
  checkEyeRegionQuality,
1927
2078
  checkFrameQuality,
1928
2079
  decodeBase64,
2080
+ detectCameraAngle,
1929
2081
  detectFaceRoll,
1930
2082
  detectSpecularHighlights,
1931
2083
  encodeBase64,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moveris/shared",
3
- "version": "2.7.0",
3
+ "version": "3.0.0",
4
4
  "description": "Core business logic for Moveris Live SDK",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",