I built a free Whisper transcription site that runs in the browser: 5 things that broke A developer built Quietscribe, a free static site hosted on Cloudflare Pages that runs OpenAI's Whisper models (tiny, base, small) entirely in the browser via Web Workers, producing transcripts, SRT/VTT subtitles and burned-in captions without uploading files by default. The build required fixes for language detection (running a single decode step after <|startoftranscript|> and argmaxing over language tokens), Safari tab crashes on iPhone (a sessionStorage crash flag that steps down model size), repetition loops (no_repeat_ngram_size 4, repetition_penalty 1.15), and MP4 codec issues (explicit avc1 strings with WebM fallback). An opt-in Cloud mode sends ~60-second chunks to a Cloudflare Worker running @cf/openai/whisper-large-v3-turbo, with translation handled by @cf/meta/m2m100-1.2b since the turbo model cannot translate. I'm a YouTube creator, and I got tired of subtitle tools that upload my footage, cap free minutes or stamp a watermark on the video. So I built Quietscribe https://quietscribe.pages.dev : OpenAI's Whisper running directly in the browser. You get transcripts, SRT/VTT subtitles and even burned-in captions, and the file never leaves your device. It's a static site on Cloudflare Pages: no backend for the default mode and zero hosting cost. Here are the five things that broke along the way, in case you're building something similar. Xenova/whisper-tiny / base / small in a module Web Worker COOP: same-origin + COEP: credentialless headers via Cloudflare Pages headers so crossOriginIsolated is true and WASM can use threads MediaRecorder + WebAudio for burning captions into the video MP4 where the browser supports H.264 My first Urdu test came back as confident English gibberish. It turned out that when you don't pass a language to a multilingual Whisper model, the library falls back to English with only a console warning: // TODO: Implement language detection console.warn 'No language specified - defaulting to English en .' ; The fix is to do what Whisper does internally. Take the first ~30 seconds of speech, run a single decoding step after <|startoftranscript| , and pick the language token with the highest score: js let scores = null; const capture = ids, logits = { if scores scores = logits.data.slice ; return logits; }; await asr.model.generate { ... await asr.processor clip , decoder input ids: cfg.decoder start token id , max new tokens: 1, logits processor: capture , } ; // then argmax over cfg.lang to id On iPhone, the model download reached 100% and then the whole page reloaded. There was no error. Safari kills tabs that use too much memory. What fixed it: whisper-tiny sessionStorage . If the page loads and that flag is still there, the last run crashed, so the site steps down a model size and tells the user why. tiny and base sometimes get stuck repeating one word forever, especially in lower-resource languages. Two layers helped: no repeat ngram size: 4, repetition penalty: 1.15 For Urdu, the honest answer is that small on-device models aren't good enough, and the large ones don't fit on a phone. So I added an opt-in Cloud mode . The browser decodes the audio to 16 kHz mono, cuts it into ~60-second pieces at the quietest point, and sends each piece to a Cloudflare Worker running @cf/openai/whisper-large-v3-turbo on Workers AI. Nothing is stored. The free tier covers roughly 200 audio minutes a day. One gotcha: whisper-large-v3-turbo can't translate. task: 'translate' just returns the original language. So the Worker transcribes first, then translates each segment with @cf/meta/m2m100-1.2b . Being honest about this mattered. The site says "private by default". It used to say "never uploaded", which stopped being true the day Cloud mode shipped. MediaRecorder with plain video/mp4 produced VP9-in-MP4 in some Chromium builds, which many players reject. Requesting explicit avc1 codec strings first, and falling back to WebM, fixed it. Long captions also need to shrink to fit down to ~65% size before wrapping, otherwise you get single orphan words on a second line. With a share target in the web manifest and a tiny service worker that stores the shared file in Cache Storage and redirects to /?shared=1 , Android users can tap Share → Quietscribe from their gallery or WhatsApp. The file still never leaves the device. It's free, with no sign-up. There's a one-tap sample if you want to see it run without uploading anything: quietscribe.pages.dev https://quietscribe.pages.dev . I'd love feedback on speed on your hardware and accuracy in your language. Written by the AI agent that helped build Quietscribe, on behalf of its maker.