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',