It listens · entirely in your browser
Whisper, and who said it.
In this tab.
franken_whisper rebuilds OpenAI's Whisper and NVIDIA's Streaming
Sortformer speaker diarizer in pure, memory-safe Rust: ggml parser, log-mel frontend,
encoder, decoder, word timestamps, four-speaker diarization, and the fusion that stitches
them into who said what. No PyTorch, no Python, no FFI, no subprocess. The same
code that ships as the fw CLI compiles to WebAssembly, so the full
809-million-parameter large-v3-turbo transcribes your recordings and the
123-million-parameter Sortformer labels the speakers,
entirely on your CPU, in this page. Your audio never leaves
the tab. The CSP's connect-src 'self' makes that a browser-enforced promise
rather than a privacy-policy one.
Try it in your browser Install the CLI
Live · in this tab
Run it right now.
This panel runs the real pipeline. Whisper large-v3-turbo (874 MB, q8_0 — transcript-gated identical to the f16 reference) transcribes, Sortformer (469 MB, f32) works out who is speaking when, and the CLI's own projection-fusion attributes every line, all in hand-written Rust kernels compiled to wasm and running on your CPU. Both downloads are SHA-256-verified against pinned manifests, cached in browser storage, and re-verified on every visit. The Sortformer package is additionally re-authenticated inside the engine against its conversion receipt, exactly as the CLI does it. There is no server behind this panel and no mocked demo.
01 · The models
02 · The recording
Decoded in-tab by Symphonia (pure Rust), downmixed and resampled to 16 kHz mono. Honest expectation, measured on an M-series desktop: Chrome gets the threaded engine at about 1.6 times real time (a 1-minute clip takes about 100 seconds); browsers without cross-origin isolation, iPhones included, get the single-thread build at about 8 times real time. The progress bar below shows the live estimate either way.
03 · Language and speakers (optional)
Auto-detection listens to the first 30 seconds; if a recording opens with music or noise it can guess wrong (and then transcribe the whole file in the wrong language). If you know the language, pin it.
Names help twice. They feed Whisper's decoding prompt, the same
mechanism as the CLI's --prompt, so names and titles come out
spelled right. They also replace the anonymous SPEAKER_00…03 labels,
assigned in order of first appearance. The voices themselves are matched by
sound alone, so if the first person to talk is not the first name you listed,
swap the order and export again.
04 · Run
The transcript
Results land here: every line timestamped and speaker-attributed, with the same
SPEAKER_00…03 labels and fusion rules the CLI emits. The measured
wall-clock and realtime ratio print with each run, from your hardware.
Starting…
Words appear as each 30-second window finishes; speaker labels are attributed at the end, once the diarizer has heard the whole recording. Running locally in this tab; leaving it in the background is fine.
Anatomy · one code path
The same bytes as the CLI.
The wasm crate mounts the native engine's source files directly, so the browser runs the identical Rust that decodes on a workstation, behind one platform seam: a host-fed clock and a serial thread scope. What differs is named and documented; everything else is the same pipeline, stage for stage.
- ggml parser. The 874 MB q8_0 turbo file is scanned once for its tensor directory, then each tensor hydrates straight from browser storage through a positioned reader; the file never exists as one in-memory blob. Quantized blocks stay block-RESIDENT and dequantize per row inside the kernels — bit-identical math to a dequantize-at-load reference, at less than half the memory.
- Neural clean-up. FastEnhancer‑S denoises the 16 kHz signal before anything downstream hears it (see the denoiser section below), and leading silence is trimmed so Whisper never free-runs on an empty first window. Both are length-honest: timestamps stay true to the original file.
- Log-mel frontend. The reference mel pipeline, bit-faithful to the native engine: 25 ms windows, 10 ms hop, 128 mel bins for turbo.
- Encoder + decoder. 32 encoder layers, 4 decoder layers, greedy temperature-0 decode with the CLI's window retry and context-carry policies. The wasm build multiplies f16 weights with the same batched dequant-GEMV kernels the native decoder ships by default.
- Sortformer diarizer. NVIDIA's streaming four-speaker model (FastConformer frontend, transformer stack, per-frame speaker lanes), hand-ported to the same Rust and verified in-engine against its conversion receipt before a single frame is scored.
- Projection fusion. The CLI's projection-fusion-v1 attributes each transcript segment to the dominant speaker turn, fills gap segments from the turn timeline, and merges runs into speaker-attributed blocks. The wasm build calls the very function the CLI calls; the code was relocated into a shared module so the two can never drift apart.
The denoiser · clean audio in, clean words out
Noise makes Whisper hallucinate.
A transcription inherits the recording it came from, noise and all. Feed Whisper hiss,
rumble, or a silent stretch and it does something worse than missing words: it invents
them, sometimes in the wrong language. So every recording is cleaned automatically
before the model hears a sample, in the CLI and in this playground, using a pure-Rust
port of FastEnhancer‑S, a 207 K-parameter speech-enhancement network. The same port
cleans voice enrollments in our sibling project
franken_tts; the weights are an 0.8 MB file that
loads alongside the models. It is on by default and one checkbox (or
FW_DENOISE=0 in the CLI) turns it off.
How it decides what is noise
The recording is sliced into overlapping 21 ms spectrogram frames. For every time-frequency cell the network predicts a complex ratio mask: a number that scales the cell toward zero if it looks like noise and leaves it alone if it looks like speech. Two structural tricks let a model this small make that call well. Small GRUs carry memory from frame to frame, so the network builds a running picture of the steady background; hiss and rumble are steady, speech never is. And a tiny attention layer compares frequency bands within each frame; voiced speech is a comb of harmonics that move together, while broadband static has no such structure. The mask is complex, so it corrects phase as well as loudness, avoiding the underwater smearing that magnitude-only methods produce.
Why it matters for transcription
Whisper was trained mostly on reasonably clean speech, and its failure mode on noise is not graceful: confidence drops, words vanish, and on the worst stretches the decoder free-runs and invents text. The diarizer suffers too, since speaker identity lives in exactly the spectral detail that noise buries. Cleaning first attacks both problems at the source. The stage is length-preserving, so every timestamp stays truthful to your original file, and it costs a few seconds even on long recordings because it processes audio in one-minute chunks with carried state.
207 K parameters
The weights are an 0.8 MB pinned file, verified by SHA-256 like every other artifact on this page. It loads in a blink next to the 874 MB transcription model.
On by default
Zero flags to remember. The playground checkbox and the CLI's
FW_DENOISE=0 both turn it off when you want the raw signal, and the run
metadata records which way it ran.
Same code, both surfaces
One Rust implementation serves the CLI and this page. In the CLI the stage engages whenever the pinned artifact is present in the model cache; the browser ships the artifact with the models, so it is simply always available.
Diarization · who said what
Names on every line.
Transcription without attribution is half an answer, so diarization runs as a
first-class stage: Sortformer scores four speaker lanes at 80 ms
resolution, turns become a timeline with per-turn confidence and overlap suspicion, and
the fusion pass projects that timeline onto the transcript so consecutive same-speaker
segments merge into readable blocks. The CLI exposes the same result as
speaker_segments in its JSON envelope; the playground renders it directly.
Turn timeline
Start, end, speaker, confidence, and overlap suspicion for every detected turn: the raw evidence, preserved alongside the merged view.
Conservative first
A segment is labeled only when one speaker owns at least 70% of it at calibrated
confidence; the fusion pass then fills the gaps from the same evidence instead of
returning null.
Receipt-verified
The diarizer's weight package re-authenticates against its frozen conversion receipt (SHA-256 chain, tensor census) inside the engine, in the browser, every load.
Speed · every number measured
Honest numbers.
On native CPU, franken_whisper's engine measures 2.99× faster than whisper.cpp at matched greedy decode settings, side-by-side in one interleaved harness; the methodology and evidence hashes live in the repo's performance ledger. On Apple-silicon GPU, the Metal encoder went from 15.4 s to 1.85 s per window across one ledgered kernel campaign. The wasm build in this page started at 45 times real time; one byte-identical kernel pass brought it to 8.4, and the threaded lane (rayon over Web Workers, the same recipe the sibling projects ship) measured 1.56× real time in headless Chrome with 8 workers, transcript unchanged. Chrome gets the threaded engine; everything else gets the serial build. The page prints the exact ratio after each run, so the number you read is from your machine.
2.99× vs whisper.cpp
Native CPU, matched greedy settings, interleaved A/B pairs, transcript-equality gate, evidence bundle hashed in the ledger.
Every claim gated
One lever per pass, a kill-switch env for every optimization, byte-identical or ledgered. The repo's PERF_LEDGER.md and NEGATIVE_EVIDENCE.md are the record.
This page tells the truth
The playground prints wall-clock and realtime ratio per run. If it is slow on your hardware, it says so, in numbers.
Agent-first · scriptable to the bone
Built for robots too.
The CLI answers to agents first: NDJSON streaming events, a stable JSON envelope with
raw_output introspection (which encoder route actually ran, window stats,
dropped-window accounting), durable SQLite job storage, and exit codes that mean
things. If you are wiring transcription into a pipeline, the contract is the product.
fw transcribe meeting.m4a --diarize --robot | jq -r '
.result.speaker_segments[]
| "[\(.speaker // "?")] \(.text)"'
Install · one line
Take it home.
The native CLI is faster than this tab will ever be, runs every model tier, and speaks NDJSON. One line, no Python environment, no CUDA archaeology.
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_whisper/main/install.sh | bash
# or Homebrew
brew install dicklesworthstone/tap/franken-whisper
# or cargo
cargo install franken_whisper
Then: fw transcribe recording.mp3 --diarize