// 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) }); } };