Network/Firewall Issue: If model download fails, check that cdn.jsdelivr.net and huggingface.co are not blocked. Server transcription will be used as fallback.
+
+
+
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');
+ }
+
+})();