- Python 99.7%
- Dockerfile 0.2%
- Shell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| tests | ||
| tools | ||
| veritarr | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| Dockerfile | ||
| PLAN-manager.md | ||
| PLAN.md | ||
| pyproject.toml | ||
| README.md | ||
veritarr
The subtitle manager of a movie/series library. It checks every subtitle (does it belong to the video, is it
in sync, is the file sane), fixes what it safely can, downloads what is missing or wrong from providers
(OpenSubtitles.com, Gestdown) after checking each download against the video, translates what no provider has
(AI), keeps only external subtitle files (embedded tracks are written out, then stripped by a lossless remux)
and keeps a backup of every version. Radarr/Sonarr give the catalog and the events; Plex/Jellyfin and Gotify are
told about changes. Design: PLAN.md, PLAN-manager.md.
Running
services:
veritarr:
image: git.exithub.net/ivan.pavlina/veritarr:latest
restart: unless-stopped
# Same user as the arrs and Plex, so written subtitles keep a usable owner.
user: "1000:1000"
ports: ["8090:8080"]
environment:
VERITARR_ROOTS: /mnt/media/movies:/mnt/media/shows
VERITARR_ASR_URL: http://gaming-vm.lan:8910
volumes:
- /mnt/media:/mnt/media # read-write: fixes are written in place
- /data/arrs/veritarr:/data # DB, backups, downloaded models
Open http://<host>:8090/: the first visit asks for a user name and password (one login). The UI lists
the subtitles that need review, shows each check (transcript and subtitle side by side per window) with
actions (apply the fitted timing, shift, rename the language tag, approve, remove and search again, recheck),
every change with a restore button, the wanted list, providers, the event history and all settings.
Settings: the environment gives the starting values only. On start, every setting given in the environment and
not in the DB yet is copied into the DB; from then on the Settings page is the place to change it, and the
variable can be removed from the compose file (VERITARR_DATA and VERITARR_THREADS stay environment only).
| Variable | Default | |
|---|---|---|
VERITARR_ROOTS |
Library roots, :-separated |
|
VERITARR_DATA |
/data |
DB (veritarr.db), backups/, models (HF_HOME=/data/models in the image) |
VERITARR_SCAN_INTERVAL |
1800 |
Seconds between library scans (at least 60) |
VERITARR_SETTLE |
300 |
A file is taken once its mtime is this old |
VERITARR_LANGUAGES |
Subtitle languages to check, e.g. hr en; other files and embedded tracks are skipped (untagged tracks by their text). Empty: all |
|
VERITARR_WORKERS |
1 |
Videos checked at the same time; with a GPU server 2-3 keep it busy while the others decode |
VERITARR_ASR_URL |
whisper.cpp server (GPU); without it everything runs on the CPU model | |
VERITARR_ASR_MODEL |
large-v3 |
Model that server runs (part of the transcript cache key) |
VERITARR_CPU_MODEL |
small |
faster-whisper fallback when the server is down; empty disables it |
VERITARR_CPU_THREADS |
2 |
Threads for the fallback |
VERITARR_THREADS |
4 |
torch / embedding threads |
VERITARR_FIX |
true |
Rewrite confident fixes (timing, encoding); false only records checks |
VERITARR_QUARANTINE |
false |
Move a mismatch into the backup store right away (the planner does it anyway when it finds no replacement; needs FIX) |
VERITARR_ACCEPT_VARIANTS |
false |
A close variant of the tagged language (Serbian or Bosnian tagged hr) is fine when there is no better one; off: it stays uncertain and the planner looks for a better one |
VERITARR_TRANSLATE_VARIANTS |
false |
The AI rewrites a close variant (Serbian or Bosnian tagged hr) into the tagged language, in place; numbering and timing stay, it can be restored (needs the AI and FIX) |
VERITARR_STRIP_EMBEDDED |
false |
Write embedded text subtitles (the checked languages) out as files and remove every subtitle track from the video by a lossless ffmpeg remux, verified before it replaces the original; a language embedded only as an image (PGS) is read as text (OCR, Tesseract; models fetched on first use into /data/models/tessdata) and waits for a good file first. Needs FIX; a title can also be done alone from its page |
VERITARR_BROKEN_VIDEOS |
report |
A broken video (truncated, or far shorter than Radarr/Sonarr say with a second signal): report (Review, Gotify) or redownload (marked as failed in the arr, file deleted, searched again; needs FIX, never for manual imports or season packs) |
VERITARR_REDOWNLOAD_PER_DAY |
5 |
At most this many automatic redownloads a day |
VERITARR_SEARCH_MISSING |
false |
The planner: missing, wrong or uncertain subtitles are searched for at the providers, then translated (hr from sr/bs/en). With DOWNLOADS off it only records what it would try; with FIX off nothing is written (dry run). Wanted page |
VERITARR_SOURCES |
hr:providers,sr,bs,en en:providers |
Per language, the sources in order: providers or a language to translate from |
VERITARR_DOWNLOADS |
false |
"Download from providers": the master switch for any download |
VERITARR_PROVIDERS |
opensubtitles gestdown |
Providers in use (Providers page: health, quota, Test) |
VERITARR_OPENSUBTITLES_API_KEY, _USERNAME, _PASSWORD |
OpenSubtitles.com: your own API consumer key plus the account | |
VERITARR_DOWNLOADS_PER_RUN |
3 |
Downloads per video and search |
VERITARR_HI_LAST |
true |
Hearing-impaired subtitles only when no other one passes |
VERITARR_RADARR_URL, _API_KEY |
Catalog (movie or series, ids, runtimes) and webhook events; Settings > "Connect Radarr/Sonarr" sets up the webhook | |
VERITARR_SONARR_URL, _API_KEY |
The same for series (episode numbers of each file) | |
VERITARR_HOOK_URL |
http://veritarr:8080 |
How Radarr/Sonarr reach veritarr (for the Connect button) |
VERITARR_ARR_SYNC_INTERVAL |
21600 |
Seconds between full catalog syncs (webhooks keep it current in between) |
VERITARR_ROOT_KINDS |
path=movies or path=series per root, for titles the arrs do not know; else guessed from the layout |
|
VERITARR_PLEX_URL, _TOKEN, _PATH_MAP |
Partial scan of the folder after veritarr changes a subtitle | |
VERITARR_JELLYFIN_URL, _API_KEY, _PATH_MAP |
Library/Media/Updated for the changed subtitle |
|
VERITARR_GOTIFY_URL, _TOKEN |
Digest of subtitles that need review | |
VERITARR_WEBHOOK_URL |
The same digest as a JSON POST {"title", "message", "items"} (Home Assistant, ntfy, ...) |
|
VERITARR_GOTIFY_INTERVAL |
21600 |
At most one digest per this many seconds while the queue is busy; one when it runs empty |
VERITARR_LLM_URL, _API_KEY, _MODEL |
Optional AI second opinion: an OpenAI-compatible chat API (DeepSeek, Mistral, Ollama, ...) | |
VERITARR_AI_MIN_BALANCE |
0.5 |
No AI request once the money left on the AI account (DeepSeek's balance) is down to this; 0: no floor |
A path map is from:to, e.g. /mnt/media:/data when Plex mounts the library at /data.
The first scan queues the whole library as backfill; it only runs while the GPU server answers (when one is configured). New or changed files go first and fall back to the CPU model.
Integrations
All optional; an integration that is not configured or fails is skipped and logged, it never fails a check.
- Radarr / Sonarr (
RADARR_URL/_API_KEY,SONARR_URL/_API_KEY): the catalog (movie or series, episode numbers, ids, runtimes; synced every 6 h) and the webhook (import, upgrade, rename, delete, added): the "Connect Radarr/Sonarr" button in Settings sets it up, URLhttp://veritarr:8080/api/hook/radarr(or/sonarr). - Providers: OpenSubtitles.com (your own free API consumer key plus the account; VIP gives 1000 downloads a day) and Gestdown (series, no account). Providers page: health, quota, Test.
- Plex / Jellyfin: refreshed after every rewrite, replacement, quarantine and restore.
- Gotify: one message listing the subtitles that need review (mismatch, uncertain, error).
- AI second opinion (
LLM_URL+LLM_MODEL, usuallyLLM_API_KEY): only for subtitles (files and embedded tracks) the checks cannot decide (uncertain) or call a mismatch. The model pairs the speech lines of each window with the subtitle lines (a translation counts); veritarr fits the timing from the pairs. It can rescue a subtitle (ok/fixed) or confirm a mismatch, never turn an uncertain one into a mismatch. Answers are cached; the token use is on the Settings page. DeepSeek:https://api.deepseek.com/v1, modeldeepseek-flash, extra fields{"thinking": {"type": "disabled"}}(with reasoning a call used ~22k output tokens, ~1.4 cents; without ~0.1). - One subtitle per language: good subtitle files of one video in one language and kind (forced and SDH
count apart), e.g.
Movie.hr.srt,Movie.srt,Movie.1.srt: with fixes on the best one keeps (or gets) the tagged name, the others go into the backup store (restorable from History). With the AI on, every good subtitle file is also reviewed for its language variant and quality (60 lines spread over the file, cached by content): the right variant wins first (Croatian over Serbian in Latin script, which lingua cannot tell apart), then quality, matching windows and timing. A tagged file that is alone and turns out to be another variant (Serbian tagged hr) or unusable (quality 1/5) becomes uncertain, so the planner looks for a better one (or it is translated in place withTRANSLATE_VARIANTS). - One file for many videos: an undecided subtitle file whose exact content is also the subtitle of two or
more other videos is a mismatch (a season of Breaking Bad had two
.en.hi.srtfiles copied over all episodes).
Fewer decisions for a person
With fixes on, veritarr tries to end every subtitle file in a settled state on its own:
- in sync: nothing; off by a shift, a frame-rate ratio or in parts (another cut, confirmed by the content windows): retimed;
- not this video, or clearly another language or script than its name says: the planner downloads candidates, checks each against the video and installs the first that passes (else it is translated from a close variant or English); with nothing found the wrong file goes into the backup store and the search goes on with a backoff;
- undecided (after the AI second opinion, when configured): kept, and a better one is looked for weekly.
API
The API needs the login session or the X-Api-Key header (key on the Settings page); /health and the
hooks are open (they only queue paths under the library roots).
POST /api/check {"path": ...}— check a video, subtitle or directory now (only enqueues).POST /api/hook/radarr,/api/hook/sonarr(webhook JSON): imports, upgrades, renames, deletes; paths outside the roots are ignored.POST /api/scan— scan the library now.GET /api/status— queue, current job, ASR backend, integrations, verdict counts.GET /api/subtitles[?verdict=mismatch],GET /api/subtitles/{id}— results, review items first.GET /api/actions,POST /api/actions/{id}/restore— what was changed, and putting the original back (the current file is backed up first; the restored file is never fixed automatically again).
Development
python3 -m venv /tmp/veritarr-venv && /tmp/veritarr-venv/bin/pip install -e '.[dev]'
/tmp/veritarr-venv/bin/python -m pytest -q tests
python -m veritarr VIDEO --sub FILE ... -v # one video from the command line