@richard.fadiora/liveness-detection 4.0.1 → 4.2.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.
@@ -0,0 +1,22 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const P=require("@mediapipe/tasks-vision"),b=require("react"),ne=require("react-webcam"),$=async(p,t,c)=>{const i=new FormData;t.forEach((n,a)=>{i.append("files",n,`frame_${a}.jpg`)}),i.append("challenge",JSON.stringify(c));const r=await fetch(`${p}/v1/verify`,{method:"POST",body:i});if(!r.ok)throw new Error("Network response was not ok");return await r.json()},ae=Object.freeze(Object.defineProperty({__proto__:null,verifyLiveness:$},Symbol.toStringTag,{value:"Module"}));class U{constructor(t){this.models={face:null,hand:null},this.webcam=null,this.currentStepRef=0,this.isStepTransitioningRef=!1,this.timerId=null,this.requestId=null,this.offscreenCanvas=document.createElement("canvas"),this.CHALLENGE_POOL=["Smile","Blink","Turn_Head","Thumbs_Up"],this.attachVideo=c=>{this.webcam=c,console.log("[LivenessEngine] Video stream attached.")},this.start=()=>{if(this.state.status!=="ready"||!this.webcam){console.warn("[LivenessEngine] Engine not ready or video missing.");return}console.log("[LivenessEngine] Session Starting..."),this.state.status="capturing",this.updateState({status:"capturing"}),this.startTimer(),this.detectLoop()},this.stop=()=>{this.timerId&&clearInterval(this.timerId),this.requestId&&cancelAnimationFrame(this.requestId),this.timerId=null,this.requestId=null},this.reset=()=>{this.stop(),this.currentStepRef=0,this.isStepTransitioningRef=!1,this.generateSequence(),this.updateState({status:"ready",currentStep:0,timeLeft:this.config.duration,isStepTransitioning:!1,errorMsg:""})},this.detectLoop=()=>{var i;if(this.state.status!=="capturing"||!this.webcam||!this.models.face)return;const c=performance.now();try{const r=this.models.face.detectForVideo(this.webcam,c),l=(i=this.models.hand)==null?void 0:i.detectForVideo(this.webcam,c),n=this.state.sequence[this.currentStepRef];if(this.checkAction(r,l,n)){this.handleStepSuccess();return}}catch(r){console.error("[LivenessEngine] Loop detection error:",r)}this.requestId=requestAnimationFrame(this.detectLoop)},this.config={apiUrl:t.apiUrl,duration:t.duration??60,smileThreshold:t.smileThreshold??.2,blinkThreshold:t.blinkThreshold??.012,minturnHeadThreshold:t.minturnHeadThreshold??.15,maxturnHeadThreshold:t.maxturnHeadThreshold??.85,onStateChange:t.onStateChange||(()=>{}),onComplete:t.onComplete||(()=>{}),onError:t.onError||(()=>{})},this.state={status:"loading",sequence:[],currentStep:0,timeLeft:this.config.duration,isStepTransitioning:!1,errorMsg:""},this.offscreenCanvas.width=224,this.offscreenCanvas.height=224}async loadModels(){console.log("[LivenessEngine] Loading AI Models...");try{const t=await P.FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm");this.models.face=await P.FaceLandmarker.createFromOptions(t,{baseOptions:{modelAssetPath:"https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",delegate:"GPU"},outputFaceBlendshapes:!0,runningMode:"VIDEO"}),this.models.hand=await P.HandLandmarker.createFromOptions(t,{baseOptions:{modelAssetPath:"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task",delegate:"GPU"},runningMode:"VIDEO",numHands:1}),this.generateSequence(),this.updateState({status:"ready"}),console.log("[LivenessEngine] Models Loaded.")}catch(t){console.error("[LivenessEngine] Init Error:",t),this.updateState({status:"error",errorMsg:"Failed to load security assets."})}}updateState(t){this.state={...this.state,...t},this.config.onStateChange(this.state)}generateSequence(){const t=[...this.CHALLENGE_POOL].sort(()=>.5-Math.random()).slice(0,3);return this.state.sequence=t,t}startTimer(){this.timerId&&clearInterval(this.timerId),this.timerId=setInterval(()=>{this.state.timeLeft>0?this.updateState({timeLeft:this.state.timeLeft-1}):(this.stop(),this.updateState({status:"expired"}))},1e3)}handleStepSuccess(){this.isStepTransitioningRef||(this.isStepTransitioningRef=!0,this.updateState({isStepTransitioning:!0}),setTimeout(()=>{this.currentStepRef<this.state.sequence.length-1?(this.currentStepRef++,this.isStepTransitioningRef=!1,this.updateState({currentStep:this.currentStepRef,isStepTransitioning:!1}),this.requestId=requestAnimationFrame(this.detectLoop)):this.sendFinalProof()},1500))}checkAction(t,c,i){var r,l;if(this.isStepTransitioningRef)return!1;if(i==="Thumbs_Up"&&((r=c==null?void 0:c.landmarks)==null?void 0:r.length)>0){const n=c.landmarks[0];return n[4].y<n[2].y&&[8,12,16,20].every(a=>n[a].y>n[a-2].y)}if(((l=t==null?void 0:t.faceLandmarks)==null?void 0:l.length)>0){const n=t.faceLandmarks[0];switch(i){case"Smile":return n[291].x-n[61].x>this.config.smileThreshold;case"Blink":const a=Math.abs(n[159].y-n[145].y),h=Math.abs(n[386].y-n[374].y);return(a+h)/2<this.config.blinkThreshold;case"Turn_Head":const m=(n[1].x-n[33].x)/(n[263].x-n[33].x);return m<this.config.minturnHeadThreshold||m>this.config.maxturnHeadThreshold;default:return!1}}return!1}async sendFinalProof(){var t,c;this.stop(),this.updateState({status:"verifying"});try{const i=[];let r="";for(let a=0;a<5&&this.webcam;a++){const h=(t=this.models.face)==null?void 0:t.detectForVideo(this.webcam,performance.now());if((c=h==null?void 0:h.faceLandmarks)!=null&&c[0]){const m=this.getFaceCrop(this.webcam,h.faceLandmarks[0]),o=await new Promise(g=>m.toBlob(g,"image/jpeg",.9));o&&i.push(o),a===4&&(r=m.toDataURL("image/jpeg"))}await new Promise(m=>setTimeout(m,100))}const{verifyLiveness:l}=await Promise.resolve().then(()=>ae),n=await l(this.config.apiUrl,i,this.state.sequence);if(n.is_live)this.updateState({status:"success"}),this.config.onComplete({success:!0,image:r,skinConfidence:n.skin_confidence});else{const a=n.reason||"Liveness check failed";this.updateState({status:"error",errorMsg:a}),this.config.onError({success:!1,reason:a})}}catch{this.updateState({status:"error",errorMsg:"Verification failed."}),this.config.onError({success:!1,reason:"Network error"})}}getFaceCrop(t,c){const i=this.offscreenCanvas.getContext("2d"),r=c.map(S=>S.x*t.videoWidth),l=c.map(S=>S.y*t.videoHeight),n=Math.min(...r),a=Math.max(...r),h=Math.min(...l),m=Math.max(...l),o=a-n,g=m-h,E=o*.3;return i.clearRect(0,0,224,224),i.drawImage(t,n-E,h-E,o+E*2,g+E*2,0,0,224,224),this.offscreenCanvas}}const H=p=>{const[t,c]=b.useState({status:"loading",sequence:[],currentStep:0,timeLeft:p.duration||60,isStepTransitioning:!1,errorMsg:""}),i=b.useRef(null),r=b.useRef(null);b.useEffect(()=>{const a=new U({...p,onStateChange:h=>c({...h})});return i.current=a,a.loadModels(),()=>a.stop()},[]),b.useEffect(()=>{var a,h;if(t.status!=="loading"&&t.status!=="error"){const m=(a=r.current)==null?void 0:a.video;if(m&&m.readyState>=2)(h=i.current)==null||h.attachVideo(m);else{const o=setInterval(()=>{var E,S;const g=(E=r.current)==null?void 0:E.video;g&&g.readyState>=2&&((S=i.current)==null||S.attachVideo(g),clearInterval(o))},100);return()=>clearInterval(o)}}},[t.status]);const l=b.useCallback(()=>{var a;console.log("[Hook] Start Clicked"),(a=i.current)==null||a.start()},[]),n=b.useCallback(()=>{var a;console.log("[Hook] Reset Clicked"),(a=i.current)==null||a.reset()},[]);return{...t,webcamRef:r,start:l,reset:n}};var A={exports:{}},T={};/**
2
+ * @license React
3
+ * react-jsx-runtime.production.js
4
+ *
5
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var Y;function ie(){if(Y)return T;Y=1;var p=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function c(i,r,l){var n=null;if(l!==void 0&&(n=""+l),r.key!==void 0&&(n=""+r.key),"key"in r){l={};for(var a in r)a!=="key"&&(l[a]=r[a])}else l=r;return r=l.ref,{$$typeof:p,type:i,key:n,ref:r!==void 0?r:null,props:l}}return T.Fragment=t,T.jsx=c,T.jsxs=c,T}var k={};/**
10
+ * @license React
11
+ * react-jsx-runtime.development.js
12
+ *
13
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
14
+ *
15
+ * This source code is licensed under the MIT license found in the
16
+ * LICENSE file in the root directory of this source tree.
17
+ */var D;function oe(){return D||(D=1,process.env.NODE_ENV!=="production"&&function(){function p(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===te?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case R:return"Fragment";case z:return"Profiler";case G:return"StrictMode";case Z:return"Suspense";case Q:return"SuspenseList";case ee:return"Activity"}if(typeof e=="object")switch(typeof e.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),e.$$typeof){case W:return"Portal";case J:return e.displayName||"Context";case B:return(e._context.displayName||"Context")+".Consumer";case X:var s=e.render;return e=e.displayName,e||(e=s.displayName||s.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case K:return s=e.displayName||null,s!==null?s:p(e.type)||"Memo";case w:s=e._payload,e=e._init;try{return p(e(s))}catch{}}return null}function t(e){return""+e}function c(e){try{t(e);var s=!1}catch{s=!0}if(s){s=console;var u=s.error,d=typeof Symbol=="function"&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||"Object";return u.call(s,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",d),t(e)}}function i(e){if(e===R)return"<>";if(typeof e=="object"&&e!==null&&e.$$typeof===w)return"<...>";try{var s=p(e);return s?"<"+s+">":"<...>"}catch{return"<...>"}}function r(){var e=x.A;return e===null?null:e.getOwner()}function l(){return Error("react-stack-top-frame")}function n(e){if(I.call(e,"key")){var s=Object.getOwnPropertyDescriptor(e,"key").get;if(s&&s.isReactWarning)return!1}return e.key!==void 0}function a(e,s){function u(){N||(N=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",s))}u.isReactWarning=!0,Object.defineProperty(e,"key",{get:u,configurable:!0})}function h(){var e=p(this.type);return F[e]||(F[e]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),e=this.props.ref,e!==void 0?e:null}function m(e,s,u,d,y,L){var f=u.ref;return e={$$typeof:C,type:e,key:s,props:u,_owner:d},(f!==void 0?f:null)!==null?Object.defineProperty(e,"ref",{enumerable:!1,get:h}):Object.defineProperty(e,"ref",{enumerable:!1,value:null}),e._store={},Object.defineProperty(e._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:y}),Object.defineProperty(e,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:L}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function o(e,s,u,d,y,L){var f=s.children;if(f!==void 0)if(d)if(re(f)){for(d=0;d<f.length;d++)g(f[d]);Object.freeze&&Object.freeze(f)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else g(f);if(I.call(s,"key")){f=p(e);var _=Object.keys(s).filter(function(se){return se!=="key"});d=0<_.length?"{key: someKey, "+_.join(": ..., ")+": ...}":"{key: someKey}",V[f+d]||(_=0<_.length?"{"+_.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
18
+ let props = %s;
19
+ <%s {...props} />
20
+ React keys must be passed directly to JSX without using spread:
21
+ let props = %s;
22
+ <%s key={someKey} {...props} />`,d,f,_,f),V[f+d]=!0)}if(f=null,u!==void 0&&(c(u),f=""+u),n(s)&&(c(s.key),f=""+s.key),"key"in s){u={};for(var j in s)j!=="key"&&(u[j]=s[j])}else u=s;return f&&a(u,typeof e=="function"?e.displayName||e.name||"Unknown":e),m(e,f,u,r(),y,L)}function g(e){E(e)?e._store&&(e._store.validated=1):typeof e=="object"&&e!==null&&e.$$typeof===w&&(e._payload.status==="fulfilled"?E(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function E(e){return typeof e=="object"&&e!==null&&e.$$typeof===C}var S=b,C=Symbol.for("react.transitional.element"),W=Symbol.for("react.portal"),R=Symbol.for("react.fragment"),G=Symbol.for("react.strict_mode"),z=Symbol.for("react.profiler"),B=Symbol.for("react.consumer"),J=Symbol.for("react.context"),X=Symbol.for("react.forward_ref"),Z=Symbol.for("react.suspense"),Q=Symbol.for("react.suspense_list"),K=Symbol.for("react.memo"),w=Symbol.for("react.lazy"),ee=Symbol.for("react.activity"),te=Symbol.for("react.client.reference"),x=S.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,I=Object.prototype.hasOwnProperty,re=Array.isArray,O=console.createTask?console.createTask:function(){return null};S={react_stack_bottom_frame:function(e){return e()}};var N,F={},M=S.react_stack_bottom_frame.bind(S,l)(),q=O(i(l)),V={};k.Fragment=R,k.jsx=function(e,s,u){var d=1e4>x.recentlyCreatedOwnerStacks++;return o(e,s,u,!1,d?Error("react-stack-top-frame"):M,d?O(i(e)):q)},k.jsxs=function(e,s,u){var d=1e4>x.recentlyCreatedOwnerStacks++;return o(e,s,u,!0,d?Error("react-stack-top-frame"):M,d?O(i(e)):q)}}()),k}process.env.NODE_ENV==="production"?A.exports=ie():A.exports=oe();var v=A.exports;const ce=({apiUrl:p,duration:t,onComplete:c,onError:i,classNames:r={},render:l,smileThreshold:n,blinkThreshold:a,minturnHeadThreshold:h,maxturnHeadThreshold:m})=>{var g;const o=H({apiUrl:p,duration:t,smileThreshold:n,blinkThreshold:a,minturnHeadThreshold:h,maxturnHeadThreshold:m,onComplete:c,onError:i});return l?l(o):v.jsxs("div",{className:r.container,children:[o.status==="loading"&&v.jsx("div",{children:"Initializing..."}),["ready","capturing","verifying"].includes(o.status)&&v.jsxs(v.Fragment,{children:[v.jsx(ne,{ref:o.webcamRef,mirrored:!0,screenshotFormat:"image/jpeg",className:r.webcam}),o.status==="capturing"&&v.jsx("div",{className:"liveness-challenge",children:o.isStepTransitioning?"Step verified! Get ready for the next...":`Do this: ${(g=o.sequence[o.currentStep])==null?void 0:g.replace("_"," ")}`}),["capturing","ready"].includes(o.status)&&v.jsxs("div",{className:r.timer,children:[o.timeLeft,"s"]}),o.status==="ready"&&v.jsx("button",{onClick:o.start,className:r.button,children:"Start Challenge"}),o.status==="verifying"&&v.jsx("div",{children:"Analyzing..."})]}),o.status==="error"&&v.jsxs("div",{className:r.error,children:[o.errorMsg,v.jsx("button",{onClick:o.reset,className:r.button,children:"Retry"})]}),o.status==="success"&&v.jsx("div",{className:r.success,children:"Verification Complete"})]})};exports.LivenessEngine=U;exports.LivenessSDK=ce;exports.useLiveness=H;exports.verifyLiveness=$;
@@ -0,0 +1,90 @@
1
+ import { default as default_2 } from 'react-webcam';
2
+
3
+ declare type Challenge = "Smile" | "Blink" | "Turn_Head" | "Thumbs_Up";
4
+
5
+ export declare class LivenessEngine {
6
+ private config;
7
+ private models;
8
+ private webcam;
9
+ private state;
10
+ private currentStepRef;
11
+ private isStepTransitioningRef;
12
+ private timerId;
13
+ private requestId;
14
+ private offscreenCanvas;
15
+ private CHALLENGE_POOL;
16
+ constructor(config: LivenessEngineConfig);
17
+ /**
18
+ * Phase 1: Load AI Assets (Call this immediately on mount)
19
+ */
20
+ loadModels(): Promise<void>;
21
+ /**
22
+ * Phase 2: Attach Video (Call this once the Webcam is in the DOM)
23
+ */
24
+ attachVideo: (video: HTMLVideoElement) => void;
25
+ start: () => void;
26
+ stop: () => void;
27
+ reset: () => void;
28
+ private updateState;
29
+ private generateSequence;
30
+ private startTimer;
31
+ private detectLoop;
32
+ private handleStepSuccess;
33
+ private checkAction;
34
+ private sendFinalProof;
35
+ private getFaceCrop;
36
+ }
37
+
38
+ export declare interface LivenessEngineConfig {
39
+ apiUrl: string;
40
+ duration?: number;
41
+ smileThreshold?: number;
42
+ blinkThreshold?: number;
43
+ minturnHeadThreshold?: number;
44
+ maxturnHeadThreshold?: number;
45
+ onStateChange?: (state: LivenessState) => void;
46
+ onComplete?: (result: LivenessSDKResult) => void;
47
+ onError?: (error: LivenessSDKResult) => void;
48
+ }
49
+
50
+ export declare interface LivenessResponse {
51
+ is_live: boolean;
52
+ reason?: string;
53
+ skin_confidence?: number;
54
+ [key: string]: any;
55
+ }
56
+
57
+ export declare interface LivenessSDKResult {
58
+ success: boolean;
59
+ image?: string;
60
+ reason?: string;
61
+ skinConfidence?: number;
62
+ }
63
+
64
+ export declare interface LivenessState {
65
+ status: "loading" | "ready" | "capturing" | "verifying" | "success" | "error" | "expired";
66
+ sequence: Challenge[];
67
+ currentStep: number;
68
+ timeLeft: number;
69
+ isStepTransitioning: boolean;
70
+ errorMsg: string;
71
+ }
72
+
73
+ export declare const useLiveness: (config: LivenessEngineConfig) => UseLivenessHook;
74
+
75
+ export declare interface UseLivenessHook extends LivenessState {
76
+ webcamRef: React.RefObject<default_2>;
77
+ start: () => void;
78
+ reset: () => void;
79
+ }
80
+
81
+ /**
82
+ * Sends captured frames to the backend API for liveness verification.
83
+ * @param apiUrl - Base URL of the liveness backend
84
+ * @param frameBlobs - Array of Blob objects representing captured frames
85
+ * @param challenge - The current challenge string
86
+ * @returns LivenessResponse from backend
87
+ */
88
+ export declare const verifyLiveness: (apiUrl: string, frameBlobs: Blob[], challenge: Challenge[]) => Promise<LivenessResponse>;
89
+
90
+ export { }