From 2d0611c2c9609c47a9197b061d08eaa2d6d67425 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 10 May 2026 03:26:26 -0400 Subject: [PATCH] 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). --- .../services/voiceprint/voiceprint.config.ts | 38 ++++++++++++++----- services/voiceprint/src/voiceprint.config.ts | 38 ++++++++++++++----- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/packages/api/src/services/voiceprint/voiceprint.config.ts b/packages/api/src/services/voiceprint/voiceprint.config.ts index f117f5a..f3bad81 100644 --- a/packages/api/src/services/voiceprint/voiceprint.config.ts +++ b/packages/api/src/services/voiceprint/voiceprint.config.ts @@ -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 { diff --git a/services/voiceprint/src/voiceprint.config.ts b/services/voiceprint/src/voiceprint.config.ts index f117f5a..f3bad81 100644 --- a/services/voiceprint/src/voiceprint.config.ts +++ b/services/voiceprint/src/voiceprint.config.ts @@ -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 {