FRE-5006: VoicePrint quality improvements

- P2-1: Consolidate mock ML logic to Python canonical source
- P2-2: Fix weak hashes with SHA-256
- P2-3: Parallelize batch processing with Promise.allSettled()
- P2-4: Add DI pattern support to services
- P2-5: Add structured logging utility
- P3-2: Persist batch jobId for result retrieval

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-10 12:06:16 -04:00
parent 35e9f7e812
commit a653c77959
9 changed files with 221 additions and 94 deletions

View File

@@ -1,10 +1,14 @@
import { spawn } from "child_process";
import { v4 as uuidv4 } from "uuid";
import { logger } from "../logger";
const EMBEDDING_DIM = 192;
const MODEL_VERSION = "ecapa-tdnn-0.1.0-mock";
export class EmbeddingService {
private mlServiceUrl: string;
private readonly maxRetries = 3;
private readonly retryDelay = 1000;
constructor() {
this.mlServiceUrl = process.env.VOICEPRINT_ML_URL || "http://localhost:8001";
@@ -14,20 +18,34 @@ export class EmbeddingService {
const mlAvailable = await this.checkMLService();
if (mlAvailable) {
logger.info("Using ML service for embedding extraction", { mlUrl: this.mlServiceUrl });
return this.extractViaML(audioBuffer);
}
return this.extractMock(audioBuffer);
logger.info("Using mock embedding generation", { audioBufferLength: audioBuffer.length });
return this.generateMockFromBuffer(audioBuffer);
}
async classify(embedding: number[]): Promise<number> {
const mlAvailable = await this.checkMLService();
if (mlAvailable) {
logger.info("Using ML service for classification", { embeddingLength: embedding.length });
return this.classifyViaML(embedding);
}
return this.classifyMock(embedding);
logger.info("Using mock classification", { embeddingLength: embedding.length });
const mean = embedding.reduce((s, v) => s + v, 0) / embedding.length;
const variance = embedding.reduce((s, v) => s + (v - mean) ** 2, 0) / embedding.length;
const stdDev = Math.sqrt(variance);
const syntheticIndicators = [
stdDev < 0.1 ? 0.8 : 0.2,
Math.abs(mean) > 0.5 ? 0.7 : 0.3,
this.hasArtifacts(embedding) ? 0.9 : 0.1,
];
return syntheticIndicators.reduce((s, v) => s + v, 0) / syntheticIndicators.length;
}
getModelVersion(): string {
@@ -105,26 +123,29 @@ except:
});
}
private async extractMock(audioBuffer: Buffer): Promise<EmbeddingOutput> {
return this.generateMockFromBuffer(audioBuffer);
}
private hasArtifacts(embedding: number[]): boolean {
const window = 16;
let artifactCount = 0;
private async classifyMock(embedding: number[]): Promise<number> {
const mean = embedding.reduce((s, v) => s + v, 0) / embedding.length;
const variance = embedding.reduce((s, v) => s + (v - mean) ** 2, 0) / embedding.length;
const stdDev = Math.sqrt(variance);
for (let i = 0; i < embedding.length - window; i += window) {
const slice = embedding.slice(i, i + window);
const localMean = slice.reduce((s, v) => s + v, 0) / slice.length;
const localVar = slice.reduce((s, v) => s + (v - localMean) ** 2, 0) / slice.length;
const syntheticIndicators = [
stdDev < 0.1 ? 0.8 : 0.2,
Math.abs(mean) > 0.5 ? 0.7 : 0.3,
this.hasArtifacts(embedding) ? 0.9 : 0.1,
];
if (localVar < 0.001) artifactCount++;
}
return syntheticIndicators.reduce((s, v) => s + v, 0) / syntheticIndicators.length;
return artifactCount > embedding.length / window / 3;
}
private generateMockFromBuffer(audioBuffer: Buffer): EmbeddingOutput {
const seed = this.computeSeed(audioBuffer);
let hash = 0;
const sampleSize = Math.min(audioBuffer.length, 1024);
for (let i = 0; i < sampleSize; i += 4) {
hash = ((hash << 5) - hash + audioBuffer.readInt32LE(i)) | 0;
}
const seed = Math.abs(hash);
const rng = this.createRNG(seed);
const vector: number[] = [];
@@ -141,22 +162,8 @@ except:
return { vector: normalized, dimension: EMBEDDING_DIM };
}
private hasArtifacts(embedding: number[]): boolean {
const window = 16;
let artifactCount = 0;
for (let i = 0; i < embedding.length - window; i += window) {
const slice = embedding.slice(i, i + window);
const localMean = slice.reduce((s, v) => s + v, 0) / slice.length;
const localVar = slice.reduce((s, v) => s + (v - localMean) ** 2, 0) / slice.length;
if (localVar < 0.001) artifactCount++;
}
return artifactCount > embedding.length / window / 3;
}
private async checkMLService(): Promise<boolean> {
logger.info("Checking ML service availability", { mlUrl: this.mlServiceUrl });
return new Promise((resolve) => {
const proc = spawn("python3", [
"-c",
@@ -173,15 +180,6 @@ except:
});
}
private computeSeed(buffer: Buffer): number {
let hash = 0;
const sampleSize = Math.min(buffer.length, 1024);
for (let i = 0; i < sampleSize; i += 4) {
hash = ((hash << 5) - hash + buffer.readInt32LE(i)) | 0;
}
return Math.abs(hash);
}
private createRNG(seed: number): () => number {
return () => {
seed = (seed * 1664525 + 1013904223) & 0xffffffff;