Usage
Reading WebVTT caption files
import webvtt
vtt = webvtt.read('captions.vtt')
# iterate over the captions
for caption in vtt:
print(caption.start) # start timestamp in text format
print(caption.end) # end timestamp in text format
print(caption.text) # caption text
print(caption.voice) # voice span if present
# iterate over the lines of a particular caption
for line in vtt[0].lines:
print(line)
# the caption text is returned clean without cue tags,
# the raw text of a caption is available in raw_text
vtt[0].text # 'This is a caption text'
vtt[0].raw_text # 'This is a <c.colorE5E5E5>caption</c> text'
# caption identifiers
vtt[0].identifier # 'chapter 1'
Reading WebVTT caption files from a file-like object
import webvtt
import requests
from io import StringIO
payload = requests.get('https://example.com/captions.vtt').text
buffer = StringIO(payload)
for caption in webvtt.from_buffer(buffer):
print(caption.start)
print(caption.end)
print(caption.text)
Reading caption files from a binary file-like object
Binary data, such as a file opened in 'rb' mode or a BytesIO object,
is decoded with the encoding of its Byte Order Mark, if any, or else with
encoding, UTF-8 by default. With a Byte Order Mark, saving keeps its
encoding and the Byte Order Mark, as when reading a file.
import webvtt
from io import BytesIO
with open('captions.vtt', 'rb') as f:
vtt = webvtt.from_buffer(f)
# formats supported: vtt, srt, sbv
with open('captions.srt', 'rb') as f:
vtt = webvtt.from_buffer(f, format='srt')
# binary data in another encoding without a Byte Order Mark
buffer = BytesIO('WEBVTT\n'.encode('latin-1'))
vtt = webvtt.from_buffer(buffer, encoding='latin-1')
A string is not a file-like object and raises TypeError: use
from_string to read captions from a string.
The path of a file opened by name is kept in the file attribute, so
save() without a path saves to it. A file-like object without a path,
such as standard input, a file opened from a file descriptor or a
BytesIO object, gives captions without a file, as when creating
WebVTT objects, so save() needs a path.
Reading WebVTT captions from a string
import webvtt
import textwrap
vtt = webvtt.from_string(textwrap.dedent("""
WEBVTT
00:00:00.500 --> 00:00:07.000
Caption #1
00:00:07.000 --> 00:00:11.890
Caption #2 line 1
Caption #2 line 2
00:00:11.890 --> 00:00:16.320
Caption #3
""").strip()
)
for caption in vtt:
print(caption.start)
print(caption.end)
print(caption.text)
Iterating over a slice of captions
import webvtt
vtt = webvtt.read('captions.vtt')
for caption in vtt.iter_slice(start='00:00:11.000',
end='00:00:27.000'
):
print(caption.start)
print(caption.end)
print(caption.text)
Only the captions within the range are returned: a caption that starts
before start or ends after end is left out, even if part of it is in
the range.
Creating captions
from webvtt import WebVTT, Caption
vtt = WebVTT()
# creating a caption with a list of lines
caption = Caption(
'00:00:00.500',
'00:00:07.000',
['Caption line 1', 'Caption line 2']
)
# an identifier can be assigned
caption.identifier = 'chapter 1'
# adding a caption
vtt.captions.append(caption)
# creating another caption with a text
caption = Caption(
'00:00:07.000',
'00:00:11.890',
'Caption line 1\nCaption line 2'
)
vtt.captions.append(caption)
# save the captions to a file
vtt.save('captions.vtt')
The lists given to WebVTT, such as WebVTT(captions=captions), are
kept as its attributes, not copied, so changes to them show in the
instance. The list of lines given to a Caption or a Style is
copied.
As in WebVTT, only CR, LF and CRLF break text into lines. Other characters such as U+2028 or a form feed stay in the line.
Text that could not be read back from the file raises ValueError when
saving or writing the captions or getting their content, and an existing
file is left unchanged:
an empty line followed by more text, or
-->, in a caption or a stylea line break or
-->in an identifieran empty line followed by more text, or
-->, in a commenta line break or
-->in the header text
A WebVTT object created from scratch has no filename, so save()
requires an output path. Calling it without one raises
MissingFilenameError. The file attribute holds the path the
captions were read from or last saved to, and can be set to choose where
save() writes:
import webvtt
vtt = webvtt.from_srt('captions.srt')
# save() would write captions.vtt, next to the file that was read
vtt.file = 'new_captions.vtt'
vtt.save()
WebVTT takes keyword arguments only and does not read files, so
WebVTT('captions.vtt') raises TypeError: use webvtt.read() to
read captions from a file. The file argument of WebVTT is
deprecated in favor of the file attribute.
Manipulating captions
import webvtt
vtt = webvtt.read('captions.vtt')
# update start timestamp
vtt[0].start = '00:00:01.250'
# update end timestamp
vtt[0].end = '00:00:03.890'
# update caption text
vtt[0].text = 'New caption text'
# or set its lines
vtt[0].text = ['New caption text', 'on two lines']
# delete a caption
del vtt.captions[2]
The text of a caption is a string, split on its line breaks, or a sequence
of lines, as when creating a Caption. Any other value raises
TypeError.
Cue settings
The cue settings that follow the timestamps of a cue, such as
line:0 align:start, position and align the caption. They are available
as a CueSettings object in the settings attribute of a caption,
with a property for each setting that returns its value or None when it
is not set:
import webvtt
from webvtt import Caption, CueSettings
vtt = webvtt.read('captions.vtt')
caption = vtt[0]
caption.settings.line # '10%,end'
caption.settings.align # 'start'
str(caption.settings) # 'line:10%,end align:start'
# change, add or remove a setting
caption.settings.line = '0'
caption.settings.position = '50%,center'
caption.settings.size = None
# replace or clear all the settings
caption.settings = 'line:0 align:start'
caption.settings = None
# create a caption with settings
caption = Caption(
'00:00:00.500',
'00:00:07.000',
'Caption text',
settings=CueSettings(line='0', align='start')
)
The supported settings are vertical, line, position, size,
align and region. Setting a value that is not valid as per the
WebVTT specification, such as align = 'middle', raises
MalformedCaptionError.
The settings are saved as they were read, in the same order and including
the ones that are not valid or not known, separated by a single space.
When a setting is not valid, such as align:middle from an older
version of the specification, its property returns None and assigning
a new value replaces it. When converting to SRT, the cue settings are not
included as the format does not support them.
Shifting captions
Captions can be moved earlier or later by an offset, given as a number of
seconds or a timedelta:
import webvtt
from datetime import timedelta
vtt = webvtt.read('captions.vtt')
# show all the captions 1.5 seconds earlier
vtt.shift(-1.5)
# show the captions from 00:12:30 onwards 3 seconds later
vtt.shift(3, start='00:12:30.000')
# shift a single caption
vtt[4].shift(0.25)
# the offset can also be a timedelta
vtt.shift(timedelta(minutes=1, seconds=30))
vtt.save()
The offset is rounded to the nearest millisecond of its decimal value, with
halves rounded away from zero, so 0.0005 is 1 millisecond and -0.0005
is -1 millisecond. A timestamp that would fall before zero is set to
00:00:00.000, so a caption moved entirely before the start is kept with
00:00:00.000 --> 00:00:00.000 timings.
Shifting is also available from the command line, see Command line.
Synchronizing captions
When the captions are in sync at the start but drift further out of sync
over time, usually because they were timed for a video with a different
frame rate or cut, shifting is not enough. sync moves the captions to
two or more points, each a (from, to) pair where the time from in
the captions is moved to to:
import webvtt
vtt = webvtt.read('captions.vtt')
# the caption at 00:00:05.000 should be at 00:00:04.200 and the one at
# 01:30:00.000 at 01:31:12.500, everything else is moved along
vtt.sync(('00:00:05.000', '00:00:04.200'),
('01:30:00.000', '01:31:12.500'))
# points can be taken from the captions themselves
vtt.sync((vtt[0].start, '00:00:04.200'), (vtt[-1].start, '01:31:12.500'))
# with more points, each part between two points is synchronized on its
# own, for captions whose drift is not constant
vtt.sync(('00:00:05.000', '00:00:04.200'),
('00:45:00.000', '00:45:30.000'),
('01:30:00.000', '01:31:12.500'))
# captions timed for a 25 fps video played with a 23.976 fps one
vtt.change_framerate(25, 23.976)
# exact frame rates, like the NTSC 24000/1001, can be given as fractions
from fractions import Fraction
vtt.change_framerate(25, Fraction(24000, 1001))
vtt.save()
The times of the points can be timestamp strings, Timestamp objects,
numbers of seconds or timedelta objects, and they can be given in any
order. Times between two points are moved along the line between them, and
times before the first or after the last point along the line of the
outermost points. Start and end times are both moved, so the duration of
the captions changes with the timing.
As with shift, times are rounded to the nearest millisecond with halves
rounded away from zero, and a time that would fall before zero is set to
00:00:00.000. A ValueError is raised if fewer than two points are
given, two points have the same from time, or the to times are not
in the same order as the from times, since that would change the order
of the captions.
Use shift when all the captions are off by the same amount and sync
when the difference grows along the video.
Synchronizing is also available from the command line, see Command line.
Timestamps
from webvtt.models import Timestamp
# create a timestamp from a number of seconds, rounded to the
# nearest millisecond with halves rounded away from zero
str(Timestamp.from_seconds(5051.67)) # '01:24:11.670'
# in_seconds leaves the milliseconds out
Timestamp.from_string('01:24:11.670').in_seconds() # 5051
Timestamp.from_string('01:24:11.670').in_milliseconds() # 5051670
Saving captions
import webvtt
vtt = webvtt.read('captions.vtt')
# save to the same file
vtt.save()
# save to a different file
vtt.save('new_captions.vtt')
# save to a file path, the .vtt extension is added when it has none
vtt.save('other/folder/new_captions')
# save to a folder, keeping the name of the file that was read
vtt.save('other/folder')
# write to an open file
with open('other_captions.vtt', 'w') as f:
vtt.write(f)
Without a path, or with a folder, the name of the file that was read or
last saved is used. A file with the .vtt or .webvtt extension
keeps it, and any other extension is replaced by .vtt, so captions
read from captions.srt are saved to captions.vtt. save_as_srt()
does the same with .srt.
Retrieving WebVTT-formatted captions
WebVTT content can be retrieved without an output file:
import webvtt
vtt = webvtt.read('captions.vtt')
# print the content in WebVTT format
print(vtt.content)
Converting captions
Captions can be read from the following formats:
SubRip (.srt)
YouTube SBV (.sbv)
import webvtt
# read captions from SRT format
vtt = webvtt.from_srt('captions.srt')
# save the captions in WebVTT format
vtt.save()
# the conversion can be done by chaining the method calls
webvtt.from_srt('captions.srt').save()
# the same for SBV format
vtt = webvtt.from_sbv('captions.sbv')
Some SRT files have empty lines in the caption text. When reading SRT, a new caption starts at a cue index followed by the cue timings, even without an empty line before it. Any other text after an empty line stays in the current caption, and the empty lines are removed.
The cue timings of SRT are read as in most SRT readers. The separator before
the milliseconds can be a comma or a dot, the fields can be shorter, as in
0:0:7,500, and the milliseconds are read as a number, so 00:00:07,5
is 5 milliseconds. A field over its range carries over, so 00:00:07,1500
is read as 00:00:08.500. The hours and the milliseconds are required, and
a cue without them is skipped. The cue timings of SBV carry over in the same
way, so 0:61:00.000 is read as 01:01:00.000.
Blank lines at the start of SRT and SBV files are skipped, and cues without text are skipped, so a file whose first cue has no text is read. SRT cues without an index are read, as most SRT players do, but the first cue of the file needs one for the content to be read as SRT.
Convert WebVTT captions to other formats:
SubRip (.srt)
import webvtt
# save in SRT format
vtt = webvtt.read('captions.vtt')
vtt.save_as_srt()
# write to an open file in SRT format
with open('captions.srt', 'w') as f:
vtt.write(f, format='srt')
Captions without text, such as text of blank lines, are left out of the SRT file, as SRT cannot have a cue without text, and the others are numbered consecutively.
The cue tags are removed when converting to SRT. To keep the formatting
instead, use keep_formatting. Bold, italic, underline and font tags are
kept, and the cue classes with a color in the styles become font tags
with that color. Other cue tags are removed, keeping their text:
import webvtt
vtt = webvtt.from_string("""\
WEBVTT
STYLE
::cue(.magenta) {
color: #ff00ff;
}
00:00:00.500 --> 00:00:07.000
<c.magenta><i>Hello</i> world</c>
""")
vtt.save_as_srt('captions.srt', keep_formatting=True)
# the caption is saved as:
# <font color="#ff00ff"><i>Hello</i> world</font>
Only the color of ::cue(.class) selectors is read. When a cue has more
than one class with a color, the rule that comes last in the styles applies.
Character references such as < are written as they are.
The encoding given to read a file without a Byte Order Mark is only
used for reading, as WebVTT files are UTF-8: save() writes UTF-8 unless
another encoding is given. SRT files have no fixed encoding, so captions
read with from_srt or from_buffer in SRT format and an
encoding are saved by save_as_srt() in the same encoding:
import webvtt
vtt = webvtt.from_srt('captions.srt', encoding='cp1252')
# saved in cp1252
vtt.save_as_srt('edited.srt')
# saved in UTF-8, also for text that cp1252 cannot write
vtt.save_as_srt('edited.srt', encoding='utf-8')
# saved in UTF-8
vtt.save()
Saving text that the encoding cannot write raises UnicodeEncodeError,
leaving an existing file unchanged.
Segmenting captions for HLS
import webvtt
# write 10-second segments and a prog_index.m3u8 playlist to output/path
webvtt.segment('captions.vtt', 'output/path')
# use 6-second segments and a different MPEG-TS value
webvtt.segment('captions.vtt', 'other/path', seconds=6, mpegts=0)
# segment captions already read, edited or from another format,
# without saving them first
vtt = webvtt.from_srt('captions.srt')
vtt.change_framerate(25, 23.976)
webvtt.segment(vtt, 'srt/path')
Every segment has the styles and regions of the captions, and the captions
in it keep their identifiers and cue settings. Comments are not written to
the segments. seconds is a whole number above zero and mpegts a
whole number from 0 to 8589934591.
Segmenting into a folder with the playlist or segments of an earlier run
raises FileExistsError, so they are not replaced by mistake. Use
overwrite=True to replace them, which also deletes the segments of the
earlier run that the new playlist does not have. Other files in the folder
are left alone:
# segment again into output/path, replacing the earlier segments
webvtt.segment('captions.vtt', 'output/path', overwrite=True)
The playlist ends with the segment of the last caption, and every segment
lasts seconds, as the length of the media is not known. Give it with
duration, as a timestamp, a number of seconds or a timedelta, so the
playlist covers all the media, with empty segments after the last caption,
and its last segment ends with the media:
# a video of 1 minute 25.5 seconds: 9 segments, the last of 5.5 seconds
webvtt.segment('captions.vtt', 'output/path', duration='00:01:25.500')
A caption starting at or after the end of the media raises ValueError, as
the duration is likely wrong, and a caption ending after it is kept in the
last segment.
Segmentation is also available from the command line, see Command line.
WebVTT files with Byte Order Mark (BOM)
When the WebVTT file has a BOM, saving it keeps the BOM:
import webvtt
vtt = webvtt.read('captions_with_bom.vtt')
# saved file keeps the BOM
vtt.save()
Add a BOM to a file without it:
import webvtt
vtt = webvtt.read('captions_without_bom.vtt')
# saved file has a BOM
vtt.save(add_bom=True)
Remove the BOM from a file:
import webvtt
vtt = webvtt.read('captions_with_bom.vtt')
# saved file does not have a BOM
vtt.save(add_bom=False)
Save a file with a different encoding:
import webvtt
vtt = webvtt.read('captions.vtt')
vtt.save(encoding='utf-32-le')
# save in a different encoding with a BOM
vtt.save(encoding='utf-32-le',
add_bom=True
)
Encoding names are matched as Python does, so UTF-8 and utf8 are
the same as utf-8. The utf-8-sig, utf-16 and utf-32
codecs always write a BOM, utf-16 and utf-32 in the byte order of
the platform, so add_bom=False raises ValueError with them: use
utf-16-le, utf-16-be, utf-32-le or utf-32-be to choose the
byte order and whether to add a BOM. add_bom=True raises
ValueError for an encoding without a BOM, such as latin-1, while
the BOM of the file that was read is left out when saving in such an
encoding. When saving raises, an existing file is left unchanged.
WebVTT styles
import webvtt
vtt = webvtt.read('captions.vtt')
for style in vtt.styles:
print(style.text)
# retrieve list of lines
print(style.lines)
STYLE blocks after the first cue are not styles as per the WebVTT
specification and are ignored, or raise MalformedCaptionError with
strict parsing.
Adding styles:
import webvtt
vtt = webvtt.read('captions.vtt')
vtt.styles.append(
webvtt.Style('::cue(b) {\n color: peachpuff;\n}')
)
# list of lines is supported
vtt.styles.append(
webvtt.Style(['::cue(b) {',
' color: peachpuff;',
'}'
])
)
Updating styles:
import webvtt
vtt = webvtt.read('captions.vtt')
vtt.styles[0].lines[1] = ' color: papayawhip;'
WebVTT regions
Regions are areas of the video where captions are rendered, defined in
REGION blocks before the first cue and used by the captions through
the region cue setting. They are available in the regions list as
Region objects, with a property for each setting that returns its
value or None when it is not set:
import webvtt
from webvtt import Region
vtt = webvtt.read('captions.vtt')
for region in vtt.regions:
print(region.id, region.width, region.lines)
# change a region
vtt.regions[0].width = '50%'
vtt.regions[0].scroll = None
# add a region and use it in a caption
vtt.regions.append(
Region(
id='fred',
width='40%',
lines='3',
regionanchor='0%,100%',
viewportanchor='10%,90%',
scroll='up'
)
)
vtt.captions[0].settings.region = 'fred'
The supported settings are id, width, lines, regionanchor,
viewportanchor and scroll. Setting a value that is not valid raises
MalformedRegionError, a subclass of MalformedCaptionError, and the
settings are saved as they were read, including the ones that are not valid
or not known, with a setting per line. REGION blocks after the first cue
are not regions as per the WebVTT specification and are ignored, or raise
MalformedRegionError with strict parsing. When converting to SRT, the
regions are not included.
Removing or renaming a region does not update the captions that use it.
WebVTT header
The text following WEBVTT in the first line and the metadata lines
after it, such as the X-TIMESTAMP-MAP used by HLS, are kept when
reading and written back when saving:
WEBVTT - Sample title
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:126000
Kind: captions
00:00:00.500 --> 00:00:07.000
Caption text #1
import webvtt
vtt = webvtt.read('captions.vtt')
vtt.header_text # '- Sample title'
vtt.metadata
# ['X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:126000', 'Kind: captions']
vtt.metadata.append('Language: en')
Metadata lines are kept as they are, in order. Shifting or synchronizing the
captions does not change them, so X-TIMESTAMP-MAP keeps mapping the file
to the same media time. Saving, writing or getting the content raises
ValueError if the header text has a line break or contains -->, or if
a metadata line is empty, has a line break or contains -->, since that
would break the file. Header text with --> is not valid WebVTT and is
read as empty, or raises MalformedCaptionError with strict parsing.
WebVTT comments
Comments can be added to or retrieved from different items:
import webvtt
vtt = webvtt.read('captions.vtt')
# comments from the top of the file
print(vtt.header_comments)
# comments from the bottom of the file
print(vtt.footer_comments)
# comments in a style
print(vtt.styles[0].comments)
# comments in a caption
print(vtt.captions[0].comments)
# comments are just a list of strings
vtt.captions[5].comments.append('caption for review')
A block is a comment when it starts with NOTE followed by a space, a
tab or a line break. A block starting with other text, such as
NOTEBOOK, or containing --> is not a comment as per the WebVTT
specification, so it is ignored when reading, or raises
MalformedCaptionError with strict parsing.
Handling errors
The errors raised by webvtt-py are available in the webvtt.errors
module:
MalformedFileError: the content is not valid for the expected format (WebVTT, SRT or SBV).MalformedCaptionError: a caption or cue setting is not valid, or the content does not pass strict parsing.MalformedRegionError: a region setting is not valid, or a region does not pass strict parsing. It is a subclass ofMalformedCaptionError, so catching that error also catches it.MissingFilenameError: captions are saved without an output path and there is no file to derive it from.
They all subclass WebVTTError, so it catches any of them.
MalformedFileError and MalformedCaptionError are also subclasses of
ValueError, like the other errors for values that are not valid, such
as text that could not be read back when saving. Those other errors are
the built-in ValueError and TypeError, which WebVTTError does
not catch.
import webvtt
from webvtt.errors import (
MalformedFileError,
MalformedCaptionError,
MissingFilenameError
)
try:
vtt = webvtt.read('captions.vtt')
except MalformedFileError:
print('captions.vtt is not a valid WebVTT file')
except MalformedCaptionError as e:
print(f'captions.vtt has an invalid caption: {e}')
# an instance not read from a file needs an output path
vtt = webvtt.WebVTT()
try:
vtt.save()
except MissingFilenameError:
vtt.save('captions.vtt')
Parsing rules
The WebVTT parser follows the parsing rules of the WebVTT specification:
blocks are separated by empty lines, so lines containing only whitespace
are part of the cue text, and a line containing --> starts a new
block when it is not the timings line of the current one. Cues without
text are kept as captions with empty text.
Timestamps with fields shorter than the specification requires, such as
0:0:13.540 in Microsoft Teams transcripts or 0:1:5.2, are accepted
and the captions are saved in the standard 00:00:13.540 and
00:01:05.200 format. The fraction of a second is padded on the right,
so .2 is read as 200 milliseconds.
Strict parsing
By default the parser ignores blocks it cannot parse, so a cue with
malformed timings such as 00:00:00.-004 --> 00:00:04.300 or out of
range ones such as 00:00:61.000 --> 00:00:62.000 is skipped.
To validate the content, pass strict=True to read,
from_buffer or from_string and a MalformedCaptionError is
raised instead. Strict parsing also raises the error for:
timestamps that are not in the WebVTT format
cues whose end time is not after the start time
cue and region settings that are not known, not valid or repeated
blocks after the header that are not a cue, comment, style or region, such as text without cue timings, or a comment that does not start with
NOTEfollowed by a space, a tab or a line break, or contains-->styles and regions after the first cue
a cue that is not separated from the previous cue, comment or style by an empty line
For region settings and regions after the first cue, the error is
MalformedRegionError, a subclass of MalformedCaptionError.
Strict parsing also raises MalformedFileError when WEBVTT is
followed by text other than a space or a tab in the first line, such as
WEBVTTX, which the default parsing reads.
Strict parsing is only for WebVTT. SRT and SBV have no specification to
validate against, so from_buffer raises ValueError for strict=True
with the srt or sbv format.
import webvtt
from webvtt.errors import MalformedCaptionError
try:
vtt = webvtt.read('captions.vtt', strict=True)
except MalformedCaptionError as e:
print(f'captions.vtt is not valid WebVTT: {e}')