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
This commit is contained in:
Daniel Onyejesi 2026-03-25 20:07:10 +00:00
parent 805d0fea55
commit e0757310c8
5 changed files with 56 additions and 11 deletions

View file

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

View file

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

View file

@ -20,23 +20,32 @@
var isPaused = false;
var recognition = createSpeechRecognition();
var finalText = '';
var sessionFinals = '';
function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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 ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
var combined = finalText + sessionFinals;
transcript.innerHTML = escHtml(combined) + (interim ? '<span style="color:#9ca3af;">' + escHtml(interim) + '</span>' : '');
};
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'); }

View file

@ -18,22 +18,31 @@
var isRecording = false;
var recognition = createSpeechRecognition();
var finalText = '';
var sessionFinals = '';
function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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 ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
var combined = finalText + sessionFinals;
transcript.innerHTML = escHtml(combined) + (interim ? '<span style="color:#9ca3af;">' + escHtml(interim) + '</span>' : '');
};
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;

View file

@ -20,22 +20,31 @@
var isPaused = false;
var recognition = createSpeechRecognition();
var finalText = '';
var sessionFinals = '';
function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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 ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
var combined = finalText + sessionFinals;
transcript.innerHTML = escHtml(combined) + (interim ? '<span style="color:#9ca3af;">' + escHtml(interim) + '</span>' : '');
};
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'); }