Add LiteLLM STT and TTS support
- TRANSCRIBE_PROVIDER=litellm routes audio to LiteLLM /audio/transcriptions - TTS_PROVIDER=litellm routes to LiteLLM /audio/speech - Both auto-detect when LITELLM_API_BASE is set (no extra config needed) - LITELLM_STT_MODEL (default: whisper-1), LITELLM_TTS_MODEL (default: tts-1) - LITELLM_TTS_VOICE (default: alloy) — alloy/echo/fable/onyx/nova/shimmer - ElevenLabs still works if ELEVENLABS_API_KEY is set and TTS_PROVIDER=elevenlabs - Health endpoint now reports tts provider
This commit is contained in:
parent
3cd7268e5d
commit
90fccbe58d
4 changed files with 93 additions and 23 deletions
|
|
@ -32,6 +32,15 @@ OPENROUTER_API_KEY=sk-or-v1-your-key
|
|||
# LITELLM_API_BASE=http://localhost:4000
|
||||
# LITELLM_API_KEY=sk-litellm-your-key
|
||||
# Admin can discover available models via the admin panel
|
||||
#
|
||||
# LiteLLM Speech-to-Text (auto-enabled when LITELLM_API_BASE is set)
|
||||
# TRANSCRIBE_PROVIDER=litellm
|
||||
# LITELLM_STT_MODEL=whisper-1 # or: groq/whisper-large-v3, openai/whisper-1
|
||||
#
|
||||
# LiteLLM Text-to-Speech (auto-enabled when LITELLM_API_BASE is set)
|
||||
# TTS_PROVIDER=litellm
|
||||
# LITELLM_TTS_MODEL=tts-1 # or: tts-1-hd, openai/tts-1
|
||||
# LITELLM_TTS_VOICE=alloy # alloy, echo, fable, onyx, nova, shimmer
|
||||
|
||||
# ============================================================
|
||||
# TRANSCRIPTION (speech-to-text)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,8 @@ app.get('/api/health', (req, res) => {
|
|||
azure: process.env.AZURE_OPENAI_ENDPOINT ? 'configured' : 'not configured',
|
||||
vertex: process.env.GOOGLE_VERTEX_PROJECT ? 'configured' : 'not configured',
|
||||
litellm: process.env.LITELLM_API_BASE ? 'configured' : 'not configured',
|
||||
whisper: process.env.OPENAI_API_KEY ? 'configured' : 'missing'
|
||||
whisper: process.env.OPENAI_API_KEY ? 'configured' : 'missing',
|
||||
tts: process.env.LITELLM_API_BASE ? 'litellm' : (process.env.ELEVENLABS_API_KEY ? 'elevenlabs' : 'none')
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const { whisperClient } = require('../utils/ai');
|
||||
const { whisperClient, litellmClient } = require('../utils/ai');
|
||||
const { transcribeWithAWS, isAWSTranscribeConfigured } = require('../utils/transcribeAWS');
|
||||
const { transcribeWithLocal, isLocalWhisperConfigured } = require('../utils/transcribeLocal');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
|
@ -9,24 +9,29 @@ const { authMiddleware } = require('../middleware/auth');
|
|||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });
|
||||
|
||||
// Determine which provider to use:
|
||||
// TRANSCRIBE_PROVIDER=aws → always use Amazon Transcribe
|
||||
// TRANSCRIBE_PROVIDER=openai → always use OpenAI Whisper
|
||||
// (default) if AWS Bedrock region is set → use AWS Transcribe
|
||||
// otherwise → use OpenAI Whisper
|
||||
// TRANSCRIBE_PROVIDER=litellm → LiteLLM proxy (any Whisper-compatible model)
|
||||
// TRANSCRIBE_PROVIDER=aws → Amazon Transcribe
|
||||
// TRANSCRIBE_PROVIDER=local → local whisper.cpp / faster-whisper
|
||||
// TRANSCRIBE_PROVIDER=openai → OpenAI Whisper directly
|
||||
// (auto) LITELLM_API_BASE set → litellm
|
||||
// (auto) AWS Bedrock region set → aws
|
||||
// (default) → openai whisper
|
||||
function getTranscribeProvider() {
|
||||
var env = process.env.TRANSCRIBE_PROVIDER;
|
||||
if (env === 'local') return 'local';
|
||||
if (env === 'aws') return 'aws';
|
||||
if (env === 'openai') return 'openai';
|
||||
// Auto-detect: prefer AWS when Bedrock is already configured
|
||||
if (env === 'litellm') return 'litellm';
|
||||
if (env === 'local') return 'local';
|
||||
if (env === 'aws') return 'aws';
|
||||
if (env === 'openai') return 'openai';
|
||||
// Auto-detect
|
||||
if (process.env.LITELLM_API_BASE && litellmClient) return 'litellm';
|
||||
if (isAWSTranscribeConfigured()) return 'aws';
|
||||
return 'openai';
|
||||
}
|
||||
|
||||
// Check if any transcription provider is actually available
|
||||
function isTranscribeAvailable() {
|
||||
if (isLocalWhisperConfigured()) return true;
|
||||
if (isAWSTranscribeConfigured()) return true;
|
||||
if (litellmClient) return true;
|
||||
if (whisperClient) return true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -63,7 +68,23 @@ router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, r
|
|||
return res.json({ success: true, text: text, provider: 'aws-transcribe', duration: Date.now() - startTime });
|
||||
}
|
||||
|
||||
// OpenAI Whisper
|
||||
if (provider === 'litellm') {
|
||||
if (!litellmClient) return res.status(400).json({ error: 'LiteLLM not configured. Set LITELLM_API_BASE.' });
|
||||
var sttModel = process.env.LITELLM_STT_MODEL || 'whisper-1';
|
||||
var file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' });
|
||||
var result = await litellmClient.audio.transcriptions.create({
|
||||
file: file,
|
||||
model: sttModel,
|
||||
language: 'en',
|
||||
response_format: 'text',
|
||||
prompt: 'Medical patient encounter. Pediatric. Clinical terms, diagnoses, medications.'
|
||||
});
|
||||
var text = typeof result === 'string' ? result : result.text;
|
||||
console.log('[Transcribe] LiteLLM (' + sttModel + ') done in ' + (Date.now() - startTime) + 'ms');
|
||||
return res.json({ success: true, text: text, provider: 'litellm-stt', duration: Date.now() - startTime });
|
||||
}
|
||||
|
||||
// OpenAI Whisper (direct)
|
||||
if (!whisperClient) return res.status(400).json({ error: 'Whisper not configured. Set OPENAI_API_KEY.' });
|
||||
var file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' });
|
||||
var result = await whisperClient.audio.transcriptions.create({
|
||||
|
|
|
|||
|
|
@ -1,22 +1,61 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
const { litellmClient } = require('../utils/ai');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
// TTS_PROVIDER=litellm → LiteLLM proxy (routes to any OpenAI-compatible TTS)
|
||||
// TTS_PROVIDER=elevenlabs → ElevenLabs (not HIPAA)
|
||||
// (auto) LITELLM_API_BASE set → litellm
|
||||
// (auto) ELEVENLABS_API_KEY set → elevenlabs
|
||||
function getTTSProvider() {
|
||||
var env = process.env.TTS_PROVIDER;
|
||||
if (env === 'litellm') return 'litellm';
|
||||
if (env === 'elevenlabs') return 'elevenlabs';
|
||||
if (process.env.LITELLM_API_BASE && litellmClient) return 'litellm';
|
||||
if (process.env.ELEVENLABS_API_KEY) return 'elevenlabs';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
var ttsProvider = getTTSProvider();
|
||||
console.log('🔊 TTS provider:', ttsProvider);
|
||||
|
||||
router.post('/text-to-speech', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
if (!process.env.ELEVENLABS_API_KEY) return res.status(400).json({ error: 'Not configured' });
|
||||
const response = 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: req.body.text.substring(0, 5000), model_id: 'eleven_turbo_v2_5', voice_settings: { stability: 0.5, similarity_boost: 0.75 } },
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
res.set('Content-Type', 'audio/mpeg');
|
||||
res.send(Buffer.from(response.data));
|
||||
var text = (req.body.text || '').substring(0, 5000);
|
||||
if (!text) return res.status(400).json({ error: 'No text provided' });
|
||||
|
||||
if (ttsProvider === 'litellm') {
|
||||
if (!litellmClient) return res.status(400).json({ error: 'LiteLLM not configured. Set LITELLM_API_BASE.' });
|
||||
var ttsModel = process.env.LITELLM_TTS_MODEL || 'tts-1';
|
||||
var ttsVoice = process.env.LITELLM_TTS_VOICE || 'alloy';
|
||||
var mp3 = await litellmClient.audio.speech.create({
|
||||
model: ttsModel,
|
||||
voice: ttsVoice,
|
||||
input: text,
|
||||
response_format: 'mp3'
|
||||
});
|
||||
var buffer = Buffer.from(await mp3.arrayBuffer());
|
||||
res.set('Content-Type', 'audio/mpeg');
|
||||
return res.send(buffer);
|
||||
}
|
||||
|
||||
if (ttsProvider === 'elevenlabs') {
|
||||
if (!process.env.ELEVENLABS_API_KEY) return res.status(400).json({ error: 'ElevenLabs not configured' });
|
||||
const response = 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(response.data));
|
||||
}
|
||||
|
||||
res.status(400).json({ error: 'TTS not configured. Set LITELLM_API_BASE or ELEVENLABS_API_KEY.' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'TTS failed' });
|
||||
res.status(500).json({ error: 'TTS failed: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue