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:
parent
6a7103a3f9
commit
d5d0ddcb95
4 changed files with 20 additions and 12 deletions
11
.env.example
11
.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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue