@iam-protocol/pulse-sdk 0.2.6 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iam-protocol/pulse-sdk",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Client-side SDK for IAM Protocol — sensor capture, TBH generation, ZK proof construction",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -21,6 +21,7 @@
21
21
  "dependencies": {
22
22
  "circomlibjs": "^0.1.7",
23
23
  "meyda": "^5.6.3",
24
+ "pitchfinder": "^2.3.4",
24
25
  "snarkjs": "^0.7.6"
25
26
  },
26
27
  "peerDependencies": {
package/src/config.ts CHANGED
@@ -9,7 +9,7 @@ export const BN254_SCALAR_FIELD = BigInt(
9
9
  );
10
10
 
11
11
  export const FINGERPRINT_BITS = 256;
12
- export const DEFAULT_THRESHOLD = 30;
12
+ export const DEFAULT_THRESHOLD = 96;
13
13
  export const DEFAULT_MIN_DISTANCE = 3;
14
14
  export const NUM_PUBLIC_INPUTS = 4;
15
15
 
@@ -1,5 +1,5 @@
1
1
  import type { MotionSample, TouchSample } from "../sensor/types";
2
- import { condense, variance } from "./statistics";
2
+ import { condense, variance, entropy } from "./statistics";
3
3
 
4
4
  /**
5
5
  * Extract kinematic features from motion (IMU) data.
@@ -122,3 +122,173 @@ function derivative(values: number[]): number[] {
122
122
  }
123
123
  return d;
124
124
  }
125
+
126
+ /**
127
+ * Extract mouse dynamics features as a desktop replacement for motion sensor data.
128
+ * Captures behavioral patterns from mouse/pointer movement that are user-specific:
129
+ * path curvature, speed patterns, micro-corrections, pause behavior.
130
+ *
131
+ * Returns: 54 values (matches motion feature dimension for consistent SimHash input)
132
+ */
133
+ export function extractMouseDynamics(samples: TouchSample[]): number[] {
134
+ if (samples.length < 10) return new Array(54).fill(0);
135
+
136
+ const x = samples.map((s) => s.x);
137
+ const y = samples.map((s) => s.y);
138
+ const pressure = samples.map((s) => s.pressure);
139
+ const area = samples.map((s) => s.width * s.height);
140
+
141
+ // Velocity
142
+ const vx = derivative(x);
143
+ const vy = derivative(y);
144
+ const speed = vx.map((dx, i) => Math.sqrt(dx * dx + (vy[i] ?? 0) * (vy[i] ?? 0)));
145
+
146
+ // Acceleration
147
+ const accX = derivative(vx);
148
+ const accY = derivative(vy);
149
+ const acc = accX.map((ax, i) => Math.sqrt(ax * ax + (accY[i] ?? 0) * (accY[i] ?? 0)));
150
+
151
+ // Jerk (derivative of acceleration)
152
+ const jerkX = derivative(accX);
153
+ const jerkY = derivative(accY);
154
+ const jerk = jerkX.map((jx, i) => Math.sqrt(jx * jx + (jerkY[i] ?? 0) * (jerkY[i] ?? 0)));
155
+
156
+ // Path curvature: angle change between consecutive movement vectors
157
+ const curvatures: number[] = [];
158
+ for (let i = 1; i < vx.length; i++) {
159
+ const angle1 = Math.atan2(vy[i - 1] ?? 0, vx[i - 1] ?? 0);
160
+ const angle2 = Math.atan2(vy[i] ?? 0, vx[i] ?? 0);
161
+ let diff = angle2 - angle1;
162
+ while (diff > Math.PI) diff -= 2 * Math.PI;
163
+ while (diff < -Math.PI) diff += 2 * Math.PI;
164
+ curvatures.push(Math.abs(diff));
165
+ }
166
+
167
+ // Movement directions for directional entropy
168
+ const directions = vx.map((dx, i) => Math.atan2(vy[i] ?? 0, dx));
169
+
170
+ // Micro-corrections: direction reversals
171
+ let reversals = 0;
172
+ for (let i = 2; i < directions.length; i++) {
173
+ const d1 = directions[i - 1]! - directions[i - 2]!;
174
+ const d2 = directions[i]! - directions[i - 1]!;
175
+ if (d1 * d2 < 0) reversals++;
176
+ }
177
+ const reversalRate = directions.length > 2 ? reversals / (directions.length - 2) : 0;
178
+ const reversalMagnitude = curvatures.length > 0
179
+ ? curvatures.reduce((a, b) => a + b, 0) / curvatures.length
180
+ : 0;
181
+
182
+ // Pause detection: frames where speed is near zero
183
+ const speedThreshold = 0.5;
184
+ const pauseFrames = speed.filter((s) => s < speedThreshold).length;
185
+ const pauseRatio = speed.length > 0 ? pauseFrames / speed.length : 0;
186
+
187
+ // Path efficiency: straight-line distance / total path length
188
+ const totalPathLength = speed.reduce((a, b) => a + b, 0);
189
+ const straightLine = Math.sqrt(
190
+ (x[x.length - 1]! - x[0]!) ** 2 + (y[y.length - 1]! - y[0]!) ** 2
191
+ );
192
+ const pathEfficiency = totalPathLength > 0 ? straightLine / totalPathLength : 0;
193
+
194
+ // Movement durations between pauses
195
+ const movementDurations: number[] = [];
196
+ let currentDuration = 0;
197
+ for (const s of speed) {
198
+ if (s >= speedThreshold) {
199
+ currentDuration++;
200
+ } else if (currentDuration > 0) {
201
+ movementDurations.push(currentDuration);
202
+ currentDuration = 0;
203
+ }
204
+ }
205
+ if (currentDuration > 0) movementDurations.push(currentDuration);
206
+
207
+ // Segment lengths between direction changes
208
+ const segmentLengths: number[] = [];
209
+ let segLen = 0;
210
+ for (let i = 1; i < directions.length; i++) {
211
+ segLen += speed[i] ?? 0;
212
+ const angleDiff = Math.abs(directions[i]! - directions[i - 1]!);
213
+ if (angleDiff > Math.PI / 4) {
214
+ segmentLengths.push(segLen);
215
+ segLen = 0;
216
+ }
217
+ }
218
+ if (segLen > 0) segmentLengths.push(segLen);
219
+
220
+ // Windowed jitter variance of speed
221
+ const windowSize = Math.max(5, Math.floor(speed.length / 4));
222
+ const windowVariances: number[] = [];
223
+ for (let i = 0; i + windowSize <= speed.length; i += windowSize) {
224
+ const window = speed.slice(i, i + windowSize);
225
+ windowVariances.push(variance(window));
226
+ }
227
+ const speedJitter = windowVariances.length > 1 ? variance(windowVariances) : 0;
228
+
229
+ // Path length normalized by capture duration
230
+ const duration = samples.length > 1
231
+ ? (samples[samples.length - 1]!.timestamp - samples[0]!.timestamp) / 1000
232
+ : 1;
233
+ const normalizedPathLength = totalPathLength / Math.max(duration, 0.001);
234
+
235
+ // Angle autocorrelation at lags 1, 2, 3
236
+ const angleAutoCorr: number[] = [];
237
+ for (let lag = 1; lag <= 3; lag++) {
238
+ if (directions.length <= lag) {
239
+ angleAutoCorr.push(0);
240
+ continue;
241
+ }
242
+ const n = directions.length - lag;
243
+ const meanDir = directions.reduce((a, b) => a + b, 0) / directions.length;
244
+ let num = 0;
245
+ let den = 0;
246
+ for (let i = 0; i < n; i++) {
247
+ num += (directions[i]! - meanDir) * (directions[i + lag]! - meanDir);
248
+ den += (directions[i]! - meanDir) ** 2;
249
+ }
250
+ angleAutoCorr.push(den > 0 ? num / den : 0);
251
+ }
252
+
253
+ // Assemble 54 features
254
+ const curvatureStats = condense(curvatures); // 4
255
+ const dirEntropy = entropy(directions, 16); // 1
256
+ const speedStats = condense(speed); // 4
257
+ const accStats = condense(acc); // 4
258
+ // micro-corrections: reversalRate + reversalMagnitude // 2
259
+ // pauseRatio // 1
260
+ // pathEfficiency // 1
261
+ // speedJitter // 1
262
+ const jerkStats = condense(jerk); // 4
263
+ const vxStats = condense(vx); // 4
264
+ const vyStats = condense(vy); // 4
265
+ const accXStats = condense(accX); // 4
266
+ const accYStats = condense(accY); // 4
267
+ const pressureStats = condense(pressure); // 4
268
+ const moveDurStats = condense(movementDurations); // 4
269
+ const segLenStats = condense(segmentLengths); // 4
270
+ // angleAutoCorr[0..2] // 3
271
+ // normalizedPathLength // 1
272
+ // Total: 4+1+4+4+2+1+1+1+4+4+4+4+4+4+4+4+3+1 = 54
273
+
274
+ return [
275
+ curvatureStats.mean, curvatureStats.variance, curvatureStats.skewness, curvatureStats.kurtosis,
276
+ dirEntropy,
277
+ speedStats.mean, speedStats.variance, speedStats.skewness, speedStats.kurtosis,
278
+ accStats.mean, accStats.variance, accStats.skewness, accStats.kurtosis,
279
+ reversalRate, reversalMagnitude,
280
+ pauseRatio,
281
+ pathEfficiency,
282
+ speedJitter,
283
+ jerkStats.mean, jerkStats.variance, jerkStats.skewness, jerkStats.kurtosis,
284
+ vxStats.mean, vxStats.variance, vxStats.skewness, vxStats.kurtosis,
285
+ vyStats.mean, vyStats.variance, vyStats.skewness, vyStats.kurtosis,
286
+ accXStats.mean, accXStats.variance, accXStats.skewness, accXStats.kurtosis,
287
+ accYStats.mean, accYStats.variance, accYStats.skewness, accYStats.kurtosis,
288
+ pressureStats.mean, pressureStats.variance, pressureStats.skewness, pressureStats.kurtosis,
289
+ moveDurStats.mean, moveDurStats.variance, moveDurStats.skewness, moveDurStats.kurtosis,
290
+ segLenStats.mean, segLenStats.variance, segLenStats.skewness, segLenStats.kurtosis,
291
+ angleAutoCorr[0] ?? 0, angleAutoCorr[1] ?? 0, angleAutoCorr[2] ?? 0,
292
+ normalizedPathLength,
293
+ ];
294
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Linear Predictive Coding (LPC) for formant detection.
3
+ *
4
+ * Implements Levinson-Durbin recursion for LPC coefficient computation
5
+ * and polynomial root-finding for formant frequency estimation.
6
+ */
7
+
8
+ /**
9
+ * Compute autocorrelation of a signal for lags 0..order.
10
+ */
11
+ function autocorrelate(signal: Float32Array, order: number): number[] {
12
+ const r: number[] = [];
13
+ for (let lag = 0; lag <= order; lag++) {
14
+ let sum = 0;
15
+ for (let i = 0; i < signal.length - lag; i++) {
16
+ sum += signal[i]! * signal[i + lag]!;
17
+ }
18
+ r.push(sum);
19
+ }
20
+ return r;
21
+ }
22
+
23
+ /**
24
+ * Levinson-Durbin recursion to compute LPC coefficients from autocorrelation.
25
+ * Returns the LPC coefficients a[1..order] (a[0] is implicitly 1).
26
+ */
27
+ function levinsonDurbin(r: number[], order: number): number[] {
28
+ const a: number[] = new Array(order + 1).fill(0);
29
+ const aTemp: number[] = new Array(order + 1).fill(0);
30
+ a[0] = 1;
31
+
32
+ let error = r[0]!;
33
+ if (error === 0) return new Array(order).fill(0);
34
+
35
+ for (let i = 1; i <= order; i++) {
36
+ let lambda = 0;
37
+ for (let j = 1; j < i; j++) {
38
+ lambda += a[j]! * r[i - j]!;
39
+ }
40
+ lambda = -(r[i]! + lambda) / error;
41
+
42
+ for (let j = 1; j < i; j++) {
43
+ aTemp[j] = a[j]! + lambda * a[i - j]!;
44
+ }
45
+ aTemp[i] = lambda;
46
+
47
+ for (let j = 1; j <= i; j++) {
48
+ a[j] = aTemp[j]!;
49
+ }
50
+
51
+ error *= 1 - lambda * lambda;
52
+ if (error <= 0) break;
53
+ }
54
+
55
+ return a.slice(1);
56
+ }
57
+
58
+ /**
59
+ * Find roots of a polynomial using the Durand-Kerner method.
60
+ * The polynomial is 1 + a[0]*z^-1 + a[1]*z^-2 + ... + a[n-1]*z^-n
61
+ * which is equivalent to z^n + a[0]*z^(n-1) + ... + a[n-1] = 0.
62
+ *
63
+ * Returns complex roots as [real, imag] pairs.
64
+ */
65
+ function findRoots(coefficients: number[], maxIterations: number = 50): [number, number][] {
66
+ const n = coefficients.length;
67
+ if (n === 0) return [];
68
+
69
+ // Initial guesses: points on a circle of radius 0.9
70
+ const roots: [number, number][] = [];
71
+ for (let i = 0; i < n; i++) {
72
+ const angle = (2 * Math.PI * i) / n + 0.1;
73
+ roots.push([0.9 * Math.cos(angle), 0.9 * Math.sin(angle)]);
74
+ }
75
+
76
+ for (let iter = 0; iter < maxIterations; iter++) {
77
+ let maxShift = 0;
78
+
79
+ for (let i = 0; i < n; i++) {
80
+ // Evaluate polynomial at roots[i]: z^n + a[0]*z^(n-1) + ... + a[n-1]
81
+ let pReal = 1;
82
+ let pImag = 0;
83
+ let zPowReal = 1;
84
+ let zPowImag = 0;
85
+
86
+ // Compute z^n by repeated multiplication
87
+ const [rr, ri] = roots[i]!;
88
+ let curReal = 1;
89
+ let curImag = 0;
90
+
91
+ // Evaluate as: z^n + sum(a[k] * z^(n-1-k))
92
+ // Start with z^n
93
+ let znReal = 1;
94
+ let znImag = 0;
95
+ for (let k = 0; k < n; k++) {
96
+ const newReal = znReal * rr - znImag * ri;
97
+ const newImag = znReal * ri + znImag * rr;
98
+ znReal = newReal;
99
+ znImag = newImag;
100
+ }
101
+ pReal = znReal;
102
+ pImag = znImag;
103
+
104
+ // Add coefficient terms: a[k] * z^(n-1-k)
105
+ zPowReal = 1;
106
+ zPowImag = 0;
107
+ for (let k = n - 1; k >= 0; k--) {
108
+ pReal += coefficients[k]! * zPowReal;
109
+ pImag += coefficients[k]! * zPowImag;
110
+ const newReal = zPowReal * rr - zPowImag * ri;
111
+ const newImag = zPowReal * ri + zPowImag * rr;
112
+ zPowReal = newReal;
113
+ zPowImag = newImag;
114
+ }
115
+
116
+ // Compute product of (roots[i] - roots[j]) for j != i
117
+ let denomReal = 1;
118
+ let denomImag = 0;
119
+ for (let j = 0; j < n; j++) {
120
+ if (j === i) continue;
121
+ const diffReal = rr - roots[j]![0];
122
+ const diffImag = ri - roots[j]![1];
123
+ const newReal = denomReal * diffReal - denomImag * diffImag;
124
+ const newImag = denomReal * diffImag + denomImag * diffReal;
125
+ denomReal = newReal;
126
+ denomImag = newImag;
127
+ }
128
+
129
+ // Divide p / denom
130
+ const denomMag2 = denomReal * denomReal + denomImag * denomImag;
131
+ if (denomMag2 < 1e-30) continue;
132
+
133
+ const shiftReal = (pReal * denomReal + pImag * denomImag) / denomMag2;
134
+ const shiftImag = (pImag * denomReal - pReal * denomImag) / denomMag2;
135
+
136
+ roots[i] = [rr - shiftReal, ri - shiftImag];
137
+ maxShift = Math.max(maxShift, Math.sqrt(shiftReal * shiftReal + shiftImag * shiftImag));
138
+ }
139
+
140
+ if (maxShift < 1e-10) break;
141
+ }
142
+
143
+ return roots;
144
+ }
145
+
146
+ /**
147
+ * Extract formant frequencies (F1, F2, F3) from a single audio frame.
148
+ * Returns [F1, F2, F3] in Hz, or null if extraction fails.
149
+ */
150
+ function extractFormants(
151
+ frame: Float32Array,
152
+ sampleRate: number,
153
+ lpcOrder: number = 12
154
+ ): [number, number, number] | null {
155
+ const r = autocorrelate(frame, lpcOrder);
156
+ const coeffs = levinsonDurbin(r, lpcOrder);
157
+
158
+ const roots = findRoots(coeffs);
159
+
160
+ // Convert roots to frequencies, keep only positive-frequency roots
161
+ const formantCandidates: number[] = [];
162
+
163
+ for (const [real, imag] of roots) {
164
+ if (imag <= 0) continue; // Keep only positive-frequency roots
165
+
166
+ const freq = (Math.atan2(imag, real) / (2 * Math.PI)) * sampleRate;
167
+ const bandwidth = (-sampleRate / (2 * Math.PI)) * Math.log(Math.sqrt(real * real + imag * imag));
168
+
169
+ // Filter: formants are in 200-5000Hz range with reasonable bandwidth
170
+ if (freq > 200 && freq < 5000 && bandwidth < 500) {
171
+ formantCandidates.push(freq);
172
+ }
173
+ }
174
+
175
+ formantCandidates.sort((a, b) => a - b);
176
+
177
+ if (formantCandidates.length < 3) return null;
178
+
179
+ return [formantCandidates[0]!, formantCandidates[1]!, formantCandidates[2]!];
180
+ }
181
+
182
+ /**
183
+ * Extract formant ratio time series (F1/F2 and F2/F3) from audio.
184
+ * Returns { f1f2: number[], f2f3: number[] } — one ratio per frame where formants were detected.
185
+ */
186
+ export function extractFormantRatios(
187
+ samples: Float32Array,
188
+ sampleRate: number,
189
+ frameSize: number,
190
+ hopSize: number
191
+ ): { f1f2: number[]; f2f3: number[] } {
192
+ const f1f2: number[] = [];
193
+ const f2f3: number[] = [];
194
+ const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
195
+
196
+ for (let i = 0; i < numFrames; i++) {
197
+ const start = i * hopSize;
198
+ const frame = samples.slice(start, start + frameSize);
199
+
200
+ // Apply Hamming window
201
+ const windowed = new Float32Array(frameSize);
202
+ for (let j = 0; j < frameSize; j++) {
203
+ windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos((2 * Math.PI * j) / (frameSize - 1)));
204
+ }
205
+
206
+ const formants = extractFormants(windowed, sampleRate);
207
+ if (formants) {
208
+ const [f1, f2, f3] = formants;
209
+ if (f2 > 0) f1f2.push(f1 / f2);
210
+ if (f3 > 0) f2f3.push(f2 / f3);
211
+ }
212
+ }
213
+
214
+ return { f1f2, f2f3 };
215
+ }