diff --git a/package.json b/package.json index 1198ff6..1caa55d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pediatric-ai-scribe", - "version": "7.0.0", + "version": "8.0.0", "description": "AI-powered pediatric clinical documentation platform", "main": "server.js", "scripts": { diff --git a/public/js/app.js b/public/js/app.js index cb30b96..e775d0a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -542,9 +542,32 @@ function createSpeechRecognition() { rec.continuous = true; rec.interimResults = true; rec.lang = 'en-US'; + rec.maxAlternatives = 1; return rec; } +// Deduplicate speech recognition finals — Chrome can repeat text across restarts +function deduplicateFinal(newText, existingText) { + if (!newText || !existingText) return newText; + var trimmed = newText.trim(); + if (!trimmed) return ''; + // Check if the new text is already at the end of existing text + if (existingText.trimEnd().endsWith(trimmed)) return ''; + // Check for partial overlap (last sentence repeated) + var words = trimmed.split(/\s+/); + if (words.length >= 3) { + var tail = existingText.trimEnd().split(/\s+/).slice(-words.length).join(' '); + if (tail === trimmed) return ''; + // Check if first half of new text overlaps with end of existing + var half = Math.ceil(words.length / 2); + var firstHalf = words.slice(0, half).join(' '); + if (existingText.trimEnd().endsWith(firstHalf)) { + return words.slice(half).join(' ') + ' '; + } + } + return newText; +} + // PWA Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(function() {}); diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js index 52c545b..b770627 100644 --- a/public/js/liveEncounter.js +++ b/public/js/liveEncounter.js @@ -28,8 +28,13 @@ recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { - if (e.results[i].isFinal) sessionFinals += e.results[i][0].transcript + ' '; - else interim = e.results[i][0].transcript; + if (e.results[i].isFinal) { + var chunk = e.results[i][0].transcript + ' '; + var deduped = deduplicateFinal(chunk, finalText + sessionFinals); + sessionFinals += deduped; + } else { + interim = e.results[i][0].transcript; + } } var combined = finalText + sessionFinals; transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : ''); @@ -39,7 +44,10 @@ sessionFinals = ''; if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; - recognition.onerror = function() {}; + recognition.onerror = function(e) { + if (e.error === 'no-speech' || e.error === 'aborted') return; + console.warn('[SpeechRecognition] error:', e.error); + }; } recordBtn.addEventListener('click', function() { diff --git a/public/js/soap.js b/public/js/soap.js index b33f5e5..f42ce1e 100644 --- a/public/js/soap.js +++ b/public/js/soap.js @@ -26,8 +26,13 @@ recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { - if (e.results[i].isFinal) sessionFinals += e.results[i][0].transcript + ' '; - else interim = e.results[i][0].transcript; + if (e.results[i].isFinal) { + var chunk = e.results[i][0].transcript + ' '; + var deduped = deduplicateFinal(chunk, finalText + sessionFinals); + sessionFinals += deduped; + } else { + interim = e.results[i][0].transcript; + } } var combined = finalText + sessionFinals; transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : ''); diff --git a/public/js/voiceDictation.js b/public/js/voiceDictation.js index 5e50839..f8c5b68 100644 --- a/public/js/voiceDictation.js +++ b/public/js/voiceDictation.js @@ -28,8 +28,13 @@ recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { - if (e.results[i].isFinal) sessionFinals += e.results[i][0].transcript + ' '; - else interim = e.results[i][0].transcript; + if (e.results[i].isFinal) { + var chunk = e.results[i][0].transcript + ' '; + var deduped = deduplicateFinal(chunk, finalText + sessionFinals); + sessionFinals += deduped; + } else { + interim = e.results[i][0].transcript; + } } var combined = finalText + sessionFinals; transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : '');