/** * Transcript-translation Web Worker (D-045): Opus-MT through * Transformers.js, off the main thread (D-003). * * A dedicated worker rather than a message type on the Whisper worker: * - its own onnxruntime instance gets the full WASM thread pool. ort fixes * the pool at first init (D-023), and a Whisper worker that initialised * ort for WebGPU/WebNN runs WASM single-threaded for life; * - cancelling a translation terminates only this worker — the loaded * Whisper pipeline survives, and a Whisper cancel/restart (D-018/D-025) * never kills a translation; * - the model is downloaded only when the user asks for a translation and * cached by Transformers.js in the Cache API like Whisper weights * (D-005/D-020), so a cached model works offline. * WASM only for now: Opus-MT is small, and WebGPU behaviour can't be * measured in this environment. */ import { env, type ProgressInfo } from "@huggingface/transformers"; import { getTranslationModelById, translateUnits } from "../lib/translation"; import { loadOpusMt, type OpusMtTranslator } from "./opusMt"; import { DownloadTally, isTranslateRequest, type TranslateRequest, type TranslateWorkerMessage, } from "./translateProtocol"; // Same-origin onnxruntime binaries (offline, COEP) — see inference.worker.ts. if (env.backends.onnx.wasm) { env.backends.onnx.wasm.wasmPaths = "./ort/"; // D-023: with cross-origin isolation, use most of the machine. env.backends.onnx.wasm.numThreads = globalThis.crossOriginIsolated ? Math.max(1, Math.min((navigator.hardwareConcurrency || 4) - 1, 12)) : 1; } interface DedicatedWorkerScope { postMessage(message: TranslateWorkerMessage): void; addEventListener(type: "message", listener: (event: MessageEvent) => void): void; } const scope = self as unknown as DedicatedWorkerScope; /** One loaded model at a time; switching direction disposes the other. */ let active: { modelId: string; promise: Promise } | null = null; function load(modelId: string, onProgress: (info: ProgressInfo) => void): Promise { if (active?.modelId === modelId) return active.promise; const previous = active; const promise = (async () => { if (previous) await (await previous.promise.catch(() => null))?.dispose(); return loadOpusMt(modelId, { device: "wasm", progressCallback: onProgress }); })(); active = { modelId, promise }; promise.catch(() => { if (active?.promise === promise) active = null; // allow a clean retry }); return promise; } async function handle(request: TranslateRequest): Promise { const { requestId } = request; if (!getTranslationModelById(request.modelId)) { throw new Error(`Unknown translation model: ${request.modelId}`); } const tally = new DownloadTally(); const translator = await load(request.modelId, (info) => { if (info.status === "progress") { const { loaded, total } = tally.update(info.file, info.loaded, info.total); scope.postMessage({ type: "translate-download", requestId, loaded, total }); } }); const texts = await translateUnits(request.units, { translateBatch: (pieces) => translator.translateBatch(pieces), countTokens: translator.countTokens, onProgress: (done, total) => scope.postMessage({ type: "translate-progress", requestId, done, total }), }); scope.postMessage({ type: "translate-result", requestId, texts }); } // Requests run one at a time, in arrival order: the UI only ever has one in // flight, and serialising keeps two passes from sharing the thread pool. let queue: Promise = Promise.resolve(); scope.addEventListener("message", (event) => { const data = event.data; if (!isTranslateRequest(data)) return; queue = queue.then(() => handle(data).catch((error: unknown) => { scope.postMessage({ type: "translate-error", requestId: data.requestId, message: error instanceof Error ? error.message : String(error), }); }), ); });