SyncOrbit is a self-hosted tool for analyzing, fixing, and managing subtitle synchronization in large movie libraries.
  • Python 46.3%
  • JavaScript 43.8%
  • HTML 4.5%
  • CSS 4.5%
  • Dockerfile 0.7%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-03 15:58:27 +03:00
docs Major feature, new tab for tv shows 2026-09-02 10:18:42 +03:00
public Fix per-row reanalyze button not beig enabled after job finished 2026-09-03 15:58:27 +03:00
python Fix collect_reference_candidates and search clear button 2026-09-02 11:08:52 +03:00
whisperx-service Fix dead code and update documents 2026-08-19 15:56:57 +03:00
.dockerignore Change worflow to local image build in Unraid share 2026-08-19 14:02:17 +03:00
.env.example Major feature, new tab for tv shows 2026-09-02 10:18:42 +03:00
.gitignore Explain why subtitle is not synced and add drift overlay graph 2026-08-20 19:15:29 +03:00
db.cjs Add redecide.py to container 2026-09-02 09:05:08 +03:00
deploy.sh Change worflow to local image build in Unraid share 2026-08-19 14:02:17 +03:00
docker-compose.yml Major feature, new tab for tv shows 2026-09-02 10:18:42 +03:00
Dockerfile Change worflow to local image build in Unraid share 2026-08-19 14:02:17 +03:00
LICENSE Initial commit 2025-11-17 10:18:22 +02:00
package-lock.json fix: update package-lock.json 2025-12-19 17:27:59 +02:00
package.json chore: add sqlite 2025-12-19 16:59:18 +02:00
README.md Update whisper container ifno with needed path mappings 2026-09-03 12:38:18 +03:00
server.cjs Change whisper endpoint back to fire-and-forget 2026-09-03 13:17:34 +03:00

SyncOrbit

SyncOrbit is a self-hosted tool for analyzing, fixing, and managing subtitle synchronization in large movie libraries.

It combines automated alignment, speech-based references, and human-in-the-loop workflows to solve one problem well:

“Which subtitle is correct, and how do I make it stay that way?”

What SyncOrbit Does

  • Scans a movie library and analyzes subtitle sync quality
  • Aligns subtitles using multiple reference strategies:
    • Original English subtitles
    • WhisperX-generated speech references
    • FFSubSync-aligned references
  • Scores and classifies results (synced / needs adjustment / bad)
  • Lets you reanalyze individual movies or run batch jobs
  • Lets you autocorrect individual movies based on previous analysis
  • Keeps state in a database (not fragile CSV glue)
  • Designed for large libraries (thousands of movies)

Design Philosophy

SyncOrbit follows a few strict principles:

  • Automation first, but not blindly
  • Never delete expensive work (e.g. Whisper references)
  • Newest reference wins (simple, intuitive decision model)
  • UI should explain decisions, not hide them
  • Everything must be inspectable and reversible

It is intentionally not a media manager like Radarr or Bazarr — SyncOrbit assumes those already exist.

🏗 Architecture Overview

┌────────────┐
│   Browser  │
│   (UI)     │
└─────┬──────┘
      │ REST
┌─────▼──────┐
│  SyncOrbit │
│  Node.js   │
│  API + UI  │
└─────┬──────┘
      │
      ├─ SQLite DB  (library state, decisions, timestamps)
      ├─ Python tools
      │    ├─ batch_scan.py
      │    ├─ align.py
      |    |─ autocorrect.py
      │    └─ ffsubsync
      │
      └─ (optional)
           WhisperX service (separate container)

Key idea

  • SyncOrbit orchestrates
  • Python does the heavy lifting
  • WhisperX is isolated and optional

Components

  1. SyncOrbit (main container)

    • Node.js backend (API)
    • HTML / JS frontend
    • SQLite database
    • Coordinates batch jobs and single-movie actions
  2. Python tools

    • batch_scan.py – full library analysis
    • align.py – subtitle alignment + statistics
    • autocorrect.py - correct timing (also by segments)
    • ffsubsync – reference creation from audio
  3. WhisperX service (optional)

    • Separate container
    • Exposes a simple HTTP API
    • Generates ref.srt when requested
    • Runs asynchronously (does not block UI)

If WhisperX is not running, SyncOrbit continues to work normally.

Folder Structure (important)

/media
  /Movie Name (Year)
    Movie.mkv
    Movie.en.srt
    Movie.fi.srt
    folder.jpg

/app/data
  /analysis
    /Movie Name (Year)
      analysis.syncinfo
  /ref
    /Movie Name (Year)
      ref.srt
  /resync
    /Movie Name (Year)
      *.synced.srt
  /autocorrect
    /Movie Name (Year)
      *.corrected.srt
  ignore_list.json
  syncorbit.db

Decisions & References

Each movie ends up with:

  • decision

    • synced
    • needs_adjustment
    • unresolvable
  • best_reference

    • en
    • whisper
    • ffsync
  • reference_path

  • alignment statistics (anchors, drift, offsets)

Rule:

The newest valid reference wins — unless manually overridden.

TV Shows

SyncOrbit also manages TV libraries with a parallel tv_episodes table and a dedicated TV Shows tab (next to the Movies tab).

Directory structure — one row per episode, scanned from Show/Season N/:

/media_tv
  /Breaking Bad
    /Season 1
      Breaking Bad - S01E01.fi.srt   → row: "Breaking Bad - S01E01"
      Breaking Bad - S01E01.en.srt
    /Season 2
      ...
  • Loose/flat subtitle files not inside a Show/Season N/ path are ignored.
  • Add the TV mount and a TV_MEDIA_PATH env var (see Deploying on Unraid).
  • Run batch_scan.py --tv (or the Scan TV Shows button in the UI) to populate the table. All per-item and bulk actions (reanalyze, auto-correct, whisper, ffsubsync, ignore) work for episodes too.

UI Highlights

  • Fast library table (DB-backed)
  • Hover poster previews (folder.jpg)
  • Reference badges (EN / Whisper / FFSync)
  • Inline reanalyze buttons with live feedback
  • Bulk actions:
    • Ignore movies
    • Run FFSubSync
    • Touch / create Whisper references
  • Analysis graphs and statistics per movie
  • Every decision comes with a plain-language Why: explanation (see docs/ui.md)

Batch Workflow

Typical nightly flow:

  1. batch_scan.py (cron)
  2. Remove missing movies from DB
  3. Analyze new or changed subtitles
  4. Update decisions & stats
  5. UI reflects changes instantly

How I Actually Use SyncOrbit

SyncOrbit is not something I run once and forget.

I use it as a long-term maintenance tool for a curated movie library.

Here is my real, day-to-day workflow.

  1. Nightly batch scan (fully automatic)

    I run batch_scan.py on a schedule (cron).

    This does the heavy lifting:

    • scans the whole library
    • analyzes subtitle alignment
    • updates the database
    • removes movies that no longer exist on disk
    • skips ignored movies

    I don’t watch the output. I just let it run.

    SyncOrbit is designed so that nothing breaks if this takes hours.

  2. Open SyncOrbit and sort by “recent”

    When I open the UI, I sort the library by Finnish subtitle modification time.

    This immediately shows:

    • new movies
    • replaced releases
    • subtitles that were recently touched

    I almost never scroll the full list — I focus on what changed.

  3. Look at decisions, not files

    I don’t inspect subtitle files directly.

    Instead, I look at:

    • decision (synced, needs_adjustment, unresolvable)
    • the Why: explanation under it
    • anchor count
    • drift span
    • reference badge (EN / Whisper / FFSubSync)

    This tells me where attention is needed without guessing.

    Most movies are fine. I don’t touch them.

  4. Whisper only when needed

    Whisper references are expensive (slow, heavy).

    So my rule is simple:

    • If EN subtitles sync well → do nothing
    • If EN fails badly → create Whisper reference
    • Never delete Whisper references once created

    I use the bulk “Touch Whisper” action to:

    • reuse existing Whisper references
    • or request new ones in the background

    While Whisper runs, I continue using SyncOrbit normally.

  5. FFSubSync as a repair tool, not default

    FFSubSync is powerful but imperfect.

    I use it when:

    • Whisper alignment still struggles
    • timing drift looks systematic
    • I need a better EN-based reference

    FFSubSync results are explicit references, not magic fixes.

    They live alongside other references and can be compared later.

  6. Reanalyze single movies when something looks off

    If a movie stands out:

    • I click Reanalyze
    • SyncOrbit automatically picks the newest reference
    • Analysis updates without rerunning the whole library

    This is fast enough to use interactively.

  7. Ignore movies intentionally

    Sometimes a movie is:

    • missing subtitles
    • waiting for a better release
    • intentionally excluded

    I mark it as ignored.

    Ignored movies:

    • stay in the database
    • are visible but dimmed
    • are excluded from analysis until re-enabled

    This avoids repeated “false problems”.

  8. Trust the system, but verify visually

    If numbers look suspicious:

    • I open the analysis view
    • inspect offset graphs
    • confirm that subtitles feel right in playback

    SyncOrbit helps me decide where to look, not replace judgment.

The Core Insight

Most subtitle problems don’t need fixing — they need identification.

SyncOrbit exists to answer:

  • Which subtitles are good enough?
  • Which ones are worth the time?
  • What changed since last time?

Once those questions are answered, the rest is easy.

Getting Started (high level)

SyncOrbit builds and runs locally — no CI/CD or image registry required.

git clone https://github.com/velinea/syncorbit
cd syncorbit

# configure your media + data paths once
cp .env.example .env
# edit MEDIA_PATH and DATA_PATH in .env

# build + run
docker compose up -d --build

Deploying on Unraid

Unraid runs the same image it builds, so there is no reason to build elsewhere.

  1. One-time setup — clone the repo into a user share:

    git clone https://github.com/velinea/syncorbit /mnt/user/appdata/syncorbit
    
  2. Configure paths — create .env from .env.example and point MEDIA_PATH at your movies, TV_MEDIA_PATH at your TV library (e.g. /mnt/user/Media/Tv), and DATA_PATH at a persistent app-data folder.

    The TV path is mounted read-only into the container at /app/media_tv. When managing the container in the Unraid Docker UI (not compose), add a second Path mount: Container /app/media_tv, Host /mnt/user/Media/Tv, Read-Only.

  3. Update, build, restart — from the repo folder (or via the Unraid User Scripts plugin):

    ./deploy.sh
    

    deploy.sh pulls latest code, rebuilds the image, and recreates the container.

  4. Optional nightly batch scan — run a scheduled User Script:

    docker exec syncorbit /app/.venv/bin/python3 /app/python/batch_scan.py
    

You can either manage the container through the Unraid Docker UI (point the template at the locally built syncorbit:latest image) or use Unraid's Compose Manager. The included docker-compose.yml supports both.

Optional: WhisperX Integration

  • Run WhisperX as a separate container
  • Same Docker network as SyncOrbit
  • SyncOrbit calls it only when needed
  • Long jobs run in the background

WhisperX container mounts

WhisperX receives the video path exactly as SyncOrbit sees it internally, so it must mount the same media paths at the same container-internal locations as the SyncOrbit container:

  • /app/media → your Movies host path (e.g. /mnt/user/Media/Movies)
  • /app/media_tv → your TV host path (e.g. /mnt/user/Media/Tv)

It also needs /app/data pointing at the same SyncOrbit appdata host path, so the generated ref.srt lands where SyncOrbit reads it. If a media mount is missing, Whisper touch_whisper reports no_video_found (or can't run) even though the network is fine.

This avoids:

  • UI blocking
  • Dependency hell
  • GPU coupling

What SyncOrbit Is Not

  • Not a subtitle downloader
  • Not a media manager
  • Not real-time
  • Not cloud-based

It is a library maintenance tool.

Project Status

  • Actively used
  • Architecture stabilized
  • UX still evolving
  • Not yet “one-click install”

Expect iteration.

License

MIT

Credits & Inspiration

  • FFSubSync
  • Whisper / WhisperX
  • Radarr / Bazarr philosophy
  • Plex UX patterns