From e6091c299f06f78cbb0e82b5a35b06978f9dd3ce Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Sun, 29 Mar 2026 22:09:56 -0400 Subject: [PATCH] Fix TTS axios/Vertex, generic toast, add stop button to encounter - TTS: switch from OpenAI SDK to axios (same fix as STT), drop voice param since it's configured inside LiteLLM per model - Fix 'ElevenLabs unavailable' toast shown even when provider is LiteLLM - Add dedicated red Stop button to encounter recording UI --- public/components/encounter.html | 1 + public/js/app.js | 2 +- public/js/liveEncounter.js | 10 ++++++++++ src/routes/tts.js | 30 ++++++++++++++++++------------ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/public/components/encounter.html b/public/components/encounter.html index 0de9f5a..37139c0 100644 --- a/public/components/encounter.html +++ b/public/components/encounter.html @@ -50,6 +50,7 @@
+
diff --git a/public/js/app.js b/public/js/app.js index 2d56d30..6d5e5eb 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -410,7 +410,7 @@ function speakText(elementId) { currentlyReadingId = elementId; if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } window.speechSynthesis.speak(utter); - showToast('ElevenLabs unavailable — using browser voice', 'info'); + showToast('TTS unavailable — using browser voice', 'info'); } else { showToast('Read aloud failed: ' + err.message, 'error'); } diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js index 979f895..f713bb8 100644 --- a/public/js/liveEncounter.js +++ b/public/js/liveEncounter.js @@ -14,6 +14,7 @@ var hpiText = document.getElementById('enc-hpi-text'); var modelTag = document.getElementById('enc-model-tag'); + var stopBtn = document.getElementById('enc-stop-btn'); var recorder = new AudioRecorder(); recorder._module = 'encounter'; var timer = createTimer(timerEl); @@ -61,6 +62,7 @@ recordBtn.classList.add('recording'); recordBtn.querySelector('span').textContent = 'Stop'; if (pauseBtn) pauseBtn.classList.remove('hidden'); + if (stopBtn) stopBtn.classList.remove('hidden'); indicator.classList.remove('hidden'); timer.start(); if (recognition) try { recognition.start(); } catch(e) {} @@ -74,6 +76,7 @@ recordBtn.classList.remove('recording'); recordBtn.querySelector('span').textContent = 'Start Recording'; if (pauseBtn) { pauseBtn.classList.add('hidden'); pauseBtn.innerHTML = ' Pause'; } + if (stopBtn) stopBtn.classList.add('hidden'); indicator.classList.add('hidden'); if (recognition) try { recognition.stop(); } catch(e) {} document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } })); @@ -105,6 +108,13 @@ } }); + // Dedicated stop button (always visible during recording) + if (stopBtn) { + stopBtn.addEventListener('click', function() { + if (isRecording) recordBtn.click(); + }); + } + // Pause / Resume if (pauseBtn) { pauseBtn.addEventListener('click', function() { diff --git a/src/routes/tts.js b/src/routes/tts.js index f822a42..26b958b 100644 --- a/src/routes/tts.js +++ b/src/routes/tts.js @@ -1,18 +1,17 @@ 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=litellm → LiteLLM proxy (Vertex AI TTS, OpenAI TTS, etc.) // 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 === 'litellm') return 'litellm'; if (env === 'elevenlabs') return 'elevenlabs'; - if (process.env.LITELLM_API_BASE && litellmClient) return 'litellm'; + if (process.env.LITELLM_API_BASE) return 'litellm'; if (process.env.ELEVENLABS_API_KEY) return 'elevenlabs'; return 'none'; } @@ -26,18 +25,24 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => { 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.' }); + if (!process.env.LITELLM_API_BASE) return res.status(400).json({ error: 'LITELLM_API_BASE not set.' }); var ttsModel = process.env.LITELLM_TTS_MODEL || 'tts-1'; - var ttsVoice = process.env.LITELLM_TTS_VOICE || 'alloy'; - var mp3 = await litellmClient.audio.speech.create({ + var base = process.env.LITELLM_API_BASE.replace(/\/+$/, ''); + var ttsHeaders = { 'Content-Type': 'application/json' }; + if (process.env.LITELLM_API_KEY) ttsHeaders['Authorization'] = 'Bearer ' + process.env.LITELLM_API_KEY; + + // Send only model + input — voice is configured inside LiteLLM per model + var ttsResp = await axios.post(base + '/v1/audio/speech', { model: ttsModel, - voice: ttsVoice, - input: text, - response_format: 'mp3' + input: text + }, { + headers: ttsHeaders, + responseType: 'arraybuffer', + timeout: 60000 }); - var buffer = Buffer.from(await mp3.arrayBuffer()); + res.set('Content-Type', 'audio/mpeg'); - return res.send(buffer); + return res.send(Buffer.from(ttsResp.data)); } if (ttsProvider === 'elevenlabs') { @@ -55,6 +60,7 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => { res.status(400).json({ error: 'TTS not configured. Set LITELLM_API_BASE or ELEVENLABS_API_KEY.' }); } catch (err) { + console.error('[TTS] Error:', err.message); res.status(500).json({ error: 'TTS failed: ' + err.message }); } });