@jaehyun-ko/speaker-verification
Version:
Real-time speaker verification in the browser using NeXt-TDNN models
116 lines (115 loc) • 3.99 kB
JavaScript
;
/**
* Score Normalization (S-norm) implementation
* Based on the paper: adaptive s-norm with cohort selection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScoreNormalizer = void 0;
class ScoreNormalizer {
constructor(config = {}) {
this.cohortEmbeddings = [];
this.config = {
cohortSize: config.cohortSize || 6000,
topK: config.topK || 300
};
}
/**
* Add cohort embeddings for score normalization
*/
addCohortEmbeddings(embeddings) {
this.cohortEmbeddings = embeddings.slice(0, this.config.cohortSize);
}
/**
* Load cohort embeddings from a file
*/
async loadCohortEmbeddings(url) {
try {
const response = await fetch(url);
const data = await response.json();
// Convert array of arrays to Float32Arrays
const embeddings = data.embeddings.map((emb) => new Float32Array(emb));
this.addCohortEmbeddings(embeddings);
}
catch (error) {
throw error;
}
}
/**
* Compute cosine similarity between two embeddings
*/
computeSimilarity(emb1, emb2) {
let dotProduct = 0;
for (let i = 0; i < emb1.length; i++) {
dotProduct += emb1[i] * emb2[i];
}
return dotProduct; // Embeddings are already normalized
}
/**
* Apply S-normalization to a raw score
*
* @param enrollEmbedding - Enrollment speaker embedding
* @param testEmbedding - Test speaker embedding
* @param rawScore - Raw cosine similarity score
* @returns Normalized score
*/
normalize(enrollEmbedding, testEmbedding, rawScore) {
if (this.cohortEmbeddings.length === 0) {
return rawScore;
}
// Compute scores between enrollment and cohort
const enrollCohortScores = [];
for (const cohortEmb of this.cohortEmbeddings) {
const score = this.computeSimilarity(enrollEmbedding, cohortEmb);
enrollCohortScores.push(score);
}
// Compute scores between test and cohort
const testCohortScores = [];
for (const cohortEmb of this.cohortEmbeddings) {
const score = this.computeSimilarity(testEmbedding, cohortEmb);
testCohortScores.push(score);
}
// Sort and select top-K scores
enrollCohortScores.sort((a, b) => b - a); // Descending order
testCohortScores.sort((a, b) => b - a);
const topKEnroll = enrollCohortScores.slice(0, this.config.topK);
const topKTest = testCohortScores.slice(0, this.config.topK);
// Compute statistics
const meanEnroll = this.computeMean(topKEnroll);
const stdEnroll = this.computeStd(topKEnroll, meanEnroll);
const meanTest = this.computeMean(topKTest);
const stdTest = this.computeStd(topKTest, meanTest);
// Apply symmetric S-normalization
const normalizedScore = 0.5 * ((rawScore - meanEnroll) / (stdEnroll + 1e-6) +
(rawScore - meanTest) / (stdTest + 1e-6));
return normalizedScore;
}
/**
* Compute mean of an array
*/
computeMean(values) {
if (values.length === 0)
return 0;
const sum = values.reduce((a, b) => a + b, 0);
return sum / values.length;
}
/**
* Compute standard deviation of an array
*/
computeStd(values, mean) {
if (values.length === 0)
return 1;
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
return Math.sqrt(variance);
}
/**
* Get statistics about the cohort
*/
getCohortStats() {
return {
size: this.cohortEmbeddings.length,
topK: this.config.topK,
loaded: this.cohortEmbeddings.length > 0
};
}
}
exports.ScoreNormalizer = ScoreNormalizer;