@iam-protocol/pulse-sdk 0.2.6 → 0.3.1

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.mjs CHANGED
@@ -12945,7 +12945,7 @@ var BN254_SCALAR_FIELD = BigInt(
12945
12945
  "21888242871839275222246405745257275088548364400416034343698204186575808495617"
12946
12946
  );
12947
12947
  var FINGERPRINT_BITS = 256;
12948
- var DEFAULT_THRESHOLD = 30;
12948
+ var DEFAULT_THRESHOLD = 96;
12949
12949
  var DEFAULT_MIN_DISTANCE = 3;
12950
12950
  var NUM_PUBLIC_INPUTS = 4;
12951
12951
  var PROOF_A_SIZE = 64;
@@ -13230,11 +13230,153 @@ function fuseFeatures(audio, motion, touch) {
13230
13230
  return [...audio, ...motion, ...touch];
13231
13231
  }
13232
13232
 
13233
- // src/extraction/mfcc.ts
13233
+ // src/extraction/lpc.ts
13234
+ function autocorrelate(signal, order) {
13235
+ const r = [];
13236
+ for (let lag = 0; lag <= order; lag++) {
13237
+ let sum = 0;
13238
+ for (let i = 0; i < signal.length - lag; i++) {
13239
+ sum += signal[i] * signal[i + lag];
13240
+ }
13241
+ r.push(sum);
13242
+ }
13243
+ return r;
13244
+ }
13245
+ function levinsonDurbin(r, order) {
13246
+ const a = new Array(order + 1).fill(0);
13247
+ const aTemp = new Array(order + 1).fill(0);
13248
+ a[0] = 1;
13249
+ let error = r[0];
13250
+ if (error === 0) return new Array(order).fill(0);
13251
+ for (let i = 1; i <= order; i++) {
13252
+ let lambda = 0;
13253
+ for (let j = 1; j < i; j++) {
13254
+ lambda += a[j] * r[i - j];
13255
+ }
13256
+ lambda = -(r[i] + lambda) / error;
13257
+ for (let j = 1; j < i; j++) {
13258
+ aTemp[j] = a[j] + lambda * a[i - j];
13259
+ }
13260
+ aTemp[i] = lambda;
13261
+ for (let j = 1; j <= i; j++) {
13262
+ a[j] = aTemp[j];
13263
+ }
13264
+ error *= 1 - lambda * lambda;
13265
+ if (error <= 0) break;
13266
+ }
13267
+ return a.slice(1);
13268
+ }
13269
+ function findRoots(coefficients, maxIterations = 50) {
13270
+ const n = coefficients.length;
13271
+ if (n === 0) return [];
13272
+ const roots = [];
13273
+ for (let i = 0; i < n; i++) {
13274
+ const angle = 2 * Math.PI * i / n + 0.1;
13275
+ roots.push([0.9 * Math.cos(angle), 0.9 * Math.sin(angle)]);
13276
+ }
13277
+ for (let iter = 0; iter < maxIterations; iter++) {
13278
+ let maxShift = 0;
13279
+ for (let i = 0; i < n; i++) {
13280
+ let pReal = 1;
13281
+ let pImag = 0;
13282
+ let zPowReal = 1;
13283
+ let zPowImag = 0;
13284
+ const [rr, ri] = roots[i];
13285
+ let curReal = 1;
13286
+ let curImag = 0;
13287
+ let znReal = 1;
13288
+ let znImag = 0;
13289
+ for (let k = 0; k < n; k++) {
13290
+ const newReal = znReal * rr - znImag * ri;
13291
+ const newImag = znReal * ri + znImag * rr;
13292
+ znReal = newReal;
13293
+ znImag = newImag;
13294
+ }
13295
+ pReal = znReal;
13296
+ pImag = znImag;
13297
+ zPowReal = 1;
13298
+ zPowImag = 0;
13299
+ for (let k = n - 1; k >= 0; k--) {
13300
+ pReal += coefficients[k] * zPowReal;
13301
+ pImag += coefficients[k] * zPowImag;
13302
+ const newReal = zPowReal * rr - zPowImag * ri;
13303
+ const newImag = zPowReal * ri + zPowImag * rr;
13304
+ zPowReal = newReal;
13305
+ zPowImag = newImag;
13306
+ }
13307
+ let denomReal = 1;
13308
+ let denomImag = 0;
13309
+ for (let j = 0; j < n; j++) {
13310
+ if (j === i) continue;
13311
+ const diffReal = rr - roots[j][0];
13312
+ const diffImag = ri - roots[j][1];
13313
+ const newReal = denomReal * diffReal - denomImag * diffImag;
13314
+ const newImag = denomReal * diffImag + denomImag * diffReal;
13315
+ denomReal = newReal;
13316
+ denomImag = newImag;
13317
+ }
13318
+ const denomMag2 = denomReal * denomReal + denomImag * denomImag;
13319
+ if (denomMag2 < 1e-30) continue;
13320
+ const shiftReal = (pReal * denomReal + pImag * denomImag) / denomMag2;
13321
+ const shiftImag = (pImag * denomReal - pReal * denomImag) / denomMag2;
13322
+ roots[i] = [rr - shiftReal, ri - shiftImag];
13323
+ maxShift = Math.max(maxShift, Math.sqrt(shiftReal * shiftReal + shiftImag * shiftImag));
13324
+ }
13325
+ if (maxShift < 1e-10) break;
13326
+ }
13327
+ return roots;
13328
+ }
13329
+ function extractFormants(frame, sampleRate, lpcOrder = 12) {
13330
+ const r = autocorrelate(frame, lpcOrder);
13331
+ const coeffs = levinsonDurbin(r, lpcOrder);
13332
+ const roots = findRoots(coeffs);
13333
+ const formantCandidates = [];
13334
+ for (const [real, imag] of roots) {
13335
+ if (imag <= 0) continue;
13336
+ const freq = Math.atan2(imag, real) / (2 * Math.PI) * sampleRate;
13337
+ const bandwidth = -sampleRate / (2 * Math.PI) * Math.log(Math.sqrt(real * real + imag * imag));
13338
+ if (freq > 200 && freq < 5e3 && bandwidth < 500) {
13339
+ formantCandidates.push(freq);
13340
+ }
13341
+ }
13342
+ formantCandidates.sort((a, b) => a - b);
13343
+ if (formantCandidates.length < 3) return null;
13344
+ return [formantCandidates[0], formantCandidates[1], formantCandidates[2]];
13345
+ }
13346
+ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
13347
+ const f1f2 = [];
13348
+ const f2f3 = [];
13349
+ const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
13350
+ for (let i = 0; i < numFrames; i++) {
13351
+ const start = i * hopSize;
13352
+ const frame = samples.slice(start, start + frameSize);
13353
+ const windowed = new Float32Array(frameSize);
13354
+ for (let j = 0; j < frameSize; j++) {
13355
+ windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
13356
+ }
13357
+ const formants = extractFormants(windowed, sampleRate);
13358
+ if (formants) {
13359
+ const [f1, f2, f3] = formants;
13360
+ if (f2 > 0) f1f2.push(f1 / f2);
13361
+ if (f3 > 0) f2f3.push(f2 / f3);
13362
+ }
13363
+ }
13364
+ return { f1f2, f2f3 };
13365
+ }
13366
+
13367
+ // src/extraction/speaker.ts
13234
13368
  var FRAME_SIZE = 512;
13235
13369
  var HOP_SIZE = 160;
13236
- var NUM_MFCC = 13;
13370
+ var SPEAKER_FEATURE_COUNT = 44;
13371
+ var pitchDetector = null;
13237
13372
  var meydaModule = null;
13373
+ async function getPitchDetector() {
13374
+ if (!pitchDetector) {
13375
+ const PitchFinder = await import("pitchfinder");
13376
+ pitchDetector = PitchFinder.YIN({ sampleRate: 16e3 });
13377
+ }
13378
+ return pitchDetector;
13379
+ }
13238
13380
  async function getMeyda() {
13239
13381
  if (!meydaModule) {
13240
13382
  try {
@@ -13245,66 +13387,230 @@ async function getMeyda() {
13245
13387
  }
13246
13388
  return meydaModule.default ?? meydaModule;
13247
13389
  }
13248
- async function extractMFCC(audio) {
13249
- const { samples, sampleRate } = audio;
13250
- const Meyda = await getMeyda();
13251
- if (!Meyda) {
13252
- console.warn("[IAM SDK] Meyda library failed to load. Audio features will be zeros.");
13253
- return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13254
- }
13390
+ async function detectF0Contour(samples, sampleRate) {
13391
+ const detect = await getPitchDetector();
13392
+ const f0 = [];
13393
+ const amplitudes = [];
13394
+ const periods = [];
13395
+ const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13396
+ for (let i = 0; i < numFrames; i++) {
13397
+ const start = i * HOP_SIZE;
13398
+ const frame = samples.slice(start, start + FRAME_SIZE);
13399
+ const pitch = detect(frame);
13400
+ if (pitch && pitch > 50 && pitch < 600) {
13401
+ f0.push(pitch);
13402
+ periods.push(1 / pitch);
13403
+ } else {
13404
+ f0.push(0);
13405
+ }
13406
+ let sum = 0;
13407
+ for (let j = 0; j < frame.length; j++) {
13408
+ sum += (frame[j] ?? 0) * (frame[j] ?? 0);
13409
+ }
13410
+ amplitudes.push(Math.sqrt(sum / frame.length));
13411
+ }
13412
+ return { f0, amplitudes, periods };
13413
+ }
13414
+ function computeJitter(periods) {
13415
+ const voiced = periods.filter((p) => p > 0);
13416
+ if (voiced.length < 3) return [0, 0, 0, 0];
13417
+ const meanPeriod = voiced.reduce((a, b) => a + b, 0) / voiced.length;
13418
+ if (meanPeriod === 0) return [0, 0, 0, 0];
13419
+ let localSum = 0;
13420
+ for (let i = 1; i < voiced.length; i++) {
13421
+ localSum += Math.abs(voiced[i] - voiced[i - 1]);
13422
+ }
13423
+ const jitterLocal = localSum / (voiced.length - 1) / meanPeriod;
13424
+ let rapSum = 0;
13425
+ for (let i = 1; i < voiced.length - 1; i++) {
13426
+ const avg3 = (voiced[i - 1] + voiced[i] + voiced[i + 1]) / 3;
13427
+ rapSum += Math.abs(voiced[i] - avg3);
13428
+ }
13429
+ const jitterRAP = voiced.length > 2 ? rapSum / (voiced.length - 2) / meanPeriod : 0;
13430
+ let ppq5Sum = 0;
13431
+ let ppq5Count = 0;
13432
+ for (let i = 2; i < voiced.length - 2; i++) {
13433
+ const avg5 = (voiced[i - 2] + voiced[i - 1] + voiced[i] + voiced[i + 1] + voiced[i + 2]) / 5;
13434
+ ppq5Sum += Math.abs(voiced[i] - avg5);
13435
+ ppq5Count++;
13436
+ }
13437
+ const jitterPPQ5 = ppq5Count > 0 ? ppq5Sum / ppq5Count / meanPeriod : 0;
13438
+ let ddpSum = 0;
13439
+ for (let i = 1; i < voiced.length - 1; i++) {
13440
+ const d1 = voiced[i] - voiced[i - 1];
13441
+ const d2 = voiced[i + 1] - voiced[i];
13442
+ ddpSum += Math.abs(d2 - d1);
13443
+ }
13444
+ const jitterDDP = voiced.length > 2 ? ddpSum / (voiced.length - 2) / meanPeriod : 0;
13445
+ return [jitterLocal, jitterRAP, jitterPPQ5, jitterDDP];
13446
+ }
13447
+ function computeShimmer(amplitudes, f0) {
13448
+ const voicedAmps = amplitudes.filter((_, i) => f0[i] > 0);
13449
+ if (voicedAmps.length < 3) return [0, 0, 0, 0];
13450
+ const meanAmp = voicedAmps.reduce((a, b) => a + b, 0) / voicedAmps.length;
13451
+ if (meanAmp === 0) return [0, 0, 0, 0];
13452
+ let localSum = 0;
13453
+ for (let i = 1; i < voicedAmps.length; i++) {
13454
+ localSum += Math.abs(voicedAmps[i] - voicedAmps[i - 1]);
13455
+ }
13456
+ const shimmerLocal = localSum / (voicedAmps.length - 1) / meanAmp;
13457
+ let apq3Sum = 0;
13458
+ for (let i = 1; i < voicedAmps.length - 1; i++) {
13459
+ const avg3 = (voicedAmps[i - 1] + voicedAmps[i] + voicedAmps[i + 1]) / 3;
13460
+ apq3Sum += Math.abs(voicedAmps[i] - avg3);
13461
+ }
13462
+ const shimmerAPQ3 = voicedAmps.length > 2 ? apq3Sum / (voicedAmps.length - 2) / meanAmp : 0;
13463
+ let apq5Sum = 0;
13464
+ let apq5Count = 0;
13465
+ for (let i = 2; i < voicedAmps.length - 2; i++) {
13466
+ const avg5 = (voicedAmps[i - 2] + voicedAmps[i - 1] + voicedAmps[i] + voicedAmps[i + 1] + voicedAmps[i + 2]) / 5;
13467
+ apq5Sum += Math.abs(voicedAmps[i] - avg5);
13468
+ apq5Count++;
13469
+ }
13470
+ const shimmerAPQ5 = apq5Count > 0 ? apq5Sum / apq5Count / meanAmp : 0;
13471
+ let ddaSum = 0;
13472
+ for (let i = 1; i < voicedAmps.length - 1; i++) {
13473
+ const d1 = voicedAmps[i] - voicedAmps[i - 1];
13474
+ const d2 = voicedAmps[i + 1] - voicedAmps[i];
13475
+ ddaSum += Math.abs(d2 - d1);
13476
+ }
13477
+ const shimmerDDA = voicedAmps.length > 2 ? ddaSum / (voicedAmps.length - 2) / meanAmp : 0;
13478
+ return [shimmerLocal, shimmerAPQ3, shimmerAPQ5, shimmerDDA];
13479
+ }
13480
+ function computeHNR(samples, sampleRate, f0Contour) {
13481
+ const hnr = [];
13255
13482
  const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13256
- if (numFrames < 3) {
13257
- console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Need at least 3.`);
13258
- return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13483
+ for (let i = 0; i < numFrames && i < f0Contour.length; i++) {
13484
+ const f0 = f0Contour[i];
13485
+ if (f0 <= 0) continue;
13486
+ const start = i * HOP_SIZE;
13487
+ const frame = samples.slice(start, start + FRAME_SIZE);
13488
+ const period = Math.round(sampleRate / f0);
13489
+ if (period <= 0 || period >= frame.length) continue;
13490
+ let num = 0;
13491
+ let den = 0;
13492
+ for (let j = 0; j < frame.length - period; j++) {
13493
+ num += (frame[j] ?? 0) * (frame[j + period] ?? 0);
13494
+ den += (frame[j] ?? 0) * (frame[j] ?? 0);
13495
+ }
13496
+ if (den > 0) {
13497
+ const r = num / den;
13498
+ const clampedR = Math.max(1e-3, Math.min(0.999, r));
13499
+ hnr.push(10 * Math.log10(clampedR / (1 - clampedR)));
13500
+ }
13259
13501
  }
13260
- const mfccFrames = [];
13502
+ return hnr;
13503
+ }
13504
+ async function computeLTAS(samples, sampleRate) {
13505
+ const Meyda = await getMeyda();
13506
+ if (!Meyda) return new Array(8).fill(0);
13507
+ const centroids = [];
13508
+ const rolloffs = [];
13509
+ const flatnesses = [];
13510
+ const spreads = [];
13511
+ const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13261
13512
  for (let i = 0; i < numFrames; i++) {
13262
13513
  const start = i * HOP_SIZE;
13263
13514
  const frame = samples.slice(start, start + FRAME_SIZE);
13264
13515
  const paddedFrame = new Float32Array(FRAME_SIZE);
13265
13516
  paddedFrame.set(frame);
13266
- const features2 = Meyda.extract(["mfcc"], paddedFrame, {
13267
- sampleRate,
13268
- bufferSize: FRAME_SIZE,
13269
- numberOfMFCCCoefficients: NUM_MFCC
13270
- });
13271
- if (features2?.mfcc) {
13272
- mfccFrames.push(features2.mfcc);
13273
- }
13274
- }
13275
- if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13276
- const deltaFrames = computeDeltas(mfccFrames);
13277
- const deltaDeltaFrames = computeDeltas(deltaFrames);
13278
- const features = [];
13279
- for (let c = 0; c < NUM_MFCC; c++) {
13280
- const raw = mfccFrames.map((f) => f[c] ?? 0);
13281
- const stats = condense(raw);
13282
- features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
13283
- }
13284
- for (let c = 0; c < NUM_MFCC; c++) {
13285
- const delta = deltaFrames.map((f) => f[c] ?? 0);
13286
- const stats = condense(delta);
13287
- features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
13288
- }
13289
- for (let c = 0; c < NUM_MFCC; c++) {
13290
- const dd = deltaDeltaFrames.map((f) => f[c] ?? 0);
13291
- const stats = condense(dd);
13292
- features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
13293
- }
13294
- for (let c = 0; c < NUM_MFCC; c++) {
13295
- const raw = mfccFrames.map((f) => f[c] ?? 0);
13296
- features.push(entropy(raw));
13297
- }
13298
- return features;
13517
+ const features = Meyda.extract(
13518
+ ["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
13519
+ paddedFrame,
13520
+ { sampleRate, bufferSize: FRAME_SIZE }
13521
+ );
13522
+ if (features) {
13523
+ if (typeof features.spectralCentroid === "number") centroids.push(features.spectralCentroid);
13524
+ if (typeof features.spectralRolloff === "number") rolloffs.push(features.spectralRolloff);
13525
+ if (typeof features.spectralFlatness === "number") flatnesses.push(features.spectralFlatness);
13526
+ if (typeof features.spectralSpread === "number") spreads.push(features.spectralSpread);
13527
+ }
13528
+ }
13529
+ const m = (arr) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
13530
+ const v = (arr) => {
13531
+ if (arr.length < 2) return 0;
13532
+ const mu = m(arr);
13533
+ return arr.reduce((sum, x) => sum + (x - mu) * (x - mu), 0) / (arr.length - 1);
13534
+ };
13535
+ return [
13536
+ m(centroids),
13537
+ v(centroids),
13538
+ m(rolloffs),
13539
+ v(rolloffs),
13540
+ m(flatnesses),
13541
+ v(flatnesses),
13542
+ m(spreads),
13543
+ v(spreads)
13544
+ ];
13299
13545
  }
13300
- function computeDeltas(frames) {
13301
- const deltas = [];
13302
- for (let i = 1; i < frames.length; i++) {
13303
- const prev = frames[i - 1];
13304
- const curr = frames[i];
13305
- deltas.push(curr.map((v, j) => v - (prev[j] ?? 0)));
13546
+ function derivative(values) {
13547
+ const d = [];
13548
+ for (let i = 1; i < values.length; i++) {
13549
+ d.push(values[i] - values[i - 1]);
13306
13550
  }
13307
- return deltas;
13551
+ return d;
13552
+ }
13553
+ async function extractSpeakerFeatures(audio) {
13554
+ const { samples, sampleRate } = audio;
13555
+ const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13556
+ if (numFrames < 5) {
13557
+ console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Speaker features will be zeros.`);
13558
+ return new Array(SPEAKER_FEATURE_COUNT).fill(0);
13559
+ }
13560
+ const { f0, amplitudes, periods } = await detectF0Contour(samples, sampleRate);
13561
+ const voicedF0 = f0.filter((v) => v > 0);
13562
+ const voicedRatio = voicedF0.length / f0.length;
13563
+ const f0Stats = condense(voicedF0);
13564
+ const f0Entropy = entropy(voicedF0);
13565
+ const f0Features = [f0Stats.mean, f0Stats.variance, f0Stats.skewness, f0Stats.kurtosis, f0Entropy];
13566
+ const f0Delta = derivative(voicedF0);
13567
+ const f0DeltaStats = condense(f0Delta);
13568
+ const f0DeltaFeatures = [f0DeltaStats.mean, f0DeltaStats.variance, f0DeltaStats.skewness, f0DeltaStats.kurtosis];
13569
+ const jitterFeatures = computeJitter(periods);
13570
+ const shimmerFeatures = computeShimmer(amplitudes, f0);
13571
+ const hnrValues = computeHNR(samples, sampleRate, f0);
13572
+ const hnrStats = condense(hnrValues);
13573
+ const hnrEntropy = entropy(hnrValues);
13574
+ const hnrFeatures = [hnrStats.mean, hnrStats.variance, hnrStats.skewness, hnrStats.kurtosis, hnrEntropy];
13575
+ const { f1f2, f2f3 } = extractFormantRatios(samples, sampleRate, FRAME_SIZE, HOP_SIZE);
13576
+ const f1f2Stats = condense(f1f2);
13577
+ const f2f3Stats = condense(f2f3);
13578
+ const formantFeatures = [
13579
+ f1f2Stats.mean,
13580
+ f1f2Stats.variance,
13581
+ f1f2Stats.skewness,
13582
+ f1f2Stats.kurtosis,
13583
+ f2f3Stats.mean,
13584
+ f2f3Stats.variance,
13585
+ f2f3Stats.skewness,
13586
+ f2f3Stats.kurtosis
13587
+ ];
13588
+ const ltasFeatures = await computeLTAS(samples, sampleRate);
13589
+ const voicingFeatures = [voicedRatio];
13590
+ const ampStats = condense(amplitudes);
13591
+ const ampEntropy = entropy(amplitudes);
13592
+ const ampFeatures = [ampStats.mean, ampStats.variance, ampStats.skewness, ampStats.kurtosis, ampEntropy];
13593
+ const features = [
13594
+ ...f0Features,
13595
+ // 5
13596
+ ...f0DeltaFeatures,
13597
+ // 4
13598
+ ...jitterFeatures,
13599
+ // 4
13600
+ ...shimmerFeatures,
13601
+ // 4
13602
+ ...hnrFeatures,
13603
+ // 5
13604
+ ...formantFeatures,
13605
+ // 8
13606
+ ...ltasFeatures,
13607
+ // 8
13608
+ ...voicingFeatures,
13609
+ // 1
13610
+ ...ampFeatures
13611
+ // 5
13612
+ ];
13613
+ return features;
13308
13614
  }
13309
13615
 
13310
13616
  // src/extraction/kinematic.ts
@@ -13320,8 +13626,8 @@ function extractMotionFeatures(samples) {
13320
13626
  };
13321
13627
  const features = [];
13322
13628
  for (const values of Object.values(axes)) {
13323
- const jerk = derivative(values);
13324
- const jounce = derivative(jerk);
13629
+ const jerk = derivative2(values);
13630
+ const jounce = derivative2(jerk);
13325
13631
  const jerkStats = condense(jerk);
13326
13632
  const jounceStats = condense(jounce);
13327
13633
  features.push(
@@ -13336,7 +13642,7 @@ function extractMotionFeatures(samples) {
13336
13642
  );
13337
13643
  }
13338
13644
  for (const values of Object.values(axes)) {
13339
- const jerk = derivative(values);
13645
+ const jerk = derivative2(values);
13340
13646
  const windowSize = Math.max(5, Math.floor(jerk.length / 4));
13341
13647
  const windowVariances = [];
13342
13648
  for (let i = 0; i <= jerk.length - windowSize; i += windowSize) {
@@ -13353,18 +13659,18 @@ function extractTouchFeatures(samples) {
13353
13659
  const pressure = samples.map((s) => s.pressure);
13354
13660
  const area = samples.map((s) => s.width * s.height);
13355
13661
  const features = [];
13356
- const vx = derivative(x);
13357
- const accX = derivative(vx);
13662
+ const vx = derivative2(x);
13663
+ const accX = derivative2(vx);
13358
13664
  features.push(...Object.values(condense(vx)));
13359
13665
  features.push(...Object.values(condense(accX)));
13360
- const vy = derivative(y);
13361
- const accY = derivative(vy);
13666
+ const vy = derivative2(y);
13667
+ const accY = derivative2(vy);
13362
13668
  features.push(...Object.values(condense(vy)));
13363
13669
  features.push(...Object.values(condense(accY)));
13364
13670
  features.push(...Object.values(condense(pressure)));
13365
13671
  features.push(...Object.values(condense(area)));
13366
- const jerkX = derivative(accX);
13367
- const jerkY = derivative(accY);
13672
+ const jerkX = derivative2(accX);
13673
+ const jerkY = derivative2(accY);
13368
13674
  features.push(...Object.values(condense(jerkX)));
13369
13675
  features.push(...Object.values(condense(jerkY)));
13370
13676
  for (const values of [vx, vy, pressure, area]) {
@@ -13377,13 +13683,170 @@ function extractTouchFeatures(samples) {
13377
13683
  }
13378
13684
  return features;
13379
13685
  }
13380
- function derivative(values) {
13686
+ function derivative2(values) {
13381
13687
  const d = [];
13382
13688
  for (let i = 1; i < values.length; i++) {
13383
13689
  d.push((values[i] ?? 0) - (values[i - 1] ?? 0));
13384
13690
  }
13385
13691
  return d;
13386
13692
  }
13693
+ function extractMouseDynamics(samples) {
13694
+ if (samples.length < 10) return new Array(54).fill(0);
13695
+ const x = samples.map((s) => s.x);
13696
+ const y = samples.map((s) => s.y);
13697
+ const pressure = samples.map((s) => s.pressure);
13698
+ const area = samples.map((s) => s.width * s.height);
13699
+ const vx = derivative2(x);
13700
+ const vy = derivative2(y);
13701
+ const speed = vx.map((dx, i) => Math.sqrt(dx * dx + (vy[i] ?? 0) * (vy[i] ?? 0)));
13702
+ const accX = derivative2(vx);
13703
+ const accY = derivative2(vy);
13704
+ const acc = accX.map((ax, i) => Math.sqrt(ax * ax + (accY[i] ?? 0) * (accY[i] ?? 0)));
13705
+ const jerkX = derivative2(accX);
13706
+ const jerkY = derivative2(accY);
13707
+ const jerk = jerkX.map((jx, i) => Math.sqrt(jx * jx + (jerkY[i] ?? 0) * (jerkY[i] ?? 0)));
13708
+ const curvatures = [];
13709
+ for (let i = 1; i < vx.length; i++) {
13710
+ const angle1 = Math.atan2(vy[i - 1] ?? 0, vx[i - 1] ?? 0);
13711
+ const angle2 = Math.atan2(vy[i] ?? 0, vx[i] ?? 0);
13712
+ let diff = angle2 - angle1;
13713
+ while (diff > Math.PI) diff -= 2 * Math.PI;
13714
+ while (diff < -Math.PI) diff += 2 * Math.PI;
13715
+ curvatures.push(Math.abs(diff));
13716
+ }
13717
+ const directions = vx.map((dx, i) => Math.atan2(vy[i] ?? 0, dx));
13718
+ let reversals = 0;
13719
+ for (let i = 2; i < directions.length; i++) {
13720
+ const d1 = directions[i - 1] - directions[i - 2];
13721
+ const d2 = directions[i] - directions[i - 1];
13722
+ if (d1 * d2 < 0) reversals++;
13723
+ }
13724
+ const reversalRate = directions.length > 2 ? reversals / (directions.length - 2) : 0;
13725
+ const reversalMagnitude = curvatures.length > 0 ? curvatures.reduce((a, b) => a + b, 0) / curvatures.length : 0;
13726
+ const speedThreshold = 0.5;
13727
+ const pauseFrames = speed.filter((s) => s < speedThreshold).length;
13728
+ const pauseRatio = speed.length > 0 ? pauseFrames / speed.length : 0;
13729
+ const totalPathLength = speed.reduce((a, b) => a + b, 0);
13730
+ const straightLine = Math.sqrt(
13731
+ (x[x.length - 1] - x[0]) ** 2 + (y[y.length - 1] - y[0]) ** 2
13732
+ );
13733
+ const pathEfficiency = totalPathLength > 0 ? straightLine / totalPathLength : 0;
13734
+ const movementDurations = [];
13735
+ let currentDuration = 0;
13736
+ for (const s of speed) {
13737
+ if (s >= speedThreshold) {
13738
+ currentDuration++;
13739
+ } else if (currentDuration > 0) {
13740
+ movementDurations.push(currentDuration);
13741
+ currentDuration = 0;
13742
+ }
13743
+ }
13744
+ if (currentDuration > 0) movementDurations.push(currentDuration);
13745
+ const segmentLengths = [];
13746
+ let segLen = 0;
13747
+ for (let i = 1; i < directions.length; i++) {
13748
+ segLen += speed[i] ?? 0;
13749
+ const angleDiff = Math.abs(directions[i] - directions[i - 1]);
13750
+ if (angleDiff > Math.PI / 4) {
13751
+ segmentLengths.push(segLen);
13752
+ segLen = 0;
13753
+ }
13754
+ }
13755
+ if (segLen > 0) segmentLengths.push(segLen);
13756
+ const windowSize = Math.max(5, Math.floor(speed.length / 4));
13757
+ const windowVariances = [];
13758
+ for (let i = 0; i + windowSize <= speed.length; i += windowSize) {
13759
+ const window2 = speed.slice(i, i + windowSize);
13760
+ windowVariances.push(variance(window2));
13761
+ }
13762
+ const speedJitter = windowVariances.length > 1 ? variance(windowVariances) : 0;
13763
+ const duration = samples.length > 1 ? (samples[samples.length - 1].timestamp - samples[0].timestamp) / 1e3 : 1;
13764
+ const normalizedPathLength = totalPathLength / Math.max(duration, 1e-3);
13765
+ const angleAutoCorr = [];
13766
+ for (let lag = 1; lag <= 3; lag++) {
13767
+ if (directions.length <= lag) {
13768
+ angleAutoCorr.push(0);
13769
+ continue;
13770
+ }
13771
+ const n = directions.length - lag;
13772
+ const meanDir = directions.reduce((a, b) => a + b, 0) / directions.length;
13773
+ let num = 0;
13774
+ let den = 0;
13775
+ for (let i = 0; i < n; i++) {
13776
+ num += (directions[i] - meanDir) * (directions[i + lag] - meanDir);
13777
+ den += (directions[i] - meanDir) ** 2;
13778
+ }
13779
+ angleAutoCorr.push(den > 0 ? num / den : 0);
13780
+ }
13781
+ const curvatureStats = condense(curvatures);
13782
+ const dirEntropy = entropy(directions, 16);
13783
+ const speedStats = condense(speed);
13784
+ const accStats = condense(acc);
13785
+ const jerkStats = condense(jerk);
13786
+ const vxStats = condense(vx);
13787
+ const vyStats = condense(vy);
13788
+ const accXStats = condense(accX);
13789
+ const accYStats = condense(accY);
13790
+ const pressureStats = condense(pressure);
13791
+ const moveDurStats = condense(movementDurations);
13792
+ const segLenStats = condense(segmentLengths);
13793
+ return [
13794
+ curvatureStats.mean,
13795
+ curvatureStats.variance,
13796
+ curvatureStats.skewness,
13797
+ curvatureStats.kurtosis,
13798
+ dirEntropy,
13799
+ speedStats.mean,
13800
+ speedStats.variance,
13801
+ speedStats.skewness,
13802
+ speedStats.kurtosis,
13803
+ accStats.mean,
13804
+ accStats.variance,
13805
+ accStats.skewness,
13806
+ accStats.kurtosis,
13807
+ reversalRate,
13808
+ reversalMagnitude,
13809
+ pauseRatio,
13810
+ pathEfficiency,
13811
+ speedJitter,
13812
+ jerkStats.mean,
13813
+ jerkStats.variance,
13814
+ jerkStats.skewness,
13815
+ jerkStats.kurtosis,
13816
+ vxStats.mean,
13817
+ vxStats.variance,
13818
+ vxStats.skewness,
13819
+ vxStats.kurtosis,
13820
+ vyStats.mean,
13821
+ vyStats.variance,
13822
+ vyStats.skewness,
13823
+ vyStats.kurtosis,
13824
+ accXStats.mean,
13825
+ accXStats.variance,
13826
+ accXStats.skewness,
13827
+ accXStats.kurtosis,
13828
+ accYStats.mean,
13829
+ accYStats.variance,
13830
+ accYStats.skewness,
13831
+ accYStats.kurtosis,
13832
+ pressureStats.mean,
13833
+ pressureStats.variance,
13834
+ pressureStats.skewness,
13835
+ pressureStats.kurtosis,
13836
+ moveDurStats.mean,
13837
+ moveDurStats.variance,
13838
+ moveDurStats.skewness,
13839
+ moveDurStats.kurtosis,
13840
+ segLenStats.mean,
13841
+ segLenStats.variance,
13842
+ segLenStats.skewness,
13843
+ segLenStats.kurtosis,
13844
+ angleAutoCorr[0] ?? 0,
13845
+ angleAutoCorr[1] ?? 0,
13846
+ angleAutoCorr[2] ?? 0,
13847
+ normalizedPathLength
13848
+ ];
13849
+ }
13387
13850
 
13388
13851
  // src/hashing/simhash.ts
13389
13852
  function mulberry32(seed) {
@@ -13422,7 +13885,7 @@ function getHyperplanes(dimension) {
13422
13885
  cachedDimension = dimension;
13423
13886
  return planes;
13424
13887
  }
13425
- var EXPECTED_FEATURE_DIMENSION = 259;
13888
+ var EXPECTED_FEATURE_DIMENSION = 134;
13426
13889
  function simhash(features) {
13427
13890
  if (features.length === 0) {
13428
13891
  return new Array(FINGERPRINT_BITS).fill(0);
@@ -13808,8 +14271,9 @@ var inMemoryStore = null;
13808
14271
 
13809
14272
  // src/pulse.ts
13810
14273
  async function extractFeatures(data) {
13811
- const audioFeatures = data.audio ? await extractMFCC(data.audio) : new Array(169).fill(0);
13812
- const motionFeatures = extractMotionFeatures(data.motion);
14274
+ const audioFeatures = data.audio ? await extractSpeakerFeatures(data.audio) : new Array(SPEAKER_FEATURE_COUNT).fill(0);
14275
+ const hasMotion = data.motion.length >= MIN_MOTION_SAMPLES;
14276
+ const motionFeatures = hasMotion ? extractMotionFeatures(data.motion) : extractMouseDynamics(data.touch);
13813
14277
  const touchFeatures = extractTouchFeatures(data.touch);
13814
14278
  return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
13815
14279
  }
@@ -13840,6 +14304,10 @@ async function processSensorData(sensorData, config, wallet, connection) {
13840
14304
  };
13841
14305
  }
13842
14306
  const features = await extractFeatures(sensorData);
14307
+ const nonZero = features.filter((v) => v !== 0).length;
14308
+ console.log(
14309
+ `[IAM SDK] Feature vector: ${features.length} dimensions, ${nonZero} non-zero. Audio[0..43]: ${features.slice(0, 44).filter((v) => v !== 0).length} non-zero. Motion/Mouse[44..97]: ${features.slice(44, 98).filter((v) => v !== 0).length} non-zero. Touch[98..133]: ${features.slice(98, 134).filter((v) => v !== 0).length} non-zero.`
14310
+ );
13843
14311
  const fingerprint = simhash(features);
13844
14312
  const tbh = await generateTBH(fingerprint);
13845
14313
  const previousData = loadVerificationData();
@@ -13852,6 +14320,10 @@ async function processSensorData(sensorData, config, wallet, connection) {
13852
14320
  commitment: BigInt(previousData.commitment),
13853
14321
  commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
13854
14322
  };
14323
+ const distance = hammingDistance(fingerprint, previousData.fingerprint);
14324
+ console.log(
14325
+ `[IAM SDK] Re-verification: Hamming distance = ${distance} / 256 bits (threshold = ${config.threshold})`
14326
+ );
13855
14327
  const circuitInput = prepareCircuitInput(
13856
14328
  tbh,
13857
14329
  previousTBH,
@@ -14295,11 +14767,16 @@ export {
14295
14767
  PROGRAM_IDS,
14296
14768
  PulseSDK,
14297
14769
  PulseSession,
14770
+ SPEAKER_FEATURE_COUNT,
14298
14771
  autocorrelation,
14299
14772
  bigintToBytes32,
14300
14773
  computeCommitment,
14301
14774
  condense,
14302
14775
  entropy,
14776
+ extractMotionFeatures,
14777
+ extractMouseDynamics,
14778
+ extractSpeakerFeatures,
14779
+ extractTouchFeatures,
14303
14780
  fetchIdentityState,
14304
14781
  fuseFeatures,
14305
14782
  generateLissajousPoints,