mpv Auto Subtitles
Date: 2026-09-12
Host: cachyos-B450 (local)
Config lives in: ~/.config/mpv → symlink to dotfiles/config/mpv/
Verified with: The Worst Witch S01E01 (24-min episode, 195 MB)
Summary
mpv gets subtitles in three layers, in this order. Each layer only runs when the
previous one found nothing:
| # |
Layer |
What runs |
Trigger |
| 1 |
Local files |
mpv itself |
on load |
| 2 |
Download |
autosub.lua → subliminal |
on load, if no sub in the wanted language |
| 3 |
Local AI |
whispersubs.lua → whisper-cli (whisper.cpp) |
manual: Ctrl+. |
slang=en decides what counts as "already have it".
Layer 1 — local subtitle files
mpv.conf:
slang=en # preferred subtitle language
sub-auto=fuzzy # don't require an exact filename match
sub-file-paths=sub:subtitles:subs # also look in these subfolders
Covers files like Video.mkv + Video.en.srt, including the .en.srt files that
subliminal writes next to the video.
Layer 2 — automatic download (subliminal)
- Script:
~/.config/mpv/scripts/autosub.lua (davidde/mpv-autosub)
- Subliminal: 2.7.1 installed via pipx →
~/.local/bin/subliminal (not an Arch
repo package; pipx keeps it out of the system Python)
- Config:
~/.config/subliminal/subliminal.toml → symlink to
dotfiles/config/subliminal/subliminal.toml
Behaviour:
- languages:
en first, then es (first language with a hit wins; the script does
not download both)
- hotkeys:
b = English, n = Spanish
- skipped by design: files under 15 minutes, audio files, network streams
- skipped when a subtitle in that language is already present
-d <video dir> + -e utf-8: the .srt lands next to the video, UTF-8
sub-auto=fuzzy + rescan_external_files make it load immediately, no restart
Provider reality (measured 2026-09-12, subliminal 2.7.x)
| Provider |
Result |
opensubtitles (opensubtitles.org) |
works anonymously, movie + TV, 1–2 s per lookup |
opensubtitlescom (opensubtitles.com) |
finds candidates but never downloaded — needs a free account (≈5 downloads/day) |
tvsubtitles |
HTTP 403 for scripted requests |
podnapisi, addic7ed, bsplayer, subtitulamos, subtis, gestdown, napiprojekt |
returned nothing |
Config therefore pins the one working provider and drops the broken refiner:
[download]
provider = ["opensubtitles"]
ignore_refiner = ["omdb"] # subliminal's bundled OMDb key is expired → 401 on every lookup
A provider login (if ever needed) goes under [provider.<name>]:
[provider.opensubtitlescom]
username = "..."
password = "..."
Subliminal measure-check for new providers:
subliminal --debug download -p <provider> -l en -f -d /tmp/subtest <video>
--debug is a global option (before download), and -R omdb after it skips the
OMDb refiner. Search hits are not downloads — check that a .srt actually appeared.
Layer 3 — local AI subtitles (whisper.cpp)
- Engine:
whisper-cpp from the CachyOS repo → /usr/bin/whisper-cli
(CPU-only: ggml backends in /usr/lib/ggml, no Vulkan/ROCm build)
- Models:
~/.local/share/whisper.cpp/models/
ggml-small.en.bin — 466 MB, default (better accuracy)
ggml-base.en.bin — 142 MB, faster
- Script:
~/.config/mpv/scripts/whispersubs.lua (GhostNaN/whisper-subs),
WHISPER_CMD = whisper-cli -m <small.en path> --threads 8 --language en --no-prints
Ctrl+. toggles it; 15 s chunks ahead of playback; writes
<video>.srt next to the video when it reaches the end
Throughput (Ryzen 7 5700G, 8 threads, 120 s of real dialogue)
| Model |
Threads |
Time for 120 s audio |
Realtime factor |
| base.en |
8 |
7 s |
~17× |
| small.en |
8 |
16 s |
~7.5× |
| small.en |
4 |
24 s |
~5× |
Full 24-min episode end-to-end: 395 cues, 00:00:00 → 00:23:45, 300 s wall clock
(the script reloads the model per 15 s chunk, hence ~5× rather than the raw 7.5×).
Files
| Path |
Role |
~/.config/mpv/mpv.conf |
slang, sub-auto=fuzzy, sub-file-paths |
~/.config/mpv/scripts/autosub.lua |
download on load (subliminal path + languages) |
~/.config/mpv/scripts/whispersubs.lua |
Ctrl+. live Whisper subtitles |
~/.config/subliminal/subliminal.toml |
providers, ignore_refiner, logins |
~/.local/bin/subliminal |
pipx venv entry point (pipx upgrade subliminal) |
~/.local/share/whisper.cpp/models/ |
ggml Whisper models |
/tmp/mpv_whisper_tmp_<pid>.* |
whisper-subs scratch files (auto-cleaned) |
All of ~/.config/mpv and ~/.config/subliminal are symlinks into the dotfiles repo,
so the setup is version-controlled. The CachyOS post-install script installs it:
step 4b (whisper-cpp), step 12 (subliminal via pipx), step 14b (small.en model).
Verification (headless, no display needed)
# download layer
mpv --vo=null --ao=null --length=15 "<video without subs>" # expect: [autosub] … subtitles ready!
# whisper layer: load a video, press Ctrl+. over the IPC socket, then poll the scratch file
mpv --idle=yes --input-ipc-server=/tmp/mpvsock &
# {"command":["loadfile","<video>"]} then {"command":["keypress","ctrl+."]}
ls -l /tmp/mpv_whisper_tmp_sub_*.srt
# engine speed
whisper-cli -m ~/.local/share/whisper.cpp/models/ggml-small.en.bin -f chunk.wav -t 8 -np -osrt -of out
get_property input-bindings does not list script-added bindings — only the
functional keypress test proves Ctrl+. is wired up.
Pitfalls
- Only opensubtitles.org works without credentials. If it dies, open a free
opensubtitles.com account and switch the provider list (config above).
Ctrl+. / b / n must be free in input.conf (they are, as of this date).
- Whisper produces an empty cue file for silent stretches; ffmpeg then logs
Invalid data found when processing input and that chunk is skipped — harmless.
- Install lag:
pacman -S whisper-cpp 404'd on a stale mirror until
cachyos-rate-mirrors + pacman -Syy.
ignored-provider names change between subliminal releases; re-run the
measure-check above before trusting a provider list copied from the internet.
- Subliminal writes UTF-8 regardless of the source encoding (
-e utf-8) so old
sub-codepage tweaks in mpv are unnecessary.