@iam-protocol/pulse-sdk 0.3.8 → 0.4.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 +22 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/extraction/speaker.ts +10 -8
- package/src/extraction/statistics.ts +11 -6
- package/src/pulse.ts +6 -1
package/package.json
CHANGED
|
@@ -20,12 +20,14 @@ const SPEAKER_FEATURE_COUNT = 44;
|
|
|
20
20
|
|
|
21
21
|
// Dynamic imports for browser compatibility
|
|
22
22
|
let pitchDetector: ((buf: Float32Array) => number | null) | null = null;
|
|
23
|
+
let pitchDetectorRate = 0;
|
|
23
24
|
let meydaModule: any = null;
|
|
24
25
|
|
|
25
|
-
async function getPitchDetector(): Promise<(buf: Float32Array) => number | null> {
|
|
26
|
-
if (!pitchDetector) {
|
|
26
|
+
async function getPitchDetector(sampleRate: number): Promise<(buf: Float32Array) => number | null> {
|
|
27
|
+
if (!pitchDetector || pitchDetectorRate !== sampleRate) {
|
|
27
28
|
const PitchFinder = await import("pitchfinder");
|
|
28
|
-
pitchDetector = PitchFinder.YIN({ sampleRate
|
|
29
|
+
pitchDetector = PitchFinder.YIN({ sampleRate });
|
|
30
|
+
pitchDetectorRate = sampleRate;
|
|
29
31
|
}
|
|
30
32
|
return pitchDetector;
|
|
31
33
|
}
|
|
@@ -48,7 +50,7 @@ async function detectF0Contour(
|
|
|
48
50
|
samples: Float32Array,
|
|
49
51
|
sampleRate: number
|
|
50
52
|
): Promise<{ f0: number[]; amplitudes: number[]; periods: number[] }> {
|
|
51
|
-
const detect = await getPitchDetector();
|
|
53
|
+
const detect = await getPitchDetector(sampleRate);
|
|
52
54
|
const f0: number[] = [];
|
|
53
55
|
const amplitudes: number[] = [];
|
|
54
56
|
const periods: number[] = [];
|
|
@@ -244,10 +246,10 @@ async function computeLTAS(
|
|
|
244
246
|
);
|
|
245
247
|
|
|
246
248
|
if (features) {
|
|
247
|
-
if (
|
|
248
|
-
if (
|
|
249
|
-
if (
|
|
250
|
-
if (
|
|
249
|
+
if (Number.isFinite(features.spectralCentroid)) centroids.push(features.spectralCentroid);
|
|
250
|
+
if (Number.isFinite(features.spectralRolloff)) rolloffs.push(features.spectralRolloff);
|
|
251
|
+
if (Number.isFinite(features.spectralFlatness)) flatnesses.push(features.spectralFlatness);
|
|
252
|
+
if (Number.isFinite(features.spectralSpread)) spreads.push(features.spectralSpread);
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -108,16 +108,21 @@ export function autocorrelation(values: number[], lag: number = 1): number {
|
|
|
108
108
|
function normalizeGroup(features: number[]): number[] {
|
|
109
109
|
if (features.length === 0) return features;
|
|
110
110
|
|
|
111
|
+
// Sanitize NaN/Infinity to 0 before computing stats.
|
|
112
|
+
// Meyda spectral features can produce NaN on near-silent frames (0/0),
|
|
113
|
+
// and a single NaN would poison the entire modality group.
|
|
114
|
+
const clean = features.map((v) => (Number.isFinite(v) ? v : 0));
|
|
115
|
+
|
|
111
116
|
let sum = 0;
|
|
112
|
-
for (const v of
|
|
113
|
-
const mean = sum /
|
|
117
|
+
for (const v of clean) sum += v;
|
|
118
|
+
const mean = sum / clean.length;
|
|
114
119
|
|
|
115
120
|
let sqSum = 0;
|
|
116
|
-
for (const v of
|
|
117
|
-
const std = Math.sqrt(sqSum /
|
|
121
|
+
for (const v of clean) sqSum += (v - mean) * (v - mean);
|
|
122
|
+
const std = Math.sqrt(sqSum / clean.length);
|
|
118
123
|
|
|
119
|
-
if (std === 0) return
|
|
120
|
-
return
|
|
124
|
+
if (std === 0) return clean.map(() => 0);
|
|
125
|
+
return clean.map((v) => (v - mean) / std);
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
export function fuseFeatures(
|
package/src/pulse.ts
CHANGED
|
@@ -181,11 +181,16 @@ async function processSensorData(
|
|
|
181
181
|
const audioNZ = features.slice(0, 44).filter((v) => v !== 0).length;
|
|
182
182
|
const motionNZ = features.slice(44, 98).filter((v) => v !== 0).length;
|
|
183
183
|
const touchNZ = features.slice(98, 134).filter((v) => v !== 0).length;
|
|
184
|
+
const rawAudio = sensorData.audio?.samples.length ?? 0;
|
|
185
|
+
const rawMotion = sensorData.motion.length;
|
|
186
|
+
const rawTouch = sensorData.touch.length;
|
|
187
|
+
// First 3 feature values as a fingerprint to detect identical data
|
|
188
|
+
const sig = features.slice(0, 3).map((v) => v.toFixed(4)).join(",");
|
|
184
189
|
return {
|
|
185
190
|
success: false,
|
|
186
191
|
commitment: tbh.commitmentBytes,
|
|
187
192
|
isFirstVerification: false,
|
|
188
|
-
error: `Proof failed (
|
|
193
|
+
error: `Proof failed (dist=${distance}, feat=${audioNZ}/${motionNZ}/${touchNZ}, raw=${rawAudio}/${rawMotion}/${rawTouch}, sig=${sig}): ${proofErr?.message ?? proofErr}`,
|
|
189
194
|
};
|
|
190
195
|
}
|
|
191
196
|
}
|