pediatric-ai-scribe-v3/public/js/whisperWorker.js
Daniel Onyejesi 59bf0a5cd8 Add browser-side Whisper transcription (local, zero network, HIPAA-safe)
- whisperWorker.js: Web Worker running @xenova/transformers Whisper in WASM
- browserWhisper.js: main-thread manager — audio→Float32 conversion, worker lifecycle
- transcribeAudio() checks BrowserWhisper.isEnabled() first, falls back to server
- Settings UI: enable/disable, model picker (tiny/base/small), pre-download button
- CSP: add wasm-unsafe-eval, cdn.jsdelivr.net, HuggingFace CDN domains
- Default: whisper-tiny.en (~39MB, ~2-3s per clip)
2026-03-31 07:30:17 -04:00

53 lines
1.7 KiB
JavaScript

// ============================================================
// WHISPER WORKER — runs @xenova/transformers in a Web Worker
// Audio never leaves the device. Model cached in IndexedDB.
// ============================================================
importScripts('https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2');
var T = self.transformers || transformers;
T.env.allowLocalModels = false;
T.env.useBrowserCache = true;
var _pipe = null;
var _loadedModel = null;
async function load(modelName) {
if (_pipe && _loadedModel === modelName) return;
self.postMessage({ type: 'loading', model: modelName });
_pipe = await T.pipeline('automatic-speech-recognition', modelName, {
progress_callback: function(p) {
if (p.status === 'downloading' || p.status === 'progress') {
self.postMessage({ type: 'progress', file: p.file || '', progress: Math.round(p.progress || 0) });
}
}
});
_loadedModel = modelName;
self.postMessage({ type: 'ready', model: modelName });
}
self.addEventListener('message', async function(e) {
var d = e.data;
if (d.type === 'load') {
try { await load(d.model || 'Xenova/whisper-tiny.en'); }
catch(err) { self.postMessage({ type: 'error', message: err.message }); }
return;
}
if (d.type === 'transcribe') {
try {
await load(d.model || 'Xenova/whisper-tiny.en');
var result = await _pipe(d.audio, {
language: 'english',
task: 'transcribe',
chunk_length_s: 30,
stride_length_s: 5,
return_timestamps: false
});
self.postMessage({ type: 'result', text: result.text.trim() });
} catch(err) {
self.postMessage({ type: 'error', message: err.message });
}
}
});