diff --git a/public/index.html b/public/index.html index e1217ea..e280f42 100644 --- a/public/index.html +++ b/public/index.html @@ -475,8 +475,8 @@ integrity="sha384-JUh163oCRItcbPme8pYnROHQMC6fNKTBWtRG3I3I0erJkzNgL7uxKlNwcrcFKeqF" crossorigin="anonymous" referrerpolicy="no-referrer" defer> - - + + diff --git a/public/js/audioBackup.js b/public/js/audioBackup.js index fbeb206..fb1fb3b 100644 --- a/public/js/audioBackup.js +++ b/public/js/audioBackup.js @@ -4,13 +4,12 @@ // Falls back to IndexedDB if server save fails. // ============================================================ -(function() { - var DB_NAME = 'PedScribeAudioBackup'; - var STORE_NAME = 'recordings'; - var DB_VERSION = 1; - var MAX_AGE_MS = 24 * 60 * 60 * 1000; +var DB_NAME = 'PedScribeAudioBackup'; +var STORE_NAME = 'recordings'; +var DB_VERSION = 1; +var MAX_AGE_MS = 24 * 60 * 60 * 1000; - var _db = null; +var _db = null; function openDB() { if (_db) return Promise.resolve(_db); @@ -293,5 +292,4 @@ return String(str).replace(/&/g, '&').replace(//g, '>'); } - cleanupOldLocalBackups(); -})(); +cleanupOldLocalBackups(); diff --git a/public/js/speechRecognition.js b/public/js/speechRecognition.js index 12a6aa5..d8c62ad 100644 --- a/public/js/speechRecognition.js +++ b/public/js/speechRecognition.js @@ -5,156 +5,152 @@ // Only use if you accept this trade-off for real-time streaming // ============================================================ -(function() { +var STORAGE_ENABLED = 'ped_web_speech_enabled'; - var STORAGE_ENABLED = 'ped_web_speech_enabled'; +var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; +var _recognition = null; +var _transcript = ''; +var _isListening = false; +var _onPartialCallback = null; +var _onFinalCallback = null; - var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; - var _recognition = null; - var _transcript = ''; - var _isListening = false; - var _onPartialCallback = null; - var _onFinalCallback = null; +window.WebSpeechRecognition = { - window.WebSpeechRecognition = { + isSupported: function() { + return typeof SpeechRecognition !== 'undefined'; + }, - isSupported: function() { - return typeof SpeechRecognition !== 'undefined'; - }, - - isEnabled: function() { - try { - return localStorage.getItem(STORAGE_ENABLED) === '1'; - } catch(e) { - return false; - } - }, - - setEnabled: function(val) { - try { - localStorage.setItem(STORAGE_ENABLED, val ? '1' : '0'); - } catch(e) {} - }, - - startListening: function(options) { - if (!this.isSupported()) { - return Promise.reject(new Error('Web Speech API not supported')); - } - - if (!this.isEnabled()) { - return Promise.reject(new Error('Web Speech Recognition not enabled')); - } - - _transcript = ''; - _isListening = true; - - var opts = options || {}; - _onPartialCallback = opts.onPartial || function() {}; - _onFinalCallback = opts.onFinal || function() {}; - - _recognition = new SpeechRecognition(); - _recognition.continuous = true; // Keep listening - _recognition.interimResults = true; // Show partial results - _recognition.lang = opts.language || 'en-US'; - _recognition.maxAlternatives = 1; - - return new Promise(function(resolve, reject) { - _recognition.onstart = function() { - console.log('[WebSpeech] Started listening'); - }; - - _recognition.onresult = function(event) { - var interim = ''; - var final = ''; - - for (var i = event.resultIndex; i < event.results.length; i++) { - var transcript = event.results[i][0].transcript; - - if (event.results[i].isFinal) { - final += transcript + ' '; - _transcript += transcript + ' '; - } else { - interim += transcript; - } - } - - // Callback with partial results (shown in real-time) - if (interim && _onPartialCallback) { - _onPartialCallback(interim); - } - - // Callback with final results (confirmed words) - if (final && _onFinalCallback) { - _onFinalCallback(final.trim()); - } - }; - - _recognition.onerror = function(event) { - console.error('[WebSpeech] Error:', event.error); - _isListening = false; - - if (event.error === 'no-speech') { - reject(new Error('No speech detected')); - } else if (event.error === 'not-allowed') { - reject(new Error('Microphone permission denied')); - } else { - reject(new Error('Speech recognition error: ' + event.error)); - } - }; - - _recognition.onend = function() { - _isListening = false; - console.log('[WebSpeech] Stopped listening'); - resolve(_transcript.trim()); - }; - - try { - _recognition.start(); - } catch (e) { - reject(new Error('Failed to start recognition: ' + e.message)); - } - }); - }, - - stopListening: function() { - if (_recognition && _isListening) { - _recognition.stop(); - } - }, - - isListening: function() { - return _isListening; - }, - - getCurrentTranscript: function() { - return _transcript.trim(); - }, - - // Check if browser likely sends to cloud - getPrivacyInfo: function() { - var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); - var isEdge = /Edg/.test(navigator.userAgent); - - if (isChrome || isEdge) { - return { - provider: 'Google Cloud Speech', - privacy: 'Audio sent to Google servers', - warning: 'NOT HIPAA-compliant' - }; - } else if (/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)) { - return { - provider: 'Apple Speech Recognition', - privacy: 'May process on-device or Apple servers', - warning: 'Check Apple privacy policy' - }; - } else { - return { - provider: 'Unknown', - privacy: 'May send audio to cloud servers', - warning: 'Privacy unknown' - }; - } + isEnabled: function() { + try { + return localStorage.getItem(STORAGE_ENABLED) === '1'; + } catch(e) { + return false; } - }; + }, -})(); + setEnabled: function(val) { + try { + localStorage.setItem(STORAGE_ENABLED, val ? '1' : '0'); + } catch(e) {} + }, + + startListening: function(options) { + if (!this.isSupported()) { + return Promise.reject(new Error('Web Speech API not supported')); + } + + if (!this.isEnabled()) { + return Promise.reject(new Error('Web Speech Recognition not enabled')); + } + + _transcript = ''; + _isListening = true; + + var opts = options || {}; + _onPartialCallback = opts.onPartial || function() {}; + _onFinalCallback = opts.onFinal || function() {}; + + _recognition = new SpeechRecognition(); + _recognition.continuous = true; // Keep listening + _recognition.interimResults = true; // Show partial results + _recognition.lang = opts.language || 'en-US'; + _recognition.maxAlternatives = 1; + + return new Promise(function(resolve, reject) { + _recognition.onstart = function() { + console.log('[WebSpeech] Started listening'); + }; + + _recognition.onresult = function(event) { + var interim = ''; + var final = ''; + + for (var i = event.resultIndex; i < event.results.length; i++) { + var transcript = event.results[i][0].transcript; + + if (event.results[i].isFinal) { + final += transcript + ' '; + _transcript += transcript + ' '; + } else { + interim += transcript; + } + } + + // Callback with partial results (shown in real-time) + if (interim && _onPartialCallback) { + _onPartialCallback(interim); + } + + // Callback with final results (confirmed words) + if (final && _onFinalCallback) { + _onFinalCallback(final.trim()); + } + }; + + _recognition.onerror = function(event) { + console.error('[WebSpeech] Error:', event.error); + _isListening = false; + + if (event.error === 'no-speech') { + reject(new Error('No speech detected')); + } else if (event.error === 'not-allowed') { + reject(new Error('Microphone permission denied')); + } else { + reject(new Error('Speech recognition error: ' + event.error)); + } + }; + + _recognition.onend = function() { + _isListening = false; + console.log('[WebSpeech] Stopped listening'); + resolve(_transcript.trim()); + }; + + try { + _recognition.start(); + } catch (e) { + reject(new Error('Failed to start recognition: ' + e.message)); + } + }); + }, + + stopListening: function() { + if (_recognition && _isListening) { + _recognition.stop(); + } + }, + + isListening: function() { + return _isListening; + }, + + getCurrentTranscript: function() { + return _transcript.trim(); + }, + + // Check if browser likely sends to cloud + getPrivacyInfo: function() { + var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); + var isEdge = /Edg/.test(navigator.userAgent); + + if (isChrome || isEdge) { + return { + provider: 'Google Cloud Speech', + privacy: 'Audio sent to Google servers', + warning: 'NOT HIPAA-compliant' + }; + } else if (/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)) { + return { + provider: 'Apple Speech Recognition', + privacy: 'May process on-device or Apple servers', + warning: 'Check Apple privacy policy' + }; + } else { + return { + provider: 'Unknown', + privacy: 'May send audio to cloud servers', + warning: 'Privacy unknown' + }; + } + } +}; diff --git a/test/module-entrypoints.test.js b/test/module-entrypoints.test.js index 71b4a88..ccc7f6a 100644 --- a/test/module-entrypoints.test.js +++ b/test/module-entrypoints.test.js @@ -9,6 +9,7 @@ const moduleEntrypoints = [ 'public/js/clinicalAssistant.js', 'public/js/admin.js', 'public/js/admin-docs.js', + 'public/js/audioBackup.js', 'public/js/authFetch.js', 'public/js/diagrams.js', 'public/js/drugs-loader.js', @@ -22,6 +23,7 @@ const moduleEntrypoints = [ 'public/js/sickVisit.js', 'public/js/soap.js', 'public/js/secureStorage.js', + 'public/js/speechRecognition.js', 'public/js/transcriptionSettings.js', 'public/js/ui-state.js', 'public/js/wellVisit.js',