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 { z } from 'zod';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
import { checkFlag } from './voiceprint.feature-flags';
|
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({
|
const envSchema = z.object({
|
||||||
ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'),
|
ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'),
|
||||||
ML_SERVICE_URL: z.string().default('http://localhost:8001'),
|
ML_SERVICE_URL: z.string().default('http://localhost:8001'),
|
||||||
FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'),
|
FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'),
|
||||||
AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'),
|
AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'),
|
||||||
AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'),
|
AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'),
|
||||||
SYNTHETIC_THRESHOLD: z.string().transform(Number).default(0.75),
|
SYNTHETIC_THRESHOLD: z.string().transform(Number).default('0.75'),
|
||||||
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default(3),
|
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default('3'),
|
||||||
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default(60),
|
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default('60'),
|
||||||
EMBEDDING_DIMENSIONS: z.string().transform(Number).default(192),
|
EMBEDDING_DIMENSIONS: z.string().transform(Number).default('192'),
|
||||||
BATCH_MAX_FILES: z.string().transform(Number).default(20),
|
BATCH_MAX_FILES: z.string().transform(Number).default('20'),
|
||||||
ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(30000),
|
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,
|
ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH,
|
||||||
ML_SERVICE_URL: process.env.ML_SERVICE_URL,
|
ML_SERVICE_URL: process.env.ML_SERVICE_URL,
|
||||||
FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH,
|
FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH,
|
||||||
@@ -28,7 +30,23 @@ export const voicePrintEnv = envSchema.parse({
|
|||||||
EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS,
|
EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS,
|
||||||
BATCH_MAX_FILES: process.env.BATCH_MAX_FILES,
|
BATCH_MAX_FILES: process.env.BATCH_MAX_FILES,
|
||||||
ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS,
|
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
|
// Audio source types
|
||||||
export enum VoicePrintSource {
|
export enum VoicePrintSource {
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
import { checkFlag } from './voiceprint.feature-flags';
|
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({
|
const envSchema = z.object({
|
||||||
ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'),
|
ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'),
|
||||||
ML_SERVICE_URL: z.string().default('http://localhost:8001'),
|
ML_SERVICE_URL: z.string().default('http://localhost:8001'),
|
||||||
FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'),
|
FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'),
|
||||||
AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'),
|
AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'),
|
||||||
AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'),
|
AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'),
|
||||||
SYNTHETIC_THRESHOLD: z.string().transform(Number).default(0.75),
|
SYNTHETIC_THRESHOLD: z.string().transform(Number).default('0.75'),
|
||||||
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default(3),
|
ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default('3'),
|
||||||
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default(60),
|
ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default('60'),
|
||||||
EMBEDDING_DIMENSIONS: z.string().transform(Number).default(192),
|
EMBEDDING_DIMENSIONS: z.string().transform(Number).default('192'),
|
||||||
BATCH_MAX_FILES: z.string().transform(Number).default(20),
|
BATCH_MAX_FILES: z.string().transform(Number).default('20'),
|
||||||
ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(30000),
|
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,
|
ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH,
|
||||||
ML_SERVICE_URL: process.env.ML_SERVICE_URL,
|
ML_SERVICE_URL: process.env.ML_SERVICE_URL,
|
||||||
FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH,
|
FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH,
|
||||||
@@ -28,7 +30,23 @@ export const voicePrintEnv = envSchema.parse({
|
|||||||
EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS,
|
EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS,
|
||||||
BATCH_MAX_FILES: process.env.BATCH_MAX_FILES,
|
BATCH_MAX_FILES: process.env.BATCH_MAX_FILES,
|
||||||
ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS,
|
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
|
// Audio source types
|
||||||
export enum VoicePrintSource {
|
export enum VoicePrintSource {
|
||||||
|
|||||||
Reference in New Issue
Block a user