pediatric-ai-scribe-v3/src/routes/userPreferences.js
Daniel 54552df795 Stop leaking e.message to clients across all routes
88 occurrences of res.status(500).json({ error: e.message }) (or
err.message) swept to generic 'Request failed'. Server-side
console.error / logger.error calls are untouched, so the full detail
still lands in logs and Grafana.

Covers: admin, adminConfig, adminMilestones, chartReview, documents,
encounters, hospitalCourse, hpi, learningAdmin, learningAI, learningHub,
logs, memories, milestones, oidc, refine, sessions, sickVisit, soap,
userPreferences, wellVisit.

Also extends .gitignore to exclude .env.backup-* files.
2026-04-14 03:04:24 +02:00

157 lines
6.1 KiB
JavaScript

// ============================================================
// USER PREFERENCES — STT model & TTS voice selection
// ============================================================
var express = require('express');
var router = express.Router();
var db = require('../db/database');
var { authMiddleware } = require('../middleware/auth');
router.use(authMiddleware);
// Get current user's STT/TTS preferences
router.get('/preferences', async function(req, res) {
try {
var user = await db.get('SELECT stt_model, tts_voice FROM users WHERE id = ?', [req.user.id]);
res.json({
success: true,
stt_model: user?.stt_model || null,
tts_voice: user?.tts_voice || null
});
} catch (err) {
console.error('[Preferences]', err.message);
res.status(500).json({ error: 'Request failed' });
}
});
// Update user's STT/TTS preferences
router.post('/preferences', async function(req, res) {
try {
var { stt_model, tts_voice } = req.body;
await db.run(
'UPDATE users SET stt_model = ?, tts_voice = ? WHERE id = ?',
[stt_model || null, tts_voice || null, req.user.id]
);
res.json({ success: true });
} catch (err) {
console.error('[Preferences]', err.message);
res.status(500).json({ error: 'Request failed' });
}
});
// Get available STT models and TTS voices
router.get('/preferences/options', async function(req, res) {
try {
var provider = process.env.TRANSCRIBE_PROVIDER;
var ttsProvider = process.env.TTS_PROVIDER;
// Auto-detect providers if not explicitly set
if (!provider) {
if (process.env.VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT) provider = 'google';
else if (process.env.AWS_BEDROCK_REGION) provider = 'aws';
else if (process.env.LITELLM_API_BASE) provider = 'litellm';
else if (process.env.OPENAI_API_KEY) provider = 'openai';
}
if (!ttsProvider) {
if (process.env.VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT) ttsProvider = 'google';
else if (process.env.LITELLM_API_BASE) ttsProvider = 'litellm';
else if (process.env.OPENAI_API_KEY) ttsProvider = 'openai';
else if (process.env.ELEVENLABS_API_KEY) ttsProvider = 'elevenlabs';
}
// STT Models
var sttModels = [];
if (provider === 'google') {
sttModels = [
{ value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash (Experimental, fastest)' },
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash (Fast, accurate)' },
{ value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash (Stable)' },
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro (Best quality, slower)' }
];
} else if (provider === 'litellm') {
sttModels = [
{ value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash Exp' },
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
{ value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' },
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' },
{ value: 'whisper-1', label: 'OpenAI Whisper-1' },
{ value: 'whisper-large-v3', label: 'Whisper Large v3' }
];
} else if (provider === 'openai') {
sttModels = [
{ value: 'whisper-1', label: 'Whisper-1 (OpenAI standard)' }
];
} else if (provider === 'aws') {
sttModels = [
{ value: 'default', label: 'AWS Transcribe (Standard)' },
{ value: 'medical', label: 'AWS Transcribe Medical' }
];
} else if (provider === 'local') {
sttModels = [
{ value: 'tiny', label: 'Whisper Tiny (fastest)' },
{ value: 'base', label: 'Whisper Base' },
{ value: 'small', label: 'Whisper Small' },
{ value: 'medium', label: 'Whisper Medium' },
{ value: 'large', label: 'Whisper Large (best quality)' }
];
}
// TTS Voices
var ttsVoices = [];
if (ttsProvider === 'google') {
ttsVoices = [
{ value: 'en-US-Journey-F', label: 'Journey (Female, natural)' },
{ value: 'en-US-Journey-D', label: 'Journey (Male, natural)' },
{ value: 'en-US-Studio-O', label: 'Studio O (Female, expressive)' },
{ value: 'en-US-Studio-M', label: 'Studio M (Male, expressive)' },
{ value: 'en-US-Neural2-A', label: 'Neural2 A (Male, standard)' },
{ value: 'en-US-Neural2-C', label: 'Neural2 C (Female, standard)' },
{ value: 'en-US-Neural2-D', label: 'Neural2 D (Male, standard)' },
{ value: 'en-US-Neural2-E', label: 'Neural2 E (Female, standard)' },
{ value: 'en-US-Neural2-F', label: 'Neural2 F (Female, standard)' },
{ value: 'en-US-Neural2-G', label: 'Neural2 G (Female, standard)' },
{ value: 'en-US-Neural2-H', label: 'Neural2 H (Female, standard)' },
{ value: 'en-US-Neural2-I', label: 'Neural2 I (Male, standard)' },
{ value: 'en-US-Neural2-J', label: 'Neural2 J (Male, standard)' }
];
} else if (ttsProvider === 'litellm' || ttsProvider === 'openai') {
ttsVoices = [
{ value: 'alloy', label: 'Alloy (Neutral)' },
{ value: 'echo', label: 'Echo (Male)' },
{ value: 'fable', label: 'Fable (British Male)' },
{ value: 'onyx', label: 'Onyx (Deep Male)' },
{ value: 'nova', label: 'Nova (Female)' },
{ value: 'shimmer', label: 'Shimmer (Soft Female)' }
];
} else if (ttsProvider === 'elevenlabs') {
ttsVoices = [
{ value: 'adam', label: 'Adam (Male, deep)' },
{ value: 'rachel', label: 'Rachel (Female, calm)' },
{ value: 'domi', label: 'Domi (Female, strong)' },
{ value: 'bella', label: 'Bella (Female, soft)' },
{ value: 'antoni', label: 'Antoni (Male, deep)' },
{ value: 'elli', label: 'Elli (Female, young)' },
{ value: 'josh', label: 'Josh (Male, narration)' },
{ value: 'arnold', label: 'Arnold (Male, crisp)' },
{ value: 'sam', label: 'Sam (Male, raspy)' }
];
}
res.json({
success: true,
sttProvider: provider || 'none',
sttModels: sttModels,
ttsProvider: ttsProvider || 'browser',
ttsVoices: ttsVoices
});
} catch (err) {
console.error('[Preferences]', err.message);
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;