3 Home
xenarathon edited this page 2026-07-01 08:37:06 -04:00

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:

  1. 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).
  2. 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).
  3. LLM repair mid-/low-confidence and name-suspect lines (local qwen3:8b), anchored on the embedded fansub dialogue when present.
  4. Hallucination gate — drop music/silence/blocklist + within-card loops, collapse runaway repeat runs, flag the merely-uncertain.
  5. Merge the on-screen signs & song lyrics 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.

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 01Season 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.fail marker (skipped on resume); delete it to retry.
  • Re-doing old output: delete an episode's sidecars (and its .dubtitles.done stamp) to force a fresh pass; to strip an already-muxed track, mkvmerge it out first.