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
5ad1d211eb
commit
b64c967765
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
|
# LiteLLM Speech-to-Text
|
||||||
# TRANSCRIBE_PROVIDER=litellm
|
# 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.
|
# NOTE: vertex_ai/chirp does NOT work via LiteLLM audio proxy.
|
||||||
# For Vertex AI speech, use TRANSCRIBE_PROVIDER=google (Gemini inline audio).
|
# 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)
|
# TTS_PROVIDER=litellm (auto-detected when LITELLM_API_BASE set)
|
||||||
# LITELLM_TTS_MODEL=tts-1 # model_name alias in LiteLLM config
|
# LITELLM_TTS_MODEL=tts-1 # Use model name from your LiteLLM model_list
|
||||||
# LITELLM_TTS_VOICE=en-US-Journey-F # passed to LiteLLM, supports Google Cloud voice names
|
# 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)
|
# TRANSCRIPTION (speech-to-text)
|
||||||
|
|
|
||||||
|
|
@ -389,20 +389,21 @@ function speakText(elementId) {
|
||||||
})
|
})
|
||||||
.then(function(r) {
|
.then(function(r) {
|
||||||
if (!r.ok) throw new Error('TTS request failed (' + r.status + ')');
|
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) {
|
.then(function(result) {
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(result.blob);
|
||||||
currentAudio = new Audio(url);
|
currentAudio = new Audio(url);
|
||||||
currentAudio.onended = function() { URL.revokeObjectURL(url); stopReading(); };
|
currentAudio.onended = function() { URL.revokeObjectURL(url); stopReading(); };
|
||||||
currentAudio.onerror = function() { URL.revokeObjectURL(url); stopReading(); showToast('Audio playback error', 'error'); };
|
currentAudio.onerror = function() { URL.revokeObjectURL(url); stopReading(); showToast('Audio playback error', 'error'); };
|
||||||
currentAudio.play();
|
currentAudio.play();
|
||||||
if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = '<i class="fas fa-stop"></i> Stop'; }
|
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) {
|
.catch(function(err) {
|
||||||
stopReading();
|
stopReading();
|
||||||
// Fallback to browser TTS if ElevenLabs fails
|
// Fallback to browser TTS if server TTS fails
|
||||||
if ('speechSynthesis' in window) {
|
if ('speechSynthesis' in window) {
|
||||||
var utter = new SpeechSynthesisUtterance(text);
|
var utter = new SpeechSynthesisUtterance(text);
|
||||||
utter.rate = 0.9;
|
utter.rate = 0.9;
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ app.use(cors({
|
||||||
if (origin === allowedOrigin) return callback(null, true);
|
if (origin === allowedOrigin) return callback(null, true);
|
||||||
callback(new Error('CORS: origin not allowed'));
|
callback(new Error('CORS: origin not allowed'));
|
||||||
},
|
},
|
||||||
credentials: true
|
credentials: true,
|
||||||
|
exposedHeaders: ['X-TTS-Provider']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,14 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => {
|
||||||
if (ttsProvider === 'google') {
|
if (ttsProvider === 'google') {
|
||||||
var buffer = await synthesizeWithGoogleTTS(text);
|
var buffer = await synthesizeWithGoogleTTS(text);
|
||||||
res.set('Content-Type', 'audio/mpeg');
|
res.set('Content-Type', 'audio/mpeg');
|
||||||
|
res.set('X-TTS-Provider', 'google-tts');
|
||||||
return res.send(buffer);
|
return res.send(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ttsProvider === 'litellm') {
|
if (ttsProvider === 'litellm') {
|
||||||
if (!process.env.LITELLM_API_BASE) return res.status(400).json({ error: 'LITELLM_API_BASE not set.' });
|
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)
|
// Use the full model path (e.g. vertex_ai/google-tts) or the model_name alias
|
||||||
// NOT the underlying vertex_ai/... path — LiteLLM routes by 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 ttsModel = process.env.LITELLM_TTS_MODEL || 'tts-1';
|
||||||
var ttsVoice = process.env.LITELLM_TTS_VOICE || 'en-US-Journey-F';
|
var ttsVoice = process.env.LITELLM_TTS_VOICE || 'en-US-Journey-F';
|
||||||
var base = process.env.LITELLM_API_BASE.replace(/\/+$/, '');
|
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 }
|
{ headers: ttsHeaders, responseType: 'arraybuffer', timeout: 60000 }
|
||||||
);
|
);
|
||||||
res.set('Content-Type', 'audio/mpeg');
|
res.set('Content-Type', 'audio/mpeg');
|
||||||
|
res.set('X-TTS-Provider', 'litellm/' + ttsModel);
|
||||||
return res.send(Buffer.from(ttsResp.data));
|
return res.send(Buffer.from(ttsResp.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +66,7 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => {
|
||||||
responseType: 'arraybuffer'
|
responseType: 'arraybuffer'
|
||||||
});
|
});
|
||||||
res.set('Content-Type', 'audio/mpeg');
|
res.set('Content-Type', 'audio/mpeg');
|
||||||
|
res.set('X-TTS-Provider', 'elevenlabs');
|
||||||
return res.send(Buffer.from(resp.data));
|
return res.send(Buffer.from(resp.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue