From 90fccbe58d3451f1ed0504199c867e5af48bca24 Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Sun, 29 Mar 2026 19:11:16 -0400 Subject: [PATCH] Add LiteLLM STT and TTS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.example | 9 ++++++ server.js | 3 +- src/routes/transcribe.js | 43 ++++++++++++++++++++-------- src/routes/tts.js | 61 ++++++++++++++++++++++++++++++++-------- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 6988f42..ab45597 100644 --- a/.env.example +++ b/.env.example @@ -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) diff --git a/server.js b/server.js index c0db386..2e56b34 100644 --- a/server.js +++ b/server.js @@ -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') }); }); diff --git a/src/routes/transcribe.js b/src/routes/transcribe.js index 4eb8950..fcb6d73 100644 --- a/src/routes/transcribe.js +++ b/src/routes/transcribe.js @@ -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({ diff --git a/src/routes/tts.js b/src/routes/tts.js index 20cf60b..f822a42 100644 --- a/src/routes/tts.js +++ b/src/routes/tts.js @@ -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 }); } });