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.
160 lines
4.7 KiB
JavaScript
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'
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
})();
|