All apps · 0 apps
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
Readme
View on GitHubTranslarr
The arr you reach for when your release has subtitles in the wrong language and nobody has authored English ones yet.
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.srtinfix 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:
- In Sonarr/Radarr, create a tag named
sonarr_translate(orradarr_translate) - Apply the tag to specific items
- Configure the Connect webhook below
- 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>(ifWEBHOOK_SECRETis 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.newandmedia.playevents
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
- The plugin registers as an
ISubtitleProviderinside Emby - When you open the subtitle search modal, the plugin shows a "translate to EN" entry
- Clicking it calls the Translarr server's
/translateAPI - The server queues the job, runs it through the LLM pipeline, and writes the
.srt - 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
- Download
Translarr.dll - Copy it to your Emby plugins directory (
/config/plugins/inside the Emby container, or/mnt/user/appdata/emby/plugins/on Unraid) - Restart Emby
- Go to Emby -> Settings -> Plugins -> Translarr
- 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.
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/jesposito/translarr:mainRuntime arguments
- Web UI
http://[IP]:[PORT:9000]/- Network
bridge- Shell
sh- Privileged
- false
Template configuration
Host port to map to container port 9000 (Web UI and API).
- Target
- 9000
- Default
- 9100
- Value
- 9100
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
Persistent storage for database and settings. Include this in backups.
- Target
- /data
- Default
- /mnt/user/appdata/translarr
- Value
- /mnt/user/appdata/translarr
anthropic, openai, ollama, deepseek, or gemini
- Target
- LLM_PROVIDER
- Default
- anthropic
- Value
- anthropic
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
Required when LLM Provider is anthropic. Get one at console.anthropic.com.
- Target
- ANTHROPIC_API_KEY
ISO 639-1 code for target language (en, de, es, fr, ja, etc.).
- Target
- TARGET_LANG
- Default
- en
- Value
- en
Hard kill-switch in cents (1000 = $10). Translations stop until midnight UTC.
- Target
- MAX_COST_CENTS_PER_DAY
- Default
- 1000
- Value
- 1000
Required when LLM Provider is openai.
- Target
- OPENAI_API_KEY
Required when LLM Provider is deepseek. Approximately $0.10 per episode.
- Target
- DEEPSEEK_API_KEY
Required when LLM Provider is gemini. Flash tier is approximately $0.08 per episode.
- Target
- GEMINI_API_KEY
Max spend per single translation in cents (500 = $5).
- Target
- MAX_COST_CENTS_PER_JOB
- Default
- 500
- Value
- 500
How many translations may run in parallel.
- Target
- MAX_CONCURRENT
- Default
- 2
- Value
- 2
Shared secret for arr webhook auth. Empty = no auth (fine on a private LAN).
- Target
- WEBHOOK_SECRET
Emby server URL for library refresh hooks (e.g. http://emby:8096).
- Target
- EMBY_URL
Emby API key for library refresh and the in-player subtitle picker.
- Target
- EMBY_API_KEY
Jellyfin server URL for library refresh hooks.
- Target
- JELLYFIN_URL
Jellyfin API key for library refresh.
- Target
- JELLYFIN_API_KEY
ntfy.sh push endpoint for job notifications. Empty = off.
- Target
- NTFY_URL
Ollama server URL (for the free local provider).
- Target
- OLLAMA_HOST
- Default
- http://ollama:11434
- Value
- http://ollama:11434