Skip to main content

Thabit Translator

A CPU-only subtitle automation tool: download, extract, translate and transcribe subtitles from any video file.

It ships in two forms:

  1. CLI — core/, the pipeline itself. Install it with pipx from PyPI, or run it straight from a checkout.
  2. Jellyfin 10.11+ plugin — jellyfin-plugin/, a .NET assembly that runs that same pipeline as a subprocess inside a derived jellyfin/jellyfin:10.11 image.

All commands below run from the repository root (git clone … && cd thabit-translator).

Contents

Features

  • Download subtitles (OpenSubtitles, SubDL, SubSource) with file-hash / structured episode search and a relevance guard
  • Translate subtitles between 16+ languages using Argos Translate
  • Extract embedded subtitle tracks from video files (FFmpeg)
  • Transcribe speech to text using Vosk (opt-in — it is hours of CPU)
  • Auto mode — one command runs the whole chain (download → extract → translate → STT) on a single file or a whole folder
  • Jellyfin integration — a subtitle provider in the per-item download menu, a scheduled task that fills in missing subtitles, a configuration page and GET /ThabitTranslator/status

Everything is CPU-only: the first run installs a CPU-only PyTorch build (~1.6 GB total, once).

Installation

CLI with pipx

Requirements: Ubuntu/Debian-family Linux, python3 (3.9+), python3-venv, ffmpeg.

pipx install thabit-translator

thabit-translator --help

Notes:

  • The first run downloads the ML stack (Argos, Vosk, CPU-only torch, ~1.6 GB) into a per-app virtual environment — this is the same bootstrap the source checkout and the Jellyfin plugin use, and it repairs itself on later runs.
  • On first run the config is created at ~/.config/thabit/thabit_translator.conf — add your provider API keys there (see Configuration).
  • pipx never touches your system Python; upgrade with pipx upgrade thabit-translator.

Jellyfin plugin

Two ways to install a release:

A. Through this project's plugin repository (recommended) — in Jellyfin (Dashboard → Plugins → Repositories → +), add the repository and install from the catalog:

  1. Repository URL: https://raw.githubusercontent.com/mbnoimi/thabit-translator/main/manifest.json
  2. Dashboard → Plugins → Catalog → search Thabit Translator → Install
  3. Restart Jellyfin (Installed → Thabit Translator → Restart)

This manifest.json is the plugin-repository catalog this project publishes: it is committed to main automatically by the release workflow, points its sourceUrl at the tagged GitHub release zip, and carries that zip's MD5 as checksum — Jellyfin verifies both on install and on updates, so a version bump only needs a git tag v<version> push.

B. Manual copy — unzip the release zip into the server's plugin directory and restart:

mkdir -p  <config>/plugins/Jellyfin.Plugin.ThabitTranslator
unzip thabit-translator-plugin_<version>.zip -d <config>/plugins/Jellyfin.Plugin.ThabitTranslator

<config> is /config for the official container image, /var/lib/jellyfin/plugins for the deb/rpm packages. Jellyfin discovers plugins from directories containing the assembly — a loose .zip is not picked up.

Runtime requirements for A and B (the official image misses all of these):

Requirement Notes
Jellyfin ≥ 10.11 targetAbi 10.11.0.0
python3 + python3-venv python3 -m venv must work (Debian: install python3-venv)
ffmpeg/ffprobe on PATH the official image keeps them only under /usr/lib/jellyfin-ffmpeg/
Writable plugin data folder library/, .venv_thabit/, thabit_translator.conf, staging/, home/ live there
Ubuntu/Debian-family /etc/os-release the library's own system check insists on it

The DLL carries the whole Python library with it (extracted into the plugin data folder on load), and the venv bootstrap creates everything else on first run — or up front via the Prepare runtime button on the config page.

From source

git clone https://github.com/mbnoimi/thabit-translator.git
cd thabit-translator

python3 core/thabit_translator/__main__.py --help     # venv bootstraps itself on first run
# equivalently, from core/:
python3 -m thabit_translator --help

The shared venv lives at .venv_thabit in the repository root: every run verifies it and installs whatever is missing, so a partial venv repairs itself instead of crashing with ModuleNotFoundError. Delete .venv_thabit to rebuild from scratch.

Usage

Modes: auto, translate, extract, download, stt (or no argument for the interactive menu). The examples below use the pipx-installed command; from a checkout, replace thabit-translator with python3 -m thabit_translator (run from core/).

Mode Purpose Input
auto Download → extract → translate → STT fallback file or folder
translate Convert an existing SRT to another language .srt file
extract List / extract embedded subtitle streams single video
download Fetch subtitles from providers single video
stt Generate subtitles from speech single video
# Any video, or an existing subtitle file (subtitle in = just translate it)
thabit-translator auto "movie.mp4"
thabit-translator auto "movie.srt" -t fr

# Custom target language (default comes from config)
thabit-translator auto "movie.mp4" -t ar

# A folder - season, whole show, or a movies folder (recursive batch)
thabit-translator auto "/path/to/Show/Season 01"
thabit-translator auto "/path/to/Show"             # every season
thabit-translator auto "/path/to/Show" --force     # redo files that already have one

Auto mode flow:

  1. Try downloading a subtitle in the target language (hash → query, all providers)
  2. Try extracting embedded subtitles from the video
  3. Try downloading an English subtitle as fallback → translate
  4. Use Vosk STT as last resort → translate

Input resolution: if the given path does not exist, the tool matches the real file in the same folder and works with that instead (Monk - S01E3.mp4 → Monk - S01E03.mp4). Applies to all five modes.

Output naming: the subtitle name always mirrors the source file exactly — Monk - S01E03.mp4 produces Monk - S01E03.ar.srt, never a name derived from a mistyped input path.

Folder mode (batch): every video in the folder is processed in sorted order and each subtitle is written next to its video:

  • Videos that already have a valid <name>.<lang>.srt are skipped, so an interrupted run can simply be re-run and continues where it stopped
  • --force re-processes files that already have a subtitle
  • Hidden files and files whose name contains sample are ignored
  • One failing video never aborts the batch — it is reported in the summary
  • Ends with a summary: [AUTO] Folder done: 12 ok, 1 skipped, 0 failed
  • extract, download and stt remain single-file only

Never blocks on input: no provider call can stall a batch. When several candidates match, auto takes option 1 instead of prompting — only the download command asks you to pick, and only when stdin is a terminal.

STT asks first: when the providers have nothing (or are blocked), the file has no embedded track and the English download failed too, auto stops before transcribing, because STT is by far the slowest step:

Start STT? [y]es  [n]o/skip  [a]lways for this run:

--stt ask (default) prompts on a terminal, --stt yes never asks (the right choice for a subprocess caller such as the Jellyfin plugin), --stt no never runs STT. With no terminal to ask, the video is skipped with a message instead of hanging, and folder mode counts those as STT-declined:

[AUTO] Folder done: 12 ok, 3 skipped, 1 STT-declined, 0 failed

Episode search is structured, not textual

Monk - S01E12 is not a title any provider indexes. auto resolves the series once (query=Monk, type=episode → parent_imdb_id, cached per show name, so a 124-video folder pays for one lookup) and asks for that exact season/episode; when the API knows no such episode it widens to the whole season and lets the name filter pick the release labelled S01E12 — OpenSubtitles files some episodes off by one (its S01E13 entry is the file released as Monk.S01E12...). SubDL gets type=tv with season_number/episode_number instead of a movie-name search, which is what its "can't find movie or tv" error was about.

Wrong-title protection: a result must really be your episode. Names are parsed with guessit (what subliminal/bazarr use), so S01E12, 1x12, Season 1 Episode 12 and scene releases all mean the same episode, and plain movie names carry no code at all. A candidate is rejected when its own name carries a different code, when the API's season/episode metadata says something else and the name adds nothing, or when nothing identifies it and it did not come from an exact file-hash match. Rejections are logged as dropped N result(s) for a different episode, and a provider whose search comes back empty is queried once instead of repeating the same search for every candidate index.

Provider limits and quota handling

OpenSubtitles allows ~20 downloads per day per account. When the quota runs out the workflow falls back to Vosk STT for every remaining video, folder mode included — correct but very slow on long videos, so prefer running one season at a time. Once OpenSubtitles answers with HTTP 406 (remaining: 0) it is skipped until that response's reset_time (usually 23:59 UTC) and then re-enabled automatically, so a batch crossing the reset keeps working without a restart. Every mention of it names the cause — your account's limit, not "no subtitles":

[WARN] OpenSubtitles: your account hit its daily download limit (20/day), reset Sun 02:59 +03
[AUTO] OpenSubtitles is over your account's daily download limit until Sun 02:59 +03 - it may already have this subtitle
[INFO] OpenSubtitles skipped: your account's daily download limit until Sun 02:59 +03 - trying: subdl, subsource

SubDL's free plan allows 2,000 searches/day but only 50 downloads/day (a download link without api_key is instead capped at 300/day per IP), so a SubDL 429 is nearly always the download quota, not a missing subtitle. The tool names which one it was, drops the provider for the rest of the run, and picks it up again on its own when the limit's own short window expires:

[WARN] SubDL: your account's daily download limit is spent (50 downloads/day on the free plan)
[INFO] subdl skipped: your account's daily download limit is spent (50 downloads/day on the free plan) - trying: subsource

A short throttle (Retry-After up to 30s) is waited out and retried once instead of being given up on, and expired windows are re-checked. Missing or rejected credentials are reported once as a setup problem rather than retried for every candidate index of every video, and API keys are masked in logged download URLs. Each reason is printed once per run, so a season where all three providers are exhausted stays readable.

Only OpenSubtitles supports a file-hash lookup, so SubDL and SubSource are asked once per video rather than twice (the "retry by query" pass used to repeat the exact same request). SubSource has no Monk TV episodes at all and returns irrelevant results — these are dropped by the relevance guard.

Other modes

# Translate: the one mode where you name the output yourself (-s defaults to en, -t to ar)
thabit-translator translate input.srt output.ar.srt -s en -t ar
thabit-translator translate movie.srt french.srt -t fr

# Extract embedded subtitles (list first, then pick the stream index)
thabit-translator extract "movie.mp4" -l
thabit-translator extract "movie.mp4"          # first stream
thabit-translator extract "movie.mp4" -s 2     # specific stream
thabit-translator extract "movie.mp4" --src en # language hint for untagged streams

# Download from providers
thabit-translator download "movie.mp4" -t ar
thabit-translator download "movie.mp4" -t ar -q "Movie Title 2024"   # custom query
thabit-translator download "movie.mp4" -t ar -p opensubtitles         # one provider
thabit-translator download "movie.mp4" -t ar --select 0               # pick by index

# Speech to text
thabit-translator stt "movie.mp4" -s en
thabit-translator stt "movie.mp4" -s en -t fr
thabit-translator stt "movie.mp4" -s en -m vosk-model-small-en-us-0.15 -t ar --max-duration 5.0 --max-chars 60
  • download searches by file hash first, then falls back to a filename query; --no-hash skips the hash lookup, --no-auto disables auto-pick (use with --select N)
  • The Vosk model for STT is downloaded on first use (~40–70 MB) and cached in ~/.cache/thabit_translator/vosk_models/

Running with no arguments opens an interactive prompt (thabit>): type the same commands as above but without thabit-translator, ? reprints the menu, q quits.

Configuration

Provider API keys and defaults live in one INI file. Lookup order:

  1. $THABIT_CONFIG (explicit path — also what any -c/--config argument does)
  2. core/thabit_translator.conf (source checkout, next to the package)
  3. ~/.config/thabit/thabit_translator.conf
  4. ~/.thabit_translator.conf

The first file that exists wins. If none exists (fresh pipx install), the shipped template is copied to ~/.config/thabit/thabit_translator.conf on first run — that is the file to edit. Create it manually from core/thabit_translator/thabit_translator.conf.template when working from a checkout:

[opensubtitles]
api_key = YOUR_API_KEY
username = your_username
password = your_password
default_language = ar
user_agent = ThabitTranslator v1.0

[subdl]
api_key = YOUR_SUBDL_KEY

[subsource]
api_key = YOUR_SUBSOURCE_KEY

[settings]
default_source_lang = "en"
default_target_lang = "ar"
verbose = true
cache_dir = "~/.cache/thabit_translator"

[providers]
enabled = opensubtitles,subdl,subsource

Get API keys from:

Without keys the pipeline falls back to embedded tracks (and STT in auto mode).

Jellyfin plugin settings

Dashboard → Plugins → Thabit Translator, or directly http://localhost:8096/web/ConfigurationPage?name=Thabit%20Translator (admin session cookie or X-Emby-Token required — the page name is the plugin name, not the C# namespace).

Setting Default Notes
OpenSubtitles / SubDL / SubSource keys empty rendered into thabit_translator.conf; without them the pipeline falls back to embedded tracks
Target languages ar comma separated ISO-639-1, shown in the download menu
Source language en language the produced subtitle is in
Enabled providers all three
STT policy ask no skips, yes runs Vosk, ask only asks when a terminal exists — a scheduled run never has one, so it skips (see core/auto_mode.md)
Auto task enabled, max 0 (unlimited) languages can be narrowed per library by that library's own "Download subtitles in these languages"

Saving the settings regenerates thabit_translator.conf immediately; every run re-writes it too, so a change never needs a restart. Status for debugging:

curl -H "X-Emby-Token: $TOKEN" http://localhost:8096/ThabitTranslator/status
 # {"Loaded":true,"DataFolder":…,"TargetLanguages":"ar","PythonPath":"python3",
 #  "PythonAvailable":true,"PythonDetail":"Python 3.13.5 at python3","VenvReady":true,
 #  "LibraryPath":…,"LibraryExtracted":true,"IsBusy":false,…}

Supported languages

Translation (Argos Translate)

Code Language Code Language
en English ar Arabic
fr French de German
es Spanish it Italian
pt Portuguese ru Russian
zh Chinese ja Japanese
ko Korean tr Turkish

Speech recognition (Vosk)

Code Model Size
en, en_US, en_GB vosk-model-small-en-us-0.15 ~40MB
en_IN vosk-model-small-en-in-0.4 ~45MB
ar, ar_SA vosk-model-small-ar-0.22 ~60MB
fr, fr_FR vosk-model-small-fr-0.22 ~55MB
de, de_DE vosk-model-small-de-0.15 ~40MB
es, es_ES vosk-model-small-es-0.22 ~55MB
ru, ru_RU vosk-model-small-ru-0.4 ~70MB
pt, pt_BR vosk-model-small-pt-0.3 ~60MB
tr, tr_TR vosk-model-small-tr-0.3 ~55MB
zh, zh_CN vosk-model-small-cn-0.3 ~80MB
ja, ja_JP vosk-model-small-ja-0.22 ~55MB
ko, ko_KR vosk-model-small-ko-0.22 ~55MB
it, it_IT vosk-model-small-it-0.4 ~70MB
nl, nl_NL vosk-model-small-nl-0.22 ~55MB
pl, pl_PL vosk-model-small-pl-0.4 ~70MB
uk, uk_UA vosk-model-small-uk-0.4 ~70MB

Output files

Subtitles are saved next to the input file, named after it:

movie.mp4           → movie.ar.srt     (Arabic subtitle)
video.mkv           → video.en.srt     (English subtitle)
subtitle.srt        → subtitle.fr.srt  (French translation)

The base name always matches the source file exactly, including zero padding: Monk - S01E03.mp4 produces Monk - S01E03.ar.srt.

Troubleshooting

CLI

Symptom Fix
ModuleNotFoundError: No module named '...' the venv was skipped/incomplete — the bootstrap fixes it on the next run; if not, rm -rf .venv_thabit and rerun
Missing essential system tools sudo apt install ffmpeg mkvtoolnix (mkvtoolnix recommended for MKV)
Download fails or returns empty file the tool retries by query, walks several results, validates downloads, then falls back to embedded tracks or STT
Folder run slows to a crawl after ~20 files OpenSubtitles' ~20/day account quota ran out; every remaining video falls back to STT (minutes–hours each). Stop (Ctrl+C) and re-run later — videos with a valid <name>.<lang>.srt are skipped, so the batch resumes
STT is slow / first run slow first run downloads the Vosk model (~40–70 MB, cached afterwards in ~/.cache/thabit_translator/vosk_models/) or, on a fresh install, the ~1.6 GB ML stack
Extraction fails install mkvtoolnix for better MKV support
PyPI install cannot reach the CPU torch index the bootstrap falls back to PyPI automatically (larger download)

Jellyfin plugin

Symptom Cause / fix
A hand-copied DLL "reverts" after a restart entrypoint.sh reinstalls the image's plugin on every start — rebuild (build → image → up)
ConfigurationPage → 404 wrong name=: use Thabit Translator (the page name), not the C# namespace
401 on API calls Authorization: MediaBrowser Token="<token>", Client=…, DeviceId=…, Version=… — a bare Token= header is rejected
"no API key configured" warnings settings are empty; paste the keys on the config page
Red banner "Python was not found" install python3 + python3-venv (or set the interpreter path) — the status API's PythonDetail says what was tried
First run takes many minutes normal: the venv bootstrap downloads ~1.6 GB once; use Prepare runtime to do it deliberately
STT skipped (no terminal to ask …) expected: STT policy is ask and a subprocess has no TTY; set the policy to yes to transcribe anyway

License

Licensed under the GNU Lesser General Public License v3.0 only (LGPL-3.0-only) — see LICENSE. Copyright (C) 2024-2026 mbnoimi.

Support

Metadata

Release files for thabit-translator 0.0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for thabit-translator 0.0.2.1
File Size Uploaded
thabit_translator-0.0.2.1.tar.gz 70.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for thabit-translator 0.0.2.1
File Interpreter ABI Platform
thabit_translator-0.0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 142.1 kB

Release files / thabit_translator-0.0.2.1.tar.gz

Download URL thabit_translator-0.0.2.1.tar.gz
Size 70.4 kB
Tags Source
SHA-256 checksum
How to use checksums
be4e66cf621316654ea825498fe546b7484c768efda16d65cafd98a7e4b3dc73
BLAKE2b-256 checksum
How to use checksums
961c5fa0464ca5b5393dfad0705fbc6935e83c48191e4d031232e5fdda8fad2f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / thabit_translator-0.0.2.1-py3-none-any.whl

Download URL thabit_translator-0.0.2.1-py3-none-any.whl
Size 71.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
294d4b0b92cdf1b7806597b08d04bbf280e6eb4a764bae96db431fe5684a98f4
BLAKE2b-256 checksum
How to use checksums
f47035e468605f6b3cc01c4015cdaa25d4123686a77f51ce1dda950ab2c92956
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

This release

0.0.2.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page