@iam-protocol/pulse-sdk 0.1.0 → 0.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.
- package/dist/index.d.mts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +67 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +2 -1
- package/src/extraction/kinematic.ts +28 -5
- package/src/extraction/mfcc.ts +11 -4
- package/src/extraction/statistics.ts +46 -0
- package/src/index.ts +2 -2
- package/src/proof/prover.ts +4 -2
- package/src/proof/types.ts +1 -0
- package/src/pulse.ts +1 -1
- package/test/integration.test.ts +3 -2
- package/test/serializer.test.ts +1 -0
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -10,7 +10,8 @@ export const BN254_SCALAR_FIELD = BigInt(
|
|
|
10
10
|
|
|
11
11
|
export const FINGERPRINT_BITS = 256;
|
|
12
12
|
export const DEFAULT_THRESHOLD = 30;
|
|
13
|
-
export const
|
|
13
|
+
export const DEFAULT_MIN_DISTANCE = 3;
|
|
14
|
+
export const NUM_PUBLIC_INPUTS = 4;
|
|
14
15
|
|
|
15
16
|
export const PROOF_A_SIZE = 64;
|
|
16
17
|
export const PROOF_B_SIZE = 128;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { MotionSample, TouchSample } from "../sensor/types";
|
|
2
|
-
import { condense } from "./statistics";
|
|
2
|
+
import { condense, variance } from "./statistics";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Extract kinematic features from motion (IMU) data.
|
|
6
6
|
* Computes jerk (3rd derivative) and jounce (4th derivative) of acceleration,
|
|
7
7
|
* then condenses each axis into statistics.
|
|
8
8
|
*
|
|
9
|
-
* Returns: ~
|
|
9
|
+
* Returns: ~54 values (6 axes × 2 derivatives × 4 stats + 6 jitter variance values)
|
|
10
10
|
*/
|
|
11
11
|
export function extractMotionFeatures(samples: MotionSample[]): number[] {
|
|
12
|
-
if (samples.length < 5) return new Array(
|
|
12
|
+
if (samples.length < 5) return new Array(54).fill(0);
|
|
13
13
|
|
|
14
14
|
// Extract acceleration and rotation time series
|
|
15
15
|
const axes = {
|
|
@@ -44,6 +44,19 @@ export function extractMotionFeatures(samples: MotionSample[]): number[] {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Jitter variance per axis: variance of windowed jerk variance.
|
|
48
|
+
// Real human tremor fluctuates over time (high jitter variance).
|
|
49
|
+
// Synthetic/replay data has constant jitter (low jitter variance).
|
|
50
|
+
for (const values of Object.values(axes)) {
|
|
51
|
+
const jerk = derivative(values);
|
|
52
|
+
const windowSize = Math.max(5, Math.floor(jerk.length / 4));
|
|
53
|
+
const windowVariances: number[] = [];
|
|
54
|
+
for (let i = 0; i <= jerk.length - windowSize; i += windowSize) {
|
|
55
|
+
windowVariances.push(variance(jerk.slice(i, i + windowSize)));
|
|
56
|
+
}
|
|
57
|
+
features.push(windowVariances.length >= 2 ? variance(windowVariances) : 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
return features;
|
|
48
61
|
}
|
|
49
62
|
|
|
@@ -52,10 +65,10 @@ export function extractMotionFeatures(samples: MotionSample[]): number[] {
|
|
|
52
65
|
* Computes velocity and acceleration of touch coordinates,
|
|
53
66
|
* plus pressure and area statistics.
|
|
54
67
|
*
|
|
55
|
-
* Returns: ~32
|
|
68
|
+
* Returns: ~36 values (32 base + 4 jitter variance for x, y, pressure, area)
|
|
56
69
|
*/
|
|
57
70
|
export function extractTouchFeatures(samples: TouchSample[]): number[] {
|
|
58
|
-
if (samples.length < 5) return new Array(
|
|
71
|
+
if (samples.length < 5) return new Array(36).fill(0);
|
|
59
72
|
|
|
60
73
|
const x = samples.map((s) => s.x);
|
|
61
74
|
const y = samples.map((s) => s.y);
|
|
@@ -88,6 +101,16 @@ export function extractTouchFeatures(samples: TouchSample[]): number[] {
|
|
|
88
101
|
features.push(...Object.values(condense(jerkX)));
|
|
89
102
|
features.push(...Object.values(condense(jerkY)));
|
|
90
103
|
|
|
104
|
+
// Jitter variance for touch signals: detects synthetic smoothness
|
|
105
|
+
for (const values of [vx, vy, pressure, area]) {
|
|
106
|
+
const windowSize = Math.max(5, Math.floor(values.length / 4));
|
|
107
|
+
const windowVariances: number[] = [];
|
|
108
|
+
for (let i = 0; i <= values.length - windowSize; i += windowSize) {
|
|
109
|
+
windowVariances.push(variance(values.slice(i, i + windowSize)));
|
|
110
|
+
}
|
|
111
|
+
features.push(windowVariances.length >= 2 ? variance(windowVariances) : 0);
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
return features;
|
|
92
115
|
}
|
|
93
116
|
|
package/src/extraction/mfcc.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AudioCapture } from "../sensor/types";
|
|
2
|
-
import { condense } from "./statistics";
|
|
2
|
+
import { condense, entropy } from "./statistics";
|
|
3
3
|
|
|
4
4
|
// Frame parameters matching the research paper spec
|
|
5
5
|
const FRAME_SIZE = 400; // 25ms at 16kHz
|
|
@@ -11,7 +11,7 @@ const NUM_MFCC = 13;
|
|
|
11
11
|
* Computes 13 MFCCs per frame, plus delta and delta-delta coefficients,
|
|
12
12
|
* then condenses each coefficient's time series into 4 statistics.
|
|
13
13
|
*
|
|
14
|
-
* Returns: 13 coefficients × 3 (raw + delta + delta-delta) × 4 stats =
|
|
14
|
+
* Returns: 13 coefficients × 3 (raw + delta + delta-delta) × 4 stats + 13 entropy values = 169 values
|
|
15
15
|
*/
|
|
16
16
|
export function extractMFCC(audio: AudioCapture): number[] {
|
|
17
17
|
const { samples, sampleRate } = audio;
|
|
@@ -22,12 +22,12 @@ export function extractMFCC(audio: AudioCapture): number[] {
|
|
|
22
22
|
Meyda = require("meyda");
|
|
23
23
|
} catch {
|
|
24
24
|
// Meyda not available — return zeros (fallback for environments without it)
|
|
25
|
-
return new Array(NUM_MFCC * 3 * 4).fill(0);
|
|
25
|
+
return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// Extract MFCCs per frame
|
|
29
29
|
const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
|
|
30
|
-
if (numFrames < 3) return new Array(NUM_MFCC * 3 * 4).fill(0);
|
|
30
|
+
if (numFrames < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
|
|
31
31
|
|
|
32
32
|
const mfccFrames: number[][] = [];
|
|
33
33
|
|
|
@@ -78,6 +78,13 @@ export function extractMFCC(audio: AudioCapture): number[] {
|
|
|
78
78
|
features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Entropy per MFCC coefficient: measures information density across frames.
|
|
82
|
+
// Real speech has moderate, varied entropy. Synthetic audio is too uniform or too structured.
|
|
83
|
+
for (let c = 0; c < NUM_MFCC; c++) {
|
|
84
|
+
const raw = mfccFrames.map((f) => f[c] ?? 0);
|
|
85
|
+
features.push(entropy(raw));
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
return features;
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -50,6 +50,52 @@ export function condense(values: number[]): StatsSummary {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Shannon entropy over histogram bins. Measures information density.
|
|
55
|
+
* Real human data has moderate entropy (varied but structured).
|
|
56
|
+
* Synthetic data is either too uniform (high entropy) or too structured (low entropy).
|
|
57
|
+
*/
|
|
58
|
+
export function entropy(values: number[], bins: number = 16): number {
|
|
59
|
+
if (values.length < 2) return 0;
|
|
60
|
+
const min = Math.min(...values);
|
|
61
|
+
const max = Math.max(...values);
|
|
62
|
+
if (min === max) return 0;
|
|
63
|
+
|
|
64
|
+
const counts = new Array(bins).fill(0);
|
|
65
|
+
const range = max - min;
|
|
66
|
+
for (const v of values) {
|
|
67
|
+
const idx = Math.min(Math.floor(((v - min) / range) * bins), bins - 1);
|
|
68
|
+
counts[idx]++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let h = 0;
|
|
72
|
+
for (const c of counts) {
|
|
73
|
+
if (c > 0) {
|
|
74
|
+
const p = c / values.length;
|
|
75
|
+
h -= p * Math.log2(p);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return h;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Autocorrelation at a given lag. Detects periodic synthetic patterns.
|
|
83
|
+
* Real human data has low autocorrelation at most lags (chaotic/noisy).
|
|
84
|
+
* Synthetic data often has high autocorrelation (periodic/smooth).
|
|
85
|
+
*/
|
|
86
|
+
export function autocorrelation(values: number[], lag: number = 1): number {
|
|
87
|
+
if (values.length <= lag) return 0;
|
|
88
|
+
const m = mean(values);
|
|
89
|
+
const v = variance(values, m);
|
|
90
|
+
if (v === 0) return 0;
|
|
91
|
+
|
|
92
|
+
let sum = 0;
|
|
93
|
+
for (let i = 0; i < values.length - lag; i++) {
|
|
94
|
+
sum += (values[i]! - m) * (values[i + lag]! - m);
|
|
95
|
+
}
|
|
96
|
+
return sum / ((values.length - lag) * v);
|
|
97
|
+
}
|
|
98
|
+
|
|
53
99
|
export function fuseFeatures(
|
|
54
100
|
audio: number[],
|
|
55
101
|
motion: number[],
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { PulseSDK, PulseSession } from "./pulse";
|
|
|
3
3
|
|
|
4
4
|
// Configuration
|
|
5
5
|
export type { PulseConfig } from "./config";
|
|
6
|
-
export { PROGRAM_IDS, DEFAULT_THRESHOLD, FINGERPRINT_BITS, MIN_CAPTURE_MS, MAX_CAPTURE_MS, DEFAULT_CAPTURE_MS } from "./config";
|
|
6
|
+
export { PROGRAM_IDS, DEFAULT_THRESHOLD, DEFAULT_MIN_DISTANCE, FINGERPRINT_BITS, MIN_CAPTURE_MS, MAX_CAPTURE_MS, DEFAULT_CAPTURE_MS } from "./config";
|
|
7
7
|
|
|
8
8
|
// Hashing
|
|
9
9
|
export type { TemporalFingerprint, TBH, PackedFingerprint } from "./hashing/types";
|
|
@@ -18,7 +18,7 @@ export {
|
|
|
18
18
|
|
|
19
19
|
// Feature extraction
|
|
20
20
|
export type { StatsSummary, FeatureVector, FusedFeatureVector } from "./extraction/types";
|
|
21
|
-
export { mean, variance, skewness, kurtosis, condense, fuseFeatures } from "./extraction/statistics";
|
|
21
|
+
export { mean, variance, skewness, kurtosis, condense, entropy, autocorrelation, fuseFeatures } from "./extraction/statistics";
|
|
22
22
|
|
|
23
23
|
// Proof generation
|
|
24
24
|
export type { SolanaProof, CircuitInput, ProofResult } from "./proof/types";
|
package/src/proof/prover.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TBH } from "../hashing/types";
|
|
2
2
|
import type { CircuitInput, ProofResult, SolanaProof } from "./types";
|
|
3
3
|
import { serializeProof } from "./serializer";
|
|
4
|
-
import { DEFAULT_THRESHOLD } from "../config";
|
|
4
|
+
import { DEFAULT_THRESHOLD, DEFAULT_MIN_DISTANCE } from "../config";
|
|
5
5
|
|
|
6
6
|
// Use dynamic import for snarkjs (it's a CJS module)
|
|
7
7
|
let snarkjsModule: any = null;
|
|
@@ -19,7 +19,8 @@ async function getSnarkjs(): Promise<any> {
|
|
|
19
19
|
export function prepareCircuitInput(
|
|
20
20
|
current: TBH,
|
|
21
21
|
previous: TBH,
|
|
22
|
-
threshold: number = DEFAULT_THRESHOLD
|
|
22
|
+
threshold: number = DEFAULT_THRESHOLD,
|
|
23
|
+
minDistance: number = DEFAULT_MIN_DISTANCE
|
|
23
24
|
): CircuitInput {
|
|
24
25
|
return {
|
|
25
26
|
ft_new: current.fingerprint,
|
|
@@ -29,6 +30,7 @@ export function prepareCircuitInput(
|
|
|
29
30
|
commitment_new: current.commitment.toString(),
|
|
30
31
|
commitment_prev: previous.commitment.toString(),
|
|
31
32
|
threshold: threshold.toString(),
|
|
33
|
+
min_distance: minDistance.toString(),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
package/src/proof/types.ts
CHANGED
package/src/pulse.ts
CHANGED
|
@@ -35,7 +35,7 @@ type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> &
|
|
|
35
35
|
function extractFeatures(data: SensorData): number[] {
|
|
36
36
|
const audioFeatures = data.audio
|
|
37
37
|
? extractMFCC(data.audio)
|
|
38
|
-
: new Array(
|
|
38
|
+
: new Array(169).fill(0);
|
|
39
39
|
const motionFeatures = extractMotionFeatures(data.motion);
|
|
40
40
|
const touchFeatures = extractTouchFeatures(data.touch);
|
|
41
41
|
return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
|
package/test/integration.test.ts
CHANGED
|
@@ -29,8 +29,8 @@ describe.skipIf(!circuitArtifactsExist)(
|
|
|
29
29
|
"integration: full crypto pipeline",
|
|
30
30
|
() => {
|
|
31
31
|
it("generates a valid proof from mock features end-to-end", async () => {
|
|
32
|
-
// 1. Create mock feature vector (~
|
|
33
|
-
const features = Array.from({ length:
|
|
32
|
+
// 1. Create mock feature vector (~259 random values: 169 audio + 54 motion + 36 touch)
|
|
33
|
+
const features = Array.from({ length: 259 }, (_, i) =>
|
|
34
34
|
Math.sin(i * 0.3) * Math.cos(i * 0.7)
|
|
35
35
|
);
|
|
36
36
|
|
|
@@ -56,6 +56,7 @@ describe.skipIf(!circuitArtifactsExist)(
|
|
|
56
56
|
const input = prepareCircuitInput(tbhNew, tbhPrev, 30);
|
|
57
57
|
expect(input.ft_new.length).toBe(256);
|
|
58
58
|
expect(input.threshold).toBe("30");
|
|
59
|
+
expect(input.min_distance).toBe("3");
|
|
59
60
|
|
|
60
61
|
// 6. Generate Groth16 proof
|
|
61
62
|
const { proof, publicSignals } = await generateProof(
|