DubTitlerr — Setup & Usage
The end-to-end anime "dubtitles" pipeline. It runs as one managed container and, for every show with an English dub, per episode:
- Transcribe the English-dub audio (Whisper
large-v3) and reflow it into clean, well-timed cards (sentence-split, ≤2 lines / ≤42 chars, ~17 cps, start pinned to the spoken onset). - Correct names against a per-show glossary (curated
hard_fixes+ a guarded fuzzy that won't rewrite real English words). Glossaries are auto-mined and wiki-verified (canonical, dub-preferred spellings). - LLM repair mid-/low-confidence and name-suspect lines (local qwen3:8b), anchored on the embedded fansub dialogue when present.
- Hallucination gate — drop music/silence/blocklist + within-card loops, collapse runaway repeat runs, flag the merely-uncertain.
- Merge the on-screen signs & song lyrics into the same subtitle.
- Mux + fonts — embed the result with the MKV's fonts as a default "Dubtitles" track so signs render in their real typeface (mp4 episodes are remuxed to mkv); English audio set default.
Per-episode (Plex refreshes as each finishes), incremental, and idempotent (a .dubtitles.done
stamp + embedded-track check make re-runs safe — no re-transcribe loop).
Build the image
Your stack manager probably can't build, so build on the Docker host:
docker build -f Dockerfile.builder -t fasc/dubtitle-builder:latest .
The image bakes Whisper large-v3, pysubs2, mkvtoolnix (mux), and wamerican (the
English-word gate). It's self-contained — only /media and /config are mounted.
Deploy (compose)
services:
dubtitle-builder:
image: fasc/dubtitle-builder:latest
restart: unless-stopped
environment:
WHISPER_MODEL: large-v3
COMPUTE_TYPE: int8 # fits a 6GB Pascal card; float16 for max quality
MODEL_DIR: /models # large-v3 is baked into the image
REQUIRE_ENG: "1" # skip sub-only releases (never transcribe JP as EN)
ANIME_ROOT: /media/Anime Library
ANIME_ORDER: /config/anime_order.txt
GLOSSARY_DIR: /config/glossaries
MERGE_ROOTS: /media/Anime Library
MERGE_INTERVAL: "600" # merge+mux sweep cadence (per-episode availability)
RESCAN_INTERVAL: "21600" # re-sweep for new anime every 6h
OLLAMA_URL: http://your-ollama:11434/api/generate
REPAIR_MODEL: qwen3:8b # LLM repair
VERIFY_MODEL: qwen3:8b # glossary wiki-verify adjudication
MIN_FREE_GB: "5" # skip a mux if the pool is this low (never ENOSPC)
PLEX_URL: http://your-plex:32400
PLEX_TOKEN: your-token
PLEX_SECTION: "7"
volumes:
- /path/to/Media:/media
- /path/to/config:/config # anime_order.txt + season_priority.txt + glossaries/ + wiki_cache/
deploy:
resources:
reservations:
devices: [{ driver: nvidia, count: all, capabilities: [gpu] }]
Runs as root (so it can rewrite media + set ownership on the muxed files). It needs network access to the Ollama host and to the Fandom wikis (for glossary verification).
Configuring it
Everything lives in the bind-mounted /config, editable without a rebuild.
anime_order.txt — what gets done, in what order
One show-folder name per line, top to bottom (# comments / blanks ignored). Edit to reorder or
queue; the next sweep picks it up. REQUIRE_ENG=1 skips any episode with no English audio, so
mixed/sub-only shows are handled per-episode.
season_priority.txt — watch-order priority within a show (optional)
When you're mid-show, a run otherwise walks that show's seasons alphabetically (Season 01 →
Season 36), spending hours re-doing seasons you've already watched before it reaches the arc
you're about to watch. Add a line per show to jump ahead to where you actually are:
# Show folder name : start season — do this season and everything after it FIRST
# (your forward watch order), then wrap back to the earlier seasons.
One Pace:20
The --root walk then processes seasons ≥ the start first (ascending), then the earlier ones —
the same episodes, just resequenced so the next arc lands soonest. Season/episode are read from
each file's SxxExx tag. When you move further along, bump the number and restart the container;
it re-prioritizes from wherever you now are. SEASON_START (env) is a global fallback, and with no
config at all the order is a plain sort — unchanged. The show name must match its anime_order.txt
line exactly.
glossaries/<Show folder>.json — accurate names (auto-managed)
Mining auto-builds/grows a per-show glossary from the embedded subs; the wiki-verifier then corrects spellings against the show's Fandom wiki (dub-preferred) and flags the uncertain. Shape:
{
"show": "One Piece",
"wiki": "https://onepiece.fandom.com/api.php", // optional override if auto-resolve misses
"initial_prompt": "This is One Piece. Spell names correctly: Luffy, Zoro, …",
"names": ["Luffy", "Zoro", "Spandam"],
"phrases": ["Enies Lobby", "Water 7"],
"hard_fixes": { "ruffy": "Luffy", "spondum": "Spandam" },
"verified": ["Luffy", "Zoro"], // verifier bookkeeping (incremental)
"flagged": { "SomeName": "no-match" } // needs human review
}
You can also run the verifier by hand: python3 glossary_verify.py "/config/glossaries/<Show>.json" [--wiki URL] [--force].
Env knobs
| Var | Meaning | Default |
|---|---|---|
REQUIRE_ENG |
skip releases with no English audio | 1 |
SEASON_START |
global watch-order start season (see season_priority.txt for per-show) |
— |
WHISPER_MODEL / COMPUTE_TYPE |
model + precision | large-v3 / int8 |
MERGE_INTERVAL / RESCAN_INTERVAL |
merge+mux cadence / idle before re-sweep | 600 / 21600 |
OLLAMA_URL |
local LLM endpoint (repair + verify) | — |
REPAIR_MODEL / VERIFY_MODEL |
LLM for repair / glossary verify | qwen3:8b |
MIN_FREE_GB |
skip a mux if free space is below this (no ENOSPC) | 5 |
KEEP_LANGS |
audio/sub languages kept on mux (+ original lang) | eng,en,dut,nld,nl,und |
PLEX_URL / PLEX_TOKEN / PLEX_SECTION |
Plex rescan after merges | — |
Gotchas
- Shared GPU: if another process grabs VRAM, Whisper can CUDA-OOM. The pipeline exits cleanly and the loop restarts with a fresh context (self-heals once VRAM frees) — best to give it a card it can rely on. Don't run two transcriptions on one small card at once.
- mp4 → mkv: mp4 episodes are remuxed to mkv (uniform embedded "Dubtitles" track) — Sonarr/Radarr will see a rename. The original download hardlink is never deleted (your seed/reaper owns it).
- Disk: muxing writes a full-size temp; on a near-full pool an episode is skipped + logged (retries when space frees) rather than erroring.
- Extras dirs / NCED / NCOP / -scene clips are never transcribed.
- Poison file: a hard crash leaves a
.dubtitles.failmarker (skipped on resume); delete it to retry. - Re-doing old output: delete an episode's sidecars (and its
.dubtitles.donestamp) to force a fresh pass; to strip an already-muxed track,mkvmergeit out first.