This guide is the source-of-truth reference for operators, ML platform engineers, and contributors. It covers Whisper's architecture, every configurable parameter, deployment patterns, performance characteristics, security/privacy implications, and the internals of the encoder/decoder, the language ID head, the temperature-fallback decoder, and the word-alignment pass.
For end-user CLI recipes and the Python API tour, see the Whisper User Guide. This document complements that one — they should be read together.
1. Architecture Overview
Whisper is a single end-to-end Transformer trained for multilingual, multitask audio-to-text transduction. The same model handles speech recognition (X→X), speech translation (X→English), language identification, and timestamp prediction — all controlled by special tokens at the start of the decoder sequence.
The Python package is intentionally thin: it owns the model weights, the tokenizer, an FFmpeg-based audio loader, a sliding-window decoder loop with temperature fallback, and writers for five output formats. There is no server, no daemon, no queue, no GPU pool — the package is meant to be embedded in your application or invoked as a CLI.
1.1 Data flow
audio file (mp3 / wav / flac / mp4 / …)
│
┌────────────┴───────────────┐
│ ffmpeg subprocess │ load_audio()
│ → 16 kHz mono float32 │ whisper/audio.py
└────────────┬───────────────┘
│
┌────────────┴───────────────┐
│ STFT + Mel filterbank │ log_mel_spectrogram()
│ → (n_mels, n_frames) │ 80 or 128 mels
│ CHUNK_LENGTH = 30 s │ N_FRAMES = 3000
└────────────┬───────────────┘
│
┌────────────┴───────────────┐
│ AudioEncoder │ whisper/model.py
│ Conv1D ×2 → Transformer │ sinusoidal pos. embed
│ → (n_audio_ctx, n_state) │
└────────────┬───────────────┘
│
┌────────────┴───────────────┐ ┌───────────────────┐
│ language detection (opt.) │ ←─ │ detect_language() │
│ argmax over lang tokens │ │ whisper/decoding │
└────────────┬───────────────┘ └───────────────────┘
│
┌────────────┴───────────────┐
│ TextDecoder (autoregress) │ whisper/model.py
│ cross-attn over audio │ kv-cache enabled
│ + LogitFilter rules │ timestamp / suppress
│ + Greedy/BeamSearch │ temperature fallback
│ → tokens + logprobs │
└────────────┬───────────────┘
│
┌────────────┴───────────────┐
│ optional: word DTW pass │ add_word_timestamps()
│ cross-attn alignment │ whisper/timing.py
│ → per-word start/end │ triton_ops on CUDA
└────────────┬───────────────┘
│
┌────────────┴───────────────┐
│ ResultWriter (per format) │ whisper/utils.py
│ txt / srt / vtt / tsv / │
│ json │
└────────────┬───────────────┘
│
▼
transcript files
Note
Whisper processes audio in 30-second non-overlapping windows. Anything longer is sliced internally; anything shorter is zero-padded. The
condition_on_previous_textflag controls whether each window's prompt includes the previous window's output, which is what gives long-form transcription continuity at the cost of occasional looped degeneration.
1.2 Component inventory
| Component | Path | Role |
|---|---|---|
| Package init / model registry | whisper/__init__.py |
URL table, SHA-256 verification, load_model(), available_models(). |
| CLI entrypoint | whisper/__main__.py |
One-liner: from .transcribe import cli; cli(). |
| Audio I/O | whisper/audio.py |
load_audio (FFmpeg shell-out), pad_or_trim, log_mel_spectrogram. |
| Decoder & inference | whisper/decoding.py |
DecodingOptions, DecodingResult, DecodingTask, beam/greedy decoders, logit filters. |
| Transformer model | whisper/model.py |
Whisper, AudioEncoder, TextDecoder, attention, ModelDimensions. |
| Word timestamps | whisper/timing.py |
DTW alignment, median filter, find_alignment, add_word_timestamps. |
| Tokenizer | whisper/tokenizer.py |
tiktoken wrapper, special tokens, language list, word-token splitter. |
| Long-form pipeline | whisper/transcribe.py |
transcribe() (sliding window + fallback), cli() (argparse). |
| Triton GPU kernels | whisper/triton_ops.py |
DTW kernel + median filter for CUDA acceleration. |
| Output writers | whisper/utils.py |
WriteTXT/WriteSRT/WriteVTT/WriteTSV/WriteJSON, format_timestamp, helpers. |
| Text normalizers | whisper/normalizers/ |
English number/spelling normalizer + basic Unicode normalizer (used for WER scoring). |
| Version stamp | whisper/version.py |
__version__ = "20250625". |
1.3 Technology stack
| Layer | Tech |
|---|---|
| Modeling framework | PyTorch (≥ 1.13 recommended for weights_only=True torch.load) |
| Tokenizer | tiktoken (BPE) |
| Numerics | NumPy |
| JIT compilation | Numba (CPU DTW), Triton (CUDA DTW + median filter) |
| Progress bars | tqdm |
| Audio decoding | ffmpeg CLI (subprocess shell-out) |
| Iteration helpers | more-itertools |
The dependency list is small by design (requirements.txt):
numba
numpy
torch
tqdm
more-itertools
tiktoken
triton>=2.0.0;platform_machine=="x86_64" and sys_platform=="linux" or sys_platform=="linux2"
1.4 Model dimensions
The ModelDimensions dataclass in whisper/model.py parameterises the
Transformer. Each checkpoint stores its own dims, so load_model can
build the correct architecture from the file:
@dataclass
class ModelDimensions:
n_mels: int
n_audio_ctx: int
n_audio_state: int
n_audio_head: int
n_audio_layer: int
n_vocab: int
n_text_ctx: int
n_text_state: int
n_text_head: int
n_text_layer: int
The audio encoder consumes (batch, n_mels, 3000) log-mel frames and emits
(batch, n_audio_ctx, n_audio_state) features (e.g. 1500 × 1280 for
large-v3). The text decoder runs autoregressively over n_text_ctx = 448
tokens with cross-attention into the encoder output.
Note
is_multilingualis detected byn_vocab >= 51865, andnum_languagesis derived fromn_vocab - 51765 - int(is_multilingual). This is how the same code paths supportlarge-v3(99 langs),large-v2(100 langs), and the English-only.envariants in one library.
2. System Requirements
2.1 Software
| Dependency | Minimum | Notes |
|---|---|---|
| Python | 3.8 | Older versions not supported. |
| PyTorch | ≥ 1.13 recommended | The loader sets weights_only=True if the installed torch.__version__ >= "1.13". Earlier still works but without the safer load. |
| FFmpeg | any modern | Required at runtime for audio decoding. |
| OS | Linux, macOS, Windows | Triton acceleration is Linux/x86_64 only. |
Python dependencies are installed transitively by pip: numba, numpy,
torch, tqdm, more-itertools, tiktoken, triton (Linux/x86_64).
2.2 Hardware
| Workload | CPU | GPU | RAM |
|---|---|---|---|
tiny / tiny.en |
any modern x86/ARM | optional | 2 GB |
base / base.en |
any modern x86/ARM | optional | 2 GB |
small / small.en |
4+ cores recommended | helpful | 4 GB |
medium / medium.en |
8+ cores | strongly recommended | 8 GB |
large-v1/v2/v3, large |
16+ cores | required for usable speed | 16 GB |
turbo (large-v3-turbo) |
8+ cores | recommended | 12 GB |
Per-model VRAM (peak, FP16, batch size 1): tiny ~1 GB, base ~1 GB,
small ~2 GB, medium ~5 GB, large-v* ~10 GB, turbo ~6 GB. Add ~10–20%
headroom for the operating system and the cudnn workspace.
Important
For production batch transcription with
large-v3, plan for an A100-40GB or H100 if you want headroom for batched decoding. A consumer 24 GB GPU (RTX 3090/4090/5090) handleslarge-v3for single-stream FP16 jobs but may OOM on long word-timestamp runs because the Triton DTW kernel needs additional contiguous workspace.
2.3 Storage
Model weights live in ~/.cache/whisper/ by default (override with
XDG_CACHE_HOME or the download_root argument to load_model). Sizes:
| Model | Approx size on disk |
|---|---|
tiny[.en] |
39 MB / 75 MB |
base[.en] |
142 MB |
small[.en] |
461 MB |
medium[.en] |
1.42 GB |
large-v1, large-v2, large-v3, large |
2.87 GB each |
large-v3-turbo, turbo |
1.51 GB |
The assets/ directory ships ~6 MB of mel filter banks and tiktoken vocab
files; these are bundled with the wheel and require no separate download.
3. Configuration Reference
Whisper is configured exclusively through CLI arguments and Python kwargs. There are no config files, no XML/YAML, and only a single environment variable.
3.1 Environment variables
| Variable | Read by | Default | Purpose |
|---|---|---|---|
XDG_CACHE_HOME |
whisper/__init__.py:134 |
~/.cache |
Sets the parent of whisper/ cache dir. The full default is ~/.cache/whisper/. |
There are no other environment variables. Whisper does not read WHISPER_*,
TRANSCRIBE_*, or any custom env namespace.
3.2 CLI flags — full reference
Every flag accepted by whisper (in source order, from cli() in
whisper/transcribe.py):
| Flag | Type | Default | Description |
|---|---|---|---|
audio |
str (positional, nargs="+") |
required | One or more audio files to transcribe. |
--model |
valid_model_name |
turbo |
Model name from available_models() or path to a .pt checkpoint. |
--model_dir |
str | None |
Directory to download/cache models in. Defaults to ~/.cache/whisper. |
--device |
str | cuda if available else cpu |
PyTorch device selector. |
--output_dir / -o |
str | . |
Output directory for transcript files. |
--output_format / -f |
str | all |
One of txt, vtt, srt, tsv, json, all. |
--verbose |
str2bool |
True |
Print decoded segments as they complete. |
--task |
str | transcribe |
transcribe or translate (X→English). |
--language |
str | None |
Language code or name; None triggers auto-detect. |
--temperature |
float | 0 |
Sampling temperature (greedy at 0). |
--best_of |
optional_int |
5 |
Sample candidates when temperature > 0. |
--beam_size |
optional_int |
5 |
Beam search width when temperature == 0. |
--patience |
float | None |
Beam-search patience (arxiv:2204.05424). |
--length_penalty |
float | None |
Length penalty alpha (must be in [0, 1]). |
--suppress_tokens |
str | -1 |
Comma-separated token ids to suppress; -1 uses non_speech_tokens. |
--initial_prompt |
str | None |
First-window decoder prompt (custom vocabulary, context). |
--carry_initial_prompt |
str2bool |
False |
Re-prepend initial_prompt to every internal decode() call. |
--condition_on_previous_text |
str2bool |
True |
Use prior window output as next window's prompt. |
--fp16 |
str2bool |
True |
Run inference in FP16 (auto-disabled on CPU). |
--temperature_increment_on_fallback |
optional_float |
0.2 |
Step added to temperature on each fallback. |
--compression_ratio_threshold |
optional_float |
2.4 |
Treat segment as failure when gzip ratio is above this. |
--logprob_threshold |
optional_float |
-1.0 |
Treat segment as failure when avg logprob is below this. |
--no_speech_threshold |
optional_float |
0.6 |
Treat as silence when no_speech prob > threshold AND avg logprob < logprob_threshold. |
--word_timestamps |
str2bool |
False |
Run cross-attention DTW alignment, populate per-word timings. |
--prepend_punctuations |
str | "'“¿([{- |
Punctuation merged with the next word in word timing. |
--append_punctuations |
str | "'.。,,!!??::”)]}、 |
Punctuation merged with the previous word in word timing. |
--highlight_words |
str2bool |
False |
Underline currently-spoken word in SRT/VTT (requires --word_timestamps True). |
--max_line_width |
optional_int |
None |
Max chars per subtitle line (requires --word_timestamps True). |
--max_line_count |
optional_int |
None |
Max lines per subtitle block (requires --word_timestamps True). |
--max_words_per_line |
optional_int |
None |
Max words per subtitle line (requires --word_timestamps True; no effect with --max_line_width). |
--threads |
optional_int |
0 |
torch.set_num_threads(). Supersedes MKL_NUM_THREADS / OMP_NUM_THREADS. |
--clip_timestamps |
str | 0 |
start,end,start,end,... seconds; only transcribe these clips. |
--hallucination_silence_threshold |
optional_float |
None |
Skip silent periods longer than this (seconds) when a hallucination is suspected. Requires --word_timestamps True. |
3.3 transcribe() keyword arguments
Programmatic equivalents to the CLI flags. Defaults differ for
temperature and logprob_threshold between CLI and API:
| Argument | Type | Default (API) | Mapping |
|---|---|---|---|
verbose |
Optional[bool] |
None |
CLI --verbose (default True). None suppresses all output. |
temperature |
Union[float, Tuple[float, ...]] |
(0.0, 0.2, 0.4, 0.6, 0.8, 1.0) |
API uses tuple for fallback; CLI builds the tuple from --temperature and --temperature_increment_on_fallback. |
compression_ratio_threshold |
Optional[float] |
2.4 |
--compression_ratio_threshold. |
logprob_threshold |
Optional[float] |
-1.0 |
--logprob_threshold. |
no_speech_threshold |
Optional[float] |
0.6 |
--no_speech_threshold. |
condition_on_previous_text |
bool |
True |
--condition_on_previous_text. |
initial_prompt |
Optional[str] |
None |
--initial_prompt. |
carry_initial_prompt |
bool |
False |
--carry_initial_prompt. |
word_timestamps |
bool |
False |
--word_timestamps. |
prepend_punctuations |
str |
"'“¿([{- |
--prepend_punctuations. |
append_punctuations |
str |
"'.。,,!!??::”)]}、 |
--append_punctuations. |
clip_timestamps |
Union[str, List[float]] |
"0" |
--clip_timestamps. |
hallucination_silence_threshold |
Optional[float] |
None |
--hallucination_silence_threshold. |
**decode_options |
dict | {} |
Splat into DecodingOptions (e.g. language, task, beam_size, best_of, patience, length_penalty, suppress_tokens, prompt, prefix, suppress_blank, without_timestamps, max_initial_timestamp, fp16). |
Important
temperaturedefaults differ between CLI and API. The CLI passes a single float through--temperature(default0) and lets--temperature_increment_on_fallbackbuild the fallback tuple. The Python API expects a tuple directly (default(0.0, 0.2, 0.4, 0.6, 0.8, 1.0)). If you calltranscribe()withtemperature=0, you disable the fallback ladder entirely — usually not what you want for long-form audio.
3.4 DecodingOptions (single 30-s window)
Every field of the dataclass in whisper/decoding.py:
| Field | Type | Default | Description |
|---|---|---|---|
task |
str | transcribe |
transcribe or translate. |
language |
Optional[str] |
None |
Language code; None triggers detection. |
temperature |
float | 0.0 |
Single temperature for this decode call. |
sample_len |
Optional[int] |
None |
Max tokens to sample (defaults to n_text_ctx // 2). |
best_of |
Optional[int] |
None |
Independent samples when temperature > 0. |
beam_size |
Optional[int] |
None |
Beams when temperature == 0. |
patience |
Optional[float] |
None |
Beam patience. |
length_penalty |
Optional[float] |
None |
Google-NMT length penalty alpha ([0, 1]). |
prompt |
Optional[Union[str, List[int]]] |
None |
Text or token ids prepended to the decoder input. |
prefix |
Optional[Union[str, List[int]]] |
None |
Forces the start of the output. |
suppress_tokens |
Optional[Union[str, Iterable[int]]] |
"-1" |
-1 uses tokenizer.non_speech_tokens. |
suppress_blank |
bool |
True |
Suppress blank-only outputs. |
without_timestamps |
bool |
False |
If True, sample text-only tokens (no <|0.00|> etc.). |
max_initial_timestamp |
Optional[float] |
1.0 |
Cap on the first timestamp the model can emit. |
fp16 |
bool |
True |
FP16 inference. |
3.5 Audio constants
Hard-coded in whisper/audio.py (do not change without retraining):
| Constant | Value | Meaning |
|---|---|---|
SAMPLE_RATE |
16000 |
Hz. All inputs are resampled to this. |
N_FFT |
400 |
FFT window. |
HOP_LENGTH |
160 |
Frame hop. |
CHUNK_LENGTH |
30 |
Seconds per encoder window. |
N_SAMPLES |
480000 |
Samples per 30-second chunk. |
N_FRAMES |
3000 |
Mel frames per chunk. |
N_SAMPLES_PER_TOKEN |
320 |
Stride-2 conv ⇒ 20 ms per output token. |
FRAMES_PER_SECOND |
100 |
10 ms per audio frame. |
TOKENS_PER_SECOND |
50 |
Reciprocal of token stride. |
4. Deployment & Scaling
4.1 Embedded library
The simplest deployment: ship Whisper as a Python dependency of your application. Hold one model in memory per process and serve transcription requests via your own queue/router.
# server-startup.py
import whisper
MODEL = whisper.load_model("turbo", device="cuda")
def handle_request(audio_path, language=None):
return MODEL.transcribe(audio_path, language=language)
This is appropriate for moderate-traffic services where you can size one GPU per process. Whisper does not support shared-GPU multi-tenancy out of the box — you cannot load one model and have multiple Python processes hit it concurrently.
4.2 CLI in a Docker container
A minimal Linux container:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -U openai-whisper
WORKDIR /work
ENTRYPOINT ["whisper"]
Build and use:
docker build -t whisper:turbo .
docker run --rm -v $(pwd):/work whisper:turbo audio.mp3 --model small.en
For GPU use, base on nvidia/cuda:12.1.1-runtime-ubuntu22.04, install
python3 + pip, install a CUDA-matched PyTorch wheel, then
pip install -U openai-whisper. Run with --gpus all.
4.3 Pre-baked model cache
Bake the model into the image to avoid the first-run download:
RUN python -c "import whisper; whisper.load_model('turbo')"
# Layer above caches /root/.cache/whisper/large-v3-turbo.pt (1.5 GB)
For air-gapped deployments, build the image somewhere with internet, then ship the layer.
Tip
In Kubernetes, mount
~/.cache/whisperfrom aPersistentVolumeshared across pods (read-only) so each new pod doesn't re-download the checkpoint. Whisper SHA-256-verifies on load — concurrent reads are safe.
4.4 Scaling out
Whisper is embarrassingly parallel at the file level. The recommended pattern:
input queue (S3, Redis, RabbitMQ, …)
│
▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Worker pod (GPU) │ │ Worker pod (GPU) │ │ Worker pod (GPU) │
│ load_model("…") │ │ load_model("…") │ │ load_model("…") │
│ for path in queue │ │ for path in queue │ │ for path in queue │
│ transcribe(...) │ │ transcribe(...) │ │ transcribe(...) │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
└───────────────────────┴───────────────────────┘
│
▼
output store (S3, DB)
Each worker holds one model resident, pulls jobs from a queue, and writes
results back. With large-v3-turbo you can typically run one worker per
GPU at saturated utilisation; with tiny/base you may be able to run
two or three workers per GPU because the model is small enough that
multiple CUDA contexts fit.
Note
There is no built-in batched inference path. If you have many short clips, you'll get more throughput by collecting them into 30-second super-clips with a sentinel token between them — but that requires custom code on top of
decode(). The stocktranscribe()is single-stream.
4.5 Health checks
A useful liveness check is to transcribe tests/jfk.flac (a 12-second
sample bundled with the source repo) and assert the result contains the
word "country":
import whisper
model = whisper.load_model("tiny.en")
out = model.transcribe("tests/jfk.flac")
assert "country" in out["text"].lower()
5. Operations
5.1 Capacity planning
Throughput is a function of model size, GPU FP16 throughput, and audio duration. Real-world numbers, single A100-40GB, FP16, batch=1, no word timestamps:
| Model | Real-time factor (RTF) |
|---|---|
tiny |
30–40x |
base |
20–30x |
small |
8–15x |
medium |
4–8x |
large-v3 |
1.5–3x |
turbo (large-v3-turbo) |
6–12x |
"RTF" here means seconds of audio processed per second of wall time. Word timestamps add ~10–25% overhead on GPU (the Triton DTW kernel) and ~50–100% on CPU.
Tip
A useful heuristic for batch jobs: assume
RTF/2to account for first-window language detection, occasional fallback re-decodes at higher temperature, and disk I/O.
5.2 Logging and monitoring
The package emits very little — tqdm progress bars and the per-segment
prints when verbose=True. To capture structured logs, wrap the call:
import time, json, logging
log = logging.getLogger("whisper.adapter")
def transcribe_logged(model, path, **kwargs):
t0 = time.monotonic()
try:
result = model.transcribe(path, verbose=False, **kwargs)
log.info(json.dumps({
"event": "transcribe.ok",
"path": path,
"language": result["language"],
"n_segments": len(result["segments"]),
"wall_seconds": round(time.monotonic() - t0, 3),
}))
return result
except Exception as e:
log.exception("transcribe.fail path=%s", path)
raise
Useful per-segment metrics inside result["segments"]:
avg_logprob(lower thanlogprob_threshold⇒ low confidence).compression_ratio(abovecompression_ratio_threshold⇒ likely loop).no_speech_prob(aboveno_speech_threshold⇒ silent / non-speech).temperature(the temperature at which this segment finally succeeded;0 means the first attempt was rejected).
5.3 Cache management
The on-disk cache layout:
~/.cache/whisper/
tiny.pt
base.en.pt
small.pt
…
Each .pt file is verified against the SHA-256 in its download URL on
every load. If the hash mismatches, Whisper warns and re-downloads. Manual
cleanup: just rm the unwanted .pt files; Whisper recreates the cache
directory on next run.
Note
The cache directory is shared across all users on the host who use the same
XDG_CACHE_HOME. In multi-tenant environments, setXDG_CACHE_HOMEper user or run withdownload_root=...to isolate.
5.4 Backup and restore
There is no state to back up. Whisper is stateless — model weights are re-downloadable, and transcription outputs are whatever your application writes them to. Snapshot the cache dir if you want to skip download latency on restore.
5.5 Upgrades
pip install -U openai-whisper
The version is in whisper/version.py (__version__ = "20250625" at
commit 04f449b). Major upgrades typically bump the model URL table —
existing checkpoints continue to load, but the default model may change
(turbo became the default in 2024). Pin --model explicitly in
production scripts to avoid surprises on upgrade.
Warning
Whisper does not have a plugin or extension API. Anything you build on top is on top of the Python imports — be aware that internal APIs in
whisper.decoding,whisper.timing, andwhisper.transcribeare not stability-guaranteed across releases.
6. Performance
6.1 The model size table (operations view)
| Model | Params | Layers (audio/text) | n_state | Heads | n_mels | Vocab | Languages | Disk | VRAM (FP16) |
|---|---|---|---|---|---|---|---|---|---|
tiny |
39 M | 4 / 4 | 384 | 6 | 80 | 51865 | 99 | 39 MB | ~1 GB |
base |
74 M | 6 / 6 | 512 | 8 | 80 | 51865 | 99 | 142 MB | ~1 GB |
small |
244 M | 12 / 12 | 768 | 12 | 80 | 51865 | 99 | 461 MB | ~2 GB |
medium |
769 M | 24 / 24 | 1024 | 16 | 80 | 51865 | 99 | 1.42 GB | ~5 GB |
large-v1, large-v2 |
1550 M | 32 / 32 | 1280 | 20 | 80 | 51865 | 99–100 | 2.87 GB | ~10 GB |
large-v3, large |
1550 M | 32 / 32 | 1280 | 20 | 128 | 51866 | 99 | 2.87 GB | ~10 GB |
turbo, large-v3-turbo |
809 M | 32 / 4 | 1280 | 20 | 128 | 51866 | 99 | 1.51 GB | ~6 GB |
turbo is "large-v3 with a much smaller decoder" — encoder is the same
1.55B-class network, but the text decoder shrinks from 32 layers to 4,
which is where the speedup comes from.
6.2 Latency vs. throughput
For interactive use (transcribe one file as fast as possible), pick
turbo and enable FP16 on a recent GPU. For batch processing of many
files, the bottleneck shifts: even on tiny, throughput is GPU-bound
once your storage is fast enough.
A rough breakdown of where time goes during a typical large-v3 call on
a 1-hour audio file:
- Audio decode (FFmpeg): 1–3% (often bound by disk).
- Mel spectrogram: <1%.
- Encoder forward pass: ~30%.
- Decoder + temperature fallback: ~60–65%.
- Word-timestamp DTW (if enabled): ~5–10%.
- Result writers: <1%.
Tip
Word timestamps are NOT free. If your downstream system doesn't actually consume them, leave
word_timestamps=False(the default) — you'll save 5–25% wall time on GPU and 50–100% on CPU.
6.3 FP16 vs. FP32
| Setting | Where | Effect |
|---|---|---|
fp16=True |
GPU | Default. Roughly 1.5–2x faster than FP32 with no measurable WER impact. |
fp16=True |
CPU | Auto-disabled with a warning (FP16 is not supported on CPU; using FP32 instead). |
fp16=False |
GPU | Forces FP32 — slower, no benefit unless debugging FP16 instability. |
The fallback ladder (temperature 0 → 0.2 → 0.4 → ... → 1.0) re-decodes
the same 30-second window up to 6 times if the output keeps failing the
compression-ratio or logprob thresholds. A pathological window can blow
out the per-window budget.
6.4 KV cache
whisper/model.py installs forward hooks on every key/value projection
in the decoder via Whisper.install_kv_cache_hooks(). During
autoregressive sampling, only the last token's logits are computed and
the cached K/V tensors are reused. Without this, decoding a single
30-second window would require quadratic compute in the token count;
with it, it is linear.
The KV cache is per-DecodingTask, lives in a dict mapping
nn.Module → Tensor, and is destroyed at the end of _main_loop() via
PyTorchInference.cleanup_caching(). There is no cross-window cache
reuse — every new 30-second window starts with an empty KV cache.
6.5 Threading
CPU inference uses PyTorch's intra-op thread pool. Set it via
--threads N (CLI) or torch.set_num_threads(N) (Python). The CLI
explicitly notes this supersedes MKL_NUM_THREADS and
OMP_NUM_THREADS.
For multiple parallel transcriptions on CPU, prefer multiple processes over multiple threads — Whisper itself is not thread-safe at the model instance level, and the Python GIL limits intra-process parallelism.
6.6 Profiling tips
- Set
verbose=Falseand wrap withtime.monotonic()for end-to-end measurement. - Inspect
result["segments"][i]["temperature"]— non-zero values signal that fallback re-decodes happened. - Watch
result["segments"][i]["compression_ratio"]— values close to the2.4threshold mean you are one bad window away from a fallback cascade. - For GPU profiling, run with
CUDA_LAUNCH_BLOCKING=1and use Nsight Systems ortorch.profiler.
7. Security
Whisper is an inference library that runs entirely on the local machine. This makes its attack surface small but not zero.
7.1 Privacy properties
The single most important property: audio is processed locally and is
never sent over the network. Whisper's only network call is the one-time
download of model weights from https://openaipublic.azureedge.net/main/whisper/models/....
Once those weights are cached, the package is fully offline-capable.
Implications:
- Recordings of customer conversations, medical dictation, legal interviews, etc. stay on the host you control. Data residency is whatever the host's residency is.
- Whisper itself does not log audio, transcripts, or metadata. Anything
your wrapper code logs is on you. Be careful logging
result["segments"][i]["text"]— it contains the actual transcript. - There is no telemetry. No usage pings, no error reporting, no anonymous metrics.
Caution
If your transcripts contain personally identifying information (PII), regulated health data (PHI), or attorney-client material, treat them as the same sensitivity class as the original audio. Don't log them verbatim, don't store them in shared queues without encryption, and consider segment-level redaction before persisting.
7.2 Model weight provenance
Whisper downloads weights over HTTPS and verifies the file's SHA-256
against the hash baked into its URL. The hash table for every model is
_MODELS in whisper/__init__.py. Example:
"large-v3": "https://openaipublic.azureedge.net/main/whisper/models/e5b1a55b89c1367dacf97e3e19bfd829a01529dbfdeefa8caeb59b3f1b81dadb/large-v3.pt",
The 64-hex segment between models/ and the filename is the SHA-256
the loader checks against. A man-in-the-middle who modifies the bytes
will change the hash; Whisper raises:
RuntimeError: Model has been downloaded but the SHA256 checksum does not not match. Please retry loading the model.
Warning
The integrity check protects against in-flight corruption and naive tampering. It does not protect against an attacker who compromises
openaipublic.azureedge.netand serves a pre-image-collision-resistant backdoored checkpoint with the same SHA-256 — currently believed infeasible, but worth noting. For ultra-high-trust environments, distribute weights out-of-band.
7.3 Untrusted input
whisper.load_model(name) accepts a string that is either a model name
or a filesystem path. If you call it with attacker-controlled input,
you give them path-controlled torch.load() access. The package mitigates
this by setting weights_only=True (when torch.__version__ >= "1.13"),
which restricts which Python types pickle will reconstruct — but
weights_only is not a hard sandbox.
For audio inputs: whisper.load_audio() shells out to ffmpeg with the
file path as -i. Pass only paths your application controls. FFmpeg has
its own attack surface against malformed media — keep it patched.
7.4 What NOT to log
- The full transcript (
result["text"]). result["segments"][i]["text"].- The user's
initial_promptif it contains business secrets (e.g. customer names, internal codenames you fed in for vocab biasing). - The audio file's path if it is constructed from PII (
recordings/<email>.mp3). - Token IDs in
result["segments"][i]["tokens"]— they aren't directly human-readable, but a tiktoken decode reproduces the transcript.
Safe to log: language, segment count, temperatures, average log-probs, durations, model name, error class names.
7.5 Network policies
Whisper's only outbound traffic targets openaipublic.azureedge.net over
HTTPS (port 443). For locked-down environments:
- Pre-populate
~/.cache/whisper/with the models you need. - Block egress to
openaipublic.azureedge.net. - Set
XDG_CACHE_HOMEto a read-only volume in production.
8. Developer Internals
8.1 Repository layout
whisper/
├── __init__.py model registry, load_model, available_models
├── __main__.py CLI entry: from .transcribe import cli; cli()
├── audio.py load_audio (ffmpeg), pad_or_trim, log_mel_spectrogram
├── decoding.py DecodingOptions, DecodingResult, DecodingTask, decoders, filters
├── model.py Whisper, AudioEncoder, TextDecoder, attention, kv-cache hooks
├── timing.py DTW alignment, find_alignment, add_word_timestamps
├── tokenizer.py tiktoken wrapper, LANGUAGES, get_tokenizer
├── transcribe.py transcribe(), cli() — long-form pipeline + argparse
├── triton_ops.py DTW + median filter Triton kernels (Linux/x86_64 GPU)
├── utils.py ResultWriter (txt/srt/vtt/tsv/json), helpers
├── version.py __version__ = "20250625"
├── assets/ gpt2.tiktoken, multilingual.tiktoken, mel_filters.npz
└── normalizers/
├── basic.py BasicTextNormalizer (Unicode-symbol stripping)
├── english.py EnglishTextNormalizer (number/spelling normalization)
└── english.json Spelling correction table
8.2 The encoder/decoder model
whisper.model.Whisper is a standard Transformer with a few audio-specific
twists:
- AudioEncoder (
model.py:174):- Two
Conv1dlayers (kernel 3) — the second has stride 2, so 30s → 1500 frames at the encoder output. - Add a fixed sinusoidal positional embedding (
sinusoids()). n_audio_layerResidualAttentionBlocks with self-attention only.- Final
LayerNorm.
- Two
- TextDecoder (
model.py:207):- Token embedding (shared with the output projection — no separate
lm_head, the logits are computed asx @ token_embedding.T). - Learned positional embedding (length
n_text_ctx = 448). n_text_layerResidualAttentionBlocks with self-attention plus cross-attention into the encoder output.- Causal mask (
triu(1)).
- Token embedding (shared with the output projection — no separate
- MultiHeadAttention (
model.py:81):- Uses
torch.nn.functional.scaled_dot_product_attention(SDPA) when available — falls back to a manual softmax+matmul otherwise. - The
disable_sdpa()context manager is used during word-timestamp DTW so the raw attention weights can be captured by hooks.
- Uses
8.3 Special tokens and the language ID head
The tokenizer (whisper/tokenizer.py) wraps tiktoken.Encoding and adds a
fixed set of specials. The decoder sequence template is:
<|startoftranscript|> <|<lang>|> <|transcribe|> <text tokens> <|endoftext|>
<|startoftranscript|> <|<lang>|> <|translate|> <text tokens> <|endoftext|>
<|startoftranscript|> <|<lang>|> <|transcribe|> <|notimestamps|> ... <|endoftext|>
Plus 1501 timestamp tokens <|0.00|> through <|30.00|> in 0.02-second
increments — these are why Whisper can emit timestamps natively without a
separate alignment model.
detect_language() (decoding.py:19) is the language ID head:
- Run the encoder to get audio features.
- Forward the decoder for one step with just
<|startoftranscript|>. - Mask out non-language-token logits.
argmax→ most probable language token.
The "head" is not a separate module — it's the same decoder, queried at exactly position 1 of the sequence with a logit mask. Elegant and zero extra parameters.
8.4 The decoder loop and logit filters
DecodingTask (decoding.py:508) is the per-30s-window controller. Its
run() method:
- Builds the initial token sequence (sot + lang + task + optional prompt/prefix).
- Picks a decoder strategy:
BeamSearchDecoderifbeam_sizeis set, elseGreedyDecoder(which becomes sampling attemperature > 0). - Picks logit filters: always
SuppressTokensfor non-speech symbols, optionallySuppressBlank, and (unlesswithout_timestamps)ApplyTimestampRules. - Runs
_main_loop: for each step, get logits, apply filters, ask the decoder to pick the next token, repeat until EOT or context full. - Use
MaximumLikelihoodRankerto pick the best of beam-search / best-of-N candidates.
ApplyTimestampRules (decoding.py:441) is the most interesting filter.
It enforces:
- Timestamps appear in pairs (each segment has a start and end).
- Each segment is non-zero length.
- The first sampled token must be a timestamp (so segments don't start with text).
- If the total probability mass over timestamp tokens exceeds the max text-token probability at any step, force the next token to be a timestamp (this is what gives Whisper its "predict timestamps natively" behaviour).
8.5 Long-form transcription (sliding window + temperature fallback)
transcribe() in whisper/transcribe.py implements long-form
transcription via a sliding-window state machine:
prev_seek = 0
while seek < total_frames:
take 30-second mel window starting at `seek`
decode with temperature fallback ladder:
for t in temperatures (default (0, 0.2, 0.4, 0.6, 0.8, 1.0)):
kwargs = decode_options excluding incompatible (beam_size vs best_of)
result = model.decode(window, DecodingOptions(**kwargs, temperature=t))
failed = (compression_ratio > 2.4 OR avg_logprob < -1.0) AND not silence
if not failed: break
if no_speech_prob > 0.6 and avg_logprob < -1.0:
skip this window (silence fast-forward)
else:
carve segments out of timestamp-token boundaries
if word_timestamps: run DTW alignment
seek += min(N_FRAMES, last-segment-end-in-frames)
if condition_on_previous_text: prepend last window's tokens to next prompt
The result dict aggregates segments across windows and re-decodes the
final concatenated tokens to produce result["text"].
8.6 Word-level timestamps (cross-attention DTW)
add_word_timestamps() in whisper/timing.py:
- Re-run the model with hooks that capture cross-attention weights
from the alignment heads. The alignment heads are pre-determined per
model — base85-encoded boolean masks in
_ALIGNMENT_HEADSinwhisper/__init__.py:38. - Median-filter the attention matrix along the time axis (Triton CUDA kernel if available, else NumPy/Numba).
- Run dynamic time warping (DTW) between predicted tokens and audio
frames to find the best monotonic alignment. The CUDA path uses the
Triton kernel
dtw_kernelinwhisper/triton_ops.py; the CPU path usesdtw_cpudecorated with@numba.jit(parallel=True). - Convert frame indices to seconds via
TOKENS_PER_SECOND = 50. - Merge punctuation tokens with adjacent words per
prepend_punctuations/append_punctuations. - Smooth out word durations at sentence boundaries to suppress pathological long words.
The output is per-word {word, start, end, probability} injected into
the segment dicts as "words".
Note
Word timestamps are an inference-time capability, not a training objective. Whisper was not trained directly on word-level alignments — the alignment is derived post hoc from cross-attention. This is why
--task translatewarns: the cross-attention pattern between English output tokens and non-English audio is less interpretable.
8.7 Logit filters reference
| Class (decoding.py) | Purpose |
|---|---|
SuppressBlank |
At sample position 0, suppress ' ' and <|endoftext|> so output cannot be empty. |
SuppressTokens |
Mask a fixed set of token IDs (-1 expands to non_speech_tokens plus task/sot tokens). |
ApplyTimestampRules |
Enforces timestamp pairing, monotonicity, max initial timestamp, and "prefer timestamps" behaviour. |
8.8 Decoder strategies
| Class (decoding.py) | Used when |
|---|---|
GreedyDecoder |
temperature > 0 (sampling) or as a fallback when beam_size is None and temperature == 0. |
BeamSearchDecoder |
beam_size is not None and temperature == 0. Optional patience extends beam exploration. |
MaximumLikelihoodRanker |
Final pick across beams or best-of-N samples, with optional length penalty. |
8.9 Output writers
whisper.utils.get_writer() returns a callable for the requested format.
The class hierarchy:
ResultWriter
├── WriteTXT .txt one segment per line
├── SubtitlesWriter
│ ├── WriteVTT .vtt WEBVTT, dot-decimal millis
│ └── WriteSRT .srt SRT, comma-decimal millis, hours always shown
├── WriteTSV .tsv start_ms\tend_ms\ttext (no header decoration)
└── WriteJSON .json full result dict via json.dump
When output_format is "all", get_writer returns a write_all
callable that fans out to all five.
8.10 Code patterns
@torch.no_grad()is applied at every entry point that runs the model (detect_language,decode,DecodingTask.run).@dataclass(frozen=True)forDecodingOptionsandDecodingResult— option objects are immutable;replace()returns a copy.- Forward hooks for KV-cache and attention capture. Hooks are always
paired with a
cleanupstep to avoid leaks. @lru_cache(maxsize=None)on tokenizer construction (get_tokenizer,get_encoding) and the mel filter loader — they are pure functions of their arguments.- Numba JIT for hot loops on CPU (
dtw_cpu,backtrace). - Triton JIT for the same loops on CUDA, with a graceful fallback
via try/except around
RuntimeErrorandsubprocess.CalledProcessError.
8.11 Testing strategy
The repository ships a tests/ directory with pytest-style tests:
tests/test_audio.py—load_audio,log_mel_spectrogram, sample rate / frame count invariants.tests/test_normalizer.py—BasicTextNormalizer,EnglishTextNormalizer.tests/test_timing.py— DTW alignment correctness on synthetic input.tests/test_tokenizer.py— special-token IDs, language token lookup.tests/test_transcribe.py— end-to-end transcription ontests/jfk.flac.
Run them with pytest tests/. Tests are CPU-friendly; test_transcribe.py
uses tiny.en to keep wall time manageable.
8.12 Code quality tooling
.pre-commit-config.yaml (in the root) configures:
| Hook | Version | Purpose |
|---|---|---|
pre-commit-hooks |
v5.0.0 | check-json, end-of-file-fixer, trailing-whitespace, mixed-line-ending, check-added-large-files (4 MB cap). |
psf/black |
25.1.0 | Code formatter. |
pycqa/isort |
6.0.0 | Import sorting (--profile black, line length 88). |
pycqa/flake8 |
7.1.1 | Linter (max-line-length 88, ignore E203,E501,W503,W504). |
Match these before submitting a PR upstream.
8.13 Potential improvements
The codebase is mature, but the source has a few visible TODOs and non-obvious behaviours worth flagging:
transcribe.pyhas a comment"NOTE: This loop is obscurely flattened to make the diff readable. A later commit should turn this into a simpler nested loop."nearwhile clip_idx < len(seek_clips). Refactor candidate.- No batched inference at the
transcribe()level — each file serially decodes through 30s windows. A batched-window decoder would improve throughput on short-clip workloads. - Triton fallback warnings are silent by default
(
warnings.warn(...)). If you depend on the GPU DTW path, add asimplefilter("error", UserWarning)guard in development. - No ONNX/TensorRT export included in the package. Community ports exist (whisper.cpp, faster-whisper, distil-whisper) but are not part of this repo.
8.14 Contributing
The repo accepts PRs at https://github.com/openai/whisper. Practical guidance:
- Run
pre-commit run --all-filesbefore pushing — black + isort + flake8 will be enforced. - Add a test under
tests/for any user-visible behaviour change. - Avoid breaking
DecodingOptions— it's the public Python API. - For new CLI flags, the pattern is: add an
argparse.add_argument()incli(), plumb the arg throughtranscribe(), and update thetranscribe()docstring.
9. Integration Points
9.1 Public Python API
The package's top-level imports in whisper/__init__.py:
from .audio import load_audio, log_mel_spectrogram, pad_or_trim
from .decoding import DecodingOptions, DecodingResult, decode, detect_language
from .model import ModelDimensions, Whisper
from .transcribe import transcribe
from .version import __version__
available_models() -> List[str]
load_model(name, device=None, download_root=None, in_memory=False) -> Whisper
Anything else (DecodingTask, BeamSearchDecoder, find_alignment,
get_writer, Tokenizer, LANGUAGES, TO_LANGUAGE_CODE, etc.) is
accessible by full module path but not part of the surface guaranteed
across releases.
9.2 Bundled Whisper instance methods
Methods attached to a loaded model (assigned at the bottom of
whisper/model.py):
| Method | Source | Description |
|---|---|---|
model.transcribe(audio, **opts) |
transcribe.py:38 |
Long-form transcription pipeline. |
model.decode(mel, options, **kwargs) |
decoding.py:793 |
Single 30-second decode. |
model.detect_language(mel, tokenizer=None) |
decoding.py:19 |
Returns (language_tokens, language_probs). |
model.embed_audio(mel) |
model.py:287 |
Run encoder only. |
model.logits(tokens, audio_features) |
model.py:290 |
Decoder forward pass given pre-encoded features. |
model.set_alignment_heads(dump) |
model.py:278 |
Override the heads used for word timing. |
model.install_kv_cache_hooks(cache=None) |
model.py:310 |
Returns (cache_dict, hooks). |
model.is_multilingual (property) |
model.py:303 |
True if vocab includes language tokens. |
model.num_languages (property) |
model.py:307 |
Number of supported languages for this checkpoint. |
model.device (property) |
model.py:299 |
The device the parameters live on. |
9.3 Audio input formats
load_audio() accepts any path FFmpeg can decode. There is no constraint
on container or codec. The function:
- Spawns
ffmpeg -nostdin -threads 0 -i <file> -f s16le -ac 1 -acodec pcm_s16le -ar 16000 -as a subprocess. - Reads
int16samples from stdout. - Normalises to
float32in[-1, 1](divide by 32768).
So Whisper supports MP3, WAV, FLAC, OGG, OPUS, M4A, AAC, WMA, MP4 video (audio extracted), MOV, MKV, WebM, AIFF, AU, CAF, AMR, GSM, and anything else FFmpeg builds with.
log_mel_spectrogram() also accepts a NumPy array or a PyTorch tensor —
useful when audio is already in memory (e.g. from a microphone capture
library).
9.4 Output integrations
The five built-in formats cover the common cases:
| Format | Typical consumer |
|---|---|
txt |
Simple text indexing, search. |
srt |
Video player subtitles (HandBrake, MPV, Plex). |
vtt |
HTML5 <track kind="subtitles">, web video. |
tsv |
Spreadsheet, pandas, custom analytics — start_ms\tend_ms\ttext. |
json |
Programmatic consumption with full result shape. |
For non-standard formats, write a small ResultWriter subclass:
from whisper.utils import ResultWriter
class WriteMyFormat(ResultWriter):
extension: str = "myfmt"
def write_result(self, result, file, options=None, **kwargs):
for seg in result["segments"]:
file.write(f"{seg['start']:.2f},{seg['end']:.2f}: {seg['text']}\n")
Or just consume result as a dict and write whatever you want.
9.5 No HTTP / RPC interface
Whisper does not ship a REST API, gRPC server, or any networked interface. If you need one, build it on top of the Python package — for example with FastAPI + a model held resident in process. There is no plugin system to extend Whisper itself; integration is done at the caller layer.