| name | media-whisper |
| description | Speech-to-text transcription and subtitle generation with whisper.cpp and faster-whisper: extract spoken text from audio/video, generate SRT/VTT subtitles, translate speech to English, word-level timestamps, multilingual support, speaker diarization optional. Use when the user asks to transcribe a video, auto-generate subtitles from speech, create captions, translate a foreign-language clip to English, extract dialogue as text, build an ASR pipeline, or process podcast audio into a transcript.
|
| argument-hint | [input] |
Media Whisper
Context: $ARGUMENTS
Quick start
- Transcribe video → SRT: extract 16 kHz mono WAV (Step 2) → run ASR (Step 3) → SRT is written next to the input.
- Translate foreign speech → English SRT: Step 2, then
whisper-cpp -tr ... or faster-whisper task="translate" (Step 3).
- Burn-in captions: generate SRT here, then hand off to the
ffmpeg-subtitles skill.
- Soft-mux SRT into MP4:
scripts/whisper.py srt-mux --video in.mp4 --srt out.srt --output tagged.mp4.
When to use
- Auto-generate captions (SRT/VTT) from audio or video.
- Translate non-English audio to an English transcript.
- Produce a searchable text transcript or word-level JSON for a podcast/interview.
- Build an ASR stage in a larger pipeline (precedes diarization, search indexing, summarization).
- Do NOT use for speaker identification — whisper doesn't do diarization reliably. Hand off to
pyannote-audio (see references/whisper.md).
Step 1 — Pick a backend
Two mature local backends. Pick one:
| Backend | Install | Strength | Use when |
|---|
| whisper.cpp | brew install whisper-cpp (macOS) or build from source on Linux | Zero Python deps, Metal on Apple Silicon, SRT/VTT built in | Shell-only pipelines, Apple Silicon, want a static binary |
| faster-whisper | pip install faster-whisper (or uv pip install faster-whisper) | ~4× faster on CPU (CTranslate2), word timestamps, VAD | Python pipeline, need word-level timing, GPU CUDA, long files with VAD |
| OpenAI API | n/a (HTTP) | Best-quality large-v3, no local compute | Network available, ok paying per minute, confidentiality is not an issue |
Check what's present:
uv run .claude/skills/media-whisper/scripts/whisper.py check
Download a GGML model for whisper.cpp (one-time):
bash /opt/homebrew/share/whisper.cpp/models/download-ggml-model.sh base.en
faster-whisper downloads models automatically on first use into ~/.cache/huggingface/.
Step 2 — Extract 16 kHz mono WAV
whisper.cpp requires 16 kHz mono PCM-s16le WAV. faster-whisper accepts almost any format but benefits from the same normalization. Always do this first:
ffmpeg -y -i in.mp4 -vn -ar 16000 -ac 1 -c:a pcm_s16le audio.wav
scripts/whisper.py transcribe runs this step automatically if the input is not already a WAV.
Pre-processing for better accuracy (optional, for podcast / phone audio): loudnorm + denoise before ASR. See ffmpeg-audio-filter skill — loudnorm two-pass to hit -16 LUFS, then afftdn=nr=12 for broadband noise. Whisper hallucinates less on normalized input.
Step 3 — Run ASR
Option A: whisper.cpp
whisper-cpp \
-m /opt/homebrew/share/whisper.cpp/models/ggml-base.en.bin \
-f audio.wav \
-l en \
-osrt -ovtt -otxt \
-of out
whisper-cpp -m ggml-large-v3.bin -f audio.wav -l auto -osrt -of out
whisper-cpp -m ggml-large-v3.bin -f audio.wav -tr -osrt -of out
whisper-cpp -m ggml-large-v3.bin -f audio.wav -owts -of out
whisper-cpp -m ggml-small.en-tdrz.bin -f audio.wav -tdrz -osrt -of out
Useful flags: -t N threads, -p N processors, -ml N max segment length (chars), -sow split on word, -pp print progress, -nt no timestamps (plain text mode), -et entropy threshold for hallucination guard.
Option B: faster-whisper (Python)
from faster_whisper import WhisperModel
model = WhisperModel("large-v3", device="cpu", compute_type="int8")
segments, info = model.transcribe(
"audio.wav",
language="en",
task="transcribe",
word_timestamps=True,
vad_filter=True,
vad_parameters={"min_silence_duration_ms": 500},
)
print(f"Detected language: {info.language} (prob {info.language_probability:.2f})")
for seg in segments:
print(f"[{seg.start:.2f} -> {seg.end:.2f}] {seg.text}")
segments is a generator — iterate once, write SRT as you go. scripts/whisper.py transcribe --backend faster handles this.
Option C: Driver script (recommended)
uv run .claude/skills/media-whisper/scripts/whisper.py transcribe \
--input podcast.mp3 --output-srt podcast.srt --model base.en --lang en
uv run .../whisper.py transcribe --backend cpp --input in.mp4 --output-srt out.srt
uv run .../whisper.py transcribe --backend faster --input in.mp4 --output-srt out.srt \
--word-timestamps
uv run .../whisper.py translate --input spanish.mp4 --output english.srt --model large-v3
Step 4 — Post-process and mux
Soft-mux SRT into MP4 (no re-encode):
ffmpeg -i in.mp4 -i out.srt -c copy -c:s mov_text out.mp4
uv run .../whisper.py srt-mux --video in.mp4 --srt out.srt --output tagged.mp4
Burn-in (hard-coded) subtitles: hand off to ffmpeg-subtitles skill. Or quick form:
ffmpeg -i in.mp4 -vf "subtitles=out.srt" -c:a copy burned.mp4
uv run .../whisper.py srt-burn --video in.mp4 --srt out.srt --output burned.mp4
Trim end-of-file hallucinations: whisper commonly emits a phantom "Thank you for watching" / "Subtitles by..." at EOF. Check the last cue; drop it if it appears after the audio ended.
Gotchas
- whisper.cpp requires 16 kHz mono PCM-s16le WAV. Other formats sometimes work via internal conversion but are unreliable — always normalize with ffmpeg first.
- Model sizes (FP16 GGML): tiny 39M · base 74M · small 244M · medium 769M · large-v3 1.5GB. RAM need ~= model size + 1GB working set.
- English-only models (
.en suffix) are faster AND more accurate for English than the multilingual equivalents. Never use multilingual if you know the content is English.
- Multilingual models can translate to English (
-tr / task="translate") but cannot translate between two non-English languages.
- Auto language detection uses only the first 30 s of audio. Unreliable for code-switching / mixed-language content. Pass
-l <iso> explicitly when you know the language.
- Use quantized models (
large-v3-q5_0 ~1.1GB vs 1.5GB FP16) when RAM-constrained. Accuracy loss is minimal; inference ~same speed or faster on CPU.
- faster-whisper is ~4× faster than whisper.cpp on CPU (CTranslate2 int8). Use it for long files. whisper.cpp wins on Apple Silicon with Metal for short files due to faster warm-up.
- compute_type tradeoff:
int8 fast CPU, int8_float16 good on GPU, float16 best quality but GPU-only, float32 reference.
- Smaller models drop punctuation on complex sentences. Use
large-v3 or a post-processing punctuation restorer (e.g., deepmultilingualpunctuation).
- End-of-file hallucinations are common — whisper trained on YouTube often appends "Thanks for watching", "Subtitles by ..." after real speech ends. Check final cue and drop if it extends past audio duration.
- Hallucinations on silence are the #1 quality issue. Mitigate: enable VAD (
vad_filter=True in faster-whisper), pre-trim silence with ffmpeg-audio-filter silenceremove, or bump -et entropy threshold in whisper.cpp.
chunk_length default is 30 s — audio longer than that is processed in overlapping windows. Usually fine; long silences between chunks can cause boundary glitches.
- Diarization (who spoke when) is NOT whisper.
-tdrz (tinydiarize) is a token hint, not real diarization. For speaker labels, run separately and merge on timestamps. See .
Examples
Example 1: English podcast → SRT
uv run .../whisper.py transcribe \
--input episode42.mp3 \
--output-srt episode42.srt \
--model small.en \
--lang en \
--verbose
Uses whisper.cpp if available (via small.en GGML); falls back to faster-whisper. small.en is the sweet-spot for English podcasts on CPU.
Example 2: Spanish film clip → English captions burned into video
uv run .../whisper.py translate --input scene.mov --output scene.srt --model large-v3
uv run .../whisper.py srt-burn --video scene.mov --srt scene.srt --output scene.dubbed.mp4
Example 3: Interview → word-level JSON for search index
uv run .../whisper.py transcribe \
--input interview.wav \
--output-srt interview.srt \
--backend faster \
--model large-v3 \
--word-timestamps
Example 4: Long lecture with lots of silence
uv run .../whisper.py transcribe \
--input lecture.m4a --output-srt lecture.srt \
--backend faster --model medium.en
Troubleshooting
Error: whisper-cpp: command not found
Cause: whisper.cpp binary not installed.
Solution: brew install whisper-cpp (macOS) or build from https://github.com/ggerganov/whisper.cpp. Or force faster-whisper: --backend faster.
Error: ModuleNotFoundError: No module named 'faster_whisper'
Cause: package not installed in active Python.
Solution: pip install faster-whisper (or uv pip install faster-whisper). Alternatively use --backend cpp.
Error: failed to load model: ggml-base.en.bin
Cause: model file missing.
Solution: bash /opt/homebrew/share/whisper.cpp/models/download-ggml-model.sh base.en. Pass the full path via -m if it's stored elsewhere.
Transcript contains "Thanks for watching, don't forget to subscribe"
Cause: EOF hallucination — model trained on YouTube.
Solution: drop the last cue if its start time is past the audio duration. Or pre-trim the trailing silence with ffmpeg -af silenceremove=stop_periods=1:stop_duration=2:stop_threshold=-40dB.
Subtitles drift vs. audio on long file
Cause: boundary glitches between 30 s chunks, or variable frame rate source.
Solution: remux to CFR first (ffmpeg -i in.mp4 -vsync cfr out.mp4), or run the media-subtitle-sync skill (alass / ffsubsync) on the generated SRT to re-align.
Punctuation missing from transcript
Cause: small / tiny models produce unpunctuated output.
Solution: use medium or large-v3, or post-process with deepmultilingualpunctuation / punctuators. faster-whisper's large-v3 is generally best.
faster-whisper is slow on my Mac
Cause: faster-whisper uses CTranslate2 CPU path on Apple Silicon (no Metal).
Solution: switch to whisper.cpp (--backend cpp) which has Metal acceleration. Or run faster-whisper on a CUDA box.
Auto-detected language is wrong
Cause: first 30 s of file is music / silence / wrong language.
Solution: force --lang <iso>. See language codes in references/whisper.md.