Per-cue subtitle resynchronization: neural VAD + piecewise alignment + optional ASR word anchors, with a batch GUI.
Project description
SubResync
Fix out-of-sync subtitles - every sentence, not just one global shift.
SubResync re-times subtitle files against the actual speech in your video files. Unlike most tools, which find a single time-delta and shift everything, SubResync builds a piecewise correction map - it finds ad-break jumps, framerate mis-speeds and gradual drift, then fine-tunes each cue individually onto the detected speech. It works with any subtitle language against any audio language (Hebrew or Russian subtitles on English audio are first-class citizens), because the core engine matches the rhythm of speech, not the words.
Highlights
- Per-sentence correction - global offset + framerate fix, then piecewise segments (ad breaks / re-edits), then a per-cue snap onto speech onsets. Optional word-level refinement via speech recognition.
- Batch a whole folder - drop a season directory into the GUI; videos and
subtitles are paired automatically (name stems,
S01E03patterns, language suffixes like.he.srt). - Any formats - MKV/MP4/AVI/TS/… via FFmpeg; SRT/ASS/SSA/VTT/… via pysubs2, with automatic encoding detection (Hebrew cp1255, Cyrillic cp1251, UTF-8/16 all handled).
- Can't make it worse - every computed variant is scored against the audio; if nothing beats the original file, the original is left untouched and the file is flagged for review.
- SDH-aware -
[music],(sighs), speaker labels are excluded from alignment (they have no audio correlate) but still re-timed in the output. - Dual-audio aware - releases with an untagged dub track (e.g. Russian dub + English original in one AVI) are handled: when the detected audio language doesn't match the subtitle, each track is probed with Whisper language-ID and the matching one is used for word-level features.
- GPU-accelerated ASR - Max-precision mode and
check --asruse the NVIDIA GPU automatically when available (~20-40× realtime on an RTX 3070), falling back to CPU otherwise. - See what it's doing - the GUI has a live activity log (every stage of every file), a CPU/GPU compute indicator, a per-file Match score and a Sync column (verdict + residual in ms). Double-click any finished row to preview the before→after retiming, apply a manual nudge, or undo. See docs/SCORING.md.
- Zero setup - FFmpeg and the Silero VAD model download automatically on first run. No admin rights needed.
Install & run
Just want the app, no Python? Download SubResync-Setup-<version>.exe
from the latest release,
run it (no admin rights needed - it installs for your user only), and launch
SubResync from the Start Menu like any other program. That's the whole setup -
Max-precision mode works out of the box on CPU; if a GPU is detected, the app
offers a one-time ~2 GB download (status bar - "Enable GPU acceleration") to
speed it up. Building it yourself: build_exe.ps1 +
build_installer.ps1 (needs
Inno Setup).
Developing / running from source:
# from the project folder
py -3.11 -m venv .venv
.venv\Scripts\python -m pip install -e .[gui,asr,dev]
# optional: GPU acceleration for Max mode / --asr (no CUDA Toolkit needed)
.venv\Scripts\python -m pip install -e .[cuda]
# ...or, from a plain [asr] install, let the app fetch the same libs itself:
.venv\Scripts\subresync setup --gpu
# GUI
.venv\Scripts\subresync-gui # or double-click SubResync.bat
# CLI
.venv\Scripts\subresync setup # pre-download FFmpeg/model, show GPU status
.venv\Scripts\subresync sync "D:\TV\Show Season 1" # whole folder
.venv\Scripts\subresync sync video.mkv subs.srt # single pair
.venv\Scripts\subresync sync "D:\TV\Season 1" --mode auto # Balanced, escalate hard files
.venv\Scripts\subresync sync . --mode max --json # always run ASR
.venv\Scripts\subresync pairs "D:\Movies" # preview pairing
.venv\Scripts\subresync check "D:\TV\Show Season 1" # verify sync quality
.venv\Scripts\subresync check . --synced # verify the outputs
.venv\Scripts\subresync check video.mkv subs.srt --asr # word-level check
check measures the local agreement between the subtitle and the actual
speech in every ~60 s window of the timeline and grades each window
(ok / loose / off), so you can see exactly which minutes of a file are out of
sync and by how much. With --asr it additionally transcribes the audio and
verifies that each cue's words are spoken where the cue says (same-language
subtitles only) - the strictest possible ground-truth check.
Output goes next to the video as <video-name>.synced.<lang>.srt (original
files are never touched unless you enable overwrite, which keeps a .bak).
Re-running a folder never deletes previous outputs - a second pass writes
…synced.<lang>.v2.srt, …v3.srt, and so on, so you can compare a stronger
mode against an earlier run. Each run writes one timestamped HTML report
(subresync-report-<time>.html) with every file's offsets, segments, scores
and residuals - open it in the app (the Report button) or any browser, or
ignore it. (No more one .sync.json per episode.)
Modes
| Mode | Stages | When |
|---|---|---|
| Fast | global + piecewise | Quick pass, large libraries |
| Balanced | + per-cue snap | Solid default, no speech recognition |
| Auto (recommended) | Balanced, then Max only on still-off files | Best quality-per-time on a library |
| Max precision | always run speech-recognition word anchors | Same-language subs, worst cases |
Auto runs Balanced on everything, then automatically re-runs only the files that are still out of sync in Max - so you get Max quality on the hard files without paying the speech-recognition cost on the easy ones. Max (and the Max pass inside Auto) automatically falls back to Balanced when the subtitle language doesn't match the audio language (e.g. Hebrew subs on English audio) - that is by design; cross-language sync uses speech rhythm only.
How it works
- Extract & detect speech. FFmpeg decodes the best audio track to 16 kHz mono; Silero VAD (neural, ONNX) produces a speech-probability signal on a 10 ms grid. Cached by content hash - re-runs are instant.
- Global fit. FFT cross-correlation finds the dominant offset and tests framerate ratios (23.976↔25 etc.), with golden-section refinement.
- Piecewise fit. Sliding windows measure the local residual offset; confident windows become anchors; jumps open new segments. Segment boundaries are re-optimized over cue-order splits with a collision penalty - this is what fixes ad-break / director's-cut discontinuities.
- Per-cue snap. Each sentence is nudged (±0.6 s) to maximize speech coverage inside the cue and minimize it just outside, with outlier clamping against neighbors.
- (Max mode) Word anchors. faster-whisper transcribes with word timestamps (GPU when available); cue texts are fuzzy-matched to the transcript; matched cues pin to the exact spoken word, the rest interpolate. If the audio language doesn't match the subtitle and the file has more audio tracks, each track is probed and the matching one is used.
- Verify & write. All variants are scored against the audio signal; the best one is written in the original subtitle format (UTF-8).
Project layout
For a full guided tour (every file's responsibility, the data flow through the pipeline, a "where do I change X" index), see ARCHITECTURE.md.
src/subresync/
config.py settings & mode presets
pipeline.py stage orchestration, quality gates, auto-escalation
verify.py sync-quality checker (windowed + word-level)
report.py one consolidated HTML report per run
pairing.py video<->subtitle discovery (stems, S01E01, languages)
batch.py parallel batch engine
cli.py command line
gpu.py CUDA DLL registration, GPU detection + on-demand cuBLAS/cuDNN download
certs.py TLS fix for antivirus HTTPS interception (Avast etc.)
media/ ffmpeg locate/download/extract, signal cache
subtitles/ format I/O, encoding detection, SDH classifier
vad/ Silero ONNX VAD + energy fallback
align/ global FFT fit, piecewise segments, per-cue snap,
ASR word anchors, final warp
gui/ PySide6 app (drag-drop, activity log, CPU/GPU chip, scores)
tests/ synthetic-warp recovery tests + unit tests
scripts/ bench_modes.py - score all modes on one pair
docs/PLAN.md architecture & design decisions
docs/SCORING.md what the match score means
docs/COMPARISON.md modes vs each other, and vs other tools
LICENSE MIT
THIRD_PARTY_NOTICES.md dependencies + prior-art acknowledgments
Development
.venv\Scripts\python -m pytest # test suite
The synthetic tests generate known desyncs (offset, framerate, mid-file break, per-cue jitter) and assert the engine recovers them to within tens of milliseconds. An end-to-end harness with real (TTS) speech lives in the dev scratchpad; median recovery error there is ~64 ms with a 9 s mid-file break correctly detected and placed.
Real-world validation
Measured on a full TV season (23 × 43-min AVI episodes, dual-audio release with a Russian dub + English original, English subtitles from a different edit):
- Every episode required piecewise correction: per-episode global offsets from −9.9 s to +3.7 s plus 2–15 local segments each. One episode's desync was a staircase (−10 s → −2 s in ~2 s steps at commercial-break cuts) - unfixable by any single-delta tool.
check --syncedon all outputs: median residuals 0–25 ms, every file graded in-sync / mostly-in-sync.- Word-level ground truth (
check --asr, GPU): 514/588 cues found verbatim in the audio at their subtitle position, median timing error 206 ms - for files synced by rhythm alone.
How it compares
On S02E01, the best possible single global correction (what global-only tools do) reaches match 0.66 and leaves 13 segments still 1–2 s out; SubResync's piecewise + per-cue reach 0.75 / 20 ms median / 94 % of windows in sync. SubResync is the only open-source tool that combines global + framerate + piecewise + per-cue correction with optional ASR anchoring and a "can't-make-it-worse" guarantee. Full breakdown and a capability matrix vs ffsubsync / alass / subaligner / WhisperX / AutoSubSync / subsyncarr: docs/COMPARISON.md.
License
MIT - see LICENSE. All engine code is original; the alignment approach was inspired by prior open-source work (ffsubsync, alass, WhisperX, subaligner) but no code was copied, so the GPL terms of alass/AutoSubSync do not apply. Dependency licenses and acknowledgments: THIRD_PARTY_NOTICES.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file subresync_app-0.1.2.tar.gz.
File metadata
- Download URL: subresync_app-0.1.2.tar.gz
- Upload date:
- Size: 65.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
057b21c94a4407dd3ec1e9ac3512fbbd51a80196369b256cf35b8019bd39d8de
|
|
| MD5 |
7c9a80d32298ca7b0eb3b63da5701750
|
|
| BLAKE2b-256 |
672066996f838fd4f5e50ebf67b6ad49c5114ba0b15b65f8af572a10696f7f32
|
Provenance
The following attestation bundles were made for subresync_app-0.1.2.tar.gz:
Publisher:
release.yml on grisha1998/subresync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
subresync_app-0.1.2.tar.gz -
Subject digest:
057b21c94a4407dd3ec1e9ac3512fbbd51a80196369b256cf35b8019bd39d8de - Sigstore transparency entry: 2252881856
- Sigstore integration time:
-
Permalink:
grisha1998/subresync@84b086aca94b9666554e9dea7582e9668c1ab661 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/grisha1998
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@84b086aca94b9666554e9dea7582e9668c1ab661 -
Trigger Event:
push
-
Statement type:
File details
Details for the file subresync_app-0.1.2-py3-none-any.whl.
File metadata
- Download URL: subresync_app-0.1.2-py3-none-any.whl
- Upload date:
- Size: 68.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e0f6c3fc37114d07ce79a6b5284c10de6d13c1255a156a176f638fb5e627024
|
|
| MD5 |
8562919772c241c8d10cdc39cb652a29
|
|
| BLAKE2b-256 |
331345d140d93664b0ce44b987782b8e439c1ffeab0a4088eb2159c27d09467a
|
Provenance
The following attestation bundles were made for subresync_app-0.1.2-py3-none-any.whl:
Publisher:
release.yml on grisha1998/subresync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
subresync_app-0.1.2-py3-none-any.whl -
Subject digest:
3e0f6c3fc37114d07ce79a6b5284c10de6d13c1255a156a176f638fb5e627024 - Sigstore transparency entry: 2252882409
- Sigstore integration time:
-
Permalink:
grisha1998/subresync@84b086aca94b9666554e9dea7582e9668c1ab661 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/grisha1998
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@84b086aca94b9666554e9dea7582e9668c1ab661 -
Trigger Event:
push
-
Statement type: