FRE-4529: Transfer ShieldAI code from FrenoCorp repo
Transferred ShieldAI-related files mistakenly placed in ~/code/FrenoCorp:
- Services: spamshield (feature-flags, audit-logger, error-handler), voiceprint (config, service, feature-flags), darkwatch (pipeline, scan, scheduler, watchlist, webhook)
- Packages: shared-analytics, shared-auth, shared-ui, shared-utils (new); shared-billing, jobs supplemented with unique FC files
- Server: alerts (FC version newer), routes (spamshield, darkwatch, voiceprint)
- Config: turbo.json, tsconfig.base.json, vite/vitest configs, drizzle, Dockerfile
- VoicePrint ML service
- Examples
Pending: apps/{api,web,mobile}/ structured merge, shared-db/db mapping
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,94 +1,257 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { VoiceEnrollmentService } from "@shieldai/voiceprint";
|
||||
import { AnalysisService } from "@shieldai/voiceprint";
|
||||
import { BatchAnalysisService } from "@shieldai/voiceprint";
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import {
|
||||
voiceEnrollmentService,
|
||||
analysisService,
|
||||
batchAnalysisService,
|
||||
voicePrintEnv,
|
||||
AnalysisJobStatus,
|
||||
} from '../services/voiceprint';
|
||||
|
||||
export function voiceprintRoutes(fastify: FastifyInstance) {
|
||||
const enrollmentService = new VoiceEnrollmentService();
|
||||
const analysisService = new AnalysisService();
|
||||
const batchService = new BatchAnalysisService();
|
||||
export async function voiceprintRoutes(fastify: FastifyInstance) {
|
||||
// Enroll a new voice profile
|
||||
fastify.post('/enroll', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
fastify.post("/enroll", async (request, reply) => {
|
||||
const userId = (request.user as { id: string })?.id;
|
||||
if (!userId) return reply.code(401).send({ error: "User not authenticated" });
|
||||
|
||||
const body = request.body as { label: string; audio: string; sampleRate?: number };
|
||||
|
||||
const audioBuffer = Buffer.from(body.audio, "base64");
|
||||
const enrollment = await enrollmentService.enroll(
|
||||
{ label: body.label, audioBuffer, sampleRate: body.sampleRate },
|
||||
userId
|
||||
);
|
||||
return reply.code(201).send(enrollment);
|
||||
});
|
||||
|
||||
fastify.get("/enrollments", async (request, reply) => {
|
||||
const userId = (request.user as { id: string })?.id;
|
||||
if (!userId) return reply.code(401).send({ error: "User not authenticated" });
|
||||
|
||||
const enrollments = await enrollmentService.listEnrollments(userId);
|
||||
return reply.send(enrollments);
|
||||
});
|
||||
|
||||
fastify.delete("/enrollments/:id", async (request, reply) => {
|
||||
const userId = (request.user as { id: string })?.id;
|
||||
if (!userId) return reply.code(401).send({ error: "User not authenticated" });
|
||||
|
||||
const enrollmentId = (request.params as { id: string }).id;
|
||||
const result = await enrollmentService.removeEnrollment(userId, enrollmentId);
|
||||
return reply.send({ removed: result });
|
||||
});
|
||||
|
||||
fastify.post("/analyze", async (request, reply) => {
|
||||
const userId = (request.user as { id: string })?.id;
|
||||
if (!userId) return reply.code(401).send({ error: "User not authenticated" });
|
||||
|
||||
const body = request.body as { audio: string; sampleRate?: number; analysisType?: string };
|
||||
const audioBuffer = Buffer.from(body.audio, "base64");
|
||||
|
||||
const result = await analysisService.analyze(
|
||||
{ audioBuffer, sampleRate: body.sampleRate, analysisType: body.analysisType },
|
||||
userId
|
||||
);
|
||||
return reply.code(201).send(result);
|
||||
});
|
||||
|
||||
fastify.get("/results/:id", async (request, reply) => {
|
||||
const jobId = (request.params as { id: string }).id;
|
||||
const result = await analysisService.getResult(jobId);
|
||||
|
||||
if (!result) return reply.code(404).send({ error: "Analysis result not found" });
|
||||
return reply.send(result);
|
||||
});
|
||||
|
||||
fastify.get("/results", async (request, reply) => {
|
||||
const userId = (request.user as { id: string })?.id;
|
||||
if (!userId) return reply.code(401).send({ error: "User not authenticated" });
|
||||
|
||||
const limit = parseInt((request.query as { limit?: string }).limit || "20", 10);
|
||||
const results = await analysisService.getUserResults(userId, limit);
|
||||
return reply.send(results);
|
||||
});
|
||||
|
||||
fastify.post("/batch", async (request, reply) => {
|
||||
const userId = (request.user as { id: string })?.id;
|
||||
if (!userId) return reply.code(401).send({ error: "User not authenticated" });
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const body = request.body as {
|
||||
files: Array<{ name: string; audio: string; sampleRate?: number }>;
|
||||
analysisType?: string;
|
||||
name: string;
|
||||
audio: Buffer;
|
||||
};
|
||||
|
||||
const audioBuffers = body.files.map((f) => ({
|
||||
name: f.name,
|
||||
buffer: Buffer.from(f.audio, "base64"),
|
||||
sampleRate: f.sampleRate,
|
||||
}));
|
||||
if (!body.name || !body.audio) {
|
||||
return reply.code(400).send({ error: 'name and audio are required' });
|
||||
}
|
||||
|
||||
const result = await batchService.analyzeBatch(
|
||||
{ audioBuffers, analysisType: body.analysisType },
|
||||
userId
|
||||
);
|
||||
return reply.code(201).send(result);
|
||||
try {
|
||||
const enrollment = await voiceEnrollmentService.enroll(
|
||||
userId,
|
||||
body.name,
|
||||
body.audio
|
||||
);
|
||||
return reply.code(201).send({
|
||||
enrollment: {
|
||||
id: enrollment.id,
|
||||
name: enrollment.name,
|
||||
isActive: enrollment.isActive,
|
||||
createdAt: enrollment.createdAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Enrollment failed';
|
||||
return reply.code(422).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// List user's voice enrollments
|
||||
fastify.get('/enrollments', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const isActive = request.query as { isActive?: string };
|
||||
const limit = request.query as { limit?: string };
|
||||
const offset = request.query as { offset?: string };
|
||||
|
||||
const enrollments = await voiceEnrollmentService.listEnrollments(userId, {
|
||||
isActive: isActive.isActive !== undefined
|
||||
? isActive.isActive === 'true'
|
||||
: undefined,
|
||||
limit: limit.limit ? parseInt(limit.limit, 10) : undefined,
|
||||
offset: offset.offset ? parseInt(offset.offset, 10) : undefined,
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
enrollments: enrollments.map((e) => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
isActive: e.isActive,
|
||||
createdAt: e.createdAt,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
// Remove an enrollment
|
||||
fastify.delete('/enrollments/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const enrollmentId = (request.params as { id: string }).id;
|
||||
|
||||
try {
|
||||
const enrollment = await voiceEnrollmentService.removeEnrollment(
|
||||
enrollmentId,
|
||||
userId
|
||||
);
|
||||
return reply.send({
|
||||
enrollment: {
|
||||
id: enrollment.id,
|
||||
name: enrollment.name,
|
||||
isActive: enrollment.isActive,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Removal failed';
|
||||
return reply.code(404).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze a single audio file
|
||||
fastify.post('/analyze', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const body = request.body as {
|
||||
audio: Buffer;
|
||||
enrollmentId?: string;
|
||||
audioUrl?: string;
|
||||
};
|
||||
|
||||
if (!body.audio) {
|
||||
return reply.code(400).send({ error: 'audio is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await analysisService.analyze(userId, body.audio, {
|
||||
enrollmentId: body.enrollmentId,
|
||||
audioUrl: body.audioUrl,
|
||||
});
|
||||
return reply.code(201).send({
|
||||
analysis: {
|
||||
id: result.id,
|
||||
isSynthetic: result.isSynthetic,
|
||||
confidence: result.confidence,
|
||||
analysisResult: result.analysisResult,
|
||||
createdAt: result.createdAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Analysis failed';
|
||||
return reply.code(422).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get analysis result by ID
|
||||
fastify.get('/results/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const analysisId = (request.params as { id: string }).id;
|
||||
const result = await analysisService.getResult(analysisId, userId);
|
||||
|
||||
if (!result) {
|
||||
return reply.code(404).send({ error: 'Analysis not found' });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
analysis: {
|
||||
id: result.id,
|
||||
isSynthetic: result.isSynthetic,
|
||||
confidence: result.confidence,
|
||||
analysisResult: result.analysisResult,
|
||||
createdAt: result.createdAt,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Get analysis history
|
||||
fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const query = request.query as {
|
||||
limit?: string;
|
||||
offset?: string;
|
||||
isSynthetic?: string;
|
||||
};
|
||||
|
||||
const results = await analysisService.getHistory(userId, {
|
||||
limit: query.limit ? parseInt(query.limit, 10) : undefined,
|
||||
offset: query.offset ? parseInt(query.offset, 10) : undefined,
|
||||
isSynthetic: query.isSynthetic !== undefined
|
||||
? query.isSynthetic === 'true'
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
analyses: results.map((r) => ({
|
||||
id: r.id,
|
||||
isSynthetic: r.isSynthetic,
|
||||
confidence: r.confidence,
|
||||
createdAt: r.createdAt,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
// Batch analyze multiple audio files
|
||||
fastify.post('/batch', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as FastifyRequest & { user?: { id: string } };
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.code(401).send({ error: 'User ID required' });
|
||||
}
|
||||
|
||||
const body = request.body as {
|
||||
files: Array<{
|
||||
name: string;
|
||||
audio: Buffer;
|
||||
audioUrl?: string;
|
||||
}>;
|
||||
enrollmentId?: string;
|
||||
};
|
||||
|
||||
if (!body.files || body.files.length === 0) {
|
||||
return reply.code(400).send({ error: 'files array is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await batchAnalysisService.analyzeBatch(
|
||||
userId,
|
||||
body.files.map((f) => ({
|
||||
name: f.name,
|
||||
buffer: f.audio,
|
||||
audioUrl: f.audioUrl,
|
||||
})),
|
||||
{
|
||||
enrollmentId: body.enrollmentId,
|
||||
}
|
||||
);
|
||||
|
||||
return reply.code(201).send({
|
||||
jobId: result.jobId,
|
||||
results: result.results.map((r) => ({
|
||||
id: r.id,
|
||||
isSynthetic: r.isSynthetic,
|
||||
confidence: r.confidence,
|
||||
})),
|
||||
summary: result.summary,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Batch analysis failed';
|
||||
return reply.code(422).send({ error: message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user