Subtitles
Captions for lectures, hearings and videos — SRT and WebVTT built from timed segments.
How it works
Each segment's text is split into captions of at most two lines and seven seconds, and the segment's time is shared out by character count. Built-in SRT and WebVTT output and word-level timing are coming soon; this recipe works today and will get more precise when they arrive.
python subtitles.py lecture.mp3 te-IN # writes lecture.srt and lecture.vtt1
00:00:00,000 --> 00:00:04,180
ఈ రోజు మనం భారత రాజ్యాంగం గురించి
మాట్లాడుకుందాంTips
- Use
mode: "native"for broadcast-style captions,codemixfor audiences who read English terms in English. - For a recording that is already a video, extract the audio track first (
ffmpeg -i talk.mp4 -vn -c:a aac talk.m4a); video files are coming soon. - For proceedings with several speakers, add
diarization: trueand prefix captions with the speaker.
The script
subtitles.py
"""Subtitles: SRT and WebVTT from a transcript's timed segments.
python subtitles.py lecture.mp3 te-IN # writes lecture.srt and lecture.vtt
Needs TWENTYSIX_API_KEY and TWENTYSIX_BASE_URL. Long segments are split into captions of at most two lines and
`max_s` seconds, with time shared out by character count (word-level timing is coming soon).
"""
import math
import os
import sys
from twentysix import Client
def captions(segments, max_chars=42, max_lines=2, max_s=7.0):
"""-> [(start, end, text)]. A segment becomes as many captions as its length and duration need; words are shared
out evenly by character count and so is the segment's time."""
out = []
for s in segments:
words = s["text"].split()
if "start" not in s or not words:
continue
text, dur = " ".join(words), max(0.0, s["end"] - s["start"])
n = min(len(words), max(1, math.ceil(len(text) / (max_chars * max_lines)), math.ceil(dur / max_s)))
groups, cur, done = [], [], 0
for w in words:
cur.append(w)
done += len(w) + 1
if len(groups) < n - 1 and done >= len(text) * (len(groups) + 1) / n:
groups.append(" ".join(cur))
cur = []
if cur:
groups.append(" ".join(cur))
total, t = sum(len(g) for g in groups), s["start"]
for g in groups:
end = t + dur * len(g) / total
out.append((round(t, 3), round(end, 3), wrap(g, max_chars)))
t = end
return out
def wrap(text, width):
"""Break a caption into two lines at the space nearest its middle."""
if len(text) <= width or " " not in text:
return text
mid = len(text) // 2
cut = min((i for i, ch in enumerate(text) if ch == " "), key=lambda i: abs(i - mid))
return text[:cut] + "\n" + text[cut + 1:]
def ts(sec, sep):
ms = int(round(sec * 1000))
return f"{ms // 3600000:02d}:{ms // 60000 % 60:02d}:{ms // 1000 % 60:02d}{sep}{ms % 1000:03d}"
def to_srt(caps):
return "\n".join(f"{i}\n{ts(a, ',')} --> {ts(b, ',')}\n{text}\n" for i, (a, b, text) in enumerate(caps, 1))
def to_vtt(caps):
return "WEBVTT\n\n" + "\n".join(f"{ts(a, '.')} --> {ts(b, '.')}\n{text}\n" for a, b, text in caps)
if __name__ == "__main__":
path, language = sys.argv[1], (sys.argv[2] if len(sys.argv) > 2 else "hi-IN")
with Client() as client:
result = client.transcribe_file(path, language, mode="native")
caps = captions(result["segments"])
stem = os.path.splitext(path)[0]
open(stem + ".srt", "w", encoding="utf-8").write(to_srt(caps))
open(stem + ".vtt", "w", encoding="utf-8").write(to_vtt(caps))
print(f"{len(caps)} captions → {stem}.srt, {stem}.vtt")