Fix out-of-sync
.srtsubtitle files from the command line. Three modes, one file, zero dependencies.
Subtitle timing breaks for three boring reasons:
- Constant offset — the whole track is N seconds early or late.
- Linear drift — small at the start, larger at the end. Different cut, different release, framerate mismatch.
- 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
# 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/srtuneOr clone:
git clone https://github.com/asech109/srtune.git
cd srtune
python3 srtune.py --helpNo pip install. No virtualenv. Pure Python 3.9+ standard library.
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 |
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,000This 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,000srtune fps movie.srt --from 23.976 --to 25 # PAL speed-up
srtune fps movie.srt --from 25 --to 23.976 # reverse$ 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--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.
| 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 |
- Encoding-aware. Detects UTF-8 with or without BOM, falls back to CP1252,
then Latin-1. Use
--encodingfor 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.bakautomatically. - Integer milliseconds internally. No float drift over a 3-hour timeline.
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)
python3 -m unittest test_srtune.py40 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.
- SRT only. No WebVTT, no ASS/SSA. WebVTT support is on the roadmap behind a
--vttflag. - No speech-recognition-based auto-resync (a different problem).
- Encoding auto-detect covers UTF-8 / CP1252 / Latin-1. Anything else needs
--encoding.
MIT.