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 {