Skip to content
asech109Public

About

Fix out-of-sync .srt subtitles from the CLI. Constant shift, two-anchor linear correction, and framerate conversion in one zero-dependency Python script.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

srtune

Fix out-of-sync .srt subtitle files from the command line. Three modes, one file, zero dependencies.

Subtitle timing breaks for three boring reasons:

  1. Constant offset — the whole track is N seconds early or late.
  2. Linear drift — small at the start, larger at the end. Different cut, different release, framerate mismatch.
  3. Framerate change — the classic 23.976 ↔ 25 fps PAL/NTSC speed-up.

Most CLI tools handle one of those and only one. The full-featured fixers (Subtitle Edit, Aegisub) are GUIs. Online services demand you upload your file. srtune is a single Python script that covers all three modes, runs anywhere Python 3.9+ is installed, and refuses to corrupt your file on the way out.

srtune shift   movie.srt --by -2.5s
srtune linear  movie.srt --first 00:00:01,000 --last 01:39:00,000
srtune fps     movie.srt --from 23.976 --to 25
srtune info    movie.srt

Install

# Drop the single script anywhere on your PATH:
curl -fsSL https://raw.githubusercontent.com/asech109/srtune/main/srtune.py -o ~/.local/bin/srtune
chmod +x ~/.local/bin/srtune

Or clone:

git clone https://github.com/asech109/srtune.git
cd srtune
python3 srtune.py --help

No pip install. No virtualenv. Pure Python 3.9+ standard library.

Tour

1. shift — constant offset

The whole track is 2.5 seconds late:

srtune shift movie.srt --by -2.5s

--by accepts any of:

Form Meaning
+2s, -1.5s seconds, optional sign
1500ms milliseconds
2m minutes
00:00:01,500 SRT-style timestamp
12345 bare integer milliseconds

2. linear — two-anchor correction (handles drift)

The killer feature. You watched the first cue and you know it should start at 00:00:01,000. You scrubbed to the end and the last cue should land at 01:39:00,000. srtune computes the affine map f(t) = a·t + b that hits both anchors and applies it to every cue:

srtune linear movie.srt --first 00:00:01,000 --last 01:39:00,000

This is strictly more powerful than a constant shift — it corrects offset and drift in one pass.

For the rare case where the anchors aren't the first/last cue:

srtune linear movie.srt --src1 00:00:05,000 --dst1 00:00:07,500 \
                       --src2 01:30:00,000 --dst2 01:32:30,000

3. fps — framerate ratio

srtune fps movie.srt --from 23.976 --to 25     # PAL speed-up
srtune fps movie.srt --from 25 --to 23.976     # reverse

4. info — inspect a file without touching it

$ srtune info movie.srt
file: movie.srt
encoding: utf-8 (BOM)
line endings: CRLF
cues: 1247
first cue: 00:00:01,234 --> 00:00:03,891
last cue:  01:42:18,500 --> 01:42:21,700
span: 01:42:20,466

Preview before committing

--dry-run works on every write command:

$ srtune linear movie.srt --first 00:00:01,000 --last 01:39:00,000 --dry-run
Preview (dry-run, no file written)
  BEFORE:
    #1  00:00:02,000 --> 00:00:05,000  First line should appear at 00:00:01,000
    #2  00:00:30,000 --> 00:00:33,000  Mid-track cue.
    #3  00:01:00,000 --> 00:01:03,000  Should be 00:00:50,000 — slight drift.
    #4  00:02:00,000 --> 00:02:03,000  Should be 00:01:39,000 — drift grows.
  AFTER :
    #1  00:00:01,000 --> 00:00:03,492  First line should appear at 00:00:01,000
    #2  00:00:24,254 --> 00:00:26,746  Mid-track cue.
    #3  00:00:49,169 --> 00:00:51,661  Should be 00:00:50,000 — slight drift.
    #4  00:01:39,000 --> 00:01:41,492  Should be 00:01:39,000 — drift grows.

In-place edits always create FILE.bak next to the original. Pass -o OUTPUT to write somewhere else and skip the backup.

Why not just use…?

Tool Shift Linear FPS CLI Deps
srtune ✅ ✅ ✅ ✅ none
Subtitle Edit ✅ ✅ ✅ partial .NET, Windows GUI
Aegisub ✅ ✅ ✅ no wxWidgets GUI
subshift (npm) ✅ ❌ ❌ ✅ Node
subsync (npm) ❌ ✅ ❌ ✅ Node
srt (PyPI) ✅ ❌ ❌ partial Python pkg
SubShifter (web) ✅ ✅ ❌ ❌ upload your file

Robustness

  • Encoding-aware. Detects UTF-8 with or without BOM, falls back to CP1252, then Latin-1. Use --encoding for anything else. The original encoding and BOM are preserved on write.
  • Line-ending preserving. CRLF in, CRLF out. LF in, LF out.
  • Lenient parser. Malformed blocks are skipped with a warning rather than blowing up the file.
  • Always backs up. In-place writes create FILE.bak automatically.
  • Integer milliseconds internally. No float drift over a 3-hour timeline.

Architecture

Four-layer, single file (~475 lines).

CLI (argparse, I/O, encoding detection, dry-run preview)
    ↓
Transformations (shift, linear, linear_anchors, fps_scale — pure functions)
    ↓
Time arithmetic (parse_timestamp, format_timestamp — integer ms)
    ↓
SRT parser & writer (lenient block parser, payload kept verbatim)

Development

python3 -m unittest test_srtune.py

40 unit tests in <10 ms. Coverage:

  • Timestamp parsing (every accepted form + error paths)
  • SRT parser (clean, CRLF, missing indices, malformed block, multi-line payload)
  • Transformations (shift / linear / fps, edge cases, degenerate inputs)
  • Round-trip rendering (text preserved verbatim, sequential renumbering, CRLF)
  • Encoding (BOM, CP1252 fallback)
  • CLI integration (every subcommand, every failure path)

See DISCOVERY.md, DESIGN.md, and VALIDATION.md for the full design notes.

Limitations

  • SRT only. No WebVTT, no ASS/SSA. WebVTT support is on the roadmap behind a --vtt flag.
  • No speech-recognition-based auto-resync (a different problem).
  • Encoding auto-detect covers UTF-8 / CP1252 / Latin-1. Anything else needs --encoding.

License

MIT.

About

Fix out-of-sync .srt subtitles from the CLI. Constant shift, two-anchor linear correction, and framerate conversion in one zero-dependency Python script.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages