From 9aecbb887484b8b1fa39456a3f14b3d12fe648a8 Mon Sep 17 00:00:00 2001 From: Kresna Date: Sat, 1 Aug 2026 19:14:24 +0700 Subject: [PATCH] fix(voice-to-text): proxy HF model weights same-origin so they cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Whisper model re-downloaded on every refresh: transformers.js fetches weights from huggingface.co, which 302-redirects large files to a CDN, and neither the browser nor Workbox reliably caches redirected cross-origin responses. Add a /hf/* proxy to the edge Worker that resolves the redirect server-side and returns the bytes same-origin with 'cache-control: immutable' (+ Cloudflare edge cache). Point transformers.js env.remoteHost at it (skipped on localhost dev). The browser HTTP-caches the same-origin immutable response — the same mechanism that already makes the self-hosted OCR model cache reliably — so it survives refreshes. Falls back to the default host if env can't be set. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/tools/media/stt.engine.ts | 13 ++++++++++++- worker/index.js | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/tools/media/stt.engine.ts b/src/tools/media/stt.engine.ts index d2cee42..07c3e70 100644 --- a/src/tools/media/stt.engine.ts +++ b/src/tools/media/stt.engine.ts @@ -59,7 +59,18 @@ export async function createTranscriber( ): Promise { if (cached && cached.model === model) return cached.transcriber; - const { pipeline } = await import('@huggingface/transformers'); + const { pipeline, env } = await import('@huggingface/transformers'); + + // Fetch model weights through our same-origin /hf proxy so they cache reliably + // (Hugging Face redirects large files to a CDN the browser won't cache). Skip on + // localhost dev, where the Cloudflare Worker proxy isn't running. + try { + const origin = self.location?.origin ?? ''; + if (origin && !/localhost|127\.0\.0\.1|\[::1\]/.test(origin)) { + env.remoteHost = `${origin}/hf`; + } + } catch { /* non-worker context — leave the default */ } + const backend: SttBackend = (await webgpuAvailable()) ? 'webgpu' : 'wasm'; const pipe = await pipeline('automatic-speech-recognition', model, { diff --git a/worker/index.js b/worker/index.js index fdc4231..161263a 100644 --- a/worker/index.js +++ b/worker/index.js @@ -28,6 +28,31 @@ export default { return env.SIGNAL.getByName(roomId).fetch(request); } + // Same-origin proxy for Hugging Face model files. transformers.js fetches + // Whisper weights from huggingface.co, which 302-redirects large files to a + // CDN — the browser/Workbox can't reliably cache those redirected responses, + // so the model re-downloaded on every refresh. Proxying resolves the redirect + // server-side and returns the bytes same-origin with an immutable cache header, + // so the browser HTTP-caches them and they survive refreshes. + if (url.pathname.startsWith('/hf/')) { + if (request.method !== 'GET' && request.method !== 'HEAD') { + return new Response('Method not allowed', { status: 405 }); + } + const upstream = 'https://huggingface.co/' + url.pathname.slice('/hf/'.length) + url.search; + const res = await fetch(upstream, { + headers: { 'user-agent': 'goodwebtools-proxy' }, + cf: { cacheEverything: true, cacheTtl: 31536000 }, + }); + if (!res.ok && res.status !== 206) { + return new Response('Upstream model fetch failed', { status: res.status || 502 }); + } + const headers = new Headers(res.headers); + headers.set('cache-control', 'public, max-age=31536000, immutable'); + headers.set('access-control-allow-origin', '*'); + headers.delete('set-cookie'); + return new Response(res.body, { status: res.status, headers }); + } + if (url.pathname.startsWith('/models/')) { if (request.method !== 'GET' && request.method !== 'HEAD') { return new Response('Method not allowed', { status: 405 });