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 });