No description
  • Python 99.7%
  • Dockerfile 0.2%
  • Shell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ivan Pavlina c413127dc0
All checks were successful
ci / check (push) Successful in 1m8s
ci / image (push) Successful in 5s
ci / deploy (push) Successful in 19s
ci / notify (push) Successful in 1s
An approved .ass is a translation source like an approved .srt (JJK S03E09: its own track, a check stayed uncertain, hr was "not found"); version 0.21.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:46:03 +01:00
.forgejo/workflows CI deploys main builds to docksys: deploy job over a key pinned to /usr/local/bin/veritarr-deploy 2026-11-03 13:40:52 +01:00
tests An approved .ass is a translation source like an approved .srt (JJK S03E09: its own track, a check stayed uncertain, hr was "not found"); version 0.21.1 2026-11-04 09:46:03 +01:00
tools Fewer decisions for a person: confirmed piecewise timing is a fix, one trial replacement for undecided subtitles (the better one is kept), approved files leave Review, optional AI second opinion; version 0.7.0 2026-10-31 15:57:36 +01:00
veritarr An approved .ass is a translation source like an approved .srt (JJK S03E09: its own track, a check stayed uncertain, hr was "not found"); version 0.21.1 2026-11-04 09:46:03 +01:00
.dockerignore Phase 1: core service 2026-10-30 19:00:33 +01:00
.gitignore Phase 0: CLI prototype of the pipeline, corpus evaluation and results 2026-10-30 18:30:45 +01:00
CLAUDE.md Sparse dialogue is checkable: timing fits only on cues with words, more windows when fewer than 3 heard words, the wide search no wider than 0.3 of a short video, and windows that find the subtitle as it is decide when the speech activity tells nothing; version 0.21.0 2026-11-04 08:43:51 +01:00
Dockerfile Picture subtitles (PGS) are read as text: a language embedded only as images gets a file of its own, checked like any other, instead of waiting for a provider; version 0.19.0 2026-11-03 21:01:38 +01:00
PLAN-manager.md A language pinned for no dialogue settles its file like the owner's pin (it stayed in Review as uncertain); version 0.20.1 2026-11-04 05:38:22 +01:00
PLAN.md Plan: veritarr as the subtitle manager (own providers, Radarr/Sonarr catalog, verified downloads in staging, translation sources, history, GUI rewrite, Bazarr retired) 2026-11-01 09:53:36 +01:00
pyproject.toml Keep the dependency layer cached between commits: the version lives only in veritarr/__init__.py 2026-11-03 13:16:07 +01:00
README.md Picture subtitles (PGS) are read as text: a language embedded only as images gets a file of its own, checked like any other, instead of waiting for a provider; version 0.19.0 2026-11-03 21:01:38 +01:00

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, URL http://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, usually LLM_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, model deepseek-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 with TRANSLATE_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.srt files 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