From bf89cad95487aedf9cd890d3a506c540680a3d38 Mon Sep 17 00:00:00 2001 From: ifedan-ed Date: Sun, 29 Mar 2026 02:09:53 +0000 Subject: [PATCH] Zero-config speech: skip upload when no transcription API, use browser speech directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GET /api/transcribe/status endpoint — returns whether any server transcription provider (Whisper/AWS/Local) is configured - Frontend checks status on login via checkTranscribeStatus() - When no provider configured: recording stops instantly, keeps live Web Speech API text, shows friendly toast — no error, no upload wait - Works in encounter, dictation, and SOAP tabs - App now works fully out-of-the-box with just an AI provider key --- public/js/app.js | 24 +++++++++++++++++++++++ public/js/auth.js | 3 +++ public/js/liveEncounter.js | 39 +++++++++++++++++++++++-------------- public/js/soap.js | 38 ++++++++++++++++++++++-------------- public/js/voiceDictation.js | 38 ++++++++++++++++++++++-------------- src/routes/transcribe.js | 16 ++++++++++++++- 6 files changed, 112 insertions(+), 46 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 542b9ae..2d56d30 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -482,7 +482,31 @@ AudioRecorder.prototype.stop = function() { }); }; +// Check if server-side transcription (Whisper/AWS) is available +window._transcribeAvailable = null; // null = not checked yet, true/false after check +function checkTranscribeStatus() { + var token = window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || ''; + if (!token) return; + fetch('/api/transcribe/status', { + headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + window._transcribeAvailable = !!data.available; + window._transcribeProvider = data.provider || 'none'; + if (!data.available) { + console.log('[Transcribe] No server transcription configured — using browser speech recognition only'); + } + }) + .catch(function() { window._transcribeAvailable = false; }); +} + function transcribeAudio(blob) { + // If no server transcription is configured, skip upload entirely + if (window._transcribeAvailable === false) { + return Promise.resolve({ success: false, noProvider: true, error: 'No transcription API configured — using live transcript' }); + } + var startTime = Date.now(); var formData = new FormData(); formData.append('audio', blob, 'audio.webm'); diff --git a/public/js/auth.js b/public/js/auth.js index 57da1d8..09fa36f 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -167,6 +167,9 @@ document.addEventListener('DOMContentLoaded', function() { // Load announcement banner if function is available if (typeof loadAnnouncement === 'function') loadAnnouncement(); + + // Check if server-side transcription is configured + if (typeof checkTranscribeStatus === 'function') checkTranscribeStatus(); } function exitApp() { diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js index 2972cab..979f895 100644 --- a/public/js/liveEncounter.js +++ b/public/js/liveEncounter.js @@ -78,21 +78,30 @@ if (recognition) try { recognition.stop(); } catch(e) {} document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } })); - showLoading('Transcribing...'); - recorder.stop().then(function(blob) { - if (!blob || blob.size === 0) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; } - if (blob.size > 24 * 1024 * 1024) { - hideLoading(); - transcript.textContent = finalText.trim(); - showToast('Recording too large for AI transcription — using live transcript', 'info'); - return; - } - return transcribeAudio(blob).then(function(data) { - hideLoading(); - if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } - else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Failed', 'error'); } - }); - }).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(err.message, 'error'); }); + // If no server transcription API, use live transcript directly (no upload) + var liveText = (finalText + sessionFinals).trim(); + if (window._transcribeAvailable === false) { + recorder.stop().then(function() {}); + if (liveText) transcript.textContent = liveText; + showToast('Using browser speech recognition (no transcription API configured)', 'info'); + } else { + showLoading('Transcribing...'); + recorder.stop().then(function(blob) { + if (!blob || blob.size === 0) { hideLoading(); if (liveText) transcript.textContent = liveText; return; } + if (blob.size > 24 * 1024 * 1024) { + hideLoading(); + transcript.textContent = liveText; + showToast('Recording too large for AI transcription — using live transcript', 'info'); + return; + } + return transcribeAudio(blob).then(function(data) { + hideLoading(); + if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } + else if (data.noProvider) { if (liveText) transcript.textContent = liveText; showToast('Using live transcript', 'info'); } + else { if (liveText) transcript.textContent = liveText; showToast(data.error || 'Failed', 'error'); } + }); + }).catch(function(err) { hideLoading(); if (liveText) transcript.textContent = liveText; showToast(err.message, 'error'); }); + } } }); diff --git a/public/js/soap.js b/public/js/soap.js index 94ff92c..1e7f6e5 100644 --- a/public/js/soap.js +++ b/public/js/soap.js @@ -65,21 +65,29 @@ indicator.classList.add('hidden'); if (recognition) try { recognition.stop(); } catch(e) {} - showLoading('Transcribing...'); - recorder.stop().then(function(blob) { - if (!blob) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; } - if (blob.size > 24 * 1024 * 1024) { - hideLoading(); - transcript.textContent = finalText.trim(); - showToast('Recording too large for AI transcription — using live transcript', 'info'); - return; - } - return transcribeAudio(blob).then(function(data) { - hideLoading(); - if (data.success) transcript.textContent = data.text; - else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Transcription failed — using live transcript', 'error'); } - }); - }).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); }); + var liveText = (finalText + sessionFinals).trim(); + if (window._transcribeAvailable === false) { + recorder.stop().then(function() {}); + if (liveText) transcript.textContent = liveText; + showToast('Using browser speech recognition', 'info'); + } else { + showLoading('Transcribing...'); + recorder.stop().then(function(blob) { + if (!blob) { hideLoading(); if (liveText) transcript.textContent = liveText; return; } + if (blob.size > 24 * 1024 * 1024) { + hideLoading(); + transcript.textContent = liveText; + showToast('Recording too large for AI transcription — using live transcript', 'info'); + return; + } + return transcribeAudio(blob).then(function(data) { + hideLoading(); + if (data.success) transcript.textContent = data.text; + else if (data.noProvider) { if (liveText) transcript.textContent = liveText; showToast('Using live transcript', 'info'); } + else { if (liveText) transcript.textContent = liveText; showToast(data.error || 'Transcription failed — using live transcript', 'error'); } + }); + }).catch(function(err) { hideLoading(); if (liveText) transcript.textContent = liveText; showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); }); + } } }); diff --git a/public/js/voiceDictation.js b/public/js/voiceDictation.js index c49adf7..ac23a57 100644 --- a/public/js/voiceDictation.js +++ b/public/js/voiceDictation.js @@ -73,21 +73,29 @@ if (recognition) try { recognition.stop(); } catch(e) {} document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'dict' } })); - showLoading('Transcribing...'); - recorder.stop().then(function(blob) { - if (!blob || blob.size === 0) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; } - if (blob.size > 24 * 1024 * 1024) { - hideLoading(); - transcript.textContent = finalText.trim(); - showToast('Recording too large for AI transcription — using live transcript', 'info'); - return; - } - return transcribeAudio(blob).then(function(data) { - hideLoading(); - if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } - else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Transcription failed — using live transcript', 'error'); } - }); - }).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); }); + var liveText = (finalText + sessionFinals).trim(); + if (window._transcribeAvailable === false) { + recorder.stop().then(function() {}); + if (liveText) transcript.textContent = liveText; + showToast('Using browser speech recognition (no transcription API configured)', 'info'); + } else { + showLoading('Transcribing...'); + recorder.stop().then(function(blob) { + if (!blob || blob.size === 0) { hideLoading(); if (liveText) transcript.textContent = liveText; return; } + if (blob.size > 24 * 1024 * 1024) { + hideLoading(); + transcript.textContent = liveText; + showToast('Recording too large for AI transcription — using live transcript', 'info'); + return; + } + return transcribeAudio(blob).then(function(data) { + hideLoading(); + if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } + else if (data.noProvider) { if (liveText) transcript.textContent = liveText; showToast('Using live transcript', 'info'); } + else { if (liveText) transcript.textContent = liveText; showToast(data.error || 'Transcription failed — using live transcript', 'error'); } + }); + }).catch(function(err) { hideLoading(); if (liveText) transcript.textContent = liveText; showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); }); + } } }); diff --git a/src/routes/transcribe.js b/src/routes/transcribe.js index f1d29ce..4eb8950 100644 --- a/src/routes/transcribe.js +++ b/src/routes/transcribe.js @@ -23,9 +23,23 @@ function getTranscribeProvider() { return 'openai'; } +// Check if any transcription provider is actually available +function isTranscribeAvailable() { + if (isLocalWhisperConfigured()) return true; + if (isAWSTranscribeConfigured()) return true; + if (whisperClient) return true; + return false; +} + var provider = getTranscribeProvider(); var medical = process.env.AWS_TRANSCRIBE_MEDICAL === 'true'; -console.log('🎙️ Transcribe provider:', provider + (provider === 'aws' && medical ? ' (Medical)' : '')); +var available = isTranscribeAvailable(); +console.log('🎙️ Transcribe provider:', provider + (provider === 'aws' && medical ? ' (Medical)' : '') + (available ? '' : ' (NOT CONFIGURED — browser speech only)')); + +// Status endpoint — frontend checks this to decide whether to upload audio +router.get('/transcribe/status', authMiddleware, (req, res) => { + res.json({ available: available, provider: available ? provider : 'none' }); +}); router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => { try {