diff --git a/public/components/settings.html b/public/components/settings.html index 9263dff..6115549 100644 --- a/public/components/settings.html +++ b/public/components/settings.html @@ -61,6 +61,30 @@ + +
+

Real-Time Streaming Transcription

+
+

Privacy Warning: Uses your browser's built-in speech recognition, which may send audio to cloud servers (Chrome/Edge send to Google). Only enable if you accept this trade-off for real-time transcription.

+
+

See words appear as you speak (streaming). Overrides browser and server transcription when enabled. Not HIPAA-compliant in most browsers.

+ +
+ + +
+ +
+

Current Browser:

+

Detecting...

+
+ +

Trade-off: Immediate transcription vs. privacy. For maximum privacy, use Browser Whisper (offline batch mode) or Server transcription with HIPAA-eligible provider.

+
+

Two-Factor Authentication

diff --git a/public/index.html b/public/index.html index 95c8edd..5442436 100644 --- a/public/index.html +++ b/public/index.html @@ -303,6 +303,8 @@ + + diff --git a/public/js/speechRecognition.js b/public/js/speechRecognition.js new file mode 100644 index 0000000..12a6aa5 --- /dev/null +++ b/public/js/speechRecognition.js @@ -0,0 +1,160 @@ +// ============================================================ +// 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' + }; + } + } + }; + +})(); diff --git a/public/js/transcriptionSettings.js b/public/js/transcriptionSettings.js new file mode 100644 index 0000000..105f465 --- /dev/null +++ b/public/js/transcriptionSettings.js @@ -0,0 +1,166 @@ +// ============================================================ +// TRANSCRIPTION SETTINGS — Browser Whisper + Web Speech API +// ============================================================ + +(function() { + var _inited = false; + + document.addEventListener('tabChanged', function(e) { + if (e.detail.tab !== 'settings' || _inited) return; + _inited = true; + initTranscriptionSettings(); + }); + + // Also init on direct page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + setTimeout(function() { + if (!_inited && document.getElementById('browser-whisper-enabled')) { + _inited = true; + initTranscriptionSettings(); + } + }, 500); + }); + } else { + setTimeout(function() { + if (!_inited && document.getElementById('browser-whisper-enabled')) { + _inited = true; + initTranscriptionSettings(); + } + }, 500); + } + + function initTranscriptionSettings() { + console.log('[TranscriptionSettings] Initializing...'); + + // ── Browser Whisper ────────────────────────────────────── + var whisperCheckbox = document.getElementById('browser-whisper-enabled'); + var whisperStatus = document.getElementById('browser-whisper-status'); + var whisperModel = document.getElementById('browser-whisper-model'); + var whisperPreload = document.getElementById('btn-whisper-preload'); + var whisperProgress = document.getElementById('browser-whisper-progress'); + var whisperProgressText = document.getElementById('browser-whisper-progress-text'); + + if (whisperCheckbox && window.BrowserWhisper) { + whisperCheckbox.checked = BrowserWhisper.isEnabled(); + whisperStatus.textContent = BrowserWhisper.isEnabled() ? 'On — audio stays on device' : 'Off'; + + if (whisperModel) { + whisperModel.value = BrowserWhisper.getModel(); + } + + whisperCheckbox.addEventListener('change', function() { + BrowserWhisper.setEnabled(whisperCheckbox.checked); + whisperStatus.textContent = whisperCheckbox.checked ? 'On — audio stays on device' : 'Off'; + + if (whisperCheckbox.checked) { + // Auto-preload on enable + BrowserWhisper.preload(function(file, pct) { + if (whisperProgress && whisperProgressText) { + if (pct >= 100) { + whisperProgress.style.display = 'none'; + return; + } + whisperProgress.style.display = 'block'; + whisperProgressText.textContent = file + (pct > 0 ? ' ' + pct + '%' : ''); + } + }); + } + }); + + if (whisperModel) { + whisperModel.addEventListener('change', function() { + BrowserWhisper.setModel(whisperModel.value); + }); + } + + if (whisperPreload) { + whisperPreload.addEventListener('click', function() { + if (!BrowserWhisper || !BrowserWhisper.isSupported()) { + showToast('Browser Whisper not supported', 'error'); + return; + } + + whisperPreload.disabled = true; + whisperPreload.innerHTML = ' Downloading...'; + whisperProgress.style.display = 'block'; + whisperProgressText.textContent = 'Initializing...'; + + BrowserWhisper.setEnabled(true); + whisperCheckbox.checked = true; + whisperStatus.textContent = 'On — audio stays on device'; + + BrowserWhisper.preload(function(file, pct) { + if (pct >= 100) { + whisperProgress.style.display = 'none'; + whisperPreload.disabled = false; + whisperPreload.innerHTML = ' Pre-download model'; + showToast('Whisper model ready!', 'success'); + return; + } + whisperProgress.style.display = 'block'; + whisperProgressText.textContent = file + (pct > 0 ? ' ' + pct + '%' : ''); + }); + }); + } + } + + // ── Web Speech Recognition ─────────────────────────────── + var speechCheckbox = document.getElementById('web-speech-enabled'); + var speechStatus = document.getElementById('web-speech-status'); + var speechBrowserInfo = document.getElementById('web-speech-browser-info'); + + if (speechCheckbox && window.WebSpeechRecognition) { + speechCheckbox.checked = WebSpeechRecognition.isEnabled(); + speechStatus.textContent = WebSpeechRecognition.isEnabled() ? 'On — real-time streaming' : 'Off'; + + // Show privacy info + if (speechBrowserInfo) { + var privacyInfo = WebSpeechRecognition.getPrivacyInfo(); + speechBrowserInfo.innerHTML = + 'Provider: ' + privacyInfo.provider + '
' + + 'Privacy: ' + privacyInfo.privacy + '
' + + '⚠️ ' + privacyInfo.warning + ''; + } + + speechCheckbox.addEventListener('change', function() { + if (speechCheckbox.checked) { + // Show confirmation for privacy warning + if (!confirm( + 'WARNING: Real-time streaming uses your browser\'s speech recognition, ' + + 'which may send audio to cloud servers (e.g., Google).\n\n' + + 'This is NOT HIPAA-compliant.\n\n' + + 'Only enable if you understand and accept this privacy trade-off.\n\n' + + 'Continue?' + )) { + speechCheckbox.checked = false; + return; + } + + // Disable Browser Whisper if enabling Web Speech + if (whisperCheckbox && whisperCheckbox.checked) { + whisperCheckbox.checked = false; + BrowserWhisper.setEnabled(false); + whisperStatus.textContent = 'Off'; + showToast('Browser Whisper disabled (Web Speech takes priority)', 'info'); + } + } + + WebSpeechRecognition.setEnabled(speechCheckbox.checked); + speechStatus.textContent = speechCheckbox.checked ? 'On — real-time streaming' : 'Off'; + }); + + // If not supported, disable and show message + if (!WebSpeechRecognition.isSupported()) { + speechCheckbox.disabled = true; + speechStatus.textContent = 'Not supported in this browser'; + if (speechBrowserInfo) { + speechBrowserInfo.innerHTML = 'Not supported - Web Speech API not available in this browser.'; + } + } + } + + console.log('✅ Transcription settings initialized'); + } + +})();