@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.d.mts +52 -2
- package/dist/index.d.ts +52 -2
- package/dist/index.js +549 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +544 -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 +25 -5
- package/test/integration.test.ts +2 -2
- package/src/extraction/mfcc.ts +0 -113
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 =
|
|
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/
|
|
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
|
|
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
|
|
13249
|
-
const
|
|
13250
|
-
const
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
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
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
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
|
-
|
|
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
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
if (
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
const
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
const
|
|
13282
|
-
|
|
13283
|
-
}
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
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
|
|
13301
|
-
const
|
|
13302
|
-
for (let i = 1; i <
|
|
13303
|
-
|
|
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
|
|
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 =
|
|
13324
|
-
const jounce =
|
|
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 =
|
|
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 =
|
|
13357
|
-
const accX =
|
|
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 =
|
|
13361
|
-
const accY =
|
|
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 =
|
|
13367
|
-
const jerkY =
|
|
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
|
|
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 =
|
|
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
|
|
13812
|
-
const
|
|
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,
|