@jupitermetalabs/face-zk-sdk 0.3.4 → 0.3.7
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/README.md +46 -1
- package/assets/face-guidance/pose-guidance.js.txt +4 -2
- package/assets/liveness/liveness.js.txt +3 -0
- package/dist/FaceZkSdk.d.ts +69 -0
- package/dist/FaceZkSdk.js +136 -0
- package/dist/assets/face-guidance/pose-guidance.js.txt +4 -2
- package/dist/assets/liveness/liveness.js.txt +3 -0
- package/dist/assets/onnx/ort-min.d.ts +1 -0
- package/dist/assets/onnx/ort-min.js +11 -0
- package/dist/config/defaults.d.ts +51 -0
- package/dist/config/defaults.js +61 -0
- package/dist/config/types.d.ts +169 -0
- package/dist/config/types.js +17 -0
- package/dist/core/enrollment-core.d.ts +70 -0
- package/dist/core/enrollment-core.js +206 -0
- package/dist/core/idGenerator.d.ts +11 -0
- package/dist/core/idGenerator.js +32 -0
- package/dist/core/matching.d.ts +69 -0
- package/dist/core/matching.js +101 -0
- package/dist/core/types.d.ts +379 -0
- package/dist/core/types.js +37 -0
- package/dist/core/verification-core.d.ts +120 -0
- package/dist/core/verification-core.js +442 -0
- package/dist/core/zk-core.d.ts +69 -0
- package/dist/core/zk-core.js +244 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +51 -0
- package/dist/react-native/adapters/faceEmbeddingProvider.d.ts +38 -0
- package/dist/react-native/adapters/faceEmbeddingProvider.js +45 -0
- package/dist/react-native/adapters/imageDataProvider.d.ts +53 -0
- package/dist/react-native/adapters/imageDataProvider.js +134 -0
- package/dist/react-native/adapters/livenessProvider.d.ts +133 -0
- package/dist/react-native/adapters/livenessProvider.js +150 -0
- package/dist/react-native/adapters/zkProofEngine-webview.d.ts +73 -0
- package/dist/react-native/adapters/zkProofEngine-webview.js +135 -0
- package/dist/react-native/bundledRuntimeAssets.d.ts +39 -0
- package/dist/react-native/bundledRuntimeAssets.js +44 -0
- package/dist/react-native/components/FacePoseGuidanceWebView.d.ts +30 -0
- package/dist/react-native/components/FacePoseGuidanceWebView.js +530 -0
- package/dist/react-native/components/LivenessWebView.d.ts +39 -0
- package/dist/react-native/components/LivenessWebView.js +386 -0
- package/dist/react-native/components/OnnxRuntimeWebView.d.ts +58 -0
- package/dist/react-native/components/OnnxRuntimeWebView.js +518 -0
- package/dist/react-native/components/ZkProofWebView.d.ts +59 -0
- package/dist/react-native/components/ZkProofWebView.js +297 -0
- package/dist/react-native/dependencies.d.ts +144 -0
- package/dist/react-native/dependencies.js +130 -0
- package/dist/react-native/hooks/useOnnxLoader.d.ts +37 -0
- package/dist/react-native/hooks/useOnnxLoader.js +74 -0
- package/dist/react-native/hooks/useWasmLoader.d.ts +30 -0
- package/dist/react-native/hooks/useWasmLoader.js +158 -0
- package/dist/react-native/index.d.ts +61 -0
- package/dist/react-native/index.js +144 -0
- package/dist/react-native/services/FaceRecognition.d.ts +92 -0
- package/dist/react-native/services/FaceRecognition.js +674 -0
- package/dist/react-native/ui/FaceZkVerificationFlow.d.ts +97 -0
- package/dist/react-native/ui/FaceZkVerificationFlow.js +477 -0
- package/dist/react-native/ui/ReferenceEnrollmentFlow.d.ts +72 -0
- package/dist/react-native/ui/ReferenceEnrollmentFlow.js +369 -0
- package/dist/react-native/utils/faceAlignment.d.ts +37 -0
- package/dist/react-native/utils/faceAlignment.js +186 -0
- package/dist/react-native/utils/modelInitialisationChecks.d.ts +36 -0
- package/dist/react-native/utils/modelInitialisationChecks.js +128 -0
- package/dist/react-native/utils/resolveModelUri.d.ts +55 -0
- package/dist/react-native/utils/resolveModelUri.js +211 -0
- package/dist/react-native/utils/resolveRuntimeAsset.d.ts +25 -0
- package/dist/react-native/utils/resolveRuntimeAsset.js +94 -0
- package/dist/react-native/utils/resolveUiConfig.d.ts +41 -0
- package/dist/react-native/utils/resolveUiConfig.js +81 -0
- package/dist/storage/defaultStorageAdapter.d.ts +44 -0
- package/dist/storage/defaultStorageAdapter.js +344 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/face-zk.config.example.js +10 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Face+ZK SDK
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@jupitermetalabs/face-zk-sdk)
|
|
4
|
+
|
|
3
5
|
A standalone React Native and Web SDK for face verification and Zero-Knowledge (ZK) proofs.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -19,7 +21,6 @@ To keep documentation clean, each major module has its own dedicated documentati
|
|
|
19
21
|
- `storage/`: Built-in storage adapters for persisting enrolled references and proofs.
|
|
20
22
|
- `assets/`: ONNX models and WebView-based liveness/ZK scripts.
|
|
21
23
|
- `example/`: A complete Expo app demonstrating how to integrate the SDK.
|
|
22
|
-
- `example/`: A complete Expo app demonstrating how to integrate the SDK.
|
|
23
24
|
|
|
24
25
|
## Getting Started
|
|
25
26
|
|
|
@@ -179,5 +180,49 @@ Verify a live user against a saved reference.
|
|
|
179
180
|
/>
|
|
180
181
|
```
|
|
181
182
|
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### 6. Gender & Age Estimation (Optional)
|
|
186
|
+
|
|
187
|
+
Enable by passing the `ageGender` model in `initializeSdk`. Results are returned automatically alongside every verification and enrollment.
|
|
188
|
+
|
|
189
|
+
**Enable the model:**
|
|
190
|
+
```typescript
|
|
191
|
+
await initializeSdk({
|
|
192
|
+
models: {
|
|
193
|
+
detection: { module: require('./assets/models/det_500m.onnx') },
|
|
194
|
+
recognition: { module: require('./assets/models/w600k_mbf.onnx') },
|
|
195
|
+
antispoof: { module: require('./assets/models/antispoof.onnx') },
|
|
196
|
+
ageGender: { module: require('./assets/models/genderage.onnx') }, // add this
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Read results from verification:**
|
|
202
|
+
```typescript
|
|
203
|
+
<FaceZkVerificationFlow
|
|
204
|
+
onComplete={(outcome) => {
|
|
205
|
+
// outcome.liveCapture is populated after a successful live capture
|
|
206
|
+
console.log(outcome.liveCapture?.gender); // "Male" | "Female" | "Unknown"
|
|
207
|
+
console.log(outcome.liveCapture?.age); // e.g. 28
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Read results from enrollment:**
|
|
213
|
+
```typescript
|
|
214
|
+
<ReferenceEnrollmentFlow
|
|
215
|
+
onComplete={(template) => {
|
|
216
|
+
// gender/age stored in template metadata under sdkResponse
|
|
217
|
+
console.log(template.metadata?.sdkResponse?.gender);
|
|
218
|
+
console.log(template.metadata?.sdkResponse?.age);
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If `ageGender` is not configured, `gender` will be `undefined` and `age` will be `undefined` — the rest of the SDK functions normally.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
182
227
|
## Contributing
|
|
183
228
|
Please see the individual `README.md` files in each subdirectory for more technical details on the architecture.
|
|
@@ -20,6 +20,7 @@ let isGuidanceActive = false;
|
|
|
20
20
|
let stableStartTime = 0;
|
|
21
21
|
let currentStream = null;
|
|
22
22
|
let lastPose = null;
|
|
23
|
+
let lastLandmarks = null;
|
|
23
24
|
let guidanceStatus = { text: "", arrowLeft: false, arrowRight: false, isMatch: false };
|
|
24
25
|
let isRenderLoopActive = false;
|
|
25
26
|
|
|
@@ -60,13 +61,14 @@ function onResults(results) {
|
|
|
60
61
|
|
|
61
62
|
if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
|
|
62
63
|
const landmarks = results.multiFaceLandmarks[0];
|
|
64
|
+
lastLandmarks = landmarks;
|
|
63
65
|
const pose = calculatePose(landmarks);
|
|
64
66
|
const gaze = calculateGaze(landmarks);
|
|
65
67
|
const faceWidth = Math.hypot(
|
|
66
68
|
landmarks[454].x - landmarks[234].x,
|
|
67
69
|
landmarks[454].y - landmarks[234].y
|
|
68
70
|
);
|
|
69
|
-
|
|
71
|
+
|
|
70
72
|
lastPose = { ...pose, gaze, faceWidth };
|
|
71
73
|
|
|
72
74
|
// --- LIVENESS CHECK ---
|
|
@@ -327,7 +329,7 @@ function captureSuccess() {
|
|
|
327
329
|
|
|
328
330
|
// Capture Image
|
|
329
331
|
const dataUrl = canvasElement.toDataURL('image/jpeg', 0.9);
|
|
330
|
-
sendMessage('success', { image: dataUrl, pose: TARGET_POSE });
|
|
332
|
+
sendMessage('success', { image: dataUrl, pose: lastPose || TARGET_POSE, faceMeshLandmarks: lastLandmarks || undefined });
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
|
|
@@ -80,6 +80,7 @@ let lastValidationTime = 0;
|
|
|
80
80
|
|
|
81
81
|
let captureStableFrames = 0; // For near-field stability check
|
|
82
82
|
let readyToCapture = false;
|
|
83
|
+
let lastLandmarks = null;
|
|
83
84
|
|
|
84
85
|
const SPOOF_THRESHOLD_FINAL = 0.65; // Per-sample fail threshold (only flag high-confidence spoofs)
|
|
85
86
|
const SPOOF_EMA_FAIL_THRESHOLD = 0.60; // EMA-based final decision threshold
|
|
@@ -434,6 +435,7 @@ function onResults(results) {
|
|
|
434
435
|
}
|
|
435
436
|
|
|
436
437
|
const landmarks = results.multiFaceLandmarks[0];
|
|
438
|
+
lastLandmarks = landmarks;
|
|
437
439
|
|
|
438
440
|
// Calculate face width for distance checks
|
|
439
441
|
const faceWidth = Math.hypot(
|
|
@@ -788,6 +790,7 @@ function captureEvidence() {
|
|
|
788
790
|
image: dataUrl,
|
|
789
791
|
metadata: {
|
|
790
792
|
spoofScore: spoofVerdict.averageScore,
|
|
793
|
+
faceMeshLandmarks: lastLandmarks || undefined,
|
|
791
794
|
},
|
|
792
795
|
}),
|
|
793
796
|
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 JupiterMeta Labs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*
|
|
16
|
+
* Face+ZK SDK – Main SDK Class
|
|
17
|
+
*
|
|
18
|
+
* `FaceZkSdk` is the single entry point for initializing the SDK.
|
|
19
|
+
* Call `FaceZkSdk.init(config)` once at app startup before using any SDK features.
|
|
20
|
+
*
|
|
21
|
+
* @example – models bundled in the app (user copies assets via `npx face-zk setup --bundle`)
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { FaceZkSdk } from '@jupitermetalabs/face-zk-sdk';
|
|
24
|
+
*
|
|
25
|
+
* await FaceZkSdk.init({
|
|
26
|
+
* models: {
|
|
27
|
+
* detection: { module: require('./assets/face-zk/det_500m.onnx') },
|
|
28
|
+
* recognition: { module: require('./assets/face-zk/w600k_mbf.onnx') },
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example – models downloaded from CDN on first use
|
|
34
|
+
* ```ts
|
|
35
|
+
* await FaceZkSdk.init({
|
|
36
|
+
* models: {
|
|
37
|
+
* detection: { url: 'https://cdn.example.com/face-zk/det_500m.onnx' },
|
|
38
|
+
* recognition: { url: 'https://cdn.example.com/face-zk/w600k_mbf.onnx' },
|
|
39
|
+
* },
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
import type { FaceZkConfig } from "./config/types";
|
|
44
|
+
type SdkInitState = "uninitialized" | "initializing" | "ready" | "error";
|
|
45
|
+
export declare class FaceZkSdk {
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the SDK with model sources and optional feature flags.
|
|
48
|
+
* Must be called once before using any SDK features (enrollment, verification, ZK proofs).
|
|
49
|
+
*
|
|
50
|
+
* Safe to await at app startup (in App.tsx, before rendering the root navigator).
|
|
51
|
+
*/
|
|
52
|
+
static init(config: FaceZkConfig): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the resolved SDK config.
|
|
55
|
+
* Throws if `init()` hasn't been called yet.
|
|
56
|
+
*/
|
|
57
|
+
static getConfig(): FaceZkConfig;
|
|
58
|
+
/** Returns true if `init()` has completed successfully. */
|
|
59
|
+
static isInitialized(): boolean;
|
|
60
|
+
/** Returns the current init state: 'uninitialized' | 'initializing' | 'ready' | 'error' */
|
|
61
|
+
static getState(): SdkInitState;
|
|
62
|
+
/** Returns the error from the last failed `init()` call, if any. */
|
|
63
|
+
static getInitError(): Error | null;
|
|
64
|
+
/**
|
|
65
|
+
* Reset SDK state. Useful for tests or re-initialization flows.
|
|
66
|
+
*/
|
|
67
|
+
static reset(): void;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 JupiterMeta Labs
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*
|
|
17
|
+
* Face+ZK SDK – Main SDK Class
|
|
18
|
+
*
|
|
19
|
+
* `FaceZkSdk` is the single entry point for initializing the SDK.
|
|
20
|
+
* Call `FaceZkSdk.init(config)` once at app startup before using any SDK features.
|
|
21
|
+
*
|
|
22
|
+
* @example – models bundled in the app (user copies assets via `npx face-zk setup --bundle`)
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { FaceZkSdk } from '@jupitermetalabs/face-zk-sdk';
|
|
25
|
+
*
|
|
26
|
+
* await FaceZkSdk.init({
|
|
27
|
+
* models: {
|
|
28
|
+
* detection: { module: require('./assets/face-zk/det_500m.onnx') },
|
|
29
|
+
* recognition: { module: require('./assets/face-zk/w600k_mbf.onnx') },
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example – models downloaded from CDN on first use
|
|
35
|
+
* ```ts
|
|
36
|
+
* await FaceZkSdk.init({
|
|
37
|
+
* models: {
|
|
38
|
+
* detection: { url: 'https://cdn.example.com/face-zk/det_500m.onnx' },
|
|
39
|
+
* recognition: { url: 'https://cdn.example.com/face-zk/w600k_mbf.onnx' },
|
|
40
|
+
* },
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.FaceZkSdk = void 0;
|
|
46
|
+
// Module-level state (singleton)
|
|
47
|
+
let _state = "uninitialized";
|
|
48
|
+
let _config = null;
|
|
49
|
+
let _initError = null;
|
|
50
|
+
class FaceZkSdk {
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the SDK with model sources and optional feature flags.
|
|
53
|
+
* Must be called once before using any SDK features (enrollment, verification, ZK proofs).
|
|
54
|
+
*
|
|
55
|
+
* Safe to await at app startup (in App.tsx, before rendering the root navigator).
|
|
56
|
+
*/
|
|
57
|
+
static async init(config) {
|
|
58
|
+
if (_state === "initializing") {
|
|
59
|
+
throw new Error("[FaceZkSdk] Already initializing. Avoid calling init() concurrently.");
|
|
60
|
+
}
|
|
61
|
+
if (_state === "ready") {
|
|
62
|
+
throw new Error("[FaceZkSdk] Already initialized. Call FaceZkSdk.reset() before re-initializing.");
|
|
63
|
+
}
|
|
64
|
+
_state = "initializing";
|
|
65
|
+
_initError = null;
|
|
66
|
+
try {
|
|
67
|
+
validateConfig(config);
|
|
68
|
+
_config = config;
|
|
69
|
+
_state = "ready";
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
_state = "error";
|
|
73
|
+
_initError = err instanceof Error ? err : new Error(String(err));
|
|
74
|
+
throw _initError;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns the resolved SDK config.
|
|
79
|
+
* Throws if `init()` hasn't been called yet.
|
|
80
|
+
*/
|
|
81
|
+
static getConfig() {
|
|
82
|
+
if (_state !== "ready" || !_config) {
|
|
83
|
+
throw new Error("[FaceZkSdk] Not initialized. Call FaceZkSdk.init(config) before using SDK features.");
|
|
84
|
+
}
|
|
85
|
+
return _config;
|
|
86
|
+
}
|
|
87
|
+
/** Returns true if `init()` has completed successfully. */
|
|
88
|
+
static isInitialized() {
|
|
89
|
+
return _state === "ready";
|
|
90
|
+
}
|
|
91
|
+
/** Returns the current init state: 'uninitialized' | 'initializing' | 'ready' | 'error' */
|
|
92
|
+
static getState() {
|
|
93
|
+
return _state;
|
|
94
|
+
}
|
|
95
|
+
/** Returns the error from the last failed `init()` call, if any. */
|
|
96
|
+
static getInitError() {
|
|
97
|
+
return _initError;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reset SDK state. Useful for tests or re-initialization flows.
|
|
101
|
+
*/
|
|
102
|
+
static reset() {
|
|
103
|
+
_state = "uninitialized";
|
|
104
|
+
_config = null;
|
|
105
|
+
_initError = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.FaceZkSdk = FaceZkSdk;
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Internal validation
|
|
111
|
+
// ============================================================================
|
|
112
|
+
function validateConfig(config) {
|
|
113
|
+
if (!config || typeof config !== "object") {
|
|
114
|
+
throw new Error("[FaceZkSdk] config must be an object.");
|
|
115
|
+
}
|
|
116
|
+
if (!config.models || typeof config.models !== "object") {
|
|
117
|
+
throw new Error("[FaceZkSdk] config.models is required.");
|
|
118
|
+
}
|
|
119
|
+
if (!config.models.detection) {
|
|
120
|
+
throw new Error("[FaceZkSdk] config.models.detection is required. " +
|
|
121
|
+
"Provide a { module }, { url }, or { localUri }.");
|
|
122
|
+
}
|
|
123
|
+
if (!config.models.recognition) {
|
|
124
|
+
throw new Error("[FaceZkSdk] config.models.recognition is required. " +
|
|
125
|
+
"Provide a { module }, { url }, or { localUri }.");
|
|
126
|
+
}
|
|
127
|
+
// Each provided ModelSource must have at least one resolvable field
|
|
128
|
+
const modelEntries = Object.entries(config.models);
|
|
129
|
+
for (const [key, source] of modelEntries) {
|
|
130
|
+
if (!source)
|
|
131
|
+
continue;
|
|
132
|
+
if (!source.module && !source.url && !source.localUri) {
|
|
133
|
+
throw new Error(`[FaceZkSdk] config.models.${key} must have at least one of: module, url, localUri.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -20,6 +20,7 @@ let isGuidanceActive = false;
|
|
|
20
20
|
let stableStartTime = 0;
|
|
21
21
|
let currentStream = null;
|
|
22
22
|
let lastPose = null;
|
|
23
|
+
let lastLandmarks = null;
|
|
23
24
|
let guidanceStatus = { text: "", arrowLeft: false, arrowRight: false, isMatch: false };
|
|
24
25
|
let isRenderLoopActive = false;
|
|
25
26
|
|
|
@@ -60,13 +61,14 @@ function onResults(results) {
|
|
|
60
61
|
|
|
61
62
|
if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
|
|
62
63
|
const landmarks = results.multiFaceLandmarks[0];
|
|
64
|
+
lastLandmarks = landmarks;
|
|
63
65
|
const pose = calculatePose(landmarks);
|
|
64
66
|
const gaze = calculateGaze(landmarks);
|
|
65
67
|
const faceWidth = Math.hypot(
|
|
66
68
|
landmarks[454].x - landmarks[234].x,
|
|
67
69
|
landmarks[454].y - landmarks[234].y
|
|
68
70
|
);
|
|
69
|
-
|
|
71
|
+
|
|
70
72
|
lastPose = { ...pose, gaze, faceWidth };
|
|
71
73
|
|
|
72
74
|
// --- LIVENESS CHECK ---
|
|
@@ -327,7 +329,7 @@ function captureSuccess() {
|
|
|
327
329
|
|
|
328
330
|
// Capture Image
|
|
329
331
|
const dataUrl = canvasElement.toDataURL('image/jpeg', 0.9);
|
|
330
|
-
sendMessage('success', { image: dataUrl, pose: TARGET_POSE });
|
|
332
|
+
sendMessage('success', { image: dataUrl, pose: lastPose || TARGET_POSE, faceMeshLandmarks: lastLandmarks || undefined });
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
|
|
@@ -80,6 +80,7 @@ let lastValidationTime = 0;
|
|
|
80
80
|
|
|
81
81
|
let captureStableFrames = 0; // For near-field stability check
|
|
82
82
|
let readyToCapture = false;
|
|
83
|
+
let lastLandmarks = null;
|
|
83
84
|
|
|
84
85
|
const SPOOF_THRESHOLD_FINAL = 0.65; // Per-sample fail threshold (only flag high-confidence spoofs)
|
|
85
86
|
const SPOOF_EMA_FAIL_THRESHOLD = 0.60; // EMA-based final decision threshold
|
|
@@ -434,6 +435,7 @@ function onResults(results) {
|
|
|
434
435
|
}
|
|
435
436
|
|
|
436
437
|
const landmarks = results.multiFaceLandmarks[0];
|
|
438
|
+
lastLandmarks = landmarks;
|
|
437
439
|
|
|
438
440
|
// Calculate face width for distance checks
|
|
439
441
|
const faceWidth = Math.hypot(
|
|
@@ -788,6 +790,7 @@ function captureEvidence() {
|
|
|
788
790
|
image: dataUrl,
|
|
789
791
|
metadata: {
|
|
790
792
|
spoofScore: spoofVerdict.averageScore,
|
|
793
|
+
faceMeshLandmarks: lastLandmarks || undefined,
|
|
791
794
|
},
|
|
792
795
|
}),
|
|
793
796
|
);
|