v0.1.0 — Initial release
`submatch` verifies that a subtitle file actually matches the audio content of a video — catching the case where subtitle tools like subliminal or Bazarr return correctly-timed but wrong-content subtitles.
Install
```bash
pip install submatch
```
System dependencies: `ffmpeg` (`brew install ffmpeg`) and `ffsubsync` (`pip install ffsubsync`).
What's in this release
Core
- Transcribes short audio segments with Whisper and scores against subtitle text using token F1
- Dialogue-density segment sampling — picks the 30s windows with the most subtitle words per zone, skipping intros/credits
- Timing drift detection via ffsubsync, flagging offsets > 2s
- Three language signals: Whisper audio language, langdetect on subtitle text, filename convention + ffprobe metadata
- 4-state result system:
PASS,DRIFT(content matches but timing drift detected),FAIL(wrong content),UNSURE(insufficient transcription data) --resync: auto-correct drift in place on DRIFT;--pass-unsure: exit 0 for UNSURE results--keep-synced: save the timing-corrected subtitle to disk;--delete-failures: remove subtitle files that fail the match check
Cross-language matching
When subtitle and audio languages differ (e.g. English audio + Portuguese subtitles), scoring automatically switches from token F1 to multilingual semantic similarity via paraphrase-multilingual-MiniLM-L12-v2. Use --cross-threshold to tune the cutoff independently.
Batch mode
- Pair a directory of videos with their same-stem subtitles, or score one video against a subtitle directory
--recursive/-rfor Plex/Kodi/Jellyfin nested library layouts--sub-lang CODEto filter by language tag (e.g.pt,en,pt-BR)--filter GLOBto filter by filename pattern--workersfor parallel processing;--deviceto target CPU, MPS (Apple Silicon), or CUDA- Live progress with ETA, in-place result lines, and
--compactone-line-per-pair summary
Subtitle formats
SRT, WebVTT, and ASS/SSA — via pysubs2.
Output
Human-readable with ANSI colour, or --json for machine-readable output. Transcription results are cached per video so re-runs against a different subtitle skip re-transcription.
States and exit codes
| State | Meaning | Exit code |
|---|---|---|
PASS |
Content matches, no timing drift | 0 |
DRIFT |
Content matches, but timing drift detected | 1 (use --resync to fix in place) |
FAIL |
Content does not match | 1 |
UNSURE |
Not enough transcription data to decide | 1 (use --pass-unsure to exit 0) |
| — | Error (missing dependency, unreadable file, no audio track) | 2 |