@iam-protocol/pulse-sdk 0.2.5 → 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/dist/index.js CHANGED
@@ -12986,11 +12986,16 @@ __export(index_exports, {
12986
12986
  PROGRAM_IDS: () => PROGRAM_IDS,
12987
12987
  PulseSDK: () => PulseSDK,
12988
12988
  PulseSession: () => PulseSession,
12989
+ SPEAKER_FEATURE_COUNT: () => SPEAKER_FEATURE_COUNT,
12989
12990
  autocorrelation: () => autocorrelation,
12990
12991
  bigintToBytes32: () => bigintToBytes32,
12991
12992
  computeCommitment: () => computeCommitment,
12992
12993
  condense: () => condense,
12993
12994
  entropy: () => entropy,
12995
+ extractMotionFeatures: () => extractMotionFeatures,
12996
+ extractMouseDynamics: () => extractMouseDynamics,
12997
+ extractSpeakerFeatures: () => extractSpeakerFeatures,
12998
+ extractTouchFeatures: () => extractTouchFeatures,
12994
12999
  fetchIdentityState: () => fetchIdentityState,
12995
13000
  fuseFeatures: () => fuseFeatures,
12996
13001
  generateLissajousPoints: () => generateLissajousPoints,
@@ -13027,7 +13032,7 @@ var BN254_SCALAR_FIELD = BigInt(
13027
13032
  "21888242871839275222246405745257275088548364400416034343698204186575808495617"
13028
13033
  );
13029
13034
  var FINGERPRINT_BITS = 256;
13030
- var DEFAULT_THRESHOLD = 30;
13035
+ var DEFAULT_THRESHOLD = 96;
13031
13036
  var DEFAULT_MIN_DISTANCE = 3;
13032
13037
  var NUM_PUBLIC_INPUTS = 4;
13033
13038
  var PROOF_A_SIZE = 64;
@@ -13312,11 +13317,153 @@ function fuseFeatures(audio, motion, touch) {
13312
13317
  return [...audio, ...motion, ...touch];
13313
13318
  }
13314
13319
 
13315
- // src/extraction/mfcc.ts
13316
- var FRAME_SIZE = 400;
13320
+ // src/extraction/lpc.ts
13321
+ function autocorrelate(signal, order) {
13322
+ const r = [];
13323
+ for (let lag = 0; lag <= order; lag++) {
13324
+ let sum = 0;
13325
+ for (let i = 0; i < signal.length - lag; i++) {
13326
+ sum += signal[i] * signal[i + lag];
13327
+ }
13328
+ r.push(sum);
13329
+ }
13330
+ return r;
13331
+ }
13332
+ function levinsonDurbin(r, order) {
13333
+ const a = new Array(order + 1).fill(0);
13334
+ const aTemp = new Array(order + 1).fill(0);
13335
+ a[0] = 1;
13336
+ let error = r[0];
13337
+ if (error === 0) return new Array(order).fill(0);
13338
+ for (let i = 1; i <= order; i++) {
13339
+ let lambda = 0;
13340
+ for (let j = 1; j < i; j++) {
13341
+ lambda += a[j] * r[i - j];
13342
+ }
13343
+ lambda = -(r[i] + lambda) / error;
13344
+ for (let j = 1; j < i; j++) {
13345
+ aTemp[j] = a[j] + lambda * a[i - j];
13346
+ }
13347
+ aTemp[i] = lambda;
13348
+ for (let j = 1; j <= i; j++) {
13349
+ a[j] = aTemp[j];
13350
+ }
13351
+ error *= 1 - lambda * lambda;
13352
+ if (error <= 0) break;
13353
+ }
13354
+ return a.slice(1);
13355
+ }
13356
+ function findRoots(coefficients, maxIterations = 50) {
13357
+ const n = coefficients.length;
13358
+ if (n === 0) return [];
13359
+ const roots = [];
13360
+ for (let i = 0; i < n; i++) {
13361
+ const angle = 2 * Math.PI * i / n + 0.1;
13362
+ roots.push([0.9 * Math.cos(angle), 0.9 * Math.sin(angle)]);
13363
+ }
13364
+ for (let iter = 0; iter < maxIterations; iter++) {
13365
+ let maxShift = 0;
13366
+ for (let i = 0; i < n; i++) {
13367
+ let pReal = 1;
13368
+ let pImag = 0;
13369
+ let zPowReal = 1;
13370
+ let zPowImag = 0;
13371
+ const [rr, ri] = roots[i];
13372
+ let curReal = 1;
13373
+ let curImag = 0;
13374
+ let znReal = 1;
13375
+ let znImag = 0;
13376
+ for (let k = 0; k < n; k++) {
13377
+ const newReal = znReal * rr - znImag * ri;
13378
+ const newImag = znReal * ri + znImag * rr;
13379
+ znReal = newReal;
13380
+ znImag = newImag;
13381
+ }
13382
+ pReal = znReal;
13383
+ pImag = znImag;
13384
+ zPowReal = 1;
13385
+ zPowImag = 0;
13386
+ for (let k = n - 1; k >= 0; k--) {
13387
+ pReal += coefficients[k] * zPowReal;
13388
+ pImag += coefficients[k] * zPowImag;
13389
+ const newReal = zPowReal * rr - zPowImag * ri;
13390
+ const newImag = zPowReal * ri + zPowImag * rr;
13391
+ zPowReal = newReal;
13392
+ zPowImag = newImag;
13393
+ }
13394
+ let denomReal = 1;
13395
+ let denomImag = 0;
13396
+ for (let j = 0; j < n; j++) {
13397
+ if (j === i) continue;
13398
+ const diffReal = rr - roots[j][0];
13399
+ const diffImag = ri - roots[j][1];
13400
+ const newReal = denomReal * diffReal - denomImag * diffImag;
13401
+ const newImag = denomReal * diffImag + denomImag * diffReal;
13402
+ denomReal = newReal;
13403
+ denomImag = newImag;
13404
+ }
13405
+ const denomMag2 = denomReal * denomReal + denomImag * denomImag;
13406
+ if (denomMag2 < 1e-30) continue;
13407
+ const shiftReal = (pReal * denomReal + pImag * denomImag) / denomMag2;
13408
+ const shiftImag = (pImag * denomReal - pReal * denomImag) / denomMag2;
13409
+ roots[i] = [rr - shiftReal, ri - shiftImag];
13410
+ maxShift = Math.max(maxShift, Math.sqrt(shiftReal * shiftReal + shiftImag * shiftImag));
13411
+ }
13412
+ if (maxShift < 1e-10) break;
13413
+ }
13414
+ return roots;
13415
+ }
13416
+ function extractFormants(frame, sampleRate, lpcOrder = 12) {
13417
+ const r = autocorrelate(frame, lpcOrder);
13418
+ const coeffs = levinsonDurbin(r, lpcOrder);
13419
+ const roots = findRoots(coeffs);
13420
+ const formantCandidates = [];
13421
+ for (const [real, imag] of roots) {
13422
+ if (imag <= 0) continue;
13423
+ const freq = Math.atan2(imag, real) / (2 * Math.PI) * sampleRate;
13424
+ const bandwidth = -sampleRate / (2 * Math.PI) * Math.log(Math.sqrt(real * real + imag * imag));
13425
+ if (freq > 200 && freq < 5e3 && bandwidth < 500) {
13426
+ formantCandidates.push(freq);
13427
+ }
13428
+ }
13429
+ formantCandidates.sort((a, b) => a - b);
13430
+ if (formantCandidates.length < 3) return null;
13431
+ return [formantCandidates[0], formantCandidates[1], formantCandidates[2]];
13432
+ }
13433
+ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
13434
+ const f1f2 = [];
13435
+ const f2f3 = [];
13436
+ const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
13437
+ for (let i = 0; i < numFrames; i++) {
13438
+ const start = i * hopSize;
13439
+ const frame = samples.slice(start, start + frameSize);
13440
+ const windowed = new Float32Array(frameSize);
13441
+ for (let j = 0; j < frameSize; j++) {
13442
+ windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
13443
+ }
13444
+ const formants = extractFormants(windowed, sampleRate);
13445
+ if (formants) {
13446
+ const [f1, f2, f3] = formants;
13447
+ if (f2 > 0) f1f2.push(f1 / f2);
13448
+ if (f3 > 0) f2f3.push(f2 / f3);
13449
+ }
13450
+ }
13451
+ return { f1f2, f2f3 };
13452
+ }
13453
+
13454
+ // src/extraction/speaker.ts
13455
+ var FRAME_SIZE = 512;
13317
13456
  var HOP_SIZE = 160;
13318
- var NUM_MFCC = 13;
13457
+ var SPEAKER_FEATURE_COUNT = 44;
13458
+ var pitchDetector = null;
13319
13459
  var meydaModule = null;
13460
+ async function getPitchDetector() {
13461
+ if (!pitchDetector) {
13462
+ const PitchFinder = await import("pitchfinder");
13463
+ pitchDetector = PitchFinder.YIN({ sampleRate: 16e3 });
13464
+ }
13465
+ return pitchDetector;
13466
+ }
13320
13467
  async function getMeyda() {
13321
13468
  if (!meydaModule) {
13322
13469
  try {
@@ -13327,66 +13474,230 @@ async function getMeyda() {
13327
13474
  }
13328
13475
  return meydaModule.default ?? meydaModule;
13329
13476
  }
13330
- async function extractMFCC(audio) {
13331
- const { samples, sampleRate } = audio;
13332
- const Meyda = await getMeyda();
13333
- if (!Meyda) {
13334
- console.warn("[IAM SDK] Meyda library failed to load. Audio features will be zeros.");
13335
- return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13336
- }
13477
+ async function detectF0Contour(samples, sampleRate) {
13478
+ const detect = await getPitchDetector();
13479
+ const f0 = [];
13480
+ const amplitudes = [];
13481
+ const periods = [];
13337
13482
  const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13338
- if (numFrames < 3) {
13339
- console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Need at least 3.`);
13340
- return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13483
+ for (let i = 0; i < numFrames; i++) {
13484
+ const start = i * HOP_SIZE;
13485
+ const frame = samples.slice(start, start + FRAME_SIZE);
13486
+ const pitch = detect(frame);
13487
+ if (pitch && pitch > 50 && pitch < 600) {
13488
+ f0.push(pitch);
13489
+ periods.push(1 / pitch);
13490
+ } else {
13491
+ f0.push(0);
13492
+ }
13493
+ let sum = 0;
13494
+ for (let j = 0; j < frame.length; j++) {
13495
+ sum += (frame[j] ?? 0) * (frame[j] ?? 0);
13496
+ }
13497
+ amplitudes.push(Math.sqrt(sum / frame.length));
13498
+ }
13499
+ return { f0, amplitudes, periods };
13500
+ }
13501
+ function computeJitter(periods) {
13502
+ const voiced = periods.filter((p) => p > 0);
13503
+ if (voiced.length < 3) return [0, 0, 0, 0];
13504
+ const meanPeriod = voiced.reduce((a, b) => a + b, 0) / voiced.length;
13505
+ if (meanPeriod === 0) return [0, 0, 0, 0];
13506
+ let localSum = 0;
13507
+ for (let i = 1; i < voiced.length; i++) {
13508
+ localSum += Math.abs(voiced[i] - voiced[i - 1]);
13509
+ }
13510
+ const jitterLocal = localSum / (voiced.length - 1) / meanPeriod;
13511
+ let rapSum = 0;
13512
+ for (let i = 1; i < voiced.length - 1; i++) {
13513
+ const avg3 = (voiced[i - 1] + voiced[i] + voiced[i + 1]) / 3;
13514
+ rapSum += Math.abs(voiced[i] - avg3);
13515
+ }
13516
+ const jitterRAP = voiced.length > 2 ? rapSum / (voiced.length - 2) / meanPeriod : 0;
13517
+ let ppq5Sum = 0;
13518
+ let ppq5Count = 0;
13519
+ for (let i = 2; i < voiced.length - 2; i++) {
13520
+ const avg5 = (voiced[i - 2] + voiced[i - 1] + voiced[i] + voiced[i + 1] + voiced[i + 2]) / 5;
13521
+ ppq5Sum += Math.abs(voiced[i] - avg5);
13522
+ ppq5Count++;
13523
+ }
13524
+ const jitterPPQ5 = ppq5Count > 0 ? ppq5Sum / ppq5Count / meanPeriod : 0;
13525
+ let ddpSum = 0;
13526
+ for (let i = 1; i < voiced.length - 1; i++) {
13527
+ const d1 = voiced[i] - voiced[i - 1];
13528
+ const d2 = voiced[i + 1] - voiced[i];
13529
+ ddpSum += Math.abs(d2 - d1);
13530
+ }
13531
+ const jitterDDP = voiced.length > 2 ? ddpSum / (voiced.length - 2) / meanPeriod : 0;
13532
+ return [jitterLocal, jitterRAP, jitterPPQ5, jitterDDP];
13533
+ }
13534
+ function computeShimmer(amplitudes, f0) {
13535
+ const voicedAmps = amplitudes.filter((_, i) => f0[i] > 0);
13536
+ if (voicedAmps.length < 3) return [0, 0, 0, 0];
13537
+ const meanAmp = voicedAmps.reduce((a, b) => a + b, 0) / voicedAmps.length;
13538
+ if (meanAmp === 0) return [0, 0, 0, 0];
13539
+ let localSum = 0;
13540
+ for (let i = 1; i < voicedAmps.length; i++) {
13541
+ localSum += Math.abs(voicedAmps[i] - voicedAmps[i - 1]);
13542
+ }
13543
+ const shimmerLocal = localSum / (voicedAmps.length - 1) / meanAmp;
13544
+ let apq3Sum = 0;
13545
+ for (let i = 1; i < voicedAmps.length - 1; i++) {
13546
+ const avg3 = (voicedAmps[i - 1] + voicedAmps[i] + voicedAmps[i + 1]) / 3;
13547
+ apq3Sum += Math.abs(voicedAmps[i] - avg3);
13548
+ }
13549
+ const shimmerAPQ3 = voicedAmps.length > 2 ? apq3Sum / (voicedAmps.length - 2) / meanAmp : 0;
13550
+ let apq5Sum = 0;
13551
+ let apq5Count = 0;
13552
+ for (let i = 2; i < voicedAmps.length - 2; i++) {
13553
+ const avg5 = (voicedAmps[i - 2] + voicedAmps[i - 1] + voicedAmps[i] + voicedAmps[i + 1] + voicedAmps[i + 2]) / 5;
13554
+ apq5Sum += Math.abs(voicedAmps[i] - avg5);
13555
+ apq5Count++;
13556
+ }
13557
+ const shimmerAPQ5 = apq5Count > 0 ? apq5Sum / apq5Count / meanAmp : 0;
13558
+ let ddaSum = 0;
13559
+ for (let i = 1; i < voicedAmps.length - 1; i++) {
13560
+ const d1 = voicedAmps[i] - voicedAmps[i - 1];
13561
+ const d2 = voicedAmps[i + 1] - voicedAmps[i];
13562
+ ddaSum += Math.abs(d2 - d1);
13563
+ }
13564
+ const shimmerDDA = voicedAmps.length > 2 ? ddaSum / (voicedAmps.length - 2) / meanAmp : 0;
13565
+ return [shimmerLocal, shimmerAPQ3, shimmerAPQ5, shimmerDDA];
13566
+ }
13567
+ function computeHNR(samples, sampleRate, f0Contour) {
13568
+ const hnr = [];
13569
+ const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13570
+ for (let i = 0; i < numFrames && i < f0Contour.length; i++) {
13571
+ const f0 = f0Contour[i];
13572
+ if (f0 <= 0) continue;
13573
+ const start = i * HOP_SIZE;
13574
+ const frame = samples.slice(start, start + FRAME_SIZE);
13575
+ const period = Math.round(sampleRate / f0);
13576
+ if (period <= 0 || period >= frame.length) continue;
13577
+ let num = 0;
13578
+ let den = 0;
13579
+ for (let j = 0; j < frame.length - period; j++) {
13580
+ num += (frame[j] ?? 0) * (frame[j + period] ?? 0);
13581
+ den += (frame[j] ?? 0) * (frame[j] ?? 0);
13582
+ }
13583
+ if (den > 0) {
13584
+ const r = num / den;
13585
+ const clampedR = Math.max(1e-3, Math.min(0.999, r));
13586
+ hnr.push(10 * Math.log10(clampedR / (1 - clampedR)));
13587
+ }
13341
13588
  }
13342
- const mfccFrames = [];
13589
+ return hnr;
13590
+ }
13591
+ async function computeLTAS(samples, sampleRate) {
13592
+ const Meyda = await getMeyda();
13593
+ if (!Meyda) return new Array(8).fill(0);
13594
+ const centroids = [];
13595
+ const rolloffs = [];
13596
+ const flatnesses = [];
13597
+ const spreads = [];
13598
+ const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13343
13599
  for (let i = 0; i < numFrames; i++) {
13344
13600
  const start = i * HOP_SIZE;
13345
13601
  const frame = samples.slice(start, start + FRAME_SIZE);
13346
13602
  const paddedFrame = new Float32Array(FRAME_SIZE);
13347
13603
  paddedFrame.set(frame);
13348
- const features2 = Meyda.extract(["mfcc"], paddedFrame, {
13349
- sampleRate,
13350
- bufferSize: FRAME_SIZE,
13351
- numberOfMFCCCoefficients: NUM_MFCC
13352
- });
13353
- if (features2?.mfcc) {
13354
- mfccFrames.push(features2.mfcc);
13355
- }
13356
- }
13357
- if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13358
- const deltaFrames = computeDeltas(mfccFrames);
13359
- const deltaDeltaFrames = computeDeltas(deltaFrames);
13360
- const features = [];
13361
- for (let c = 0; c < NUM_MFCC; c++) {
13362
- const raw = mfccFrames.map((f) => f[c] ?? 0);
13363
- const stats = condense(raw);
13364
- features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
13365
- }
13366
- for (let c = 0; c < NUM_MFCC; c++) {
13367
- const delta = deltaFrames.map((f) => f[c] ?? 0);
13368
- const stats = condense(delta);
13369
- features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
13370
- }
13371
- for (let c = 0; c < NUM_MFCC; c++) {
13372
- const dd = deltaDeltaFrames.map((f) => f[c] ?? 0);
13373
- const stats = condense(dd);
13374
- features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
13375
- }
13376
- for (let c = 0; c < NUM_MFCC; c++) {
13377
- const raw = mfccFrames.map((f) => f[c] ?? 0);
13378
- features.push(entropy(raw));
13379
- }
13380
- return features;
13604
+ const features = Meyda.extract(
13605
+ ["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
13606
+ paddedFrame,
13607
+ { sampleRate, bufferSize: FRAME_SIZE }
13608
+ );
13609
+ if (features) {
13610
+ if (typeof features.spectralCentroid === "number") centroids.push(features.spectralCentroid);
13611
+ if (typeof features.spectralRolloff === "number") rolloffs.push(features.spectralRolloff);
13612
+ if (typeof features.spectralFlatness === "number") flatnesses.push(features.spectralFlatness);
13613
+ if (typeof features.spectralSpread === "number") spreads.push(features.spectralSpread);
13614
+ }
13615
+ }
13616
+ const m = (arr) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
13617
+ const v = (arr) => {
13618
+ if (arr.length < 2) return 0;
13619
+ const mu = m(arr);
13620
+ return arr.reduce((sum, x) => sum + (x - mu) * (x - mu), 0) / (arr.length - 1);
13621
+ };
13622
+ return [
13623
+ m(centroids),
13624
+ v(centroids),
13625
+ m(rolloffs),
13626
+ v(rolloffs),
13627
+ m(flatnesses),
13628
+ v(flatnesses),
13629
+ m(spreads),
13630
+ v(spreads)
13631
+ ];
13381
13632
  }
13382
- function computeDeltas(frames) {
13383
- const deltas = [];
13384
- for (let i = 1; i < frames.length; i++) {
13385
- const prev = frames[i - 1];
13386
- const curr = frames[i];
13387
- deltas.push(curr.map((v, j) => v - (prev[j] ?? 0)));
13633
+ function derivative(values) {
13634
+ const d = [];
13635
+ for (let i = 1; i < values.length; i++) {
13636
+ d.push(values[i] - values[i - 1]);
13388
13637
  }
13389
- return deltas;
13638
+ return d;
13639
+ }
13640
+ async function extractSpeakerFeatures(audio) {
13641
+ const { samples, sampleRate } = audio;
13642
+ const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13643
+ if (numFrames < 5) {
13644
+ console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Speaker features will be zeros.`);
13645
+ return new Array(SPEAKER_FEATURE_COUNT).fill(0);
13646
+ }
13647
+ const { f0, amplitudes, periods } = await detectF0Contour(samples, sampleRate);
13648
+ const voicedF0 = f0.filter((v) => v > 0);
13649
+ const voicedRatio = voicedF0.length / f0.length;
13650
+ const f0Stats = condense(voicedF0);
13651
+ const f0Entropy = entropy(voicedF0);
13652
+ const f0Features = [f0Stats.mean, f0Stats.variance, f0Stats.skewness, f0Stats.kurtosis, f0Entropy];
13653
+ const f0Delta = derivative(voicedF0);
13654
+ const f0DeltaStats = condense(f0Delta);
13655
+ const f0DeltaFeatures = [f0DeltaStats.mean, f0DeltaStats.variance, f0DeltaStats.skewness, f0DeltaStats.kurtosis];
13656
+ const jitterFeatures = computeJitter(periods);
13657
+ const shimmerFeatures = computeShimmer(amplitudes, f0);
13658
+ const hnrValues = computeHNR(samples, sampleRate, f0);
13659
+ const hnrStats = condense(hnrValues);
13660
+ const hnrEntropy = entropy(hnrValues);
13661
+ const hnrFeatures = [hnrStats.mean, hnrStats.variance, hnrStats.skewness, hnrStats.kurtosis, hnrEntropy];
13662
+ const { f1f2, f2f3 } = extractFormantRatios(samples, sampleRate, FRAME_SIZE, HOP_SIZE);
13663
+ const f1f2Stats = condense(f1f2);
13664
+ const f2f3Stats = condense(f2f3);
13665
+ const formantFeatures = [
13666
+ f1f2Stats.mean,
13667
+ f1f2Stats.variance,
13668
+ f1f2Stats.skewness,
13669
+ f1f2Stats.kurtosis,
13670
+ f2f3Stats.mean,
13671
+ f2f3Stats.variance,
13672
+ f2f3Stats.skewness,
13673
+ f2f3Stats.kurtosis
13674
+ ];
13675
+ const ltasFeatures = await computeLTAS(samples, sampleRate);
13676
+ const voicingFeatures = [voicedRatio];
13677
+ const ampStats = condense(amplitudes);
13678
+ const ampEntropy = entropy(amplitudes);
13679
+ const ampFeatures = [ampStats.mean, ampStats.variance, ampStats.skewness, ampStats.kurtosis, ampEntropy];
13680
+ const features = [
13681
+ ...f0Features,
13682
+ // 5
13683
+ ...f0DeltaFeatures,
13684
+ // 4
13685
+ ...jitterFeatures,
13686
+ // 4
13687
+ ...shimmerFeatures,
13688
+ // 4
13689
+ ...hnrFeatures,
13690
+ // 5
13691
+ ...formantFeatures,
13692
+ // 8
13693
+ ...ltasFeatures,
13694
+ // 8
13695
+ ...voicingFeatures,
13696
+ // 1
13697
+ ...ampFeatures
13698
+ // 5
13699
+ ];
13700
+ return features;
13390
13701
  }
13391
13702
 
13392
13703
  // src/extraction/kinematic.ts
@@ -13402,8 +13713,8 @@ function extractMotionFeatures(samples) {
13402
13713
  };
13403
13714
  const features = [];
13404
13715
  for (const values of Object.values(axes)) {
13405
- const jerk = derivative(values);
13406
- const jounce = derivative(jerk);
13716
+ const jerk = derivative2(values);
13717
+ const jounce = derivative2(jerk);
13407
13718
  const jerkStats = condense(jerk);
13408
13719
  const jounceStats = condense(jounce);
13409
13720
  features.push(
@@ -13418,7 +13729,7 @@ function extractMotionFeatures(samples) {
13418
13729
  );
13419
13730
  }
13420
13731
  for (const values of Object.values(axes)) {
13421
- const jerk = derivative(values);
13732
+ const jerk = derivative2(values);
13422
13733
  const windowSize = Math.max(5, Math.floor(jerk.length / 4));
13423
13734
  const windowVariances = [];
13424
13735
  for (let i = 0; i <= jerk.length - windowSize; i += windowSize) {
@@ -13435,18 +13746,18 @@ function extractTouchFeatures(samples) {
13435
13746
  const pressure = samples.map((s) => s.pressure);
13436
13747
  const area = samples.map((s) => s.width * s.height);
13437
13748
  const features = [];
13438
- const vx = derivative(x);
13439
- const accX = derivative(vx);
13749
+ const vx = derivative2(x);
13750
+ const accX = derivative2(vx);
13440
13751
  features.push(...Object.values(condense(vx)));
13441
13752
  features.push(...Object.values(condense(accX)));
13442
- const vy = derivative(y);
13443
- const accY = derivative(vy);
13753
+ const vy = derivative2(y);
13754
+ const accY = derivative2(vy);
13444
13755
  features.push(...Object.values(condense(vy)));
13445
13756
  features.push(...Object.values(condense(accY)));
13446
13757
  features.push(...Object.values(condense(pressure)));
13447
13758
  features.push(...Object.values(condense(area)));
13448
- const jerkX = derivative(accX);
13449
- const jerkY = derivative(accY);
13759
+ const jerkX = derivative2(accX);
13760
+ const jerkY = derivative2(accY);
13450
13761
  features.push(...Object.values(condense(jerkX)));
13451
13762
  features.push(...Object.values(condense(jerkY)));
13452
13763
  for (const values of [vx, vy, pressure, area]) {
@@ -13459,13 +13770,170 @@ function extractTouchFeatures(samples) {
13459
13770
  }
13460
13771
  return features;
13461
13772
  }
13462
- function derivative(values) {
13773
+ function derivative2(values) {
13463
13774
  const d = [];
13464
13775
  for (let i = 1; i < values.length; i++) {
13465
13776
  d.push((values[i] ?? 0) - (values[i - 1] ?? 0));
13466
13777
  }
13467
13778
  return d;
13468
13779
  }
13780
+ function extractMouseDynamics(samples) {
13781
+ if (samples.length < 10) return new Array(54).fill(0);
13782
+ const x = samples.map((s) => s.x);
13783
+ const y = samples.map((s) => s.y);
13784
+ const pressure = samples.map((s) => s.pressure);
13785
+ const area = samples.map((s) => s.width * s.height);
13786
+ const vx = derivative2(x);
13787
+ const vy = derivative2(y);
13788
+ const speed = vx.map((dx, i) => Math.sqrt(dx * dx + (vy[i] ?? 0) * (vy[i] ?? 0)));
13789
+ const accX = derivative2(vx);
13790
+ const accY = derivative2(vy);
13791
+ const acc = accX.map((ax, i) => Math.sqrt(ax * ax + (accY[i] ?? 0) * (accY[i] ?? 0)));
13792
+ const jerkX = derivative2(accX);
13793
+ const jerkY = derivative2(accY);
13794
+ const jerk = jerkX.map((jx, i) => Math.sqrt(jx * jx + (jerkY[i] ?? 0) * (jerkY[i] ?? 0)));
13795
+ const curvatures = [];
13796
+ for (let i = 1; i < vx.length; i++) {
13797
+ const angle1 = Math.atan2(vy[i - 1] ?? 0, vx[i - 1] ?? 0);
13798
+ const angle2 = Math.atan2(vy[i] ?? 0, vx[i] ?? 0);
13799
+ let diff = angle2 - angle1;
13800
+ while (diff > Math.PI) diff -= 2 * Math.PI;
13801
+ while (diff < -Math.PI) diff += 2 * Math.PI;
13802
+ curvatures.push(Math.abs(diff));
13803
+ }
13804
+ const directions = vx.map((dx, i) => Math.atan2(vy[i] ?? 0, dx));
13805
+ let reversals = 0;
13806
+ for (let i = 2; i < directions.length; i++) {
13807
+ const d1 = directions[i - 1] - directions[i - 2];
13808
+ const d2 = directions[i] - directions[i - 1];
13809
+ if (d1 * d2 < 0) reversals++;
13810
+ }
13811
+ const reversalRate = directions.length > 2 ? reversals / (directions.length - 2) : 0;
13812
+ const reversalMagnitude = curvatures.length > 0 ? curvatures.reduce((a, b) => a + b, 0) / curvatures.length : 0;
13813
+ const speedThreshold = 0.5;
13814
+ const pauseFrames = speed.filter((s) => s < speedThreshold).length;
13815
+ const pauseRatio = speed.length > 0 ? pauseFrames / speed.length : 0;
13816
+ const totalPathLength = speed.reduce((a, b) => a + b, 0);
13817
+ const straightLine = Math.sqrt(
13818
+ (x[x.length - 1] - x[0]) ** 2 + (y[y.length - 1] - y[0]) ** 2
13819
+ );
13820
+ const pathEfficiency = totalPathLength > 0 ? straightLine / totalPathLength : 0;
13821
+ const movementDurations = [];
13822
+ let currentDuration = 0;
13823
+ for (const s of speed) {
13824
+ if (s >= speedThreshold) {
13825
+ currentDuration++;
13826
+ } else if (currentDuration > 0) {
13827
+ movementDurations.push(currentDuration);
13828
+ currentDuration = 0;
13829
+ }
13830
+ }
13831
+ if (currentDuration > 0) movementDurations.push(currentDuration);
13832
+ const segmentLengths = [];
13833
+ let segLen = 0;
13834
+ for (let i = 1; i < directions.length; i++) {
13835
+ segLen += speed[i] ?? 0;
13836
+ const angleDiff = Math.abs(directions[i] - directions[i - 1]);
13837
+ if (angleDiff > Math.PI / 4) {
13838
+ segmentLengths.push(segLen);
13839
+ segLen = 0;
13840
+ }
13841
+ }
13842
+ if (segLen > 0) segmentLengths.push(segLen);
13843
+ const windowSize = Math.max(5, Math.floor(speed.length / 4));
13844
+ const windowVariances = [];
13845
+ for (let i = 0; i + windowSize <= speed.length; i += windowSize) {
13846
+ const window2 = speed.slice(i, i + windowSize);
13847
+ windowVariances.push(variance(window2));
13848
+ }
13849
+ const speedJitter = windowVariances.length > 1 ? variance(windowVariances) : 0;
13850
+ const duration = samples.length > 1 ? (samples[samples.length - 1].timestamp - samples[0].timestamp) / 1e3 : 1;
13851
+ const normalizedPathLength = totalPathLength / Math.max(duration, 1e-3);
13852
+ const angleAutoCorr = [];
13853
+ for (let lag = 1; lag <= 3; lag++) {
13854
+ if (directions.length <= lag) {
13855
+ angleAutoCorr.push(0);
13856
+ continue;
13857
+ }
13858
+ const n = directions.length - lag;
13859
+ const meanDir = directions.reduce((a, b) => a + b, 0) / directions.length;
13860
+ let num = 0;
13861
+ let den = 0;
13862
+ for (let i = 0; i < n; i++) {
13863
+ num += (directions[i] - meanDir) * (directions[i + lag] - meanDir);
13864
+ den += (directions[i] - meanDir) ** 2;
13865
+ }
13866
+ angleAutoCorr.push(den > 0 ? num / den : 0);
13867
+ }
13868
+ const curvatureStats = condense(curvatures);
13869
+ const dirEntropy = entropy(directions, 16);
13870
+ const speedStats = condense(speed);
13871
+ const accStats = condense(acc);
13872
+ const jerkStats = condense(jerk);
13873
+ const vxStats = condense(vx);
13874
+ const vyStats = condense(vy);
13875
+ const accXStats = condense(accX);
13876
+ const accYStats = condense(accY);
13877
+ const pressureStats = condense(pressure);
13878
+ const moveDurStats = condense(movementDurations);
13879
+ const segLenStats = condense(segmentLengths);
13880
+ return [
13881
+ curvatureStats.mean,
13882
+ curvatureStats.variance,
13883
+ curvatureStats.skewness,
13884
+ curvatureStats.kurtosis,
13885
+ dirEntropy,
13886
+ speedStats.mean,
13887
+ speedStats.variance,
13888
+ speedStats.skewness,
13889
+ speedStats.kurtosis,
13890
+ accStats.mean,
13891
+ accStats.variance,
13892
+ accStats.skewness,
13893
+ accStats.kurtosis,
13894
+ reversalRate,
13895
+ reversalMagnitude,
13896
+ pauseRatio,
13897
+ pathEfficiency,
13898
+ speedJitter,
13899
+ jerkStats.mean,
13900
+ jerkStats.variance,
13901
+ jerkStats.skewness,
13902
+ jerkStats.kurtosis,
13903
+ vxStats.mean,
13904
+ vxStats.variance,
13905
+ vxStats.skewness,
13906
+ vxStats.kurtosis,
13907
+ vyStats.mean,
13908
+ vyStats.variance,
13909
+ vyStats.skewness,
13910
+ vyStats.kurtosis,
13911
+ accXStats.mean,
13912
+ accXStats.variance,
13913
+ accXStats.skewness,
13914
+ accXStats.kurtosis,
13915
+ accYStats.mean,
13916
+ accYStats.variance,
13917
+ accYStats.skewness,
13918
+ accYStats.kurtosis,
13919
+ pressureStats.mean,
13920
+ pressureStats.variance,
13921
+ pressureStats.skewness,
13922
+ pressureStats.kurtosis,
13923
+ moveDurStats.mean,
13924
+ moveDurStats.variance,
13925
+ moveDurStats.skewness,
13926
+ moveDurStats.kurtosis,
13927
+ segLenStats.mean,
13928
+ segLenStats.variance,
13929
+ segLenStats.skewness,
13930
+ segLenStats.kurtosis,
13931
+ angleAutoCorr[0] ?? 0,
13932
+ angleAutoCorr[1] ?? 0,
13933
+ angleAutoCorr[2] ?? 0,
13934
+ normalizedPathLength
13935
+ ];
13936
+ }
13469
13937
 
13470
13938
  // src/hashing/simhash.ts
13471
13939
  function mulberry32(seed) {
@@ -13504,7 +13972,7 @@ function getHyperplanes(dimension) {
13504
13972
  cachedDimension = dimension;
13505
13973
  return planes;
13506
13974
  }
13507
- var EXPECTED_FEATURE_DIMENSION = 259;
13975
+ var EXPECTED_FEATURE_DIMENSION = 134;
13508
13976
  function simhash(features) {
13509
13977
  if (features.length === 0) {
13510
13978
  return new Array(FINGERPRINT_BITS).fill(0);
@@ -13890,8 +14358,9 @@ var inMemoryStore = null;
13890
14358
 
13891
14359
  // src/pulse.ts
13892
14360
  async function extractFeatures(data) {
13893
- const audioFeatures = data.audio ? await extractMFCC(data.audio) : new Array(169).fill(0);
13894
- const motionFeatures = extractMotionFeatures(data.motion);
14361
+ const audioFeatures = data.audio ? await extractSpeakerFeatures(data.audio) : new Array(SPEAKER_FEATURE_COUNT).fill(0);
14362
+ const hasMotion = data.motion.length >= MIN_MOTION_SAMPLES;
14363
+ const motionFeatures = hasMotion ? extractMotionFeatures(data.motion) : extractMouseDynamics(data.touch);
13895
14364
  const touchFeatures = extractTouchFeatures(data.touch);
13896
14365
  return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
13897
14366
  }
@@ -13934,6 +14403,10 @@ async function processSensorData(sensorData, config, wallet, connection) {
13934
14403
  commitment: BigInt(previousData.commitment),
13935
14404
  commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
13936
14405
  };
14406
+ const distance = hammingDistance(fingerprint, previousData.fingerprint);
14407
+ console.log(
14408
+ `[IAM SDK] Re-verification: Hamming distance = ${distance} / 256 bits (threshold = ${config.threshold})`
14409
+ );
13937
14410
  const circuitInput = prepareCircuitInput(
13938
14411
  tbh,
13939
14412
  previousTBH,
@@ -14378,11 +14851,16 @@ function generateLissajousSequence(count = 2) {
14378
14851
  PROGRAM_IDS,
14379
14852
  PulseSDK,
14380
14853
  PulseSession,
14854
+ SPEAKER_FEATURE_COUNT,
14381
14855
  autocorrelation,
14382
14856
  bigintToBytes32,
14383
14857
  computeCommitment,
14384
14858
  condense,
14385
14859
  entropy,
14860
+ extractMotionFeatures,
14861
+ extractMouseDynamics,
14862
+ extractSpeakerFeatures,
14863
+ extractTouchFeatures,
14386
14864
  fetchIdentityState,
14387
14865
  fuseFeatures,
14388
14866
  generateLissajousPoints,