Out-of-sync subtitle files are one of the most persistent paper cuts in personal media playback. Three failure modes account for the vast majority of bad timing:
- Constant offset. The whole track is N seconds early or late.
- Linear drift. Subtitles authored against one video master no longer match because the user has a different cut, a different framerate, or a release with extra ads — the gap between subtitle and audio grows (or shrinks) as the file progresses.
- Framerate mismatch. The classic 23.976 ↔ 25 fps PAL/NTSC speed-up: timestamps need
to be scaled by
target_fps / source_fps.
A correct subtitle fix usually requires either knowing both anchor points (when the first and last cue should appear) or knowing the framerate ratio.
- r/PleX, r/DataHoarder, OpenSubtitles forum threads recur weekly asking how to fix drift when shifting alone doesn't help. Sources confirmed via web search (May 2026).
- Existing CLI tools are fragmented and dependency-heavy:
ngerritsen/subshift— only millisecond shift, no linear/fps.spion/subsync— linear interpolation only, npm package, requires Node.antoniomacri/subtitle-resync— Python library, not a user-facing CLI binary.srt(Python) — library for shift only; user must script the rest.
- Heavyweight alternatives like Subtitle Edit (Windows GUI, .NET) or Aegisub (cross-platform GUI) work but are overkill for a one-shot CLI fix.
- Online tools (SubShifter, SubtitleTools, HappyScribe) require uploading the file — privacy-hostile and impossible to script.
The gap: no single dependency-free CLI binary that covers shift + 2-anchor linear + framerate conversion + safe preview, all from one tool.
- Linux/macOS power users running media servers (Plex, Jellyfin) who fix subtitles by hand and want a tool they can drop into a shell pipeline.
- Translators and fansubbers doing release matching across different cuts.
- Anyone writing a script (
find . -name '*.srt' -exec srtune ...) to batch-correct an archive of subtitles.
| Tool | Modes | Deps | CLI? | Cross-platform |
|---|---|---|---|---|
| Subtitle Edit | all + more | .NET | partial | no (Windows) |
| Aegisub | all + more | wx | no | yes (GUI) |
| subshift (npm) | ms shift | Node | yes | yes |
| subsync (npm) | linear | Node | yes | yes |
| antoniomacri/subtitle-resync | linear | Python lib | no | yes |
srt (PyPI) |
shift | Python | partial | yes |
| SubShifter (web) | shift, linear | server | no | yes (browser) |
srtune fills the cell: all three modes, single Python file, zero dependencies, CLI.
A single-file Python 3 CLI (srtune.py) with subcommands:
srtune shift <file> --by <duration>— constant offset (positive or negative).srtune linear <file> --first <time> --last <time>— compute affine transform from two anchor points and apply.srtune fps <file> --from <fps> --to <fps>— scale by framerate ratio.srtune info <file>— show cue count, first/last timestamps, encoding, line endings.
All write subcommands support --dry-run (preview transform), -o/--output (target
path, default in-place backup), and accept timestamps in flexible formats
(HH:MM:SS,mmm / MM:SS / 1500ms / 1.5s).
- No paid APIs, no hardware — pure-Python stdlib only (
argparse,re,dataclasses,pathlib,unittest). - MVP scope ≤ 500 lines — comfortably fits one session.
- Testable without real video — math correctness is provable from synthetic SRTs.
- Real users today — the demand is documented in forum threads and the spread of partial tools.
Risk: the SRT format has plenty of in-the-wild quirks (BOM, CRLF, malformed indices, overlapping cues, non-monotonic timestamps). The parser must tolerate these without silently corrupting files. Mitigated by an explicit lenient-parser strategy plus a fixture-based test suite.