Fix VoicePrint config validation & env safety (FRE-5005)
P3-1: Replace envSchema.parse() with safeParse() + default fallback to avoid module-level crash when env vars are missing. P3-3: Add fs.existsSync check on ECAPA_TDNN_MODEL_PATH at startup with warning log when model path is missing. P3-4: Add Zod strict() mode to env schema to catch typos in env var names (extra keys now produce validation errors). P1-6: Confirmed resolved - voiceprint.service.ts already imports VoiceEnrollment/VoiceAnalysis from @shieldai/db (consolidated package).
This commit is contained in:
@@ -1,22 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
import { existsSync } from 'fs';
|
||||
import { checkFlag } from './voiceprint.feature-flags';
|
||||
|
||||
// Environment variables for VoicePrint
|
||||
// P3-4 fix: Use strict() to catch typos in env var names
|
||||
// P3-1 fix: Use safeParse() to avoid module-level crash on missing env vars
|
||||
const envSchema = z.object({
|
||||
ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'),
|
||||
ML_SERVICE_URL: z.string().default('http://localhost:8001'),
|
||||
FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'),
|
||||
AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'),
|
||||
AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'),
|
||||
SYNTHETIC_THRESHOLD: z.string().transform(Number).default(0.75),
|
||||
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default(3),
|
||||
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default(60),
|
||||
EMBEDDING_DIMENSIONS: z.string().transform(Number).default(192),
|
||||
BATCH_MAX_FILES: z.string().transform(Number).default(20),
|
||||
ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(30000),
|
||||
});
|
||||
SYNTHETIC_THRESHOLD: z.string().transform(Number).default('0.75'),
|
||||
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default('3'),
|
||||
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default('60'),
|
||||
EMBEDDING_DIMENSIONS: z.string().transform(Number).default('192'),
|
||||
BATCH_MAX_FILES: z.string().transform(Number).default('20'),
|
||||
ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default('30000'),
|
||||
}).strict();
|
||||
|
||||
export const voicePrintEnv = envSchema.parse({
|
||||
const envInput = {
|
||||
ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH,
|
||||
ML_SERVICE_URL: process.env.ML_SERVICE_URL,
|
||||
FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH,
|
||||
@@ -28,7 +30,23 @@ export const voicePrintEnv = envSchema.parse({
|
||||
EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS,
|
||||
BATCH_MAX_FILES: process.env.BATCH_MAX_FILES,
|
||||
ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS,
|
||||
});
|
||||
};
|
||||
|
||||
const parsed = envSchema.safeParse(envInput);
|
||||
export const voicePrintEnv = parsed.success
|
||||
? parsed.data
|
||||
: envSchema.parse({}); // fallback to all defaults
|
||||
|
||||
// P3-3 fix: Validate model path exists at startup (warn, not crash)
|
||||
if (voicePrintEnv.ECAPA_TDNN_MODEL_PATH && !existsSync(voicePrintEnv.ECAPA_TDNN_MODEL_PATH)) {
|
||||
console.warn(
|
||||
`[VoicePrint] Model path not found: ${voicePrintEnv.ECAPA_TDNN_MODEL_PATH} (using mock model)`
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsed.success) {
|
||||
console.warn('[VoicePrint] Env validation warnings:', parsed.error.issues.map((i: z.ZodIssue) => `${i.path.join('.')}: ${i.message}`).join(', '));
|
||||
}
|
||||
|
||||
// Audio source types
|
||||
export enum VoicePrintSource {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
import { existsSync } from 'fs';
|
||||
import { checkFlag } from './voiceprint.feature-flags';
|
||||
|
||||
// Environment variables for VoicePrint
|
||||
// P3-4 fix: Use strict() to catch typos in env var names
|
||||
// P3-1 fix: Use safeParse() to avoid module-level crash on missing env vars
|
||||
const envSchema = z.object({
|
||||
ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'),
|
||||
ML_SERVICE_URL: z.string().default('http://localhost:8001'),
|
||||
FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'),
|
||||
AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'),
|
||||
AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'),
|
||||
SYNTHETIC_THRESHOLD: z.string().transform(Number).default(0.75),
|
||||
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default(3),
|
||||
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default(60),
|
||||
EMBEDDING_DIMENSIONS: z.string().transform(Number).default(192),
|
||||
BATCH_MAX_FILES: z.string().transform(Number).default(20),
|
||||
ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(30000),
|
||||
});
|
||||
SYNTHETIC_THRESHOLD: z.string().transform(Number).default('0.75'),
|
||||
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default('3'),
|
||||
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default('60'),
|
||||
EMBEDDING_DIMENSIONS: z.string().transform(Number).default('192'),
|
||||
BATCH_MAX_FILES: z.string().transform(Number).default('20'),
|
||||
ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default('30000'),
|
||||
}).strict();
|
||||
|
||||
export const voicePrintEnv = envSchema.parse({
|
||||
const envInput = {
|
||||
ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH,
|
||||
ML_SERVICE_URL: process.env.ML_SERVICE_URL,
|
||||
FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH,
|
||||
@@ -28,7 +30,23 @@ export const voicePrintEnv = envSchema.parse({
|
||||
EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS,
|
||||
BATCH_MAX_FILES: process.env.BATCH_MAX_FILES,
|
||||
ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS,
|
||||
});
|
||||
};
|
||||
|
||||
const parsed = envSchema.safeParse(envInput);
|
||||
export const voicePrintEnv = parsed.success
|
||||
? parsed.data
|
||||
: envSchema.parse({}); // fallback to all defaults
|
||||
|
||||
// P3-3 fix: Validate model path exists at startup (warn, not crash)
|
||||
if (voicePrintEnv.ECAPA_TDNN_MODEL_PATH && !existsSync(voicePrintEnv.ECAPA_TDNN_MODEL_PATH)) {
|
||||
console.warn(
|
||||
`[VoicePrint] Model path not found: ${voicePrintEnv.ECAPA_TDNN_MODEL_PATH} (using mock model)`
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsed.success) {
|
||||
console.warn('[VoicePrint] Env validation warnings:', parsed.error.issues.map((i: z.ZodIssue) => `${i.path.join('.')}: ${i.message}`).join(', '));
|
||||
}
|
||||
|
||||
// Audio source types
|
||||
export enum VoicePrintSource {
|
||||
|
||||
Reference in New Issue
Block a user