From 35f03ac0baa9069d72d5960c6f7befd5ff50d71a Mon Sep 17 00:00:00 2001 From: ifedan-ed Date: Sun, 29 Mar 2026 00:59:30 +0000 Subject: [PATCH] Optimize transcription speed: remove artificial delays, add timing - AWS Transcribe: remove 10ms delay between chunks (was adding ~1.25s/MB), increase chunk size from 8KB to 32KB (AWS max per frame) - Add detailed timing logs (ffmpeg, streaming, total) for diagnostics - OpenAI Whisper: use response_format='text' for faster response parsing - Frontend: show transcription time in toast, request 16kHz sample rate, increase bitrate to 32kbps Opus (better quality, still small files) - Return duration in API response for all providers --- public/js/app.js | 9 ++++++--- src/routes/transcribe.js | 15 ++++++++++++--- src/utils/transcribeAWS.js | 22 ++++++++++++++-------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 683b31f..542b9ae 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -454,11 +454,12 @@ function createTimer(el) { function AudioRecorder() { this.mediaRecorder = null; this.chunks = []; this.stream = null; } AudioRecorder.prototype.start = function() { var self = this; self.chunks = []; - return navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } }) + return navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true } }) .then(function(stream) { self.stream = stream; var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; - self.mediaRecorder = new MediaRecorder(stream, { mimeType: mime, audioBitsPerSecond: 16000 }); + // 32kbps Opus is excellent for speech — small files, fast upload, great quality + self.mediaRecorder = new MediaRecorder(stream, { mimeType: mime, audioBitsPerSecond: 32000 }); self.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) self.chunks.push(e.data); }; self.mediaRecorder.start(1000); }); @@ -482,6 +483,7 @@ AudioRecorder.prototype.stop = function() { }; function transcribeAudio(blob) { + var startTime = Date.now(); var formData = new FormData(); formData.append('audio', blob, 'audio.webm'); return fetch('/api/transcribe', { @@ -489,8 +491,9 @@ function transcribeAudio(blob) { headers: { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '') }, body: formData }).then(function(r) { return r.json(); }).then(function(data) { + var elapsed = ((Date.now() - startTime) / 1000).toFixed(1); if (data.success && data.provider) { - showToast('Transcribed via ' + data.provider, 'info'); + showToast('Transcribed via ' + data.provider + ' (' + elapsed + 's)', 'info'); } // Delete audio backup on successful transcription if (data.success && window._lastAudioBackupId) { diff --git a/src/routes/transcribe.js b/src/routes/transcribe.js index 47e0fb8..f1d29ce 100644 --- a/src/routes/transcribe.js +++ b/src/routes/transcribe.js @@ -30,10 +30,14 @@ console.log('🎙️ Transcribe provider:', provider + (provider === 'aws' && m router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => { try { if (!req.file) return res.status(400).json({ error: 'No audio' }); + var startTime = Date.now(); + var fileSize = req.file.size; + console.log('[Transcribe] Received ' + (fileSize / 1024).toFixed(0) + 'KB audio (' + (req.file.mimetype || 'unknown') + ') via ' + provider); if (provider === 'local') { var text = await transcribeWithLocal(req.file.buffer, req.file.mimetype || 'audio/webm'); - return res.json({ success: true, text: text, provider: 'local-whisper' }); + console.log('[Transcribe] Local done in ' + (Date.now() - startTime) + 'ms'); + return res.json({ success: true, text: text, provider: 'local-whisper', duration: Date.now() - startTime }); } if (provider === 'aws') { @@ -41,7 +45,8 @@ router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, r return res.status(400).json({ error: 'AWS Transcribe not configured. Set AWS_BEDROCK_REGION.' }); } var text = await transcribeWithAWS(req.file.buffer, req.file.mimetype || 'audio/webm'); - return res.json({ success: true, text: text, provider: 'aws-transcribe' }); + console.log('[Transcribe] AWS done in ' + (Date.now() - startTime) + 'ms'); + return res.json({ success: true, text: text, provider: 'aws-transcribe', duration: Date.now() - startTime }); } // OpenAI Whisper @@ -49,10 +54,14 @@ router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, r var file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' }); var result = await whisperClient.audio.transcriptions.create({ file, model: 'whisper-1', language: 'en', + response_format: 'text', prompt: 'Medical patient encounter. Pediatric. Clinical terms, diagnoses, medications.' }); - res.json({ success: true, text: result.text, provider: 'openai-whisper' }); + var text = typeof result === 'string' ? result : result.text; + console.log('[Transcribe] Whisper done in ' + (Date.now() - startTime) + 'ms'); + res.json({ success: true, text: text, provider: 'openai-whisper', duration: Date.now() - startTime }); } catch (err) { + console.error('[Transcribe] Error:', err.message); res.status(500).json({ error: err.message }); } }); diff --git a/src/utils/transcribeAWS.js b/src/utils/transcribeAWS.js index 66f27bd..2e8b414 100644 --- a/src/utils/transcribeAWS.js +++ b/src/utils/transcribeAWS.js @@ -59,10 +59,11 @@ function convertToPCM(inputBuffer) { } async function* makeAudioStream(buffer) { - for (var i = 0; i < buffer.length; i += CHUNK_SIZE) { - yield { AudioEvent: { AudioChunk: buffer.slice(i, i + CHUNK_SIZE) } }; - // Small delay between chunks to prevent overwhelming the stream - await new Promise(function(r) { setTimeout(r, 10); }); + // Send chunks as fast as possible — AWS handles backpressure via HTTP/2 flow control. + // Larger chunks (32KB) reduce overhead and speed up streaming significantly. + var streamChunkSize = 32768; // 32 KB (AWS max per event frame) + for (var i = 0; i < buffer.length; i += streamChunkSize) { + yield { AudioEvent: { AudioChunk: buffer.slice(i, i + streamChunkSize) } }; } } @@ -82,6 +83,7 @@ function logTranscribeError(label, err) { } async function transcribeWithAWS(audioBuffer, mimeType) { + var startTime = Date.now(); var TranscribeModule = require('@aws-sdk/client-transcribe-streaming'); var TranscribeStreamingClient = TranscribeModule.TranscribeStreamingClient; @@ -98,10 +100,11 @@ async function transcribeWithAWS(audioBuffer, mimeType) { var sampleRate = 48000; try { + var ffStart = Date.now(); audioToSend = await convertToPCM(audioBuffer); mediaEncoding = 'pcm'; sampleRate = 16000; - console.log('[Transcribe] ffmpeg conversion OK — PCM 16kHz, ' + audioToSend.length + ' bytes'); + console.log('[Transcribe] ffmpeg: ' + (Date.now() - ffStart) + 'ms — ' + audioBuffer.length + ' → ' + audioToSend.length + ' bytes'); } catch (ffErr) { console.warn('[Transcribe] ffmpeg unavailable, sending ogg-opus directly:', ffErr.message); if (mimeType && mimeType.indexOf('wav') !== -1) { @@ -138,6 +141,7 @@ async function transcribeWithAWS(audioBuffer, mimeType) { } // Try Medical first if enabled, with fallback to Standard + var streamStart = Date.now(); if (useMedical) { try { var client = new TranscribeStreamingClient({ region: region, credentials: credentials }); @@ -151,10 +155,11 @@ async function transcribeWithAWS(audioBuffer, mimeType) { }); var medResp = await client.send(medCmd); transcript = await collectTranscript(medResp.TranscriptResultStream); - console.log('[Transcribe] Medical OK (' + transcript.length + ' chars)'); + console.log('[Transcribe] Medical OK — ' + (Date.now() - streamStart) + 'ms, ' + transcript.length + ' chars'); } catch (medErr) { logTranscribeError('Medical failed', medErr); console.warn('[Transcribe] Falling back to standard...'); + streamStart = Date.now(); try { var client2 = new TranscribeStreamingClient({ region: region, credentials: credentials }); var stdCmd = new TranscribeModule.StartStreamTranscriptionCommand({ @@ -165,7 +170,7 @@ async function transcribeWithAWS(audioBuffer, mimeType) { }); var stdResp = await client2.send(stdCmd); transcript = await collectTranscript(stdResp.TranscriptResultStream); - console.log('[Transcribe] Standard fallback OK (' + transcript.length + ' chars)'); + console.log('[Transcribe] Standard fallback OK — ' + (Date.now() - streamStart) + 'ms, ' + transcript.length + ' chars'); } catch (stdErr) { logTranscribeError('Standard also failed', stdErr); throw stdErr; @@ -181,9 +186,10 @@ async function transcribeWithAWS(audioBuffer, mimeType) { }); var resp = await client.send(cmd); transcript = await collectTranscript(resp.TranscriptResultStream); - console.log('[Transcribe] Standard OK (' + transcript.length + ' chars)'); + console.log('[Transcribe] Standard OK — ' + (Date.now() - streamStart) + 'ms, ' + transcript.length + ' chars'); } + console.log('[Transcribe] Total: ' + (Date.now() - startTime) + 'ms'); return transcript.trim(); }