pediatric-ai-scribe-v3/src/routes/tts.js
Daniel Onyejesi 4808d08aa7 Fix STT/TTS properly per LiteLLM docs
STT: Vertex AI Chirp not supported via LiteLLM proxy (confirmed by docs).
     Now uses Gemini directly (transcribeGoogle.js) — auto-detected when
     GOOGLE_VERTEX_PROJECT is set, fallback to AWS then OpenAI.

TTS: LiteLLM Vertex TTS DOES work but requires the model_list ALIAS
     (tts-1) not the underlying path (vertex_ai/text-to-speech).
     Also pass voice param — LiteLLM supports Google Cloud voice names.
     Auto-detected when LITELLM_API_BASE is set.
2026-03-30 07:28:47 -04:00

80 lines
3.6 KiB
JavaScript

const express = require('express');
const router = express.Router();
const axios = require('axios');
const { synthesizeWithGoogleTTS, isGoogleTTSConfigured } = require('../utils/ttsGoogle');
const { authMiddleware } = require('../middleware/auth');
// 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: google > litellm > elevenlabs
function getTTSProvider() {
var env = process.env.TTS_PROVIDER;
if (env === 'google') return 'google';
if (env === 'litellm') return 'litellm';
if (env === 'elevenlabs') return 'elevenlabs';
// Auto-detect: litellm > google > elevenlabs
// LiteLLM Vertex TTS works correctly via alias (tts-1)
if (process.env.LITELLM_API_BASE) return 'litellm';
if (isGoogleTTSConfigured()) return 'google';
if (process.env.ELEVENLABS_API_KEY) return 'elevenlabs';
return 'none';
}
var 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, res) => {
try {
var text = (req.body.text || '').substring(0, 5000);
if (!text) return res.status(400).json({ error: 'No text provided' });
if (ttsProvider === 'google') {
var buffer = await synthesizeWithGoogleTTS(text);
res.set('Content-Type', 'audio/mpeg');
return res.send(buffer);
}
if (ttsProvider === 'litellm') {
if (!process.env.LITELLM_API_BASE) return res.status(400).json({ error: 'LITELLM_API_BASE not set.' });
// Model must be the model_name alias from LiteLLM model_list (e.g. tts-1, tts-1-hd)
// NOT the underlying vertex_ai/... path — LiteLLM routes by alias
var ttsModel = process.env.LITELLM_TTS_MODEL || 'tts-1';
var ttsVoice = process.env.LITELLM_TTS_VOICE || 'en-US-Journey-F';
var base = process.env.LITELLM_API_BASE.replace(/\/+$/, '');
var ttsHeaders = { 'Content-Type': 'application/json' };
if (process.env.LITELLM_API_KEY) ttsHeaders['Authorization'] = 'Bearer ' + process.env.LITELLM_API_KEY;
var ttsResp = await axios.post(base + '/v1/audio/speech',
{ model: ttsModel, voice: ttsVoice, input: text },
{ headers: ttsHeaders, responseType: 'arraybuffer', timeout: 60000 }
);
res.set('Content-Type', 'audio/mpeg');
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' });
var 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: text, model_id: 'eleven_turbo_v2_5', voice_settings: { stability: 0.5, similarity_boost: 0.75 } },
responseType: 'arraybuffer'
});
res.set('Content-Type', 'audio/mpeg');
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) {
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;