import prisma from "@shieldai/db"; import { AudioPreprocessor, AudioFeatures } from "../preprocessor/AudioPreprocessor"; import { EmbeddingService, EmbeddingOutput } from "../embedding/EmbeddingService"; import { VoiceEnrollmentService } from "../enrollment/VoiceEnrollmentService"; import { emitVoicePrintAlert } from "@shieldai/correlation"; import { AnalyzeAudioInput, AnalysisJobStatus, AnalysisType, DetectionVerdict, AnalysisResultOutput, } from "@shieldai/types"; export class AnalysisService { private preprocessor: AudioPreprocessor; private embeddingService: EmbeddingService; private enrollmentService: VoiceEnrollmentService; private readonly syntheticThreshold = 0.7; private readonly uncertainThreshold = 0.4; constructor() { this.preprocessor = new AudioPreprocessor(); this.embeddingService = new EmbeddingService(); this.enrollmentService = new VoiceEnrollmentService(); } async analyze(input: AnalyzeAudioInput, userId: string): Promise { const startTime = Date.now(); const job = await prisma.analysisJob.create({ data: { userId, analysisType: input.analysisType || AnalysisType.SYNTHETIC_DETECTION, audioFilePath: `voiceprint/${userId}/${Date.now()}.wav`, status: AnalysisJobStatus.RUNNING, }, }); try { const preprocessed = await this.preprocessor.preprocess(input.audioBuffer, input.sampleRate); const features = await this.preprocessor.extractFeatures(preprocessed.audio); const embedding = await this.embeddingService.extract(preprocessed.audio); const syntheticScore = await this.classifySynthetic(features, embedding); const verdict = this.determineVerdict(syntheticScore); const confidence = this.computeConfidence(syntheticScore, verdict); let matchedEnrollmentId: string | undefined; let matchedSimilarity: number | undefined; if (input.analysisType === AnalysisType.VOICE_MATCH) { const match = await this.enrollmentService.matchVoice(input.audioBuffer, userId); if (match) { matchedEnrollmentId = match.enrollmentId; matchedSimilarity = match.similarity; } } const processingTimeMs = Date.now() - startTime; const result = await prisma.analysisResult.create({ data: { analysisJobId: job.id, syntheticScore, verdict, confidence, processingTimeMs, matchedEnrollmentId, matchedSimilarity, modelVersion: this.embeddingService.getModelVersion(), }, }); await prisma.analysisJob.update({ where: { id: job.id }, data: { status: AnalysisJobStatus.COMPLETED, completedAt: new Date(), }, }); if (result.verdict === DetectionVerdict.SYNTHETIC || result.verdict === DetectionVerdict.UNCERTAIN) { emitVoicePrintAlert( userId, job.id, result.verdict, result.syntheticScore, result.confidence, result.matchedEnrollmentId || undefined, result.matchedSimilarity || undefined, input.analysisType || undefined ).catch((err) => console.error(`[Correlation] VoicePrint emit failed:`, err)); } return { jobId: job.id, syntheticScore: result.syntheticScore, verdict: result.verdict, confidence: result.confidence, matchedEnrollmentId: result.matchedEnrollmentId || undefined, matchedSimilarity: result.matchedSimilarity || undefined, processingTimeMs: result.processingTimeMs, modelVersion: result.modelVersion || undefined, }; } catch (err) { const message = err instanceof Error ? err.message : "Analysis failed"; await prisma.analysisJob.update({ where: { id: job.id }, data: { status: AnalysisJobStatus.FAILED, errorMessage: message, completedAt: new Date(), }, }); throw err; } } async getResult(jobId: string): Promise { const job = await prisma.analysisJob.findUnique({ where: { id: jobId }, include: { result: true }, }); if (!job || !job.result) return null; const r = job.result; return { jobId, syntheticScore: r.syntheticScore, verdict: r.verdict, confidence: r.confidence, matchedEnrollmentId: r.matchedEnrollmentId || undefined, matchedSimilarity: r.matchedSimilarity || undefined, processingTimeMs: r.processingTimeMs, modelVersion: r.modelVersion || undefined, }; } async getUserResults(userId: string, limit: number = 20): Promise { const jobs = await prisma.analysisJob.findMany({ where: { userId, status: AnalysisJobStatus.COMPLETED }, orderBy: { createdAt: "desc" }, take: limit, include: { result: true }, }); return jobs .filter((j) => j.result) .map((j) => { const r = j.result!; return { jobId: j.id, syntheticScore: r.syntheticScore, verdict: r.verdict, confidence: r.confidence, matchedEnrollmentId: r.matchedEnrollmentId || undefined, matchedSimilarity: r.matchedSimilarity || undefined, processingTimeMs: r.processingTimeMs, modelVersion: r.modelVersion || undefined, }; }); } private async classifySynthetic( features: AudioFeatures, embedding: EmbeddingOutput ): Promise { const modelScore = await this.embeddingService.classify(embedding.vector); const zcrAnomaly = Math.abs(features.zeroCrossingRate - 0.05) / 0.05; const spectralAnomaly = Math.abs(features.spectralCentroid - 500) / 500; const artifactScore = Math.min(1, (zcrAnomaly + spectralAnomaly) / 4); return 0.7 * modelScore + 0.3 * artifactScore; } private determineVerdict(score: number): DetectionVerdict { if (score >= this.syntheticThreshold) return DetectionVerdict.SYNTHETIC; if (score <= this.uncertainThreshold) return DetectionVerdict.NATURAL; return DetectionVerdict.UNCERTAIN; } private computeConfidence(score: number, verdict: DetectionVerdict): number { if (verdict === DetectionVerdict.SYNTHETIC) { return Math.min(1, (score - this.syntheticThreshold) / (1 - this.syntheticThreshold)); } if (verdict === DetectionVerdict.NATURAL) { return Math.min(1, (this.uncertainThreshold - score) / this.uncertainThreshold); } return 1 - Math.min( Math.abs(score - this.uncertainThreshold) / (this.syntheticThreshold - this.uncertainThreshold), 1 ); } }