From 0cb1cac7aebb50b7d964a3189c9d2e5891f07ae6 Mon Sep 17 00:00:00 2001 From: ifedan-ed Date: Tue, 31 Mar 2026 22:55:28 +0000 Subject: [PATCH] Fix Browser Whisper: Use ES module worker with CDN library Issue: transformers.js is an ES module package and cannot be loaded with importScripts() in classic workers. Solution: - Changed to module worker (type: 'module') - Import transformers.js from CDN as ES module - Models (42MB) still served from local server at /models/ Trade-off: - Library (900KB): Loads from cdn.jsdelivr.net once, cached - Models (42MB): Self-hosted, served from /models/ (no CDN) This is necessary because: 1. @xenova/transformers is ES module-only (package.json: "type": "module") 2. ES modules cannot use importScripts() 3. Module workers require HTTPS for imports 4. CDN is HTTPS and cacheable If CDN is blocked: - Use Web Speech API (with privacy warnings) - OR use Server Transcription (Vertex AI/AWS) Models remain self-hosted as they're 40MB+ and contain the AI. --- Dockerfile | 7 +++--- public/js/browserWhisper.js | 3 ++- public/js/whisperWorker.js | 46 +++++++++++-------------------------- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/Dockerfile b/Dockerfile index 57969cd..dc8f7fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,8 @@ COPY . . RUN mkdir -p /app/data/logs -# Download Browser Whisper models (self-hosted, no CDN dependency) +# Download Browser Whisper models (self-hosted) +# Library (900KB) loads from CDN, models (40MB+) served from our server RUN mkdir -p /app/public/models/Xenova/whisper-tiny.en/onnx && \ cd /app/public/models/Xenova/whisper-tiny.en && \ echo "Downloading Whisper model files..." && \ @@ -23,9 +24,7 @@ RUN mkdir -p /app/public/models/Xenova/whisper-tiny.en/onnx && \ curl -sL -o generation_config.json https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/generation_config.json && \ curl -sL -o onnx/encoder_model_quantized.onnx https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/onnx/encoder_model_quantized.onnx && \ curl -sL -o onnx/decoder_model_merged_quantized.onnx https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/onnx/decoder_model_merged_quantized.onnx && \ - cd /app/public/models && \ - curl -sL -o transformers.min.js https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2/dist/transformers.min.js && \ - echo "Whisper models downloaded successfully" + echo "Whisper models downloaded successfully (models: 42MB)" EXPOSE 3000 diff --git a/public/js/browserWhisper.js b/public/js/browserWhisper.js index 0a52b2b..a58515a 100644 --- a/public/js/browserWhisper.js +++ b/public/js/browserWhisper.js @@ -91,7 +91,8 @@ _loading = true; _modelLoaded = model; - _worker = new Worker('/js/whisperWorker.js'); + // Use module worker to support ES modules + _worker = new Worker('/js/whisperWorker.js', { type: 'module' }); _worker.addEventListener('message', function(e) { var d = e.data; diff --git a/public/js/whisperWorker.js b/public/js/whisperWorker.js index 79d8dcd..50e80f1 100644 --- a/public/js/whisperWorker.js +++ b/public/js/whisperWorker.js @@ -1,39 +1,22 @@ // ============================================================ // WHISPER WORKER — runs @xenova/transformers in a Web Worker // Audio never leaves the device. Model cached in IndexedDB. +// Uses ES module worker (type: 'module') // ============================================================ +import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2'; + console.log('[WhisperWorker] Starting worker initialization'); -console.log('[WhisperWorker] Loading transformers from local server...'); - -try { - // Load transformers.js from our own server (no CDN dependency!) - importScripts('/models/transformers.min.js'); - console.log('[WhisperWorker] Transformers library loaded successfully'); -} catch (err) { - console.error('[WhisperWorker] Failed to load transformers library:', err); - self.postMessage({ - type: 'error', - message: 'Failed to load transformers library: ' + err.message - }); - throw err; -} - -var T = self.transformers || transformers; -if (!T) { - console.error('[WhisperWorker] Transformers object not found after import'); - throw new Error('Transformers library did not initialize'); -} // Configure transformers.js to load models from our local server -T.env.allowLocalModels = false; -T.env.useBrowserCache = true; -T.env.allowRemoteModels = false; // Force local-only -T.env.localModelPath = '/models/'; // Our bundled models +env.allowLocalModels = false; +env.useBrowserCache = true; +env.allowRemoteModels = false; // Force local-only +env.localModelPath = '/models/'; // Our bundled models (40MB+) console.log('[WhisperWorker] Transformers configured for local models at /models/'); -var _pipe = null; -var _loadedModel = null; +let _pipe = null; +let _loadedModel = null; async function load(modelName) { if (_pipe && _loadedModel === modelName) { @@ -44,8 +27,8 @@ async function load(modelName) { self.postMessage({ type: 'loading', model: modelName }); try { - _pipe = await T.pipeline('automatic-speech-recognition', modelName, { - progress_callback: function(p) { + _pipe = await pipeline('automatic-speech-recognition', modelName, { + progress_callback: (p) => { console.log('[WhisperWorker] Progress:', p.status, p.file, p.progress); if (p.status === 'downloading' || p.status === 'progress') { self.postMessage({ type: 'progress', file: p.file || '', progress: Math.round(p.progress || 0) }); @@ -64,8 +47,8 @@ async function load(modelName) { } } -self.addEventListener('message', async function(e) { - var d = e.data; +self.addEventListener('message', async (e) => { + const d = e.data; console.log('[WhisperWorker] Message received:', d.type); if (d.type === 'load') { @@ -82,7 +65,7 @@ self.addEventListener('message', async function(e) { try { await load(d.model || 'Xenova/whisper-tiny.en'); console.log('[WhisperWorker] Starting transcription...'); - var result = await _pipe(d.audio, { + const result = await _pipe(d.audio, { language: 'english', task: 'transcribe', chunk_length_s: 30, @@ -98,5 +81,4 @@ self.addEventListener('message', async function(e) { } }); -// Log worker startup console.log('[WhisperWorker] Worker initialized');