for first push

This commit is contained in:
2026-04-29 16:29:03 -04:00
parent 218de3b03b
commit 509259bcf2
19 changed files with 1911 additions and 2 deletions

View File

@@ -0,0 +1,183 @@
import prisma from "@shieldai/db";
import { AudioPreprocessor, AudioFeatures } from "../preprocessor/AudioPreprocessor";
import { EmbeddingService, EmbeddingOutput } from "../embedding/EmbeddingService";
import { VoiceEnrollmentService } from "../enrollment/VoiceEnrollmentService";
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<AnalysisResultOutput> {
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(),
},
});
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<AnalysisResultOutput | null> {
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<AnalysisResultOutput[]> {
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<number> {
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
);
}
}

View File

@@ -0,0 +1,125 @@
import prisma from "@shieldai/db";
import { AnalysisService } from "./AnalysisService";
import {
BatchAnalyzeInput,
AnalysisJobStatus,
AnalysisType,
AnalysisResultOutput,
} from "@shieldai/types";
export class BatchAnalysisService {
private analysisService: AnalysisService;
constructor() {
this.analysisService = new AnalysisService();
}
async analyzeBatch(
input: BatchAnalyzeInput,
userId: string
): Promise<BatchResult> {
const batchId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const results: AnalysisResultOutput[] = [];
const errors: Array<{ name: string; error: string }> = [];
for (const audioInput of input.audioBuffers) {
try {
const result = await this.analysisService.analyze(
{
audioBuffer: audioInput.buffer,
sampleRate: audioInput.sampleRate,
analysisType: input.analysisType || AnalysisType.SYNTHETIC_DETECTION,
},
userId
);
results.push(result);
} catch (err) {
const message = err instanceof Error ? err.message : "Analysis failed";
errors.push({ name: audioInput.name, error: message });
}
}
const batchJob = await prisma.analysisJob.create({
data: {
userId,
analysisType: AnalysisType.BATCH,
audioFilePath: `voiceprint/${userId}/${batchId}`,
status: errors.length === input.audioBuffers.length
? AnalysisJobStatus.FAILED
: AnalysisJobStatus.COMPLETED,
errorMessage:
errors.length > 0 ? `${errors.length} of ${input.audioBuffers.length} files failed` : undefined,
completedAt: new Date(),
},
});
return {
batchId,
jobId: batchJob.id,
totalFiles: input.audioBuffers.length,
successfulResults: results.length,
failedCount: errors.length,
results,
errors,
};
}
async getBatchResult(batchJobId: string): Promise<BatchResult | null> {
const job = await prisma.analysisJob.findUnique({
where: { id: batchJobId },
include: { result: true },
});
if (!job) return null;
const childJobs = await prisma.analysisJob.findMany({
where: {
userId: job.userId,
createdAt: { gte: job.createdAt, lt: new Date(job.createdAt.getTime() + 60000) },
id: { not: job.id },
},
include: { result: true },
});
const results: AnalysisResultOutput[] = [];
const errors: Array<{ name: string; error: string }> = [];
for (const childJob of childJobs) {
if (childJob.result) {
const r = childJob.result;
results.push({
jobId: childJob.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,
});
} else if (childJob.errorMessage) {
errors.push({ name: childJob.audioFilePath, error: childJob.errorMessage });
}
}
return {
batchId: job.audioFilePath.split("/").pop() || job.id,
jobId: job.id,
totalFiles: childJobs.length,
successfulResults: results.length,
failedCount: errors.length,
results,
errors,
};
}
}
export interface BatchResult {
batchId: string;
jobId: string;
totalFiles: number;
successfulResults: number;
failedCount: number;
results: AnalysisResultOutput[];
errors: Array<{ name: string; error: string }>;
}