@aws-amplify/ui-react-liveness 3.0.15 → 3.0.17

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.
Files changed (35) hide show
  1. package/dist/esm/components/FaceLivenessDetector/FaceLivenessDetectorCore.mjs +1 -1
  2. package/dist/esm/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.mjs +1 -1
  3. package/dist/esm/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.mjs +1 -1
  4. package/dist/esm/components/FaceLivenessDetector/displayText.mjs +2 -0
  5. package/dist/esm/components/FaceLivenessDetector/service/machine/{index.mjs → machine.mjs} +43 -35
  6. package/dist/esm/components/FaceLivenessDetector/service/types/error.mjs +1 -0
  7. package/dist/esm/components/FaceLivenessDetector/service/types/liveness.mjs +0 -1
  8. package/dist/esm/components/FaceLivenessDetector/service/utils/constants.mjs +10 -2
  9. package/dist/esm/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.mjs +3 -6
  10. package/dist/esm/components/FaceLivenessDetector/service/utils/eventUtils.mjs +7 -1
  11. package/dist/esm/components/FaceLivenessDetector/service/utils/getFaceMatchStateInLivenessOval.mjs +59 -0
  12. package/dist/esm/components/FaceLivenessDetector/service/utils/liveness.mjs +22 -74
  13. package/dist/esm/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.mjs +1 -1
  14. package/dist/esm/components/FaceLivenessDetector/shared/FaceLivenessErrorModal.mjs +6 -2
  15. package/dist/esm/components/FaceLivenessDetector/shared/Hint.mjs +5 -8
  16. package/dist/esm/components/FaceLivenessDetector/utils/getDisplayText.mjs +3 -1
  17. package/dist/esm/version.mjs +1 -1
  18. package/dist/index.js +145 -122
  19. package/dist/styles.css +1 -1
  20. package/dist/types/components/FaceLivenessDetector/displayText.d.ts +2 -0
  21. package/dist/types/components/FaceLivenessDetector/service/machine/index.d.ts +1 -5
  22. package/dist/types/components/FaceLivenessDetector/service/machine/machine.d.ts +5 -0
  23. package/dist/types/components/FaceLivenessDetector/service/types/error.d.ts +1 -0
  24. package/dist/types/components/FaceLivenessDetector/service/types/liveness.d.ts +0 -1
  25. package/dist/types/components/FaceLivenessDetector/service/types/machine.d.ts +2 -3
  26. package/dist/types/components/FaceLivenessDetector/service/utils/constants.d.ts +6 -0
  27. package/dist/types/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.d.ts +1 -0
  28. package/dist/types/components/FaceLivenessDetector/service/utils/eventUtils.d.ts +1 -0
  29. package/dist/types/components/FaceLivenessDetector/service/utils/getFaceMatchStateInLivenessOval.d.ts +17 -0
  30. package/dist/types/components/FaceLivenessDetector/service/utils/index.d.ts +1 -0
  31. package/dist/types/components/FaceLivenessDetector/service/utils/liveness.d.ts +2 -8
  32. package/dist/types/components/FaceLivenessDetector/shared/FaceLivenessErrorModal.d.ts +2 -0
  33. package/dist/types/components/FaceLivenessDetector/shared/Hint.d.ts +1 -1
  34. package/dist/types/version.d.ts +1 -1
  35. package/package.json +4 -4
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { useInterpret } from '@xstate/react';
3
- import { livenessMachine } from './service/machine/index.mjs';
3
+ import { livenessMachine } from './service/machine/machine.mjs';
4
4
  import './service/types/liveness.mjs';
5
5
  import '@tensorflow/tfjs-core';
6
6
  import '@tensorflow-models/face-detection';
@@ -2,7 +2,7 @@ import React__default, { useRef, useState } from 'react';
2
2
  import { classNames } from '@aws-amplify/ui';
3
3
  import { Loader, View, Flex, Text, Label, SelectField, Button } from '@aws-amplify/ui-react';
4
4
  import { useColorMode } from '@aws-amplify/ui-react/internal';
5
- import '../service/machine/index.mjs';
5
+ import '../service/machine/machine.mjs';
6
6
  import { FaceMatchState } from '../service/types/liveness.mjs';
7
7
  import '@tensorflow/tfjs-core';
8
8
  import '@tensorflow-models/face-detection';
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { Flex, Text, Button, View } from '@aws-amplify/ui-react';
3
- import '../service/machine/index.mjs';
3
+ import '../service/machine/machine.mjs';
4
4
  import '../service/types/liveness.mjs';
5
5
  import { LivenessErrorState } from '../service/types/error.mjs';
6
6
  import '@tensorflow/tfjs-core';
@@ -1,5 +1,7 @@
1
1
  const defaultErrorDisplayText = {
2
2
  errorLabelText: 'Error',
3
+ connectionTimeoutHeaderText: 'Connection time out',
4
+ connectionTimeoutMessageText: 'Connection has timed out.',
3
5
  timeoutHeaderText: 'Time out',
4
6
  timeoutMessageText: "Face didn't fit inside oval in time limit. Try again and completely fill the oval with face in it.",
5
7
  faceDistanceHeaderText: 'Forward movement detected',
@@ -1,18 +1,18 @@
1
1
  import { nanoid } from 'nanoid';
2
2
  import { createMachine, assign, spawn, actions } from 'xstate';
3
- import { drawStaticOval, getBoundingBox, getColorsSequencesFromSessionInformation, isCameraDeviceVirtual, getFaceMatchState, isFaceDistanceBelowThreshold, estimateIllumination, getOvalDetailsFromSessionInformation, generateBboxFromLandmarks, drawLivenessOvalInCanvas, getOvalBoundingBox, getIntersectionOverUnion, getFaceMatchStateInLivenessOval, getStaticLivenessOvalDetails } from '../utils/liveness.mjs';
3
+ import { drawStaticOval, getBoundingBox, getColorsSequencesFromSessionInformation, isCameraDeviceVirtual, getFaceMatchState, isFaceDistanceBelowThreshold, estimateIllumination, getOvalDetailsFromSessionInformation, generateBboxFromLandmarks, drawLivenessOvalInCanvas, getOvalBoundingBox, getIntersectionOverUnion, getStaticLivenessOvalDetails } from '../utils/liveness.mjs';
4
4
  import { FaceMatchState } from '../types/liveness.mjs';
5
5
  import { LivenessErrorState } from '../types/error.mjs';
6
6
  import { BlazeFaceFaceDetection } from '../utils/blazefaceFaceDetection.mjs';
7
+ import { getFaceMatchStateInLivenessOval } from '../utils/getFaceMatchStateInLivenessOval.mjs';
7
8
  import { LivenessStreamProvider } from '../utils/streamProvider.mjs';
8
9
  import { FreshnessColorDisplay } from '../utils/freshnessColorDisplay.mjs';
9
- import { isServerSesssionInformationEvent, isDisconnectionEvent, isValidationExceptionEvent, isInternalServerExceptionEvent, isThrottlingExceptionEvent, isServiceQuotaExceededExceptionEvent, isInvalidSignatureRegionException } from '../utils/eventUtils.mjs';
10
+ import { isServerSesssionInformationEvent, isDisconnectionEvent, isValidationExceptionEvent, isInternalServerExceptionEvent, isThrottlingExceptionEvent, isServiceQuotaExceededExceptionEvent, isInvalidSignatureRegionException, isConnectionTimeoutError } from '../utils/eventUtils.mjs';
10
11
  import { STATIC_VIDEO_CONSTRAINTS } from '../../utils/helpers.mjs';
11
12
  import { WS_CLOSURE_CODE } from '../utils/constants.mjs';
12
13
 
13
14
  const CAMERA_ID_KEY = 'AmplifyLivenessCameraId';
14
15
  const DEFAULT_FACE_FIT_TIMEOUT = 7000;
15
- const MIN_FACE_MATCH_TIME = 1000;
16
16
  let responseStream;
17
17
  const responseStreamActor = async (callback) => {
18
18
  try {
@@ -56,14 +56,20 @@ const responseStreamActor = async (callback) => {
56
56
  }
57
57
  }
58
58
  catch (error) {
59
- let returnedError = error;
60
59
  if (isInvalidSignatureRegionException(error)) {
61
- returnedError = new Error('Invalid region in FaceLivenessDetector or credentials are scoped to the wrong region.');
62
- }
63
- if (returnedError instanceof Error) {
64
60
  callback({
65
61
  type: 'SERVER_ERROR',
66
- data: { error: returnedError },
62
+ data: {
63
+ error: new Error('Invalid region in FaceLivenessDetector or credentials are scoped to the wrong region.'),
64
+ },
65
+ });
66
+ }
67
+ else if (error instanceof Error) {
68
+ callback({
69
+ type: isConnectionTimeoutError(error)
70
+ ? 'CONNECTION_TIMEOUT'
71
+ : 'SERVER_ERROR',
72
+ data: { error },
67
73
  });
68
74
  }
69
75
  }
@@ -102,7 +108,6 @@ const livenessMachine = createMachine({
102
108
  currentDetectedFace: undefined,
103
109
  startFace: undefined,
104
110
  endFace: undefined,
105
- initialFaceMatchTime: undefined,
106
111
  },
107
112
  freshnessColorAssociatedParams: {
108
113
  freshnessColorEl: undefined,
@@ -142,6 +147,10 @@ const livenessMachine = createMachine({
142
147
  target: 'error',
143
148
  actions: 'updateErrorStateForServer',
144
149
  },
150
+ CONNECTION_TIMEOUT: {
151
+ target: 'error',
152
+ actions: 'updateErrorStateForConnectionTimeout',
153
+ },
145
154
  RUNTIME_ERROR: {
146
155
  target: 'error',
147
156
  },
@@ -287,6 +296,8 @@ const livenessMachine = createMachine({
287
296
  100: { target: 'checkRecordingStarted' },
288
297
  },
289
298
  },
299
+ // Evaluates face match and moves to checkMatch
300
+ // which continually checks for match until either timeout or face match
290
301
  ovalMatching: {
291
302
  entry: 'cancelOvalDrawingTimeout',
292
303
  invoke: {
@@ -297,29 +308,32 @@ const livenessMachine = createMachine({
297
308
  },
298
309
  },
299
310
  },
311
+ // If `hasFaceMatchedInOval` is true, then move to `delayBeforeFlash`, which pauses
312
+ // for one second to show "Hold still" text before moving to `flashFreshnessColors`.
313
+ // If not, move back to ovalMatching and re-evaluate match state
300
314
  checkMatch: {
301
315
  after: {
302
316
  0: {
303
- target: 'flashFreshnessColors',
304
- cond: 'hasFaceMatchedInOvalWithMinTime',
317
+ target: 'delayBeforeFlash',
318
+ cond: 'hasFaceMatchedInOval',
305
319
  actions: [
320
+ 'setFaceMatchTimeAndStartFace',
306
321
  'updateEndFaceMatch',
307
322
  'setupFlashFreshnessColors',
308
323
  'cancelOvalMatchTimeout',
309
324
  'cancelOvalDrawingTimeout',
310
325
  ],
311
326
  },
312
- 0.1: {
313
- target: 'ovalMatching',
314
- cond: 'hasFaceMatchedInOval',
315
- actions: 'setFaceMatchTimeAndStartFace',
316
- },
317
327
  1: {
318
328
  target: 'ovalMatching',
319
- cond: 'hasNotFaceMatchedInOval',
320
329
  },
321
330
  },
322
331
  },
332
+ delayBeforeFlash: {
333
+ after: {
334
+ 1000: 'flashFreshnessColors',
335
+ },
336
+ },
323
337
  flashFreshnessColors: {
324
338
  invoke: {
325
339
  src: 'flashColors',
@@ -584,14 +598,13 @@ const livenessMachine = createMachine({
584
598
  startFace: context.faceMatchAssociatedParams.startFace === undefined
585
599
  ? context.faceMatchAssociatedParams.currentDetectedFace
586
600
  : context.faceMatchAssociatedParams.startFace,
587
- initialFaceMatchTime: context.faceMatchAssociatedParams.initialFaceMatchTime ===
588
- undefined
589
- ? Date.now()
590
- : context.faceMatchAssociatedParams.initialFaceMatchTime,
591
601
  };
592
602
  },
593
603
  }),
594
604
  resetErrorState: assign({ errorState: (_) => undefined }),
605
+ updateErrorStateForConnectionTimeout: assign({
606
+ errorState: (_) => LivenessErrorState.CONNECTION_TIMEOUT,
607
+ }),
595
608
  updateErrorStateForTimeout: assign({
596
609
  errorState: (_, event) => event.data?.errorState || LivenessErrorState.TIMEOUT,
597
610
  }),
@@ -759,21 +772,10 @@ const livenessMachine = createMachine({
759
772
  },
760
773
  guards: {
761
774
  shouldTimeoutOnFailedAttempts: (context) => context.failedAttempts >= context.maxFailedAttempts,
762
- hasFaceMatchedInOvalWithMinTime: (context) => {
763
- const { faceMatchState, initialFaceMatchTime } = context.faceMatchAssociatedParams;
764
- const timeSinceInitialFaceMatch = Date.now() - initialFaceMatchTime;
765
- const hasMatched = faceMatchState === FaceMatchState.MATCHED &&
766
- timeSinceInitialFaceMatch >= MIN_FACE_MATCH_TIME;
767
- return hasMatched;
768
- },
769
775
  hasFaceMatchedInOval: (context) => {
770
776
  return (context.faceMatchAssociatedParams.faceMatchState ===
771
777
  FaceMatchState.MATCHED);
772
778
  },
773
- hasNotFaceMatchedInOval: (context) => {
774
- return (context.faceMatchAssociatedParams.faceMatchState !==
775
- FaceMatchState.MATCHED);
776
- },
777
779
  hasSingleFace: (context) => {
778
780
  return (context.faceMatchAssociatedParams.faceMatchState ===
779
781
  FaceMatchState.FACE_IDENTIFIED);
@@ -990,7 +992,7 @@ const livenessMachine = createMachine({
990
992
  videoWidth: videoEl.width,
991
993
  });
992
994
  // renormalize initial face
993
- const renormalizedFace = generateBboxFromLandmarks(initialFace, ovalDetails);
995
+ const renormalizedFace = generateBboxFromLandmarks(initialFace, ovalDetails, videoEl.videoHeight);
994
996
  initialFace.top = renormalizedFace.top;
995
997
  initialFace.left = renormalizedFace.left;
996
998
  initialFace.height = renormalizedFace.bottom - renormalizedFace.top;
@@ -1019,7 +1021,7 @@ const livenessMachine = createMachine({
1019
1021
  let faceMatchPercentage = 0;
1020
1022
  let detectedFace;
1021
1023
  let illuminationState;
1022
- const initialFaceBoundingBox = generateBboxFromLandmarks(initialFace, ovalDetails);
1024
+ const initialFaceBoundingBox = generateBboxFromLandmarks(initialFace, ovalDetails, videoEl.videoHeight);
1023
1025
  const { ovalBoundingBox } = getOvalBoundingBox(ovalDetails);
1024
1026
  const initialFaceIntersection = getIntersectionOverUnion(initialFaceBoundingBox, ovalBoundingBox);
1025
1027
  switch (detectedFaces.length) {
@@ -1032,7 +1034,13 @@ const livenessMachine = createMachine({
1032
1034
  case 1: {
1033
1035
  //exactly one face detected, match face with oval;
1034
1036
  detectedFace = detectedFaces[0];
1035
- const { faceMatchState: faceMatchStateInLivenessOval, faceMatchPercentage: faceMatchPercentageInLivenessOval, } = getFaceMatchStateInLivenessOval(detectedFace, ovalDetails, initialFaceIntersection, serverSessionInformation);
1037
+ const { faceMatchState: faceMatchStateInLivenessOval, faceMatchPercentage: faceMatchPercentageInLivenessOval, } = getFaceMatchStateInLivenessOval({
1038
+ face: detectedFace,
1039
+ ovalDetails: ovalDetails,
1040
+ initialFaceIntersection,
1041
+ sessionInformation: serverSessionInformation,
1042
+ frameHeight: videoEl.videoHeight,
1043
+ });
1036
1044
  faceMatchState = faceMatchStateInLivenessOval;
1037
1045
  faceMatchPercentage = faceMatchPercentageInLivenessOval;
1038
1046
  break;
@@ -2,6 +2,7 @@
2
2
  * The liveness error states
3
3
  */
4
4
  const LivenessErrorState = {
5
+ CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT',
5
6
  TIMEOUT: 'TIMEOUT',
6
7
  RUNTIME_ERROR: 'RUNTIME_ERROR',
7
8
  FRESHNESS_TIMEOUT: 'FRESHNESS_TIMEOUT',
@@ -14,7 +14,6 @@ var FaceMatchState;
14
14
  (function (FaceMatchState) {
15
15
  FaceMatchState["MATCHED"] = "MATCHED";
16
16
  FaceMatchState["TOO_FAR"] = "TOO FAR";
17
- FaceMatchState["TOO_CLOSE"] = "TOO CLOSE";
18
17
  FaceMatchState["CANT_IDENTIFY"] = "CANNOT IDENTIFY";
19
18
  FaceMatchState["FACE_IDENTIFIED"] = "ONE FACE IDENTIFIED";
20
19
  FaceMatchState["TOO_MANY"] = "TOO MANY FACES";
@@ -1,11 +1,19 @@
1
1
  // Face distance is calculated as pupilDistance / ovalWidth.
2
2
  // The further away you are from the camera the distance between your pupils will decrease, thus lowering the threshold values.
3
- // These FACE_DISTNACE_THRESHOLD values are determined by the science team and should only be changed with their approval.
3
+ // These FACE_DISTANCE_THRESHOLD values are determined by the science team and should only be changed with their approval.
4
4
  // We want to ensure at the start of a check that the user's pupilDistance/ovalWidth is below FACE_DISTANCE_THRESHOLD to ensure that they are starting
5
5
  // a certain distance away from the camera.
6
6
  const FACE_DISTANCE_THRESHOLD = 0.32;
7
7
  const REDUCED_THRESHOLD = 0.4;
8
8
  const REDUCED_THRESHOLD_MOBILE = 0.37;
9
+ // Constants from science team to determine ocular distance (space between eyes)
10
+ const PUPIL_DISTANCE_WEIGHT = 2.0;
11
+ const FACE_HEIGHT_WEIGHT = 1.8;
12
+ // Constants from science team to find face match percentage
13
+ const FACE_MATCH_RANGE_MIN = 0;
14
+ const FACE_MATCH_RANGE_MAX = 1;
15
+ const FACE_MATCH_WEIGHT_MIN = 0.25;
16
+ const FACE_MATCH_WEIGHT_MAX = 0.75;
9
17
  const WS_CLOSURE_CODE = {
10
18
  SUCCESS_CODE: 1000,
11
19
  DEFAULT_ERROR_CODE: 4000,
@@ -15,4 +23,4 @@ const WS_CLOSURE_CODE = {
15
23
  USER_ERROR_DURING_CONNECTION: 4007,
16
24
  };
17
25
 
18
- export { FACE_DISTANCE_THRESHOLD, REDUCED_THRESHOLD, REDUCED_THRESHOLD_MOBILE, WS_CLOSURE_CODE };
26
+ export { FACE_DISTANCE_THRESHOLD, FACE_HEIGHT_WEIGHT, FACE_MATCH_RANGE_MAX, FACE_MATCH_RANGE_MIN, FACE_MATCH_WEIGHT_MAX, FACE_MATCH_WEIGHT_MIN, PUPIL_DISTANCE_WEIGHT, REDUCED_THRESHOLD, REDUCED_THRESHOLD_MOBILE, WS_CLOSURE_CODE };
@@ -9,6 +9,7 @@ import { WS_CLOSURE_CODE } from '../constants.mjs';
9
9
  * Because of this the file is not fully typed at this time but we should eventually work on fully typing this file.
10
10
  */
11
11
  const DEFAULT_WS_CONNECTION_TIMEOUT_MS = 2000;
12
+ const WEBSOCKET_CONNECTION_TIMEOUT_MESSAGE = 'Websocket connection timeout';
12
13
  const isWebSocketRequest = (request) => request.protocol === 'ws:' || request.protocol === 'wss:';
13
14
  const isReadableStream = (payload) => typeof ReadableStream === 'function' && payload instanceof ReadableStream;
14
15
  /**
@@ -108,11 +109,7 @@ class CustomWebSocketFetchHandler {
108
109
  return new Promise((resolve, reject) => {
109
110
  const timeout = setTimeout(() => {
110
111
  this.removeNotUsableSockets(socket.url);
111
- reject({
112
- $metadata: {
113
- httpStatusCode: 500,
114
- },
115
- });
112
+ reject(new Error(WEBSOCKET_CONNECTION_TIMEOUT_MESSAGE));
116
113
  }, connectionTimeout);
117
114
  socket.onopen = () => {
118
115
  clearTimeout(timeout);
@@ -196,4 +193,4 @@ class CustomWebSocketFetchHandler {
196
193
  }
197
194
  }
198
195
 
199
- export { CustomWebSocketFetchHandler };
196
+ export { CustomWebSocketFetchHandler, WEBSOCKET_CONNECTION_TIMEOUT_MESSAGE };
@@ -1,7 +1,13 @@
1
+ import { WEBSOCKET_CONNECTION_TIMEOUT_MESSAGE } from './createStreamingClient/CustomWebSocketFetchHandler.mjs';
2
+
1
3
  const isServerSesssionInformationEvent = (value) => {
2
4
  return !!value
3
5
  ?.ServerSessionInformationEvent;
4
6
  };
7
+ const isConnectionTimeoutError = (error) => {
8
+ const { message } = error;
9
+ return message.includes(WEBSOCKET_CONNECTION_TIMEOUT_MESSAGE);
10
+ };
5
11
  const isDisconnectionEvent = (value) => {
6
12
  return !!value
7
13
  ?.DisconnectionEvent;
@@ -27,4 +33,4 @@ const isInvalidSignatureRegionException = (error) => {
27
33
  return (name === 'InvalidSignatureException' && message.includes('valid region'));
28
34
  };
29
35
 
30
- export { isDisconnectionEvent, isInternalServerExceptionEvent, isInvalidSignatureRegionException, isServerSesssionInformationEvent, isServiceQuotaExceededExceptionEvent, isThrottlingExceptionEvent, isValidationExceptionEvent };
36
+ export { isConnectionTimeoutError, isDisconnectionEvent, isInternalServerExceptionEvent, isInvalidSignatureRegionException, isServerSesssionInformationEvent, isServiceQuotaExceededExceptionEvent, isThrottlingExceptionEvent, isValidationExceptionEvent };
@@ -0,0 +1,59 @@
1
+ import { FaceMatchState } from '../types/liveness.mjs';
2
+ import { generateBboxFromLandmarks, getOvalBoundingBox, getIntersectionOverUnion } from './liveness.mjs';
3
+ import { FACE_MATCH_RANGE_MAX, FACE_MATCH_WEIGHT_MAX, FACE_MATCH_WEIGHT_MIN, FACE_MATCH_RANGE_MIN } from './constants.mjs';
4
+
5
+ /**
6
+ * Returns the state of the provided face with respect to the provided liveness oval.
7
+ */
8
+ function getFaceMatchStateInLivenessOval({ face, ovalDetails, initialFaceIntersection, sessionInformation, frameHeight, }) {
9
+ let faceMatchState;
10
+ const challengeConfig = sessionInformation?.Challenge?.FaceMovementAndLightChallenge
11
+ ?.ChallengeConfig;
12
+ if (!challengeConfig ||
13
+ !challengeConfig.OvalIouThreshold ||
14
+ !challengeConfig.OvalIouHeightThreshold ||
15
+ !challengeConfig.OvalIouWidthThreshold ||
16
+ !challengeConfig.FaceIouHeightThreshold ||
17
+ !challengeConfig.FaceIouWidthThreshold) {
18
+ throw new Error('Challenge information not returned from session information.');
19
+ }
20
+ const { OvalIouThreshold, FaceIouHeightThreshold, FaceIouWidthThreshold } = challengeConfig;
21
+ const faceBoundingBox = generateBboxFromLandmarks(face, ovalDetails, frameHeight);
22
+ const minFaceX = faceBoundingBox.left;
23
+ const maxFaceX = faceBoundingBox.right;
24
+ const minFaceY = faceBoundingBox.top;
25
+ const maxFaceY = faceBoundingBox.bottom;
26
+ const { ovalBoundingBox, minOvalX, minOvalY, maxOvalX, maxOvalY } = getOvalBoundingBox(ovalDetails);
27
+ const intersection = getIntersectionOverUnion(faceBoundingBox, ovalBoundingBox);
28
+ const intersectionThreshold = OvalIouThreshold;
29
+ const faceDetectionWidthThreshold = ovalDetails.width * FaceIouWidthThreshold;
30
+ const faceDetectionHeightThreshold = ovalDetails.height * FaceIouHeightThreshold;
31
+ /** From Science
32
+ * p=max(min(1,0.75∗(si−s0)/(st−s0)+0.25)),0)
33
+ */
34
+ const faceMatchPercentage = Math.max(Math.min(FACE_MATCH_RANGE_MAX, (FACE_MATCH_WEIGHT_MAX * (intersection - initialFaceIntersection)) /
35
+ (intersectionThreshold - initialFaceIntersection) +
36
+ FACE_MATCH_WEIGHT_MIN), FACE_MATCH_RANGE_MIN) * 100;
37
+ const isFaceOutsideOvalToTheLeft = minOvalX > minFaceX && maxOvalX > maxFaceX;
38
+ const isFaceOutsideOvalToTheRight = minFaceX > minOvalX && maxFaceX > maxOvalX;
39
+ const isFaceMatched = intersection > intersectionThreshold;
40
+ const isFaceMatchedClosely = minOvalY - minFaceY > faceDetectionHeightThreshold ||
41
+ maxFaceY - maxOvalY > faceDetectionHeightThreshold ||
42
+ (minOvalX - minFaceX > faceDetectionWidthThreshold &&
43
+ maxFaceX - maxOvalX > faceDetectionWidthThreshold);
44
+ if (isFaceMatched) {
45
+ faceMatchState = FaceMatchState.MATCHED;
46
+ }
47
+ else if (isFaceOutsideOvalToTheLeft || isFaceOutsideOvalToTheRight) {
48
+ faceMatchState = FaceMatchState.OFF_CENTER;
49
+ }
50
+ else if (isFaceMatchedClosely) {
51
+ faceMatchState = FaceMatchState.MATCHED;
52
+ }
53
+ else {
54
+ faceMatchState = FaceMatchState.TOO_FAR;
55
+ }
56
+ return { faceMatchState, faceMatchPercentage };
57
+ }
58
+
59
+ export { getFaceMatchStateInLivenessOval };
@@ -1,6 +1,6 @@
1
- import { FaceMatchState, IlluminationState } from '../types/liveness.mjs';
1
+ import { IlluminationState, FaceMatchState } from '../types/liveness.mjs';
2
2
  import { LivenessErrorState } from '../types/error.mjs';
3
- import { FACE_DISTANCE_THRESHOLD, REDUCED_THRESHOLD_MOBILE, REDUCED_THRESHOLD } from './constants.mjs';
3
+ import { PUPIL_DISTANCE_WEIGHT, FACE_HEIGHT_WEIGHT, FACE_DISTANCE_THRESHOLD, REDUCED_THRESHOLD_MOBILE, REDUCED_THRESHOLD } from './constants.mjs';
4
4
 
5
5
  /**
6
6
  * Returns the random number between min and max
@@ -180,87 +180,33 @@ function getPupilDistanceAndFaceHeight(face) {
180
180
  const faceHeight = Math.sqrt((eyeCenter[0] - mouth[0]) ** 2 + (eyeCenter[1] - mouth[1]) ** 2);
181
181
  return { pupilDistance, faceHeight };
182
182
  }
183
- function generateBboxFromLandmarks(face, oval) {
184
- const { leftEye, rightEye, nose, leftEar, rightEar, top: faceTop, height: faceHeight, } = face;
183
+ function generateBboxFromLandmarks(face, oval, frameHeight) {
184
+ const { leftEye, rightEye, nose, leftEar, rightEar } = face;
185
185
  const { height: ovalHeight, centerY } = oval;
186
186
  const ovalTop = centerY - ovalHeight / 2;
187
187
  const eyeCenter = [];
188
188
  eyeCenter[0] = (leftEye[0] + rightEye[0]) / 2;
189
189
  eyeCenter[1] = (leftEye[1] + rightEye[1]) / 2;
190
190
  const { pupilDistance: pd, faceHeight: fh } = getPupilDistanceAndFaceHeight(face);
191
- const alpha = 2.0, gamma = 1.8;
192
- const ow = (alpha * pd + gamma * fh) / 2;
193
- const oh = 1.618 * ow;
194
- let cx;
191
+ const ocularWidth = (PUPIL_DISTANCE_WEIGHT * pd + FACE_HEIGHT_WEIGHT * fh) / 2;
192
+ let centerFaceX, centerFaceY;
195
193
  if (eyeCenter[1] <= (ovalTop + ovalHeight) / 2) {
196
- cx = (eyeCenter[0] + nose[0]) / 2;
194
+ centerFaceX = (eyeCenter[0] + nose[0]) / 2;
195
+ centerFaceY = (eyeCenter[1] + nose[1]) / 2;
197
196
  }
198
197
  else {
199
- cx = eyeCenter[0];
198
+ // when face tilts down
199
+ centerFaceX = eyeCenter[0];
200
+ centerFaceY = eyeCenter[1];
200
201
  }
201
- const bottom = faceTop + faceHeight;
202
- const top = bottom - oh;
203
- const left = Math.min(cx - ow / 2, rightEar[0]);
204
- const right = Math.max(cx + ow / 2, leftEar[0]);
202
+ const faceWidth = ocularWidth;
203
+ const faceHeight = 1.68 * faceWidth;
204
+ const top = Math.max(centerFaceY - faceHeight / 2, 0);
205
+ const bottom = Math.min(centerFaceY + faceHeight / 2, frameHeight);
206
+ const left = Math.min(centerFaceX - ocularWidth / 2, rightEar[0]);
207
+ const right = Math.max(centerFaceX + ocularWidth / 2, leftEar[0]);
205
208
  return { bottom, left, right, top };
206
209
  }
207
- /**
208
- * Returns the state of the provided face with respect to the provided liveness oval.
209
- */
210
- // eslint-disable-next-line max-params
211
- function getFaceMatchStateInLivenessOval(face, ovalDetails, initialFaceIntersection, sessionInformation) {
212
- let faceMatchState;
213
- const challengeConfig = sessionInformation?.Challenge?.FaceMovementAndLightChallenge
214
- ?.ChallengeConfig;
215
- if (!challengeConfig ||
216
- !challengeConfig.OvalIouThreshold ||
217
- !challengeConfig.OvalIouHeightThreshold ||
218
- !challengeConfig.OvalIouWidthThreshold ||
219
- !challengeConfig.FaceIouHeightThreshold ||
220
- !challengeConfig.FaceIouWidthThreshold) {
221
- throw new Error('Challenge information not returned from session information.');
222
- }
223
- const { OvalIouThreshold, OvalIouHeightThreshold, OvalIouWidthThreshold, FaceIouHeightThreshold, FaceIouWidthThreshold, } = challengeConfig;
224
- const faceBoundingBox = generateBboxFromLandmarks(face, ovalDetails);
225
- const minFaceX = faceBoundingBox.left;
226
- const maxFaceX = faceBoundingBox.right;
227
- const minFaceY = faceBoundingBox.top;
228
- const maxFaceY = faceBoundingBox.bottom;
229
- const { ovalBoundingBox, minOvalX, minOvalY, maxOvalX, maxOvalY } = getOvalBoundingBox(ovalDetails);
230
- const intersection = getIntersectionOverUnion(faceBoundingBox, ovalBoundingBox);
231
- const intersectionThreshold = OvalIouThreshold;
232
- const ovalMatchWidthThreshold = ovalDetails.width * OvalIouWidthThreshold;
233
- const ovalMatchHeightThreshold = ovalDetails.height * OvalIouHeightThreshold;
234
- const faceDetectionWidthThreshold = ovalDetails.width * FaceIouWidthThreshold;
235
- const faceDetectionHeightThreshold = ovalDetails.height * FaceIouHeightThreshold;
236
- /** From Science
237
- * p=max(min(1,0.75∗(si−s0)/(st−s0)+0.25)),0)
238
- */
239
- const faceMatchPercentage = Math.max(Math.min(1, (0.75 * (intersection - initialFaceIntersection)) /
240
- (intersectionThreshold - initialFaceIntersection) +
241
- 0.25), 0) * 100;
242
- const faceIsOutsideOvalToTheLeft = minOvalX > minFaceX && maxOvalX > maxFaceX;
243
- const faceIsOutsideOvalToTheRight = minFaceX > minOvalX && maxFaceX > maxOvalX;
244
- if (intersection > intersectionThreshold &&
245
- Math.abs(minOvalX - minFaceX) < ovalMatchWidthThreshold &&
246
- Math.abs(maxOvalX - maxFaceX) < ovalMatchWidthThreshold &&
247
- Math.abs(maxOvalY - maxFaceY) < ovalMatchHeightThreshold) {
248
- faceMatchState = FaceMatchState.MATCHED;
249
- }
250
- else if (faceIsOutsideOvalToTheLeft || faceIsOutsideOvalToTheRight) {
251
- faceMatchState = FaceMatchState.OFF_CENTER;
252
- }
253
- else if (minOvalY - minFaceY > faceDetectionHeightThreshold ||
254
- maxFaceY - maxOvalY > faceDetectionHeightThreshold ||
255
- (minOvalX - minFaceX > faceDetectionWidthThreshold &&
256
- maxFaceX - maxOvalX > faceDetectionWidthThreshold)) {
257
- faceMatchState = FaceMatchState.TOO_CLOSE;
258
- }
259
- else {
260
- faceMatchState = FaceMatchState.TOO_FAR;
261
- }
262
- return { faceMatchState, faceMatchPercentage };
263
- }
264
210
  /**
265
211
  * Returns the illumination state in the provided video frame.
266
212
  */
@@ -436,8 +382,10 @@ async function isFaceDistanceBelowThreshold({ faceDetector, videoEl, ovalDetails
436
382
  detectedFace = detectedFaces[0];
437
383
  const { width } = ovalDetails;
438
384
  const { pupilDistance, faceHeight } = getPupilDistanceAndFaceHeight(detectedFace);
439
- const alpha = 2.0, gamma = 1.8;
440
- const calibratedPupilDistance = (alpha * pupilDistance + gamma * faceHeight) / 2 / alpha;
385
+ const calibratedPupilDistance = (PUPIL_DISTANCE_WEIGHT * pupilDistance +
386
+ FACE_HEIGHT_WEIGHT * faceHeight) /
387
+ 2 /
388
+ PUPIL_DISTANCE_WEIGHT;
441
389
  if (width) {
442
390
  isDistanceBelowThreshold =
443
391
  calibratedPupilDistance / width <
@@ -469,4 +417,4 @@ function getBoundingBox({ deviceHeight, deviceWidth, height, width, top, left, }
469
417
  };
470
418
  }
471
419
 
472
- export { clearOvalCanvas, drawLivenessOvalInCanvas, drawStaticOval, estimateIllumination, fillOverlayCanvasFractional, generateBboxFromLandmarks, getBoundingBox, getColorsSequencesFromSessionInformation, getFaceMatchState, getFaceMatchStateInLivenessOval, getIntersectionOverUnion, getOvalBoundingBox, getOvalDetailsFromSessionInformation, getRGBArrayFromColorString, getStaticLivenessOvalDetails, isCameraDeviceVirtual, isClientFreshnessColorSequence, isFaceDistanceBelowThreshold };
420
+ export { clearOvalCanvas, drawLivenessOvalInCanvas, drawStaticOval, estimateIllumination, fillOverlayCanvasFractional, generateBboxFromLandmarks, getBoundingBox, getColorsSequencesFromSessionInformation, getFaceMatchState, getIntersectionOverUnion, getOvalBoundingBox, getOvalDetailsFromSessionInformation, getRGBArrayFromColorString, getStaticLivenessOvalDetails, isCameraDeviceVirtual, isClientFreshnessColorSequence, isFaceDistanceBelowThreshold };
@@ -2,7 +2,7 @@ import React__default from 'react';
2
2
  import { ComponentClassName } from '@aws-amplify/ui';
3
3
  import { View, Flex } from '@aws-amplify/ui-react';
4
4
  import { CancelButton } from './CancelButton.mjs';
5
- import '../service/machine/index.mjs';
5
+ import '../service/machine/machine.mjs';
6
6
  import '../service/types/liveness.mjs';
7
7
  import '@tensorflow/tfjs-core';
8
8
  import '@tensorflow-models/face-detection';
@@ -1,7 +1,7 @@
1
1
  import React__default from 'react';
2
2
  import { Flex, Text, Button } from '@aws-amplify/ui-react';
3
3
  import { AlertIcon } from '@aws-amplify/ui-react/internal';
4
- import '../service/machine/index.mjs';
4
+ import '../service/machine/machine.mjs';
5
5
  import '../service/types/liveness.mjs';
6
6
  import { LivenessErrorState } from '../service/types/error.mjs';
7
7
  import '@tensorflow/tfjs-core';
@@ -19,10 +19,14 @@ import { LivenessClassNames } from '../types/classNames.mjs';
19
19
 
20
20
  const renderToastErrorModal = (props) => {
21
21
  const { error: errorState, displayText } = props;
22
- const { errorLabelText, timeoutHeaderText, timeoutMessageText, faceDistanceHeaderText, faceDistanceMessageText, multipleFacesHeaderText, multipleFacesMessageText, clientHeaderText, clientMessageText, serverHeaderText, serverMessageText, } = displayText;
22
+ const { connectionTimeoutHeaderText, connectionTimeoutMessageText, errorLabelText, timeoutHeaderText, timeoutMessageText, faceDistanceHeaderText, faceDistanceMessageText, multipleFacesHeaderText, multipleFacesMessageText, clientHeaderText, clientMessageText, serverHeaderText, serverMessageText, } = displayText;
23
23
  let heading;
24
24
  let message;
25
25
  switch (errorState) {
26
+ case LivenessErrorState.CONNECTION_TIMEOUT:
27
+ heading = connectionTimeoutHeaderText;
28
+ message = connectionTimeoutMessageText;
29
+ break;
26
30
  case LivenessErrorState.TIMEOUT:
27
31
  heading = timeoutHeaderText;
28
32
  message = timeoutMessageText;
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { VisuallyHidden, View } from '@aws-amplify/ui-react';
3
- import '../service/machine/index.mjs';
3
+ import '../service/machine/machine.mjs';
4
4
  import { FaceMatchState, IlluminationState } from '../service/types/liveness.mjs';
5
5
  import '@tensorflow/tfjs-core';
6
6
  import '@tensorflow-models/face-detection';
@@ -52,7 +52,6 @@ const Hint = ({ hintDisplayText }) => {
52
52
  [FaceMatchState.CANT_IDENTIFY]: hintDisplayText.hintCanNotIdentifyText,
53
53
  [FaceMatchState.FACE_IDENTIFIED]: hintDisplayText.hintTooFarText,
54
54
  [FaceMatchState.TOO_MANY]: hintDisplayText.hintTooManyFacesText,
55
- [FaceMatchState.TOO_CLOSE]: hintDisplayText.hintTooCloseText,
56
55
  [FaceMatchState.TOO_FAR]: hintDisplayText.hintTooFarText,
57
56
  [FaceMatchState.MATCHED]: hintDisplayText.hintHoldFaceForFreshnessText,
58
57
  [FaceMatchState.OFF_CENTER]: hintDisplayText.hintFaceOffCenterText,
@@ -98,13 +97,11 @@ const Hint = ({ hintDisplayText }) => {
98
97
  return React.createElement(DefaultToast, { text: hintDisplayText.hintHoldFaceForFreshnessText });
99
98
  }
100
99
  if (isRecording && !isFlashingFreshness) {
101
- // During face matching, we want to only show the TOO_CLOSE or
102
- // TOO_FAR texts. If FaceMatchState matches TOO_CLOSE, we'll show
103
- // the TOO_CLOSE text, but for FACE_IDENTIFED, CANT_IDENTIFY, TOO_MANY
100
+ // During face matching, we want to only show the
101
+ // TOO_FAR texts. For FACE_IDENTIFIED, CANT_IDENTIFY, TOO_MANY
104
102
  // we are defaulting to the TOO_FAR text (for now).
105
103
  let resultHintString = FaceMatchStateStringMap[FaceMatchState.TOO_FAR];
106
- if (faceMatchState === FaceMatchState.TOO_CLOSE ||
107
- faceMatchState === FaceMatchState.MATCHED) {
104
+ if (faceMatchState === FaceMatchState.MATCHED) {
108
105
  resultHintString = FaceMatchStateStringMap[faceMatchState];
109
106
  }
110
107
  // If the face is outside the oval set the aria-label to a string about centering face in oval
@@ -118,7 +115,7 @@ const Hint = ({ hintDisplayText }) => {
118
115
  faceMatchPercentage > 50) {
119
116
  a11yHintString = hintDisplayText.hintMatchIndicatorText;
120
117
  }
121
- return (React.createElement(Toast, { size: "large", variation: faceMatchState === FaceMatchState.TOO_CLOSE ? 'error' : 'primary' },
118
+ return (React.createElement(Toast, { size: "large", variation: 'primary' },
122
119
  React.createElement(VisuallyHidden, { "aria-live": "assertive" }, a11yHintString),
123
120
  React.createElement(View, { "aria-label": a11yHintString }, resultHintString)));
124
121
  }
@@ -12,7 +12,7 @@ function getDisplayText(overrideDisplayText) {
12
12
  ...defaultLivenessDisplayText,
13
13
  ...overrideDisplayText,
14
14
  };
15
- const { a11yVideoLabelText, cameraMinSpecificationsHeadingText, cameraMinSpecificationsMessageText, cameraNotFoundHeadingText, cameraNotFoundMessageText, cancelLivenessCheckText, clientHeaderText, clientMessageText, errorLabelText, hintCanNotIdentifyText, hintCenterFaceText, hintCenterFaceInstructionText, hintFaceOffCenterText, hintConnectingText, hintFaceDetectedText, hintHoldFaceForFreshnessText, hintIlluminationNormalText, hintIlluminationTooBrightText, hintIlluminationTooDarkText, hintMoveFaceFrontOfCameraText, hintTooManyFacesText, hintTooCloseText, hintTooFarText, hintVerifyingText, hintCheckCompleteText, hintMatchIndicatorText, faceDistanceHeaderText, faceDistanceMessageText, goodFitCaptionText, goodFitAltText, landscapeHeaderText, landscapeMessageText, multipleFacesHeaderText, multipleFacesMessageText, photosensitivityWarningBodyText, photosensitivityWarningHeadingText, photosensitivityWarningInfoText, photosensitivityWarningLabelText, photosensitivyWarningBodyText, photosensitivyWarningHeadingText, photosensitivyWarningInfoText, photosensitivyWarningLabelText, portraitMessageText, retryCameraPermissionsText, recordingIndicatorText, serverHeaderText, serverMessageText, startScreenBeginCheckText, timeoutHeaderText, timeoutMessageText, tooFarCaptionText, tooFarAltText, tryAgainText, waitingCameraPermissionText, } = displayText;
15
+ const { a11yVideoLabelText, cameraMinSpecificationsHeadingText, cameraMinSpecificationsMessageText, cameraNotFoundHeadingText, cameraNotFoundMessageText, cancelLivenessCheckText, connectionTimeoutHeaderText, connectionTimeoutMessageText, clientHeaderText, clientMessageText, errorLabelText, hintCanNotIdentifyText, hintCenterFaceText, hintCenterFaceInstructionText, hintFaceOffCenterText, hintConnectingText, hintFaceDetectedText, hintHoldFaceForFreshnessText, hintIlluminationNormalText, hintIlluminationTooBrightText, hintIlluminationTooDarkText, hintMoveFaceFrontOfCameraText, hintTooManyFacesText, hintTooCloseText, hintTooFarText, hintVerifyingText, hintCheckCompleteText, hintMatchIndicatorText, faceDistanceHeaderText, faceDistanceMessageText, goodFitCaptionText, goodFitAltText, landscapeHeaderText, landscapeMessageText, multipleFacesHeaderText, multipleFacesMessageText, photosensitivityWarningBodyText, photosensitivityWarningHeadingText, photosensitivityWarningInfoText, photosensitivityWarningLabelText, photosensitivyWarningBodyText, photosensitivyWarningHeadingText, photosensitivyWarningInfoText, photosensitivyWarningLabelText, portraitMessageText, retryCameraPermissionsText, recordingIndicatorText, serverHeaderText, serverMessageText, startScreenBeginCheckText, timeoutHeaderText, timeoutMessageText, tooFarCaptionText, tooFarAltText, tryAgainText, waitingCameraPermissionText, } = displayText;
16
16
  const hintDisplayText = {
17
17
  hintMoveFaceFrontOfCameraText,
18
18
  hintTooManyFacesText,
@@ -61,6 +61,8 @@ function getDisplayText(overrideDisplayText) {
61
61
  recordingIndicatorText,
62
62
  };
63
63
  const errorDisplayText = {
64
+ connectionTimeoutHeaderText,
65
+ connectionTimeoutMessageText,
64
66
  errorLabelText,
65
67
  timeoutHeaderText,
66
68
  timeoutMessageText,
@@ -1,3 +1,3 @@
1
- const VERSION = '3.0.15';
1
+ const VERSION = '3.0.17';
2
2
 
3
3
  export { VERSION };