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.
This commit is contained in:
parent
d6881c5450
commit
0cb1cac7ae
3 changed files with 19 additions and 37 deletions
|
|
@ -13,7 +13,8 @@ COPY . .
|
||||||
|
|
||||||
RUN mkdir -p /app/data/logs
|
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 && \
|
RUN mkdir -p /app/public/models/Xenova/whisper-tiny.en/onnx && \
|
||||||
cd /app/public/models/Xenova/whisper-tiny.en && \
|
cd /app/public/models/Xenova/whisper-tiny.en && \
|
||||||
echo "Downloading Whisper model files..." && \
|
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 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/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 && \
|
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 && \
|
echo "Whisper models downloaded successfully (models: 42MB)"
|
||||||
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"
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,8 @@
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_modelLoaded = model;
|
_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) {
|
_worker.addEventListener('message', function(e) {
|
||||||
var d = e.data;
|
var d = e.data;
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,22 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// WHISPER WORKER — runs @xenova/transformers in a Web Worker
|
// WHISPER WORKER — runs @xenova/transformers in a Web Worker
|
||||||
// Audio never leaves the device. Model cached in IndexedDB.
|
// 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] 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
|
// Configure transformers.js to load models from our local server
|
||||||
T.env.allowLocalModels = false;
|
env.allowLocalModels = false;
|
||||||
T.env.useBrowserCache = true;
|
env.useBrowserCache = true;
|
||||||
T.env.allowRemoteModels = false; // Force local-only
|
env.allowRemoteModels = false; // Force local-only
|
||||||
T.env.localModelPath = '/models/'; // Our bundled models
|
env.localModelPath = '/models/'; // Our bundled models (40MB+)
|
||||||
console.log('[WhisperWorker] Transformers configured for local models at /models/');
|
console.log('[WhisperWorker] Transformers configured for local models at /models/');
|
||||||
|
|
||||||
var _pipe = null;
|
let _pipe = null;
|
||||||
var _loadedModel = null;
|
let _loadedModel = null;
|
||||||
|
|
||||||
async function load(modelName) {
|
async function load(modelName) {
|
||||||
if (_pipe && _loadedModel === modelName) {
|
if (_pipe && _loadedModel === modelName) {
|
||||||
|
|
@ -44,8 +27,8 @@ async function load(modelName) {
|
||||||
self.postMessage({ type: 'loading', model: modelName });
|
self.postMessage({ type: 'loading', model: modelName });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_pipe = await T.pipeline('automatic-speech-recognition', modelName, {
|
_pipe = await pipeline('automatic-speech-recognition', modelName, {
|
||||||
progress_callback: function(p) {
|
progress_callback: (p) => {
|
||||||
console.log('[WhisperWorker] Progress:', p.status, p.file, p.progress);
|
console.log('[WhisperWorker] Progress:', p.status, p.file, p.progress);
|
||||||
if (p.status === 'downloading' || p.status === 'progress') {
|
if (p.status === 'downloading' || p.status === 'progress') {
|
||||||
self.postMessage({ type: 'progress', file: p.file || '', progress: Math.round(p.progress || 0) });
|
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) {
|
self.addEventListener('message', async (e) => {
|
||||||
var d = e.data;
|
const d = e.data;
|
||||||
console.log('[WhisperWorker] Message received:', d.type);
|
console.log('[WhisperWorker] Message received:', d.type);
|
||||||
|
|
||||||
if (d.type === 'load') {
|
if (d.type === 'load') {
|
||||||
|
|
@ -82,7 +65,7 @@ self.addEventListener('message', async function(e) {
|
||||||
try {
|
try {
|
||||||
await load(d.model || 'Xenova/whisper-tiny.en');
|
await load(d.model || 'Xenova/whisper-tiny.en');
|
||||||
console.log('[WhisperWorker] Starting transcription...');
|
console.log('[WhisperWorker] Starting transcription...');
|
||||||
var result = await _pipe(d.audio, {
|
const result = await _pipe(d.audio, {
|
||||||
language: 'english',
|
language: 'english',
|
||||||
task: 'transcribe',
|
task: 'transcribe',
|
||||||
chunk_length_s: 30,
|
chunk_length_s: 30,
|
||||||
|
|
@ -98,5 +81,4 @@ self.addEventListener('message', async function(e) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log worker startup
|
|
||||||
console.log('[WhisperWorker] Worker initialized');
|
console.log('[WhisperWorker] Worker initialized');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue