@entros/pulse-sdk 2.0.0 → 3.0.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@entros/pulse-sdk.svg)](https://www.npmjs.com/package/@entros/pulse-sdk)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@entros/pulse-sdk.svg)](https://www.npmjs.com/package/@entros/pulse-sdk)
5
5
 
6
- Client-side SDK for the Entros Protocol. Captures behavioral biometrics (voice, motion, touch), extracts a 314-dimensional statistical feature vector (v2 expansion: MFCCs, LPC coefficients, formant trajectories, voice quality, pitch contour shape, IMU FFT-band tremor, cross-axis covariance, touch curvature, gap distribution, path efficiency — see `docs/master/BLUEPRINT-feature-pipeline-v2.md`), generates a Groth16 zero-knowledge proof, and submits for on-chain verification on Solana. Raw biometric data stays on-device — only derived features and the proof are transmitted.
6
+ Client-side SDK for the Entros Protocol. Captures behavioral biometrics (voice, motion, touch), extracts a 308-dimensional statistical feature vector (v3 expansion: MFCCs with pre-emphasis (C1-C12, MFCC[0] dropped), LPC coefficients, formant trajectories, voice quality, pitch contour shape, IMU FFT-band tremor, cross-axis covariance, mouse-derived FFT / autocorrelation analogues for desktop, touch curvature, gap distribution, path efficiency — see `docs/master/BLUEPRINT-feature-pipeline-v2.md`), generates a Groth16 zero-knowledge proof, and submits for on-chain verification on Solana. Raw biometric data stays on-device — only derived features and the proof are transmitted.
7
7
 
8
8
  > **Looking for a drop-in?** Most integrators want [`@entros/verify`](https://github.com/entros-protocol/entros-verify) — a popup-pattern React component that wraps this SDK and ships verification in five lines of JSX. Use this package directly when you need to own the verification UX (custom capture canvas, branded loading states, mobile-native).
9
9
 
@@ -50,7 +50,7 @@ const result = await pulse.verify(touchElement);
50
50
  ## Pipeline
51
51
 
52
52
  1. **Capture**: Audio (16kHz), IMU (accelerometer + gyroscope), touch (pressure + area) — event-driven, caller controls duration
53
- 2. **Extract**: 314 features — speaker block (176): legacy F0 / jitter / shimmer / HNR / formant ratios / LTAS / amplitude (44) plus v2 additions: MFCCs + delta-MFCCs (78), LPC coefficient stats (24), formant absolute trajectories + bandwidths (16), voice quality CPP/tilt/H1-H2/sub-bands (9), pitch contour DCT (5). Motion block (81): legacy jerk + jounce per IMU axis (54) plus v2 additions: cross-axis covariance (6), FFT band energy on accel axes (12), physiological tremor peak (2), direction-reversal stats (3), motion-magnitude autocorrelation (4). Touch block (57): legacy velocity + pressure dynamics (36) plus v2 additions: pressure derivative (4), contact aspect ratio + area derivative (4), trajectory curvature (3), velocity autocorrelation (3), inter-touch gap distribution (4), path efficiency + per-stroke length (3).
53
+ 2. **Extract**: 308 features — speaker block (170): legacy F0 / jitter / shimmer / HNR / formant ratios / LTAS / amplitude (44) plus v3 additions: MFCCs + delta-MFCCs (72 — 12 used coefficients × 4 stats + 12 × 2 deltas, MFCC[0] dropped, pre-emphasis applied), LPC coefficient stats (24), formant absolute trajectories + bandwidths (16), voice quality CPP/tilt/H1-H2/sub-bands (9), pitch contour DCT (5). Motion block (81): legacy jerk + jounce per IMU axis (54) plus v2 additions: cross-axis covariance (6), FFT band energy on accel axes (12), physiological tremor peak (2), direction-reversal stats (3), motion-magnitude autocorrelation (4); desktop captures use mouse-derived analogues for these v2 additions. Touch block (57): legacy velocity + pressure dynamics (36) plus v2 additions: pressure derivative (4), contact aspect ratio + area derivative (4), trajectory curvature (3), velocity autocorrelation (3), inter-touch gap distribution (4), path efficiency + per-stroke length (3).
54
54
  3. **Validate**: Feature summaries sent to Entros validation server for server-side analysis
55
55
  4. **Hash**: SimHash → 256-bit Temporal Fingerprint → Poseidon commitment
56
56
  5. **Prove**: Groth16 proof that new fingerprint is within Hamming distance of previous
package/dist/index.js CHANGED
@@ -138,6 +138,25 @@ function sdkWarn(...args) {
138
138
 
139
139
  // src/sensor/audio.ts
140
140
  var TARGET_SAMPLE_RATE = 16e3;
141
+ var TARGET_CAPTURE_RMS = 0.05;
142
+ var MIN_RMS_FOR_NORMALIZATION = 1e-4;
143
+ var MAX_NORMALIZATION_GAIN = 50;
144
+ function normalizeCaptureRMS(samples) {
145
+ if (samples.length === 0) return samples;
146
+ let sumSq = 0;
147
+ for (let i = 0; i < samples.length; i++) {
148
+ const s = samples[i];
149
+ sumSq += s * s;
150
+ }
151
+ const rms = Math.sqrt(sumSq / samples.length);
152
+ if (rms < MIN_RMS_FOR_NORMALIZATION) return samples;
153
+ const gain = Math.min(TARGET_CAPTURE_RMS / rms, MAX_NORMALIZATION_GAIN);
154
+ const out = new Float32Array(samples.length);
155
+ for (let i = 0; i < samples.length; i++) {
156
+ out[i] = Math.max(-1, Math.min(1, samples[i] * gain));
157
+ }
158
+ return out;
159
+ }
141
160
  async function captureAudio(options = {}) {
142
161
  const {
143
162
  signal,
@@ -225,8 +244,9 @@ async function captureAudio(options = {}) {
225
244
  samples.set(chunk, offset);
226
245
  offset += chunk.length;
227
246
  }
247
+ const normalized = normalizeCaptureRMS(samples);
228
248
  resolve({
229
- samples,
249
+ samples: normalized,
230
250
  sampleRate: capturedSampleRate,
231
251
  duration: totalLength / capturedSampleRate
232
252
  });
@@ -401,6 +421,9 @@ function variance(values, mu) {
401
421
  for (const v of values) sum += (v - m) ** 2;
402
422
  return sum / (values.length - 1);
403
423
  }
424
+ var SKEWNESS_BOUND = 20;
425
+ var KURTOSIS_LOWER = 0;
426
+ var KURTOSIS_UPPER = 50;
404
427
  function skewness(values) {
405
428
  if (values.length < 3) return 0;
406
429
  const n = values.length;
@@ -409,7 +432,8 @@ function skewness(values) {
409
432
  if (s === 0) return 0;
410
433
  let sum = 0;
411
434
  for (const v of values) sum += ((v - m) / s) ** 3;
412
- return n / ((n - 1) * (n - 2)) * sum;
435
+ const raw = n / ((n - 1) * (n - 2)) * sum;
436
+ return Math.max(-SKEWNESS_BOUND, Math.min(SKEWNESS_BOUND, raw));
413
437
  }
414
438
  function kurtosis(values) {
415
439
  if (values.length < 4) return 0;
@@ -420,7 +444,7 @@ function kurtosis(values) {
420
444
  let sum = 0;
421
445
  for (const v of values) sum += (v - m) ** 4 / s2 ** 2;
422
446
  const k = n * (n + 1) / ((n - 1) * (n - 2) * (n - 3)) * sum - 3 * (n - 1) ** 2 / ((n - 2) * (n - 3));
423
- return k;
447
+ return Math.max(KURTOSIS_LOWER, Math.min(KURTOSIS_UPPER, k));
424
448
  }
425
449
  function condense(values) {
426
450
  const m = mean(values);
@@ -681,9 +705,20 @@ function extractLpcAnalysis(samples, sampleRate, frameSize, hopSize, lpcOrder =
681
705
 
682
706
  // src/extraction/mfcc.ts
683
707
  var NUM_MFCC_COEFFICIENTS = 13;
708
+ var MFCC_DROP_LEADING = 1;
709
+ var NUM_USED_MFCC = NUM_MFCC_COEFFICIENTS - MFCC_DROP_LEADING;
684
710
  var DELTA_REGRESSION_HALF_WIDTH = 2;
685
- var MFCC_FEATURE_COUNT = NUM_MFCC_COEFFICIENTS * 4 + // mean, var, skew, kurt per coefficient
686
- NUM_MFCC_COEFFICIENTS * 2;
711
+ var MFCC_FEATURE_COUNT = NUM_USED_MFCC * 4 + // mean, var, skew, kurt per coefficient
712
+ NUM_USED_MFCC * 2;
713
+ function applyPreEmphasis(samples) {
714
+ const out = new Float32Array(samples.length);
715
+ if (samples.length === 0) return out;
716
+ out[0] = samples[0];
717
+ for (let i = 1; i < samples.length; i++) {
718
+ out[i] = samples[i] - 0.97 * samples[i - 1];
719
+ }
720
+ return out;
721
+ }
687
722
  function computeDelta(series, halfWidth) {
688
723
  const n = series.length;
689
724
  const out = new Array(n);
@@ -733,15 +768,16 @@ async function extractMfccFeatures(samples, sampleRate, frameSize, hopSize) {
733
768
  return new Array(MFCC_FEATURE_COUNT).fill(0);
734
769
  }
735
770
  const mfccTracks = Array.from(
736
- { length: NUM_MFCC_COEFFICIENTS },
771
+ { length: NUM_USED_MFCC },
737
772
  () => []
738
773
  );
739
774
  const frame = new Float32Array(frameSize);
740
775
  Meyda.bufferSize = frameSize;
741
776
  Meyda.sampleRate = sampleRate;
777
+ const emphasized = applyPreEmphasis(samples);
742
778
  for (let i = 0; i < numFrames; i++) {
743
779
  const start = i * hopSize;
744
- frame.set(samples.subarray(start, start + frameSize), 0);
780
+ frame.set(emphasized.subarray(start, start + frameSize), 0);
745
781
  const result = Meyda.extract("mfcc", frame);
746
782
  if (!Array.isArray(result) || result.length !== NUM_MFCC_COEFFICIENTS) {
747
783
  continue;
@@ -754,21 +790,21 @@ async function extractMfccFeatures(samples, sampleRate, frameSize, hopSize) {
754
790
  }
755
791
  }
756
792
  if (!allFinite) continue;
757
- for (let c = 0; c < NUM_MFCC_COEFFICIENTS; c++) {
758
- mfccTracks[c].push(result[c]);
793
+ for (let c = 0; c < NUM_USED_MFCC; c++) {
794
+ mfccTracks[c].push(result[c + MFCC_DROP_LEADING]);
759
795
  }
760
796
  }
761
797
  const out = [];
762
798
  out.length = MFCC_FEATURE_COUNT;
763
799
  let writeIdx = 0;
764
- for (let c = 0; c < NUM_MFCC_COEFFICIENTS; c++) {
800
+ for (let c = 0; c < NUM_USED_MFCC; c++) {
765
801
  const stats = condense(mfccTracks[c]);
766
802
  out[writeIdx++] = stats.mean;
767
803
  out[writeIdx++] = stats.variance;
768
804
  out[writeIdx++] = stats.skewness;
769
805
  out[writeIdx++] = stats.kurtosis;
770
806
  }
771
- for (let c = 0; c < NUM_MFCC_COEFFICIENTS; c++) {
807
+ for (let c = 0; c < NUM_USED_MFCC; c++) {
772
808
  const delta = computeDelta(mfccTracks[c], DELTA_REGRESSION_HALF_WIDTH);
773
809
  const muDelta = mean(delta);
774
810
  out[writeIdx++] = muDelta;
@@ -1287,9 +1323,9 @@ async function extractSpeakerFeaturesDetailed(audio) {
1287
1323
  for (let i = 0; i < numFrames; i++) {
1288
1324
  const start = i * hopSize;
1289
1325
  let sum = 0;
1290
- const end = Math.min(start + frameSize, samples.length);
1326
+ const end = Math.min(start + frameSize, normalizedSamples.length);
1291
1327
  for (let j = start; j < end; j++) {
1292
- sum += (samples[j] ?? 0) * (samples[j] ?? 0);
1328
+ sum += (normalizedSamples[j] ?? 0) * (normalizedSamples[j] ?? 0);
1293
1329
  }
1294
1330
  amplitudes.push(Math.sqrt(sum / (end - start)));
1295
1331
  }
@@ -1398,15 +1434,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
1398
1434
  ...ampFeatures,
1399
1435
  // 5 [39..44] AMPLITUDE
1400
1436
  ...mfccFeatures,
1401
- // 78 [44..122] MFCC + delta-MFCC
1437
+ // 72 [44..116] MFCC + delta-MFCC (MFCC[0] dropped)
1402
1438
  ...lpcStats,
1403
- // 24 [122..146] LPC coefficient stats
1439
+ // 24 [116..140] LPC coefficient stats
1404
1440
  ...formantTrajectoryFeatures,
1405
- // 16 [146..162] Formant absolutes + dynamics + bandwidths
1441
+ // 16 [140..156] Formant absolutes + dynamics + bandwidths
1406
1442
  ...voiceQualityFeatures,
1407
- // 9 [162..171] Voice quality
1443
+ // 9 [156..165] Voice quality
1408
1444
  ...pitchShapeFeatures
1409
- // 5 [171..176] Pitch contour shape DCT
1445
+ // 5 [165..170] Pitch contour shape DCT
1410
1446
  ];
1411
1447
  return { features, f0Contour: f0 };
1412
1448
  }
@@ -1932,8 +1968,61 @@ function extractMouseDynamics(samples) {
1932
1968
  angleAutoCorr[2] ?? 0,
1933
1969
  normalizedPathLength
1934
1970
  ];
1935
- const padding = MOUSE_DYNAMICS_FEATURE_COUNT - legacyMouseDynamics.length;
1936
- return padding > 0 ? [...legacyMouseDynamics, ...new Array(padding).fill(0)] : legacyMouseDynamics;
1971
+ const v2 = computeMouseV2(samples, vx, vy, accX, accY, speed, acc, jerk, directions);
1972
+ return [...legacyMouseDynamics, ...v2];
1973
+ }
1974
+ function computeMouseV2(samples, vx, vy, accX, accY, speed, acc, jerk, directions) {
1975
+ const out = [];
1976
+ const covPairs = [
1977
+ [vx, vy],
1978
+ [vx, accX],
1979
+ [vx, accY],
1980
+ [vy, accX],
1981
+ [vy, accY],
1982
+ [accX, accY]
1983
+ ];
1984
+ for (const [a, b] of covPairs) out.push(covariance(a, b));
1985
+ const sampleRate = sampleRateFromTimestamps(samples.map((s) => s.timestamp));
1986
+ const fftSize = nextPow2(Math.max(64, speed.length));
1987
+ const bands = [
1988
+ [0, 2],
1989
+ [2, 6],
1990
+ [6, 12],
1991
+ [12, 30]
1992
+ ];
1993
+ const speedSpectrum = realFFT(meanCenter(speed), fftSize);
1994
+ const accSpectrum = realFFT(meanCenter(acc), fftSize);
1995
+ const jerkSpectrum = realFFT(meanCenter(jerk), fftSize);
1996
+ for (const spectrum of [speedSpectrum, accSpectrum, jerkSpectrum]) {
1997
+ for (const [lo, hi] of bands) {
1998
+ out.push(bandEnergy(spectrum.real, spectrum.imag, sampleRate, lo, hi));
1999
+ }
2000
+ }
2001
+ const tremor = peakInBand(
2002
+ speedSpectrum.real,
2003
+ speedSpectrum.imag,
2004
+ sampleRate,
2005
+ 4,
2006
+ 12
2007
+ );
2008
+ out.push(tremor.freq, tremor.amplitude);
2009
+ const duration = captureDurationSec(samples);
2010
+ const reversalRates = [vx, vy, speed].map(
2011
+ (channel) => duration > 0 ? signChangeCount(derivative2(channel)) / duration : 0
2012
+ );
2013
+ out.push(mean(reversalRates), variance(reversalRates));
2014
+ let dirAccum = 0;
2015
+ for (let i = 1; i < directions.length; i++) {
2016
+ let diff = directions[i] - directions[i - 1];
2017
+ while (diff > Math.PI) diff -= 2 * Math.PI;
2018
+ while (diff < -Math.PI) diff += 2 * Math.PI;
2019
+ dirAccum += Math.abs(diff);
2020
+ }
2021
+ out.push(directions.length > 1 ? dirAccum / (directions.length - 1) : 0);
2022
+ for (const lag of [1, 5, 10, 25]) {
2023
+ out.push(autocorrelation(speed, lag));
2024
+ }
2025
+ return out.map((v) => Number.isFinite(v) ? v : 0);
1937
2026
  }
1938
2027
 
1939
2028
  // src/hashing/simhash.ts