{"id":51082124,"url":"https://github.com/jesposito/translarr","last_synced_at":"2026-06-23T19:02:49.724Z","repository":{"id":357770528,"uuid":"1238467900","full_name":"jesposito/translarr","owner":"jesposito","description":"AI-powered subtitle translation for the arr stack. Translate existing foreign-language subtitle tracks (Russian fansubs, embedded Japanese, etc.) into any language via Claude/GPT/Ollama with reading-rate adaptation and ASS/SSA style preservation.","archived":false,"fork":false,"pushed_at":"2026-05-27T01:23:32.000Z","size":1390,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T02:23:20.041Z","etag":null,"topics":["ai","anthropic","arr-stack","claude","emby","jellyfin","llm","radarr","self-hosted","sonarr","subtitle-translation","whisper"],"latest_commit_sha":null,"homepage":"https://github.com/jesposito/translarr","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jesposito.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-14T06:35:45.000Z","updated_at":"2026-05-27T01:23:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jesposito/translarr","commit_stats":null,"previous_names":["jesposito/translarr"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jesposito/translarr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesposito%2Ftranslarr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesposito%2Ftranslarr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesposito%2Ftranslarr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesposito%2Ftranslarr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jesposito","download_url":"https://codeload.github.com/jesposito/translarr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jesposito%2Ftranslarr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34702919,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","anthropic","arr-stack","claude","emby","jellyfin","llm","radarr","self-hosted","sonarr","subtitle-translation","whisper"],"created_at":"2026-06-23T19:02:48.255Z","updated_at":"2026-06-23T19:02:49.719Z","avatar_url":"https://github.com/jesposito.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Translarr\n\n\u003e The arr you reach for when your release has subtitles in the wrong language and nobody has authored English ones yet.\n\n[![CI](https://github.com/jesposito/translarr/actions/workflows/ci.yml/badge.svg)](https://github.com/jesposito/translarr/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python\u0026logoColor=white)](https://python.org)\n[![AI BYO-key](https://img.shields.io/badge/AI-BYO--key%20(Claude%20·%20GPT%20·%20Ollama)-9333ea)](#)\n[![arr stack](https://img.shields.io/badge/integrates-Sonarr%20·%20Radarr%20·%20Bazarr-1abc9c)](#)\n[![Last commit](https://img.shields.io/github/last-commit/jesposito/translarr)](https://github.com/jesposito/translarr/commits/main)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)\n![Tests](https://img.shields.io/badge/tests-210-green.svg)\n![Status](https://img.shields.io/badge/status-alpha-yellow.svg)\n\n[![Join the Facet HQ Discord](https://img.shields.io/badge/Discord-Join%20Facet%20HQ-5865F2?logo=discord\u0026logoColor=white)](https://discord.gg/XD8eUudnmf)\n\nTranslarr 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.\n\nTested 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.\n\n## Quickstart\n\n```bash\ndocker run -d \\\n  --name translarr \\\n  -p 9100:9000 \\\n  -v /mnt/user/appdata/translarr:/data \\\n  -v /mnt/user/media:/media \\\n  -e ANTHROPIC_API_KEY=sk-ant-... \\\n  ghcr.io/jesposito/translarr:main\n```\n\nOpen `http://your-server:9100` for the Web UI, or verify it's running:\n\n```bash\ncurl http://localhost:9100/health\n# {\"status\":\"ok\",\"version\":\"0.1.0\",\"llm_provider\":\"anthropic\",\"llm_model\":\"claude-sonnet-4-6\"}\n```\n\nTranslate a file:\n\n```bash\ncurl -X POST http://localhost:9100/translate \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\n    \"media_path\": \"Movies/Some Movie (2025)/movie.mkv\",\n    \"source_lang\": \"ru\",\n    \"target_lang\": \"en\"\n  }'\n# {\"status\":\"queued\",\"job_id\":\"abc...\"}\n\ncurl http://localhost:9100/jobs/abc...\n# {\"state\":\"done\",\"output_path\":\"...\",\"cost_cents\":54}\n```\n\nThe container listens on port **9000** internally. Map it to whatever host port you want.\n\n## Features\n\n- **LLM router**: Anthropic Claude (default), OpenAI, DeepSeek, Google Gemini, Ollama (local, free)\n- **6 translation presets**: Quick \u0026 Cheap, Balanced, Best Quality, Local \u0026 Free, DeepSeek Budget, Gemini Flash\n- **Subtitle pipeline**: ffmpeg extraction from any container, pysubs2 parsing, ASS/SSA style-tag pass-through\n- **Sliding-context translation**: 30-line batches with 10-line prior context so names and pronouns stay consistent\n- **Reading-rate adapter**: auto-splits lines that would be unreadable, redistributes timing to match\n- **Source language auto-detection**: ffprobe reads the track's language tag so you don't have to set it manually\n- **Cost guards**: daily and per-job spend caps, per-job timeout kill-switch\n- **Live-mutable settings**: change model, provider, concurrency, cost caps via the Settings page with no restart\n- **Sonarr/Radarr/Plex webhooks**: opt-in per movie/series via tags\n- **Emby/Jellyfin webhooks**: retroactive translation on library scan or on-demand via playback events\n- **Emby plugin**: shows up in the player's subtitle search modal (see below)\n- **Item-specific library refresh**: Emby/Jellyfin refresh just the translated item (~2s) instead of doing a full scan\n- **Per-series config**: different source/target language defaults per series, applied automatically by path\n- **Glossary**: per-series term dictionaries keep character names consistent across episodes\n- **File browser**: browse your media library from the UI, see which files have translations, coverage stats\n- **Push notifications**: ntfy.sh integration for translation completion alerts\n- **Output-collision policy**: `.translarr.srt` infix means it never overwrites human/Bazarr/embedded subs\n- **Web UI**: dashboard, job history, live stats, budget tracking, library browser, glossary editor, settings\n- **210 tests, all green**\n\n## Wiring into the arr stack\n\nTranslarr only acts on items that **opt in via tag**. To enable translation for a movie or series:\n\n1. In Sonarr/Radarr, create a tag named `sonarr_translate` (or `radarr_translate`)\n2. Apply the tag to specific items\n3. Configure the Connect webhook below\n4. Imports of tagged items auto-enqueue a translation job\n\n**Radarr -\u003e Settings -\u003e Connect -\u003e Add -\u003e Webhook:**\n- URL: `http://translarr:9000/webhooks/radarr` (use your Docker host IP if Radarr is on a different network)\n- Triggers: `On Import`, `On Upgrade`\n- Optional headers: `X-Translarr-Secret: \u003csecret\u003e` (if `WEBHOOK_SECRET` is set)\n\n**Sonarr -\u003e Settings -\u003e Connect -\u003e Add -\u003e Webhook:**\n- URL: `http://translarr:9000/webhooks/sonarr`\n- Same as above, ending in `/sonarr`\n\n**Emby -\u003e Notifications -\u003e Add -\u003e Webhook:**\n- URL: `http://translarr:9000/webhooks/emby`\n- Events: `Library New`, `Library Updated`\n\n**Jellyfin** (requires the [Webhook plugin](https://github.com/jellyfin/jellyfin-plugin-webhook)):\n- URL: `http://translarr:9000/webhooks/jellyfin`\n- Triggers: `ItemAdded`\n\n**Plex** (via Plex settings or Tautulli):\n- URL: `http://translarr:9000/webhooks/plex`\n- Handles `library.new` and `media.play` events\n\n## Emby Plugin (optional)\n\nThe 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.\n\nThe plugin is **not required**. Webhooks work fine without it. This just adds the in-player UX.\n\n### How it works\n\n1. The plugin registers as an `ISubtitleProvider` inside Emby\n2. When you open the subtitle search modal, the plugin shows a \"translate to EN\" entry\n3. Clicking it calls the Translarr server's `/translate` API\n4. The server queues the job, runs it through the LLM pipeline, and writes the `.srt`\n5. Emby picks up the new subtitle file and shows it in the player\n\nThe plugin doesn't do any translation itself. It's just a thin HTTP client that talks to the Translarr container.\n\n### Install\n\n1. Download [`Translarr.dll`](https://github.com/jesposito/translarr/raw/main/plugins/emby/publish/Translarr.dll)\n2. Copy it to your Emby plugins directory (`/config/plugins/` inside the Emby container, or `/mnt/user/appdata/emby/plugins/` on Unraid)\n3. Restart Emby\n4. Go to Emby -\u003e Settings -\u003e Plugins -\u003e Translarr\n5. Set the Translarr server URL (e.g. `http://192.168.1.100:9100`) and save\n\n### Requirements\n\n- Emby Server 4.9+\n- Translarr container must be running and reachable from Emby\n- Both Emby and Translarr must see the same media files (same mount paths)\n\n## Configuration\n\nAll 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.\n\n| Variable | Default | Notes |\n|----------|---------|-------|\n| `LLM_PROVIDER` | `anthropic` | `anthropic`, `openai`, `ollama`, `deepseek`, `gemini` |\n| `LLM_MODEL` | `claude-sonnet-4-6` | Model id for your chosen provider |\n| `ANTHROPIC_API_KEY` | | Required for `anthropic` |\n| `OPENAI_API_KEY` | | Required for `openai` |\n| `DEEPSEEK_API_KEY` | | Required for `deepseek` |\n| `GEMINI_API_KEY` | | Required for `gemini` |\n| `OLLAMA_HOST` | `http://ollama:11434` | Required for `ollama` |\n| `MEDIA_ROOT` | `/media` | Where the volume is mounted inside the container |\n| `TRANSLARR_DATA_DIR` | `./data` | SQLite DB lives here |\n| `TARGET_LANG` | `en` | ISO 639-1 target language |\n| `READING_RATE_CPS` | `17` | Max chars/sec (15-17 is standard for English) |\n| `MAX_CONCURRENT` | `2` | Parallel translation jobs |\n| `CONTEXT_WINDOW_LINES` | `10` | Prior translated lines fed to the LLM for context |\n| `MAX_COST_CENTS_PER_DAY` | `1000` | Daily spend cap ($10). Goes over = HTTP 429 |\n| `MAX_COST_CENTS_PER_JOB` | `500` | Per-job cap ($5). Kills the job mid-batch if exceeded |\n| `JOB_TIMEOUT_SECONDS` | `1800` | Per-job wall-clock timeout (30 min) |\n| `RADARR_TRANSLATE_TAG` | `radarr_translate` | Tag that opts a movie in |\n| `SONARR_TRANSLATE_TAG` | `sonarr_translate` | Tag that opts a series in |\n| `WEBHOOK_SECRET` | | Optional shared secret for webhook calls |\n| `EMBY_URL` | | Emby server URL for post-translation refresh |\n| `EMBY_API_KEY` | | Emby API key |\n| `JELLYFIN_URL` | | Jellyfin server URL for post-translation refresh |\n| `JELLYFIN_API_KEY` | | Jellyfin API key |\n| `NTFY_URL` | | ntfy.sh endpoint for push notifications. Empty = off |\n| `LOG_LEVEL` | `INFO` | |\n\n## Cost estimate (2026 pricing)\n\nRough per-film cost for a 1500-event subtitle file:\n\n| Model | Per-film cost |\n|-------|---------------|\n| Claude Haiku 4.5 | ~$0.14 |\n| Claude Sonnet 4.6 | ~$0.54 |\n| Claude Opus 4.7 | ~$2.70 |\n| DeepSeek Chat | ~$0.08 |\n| Gemini 2.5 Flash | ~$0.04 |\n| Ollama qwen3:14b | $0 (local) |\n\nSee `docs/ARCHITECTURE.md` for the token-budget math.\n\n## Where Translarr fits\n\n```\n                    What you have\n                          |\n            +-------------+--------------+\n            v                            v\n  No subtitles anywhere       Subtitles in the wrong language\n            |                            |\n            v                            v\n       Whisper (subgen)            +-----------+\n       Audio -\u003e text               |TRANSLARR  |\n       (lossy, guesses names)      +-----------+\n                                          |\n            +-----------------------------+---------+\n            v                                       v\n  Embedded foreign track              External .srt provider\n  (Russian, Japanese, etc.)           (OpenSubtitles, Jimaku, planned)\n            |                                       |\n            +------------------+--------------------+\n                               v\n              LLM translation with sliding context\n              + reading-rate adapter\n              + ASS/SSA style-tag preservation\n                               |\n                               v\n              \u003cbasename\u003e.en.translarr.srt next to the video\n              + library refresh in Emby/Jellyfin\n```\n\nBazarr fetches existing subs. Subgen transcribes audio. Translarr translates the subtitle track that's already there. Different jobs.\n\n## Roadmap\n\nFree, self-hosted, no deadline.\n\n| Version | Scope | Status |\n|---------|-------|--------|\n| v0.1 | Server brain: webhooks, LLM router, sub pipeline, reading-rate, cost guards | Shipped |\n| v0.1.5 | Persistent queue, async `/translate`, worker pool, cost tracker | Shipped |\n| v0.1.6 | Source-lang auto-detect, preflight estimates, presets, Unraid template | Shipped |\n| v0.2 | Emby plugin (ISubtitleProvider + settings page) | Shipped |\n| v0.3 | DeepSeek/Gemini providers, Plex webhook, GHCR CI, file browser, glossary, per-series config | Shipped |\n| v0.4 | Jellyfin plugin | Next |\n| v0.8a | Direct subtitle provider integrations (OpenSubtitles, Jimaku, Animetosho) | Planned |\n| v0.9 | Whisper-from-audio fallback | Planned |\n| v1.0 | Strategy chain: auto-fallback through embedded, fetch, audio | Planned |\n\n## Architecture\n\n```\n                  +--------------------------------------+\n                  |          Translarr Server            |\n                  |  +-----------------------------+     |\n   Radarr --webhook--\u003e/webhooks/radarr -+           |     |\n   Sonarr --webhook--\u003e/webhooks/sonarr -+  enqueue  |     |\n     Emby --webhook--\u003e/webhooks/emby  --+     |     |     |\n  Jellyfin --webhook--\u003e/webhooks/jellyfin-+  v     |     |\n     Plex --webhook--\u003e/webhooks/plex -------+----+  |     |\n   Direct --POST---\u003e/translate ---------------+  v  |     |\n                  |                       +--------+ |     |\n                  |  workers \u003c---claim----|SQLite  | |     |\n                  |     |                 | queue  | |     |\n                  |     v                 +--------+ |     |\n                  |  Sub pipeline                    |     |\n                  |  +- ffmpeg extract OR direct .ass|     |\n                  |  +- pysubs2 parse                |     |\n                  |  +- batch + sliding context      |     |\n                  |  +- LLM router (5 providers)     |     |\n                  |  +- reading-rate adapt + split   |     |\n                  |  +- write .translarr.srt + refresh|    |\n                  +--------------------------------------+ \n                              |\n                              v\n                \u003cbasename\u003e.en.translarr.srt\n                next to source media\n```\n\n## Project structure\n\n```\ntranslarr/\n+-- server/              # Python FastAPI\n|   +-- main.py\n|   +-- config.py\n|   +-- cost_tracker.py\n|   +-- db.py            # SQLite + migrations\n|   +-- browse.py        # File browser API\n|   +-- glossary.py      # Per-series glossary persistence\n|   +-- series_config.py # Per-series language overrides\n|   +-- library_refresh.py\n|   +-- llm/             # router + 5 providers\n|   +-- queue/           # base.py + sqlite.py + worker.py\n|   +-- subs/            # extract.py + pipeline.py + reading_rate.py\n|   +-- webhooks/        # radarr, sonarr, emby, jellyfin, plex\n+-- ui/                  # SvelteKit static Web UI\n+-- plugins/\n|   +-- emby/            # C# Emby plugin (ISubtitleProvider)\n|   +-- jellyfin/        # C# Jellyfin plugin (planned)\n+-- templates/           # Unraid Community Applications template\n+-- tests/               # pytest: 210 tests\n+-- docs/\n|   +-- ARCHITECTURE.md\n|   +-- INSTALL.md\n|   +-- COMPETITIVE-ANALYSIS.md\n+-- docker-compose.yml\n+-- Dockerfile\n+-- pyproject.toml\n```\n\n## License\n\nMIT. See [`LICENSE`](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesposito%2Ftranslarr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjesposito%2Ftranslarr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesposito%2Ftranslarr/lists"}