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)
This commit is contained in:
ifedan-ed 2026-03-30 13:37:20 +00:00
parent 6a7103a3f9
commit d5d0ddcb95
4 changed files with 20 additions and 12 deletions

View file

@ -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)

View file

@ -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 = '<i class="fas fa-stop"></i> 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;

View file

@ -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());

View file

@ -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));
}