@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/dist/index.d.mts +52 -2
- package/dist/index.d.ts +52 -2
- package/dist/index.js +545 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +540 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/config.ts +1 -1
- package/src/extraction/kinematic.ts +171 -1
- package/src/extraction/lpc.ts +215 -0
- package/src/extraction/speaker.ts +361 -0
- package/src/hashing/simhash.ts +1 -1
- package/src/index.ts +2 -0
- package/src/pulse.ts +16 -5
- package/test/integration.test.ts +2 -2
- package/src/extraction/mfcc.ts +0 -113
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 =
|
|
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/
|
|
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
|
|
13316
13455
|
var FRAME_SIZE = 512;
|
|
13317
13456
|
var HOP_SIZE = 160;
|
|
13318
|
-
var
|
|
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
|
|
13331
|
-
const
|
|
13332
|
-
const
|
|
13333
|
-
|
|
13334
|
-
|
|
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
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
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
|
-
|
|
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
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
if (
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
const
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
const
|
|
13364
|
-
|
|
13365
|
-
}
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
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
|
|
13383
|
-
const
|
|
13384
|
-
for (let i = 1; i <
|
|
13385
|
-
|
|
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
|
|
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 =
|
|
13406
|
-
const jounce =
|
|
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 =
|
|
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 =
|
|
13439
|
-
const accX =
|
|
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 =
|
|
13443
|
-
const accY =
|
|
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 =
|
|
13449
|
-
const jerkY =
|
|
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
|
|
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 =
|
|
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
|
|
13894
|
-
const
|
|
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,
|