Translarr

Translarr

Docker app from jesposito's Repository

Overview

Translarr translates subtitle tracks from the wrong language into your preferred language using AI. It plugs into Sonarr, Radarr, Emby, Jellyfin, and Plex via webhooks and drops translated .srt files next to your media.

When a release lands with Russian fansubs on a Japanese anime, or hardcoded Spanish on a Korean drama, Translarr extracts the track, translates it with a context-aware LLM, adjusts timing for the target language's reading rate, and writes a clean .srt next to the video.

Features:

  • LLM-powered translation with context awareness (Anthropic Claude, OpenAI, DeepSeek, Gemini, or local Ollama)
  • Automatic reading-rate adaptation so subtitles stay readable
  • Tag-based opt-in via Sonarr/Radarr: only translates what you tag
  • On-demand translation when you press Play in Emby
  • 6 translation presets: Quick & Cheap, Balanced, Best Quality, Local & Free, DeepSeek Budget, Gemini Flash
  • Cost guards: daily and per-job spend caps
  • Push notifications via ntfy.sh
  • Web UI: dashboard, job history, settings, library browser, glossary editor
  • All settings are live-mutable, no container restart needed

Translarr

The arr you reach for when your release has subtitles in the wrong language and nobody has authored English ones yet.

CI License: MIT Python AI BYO-key arr stack Last commit License: MIT Python 3.12+ Tests Status

Join the Facet HQ Discord

Translarr is a self-hosted Docker container that plugs into Sonarr, Radarr, Emby, Jellyfin, and Plex. When an import lands with a subtitle track in the wrong language (Russian fansubs on a Japanese anime, hardcoded Spanish on a Korean drama, etc.), Translarr extracts the track, translates it with an LLM, adjusts the timing for the target language's reading speed, and writes a clean .srt next to the video.

Tested on real media: a 1,649-event subtitle file, reading-rate split to 1,964 events, 5.7 min wall clock, $0.30 in API cost.

Quickstart

docker run -d \
  --name translarr \
  -p 9100:9000 \
  -v /mnt/user/appdata/translarr:/data \
  -v /mnt/user/media:/media \
  -e ANTHROPIC_API_KEY=sk-ant-... \
  ghcr.io/jesposito/translarr:main

Open http://your-server:9100 for the Web UI, or verify it's running:

curl http://localhost:9100/health
# {"status":"ok","version":"0.1.0","llm_provider":"anthropic","llm_model":"claude-sonnet-4-6"}

Translate a file:

curl -X POST http://localhost:9100/translate \
  -H "Content-Type: application/json" \
  --data '{
    "media_path": "Movies/Some Movie (2025)/movie.mkv",
    "source_lang": "ru",
    "target_lang": "en"
  }'
# {"status":"queued","job_id":"abc..."}

curl http://localhost:9100/jobs/abc...
# {"state":"done","output_path":"...","cost_cents":54}

The container listens on port 9000 internally. Map it to whatever host port you want.

Features

  • LLM router: Anthropic Claude (default), OpenAI, DeepSeek, Google Gemini, Ollama (local, free)
  • 6 translation presets: Quick & Cheap, Balanced, Best Quality, Local & Free, DeepSeek Budget, Gemini Flash
  • Subtitle pipeline: ffmpeg extraction from any container, pysubs2 parsing, ASS/SSA style-tag pass-through
  • Sliding-context translation: 30-line batches with 10-line prior context so names and pronouns stay consistent
  • Reading-rate adapter: auto-splits lines that would be unreadable, redistributes timing to match
  • Source language auto-detection: ffprobe reads the track's language tag so you don't have to set it manually
  • Cost guards: daily and per-job spend caps, per-job timeout kill-switch
  • Live-mutable settings: change model, provider, concurrency, cost caps via the Settings page with no restart
  • Sonarr/Radarr/Plex webhooks: opt-in per movie/series via tags
  • Emby/Jellyfin webhooks: retroactive translation on library scan or on-demand via playback events
  • Emby plugin: shows up in the player's subtitle search modal (see below)
  • Item-specific library refresh: Emby/Jellyfin refresh just the translated item (~2s) instead of doing a full scan
  • Per-series config: different source/target language defaults per series, applied automatically by path
  • Glossary: per-series term dictionaries keep character names consistent across episodes
  • File browser: browse your media library from the UI, see which files have translations, coverage stats
  • Push notifications: ntfy.sh integration for translation completion alerts
  • Output-collision policy: .translarr.srt infix means it never overwrites human/Bazarr/embedded subs
  • Web UI: dashboard, job history, live stats, budget tracking, library browser, glossary editor, settings
  • 210 tests, all green

Wiring into the arr stack

Translarr only acts on items that opt in via tag. To enable translation for a movie or series:

  1. In Sonarr/Radarr, create a tag named sonarr_translate (or radarr_translate)
  2. Apply the tag to specific items
  3. Configure the Connect webhook below
  4. Imports of tagged items auto-enqueue a translation job

Radarr -> Settings -> Connect -> Add -> Webhook:

  • URL: http://translarr:9000/webhooks/radarr (use your Docker host IP if Radarr is on a different network)
  • Triggers: On Import, On Upgrade
  • Optional headers: X-Translarr-Secret: <secret> (if WEBHOOK_SECRET is set)

Sonarr -> Settings -> Connect -> Add -> Webhook:

  • URL: http://translarr:9000/webhooks/sonarr
  • Same as above, ending in /sonarr

Emby -> Notifications -> Add -> Webhook:

  • URL: http://translarr:9000/webhooks/emby
  • Events: Library New, Library Updated

Jellyfin (requires the Webhook plugin):

  • URL: http://translarr:9000/webhooks/jellyfin
  • Triggers: ItemAdded

Plex (via Plex settings or Tautulli):

  • URL: http://translarr:9000/webhooks/plex
  • Handles library.new and media.play events

Emby Plugin (optional)

The Emby plugin adds a "Translarr: translate to EN" option inside Emby's subtitle search modal. When you open the subtitle picker on a movie or episode, you'll see the Translarr option next to OpenSubtitles and other providers. Clicking it kicks off a translation.

The plugin is not required. Webhooks work fine without it. This just adds the in-player UX.

How it works

  1. The plugin registers as an ISubtitleProvider inside Emby
  2. When you open the subtitle search modal, the plugin shows a "translate to EN" entry
  3. Clicking it calls the Translarr server's /translate API
  4. The server queues the job, runs it through the LLM pipeline, and writes the .srt
  5. Emby picks up the new subtitle file and shows it in the player

The plugin doesn't do any translation itself. It's just a thin HTTP client that talks to the Translarr container.

Install

  1. Download Translarr.dll
  2. Copy it to your Emby plugins directory (/config/plugins/ inside the Emby container, or /mnt/user/appdata/emby/plugins/ on Unraid)
  3. Restart Emby
  4. Go to Emby -> Settings -> Plugins -> Translarr
  5. Set the Translarr server URL (e.g. http://192.168.1.100:9100) and save

Requirements

  • Emby Server 4.9+
  • Translarr container must be running and reachable from Emby
  • Both Emby and Translarr must see the same media files (same mount paths)

Configuration

All config via env vars, .env, or the Settings page in the Web UI. Settings changed in the UI take effect right away, no restart needed.

Variable Default Notes
LLM_PROVIDER anthropic anthropic, openai, ollama, deepseek, gemini
LLM_MODEL claude-sonnet-4-6 Model id for your chosen provider
ANTHROPIC_API_KEY Required for anthropic
OPENAI_API_KEY Required for openai
DEEPSEEK_API_KEY Required for deepseek
GEMINI_API_KEY Required for gemini
OLLAMA_HOST http://ollama:11434 Required for ollama
MEDIA_ROOT /media Where the volume is mounted inside the container
TRANSLARR_DATA_DIR ./data SQLite DB lives here
TARGET_LANG en ISO 639-1 target language
READING_RATE_CPS 17 Max chars/sec (15-17 is standard for English)
MAX_CONCURRENT 2 Parallel translation jobs
CONTEXT_WINDOW_LINES 10 Prior translated lines fed to the LLM for context
MAX_COST_CENTS_PER_DAY 1000 Daily spend cap ($10). Goes over = HTTP 429
MAX_COST_CENTS_PER_JOB 500 Per-job cap ($5). Kills the job mid-batch if exceeded
JOB_TIMEOUT_SECONDS 1800 Per-job wall-clock timeout (30 min)
RADARR_TRANSLATE_TAG radarr_translate Tag that opts a movie in
SONARR_TRANSLATE_TAG sonarr_translate Tag that opts a series in
WEBHOOK_SECRET Optional shared secret for webhook calls
EMBY_URL Emby server URL for post-translation refresh
EMBY_API_KEY Emby API key
JELLYFIN_URL Jellyfin server URL for post-translation refresh
JELLYFIN_API_KEY Jellyfin API key
NTFY_URL ntfy.sh endpoint for push notifications. Empty = off
LOG_LEVEL INFO

Cost estimate (2026 pricing)

Rough per-film cost for a 1500-event subtitle file:

Model Per-film cost
Claude Haiku 4.5 ~$0.14
Claude Sonnet 4.6 ~$0.54
Claude Opus 4.7 ~$2.70
DeepSeek Chat ~$0.08
Gemini 2.5 Flash ~$0.04
Ollama qwen3:14b $0 (local)

See docs/ARCHITECTURE.md for the token-budget math.

Where Translarr fits

                    What you have
                          |
            +-------------+--------------+
            v                            v
  No subtitles anywhere       Subtitles in the wrong language
            |                            |
            v                            v
       Whisper (subgen)            +-----------+
       Audio -> text               |TRANSLARR  |
       (lossy, guesses names)      +-----------+
                                          |
            +-----------------------------+---------+
            v                                       v
  Embedded foreign track              External .srt provider
  (Russian, Japanese, etc.)           (OpenSubtitles, Jimaku, planned)
            |                                       |
            +------------------+--------------------+
                               v
              LLM translation with sliding context
              + reading-rate adapter
              + ASS/SSA style-tag preservation
                               |
                               v
              <basename>.en.translarr.srt next to the video
              + library refresh in Emby/Jellyfin

Bazarr fetches existing subs. Subgen transcribes audio. Translarr translates the subtitle track that's already there. Different jobs.

Roadmap

Free, self-hosted, no deadline.

Version Scope Status
v0.1 Server brain: webhooks, LLM router, sub pipeline, reading-rate, cost guards Shipped
v0.1.5 Persistent queue, async /translate, worker pool, cost tracker Shipped
v0.1.6 Source-lang auto-detect, preflight estimates, presets, Unraid template Shipped
v0.2 Emby plugin (ISubtitleProvider + settings page) Shipped
v0.3 DeepSeek/Gemini providers, Plex webhook, GHCR CI, file browser, glossary, per-series config Shipped
v0.4 Jellyfin plugin Next
v0.8a Direct subtitle provider integrations (OpenSubtitles, Jimaku, Animetosho) Planned
v0.9 Whisper-from-audio fallback Planned
v1.0 Strategy chain: auto-fallback through embedded, fetch, audio Planned

Architecture

                  +--------------------------------------+
                  |          Translarr Server            |
                  |  +-----------------------------+     |
   Radarr --webhook-->/webhooks/radarr -+           |     |
   Sonarr --webhook-->/webhooks/sonarr -+  enqueue  |     |
     Emby --webhook-->/webhooks/emby  --+     |     |     |
  Jellyfin --webhook-->/webhooks/jellyfin-+  v     |     |
     Plex --webhook-->/webhooks/plex -------+----+  |     |
   Direct --POST--->/translate ---------------+  v  |     |
                  |                       +--------+ |     |
                  |  workers <---claim----|SQLite  | |     |
                  |     |                 | queue  | |     |
                  |     v                 +--------+ |     |
                  |  Sub pipeline                    |     |
                  |  +- ffmpeg extract OR direct .ass|     |
                  |  +- pysubs2 parse                |     |
                  |  +- batch + sliding context      |     |
                  |  +- LLM router (5 providers)     |     |
                  |  +- reading-rate adapt + split   |     |
                  |  +- write .translarr.srt + refresh|    |
                  +--------------------------------------+ 
                              |
                              v
                <basename>.en.translarr.srt
                next to source media

Project structure

translarr/
+-- server/              # Python FastAPI
|   +-- main.py
|   +-- config.py
|   +-- cost_tracker.py
|   +-- db.py            # SQLite + migrations
|   +-- browse.py        # File browser API
|   +-- glossary.py      # Per-series glossary persistence
|   +-- series_config.py # Per-series language overrides
|   +-- library_refresh.py
|   +-- llm/             # router + 5 providers
|   +-- queue/           # base.py + sqlite.py + worker.py
|   +-- subs/            # extract.py + pipeline.py + reading_rate.py
|   +-- webhooks/        # radarr, sonarr, emby, jellyfin, plex
+-- ui/                  # SvelteKit static Web UI
+-- plugins/
|   +-- emby/            # C# Emby plugin (ISubtitleProvider)
|   +-- jellyfin/        # C# Jellyfin plugin (planned)
+-- templates/           # Unraid Community Applications template
+-- tests/               # pytest: 210 tests
+-- docs/
|   +-- ARCHITECTURE.md
|   +-- INSTALL.md
|   +-- COMPETITIVE-ANALYSIS.md
+-- docker-compose.yml
+-- Dockerfile
+-- pyproject.toml

License

MIT. See LICENSE.

Install Translarr on Unraid in a few clicks.

Find Translarr in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for Translarr Review the template variables and paths Click Install

Related apps

Details

Repository
ghcr.io/jesposito/translarr:main
Last Updated2026-07-18
First Seen2026-05-26

Runtime arguments

Web UI
http://[IP]:[PORT:9000]/
Network
bridge
Shell
sh
Privileged
false

Template configuration

Web UI PortPorttcp

Host port to map to container port 9000 (Web UI and API).

Target
9000
Default
9100
Value
9100
Media LibraryPathrw

Path to your media library (movies, TV shows). Translarr writes translated .srt files next to source videos.

Target
/media
Default
/mnt/user/media
Value
/mnt/user/media
Config / DataPathrw

Persistent storage for database and settings. Include this in backups.

Target
/data
Default
/mnt/user/appdata/translarr
Value
/mnt/user/appdata/translarr
LLM ProviderVariable

anthropic, openai, ollama, deepseek, or gemini

Target
LLM_PROVIDER
Default
anthropic
Value
anthropic
LLM ModelVariable

Model identifier (e.g. claude-sonnet-4-6, deepseek-chat, gemini-2.5-flash).

Target
LLM_MODEL
Default
claude-sonnet-4-6
Value
claude-sonnet-4-6
Anthropic API KeyVariable

Required when LLM Provider is anthropic. Get one at console.anthropic.com.

Target
ANTHROPIC_API_KEY
Target LanguageVariable

ISO 639-1 code for target language (en, de, es, fr, ja, etc.).

Target
TARGET_LANG
Default
en
Value
en
Daily Cost Cap (cents)Variable

Hard kill-switch in cents (1000 = $10). Translations stop until midnight UTC.

Target
MAX_COST_CENTS_PER_DAY
Default
1000
Value
1000
OpenAI API KeyVariable

Required when LLM Provider is openai.

Target
OPENAI_API_KEY
DeepSeek API KeyVariable

Required when LLM Provider is deepseek. Approximately $0.10 per episode.

Target
DEEPSEEK_API_KEY
Gemini API KeyVariable

Required when LLM Provider is gemini. Flash tier is approximately $0.08 per episode.

Target
GEMINI_API_KEY
Per-Job Cost Cap (cents)Variable

Max spend per single translation in cents (500 = $5).

Target
MAX_COST_CENTS_PER_JOB
Default
500
Value
500
Max Concurrent JobsVariable

How many translations may run in parallel.

Target
MAX_CONCURRENT
Default
2
Value
2
Webhook SecretVariable

Shared secret for arr webhook auth. Empty = no auth (fine on a private LAN).

Target
WEBHOOK_SECRET
Emby URLVariable

Emby server URL for library refresh hooks (e.g. http://emby:8096).

Target
EMBY_URL
Emby API KeyVariable

Emby API key for library refresh and the in-player subtitle picker.

Target
EMBY_API_KEY
Jellyfin URLVariable

Jellyfin server URL for library refresh hooks.

Target
JELLYFIN_URL
Jellyfin API KeyVariable

Jellyfin API key for library refresh.

Target
JELLYFIN_API_KEY
ntfy URLVariable

ntfy.sh push endpoint for job notifications. Empty = off.

Target
NTFY_URL
Ollama HostVariable

Ollama server URL (for the free local provider).

Target
OLLAMA_HOST
Default
http://ollama:11434
Value
http://ollama:11434