Blog/Validate SRT subtitle timing before importing captions
OverviewAll posts
Tutorial

Validate SRT subtitle timing before importing captions

Check SRT cue intervals, media duration and timeline alignment with a tested Python example, then review caption playback in your destination tool.

VTornado API team
Covered in this article
Validate a restricted SRT timing profile
Separate overlap warnings from invalid intervals
Pair subtitles with the exact media revision
7 min reading time
Published October 17, 2026
VProduct guides by Velys Software

Before importing an SRT subtitle file, check that it parses under your consumer's rules, that its cue intervals fit the exact media file, and that its timing matches the speech. These are separate checks. A file can have valid timestamps and still belong to a different edit of the video.

This guide provides a small Python timing check for a deliberately restricted SRT profile. Use it as an application gate before a caption, translation or dubbing handoff, then review playback in the actual destination tool.

Identify the artifact before parsing it

Keep the subtitle file paired with the exact audio or video revision it describes. Record language, origin, retrieval time and whether the text was generated automatically or reviewed. A filename alone is a weak association: an editor can replace a video without changing its name.

Tornado's job-status reference describes an optional subtitle_url when subtitles are available. Treat that as a delivery reference, not proof that a track exists for every source or that its contents are SRT. Inspect the returned artifact and use a parser appropriate to its format. A missing subtitle reference does not justify inventing caption text or declaring the main media unusable.

Keep temporary access URLs out of public fixtures and reports. Store stable artifact identity separately, as described in the media storage guide. If access fails, diagnose the delivery before diagnosing subtitle syntax.

Define the format profile your importer accepts

The Library of Congress description of SRT describes numbered text blocks with start and end timestamps, subtitle text and blank-line separators. Its timestamp notation uses a comma before milliseconds. It also notes the absence of one universal character encoding and differences in formatting support.

Agree on encoding at your own boundary. For a workflow that requires UTF-8, decode with utf-8-sig to handle an optional UTF-8 byte-order mark, and reject decoding failures instead of silently replacing characters. Preserve the original bytes for a controlled repair workflow. Do not assume every SRT file found elsewhere uses that encoding.

The example below accepts sequential indices beginning at one, two-digit hours, conventional timing lines and nonempty text. It normalizes common line endings. It intentionally rejects other layouts; rejection means the file does not fit this profile, not that every subtitle player would reject it. WebVTT, ASS and XML need separate handling rather than a renamed .srt extension.

Check intervals against the delivered media

Use an observed duration for the exact artifact the importer will receive. The script accepts that duration as an integer number of milliseconds; it does not measure a media file. If your inspection tool reports a duration with rounding uncertainty, define a justified tolerance in your application instead of borrowing a universal margin.

import re

STAMP = r"([0-9]{2}):([0-5][0-9]):([0-5][0-9]),([0-9]{3})"
TIMING = re.compile(STAMP + r" --> " + STAMP)


def milliseconds(parts):
    hours, minutes, seconds, millis = map(int, parts)
    return ((hours * 60 + minutes) * 60 + seconds) * 1000 + millis


def inspect_srt(text, duration_ms):
    if type(duration_ms) is not int or duration_ms <= 0:
        raise ValueError("Use a positive duration in integer milliseconds")
    text = text.removeprefix("\ufeff").replace("\r\n", "\n").replace("\r", "\n")
    text = text.strip()
    if not text:
        raise ValueError("Empty subtitle file")
    cues, warnings = [], []
    latest_end = 0
    for number, block in enumerate(re.split(r"\n[ \t]*\n+", text), 1):
        lines = block.splitlines()
        if len(lines) < 3 or lines[0] != str(number) or not "".join(lines[2:]).strip():
            raise ValueError(f"Cue {number}: expected sequential index and text")
        match = TIMING.fullmatch(lines[1])
        if match is None:
            raise ValueError(f"Cue {number}: unsupported timing syntax")
        start = milliseconds(match.groups()[:4])
        end = milliseconds(match.groups()[4:])
        if not 0 <= start < end <= duration_ms:
            raise ValueError(f"Cue {number}: interval outside media bounds")
        if cues and start < cues[-1][0]:
            raise ValueError(f"Cue {number}: start times are out of order")
        if start < latest_end:
            warnings.append(f"Cue {number}: overlaps an earlier cue")
        cues.append((start, end, "\n".join(lines[2:])))
        latest_end = max(latest_end, end)
    return cues, warnings


sample = "1\n00:00:01,000 --> 00:00:02,000\nHello.\n\n2\n00:00:02,000 --> 00:00:03,500\nWelcome.\n"
cues, warnings = inspect_srt(sample, 4000)
assert cues[1] == (2000, 3500, "Welcome.")
assert warnings == []
assert inspect_srt("\ufeff" + sample.replace("\n", "\r\n"), 4000) == (cues, [])
assert len(inspect_srt(sample.replace("00:00:02,000\nHello", "00:00:02,500\nHello"), 4000)[1]) == 1

The four assertions in the example and thirteen rejection checks were executed locally with synthetic text. The rejection checks covered empty input, another format, invalid minutes, zero-length intervals, media-bound violations, numbering gaps, reversed start order, unsupported timestamp punctuation and invalid duration values. No live subtitles or media jobs were used.

The overlap result is a warning. Two speakers or multiple caption layers may legitimately overlap. An importer can also have stricter limitations than this helper. Investigate the warning against the destination's behavior rather than deleting one cue automatically. Adjacent intervals that meet exactly at a boundary are accepted by this example.

Separate timing errors from editorial decisions

FindingUseful next action
File cannot decode under the agreed encodingPreserve bytes and identify the actual encoding
Cue ends before it begins or exceeds the observed durationCheck source pairing and the export process
Start times move backwardReview ordering before changing cue numbers
Overlapping intervalsCheck speaker/layer intent and player support
All intervals pass but speech appears lateReview synchronization against the exact recording

Do not repair every error by sorting or clamping timestamps. Sorting can hide a wrong export order; clamping can silently remove text near the ending. Keep the unmodified input and write a new revision when you make a correction, with the reason and validation result attached.

For an excerpt, confirm whether subtitle time zero means the original recording or the beginning of the clip. A verified constant offset is useful only when the timeline has not been rearranged or stretched. The dubbing source guide explains that mapping and its limits. This validator performs no offset correction.

Preview content and timing in the destination

Watch representative passages at the beginning, middle and end, including a speaker change and any edited boundary. Compare cue appearance with the speech. Check names, numbers, punctuation, language and whether text is missing. Structural validation cannot establish transcription accuracy, translation quality, readability or accessibility completeness.

Review line wrapping and any markup in the actual renderer. This helper preserves cue text; it does not sanitize HTML or implement subtitle styling. If your application displays imported text in a web interface, render it as text or pass it through the renderer's documented safe handling. Do not interpolate untrusted caption text into HTML.

The function loads the entire string in memory and is intended for bounded inputs. Set file-size and cue-count limits appropriate to your application before using it in an upload service. For varied subtitle formats, use a maintained parser suited to your consumer and retain fixtures for the supported cases. The offline integration testing guide shows how to distinguish local checks from live integration evidence.

Hand off a reviewed pair

Store the accepted subtitle revision with its media revision, observed duration, validation profile and review result. That makes a later re-export or media replacement visible rather than silently inheriting an old approval.

Build your first Tornado media workflow to establish acquisition and delivery. Add subtitle inspection as a separate downstream stage whenever a subtitle artifact is available, and send only the verified media/subtitle pair to your chosen caption or dubbing tool.