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 51fc818e4a
commit bf89cad954
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) {
// 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');

View file

@ -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() {

View file

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

View file

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

View file

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

View file

@ -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 {