The opening of a caption tutorial shows “glaze,” and so does the final playback. At least that’s what you want. If the ending still says “glass” after you fixed the opening, a quick search for “glaze” won’t catch it. And “glass” belongs in the middle of the file, where you demonstrate the original error.
Give the check two ranges, one for the opening and one for the ending. Ask for the corrected cue exactly once in each. Leave the demonstration outside both ranges so you can change that part of the tutorial without breaking the check.
Disclosure: AI assisted the drafting of this tutorial. The included script and fixture were run locally.
Set up the fixture
Use a plain UTF-8 SRT export. The following example is a small local fixture, independent of a video project. Save it as sample.srt.
1 00:00:00,300 --> 00:00:01,900 Glaze. 2 00:00:03,200 --> 00:00:05,100 Start with the raw clip. 3 00:00:19,200 --> 00:00:20,600 Glass. 4 00:00:42,400 --> 00:00:44,100 Glaze. In this fixture the opening copy is entirely inside the first two seconds. The final segment starts at 42 seconds. Those are test values. On an actual job, use the times of the cut you are handing off. A source timecode from the raw recording will miss the shifted edit.
Compare the text inside the correct windows
This script is dependency-free and runs on Python 3.9 or newer. It uses ordinary SRT numbering, blank lines, and comma-separated timestamps. It reads a UTF-8 BOM and supports multiple text lines within a cue. The expected phrase is compared to the whole cue after normalizing case and punctuation, so a caption containing additional words will fail the test. That choice is useful for a one-line fixture and may be too strict for a longer subtitle.
import argparse from dataclasses import dataclass from pathlib import Path import re import sys TIMESTAMP = r"[0-9]{2,}:[0-5][0-9]:[0-5][0-9],[0-9]{3}" CUE_TIMES = re.compile(rf"({TIMESTAMP})\s+-->\s+({TIMESTAMP})") @dataclass(frozen=True) class Cue: start: int end: int text: str def milliseconds(timestamp): hrs, mins, secs, ms = map(int, re.split(r"[:,]", timestamp)) return ((hrs * 60 + mins) * 60 + secs) * 1000 + ms def read_cues(path): text = path.read_text(encoding="utf-8-sig").strip() if not text: raise ValueError("empty subtitle file") cues = [] for block in re.split(r"\n[ \t]*\n+", text): lines = block.splitlines() if len(lines) < 3 or not lines[0].strip().isdecimal(): raise ValueError("expected cue number, time range and text") match = CUE_TIMES.fullmatch(lines[1].strip()) if match is None: raise ValueError(f"invalid time range for cue {lines[0]}") start, end = map(milliseconds, match.groups()) if end <= start: raise ValueError(f"nonpositive duration for cue {lines[0]}") cues.append(Cue(start, end, " ".join(lines[2:]))) return cues def normalized(line): return " ".join(re.findall(r"[\w']+", line.casefold())) def review(cues, expected, opening_end, closing_start): expected = normalized(expected) if not expected: raise ValueError("expected text must contain words") if not 0 < opening_end < closing_start: raise ValueError("use nonoverlapping opening and closing ranges") opening = [cue for cue in cues if 0 <= cue.start < cue.end <= opening_end] closing = [cue for cue in cues if closing_start <= cue.start] for label, candidates in (("opening", opening), ("closing", closing)): exact = [cue for cue in candidates if normalized(cue.text) == expected] if len(exact) != 1: print(f"CHECK {label}: {len(exact)} exact matches for '{expected}'") return False print(f"PASS opening and closing each contain one cue for '{expected}'") return True def main(): parser = argparse.ArgumentParser() parser.add_argument("srt", type=Path) parser.add_argument("expected") parser.add_argument("--opening-end", type=int, required=True, help="milliseconds") parser.add_argument("--closing-start", type=int, required=True, help="milliseconds") args = parser.parse_args() try: ok = review(read_cues(args.srt), args.expected, args.opening_end, args.closing_start) except (OSError, ValueError) as exc: print(f"ERROR {exc}", file=sys.stderr) return 2 return 0 if ok else 1 if __name__ == "__main__": raise SystemExit(main()) Run a passing and a failing example
python check_cues.py sample.srt "Glaze" --opening-end 2000 --closing-start 42000 The fixture prints:
PASS opening and closing each contain one cue for 'glaze' Now duplicate the file as stale.srt and change only the last cue text to “Glass.” Repeat the command with stale.srt. It reports CHECK closing: 0 exact matches for 'glaze' and exits with code 1. The earlier wrong caption in the tutorial remains outside the checked ranges, so it never affects either test. A malformed cue exits with code 2. In a build script those codes can distinguish an expected review failure from a fixture that needs repair.
Check the export as well
After the script passes, open the actual export. It can still have the wrong SRT attached, the line can come in early, or the playback controls can cover the word. Play the opening and the end from the rendered media and listen across both boundaries. At phone size, inspect the lower area for any collision between sample captions, a tutorial label and the video player.
Some projects intentionally split the phrase across two cues. In that case set the expected text for each opening and closing cue or define a richer rule for adjacent cues. Avoid running a global substring search to compensate. It would hide the stale-copy failure this check exists to catch.

