From d5d0ddcb9500907fe037a077bd672ce436f3f9b6 Mon Sep 17 00:00:00 2001 From: ifedan-ed Date: Mon, 30 Mar 2026 13:37:20 +0000 Subject: [PATCH] Show TTS provider in toast, support full LiteLLM model paths - TTS response now includes X-TTS-Provider header (google-tts, litellm/model, elevenlabs) - Frontend reads header and shows actual provider in toast instead of hardcoded "Adam/ElevenLabs" - CORS exposes X-TTS-Provider header so frontend can access it - Updated .env.example: clarify that LITELLM_TTS_MODEL and LITELLM_STT_MODEL can be either the model_name alias OR the full provider/model path depending on your LiteLLM config (important for BAA compliance routing) --- .env.example | 11 +++++++---- public/js/app.js | 11 ++++++----- server.js | 3 ++- src/routes/tts.js | 7 +++++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 16b6f50..5951574 100644 --- a/.env.example +++ b/.env.example @@ -44,14 +44,17 @@ OPENROUTER_API_KEY=sk-or-v1-your-key # # LiteLLM Speech-to-Text # TRANSCRIBE_PROVIDER=litellm -# LITELLM_STT_MODEL=whisper-1 # model_name alias in LiteLLM config +# LITELLM_STT_MODEL=whisper-1 # Use the model name from your LiteLLM model_list +# If your LiteLLM config uses full paths as model names, use the full path: +# LITELLM_STT_MODEL=openai/whisper-1 # NOTE: vertex_ai/chirp does NOT work via LiteLLM audio proxy. # For Vertex AI speech, use TRANSCRIBE_PROVIDER=google (Gemini inline audio). # -# LiteLLM TTS: WORKS — use the model_name alias from your LiteLLM model_list +# LiteLLM TTS # TTS_PROVIDER=litellm (auto-detected when LITELLM_API_BASE set) -# LITELLM_TTS_MODEL=tts-1 # model_name alias in LiteLLM config -# LITELLM_TTS_VOICE=en-US-Journey-F # passed to LiteLLM, supports Google Cloud voice names +# LITELLM_TTS_MODEL=tts-1 # Use model name from your LiteLLM model_list +# If your config uses full paths: LITELLM_TTS_MODEL=vertex_ai/google-tts +# LITELLM_TTS_VOICE=en-US-Journey-F # Google Cloud voice name (or alloy/nova for OpenAI) # ============================================================ # TRANSCRIPTION (speech-to-text) diff --git a/public/js/app.js b/public/js/app.js index 6d5e5eb..c625edc 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -389,20 +389,21 @@ function speakText(elementId) { }) .then(function(r) { if (!r.ok) throw new Error('TTS request failed (' + r.status + ')'); - return r.blob(); + var ttsProvider = r.headers.get('X-TTS-Provider') || 'server'; + return r.blob().then(function(blob) { return { blob: blob, provider: ttsProvider }; }); }) - .then(function(blob) { - var url = URL.createObjectURL(blob); + .then(function(result) { + var url = URL.createObjectURL(result.blob); currentAudio = new Audio(url); currentAudio.onended = function() { URL.revokeObjectURL(url); stopReading(); }; currentAudio.onerror = function() { URL.revokeObjectURL(url); stopReading(); showToast('Audio playback error', 'error'); }; currentAudio.play(); if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } - showToast('Reading aloud (Adam/ElevenLabs)', 'info'); + showToast('Reading aloud (' + result.provider + ')', 'info'); }) .catch(function(err) { stopReading(); - // Fallback to browser TTS if ElevenLabs fails + // Fallback to browser TTS if server TTS fails if ('speechSynthesis' in window) { var utter = new SpeechSynthesisUtterance(text); utter.rate = 0.9; diff --git a/server.js b/server.js index 2e56b34..7429ef7 100644 --- a/server.js +++ b/server.js @@ -46,7 +46,8 @@ app.use(cors({ if (origin === allowedOrigin) return callback(null, true); callback(new Error('CORS: origin not allowed')); }, - credentials: true + credentials: true, + exposedHeaders: ['X-TTS-Provider'] })); app.use(cookieParser()); diff --git a/src/routes/tts.js b/src/routes/tts.js index 17f61b4..e6f6b01 100644 --- a/src/routes/tts.js +++ b/src/routes/tts.js @@ -34,13 +34,14 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => { if (ttsProvider === 'google') { var buffer = await synthesizeWithGoogleTTS(text); res.set('Content-Type', 'audio/mpeg'); + res.set('X-TTS-Provider', 'google-tts'); 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 + // Use the full model path (e.g. vertex_ai/google-tts) or the model_name alias + // from your LiteLLM model_list. Full path is safer if your config uses it directly. 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(/\/+$/, ''); @@ -51,6 +52,7 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => { { headers: ttsHeaders, responseType: 'arraybuffer', timeout: 60000 } ); res.set('Content-Type', 'audio/mpeg'); + res.set('X-TTS-Provider', 'litellm/' + ttsModel); return res.send(Buffer.from(ttsResp.data)); } @@ -64,6 +66,7 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => { responseType: 'arraybuffer' }); res.set('Content-Type', 'audio/mpeg'); + res.set('X-TTS-Provider', 'elevenlabs'); return res.send(Buffer.from(resp.data)); }