hushscribe / worker.js
CyberMax-tools's picture
Rename products (unique names)
d0fb3e3 verified
Raw History Blame Contribute Delete
2.65 kB
// Hearlark worker: runs Whisper in the browser with transformers.js (WebGPU when available, else WASM).
// Audio arrives as 16 kHz mono Float32Array; nothing is uploaded anywhere. © 2026 CyberMax.
import { pipeline, WhisperTextStreamer, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0';
env.allowLocalModels = false;
let asr = null;
let loadedKey = null;
async function load(model, device, post) {
const key = `${model}|${device}`;
if (asr && loadedKey === key) return asr;
if (asr) { try { await asr.dispose(); } catch {} asr = null; loadedKey = null; }
const dtype = device === 'webgpu' ? { encoder_model: 'fp32', decoder_model_merged: 'q4' } : 'q8';
asr = await pipeline('automatic-speech-recognition', model, {
device,
dtype,
progress_callback: (p) => post({ type: 'load', status: p.status, file: p.file, loaded: p.loaded, total: p.total, progress: p.progress }),
});
loadedKey = key;
return asr;
}
self.onmessage = async ({ data }) => {
if (data?.type !== 'run') return;
const post = (m) => self.postMessage(m);
try {
let device = data.device;
let p;
try {
p = await load(data.model, device, post);
} catch (e) {
if (device !== 'webgpu') throw e;
post({ type: 'info', message: `WebGPU could not start (${e?.message || e}); using the CPU instead.` });
device = 'wasm';
p = await load(data.model, device, post);
}
post({ type: 'ready', device });
const englishOnly = /\.en$/.test(data.model);
const opts = {
chunk_length_s: 30,
stride_length_s: 5,
return_timestamps: true,
...(englishOnly ? {} : { language: data.language || null, task: data.task === 'translate' ? 'translate' : 'transcribe' }),
};
let finalized = 0;
let out;
try {
const time_precision = p.processor.feature_extractor.config.chunk_length / p.model.config.max_source_positions;
const streamer = new WhisperTextStreamer(p.tokenizer, {
time_precision,
skip_prompt: true,
callback_function: (text) => post({ type: 'partial', text }),
on_finalize: () => post({ type: 'chunk', done: ++finalized }),
});
out = await p(data.audio, { ...opts, streamer });
} catch (e) {
// Streaming is only for live progress; if this transformers.js build rejects it, run without it.
if (!/stream/i.test(String(e?.message || e))) throw e;
out = await p(data.audio, opts);
}
post({ type: 'done', result: { text: out.text, chunks: out.chunks || [] }, device });
} catch (e) {
post({ type: 'error', message: String(e?.message || e) });
}
};