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 });
}
});