- actions/checkout@v4->v5, actions/setup-python@v5->v6 (Node 20 deprecated on GH runners, was forcing Node 24 + emailing deprecation warnings). - add jellyfish to the test deps (V2's phonetic-match tests need it; the workflow only installed pysubs2+pytest, so those tests were failing in CI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .github/workflows | ||
| data | ||
| glossaries | ||
| shell | ||
| specs | ||
| tests | ||
| tools | ||
| .gitignore | ||
| all_seasons.sh | ||
| anime_library.sh | ||
| anime_order.txt | ||
| common.py | ||
| common_words.txt | ||
| conftest.py | ||
| container_run.sh | ||
| Dockerfile | ||
| Dockerfile.builder | ||
| dub_signs_merge.py | ||
| gen_loop.sh | ||
| generate.py | ||
| glossary.py | ||
| glossary_verify.py | ||
| hallucination.py | ||
| LICENSE | ||
| merge_pass.sh | ||
| merge_watcher.sh | ||
| mine_glossary.py | ||
| mux.py | ||
| ordering.py | ||
| plex_refresh.py | ||
| post_season.sh | ||
| post_show.sh | ||
| pyproject.toml | ||
| README.md | ||
| recreate_srt.py | ||
| reflow.py | ||
| repair.py | ||
| REVIEW.md | ||
| run-dub-merge.sh | ||
DubTitlerr
Automatic English-dub "dubtitles" for your whole anime library — and the "Signs & Songs" track on screen at the same time.
DubTitlerr is a self-hosted, *-arr-style service that watches your anime library and, for every show with an English dub, transcribes the dub into accurate captions ("dubtitles"), repairs them with a local LLM, and merges the on-screen signs & song lyrics into the same subtitle track — so one track shows everything. It runs as one container, grows a per-show name dictionary automatically, and refreshes Plex per-episode.
The original
dub-signs-mergewas just the signs+dub merge step (documented below); the project has grown into the full pipeline (transcribe → repair → merge → mux) with additive per-show dictionaries. See the Wiki for setup & usage.
The signs+dub merge, in detail (click to expand)
If you watch anime with the English dub but still want the on-screen signs and song lyrics translated, most players (Plex included) only display one subtitle track at a time — so you get dub captions or signs, not both. This merges the two into a single subtitle file.
What's actually going on (click to expand)
Anime releases (e.g. One Pace) usually ship with:
- a "Signs and Songs" subtitle track — only on-screen text and song lyrics, carefully positioned near where they appear (it's an
.assfile with placement tags), no dialogue; and - (in this setup) a "dubtitles" sidecar — a
.srtof the English dub dialogue, generated by Whisper/subgen.
They're complementary — one is bottom-of-screen dialogue, the other is positioned signs — and they share the same timeline, so they just combine. This tool extracts the signs track from the video, appends the dub dialogue under a clean bottom style, and writes one .ass that renders both at once.
What it does
DubTitlerr runs as one restart-safe container that watches your anime library and, for every show with an English dub, runs a full pipeline per episode:
- Transcribe — pick the English-dub audio and run Whisper (large-v3), then reflow the words into clean, well-timed cards (sentence-split, ≤2 lines/≤42 chars, ~17 cps, never shown before they're spoken).
- Name correction — fix proper nouns against a per-show glossary (curated
hard_fixes+ a guarded fuzzy that won't touch real English words). The glossary is auto-built by mining the embedded subs and wiki-verified (canonical, dub-preferred spellings — see below). - LLM repair — a local model (qwen3:8b) fixes mid-/low-confidence and name-suspect lines, anchored on the embedded fansub dialogue when present.
- Hallucination gate — drop music/silence/blocklist lines and within-card loops, collapse runaway repeat runs, flag the merely-uncertain.
- Signs & songs merge — lift the on-screen signs/song-lyric events 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.
Idempotent (a .dubtitles.done stamp + embedded-track check make re-runs safe), incremental, and
per-episode (Plex refreshes as each finishes). The glossary wiki-verifier is reusable on its
own — it makes any mined or community-submitted glossary as accurate as a hand-curated one.
Quick start
The full pipeline (transcribe → repair → merge → mux) builds from Dockerfile.builder
and runs as one long-lived, restart-safe container (root, so it can rewrite/chown
sidecars) — no cron needed, it loops on its own:
# build
docker build -f Dockerfile.builder -t dubtitle-builder:latest .
# run continuously against your media (env vars configure roots/models/Plex — see the Wiki)
docker run --rm -u 0 --gpus all -v "/path/to/your/media:/media" -v "/path/to/config:/config" \
-e ANIME_ROOT="/media/Anime Library" dubtitle-builder:latest
Dockerfile (signs+dub merge only, no transcribe/repair) is deprecated — see the comment
at its top. Its old cron-based quick start (docker build -t dub-signs-merge . +
run-dub-merge.sh) still works for that narrower use case but isn't the recommended path.
Make it yours — settings
Everything is an env var, so nothing host-specific is baked in:
| Var | What | Default |
|---|---|---|
MERGE_ROOTS |
colon-separated folders to scan (inside the container) | /data/Media/Anime Library:/data/Media/Anime Movie Library |
DUB_SUFFIX |
the sidecar suffix to look for | .eng.dubtitles.srt |
MEDIA_UID / MEDIA_GID |
ownership for the written .ass |
1000 / 100 |
MEDIA_ROOT (wrapper) |
host path mounted to /data |
— |
PLEX_URL / PLEX_TOKEN / PLEX_SECTION (wrapper) |
optional Plex rescan after a merge | unset = skip |
- Point
MERGE_ROOTSat your own library folders. - If your dubtitle sidecars use a different name, change
DUB_SUFFIX. - The signs track is matched by its title containing "sign" or "song" — adjust
SIGNS_KEYWORDSin the script if your releases label it differently.
Notes & gotchas
- Plex reads
.asssidecars and renders the styling/positioning on direct-play clients; transcoding clients burn them in. - If you use subgen with
SKIP_IF_EXTERNAL_SUBTITLES_EXIST, it already treats.assas an external subtitle, so replacing the.srtwith the merged.asswon't trigger a re-transcribe loop. - The merged file keeps the same
…eng.dubtitles.*name, so it still shows up as your "Dubtitles" track — just now with signs included.
Requirements
ffmpeg/ffprobe and the pysubs2 Python package — both baked into the provided Dockerfile.
Roadmap
- Web UI — a small dashboard to watch the rollout (per-show / per-episode progress, GPU status, live logs), queue or reorder shows, kick off a re-scan, and edit per-show glossaries — instead of tailing logs over SSH.
- Per-show glossary editor — manage the name/spelling glossaries from the UI.
- Community glossary repo — a shared, TitleCardMaker-blueprints-style repository of per-show glossaries that instances can fetch on startup and submit their mined dictionaries back to (keyed by show + tvdb-id, with dedup/merge).
License
GPL-3.0 — see LICENSE.
Built with the help of Claude (Anthropic — Claude Opus 4.8).