engineering / webassembly

A 400 MB video, no upload, no server, no bill: FFmpeg runs in the tab.

The extract-audio tool takes the soundtrack out of a video without sending the video anywhere. This is how that works, and why WebAssembly is the reason a product with no backend can still do real work.

runs on
your machine
uploaded
0 bytes
key needed
none
cost per run
£0.00
first-run download
~32 MB
The principle

WebAssembly moves the work to where the data already is

Every other way of doing this starts by moving the file. WebAssembly starts by moving the program — which, for media, is the smaller of the two.

A browser can run compiled code at close to native speed, inside the same sandbox that contains the page: no filesystem, no network it is not given, no escape into the operating system. FFmpeg — a twenty-year-old C codebase — is compiled to that target and shipped as a .wasm file. Loading it is a download, not an install; running it is a function call, not a process.

Turn that round and it becomes an architecture. The conventional design for "extract the audio from this video" is: upload 400 MB, queue a job, run FFmpeg on a server you rent, store the output, hand back a link, delete it on a schedule, and answer for all of it when somebody asks where their file went. The WASM design is: fetch a 32 MB program once, run it on a file that never moves.

Nothing to upload

The video is read from a File object the browser already holds. There is no bandwidth cost, no wait proportional to file size, and no copy of somebody's holiday footage on our disk — because there is no disk of ours.

Nothing to pay

The compute is the visitor's, already bought and paid for. The marginal cost of a run is zero, so the tool can be free without a plan, a quota or a login — the first thing here that costs nothing, because nothing leaves the machine.

Nothing to secure

Privacy by construction rather than by policy. There is no bucket to misconfigure, no retention schedule to honour and no breach to disclose, because the interesting data never arrives.

Nothing to keep running

Static hosting serves bytes. No queue, no worker fleet, no autoscaling, nothing that costs money while idle and nothing to be woken at 3am for.

It scales by not scaling

A thousand people extracting audio at once is a thousand independent browsers doing their own work. The load on us is a thousand cached file fetches.

It is the same everywhere

The same module runs in Chrome, Safari and Firefox on desktop and phone. Sandboxed code that cannot see the filesystem is also code we can ship to the iOS and Android targets later without porting it.

The sequence

What actually happens when you press Extract

Nine steps. None of them is an upload, and the first four do not happen at all until the button is pressed.

  1. You choose a video. The browser hands the page a File — a handle, not a copy. Nothing has been read yet.
  2. The page imports sg-video.js from the SGraph tools origin. That module is the FFmpeg wrapper, shared with the other tools rather than written again here.
  3. FFmpeg's ES module and its util helper are imported from unpkg.com.
  4. A same-origin worker is built. FFmpeg wants to run its commands in a Web Worker created from its own URL — which is another origin, and new Worker() refuses those. So the wrapper fetches the worker's source, rewrites its relative imports to absolute CDN URLs (a blob: URL has no path to resolve ./const.js against) and creates the worker from a blob. This detail costs us a CSP directive — see below.
  5. The core arrives: ~32 MB of ffmpeg-core.js plus ffmpeg-core.wasm, fetched inside the worker and compiled by the browser's WebAssembly engine. This is the whole download, it happens once, and the browser caches it. A visitor who only reads the page pays none of it.
  6. Your file is written into a virtual filesystem — an in-memory Emscripten FS that exists inside the sandbox. FFmpeg believes it is reading a file on disk. It is reading your tab's memory.
  7. The command runs: -vn -c:a copy. Drop the video stream, copy the audio across untouched. No decode, no re-encode, no quality lost, and fast — it is a copy, not a conversion.
  8. The result is read back as a Blob and the virtual files are deleted. An object URL makes it playable and downloadable without ever touching your downloads folder.
  9. Or it goes straight on. "Transcribe this in VoiceDebrief" hands the blob to the workbench through same-origin storage — no round trip through the filesystem, and still no upload.

The one exception, stated plainly. Copying only works if the audio can live in an MP4 container. A phone or WhatsApp video is AAC and copies perfectly; a screen recording or a web download is usually Opus in WebM, which cannot be. FFmpeg reports that as "the file may not contain an audio stream", which is untrue — it has one, it just cannot be copied. The tool catches that specific failure, retries with -c:a aac, and tells you it re-encoded, rather than letting you believe your recording was silent.

The honest part

What WebAssembly costs

It is not free, and a page that only lists the upside is marketing. The trade is real and we took it deliberately.

32 MB, once

The core is large because it is all of FFmpeg. We load it on demand rather than on page load, say so in the UI before it starts, and let the browser cache handle every run after the first. A build trimmed to the codecs this tool needs would be far smaller — that is a future optimisation, not a shipped one.

Memory is the tab's

The file is held in memory rather than streamed from disk, so a very large video is limited by what the tab can allocate. Stream copy keeps this as small as it can be: no decoded frames are ever materialised.

Single-threaded here

Multi-threaded FFmpeg needs SharedArrayBuffer, which needs cross-origin isolation headers a static host does not send by default. For a stream copy this costs nothing measurable; for heavy re-encoding it would.

A CDN in the trust boundary

The core is fetched from unpkg.com, so that origin is named in this site's Content-Security-Policy. It is allowed to supply code and nothing else: no key and no audio ever travels there. Pinning our own copy of the core is on the security page as an open item, not a solved one.

The receipts that matter most

Two failures this design produced, and what they taught

The worker was refused before a single byte was fetched

Extraction stopped instantly, with four CSP errors and a progress bar that never moved. The cause: a blob worker inherits the creating page's CSP, and the module imports it then makes are checked against that policy — not against the policy of wherever the worker's source came from. The page allowed unpkg.com in script-src and connect-src, but not in worker-src. Adding it fixed the tool. What made this worth writing down is how it was confirmed: by building the same blob-worker shape against a local stand-in CDN and watching it be refused without the origin and load with it — not by reading the specification and believing it.

The next-step controls existed before there was a result

The player and both follow-on buttons were on screen from page load. The hidden attribute was set correctly the whole time — but hidden is a user-agent rule, and any author rule that sets display outranks it. #out{display:flex} quietly won. The fix is one line; the lesson is the test that now guards it, which asks the browser what it paints rather than what attribute we set. The first version of that test consulted .hidden and passed against the broken page.

Both are the same shape, and it is the shape most of this project's bugs have had: asserting the thing that was supposed to cause an effect, instead of the effect. A CSP directive is not a loaded worker; an attribute is not a painted pixel.

Where this goes next

The same argument, applied further

WebAssembly is not a trick for one tool. It is the reason "no backend" is an architecture here rather than a limitation — and the line is worth stating exactly.

What runs locally today

Video → audio, in the extract-audio tool. That is the whole of the WASM surface right now, and this page will say so until more of it is true.

What does not

Transcription is not local. The one pass sends your audio to OpenRouter and on to a model provider — that is stated on the landing page, in the workflow panel, and again here. A browser-local mode is a design goal, not a shipped feature.

Receipts