import { spawn } from "child_process"; import { logger } from './logger'; import { voicePrintEnv } from './voiceprint.config'; const EMBEDDING_DIM = 192; const MODEL_VERSION = "ecapa-tdnn-0.1.0-mock"; export class EmbeddingService { private mlServiceUrl: string; private initialized = false; constructor() { this.mlServiceUrl = process.env.VOICEPRINT_ML_URL || "http://localhost:8001"; } async initialize(): Promise { if (this.initialized) return; this.initialized = true; logger.info('Embedding service initialized', { mlUrl: this.mlServiceUrl, modelVersion: MODEL_VERSION }); } async extract(audioBuffer: Buffer): Promise { await this.initialize(); const mlAvailable = await this.checkMLService(); if (mlAvailable) { logger.info('Using ML service for embedding', { mlUrl: this.mlServiceUrl }); return this.extractViaML(audioBuffer); } logger.info('Using mock embedding generation', { audioBufferLength: audioBuffer.length }); return this.generateMockFromBuffer(audioBuffer); } async analyze(audioBuffer: Buffer): Promise<{ confidence: number; detectionType: string; features: Record; embedding: number[]; }> { const embedding = await this.extract(audioBuffer); const confidence = this.estimateSyntheticConfidence(audioBuffer, embedding); const detectionType = confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD ? 'synthetic_voice' : 'natural'; const features = this.extractAnalysisFeatures(audioBuffer, embedding); return { confidence, detectionType, features, embedding }; } getModelVersion(): string { return MODEL_VERSION; } private async extractViaML(audioBuffer: Buffer): Promise { return new Promise((resolve, reject) => { const jsonInput = audioBuffer.toString("base64"); const proc = spawn("python3", [ "-c", ` import urllib.request, json, sys req = urllib.request.Request( "${this.mlServiceUrl}/embedding", data=json.dumps({"audio": "${jsonInput.substring(0, 5000)}"}).encode(), headers={"Content-Type": "application/json"} ) try: with urllib.request.urlopen(req, timeout=60) as resp: data = json.loads(resp.read()) sys.stdout.write(json.dumps({"ok": True, "vector": data.get("embedding", []), "dim": data.get("dimension", ${EMBEDDING_DIM})})) except Exception as e: sys.stdout.write(json.dumps({"ok": False, "error": str(e)})) `, ]); let output = ""; proc.stdout.on("data", (chunk) => { output += chunk.toString(); }); proc.on("close", (code) => { try { const result = JSON.parse(output); if (result.ok && result.vector && result.vector.length === EMBEDDING_DIM) { resolve(result.vector); } else { resolve(this.generateMockFromBuffer(audioBuffer)); } } catch { resolve(this.generateMockFromBuffer(audioBuffer)); } }); }); } private generateMockFromBuffer(audioBuffer: Buffer): number[] { 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[] = []; // Box-Muller transform for Gaussian distribution for (let i = 0; i < EMBEDDING_DIM; i += 2) { const u1 = rng(); const u2 = rng(); const mag = Math.sqrt(-2 * Math.log(u1)); const z0 = mag * Math.cos(2 * Math.PI * u2); const z1 = mag * Math.sin(2 * Math.PI * u2); vector.push(parseFloat(z0.toFixed(6))); if (i + 1 < EMBEDDING_DIM) { vector.push(parseFloat(z1.toFixed(6))); } } // L2 normalize const norm = Math.sqrt(vector.reduce((s, v) => s + v * v, 0)); return vector.map((v) => parseFloat((v / norm).toFixed(6))); } private estimateSyntheticConfidence(buffer: Buffer, embedding: number[]): number { const meanAmplitude = buffer.reduce((s, v) => s + v, 0) / buffer.length / 255; const meanEmbedding = embedding.reduce((s, v) => s + v, 0) / embedding.length; const embeddingStdDev = Math.sqrt(embedding.reduce((s, v) => s + (v - meanEmbedding) ** 2, 0) / embedding.length); const amplitudeScore = Math.abs(meanAmplitude - 0.5) * 2; const embeddingScore = 1.0 - Math.min(1.0, embeddingStdDev * 2); const varianceScore = Math.min(1.0, buffer.length / 10000); return Math.min(1.0, amplitudeScore * 0.3 + embeddingScore * 0.4 + varianceScore * 0.3); } private extractAnalysisFeatures(buffer: Buffer, embedding: number[]): Record { const meanAmplitude = buffer.reduce((s, v) => s + v, 0) / buffer.length / 255; const zeroCrossings = buffer.reduce((count, v, i, arr) => { return i > 0 && ((v - 128) * (arr[i - 1] - 128) < 0) ? count + 1 : count; }, 0); return { mean_amplitude: meanAmplitude, zero_crossing_rate: zeroCrossings / buffer.length, embedding_energy: embedding.reduce((s, v) => s + v * v, 0), embedding_entropy: this.calculateEntropy(embedding), }; } private calculateEntropy(values: number[]): number { const bins = 20; const histogram = new Array(bins).fill(0); const min = Math.min(...values); const max = Math.max(...values); const range = max - min || 1; for (const v of values) { const bin = Math.min(bins - 1, Math.floor(((v - min) / range) * bins)); histogram[bin]++; } let entropy = 0; const total = values.length; for (const count of histogram) { if (count > 0) { const p = count / total; entropy -= p * Math.log2(p); } } return entropy; } private async checkMLService(): Promise { return new Promise((resolve) => { const proc = spawn("python3", [ "-c", ` import urllib.request, sys try: urllib.request.urlopen("${this.mlServiceUrl}/health", timeout=2) sys.exit(0) except: sys.exit(1) `, ]); proc.on("close", (code) => resolve(code === 0)); }); } private createRNG(seed: number): () => number { return () => { seed = (seed * 1664525 + 1013904223) & 0xffffffff; return (seed >>> 0) / 0xffffffff; }; } }