// Speech models offered in the picker, and the per-device weights they need. // // The two backends want different quantizations, so they download different // files and the picker has to quote the size for whichever one will actually // run: // // wasm — int8 encoder + int8 merged decoder. Small, CPU-friendly. // webgpu — fp32 encoder + 4-bit merged decoder. This is the combination the // transformers.js WebGPU examples use; int8 matmul is poorly served // on WebGPU. It runs far faster but downloads 2–3× as much, because // the 4-bit decoder only quantizes matmul weights and leaves the // embeddings at full precision. // // `mb` figures are the encoder + decoder + tokenizer/config bytes, taken from // the HuggingFace file listing. Everything is cached after the first visit. export const MODELS = [ { id: 'onnx-community/moonshine-tiny-ONNX', label: 'Moonshine Tiny', mb: { wasm: 32, webgpu: 79 }, note: 'fastest; lighter punctuation', }, { id: 'onnx-community/whisper-tiny.en', label: 'Whisper Tiny', mb: { wasm: 44, webgpu: 122 }, note: 'quick; less accurate on hard words', }, { id: 'onnx-community/moonshine-base-ONNX', label: 'Moonshine Base', mb: { wasm: 67, webgpu: 157 }, note: 'fast and accurate; lighter punctuation', }, { id: 'onnx-community/whisper-base.en', label: 'Whisper Base', mb: { wasm: 80, webgpu: 209 }, note: 'good accuracy and punctuation', }, { id: 'onnx-community/whisper-small.en', label: 'Whisper Small', mb: { wasm: 252, webgpu: 588 }, note: 'most accurate; slow without WebGPU', }, ]; export const DEFAULT_MODEL = 'onnx-community/whisper-base.en'; // Quantization suffixes per device — these decide both which files // transformers.js fetches and which ones we look for in the cache. export const SUFFIX = { wasm: { encoder: '_quantized', decoder: '_quantized' }, webgpu: { encoder: '', decoder: '_q4' }, }; export async function detectDevice() { if (!navigator.gpu) return 'wasm'; try { return (await navigator.gpu.requestAdapter()) ? 'webgpu' : 'wasm'; } catch { return 'wasm'; } } // The two large files for a model on a given device. Their presence in the // cache is what "downloaded" in the picker means. const weightFiles = (id, device) => { const s = SUFFIX[device]; return [ `https://huggingface.co/${id}/resolve/main/onnx/encoder_model${s.encoder}.onnx`, `https://huggingface.co/${id}/resolve/main/onnx/decoder_model_merged${s.decoder}.onnx`, ]; }; export async function cachedModels(device) { const cached = new Set(); if (!self.caches) return cached; try { const cache = await caches.open('transformers-cache'); await Promise.all( MODELS.map(async (m) => { const hits = await Promise.all(weightFiles(m.id, device).map((u) => cache.match(u))); if (hits.every(Boolean)) cached.add(m.id); }), ); } catch { /* cache unavailable (private mode, etc.) — report none cached */ } return cached; }