pediatric-ai-scribe-v3/public/js/speechRecognition.js
ifedan-ed 196f4432f0 Add Web Speech Recognition option for real-time streaming
Provides two transcription options:

1. Browser Whisper (Offline, Batch) - RECOMMENDED
   - 100% offline, zero network calls
   - HIPAA-compliant, audio never leaves device
   - Highest accuracy (Whisper)
   - Processes after recording (batch mode)
   - Models self-hosted, bundled in v2

2. Web Speech API (Real-time, Streaming) - EXPERIMENTAL
   - Real-time transcription (see words as you speak)
   - Uses browser's built-in speech recognition
   - ⚠️ Sends audio to cloud (Chrome/Edge → Google)
   - ⚠️ NOT HIPAA-compliant
   - Requires user consent with clear warnings

Features:
- Settings UI for both options
- Clear privacy warnings for Web Speech
- Mutual exclusion (only one active at a time)
- Browser detection shows which provider is used
- Confirmation dialog before enabling Web Speech

Use Cases:
- Clinical/HIPAA: Use Browser Whisper only
- Personal/Non-clinical: Can use Web Speech for real-time feedback
- Maximum privacy: Browser Whisper (offline)
- Maximum speed: Web Speech (if privacy not required)

Implementation:
- speechRecognition.js: Web Speech API wrapper
- transcriptionSettings.js: Settings UI handler
- Privacy info displayed per browser

User can choose based on their privacy vs. speed preference.
2026-03-31 21:57:24 +00:00

160 lines
4.7 KiB
JavaScript

// ============================================================
// WEB SPEECH RECOGNITION — Real-time streaming transcription
// ⚠️ PRIVACY WARNING: Uses browser's built-in ASR which may
// send audio to cloud servers (Chrome/Edge → Google servers)
// Only use if you accept this trade-off for real-time streaming
// ============================================================
(function() {
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;
window.WebSpeechRecognition = {
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'
};
}
}
};
})();