import express = require('express'); import type { Request, Response } from 'express'; const axios = require('axios'); const { synthesizeWithGoogleTTS, isGoogleTTSConfigured } = require('../utils/ttsGoogle'); const { authMiddleware } = require('../middleware/auth'); const logger = require('../utils/logger'); const { gatewayUrl } = require('../utils/errors'); const router = express.Router(); // Provider priority (auto-detect): // TTS_PROVIDER=google → Google Cloud TTS direct (HIPAA eligible) // TTS_PROVIDER=litellm → LiteLLM proxy // TTS_PROVIDER=elevenlabs → ElevenLabs (not HIPAA) // Auto: litellm > google > elevenlabs function getTTSProvider(): string { const env = process.env.TTS_PROVIDER; if (env === 'google') return 'google'; if (env === 'litellm') return 'litellm'; if (env === 'elevenlabs') return 'elevenlabs'; if (process.env.LITELLM_API_BASE) return 'litellm'; if (isGoogleTTSConfigured()) return 'google'; if (process.env.ELEVENLABS_API_KEY) return 'elevenlabs'; return 'none'; } const ttsProvider = getTTSProvider(); console.log('🔊 TTS provider:', ttsProvider + (ttsProvider === 'google' ? ' (voice: ' + (process.env.GOOGLE_TTS_VOICE || 'en-US-Journey-F') + ')' : '')); router.post('/text-to-speech', authMiddleware, async (req: Request, res: Response) => { try { const text = (req.body.text || '').substring(0, 5000); if (!text) return res.status(400).json({ error: 'No text provided' }); const db = require('../db/database'); const userPrefs = await db.get('SELECT tts_voice FROM users WHERE id = ?', [req.user!.id]); const userVoice = userPrefs?.tts_voice; let adminVoice: string = (await db.getSetting('tts.voice')) || ''; let adminModel: string = (await db.getSetting('tts.model')) || ''; if (!adminModel && adminVoice && (adminVoice.indexOf('/') !== -1 || /^(openai-tts|tts-1|elevenlabs|vertex-gemini.*tts|openai-gpt.*tts)/i.test(adminVoice))) { adminModel = adminVoice; adminVoice = ''; } const VERTEX_VOICES = ['Puck', 'Charon', 'Kore', 'Fenrir', 'Aoede', 'Leda', 'Orus', 'Zephyr']; const resolvedVoice: string = userVoice || adminVoice || ''; if (resolvedVoice) { if (VERTEX_VOICES.indexOf(resolvedVoice) !== -1) adminModel = 'vertex-gemini-2.5-flash-tts'; else if (resolvedVoice.length > 15 && /^[a-zA-Z0-9]+$/.test(resolvedVoice)) adminModel = 'elevenlabs-v3'; } if (ttsProvider === 'google') { const voice = resolvedVoice || process.env.GOOGLE_TTS_VOICE || 'en-US-Journey-F'; const buffer = await synthesizeWithGoogleTTS(text, voice); res.set('Content-Type', 'audio/mpeg'); res.set('X-TTS-Provider', 'google-tts/' + voice); logger.audit(req.user!.id, 'text_to_speech', 'TTS generated', req, { category: 'clinical' }); return res.send(buffer); } if (ttsProvider === 'litellm') { if (!process.env.LITELLM_API_BASE) return res.status(400).json({ error: 'LITELLM_API_BASE not set.' }); const rawModel = adminModel || process.env.LITELLM_TTS_MODEL || 'tts-1'; const ttsModel = rawModel.indexOf('/') !== -1 || rawModel.indexOf('-tts-') !== -1 || rawModel.startsWith('openai-') || rawModel.startsWith('elevenlabs-') || rawModel.startsWith('gemini-') || rawModel.startsWith('vertex-') ? rawModel : 'openai/' + rawModel; const ttsVoice = resolvedVoice || process.env.LITELLM_TTS_VOICE || 'alloy'; const ttsHeaders: Record = { 'Content-Type': 'application/json' }; if (process.env.LITELLM_API_KEY) ttsHeaders['Authorization'] = 'Bearer ' + process.env.LITELLM_API_KEY; const ttsResp = await axios.post(gatewayUrl('/audio/speech'), { model: ttsModel, voice: ttsVoice, input: text }, { headers: ttsHeaders, responseType: 'arraybuffer', timeout: 60000 } ); res.set('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(Buffer.from(ttsResp.data)); } if (ttsProvider === 'elevenlabs') { if (!process.env.ELEVENLABS_API_KEY) return res.status(400).json({ error: 'ElevenLabs not configured' }); const resp = await axios({ method: 'POST', url: 'https://api.elevenlabs.io/v1/text-to-speech/pNInz6obpgDQGcFmaJgB', headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY, 'Content-Type': 'application/json' }, data: { text, model_id: 'eleven_turbo_v2_5', voice_settings: { stability: 0.5, similarity_boost: 0.75 } }, responseType: 'arraybuffer', }); res.set('Content-Type', 'audio/mpeg'); res.set('X-TTS-Provider', 'elevenlabs'); logger.audit(req.user!.id, 'text_to_speech', 'TTS generated', req, { category: 'clinical' }); return res.send(Buffer.from(resp.data)); } res.status(400).json({ error: 'TTS not configured. Set GOOGLE_VERTEX_PROJECT, LITELLM_API_BASE, or ELEVENLABS_API_KEY.' }); } catch (err: any) { const 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 }); } }); export = router;