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
This commit is contained in:
parent
28c3758eb6
commit
35f03ac0ba
3 changed files with 32 additions and 14 deletions
|
|
@ -454,11 +454,12 @@ function createTimer(el) {
|
||||||
function AudioRecorder() { this.mediaRecorder = null; this.chunks = []; this.stream = null; }
|
function AudioRecorder() { this.mediaRecorder = null; this.chunks = []; this.stream = null; }
|
||||||
AudioRecorder.prototype.start = function() {
|
AudioRecorder.prototype.start = function() {
|
||||||
var self = this; self.chunks = [];
|
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) {
|
.then(function(stream) {
|
||||||
self.stream = stream;
|
self.stream = stream;
|
||||||
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
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.ondataavailable = function(e) { if (e.data.size > 0) self.chunks.push(e.data); };
|
||||||
self.mediaRecorder.start(1000);
|
self.mediaRecorder.start(1000);
|
||||||
});
|
});
|
||||||
|
|
@ -482,6 +483,7 @@ AudioRecorder.prototype.stop = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
function transcribeAudio(blob) {
|
function transcribeAudio(blob) {
|
||||||
|
var startTime = Date.now();
|
||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
formData.append('audio', blob, 'audio.webm');
|
formData.append('audio', blob, 'audio.webm');
|
||||||
return fetch('/api/transcribe', {
|
return fetch('/api/transcribe', {
|
||||||
|
|
@ -489,8 +491,9 @@ function transcribeAudio(blob) {
|
||||||
headers: { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '') },
|
headers: { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '') },
|
||||||
body: formData
|
body: formData
|
||||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
|
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
if (data.success && data.provider) {
|
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
|
// Delete audio backup on successful transcription
|
||||||
if (data.success && window._lastAudioBackupId) {
|
if (data.success && window._lastAudioBackupId) {
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,14 @@ console.log('🎙️ Transcribe provider:', provider + (provider === 'aws' && m
|
||||||
router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => {
|
router.post('/transcribe', authMiddleware, upload.single('audio'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) return res.status(400).json({ error: 'No audio' });
|
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') {
|
if (provider === 'local') {
|
||||||
var text = await transcribeWithLocal(req.file.buffer, req.file.mimetype || 'audio/webm');
|
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') {
|
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.' });
|
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');
|
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
|
// 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 file = new File([req.file.buffer], 'audio.webm', { type: req.file.mimetype || 'audio/webm' });
|
||||||
var result = await whisperClient.audio.transcriptions.create({
|
var result = await whisperClient.audio.transcriptions.create({
|
||||||
file, model: 'whisper-1', language: 'en',
|
file, model: 'whisper-1', language: 'en',
|
||||||
|
response_format: 'text',
|
||||||
prompt: 'Medical patient encounter. Pediatric. Clinical terms, diagnoses, medications.'
|
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) {
|
} catch (err) {
|
||||||
|
console.error('[Transcribe] Error:', err.message);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,11 @@ function convertToPCM(inputBuffer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function* makeAudioStream(buffer) {
|
async function* makeAudioStream(buffer) {
|
||||||
for (var i = 0; i < buffer.length; i += CHUNK_SIZE) {
|
// Send chunks as fast as possible — AWS handles backpressure via HTTP/2 flow control.
|
||||||
yield { AudioEvent: { AudioChunk: buffer.slice(i, i + CHUNK_SIZE) } };
|
// Larger chunks (32KB) reduce overhead and speed up streaming significantly.
|
||||||
// Small delay between chunks to prevent overwhelming the stream
|
var streamChunkSize = 32768; // 32 KB (AWS max per event frame)
|
||||||
await new Promise(function(r) { setTimeout(r, 10); });
|
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) {
|
async function transcribeWithAWS(audioBuffer, mimeType) {
|
||||||
|
var startTime = Date.now();
|
||||||
var TranscribeModule = require('@aws-sdk/client-transcribe-streaming');
|
var TranscribeModule = require('@aws-sdk/client-transcribe-streaming');
|
||||||
var TranscribeStreamingClient = TranscribeModule.TranscribeStreamingClient;
|
var TranscribeStreamingClient = TranscribeModule.TranscribeStreamingClient;
|
||||||
|
|
||||||
|
|
@ -98,10 +100,11 @@ async function transcribeWithAWS(audioBuffer, mimeType) {
|
||||||
var sampleRate = 48000;
|
var sampleRate = 48000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
var ffStart = Date.now();
|
||||||
audioToSend = await convertToPCM(audioBuffer);
|
audioToSend = await convertToPCM(audioBuffer);
|
||||||
mediaEncoding = 'pcm';
|
mediaEncoding = 'pcm';
|
||||||
sampleRate = 16000;
|
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) {
|
} catch (ffErr) {
|
||||||
console.warn('[Transcribe] ffmpeg unavailable, sending ogg-opus directly:', ffErr.message);
|
console.warn('[Transcribe] ffmpeg unavailable, sending ogg-opus directly:', ffErr.message);
|
||||||
if (mimeType && mimeType.indexOf('wav') !== -1) {
|
if (mimeType && mimeType.indexOf('wav') !== -1) {
|
||||||
|
|
@ -138,6 +141,7 @@ async function transcribeWithAWS(audioBuffer, mimeType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Medical first if enabled, with fallback to Standard
|
// Try Medical first if enabled, with fallback to Standard
|
||||||
|
var streamStart = Date.now();
|
||||||
if (useMedical) {
|
if (useMedical) {
|
||||||
try {
|
try {
|
||||||
var client = new TranscribeStreamingClient({ region: region, credentials: credentials });
|
var client = new TranscribeStreamingClient({ region: region, credentials: credentials });
|
||||||
|
|
@ -151,10 +155,11 @@ async function transcribeWithAWS(audioBuffer, mimeType) {
|
||||||
});
|
});
|
||||||
var medResp = await client.send(medCmd);
|
var medResp = await client.send(medCmd);
|
||||||
transcript = await collectTranscript(medResp.TranscriptResultStream);
|
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) {
|
} catch (medErr) {
|
||||||
logTranscribeError('Medical failed', medErr);
|
logTranscribeError('Medical failed', medErr);
|
||||||
console.warn('[Transcribe] Falling back to standard...');
|
console.warn('[Transcribe] Falling back to standard...');
|
||||||
|
streamStart = Date.now();
|
||||||
try {
|
try {
|
||||||
var client2 = new TranscribeStreamingClient({ region: region, credentials: credentials });
|
var client2 = new TranscribeStreamingClient({ region: region, credentials: credentials });
|
||||||
var stdCmd = new TranscribeModule.StartStreamTranscriptionCommand({
|
var stdCmd = new TranscribeModule.StartStreamTranscriptionCommand({
|
||||||
|
|
@ -165,7 +170,7 @@ async function transcribeWithAWS(audioBuffer, mimeType) {
|
||||||
});
|
});
|
||||||
var stdResp = await client2.send(stdCmd);
|
var stdResp = await client2.send(stdCmd);
|
||||||
transcript = await collectTranscript(stdResp.TranscriptResultStream);
|
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) {
|
} catch (stdErr) {
|
||||||
logTranscribeError('Standard also failed', stdErr);
|
logTranscribeError('Standard also failed', stdErr);
|
||||||
throw stdErr;
|
throw stdErr;
|
||||||
|
|
@ -181,9 +186,10 @@ async function transcribeWithAWS(audioBuffer, mimeType) {
|
||||||
});
|
});
|
||||||
var resp = await client.send(cmd);
|
var resp = await client.send(cmd);
|
||||||
transcript = await collectTranscript(resp.TranscriptResultStream);
|
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();
|
return transcript.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue