From a717bfea69437337e004cae1c5dd01d4261a26e8 Mon Sep 17 00:00:00 2001 From: Kresna Date: Sat, 1 Aug 2026 22:31:22 +0700 Subject: [PATCH] fix(models): cache OCR model + honest cache indicator so models don't re-download after refresh - PaddleOCR fetches its model from raw/media.githubusercontent.com (git-LFS); those hosts weren't in the SW ml-models-cache rule, so the OCR model re-downloaded on every refresh. Add them to the CacheFirst urlPattern alongside Hugging Face + jsDelivr. - VoiceToText: the transformers.js progress bar fires even on cache hits, which read as a re-download. Add a definitive modelCached check (caches.open('transformers-cache')) and label the bar accordingly. Audit note: the other model tools (background-removal, ffmpeg, face-blur, upscale, object-remove) load same-origin from /models/* served 'immutable, max-age=1yr' by the worker, so they cache on the HTTP layer and need no SW rule. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ubfx4XocHcECaL8twp9zsr --- astro.config.mjs | 7 ++++--- src/islands/media/VoiceToText.tsx | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) 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 && (