Merge anime dubtitles + Signs & Songs into one subtitle track
Find a file
xenarathon ccdcc26e1e
Some checks failed
tests / test (push) Failing after 21s
CI / test (push) Failing after 33s
ci: bump actions to Node-24 versions + install jellyfish
- 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>
2026-07-25 09:49:13 -04:00
.claude docs(a1): add specsmith scaffold + A1 reflow-timing spec/plan/tasks 2026-06-30 13:19:10 -04:00
.github/workflows ci: bump actions to Node-24 versions + install jellyfish 2026-07-25 09:49:13 -04:00
data refactor(data): extract COMMON/BLOCKLIST to data/ files with inline fallback (C8) 2026-07-24 17:32:32 -04:00
glossaries fix: drop hallucination_silence_threshold (it dropped real dialogue); wiki override + verify sanity-gate 2026-06-30 19:02:38 -04:00
shell fix(shell): extras_grep_pattern returns 1 on empty pattern instead of printing "()" 2026-07-24 16:55:50 -04:00
specs spec(timing-compare): v3 folds in panel consult (RANSAC drift + VAD probe) 2026-07-24 20:09:25 -04:00
tests fix(timing-compare): aggregate false_in_gap_rate null on all-vad_error + doc/strip cleanups 2026-07-24 21:38:08 -04:00
tools fix(timing-compare): aggregate false_in_gap_rate null on all-vad_error + doc/strip cleanups 2026-07-24 21:38:08 -04:00
.gitignore chore: gitignore the 8 pipeline artifact patterns 2026-07-24 16:53:55 -04:00
all_seasons.sh docs(shell): deprecate per-show docker-run orchestrators in favor of container_run.sh 2026-07-24 16:44:10 -04:00
anime_library.sh feat(shell): add --dry-run flag to anime_library.sh (C7) 2026-07-24 17:30:09 -04:00
anime_order.txt chore: priority anime_order (Tautulli watch history: One Pace, JoJo, Reborn, ...) 2026-06-30 17:48:45 -04:00
common.py fix(timing-compare): aggregate false_in_gap_rate null on all-vad_error + doc/strip cleanups 2026-07-24 21:38:08 -04:00
common_words.txt feat(c1): name_suspect() to route uncertain-name lines to the LLM 2026-06-30 15:02:14 -04:00
conftest.py generate: watch-order season priority for the --root walk 2026-06-30 21:02:01 -04:00
container_run.sh Generalize dubtitle pipeline to the whole anime library + containerize 2026-06-21 22:43:47 -04:00
Dockerfile docs: deprecate signs-only Dockerfile, point README quick start at Dockerfile.builder 2026-07-24 16:44:46 -04:00
Dockerfile.builder build: make webrtcvad install non-fatal in builder image 2026-07-25 09:45:26 -04:00
dub_signs_merge.py feat(signs-merge): warn on PlayResX/Y mismatch between source tracks (D5) 2026-07-24 18:37:31 -04:00
gen_loop.sh fix(shell): add set -e to gen_loop.sh, guard intentional fallthroughs 2026-07-24 16:46:30 -04:00
generate.py fix(generate): gate CUDA error handling on exception TYPE, not substring (C15) 2026-07-24 17:38:27 -04:00
glossary.py feat(glossary): A4 - tier-4 phonetic match via jellyfish.metaphone 2026-07-24 16:32:53 -04:00
glossary_verify.py perf(glossary_verify): parallelize adjudicate() with ThreadPoolExecutor (C2) 2026-07-24 17:26:04 -04:00
hallucination.py refactor(data): extract COMMON/BLOCKLIST to data/ files with inline fallback (C8) 2026-07-24 17:32:32 -04:00
LICENSE relicense MIT -> GPL-3.0 (copyleft, matching all F.A.S.C. repos) 2026-06-20 09:13:43 -04:00
merge_pass.sh fix(shell): POSIX '.' instead of bash 'source' for shell/lib.sh 2026-07-24 18:50:45 -04:00
merge_watcher.sh docs(shell): deprecate per-show docker-run orchestrators in favor of container_run.sh 2026-07-24 16:44:10 -04:00
mine_glossary.py refactor(data): extract COMMON/BLOCKLIST to data/ files with inline fallback (C8) 2026-07-24 17:32:32 -04:00
mux.py feat(mux): audit font-attachment count in verify() (D2) 2026-07-24 18:40:25 -04:00
ordering.py fix(ordering): stop misreporting watch-order as disabled when SEASON_START is active 2026-07-24 18:26:07 -04:00
plex_refresh.py fix(plex_refresh): use os.environ.get() with clear errors, fix ruff E401/I001 2026-07-24 16:54:35 -04:00
post_season.sh Generalize dubtitle pipeline to the whole anime library + containerize 2026-06-21 22:43:47 -04:00
post_show.sh fix(shell): POSIX '.' instead of bash 'source' for shell/lib.sh 2026-07-24 18:50:45 -04:00
pyproject.toml feat(timing-compare/T8): add webrtcvad dependency (analysis extra + builder image) 2026-07-24 21:05:53 -04:00
README.md docs: deprecate signs-only Dockerfile, point README quick start at Dockerfile.builder 2026-07-24 16:44:46 -04:00
recreate_srt.py refactor: update mine_glossary.py and recreate_srt.py imports (T8) 2026-07-24 15:03:42 -04:00
reflow.py refactor(reflow): name wrap_balance()'s fallback tuple fields (C13) 2026-07-24 17:37:08 -04:00
repair.py refactor(T1): hoist dialogue_intervals from repair.py into common.py 2026-07-24 20:23:52 -04:00
REVIEW.md docs(review): two-pass code review + v1-polish/v2-models-ops specs 2026-07-24 14:49:54 -04:00
run-dub-merge.sh dub-signs-merge: merge dubtitles + Signs & Songs into one .ass track 2026-06-19 23:00:51 -04:00

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-merge was 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 .ass file with placement tags), no dialogue; and
  • (in this setup) a "dubtitles" sidecar — a .srt of 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:

  1. 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).
  2. 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).
  3. LLM repair — a local model (qwen3:8b) fixes mid-/low-confidence and name-suspect lines, anchored on the embedded fansub dialogue when present.
  4. Hallucination gate — drop music/silence/blocklist lines and within-card loops, collapse runaway repeat runs, flag the merely-uncertain.
  5. Signs & songs merge — lift the on-screen signs/song-lyric events into the same subtitle.
  6. 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_ROOTS at 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_KEYWORDS in the script if your releases label it differently.
Notes & gotchas
  • Plex reads .ass sidecars 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 .ass as an external subtitle, so replacing the .srt with the merged .ass won'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).