Zero-config speech: skip upload when no transcription API, use browser speech directly

- 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
This commit is contained in:
ifedan-ed 2026-03-29 02:09:53 +00:00
parent fe632985c1
commit e58876ddd9
6 changed files with 112 additions and 46 deletions

View file

@ -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) { 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 startTime = Date.now();
var formData = new FormData(); var formData = new FormData();
formData.append('audio', blob, 'audio.webm'); formData.append('audio', blob, 'audio.webm');

View file

@ -167,6 +167,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Load announcement banner if function is available // Load announcement banner if function is available
if (typeof loadAnnouncement === 'function') loadAnnouncement(); if (typeof loadAnnouncement === 'function') loadAnnouncement();
// Check if server-side transcription is configured
if (typeof checkTranscribeStatus === 'function') checkTranscribeStatus();
} }
function exitApp() { function exitApp() {

View file

@ -78,21 +78,30 @@
if (recognition) try { recognition.stop(); } catch(e) {} if (recognition) try { recognition.stop(); } catch(e) {}
document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } })); document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } }));
showLoading('Transcribing...'); // If no server transcription API, use live transcript directly (no upload)
recorder.stop().then(function(blob) { var liveText = (finalText + sessionFinals).trim();
if (!blob || blob.size === 0) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; } if (window._transcribeAvailable === false) {
if (blob.size > 24 * 1024 * 1024) { recorder.stop().then(function() {});
hideLoading(); if (liveText) transcript.textContent = liveText;
transcript.textContent = finalText.trim(); showToast('Using browser speech recognition (no transcription API configured)', 'info');
showToast('Recording too large for AI transcription — using live transcript', 'info'); } else {
return; showLoading('Transcribing...');
} recorder.stop().then(function(blob) {
return transcribeAudio(blob).then(function(data) { if (!blob || blob.size === 0) { hideLoading(); if (liveText) transcript.textContent = liveText; return; }
hideLoading(); if (blob.size > 24 * 1024 * 1024) {
if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } hideLoading();
else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Failed', 'error'); } transcript.textContent = liveText;
}); showToast('Recording too large for AI transcription — using live transcript', 'info');
}).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(err.message, 'error'); }); 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'); });
}
} }
}); });

View file

@ -65,21 +65,29 @@
indicator.classList.add('hidden'); indicator.classList.add('hidden');
if (recognition) try { recognition.stop(); } catch(e) {} if (recognition) try { recognition.stop(); } catch(e) {}
showLoading('Transcribing...'); var liveText = (finalText + sessionFinals).trim();
recorder.stop().then(function(blob) { if (window._transcribeAvailable === false) {
if (!blob) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; } recorder.stop().then(function() {});
if (blob.size > 24 * 1024 * 1024) { if (liveText) transcript.textContent = liveText;
hideLoading(); showToast('Using browser speech recognition', 'info');
transcript.textContent = finalText.trim(); } else {
showToast('Recording too large for AI transcription — using live transcript', 'info'); showLoading('Transcribing...');
return; recorder.stop().then(function(blob) {
} if (!blob) { hideLoading(); if (liveText) transcript.textContent = liveText; return; }
return transcribeAudio(blob).then(function(data) { if (blob.size > 24 * 1024 * 1024) {
hideLoading(); hideLoading();
if (data.success) transcript.textContent = data.text; transcript.textContent = liveText;
else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Transcription failed — using live transcript', 'error'); } showToast('Recording too large for AI transcription — using live transcript', 'info');
}); return;
}).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); }); }
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'); });
}
} }
}); });

View file

@ -73,21 +73,29 @@
if (recognition) try { recognition.stop(); } catch(e) {} if (recognition) try { recognition.stop(); } catch(e) {}
document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'dict' } })); document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'dict' } }));
showLoading('Transcribing...'); var liveText = (finalText + sessionFinals).trim();
recorder.stop().then(function(blob) { if (window._transcribeAvailable === false) {
if (!blob || blob.size === 0) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; } recorder.stop().then(function() {});
if (blob.size > 24 * 1024 * 1024) { if (liveText) transcript.textContent = liveText;
hideLoading(); showToast('Using browser speech recognition (no transcription API configured)', 'info');
transcript.textContent = finalText.trim(); } else {
showToast('Recording too large for AI transcription — using live transcript', 'info'); showLoading('Transcribing...');
return; recorder.stop().then(function(blob) {
} if (!blob || blob.size === 0) { hideLoading(); if (liveText) transcript.textContent = liveText; return; }
return transcribeAudio(blob).then(function(data) { if (blob.size > 24 * 1024 * 1024) {
hideLoading(); hideLoading();
if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } transcript.textContent = liveText;
else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Transcription failed — using live transcript', 'error'); } showToast('Recording too large for AI transcription — using live transcript', 'info');
}); return;
}).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); }); }
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'); });
}
} }
}); });

View file

@ -23,9 +23,23 @@ function getTranscribeProvider() {
return 'openai'; 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 provider = getTranscribeProvider();
var medical = process.env.AWS_TRANSCRIBE_MEDICAL === 'true'; 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) => { router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => {
try { try {