"""Validate plain UTF-8 SRT cue timing; do not modify the source file.""" from pathlib import Path import re import sys STAMP = r"(\d{2,}):(\d{2}):(\d{2}),(\d{3})" TIMING = re.compile(rf"^{STAMP}\s*-->\s*{STAMP}$") def milliseconds(parts): hours, minutes, seconds, fraction = map(int, parts) if minutes > 59 or seconds > 59: raise ValueError("minutes and seconds must be 00-59") return ((hours * 60 + minutes) * 60 + seconds) * 1000 + fraction def check(path): text = path.read_text(encoding="utf-8-sig") blocks = re.split(r"\n\s*\n", text.replace("\r\n", "\n").strip()) if not text.strip(): raise ValueError("file is empty") errors = [] warnings = [] previous_end = None for number, block in enumerate(blocks, 1): lines = block.splitlines() if len(lines) < 3 or not lines[0].isdigit(): errors.append(f"Cue {number}: expected numeric index, timing and text") continue match = TIMING.fullmatch(lines[1].strip()) if not match: errors.append(f"Cue {number}: expected HH:MM:SS,mmm --> HH:MM:SS,mmm") continue try: start = milliseconds(match.groups()[:4]) end = milliseconds(match.groups()[4:]) except ValueError as exc: errors.append(f"Cue {number}: {exc}") continue if end <= start: errors.append(f"Cue {number}: end must be later than start") if previous_end is not None and start < previous_end: warnings.append(f"Cue {number}: overlaps or precedes the previous cue") previous_end = end for message in errors: print("ERROR " + message) for message in warnings: print("WARN " + message) if errors: return 1 print(f"PASS {len(blocks)} cues; basic timing is valid") return 0 if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python3 check-srt-timing.py captions.srt", file=sys.stderr) sys.exit(2) try: sys.exit(check(Path(sys.argv[1]))) except (OSError, UnicodeError, ValueError) as exc: print(f"ERROR {exc}", file=sys.stderr) sys.exit(2)