From f397ca77255865fd2dd6029d7e8627419565ce78 Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Wed, 25 Mar 2026 20:07:10 +0000 Subject: [PATCH] v7: fix speech recognition repetition, HTML injection, long-session guard - Fix word repetition: use sessionFinals pattern so each browser SR session starts fresh; no overlap when recognition auto-restarts - Fix HTML injection / '>' parse error: escape < > & in live transcript innerHTML before inserting speech recognition text - Add 24 MB blob guard: fall back to live SR transcript if audio file is too large for Whisper API (long sessions) - Bump version to 7.0.0, update docker-compose image tag to v7 --- docker-compose.yml | 2 +- package.json | 2 +- public/js/liveEncounter.js | 21 ++++++++++++++++++--- public/js/soap.js | 21 ++++++++++++++++++--- public/js/voiceDictation.js | 21 ++++++++++++++++++--- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0bbfaf0..6236b8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: pediatric-scribe: - image: danielonyejesi/pediatric-ai-scribe-v3:v3.1 + image: danielonyejesi/pediatric-ai-scribe-v3:v7 ports: - "3552:3000" env_file: diff --git a/package.json b/package.json index 7eb9c62..1b75350 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pediatric-ai-scribe", - "version": "3.1.0", + "version": "7.0.0", "description": "AI-powered pediatric clinical documentation platform", "main": "server.js", "scripts": { diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js index e8c0f7c..52c545b 100644 --- a/public/js/liveEncounter.js +++ b/public/js/liveEncounter.js @@ -20,23 +20,32 @@ var isPaused = false; var recognition = createSpeechRecognition(); var finalText = ''; + var sessionFinals = ''; + + function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } if (recognition) { recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { - if (e.results[i].isFinal) finalText += e.results[i][0].transcript + ' '; + if (e.results[i].isFinal) sessionFinals += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } - transcript.innerHTML = finalText + (interim ? '' + interim + '' : ''); + var combined = finalText + sessionFinals; + transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : ''); + }; + recognition.onend = function() { + finalText += sessionFinals; + sessionFinals = ''; + if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; - recognition.onend = function() { if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; recognition.onerror = function() {}; } recordBtn.addEventListener('click', function() { if (!isRecording) { finalText = ''; + sessionFinals = ''; recorder.start().then(function() { isRecording = true; isPaused = false; @@ -63,6 +72,12 @@ 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'); } diff --git a/public/js/soap.js b/public/js/soap.js index 61c6f7a..b33f5e5 100644 --- a/public/js/soap.js +++ b/public/js/soap.js @@ -18,22 +18,31 @@ var isRecording = false; var recognition = createSpeechRecognition(); var finalText = ''; + var sessionFinals = ''; + + function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } if (recognition) { recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { - if (e.results[i].isFinal) finalText += e.results[i][0].transcript + ' '; + if (e.results[i].isFinal) sessionFinals += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } - transcript.innerHTML = finalText + (interim ? '' + interim + '' : ''); + var combined = finalText + sessionFinals; + transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : ''); + }; + recognition.onend = function() { + finalText += sessionFinals; + sessionFinals = ''; + if (isRecording) try { recognition.start(); } catch(e) {} }; - recognition.onend = function() { if (isRecording) try { recognition.start(); } catch(e) {} }; } recordBtn.addEventListener('click', function() { if (!isRecording) { finalText = ''; + sessionFinals = ''; recorder.start().then(function() { isRecording = true; recordBtn.classList.add('recording'); @@ -53,6 +62,12 @@ 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; diff --git a/public/js/voiceDictation.js b/public/js/voiceDictation.js index 9c96633..5e50839 100644 --- a/public/js/voiceDictation.js +++ b/public/js/voiceDictation.js @@ -20,22 +20,31 @@ var isPaused = false; var recognition = createSpeechRecognition(); var finalText = ''; + var sessionFinals = ''; + + function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } if (recognition) { recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { - if (e.results[i].isFinal) finalText += e.results[i][0].transcript + ' '; + if (e.results[i].isFinal) sessionFinals += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } - transcript.innerHTML = finalText + (interim ? '' + interim + '' : ''); + var combined = finalText + sessionFinals; + transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : ''); + }; + recognition.onend = function() { + finalText += sessionFinals; + sessionFinals = ''; + if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; - recognition.onend = function() { if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; } recordBtn.addEventListener('click', function() { if (!isRecording) { finalText = ''; + sessionFinals = ''; recorder.start().then(function() { isRecording = true; isPaused = false; @@ -61,6 +70,12 @@ 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'); }