const express = require('express'); const router = express.Router(); const { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); var { gatewayUrl } = require('../utils/errors'); var { getLiteLLMTTSRequestOptions, chooseTTS, getTTSProvider } = require('../utils/ttsProvider'); var { getLiteLLMHeaders } = require('../utils/litellm'); // TTS is intentionally routed only through LiteLLM. Provider-specific voice // routing belongs in LiteLLM config, not this app. var ttsProvider = getTTSProvider(); console.log('🔊 TTS provider:', ttsProvider); router.post('/text-to-speech', authMiddleware, require('../utils/policy').requireFeature('read_aloud'), async (req, res) => { try { var text = (req.body.text || '').substring(0, 5000); if (!text) return res.status(400).json({ error: 'No text provided' }); // The user's choice is "model|voice" from the roster; a bare voice is a // choice saved before there was a roster. Either way chooseTTS decides, // and it is the same decision the admin test and the settings page make. var db = require('../db/database'); var userPrefs = await db.get('SELECT tts_voice FROM users WHERE id = ?', [req.user.id]); if (ttsProvider !== 'litellm' || !process.env.LITELLM_API_BASE) { return res.status(400).json({ error: 'TTS not configured. Set LITELLM_API_BASE.' }); } var chosen = chooseTTS({ roster: String(await db.getSetting('tts.roster') || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean), defaultModel: await db.getSetting('tts.model') || process.env.LITELLM_TTS_MODEL || '', defaultVoice: await db.getSetting('tts.voice') || '', envVoice: process.env.LITELLM_TTS_VOICE || '', preferred: userPrefs?.tts_voice }); var ttsModel = chosen.model; var ttsVoice = chosen.voice; if (!ttsModel) return res.status(400).json({ error: 'No LiteLLM TTS model configured.' }); var payload = Object.assign({ model: ttsModel, input: text, voice: ttsVoice }, getLiteLLMTTSRequestOptions(ttsModel)); var ttsResp = await fetch(gatewayUrl('/audio/speech'), { method: 'POST', headers: getLiteLLMHeaders('application/json'), body: JSON.stringify(payload) }); if (!ttsResp.ok) { var errBody = await ttsResp.text(); throw new Error('LiteLLM /audio/speech ' + ttsResp.status + ': ' + errBody.substring(0, 500)); } var audioBuf = Buffer.from(await ttsResp.arrayBuffer()); res.set('Content-Type', ttsResp.headers.get('content-type') || 'audio/mpeg'); res.set('X-TTS-Provider', 'litellm/' + ttsModel); logger.audit(req.user.id, 'text_to_speech', 'TTS generated', req, { category: 'clinical' }); return res.send(audioBuf); } catch (err) { var detail = err.response && err.response.data ? JSON.stringify(err.response.data).substring(0, 500) : err.message; console.error('[TTS] Error (' + ttsProvider + '):', detail); res.status(500).json({ error: 'TTS failed: ' + detail }); } }); module.exports = router;