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.
157 lines
6.1 KiB
JavaScript
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;
|