diff --git a/astro.config.mjs b/astro.config.mjs index 9d965b9..cc8aa33 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -132,9 +132,10 @@ export default defineConfig({ }, { // On-device ML model weights + runtime (Whisper from Hugging Face, ORT - // wasm from jsDelivr). CacheFirst so they load from our cache on refresh - // instead of re-downloading. Immutable, versioned by URL. - urlPattern: /^https:\/\/([^/]*\.)?(huggingface\.co|hf\.co)\/.*|^https:\/\/cdn\.jsdelivr\.net\/.*/i, + // wasm from jsDelivr, PaddleOCR models from GitHub raw/LFS). CacheFirst so + // they load from our cache on refresh instead of re-downloading. Immutable, + // versioned by URL. + urlPattern: /^https:\/\/([^/]*\.)?(huggingface\.co|hf\.co)\/.*|^https:\/\/cdn\.jsdelivr\.net\/.*|^https:\/\/(raw|media)\.githubusercontent\.com\/.*/i, handler: 'CacheFirst', options: { cacheName: 'ml-models-cache', diff --git a/src/islands/media/VoiceToText.tsx b/src/islands/media/VoiceToText.tsx index cfb933c..0228507 100644 --- a/src/islands/media/VoiceToText.tsx +++ b/src/islands/media/VoiceToText.tsx @@ -67,6 +67,7 @@ export default function VoiceToText() { const [modelProgress, setModelProgress] = useState(null); const [transcribing, setTranscribing] = useState(false); const [elapsed, setElapsed] = useState(0); + const [modelCached, setModelCached] = useState(false); const [segments, setSegments] = useState(null); const [editedText, setEditedText] = useState(''); const [tab, setTab] = useState('text'); @@ -145,6 +146,20 @@ export default function VoiceToText() { } }; + // Is the selected model already in the browser cache? (Definitive — the bar + // shows even on a cache read, so this tells the user whether it's a real download.) + const refreshModelCached = async () => { + try { + const cache = await caches.open('transformers-cache'); + const keys = await cache.keys(); + setModelCached(keys.some(r => r.url.includes(`${model}/resolve`))); + } catch { setModelCached(false); } + }; + useEffect(() => { + void refreshModelCached(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [model]); + const transcribe = async () => { if (!audioBlob) return; audioRef.current?.pause(); // don't leave the preview playing while inference blocks the thread @@ -163,6 +178,7 @@ export default function VoiceToText() { r => setModelProgress(r), ); setModelProgress(null); // model ready (or cached) — now inference (indeterminate) + void refreshModelCached(); // it's cached now setSegments(segs); setEditedText(segmentsToText(segs)); setTab('text'); @@ -268,7 +284,7 @@ export default function VoiceToText() { {modelProgress !== null && ( - + )} {busy && modelProgress === null && (