Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 17 additions & 1 deletion src/islands/media/VoiceToText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default function VoiceToText() {
const [modelProgress, setModelProgress] = useState<number | null>(null);
const [transcribing, setTranscribing] = useState(false);
const [elapsed, setElapsed] = useState(0);
const [modelCached, setModelCached] = useState(false);
const [segments, setSegments] = useState<TranscriptSegment[] | null>(null);
const [editedText, setEditedText] = useState('');
const [tab, setTab] = useState<Tab>('text');
Expand Down Expand Up @@ -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
Expand All @@ -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');
Expand Down Expand Up @@ -268,7 +284,7 @@ export default function VoiceToText() {
</div>

{modelProgress !== null && (
<ProgressBar percent={modelProgress * 100} label="Downloading model (first time only)" />
<ProgressBar percent={modelProgress * 100} label={modelCached ? 'Loading model from cache…' : 'Downloading model (first time only)…'} />
)}
{busy && modelProgress === null && (
<p className="text-sm text-muted-foreground">
Expand Down
Loading