video_helper.subtitles module
Subtitles: one timed-text format layer, read and written in one place.
Why this lives here
Subtitles are a video concern. This package already burned them into frames
(video_helper.burn_subtitles()) and already converted SubRip to WebVTT
(video_helper.srt2vtt()), while the reading and writing of the formats
themselves sat in sprezzature-audio, which had consolidated four writers
and two readers of its own. That consolidation was right and its differences
are preserved below; what was wrong was the address. A caption file is what a
player shows over a picture, so the package that knows players owns the
format, and the speech packages ask it rather than each keeping a writer.
What came with the move, and what did not
The format layer came: cues, timestamps, the reader, the WebVTT and SubRip
writers. The narration bookkeeping did not. Counting samples while an engine
concatenates audio is a fact about synthesis, not about subtitles, so
Timeline and SpeechCue stay where the voices are.
The differences the earlier consolidation kept are kept here too, because each one exists for a reason:
Voice tags. A diarized caption track emits
<v Name>once speakers are attributed. A translated track deliberately does not: W3C reserves voice tags for the same-languagecaptionstrack, and a translated subtitle is dialogue. Hencevoices=Falserather than a guess from whether a speaker happens to be set.Identifiers. The narration path writes each cue’s segment index as the WebVTT identifier, the line before the timings, which is what lets a page highlight the right block without matching on text. Nothing else needs them, so an unset
identifieremits nothing.Line structure.
parse()keeps a cue’s newlines, and so doeswrite_vtt(). They are what a renderer breaks lines on and what a line-width audit counts, and an older reader that glued them with a space could do neither: preserving them took 393 audit findings to zero across the 22 subtitle tracks of the suite’s films. A caller that wants one line asks for it withCue.collapsed()– which is the narration path, where a cue is matched to a page element by its identifier and a blank line in the payload would become a second cue and shift every index after it.The two callers want opposite things and both are right, so the writer preserves what it is given and flattening is an explicit request. The decision belongs to the caller, never to the writer, and
test_one_writer_serves_both_callers_and_neither_winspins it.
Stdlib plus os-helper for logging: no ffmpeg, no OpenCV, nothing that
makes importing a caption writer expensive.
- video_helper.subtitles.ABBREVIATIONS: frozenset[str] = frozenset({'al', 'approx', 'art', 'av', 'bd', 'cf', 'chap', 'co', 'dept', 'dr', 'e.g', 'env', 'est', 'etc', 'ex', 'fig', 'i.e', 'inc', 'jr', 'ltd', 'm', 'max', 'min', 'mlle', 'mm', 'mme', 'mr', 'mrs', 'ms', 'no', 'p', 'pers', 'ph.d', 'pp', 'pr', 'prof', 'réf', 'sg', 'sr', 'st', 'ste', 'tél', 'vol', 'vs', 'éd'})
Tokens that end in a full stop without ending a sentence. Splitting on
.alone turns “Dr. Rosling said so.” into two sentences and a highlight jumps mid-phrase, which is worse than not highlighting at all.Bilingual on purpose, and the French list is not a translation of the English one:
M.is an abbreviation in French and a sentence end in English (”…the M.”). Compared case-insensitively, matched on the token before the stop.
- class video_helper.subtitles.Cue(start, end, text, speaker=None, identifier=None)[source]
Bases:
objectOne timed piece of text.
- start, end
Seconds from the beginning of the media.
- Type:
- text
The caption. Newlines are significant: a renderer breaks lines where they are, and a line-width audit counts them.
- Type:
- speaker
Who is speaking, when that has been worked out. Written as a
<v Name>tag only when the caller asks for voices.- Type:
str or None
- identifier
The optional WebVTT cue identifier, written on the line before the timings. The narration path uses it to carry a segment index, so a page can follow along without matching on text.
- Type:
str or None
- video_helper.subtitles.FORMAT_FOR_SUFFIX: dict[str, str] = {'.srt': 'srt', '.vtt': 'vtt', '.webvtt': 'vtt'}
Which writer an output extension asks for. The extension is the request: a caller naming
captions.srthas already said what it wants, and making it repeat itself in a second argument is how the two come apart.
- video_helper.subtitles.VOICE_CUE = '<v {name}>{text}'
How a voice tag is written back out.
- video_helper.subtitles.VOICE_RE = re.compile('^<v\\s+([^>]+)>\\s*')
<v Ada Lovelace>Hello.- Type:
A voice tag introducing a speaker
- video_helper.subtitles.WRITABLE_FORMATS: frozenset[str] = frozenset({'srt', 'vtt'})
Every format this module writes.
burn_subtitlesaccepts two more,.assand.ssa, which it reads and never writes.
- video_helper.subtitles.format_timestamp(seconds, *, srt=False)[source]
Format seconds as
HH:MM:SS.mmm(WebVTT) orHH:MM:SS,mmm(SubRip).- Parameters:
seconds (float) – Offset from the start of the media. Negative input clamps to zero: a negative timestamp is never meaningful and a renderer’s behaviour on one is undefined rather than obvious.
srt (bool, optional) – Use SubRip’s comma instead of WebVTT’s full stop. The separator is the only difference between the two spellings.
- Returns:
The timestamp.
- Return type:
Examples
>>> format_timestamp(61.34) '00:01:01.340' >>> format_timestamp(61.34, srt=True) '00:01:01,340'
- video_helper.subtitles.from_centiseconds(value)[source]
Convert a whisper.cpp timestamp (10-ms units) to seconds.
- video_helper.subtitles.parse(text)[source]
Read a WebVTT or SubRip document into cues.
Both formats are accepted because this module writes both, and a reader holding one has no reason to convert it before doing anything with it.
- Parameters:
text (str) – The document’s contents.
- Returns:
Cues in time order.
- Return type:
Notes
A
<v Name>tag is lifted intoCue.speakerrather than left in the text, so a caller that re-writes the cue does not double-tag it, and one that does not want voices simply never asks for them.Newlines inside a cue are preserved. Callers that want a single line call
Cue.collapsed().A byte-order mark and CRLF endings are tolerated. Both arrive routinely from Windows subtitle tools, and both used to land inside the first cue’s identifier, where they are invisible in a diff and fatal to a parser.
- video_helper.subtitles.split_sentences(text)[source]
Split a block of prose into sentences.
- Parameters:
text (str) – One block’s text, already cleaned of markup.
- Returns:
One entry per sentence, whitespace-collapsed, never empty strings. Text with no terminator at all comes back as a single sentence.
- Return type:
Notes
Deliberately a small rule set rather than a model: it has to give the same answer for a page and for its audio, run with no dependency, and be inspectable when it gets one wrong. It handles terminators with their trailing quotes, ellipses, decimals, initials, and the abbreviations in
ABBREVIATIONSfor English and French.Sentence granularity is the unit a read-along is built on: a paragraph is too coarse to follow with the eye, and a word would need forced alignment against audio the pipeline generated itself, a statistical guess at something already known exactly.
Examples
>>> split_sentences("One. Two! Three?") ['One.', 'Two!', 'Three?'] >>> split_sentences("Dr. Rosling said so. Then he left.") ['Dr. Rosling said so.', 'Then he left.'] >>> split_sentences("It cost 3.14 euros. Exactly.") ['It cost 3.14 euros.', 'Exactly.'] >>> split_sentences("Released as v2.5 today.") ['Released as v2.5 today.'] >>> split_sentences("No terminator here") ['No terminator here']
- video_helper.subtitles.vtt2srt(vtt_file_path, srt_file_path=None)[source]
Convert a WebVTT file to SubRip.
- Parameters:
- Returns:
Path to the file written.
- Return type:
Notes
The other direction,
video_helper.srt2vtt(), is not this function’s mirror and the asymmetry is deliberate: it also lifts<font color>tags into cue classes and writes the stylesheet that binds them, which has no counterpart going the other way because SubRip carries its colours inline. Going this way, a voice tag becomes theName:prefix SubRip players render, and a WebVTT identifier is replaced by SubRip’s own numbering, which is what the format allows.Why it exists at all: every platform that takes a caption upload takes SubRip, and plenty still do not take WebVTT. A package that could only convert towards WebVTT could not deliver a caption file to the places films actually go.
Examples
>>> vtt2srt("captions.vtt") 'captions.srt'
- video_helper.subtitles.write(cues, output_path=None, *, subtitle_format='', voices=True)[source]
Render cues in whichever format was asked for, and write them if asked.
- Parameters:
output_path (str or pathlib.Path or None, optional) – Where to write. The extension picks the format (
.vtt,.srt). None renders without writing, and thensubtitle_formathas to say which format.subtitle_format (str, optional) –
"vtt"or"srt", overriding what the extension says. Needed only when there is no path, or when a caller insists on an extension that disagrees with its contents.voices (bool, optional) – Forwarded to
write_vtt(). SubRip has no voice tags, so it is ignored there rather than silently changing the output.
- Returns:
The rendered document, whether or not it was written.
- Return type:
- Raises:
ValueError – If no format can be determined, or the one named is not written here.
Examples
>>> cues = [Cue(start=0.0, end=1.5, text="Bonjour.")] >>> write(cues, subtitle_format="srt").splitlines()[0] '1'
- video_helper.subtitles.write_srt(cues)[source]
Render cues as a SubRip document.
- Parameters:
cues (list of Cue) – Timed text. Cues with no text are dropped, as in
write_vtt().- Returns:
A SubRip document, ready to write to a
.srtfile.- Return type:
Notes
SubRip has no voice tags, so a speaker is written as a plain prefix: the convention every player renders and no player hides. Cues are numbered from one, which is SubRip’s identifier, so a WebVTT identifier carrying a segment index does not survive the trip. That is the format’s limit and not a choice made here.
- video_helper.subtitles.write_vtt(cues, *, voices=True)[source]
Render cues as a WebVTT document.
- Parameters:
cues (list of Cue) – Timed text. Cues with no text are dropped: a timed blank box is on screen for its full duration and says nothing.
voices (bool, optional) – Write
<v Name>tags for cues that carry a speaker. DefaultTrue. PassFalsefor a translated subtitle track: W3C reserves voice tags for the same-languagecaptionstrack.
- Returns:
A WebVTT document, ready to write to a
.vttfile.- Return type: