@jupitermetalabs/face-zk-sdk 0.3.1 → 0.3.3
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/assets/face-guidance/pose-guidance.js.txt +1 -1
- package/cli/copy-dist-assets.js +59 -14
- package/dist/assets/face-guidance/face-logic.js.txt +77 -0
- package/dist/assets/face-guidance/index.html +173 -0
- package/dist/assets/face-guidance/pose-guidance.js.txt +404 -0
- package/dist/assets/liveness/antispoof.js.txt +143 -0
- package/dist/assets/liveness/index.html +451 -0
- package/dist/assets/liveness/liveness.js.txt +1003 -0
- package/dist/assets/mediapipe/face_mesh.js.txt +131 -0
- package/dist/assets/mediapipe/face_mesh_solution_packed_assets.data +0 -0
- package/dist/assets/mediapipe/face_mesh_solution_simd_wasm_bin.wasm +0 -0
- package/dist/assets/mediapipe/face_mesh_solution_wasm_bin.wasm +0 -0
- package/dist/assets/onnx/ort-wasm-simd.wasm +0 -0
- package/dist/assets/onnx/ort-wasm.wasm +0 -0
- package/dist/assets/onnx/ort.min.js.txt +7 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -2
|
@@ -9,7 +9,7 @@ const arrowRight = document.getElementById('arrow_right');
|
|
|
9
9
|
|
|
10
10
|
// Configuration
|
|
11
11
|
let TARGET_POSE = { yaw: 0, pitch: 0, roll: 0 };
|
|
12
|
-
let REQUIRED_STABLE_DURATION =
|
|
12
|
+
let REQUIRED_STABLE_DURATION = 1000; // 1 second
|
|
13
13
|
let YAW_THRESHOLD = 6; // degrees
|
|
14
14
|
let PITCH_THRESHOLD = 10; // degrees
|
|
15
15
|
let GAZE_THRESHOLD = 0.3; // Deviation from center (0.5)
|
package/cli/copy-dist-assets.js
CHANGED
|
@@ -17,18 +17,17 @@
|
|
|
17
17
|
"use strict";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Copies bundled
|
|
20
|
+
* Copies all bundled runtime assets into dist/assets/ after tsc compilation.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Why this is needed:
|
|
23
|
+
* bundledRuntimeAssets.ts lives at react-native/bundledRuntimeAssets.ts and
|
|
24
|
+
* uses require('../assets/...') paths. After tsc, the compiled file lands at
|
|
25
|
+
* dist/react-native/bundledRuntimeAssets.js — so '../assets/...' resolves to
|
|
26
|
+
* dist/assets/... at Metro bundle time. This script ensures all those files
|
|
27
|
+
* exist under dist/assets/.
|
|
25
28
|
*
|
|
26
29
|
* Runs automatically as part of the "build" npm script.
|
|
27
30
|
* Safe to run multiple times (idempotent).
|
|
28
|
-
*
|
|
29
|
-
* Files copied:
|
|
30
|
-
* assets/wasm/zk_face_wasm_bg.wasm → dist/assets/wasm/zk_face_wasm_bg.wasm
|
|
31
|
-
* assets/zk-worker.html → dist/assets/zk-worker.html
|
|
32
31
|
*/
|
|
33
32
|
|
|
34
33
|
const fs = require("fs");
|
|
@@ -37,23 +36,69 @@ const path = require("path");
|
|
|
37
36
|
const ROOT = path.resolve(__dirname, "..");
|
|
38
37
|
const DIST_ASSETS = path.resolve(ROOT, "dist/assets");
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// Individual file copies: [src relative to ROOT, dst relative to DIST_ASSETS]
|
|
40
|
+
const FILE_COPIES = [
|
|
41
|
+
// ZK WASM
|
|
42
|
+
{ src: "assets/wasm/zk_face_wasm_bg.wasm", dst: "wasm/zk_face_wasm_bg.wasm" },
|
|
43
|
+
{ src: "assets/zk-worker.html", dst: "zk-worker.html" },
|
|
44
|
+
// ORT Runtime
|
|
45
|
+
{ src: "assets/onnx/ort.min.js.txt", dst: "onnx/ort.min.js.txt" },
|
|
46
|
+
{ src: "assets/onnx/ort-wasm-simd.wasm", dst: "onnx/ort-wasm-simd.wasm" },
|
|
47
|
+
{ src: "assets/onnx/ort-wasm.wasm", dst: "onnx/ort-wasm.wasm" },
|
|
48
|
+
// Liveness
|
|
49
|
+
{ src: "assets/liveness/index.html", dst: "liveness/index.html" },
|
|
50
|
+
{ src: "assets/liveness/antispoof.js.txt", dst: "liveness/antispoof.js.txt" },
|
|
51
|
+
{ src: "assets/liveness/liveness.js.txt", dst: "liveness/liveness.js.txt" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Directory copies: entire source directory mirrored under DIST_ASSETS
|
|
55
|
+
const DIR_COPIES = [
|
|
56
|
+
{ src: "assets/mediapipe", dst: "mediapipe" },
|
|
57
|
+
{ src: "assets/face-guidance", dst: "face-guidance" },
|
|
43
58
|
];
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
46
61
|
|
|
47
|
-
|
|
62
|
+
function copyFile(src, dst) {
|
|
48
63
|
const srcPath = path.join(ROOT, src);
|
|
49
64
|
const dstPath = path.join(DIST_ASSETS, dst);
|
|
50
65
|
|
|
51
66
|
if (!fs.existsSync(srcPath)) {
|
|
52
67
|
process.stderr.write(`[copy-dist-assets] WARNING: ${src} not found — skipping\n`);
|
|
53
|
-
|
|
68
|
+
return;
|
|
54
69
|
}
|
|
55
70
|
|
|
71
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
56
72
|
fs.copyFileSync(srcPath, dstPath);
|
|
57
73
|
const size = Math.round(fs.statSync(dstPath).size / 1024);
|
|
58
74
|
process.stdout.write(`[copy-dist-assets] copied ${src} → dist/assets/${dst} (${size} KB)\n`);
|
|
59
75
|
}
|
|
76
|
+
|
|
77
|
+
function copyDir(srcRel, dstRel) {
|
|
78
|
+
const srcPath = path.join(ROOT, srcRel);
|
|
79
|
+
const dstPath = path.join(DIST_ASSETS, dstRel);
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(srcPath)) {
|
|
82
|
+
process.stderr.write(`[copy-dist-assets] WARNING: ${srcRel}/ not found — skipping\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fs.mkdirSync(dstPath, { recursive: true });
|
|
87
|
+
|
|
88
|
+
for (const entry of fs.readdirSync(srcPath, { withFileTypes: true })) {
|
|
89
|
+
if (entry.isFile()) {
|
|
90
|
+
copyFile(path.join(srcRel, entry.name), path.join(dstRel, entry.name));
|
|
91
|
+
}
|
|
92
|
+
// Not recursing into subdirectories — all current asset dirs are flat
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
for (const { src, dst } of FILE_COPIES) {
|
|
99
|
+
copyFile(src, dst);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const { src, dst } of DIR_COPIES) {
|
|
103
|
+
copyDir(src, dst);
|
|
104
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Face Pose Calculation Logic */
|
|
2
|
+
|
|
3
|
+
// --- POSE CALCULATION ---
|
|
4
|
+
// Distance-invariant yaw/pitch calculation with face dimension normalization
|
|
5
|
+
// This matches the calculation in liveness.js for consistent TARGET and CURRENT pose comparison
|
|
6
|
+
function calculatePose(landmarks) {
|
|
7
|
+
const nose = landmarks[1];
|
|
8
|
+
const leftCheek = landmarks[234];
|
|
9
|
+
const rightCheek = landmarks[454];
|
|
10
|
+
|
|
11
|
+
// Face Width for normalization (makes calculation distance-invariant)
|
|
12
|
+
const faceWidth = Math.hypot(
|
|
13
|
+
rightCheek.x - leftCheek.x,
|
|
14
|
+
rightCheek.y - leftCheek.y
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Yaw: Rotation around Y axis (Turning Left/Right)
|
|
18
|
+
const midPointX = (leftCheek.x + rightCheek.x) / 2;
|
|
19
|
+
// Normalize by face width to get comparable angle regardless of distance
|
|
20
|
+
// Multiplier 180 is heuristic to match standard degrees
|
|
21
|
+
const yaw = ((nose.x - midPointX) / faceWidth) * 180;
|
|
22
|
+
|
|
23
|
+
// Pitch: Rotation around X axis (Looking Up/Down)
|
|
24
|
+
const midEyeY = (landmarks[33].y + landmarks[263].y) / 2;
|
|
25
|
+
const mouthY = (landmarks[13].y + landmarks[14].y) / 2;
|
|
26
|
+
const midFaceY = (midEyeY + mouthY) / 2;
|
|
27
|
+
|
|
28
|
+
// Face Height for normalization (chin to forehead)
|
|
29
|
+
const chin = landmarks[152];
|
|
30
|
+
const forehead = landmarks[10];
|
|
31
|
+
const faceHeight = Math.hypot(
|
|
32
|
+
chin.x - forehead.x,
|
|
33
|
+
chin.y - forehead.y
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const pitch = ((nose.y - midFaceY) / faceHeight) * 180;
|
|
37
|
+
|
|
38
|
+
return { yaw, pitch, roll: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function calculateGaze(landmarks) {
|
|
42
|
+
if (!landmarks[468] || !landmarks[473]) return { x: 0.5, y: 0.5 }; // Default centered if no iris
|
|
43
|
+
|
|
44
|
+
// Left Eye (User's Left = Right on screen if mirrored, but landmarks are consistent)
|
|
45
|
+
// Left Eye Inner: 362, Outer: 263, Iris: 473
|
|
46
|
+
const rightEyeInner = landmarks[362];
|
|
47
|
+
const rightEyeOuter = landmarks[263];
|
|
48
|
+
const rightIris = landmarks[473];
|
|
49
|
+
|
|
50
|
+
// Right Eye (User's Right)
|
|
51
|
+
// Right Eye Inner: 33, Outer: 133, Iris: 468
|
|
52
|
+
const leftEyeInner = landmarks[33];
|
|
53
|
+
const leftEyeOuter = landmarks[133];
|
|
54
|
+
const leftIris = landmarks[468]; // 468 is Left Iris Center
|
|
55
|
+
|
|
56
|
+
// Calculate Horizontal Ratio (0 = Looking Left, 1 = Looking Right, 0.5 = Center)
|
|
57
|
+
|
|
58
|
+
// Left Eye horizontal distance
|
|
59
|
+
const leftEyeWidth = Math.hypot(leftEyeOuter.x - leftEyeInner.x, leftEyeOuter.y - leftEyeInner.y);
|
|
60
|
+
const leftIrisDist = Math.hypot(leftIris.x - leftEyeInner.x, leftIris.y - leftEyeInner.y);
|
|
61
|
+
const leftRatio = leftIrisDist / leftEyeWidth;
|
|
62
|
+
|
|
63
|
+
// Right Eye horizontal distance
|
|
64
|
+
const rightEyeWidth = Math.hypot(rightEyeOuter.x - rightEyeInner.x, rightEyeOuter.y - rightEyeInner.y);
|
|
65
|
+
const rightIrisDist = Math.hypot(rightIris.x - rightEyeInner.x, rightIris.y - rightEyeInner.y);
|
|
66
|
+
const rightRatio = rightIrisDist / rightEyeWidth;
|
|
67
|
+
|
|
68
|
+
// Average ratio
|
|
69
|
+
const avgRatio = (leftRatio + rightRatio) / 2;
|
|
70
|
+
return { x: avgRatio, y: 0.5 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Export functions if using modules, but here we likely just inject into global scope
|
|
74
|
+
if (typeof window !== 'undefined') {
|
|
75
|
+
window.calculatePose = calculatePose;
|
|
76
|
+
window.calculateGaze = calculateGaze;
|
|
77
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
8
|
+
/>
|
|
9
|
+
<title>Face Pose Guidance</title>
|
|
10
|
+
|
|
11
|
+
<!-- Tailwind -->
|
|
12
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
13
|
+
|
|
14
|
+
<!-- MediaPipe -->
|
|
15
|
+
<script
|
|
16
|
+
src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"
|
|
17
|
+
crossorigin="anonymous"
|
|
18
|
+
></script>
|
|
19
|
+
<script
|
|
20
|
+
src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"
|
|
21
|
+
crossorigin="anonymous"
|
|
22
|
+
></script>
|
|
23
|
+
<script
|
|
24
|
+
src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"
|
|
25
|
+
crossorigin="anonymous"
|
|
26
|
+
></script>
|
|
27
|
+
<script
|
|
28
|
+
src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"
|
|
29
|
+
crossorigin="anonymous"
|
|
30
|
+
></script>
|
|
31
|
+
<!-- ONNX Runtime Web -->
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.14.0/dist/ort.min.js"></script>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.camera-wrapper {
|
|
36
|
+
position: relative;
|
|
37
|
+
width: 100vw;
|
|
38
|
+
height: 100vh;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
background: #000;
|
|
41
|
+
display: flex;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
align-items: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#input_video {
|
|
47
|
+
display: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#output_canvas {
|
|
51
|
+
max-width: 100%;
|
|
52
|
+
max-height: 100%;
|
|
53
|
+
object-fit: contain;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.mirror {
|
|
57
|
+
transform: scaleX(-1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.overlay {
|
|
61
|
+
position: absolute;
|
|
62
|
+
top: 0;
|
|
63
|
+
left: 0;
|
|
64
|
+
right: 0;
|
|
65
|
+
bottom: 0;
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
padding: 40px 20px;
|
|
71
|
+
z-index: 10;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.message-box {
|
|
75
|
+
background: rgba(0, 0, 0, 0.6);
|
|
76
|
+
backdrop-filter: blur(4px);
|
|
77
|
+
padding: 16px 24px;
|
|
78
|
+
border-radius: 12px;
|
|
79
|
+
text-align: center;
|
|
80
|
+
margin: 0 auto;
|
|
81
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.guidance-arrow {
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: 50%;
|
|
87
|
+
font-size: 4rem;
|
|
88
|
+
color: white;
|
|
89
|
+
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
|
|
90
|
+
opacity: 0;
|
|
91
|
+
transition: opacity 0.3s;
|
|
92
|
+
transform: translateY(-50%);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.arrow-left {
|
|
96
|
+
left: 20px;
|
|
97
|
+
animation: bounce-left 1s infinite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.arrow-right {
|
|
101
|
+
right: 20px;
|
|
102
|
+
animation: bounce-right 1s infinite;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@keyframes bounce-left {
|
|
106
|
+
0%,
|
|
107
|
+
100% {
|
|
108
|
+
transform: translate(0, -50%);
|
|
109
|
+
}
|
|
110
|
+
50% {
|
|
111
|
+
transform: translate(-10px, -50%);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@keyframes bounce-right {
|
|
116
|
+
0%,
|
|
117
|
+
100% {
|
|
118
|
+
transform: translate(0, -50%);
|
|
119
|
+
}
|
|
120
|
+
50% {
|
|
121
|
+
transform: translate(10px, -50%);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body class="bg-black m-0 overflow-hidden">
|
|
127
|
+
<div class="camera-wrapper">
|
|
128
|
+
<video id="input_video" playsinline></video>
|
|
129
|
+
<canvas id="output_canvas"></canvas>
|
|
130
|
+
|
|
131
|
+
<div class="overlay">
|
|
132
|
+
<!-- Top Status -->
|
|
133
|
+
<div id="status_box" class="message-box">
|
|
134
|
+
<p id="status_text" class="text-white text-lg font-medium">
|
|
135
|
+
Initializing...
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Guidance Elements -->
|
|
140
|
+
<div id="arrow_left" class="guidance-arrow arrow-left">⬅️</div>
|
|
141
|
+
<div id="arrow_right" class="guidance-arrow arrow-right">➡️</div>
|
|
142
|
+
|
|
143
|
+
<!-- Bottom Status -->
|
|
144
|
+
<div id="instruction_box" class="message-box hidden">
|
|
145
|
+
<p id="instruction_text" class="text-slate-300 text-sm">
|
|
146
|
+
Align your face
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div
|
|
153
|
+
id="debug_info"
|
|
154
|
+
style="
|
|
155
|
+
position: absolute;
|
|
156
|
+
top: 10px;
|
|
157
|
+
left: 10px;
|
|
158
|
+
background: rgba(0, 0, 0, 0.7);
|
|
159
|
+
color: lime;
|
|
160
|
+
font-family: monospace;
|
|
161
|
+
font-size: 12px;
|
|
162
|
+
padding: 8px;
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
pointer-events: none;
|
|
165
|
+
z-index: 100;
|
|
166
|
+
white-space: pre;
|
|
167
|
+
"
|
|
168
|
+
></div>
|
|
169
|
+
|
|
170
|
+
<!-- Main Logic Script -->
|
|
171
|
+
<script src="pose-guidance.js"></script>
|
|
172
|
+
</body>
|
|
173
|
+
</html>
|