// ============================================================ // 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' }; } } }; })();