Files
transcribe/subtitles.py
T

100 lines
3.4 KiB
Python

"""Convert OpenRouter word timestamps into readable SRT cues."""
import json
import math
import re
import sys
import textwrap
LINE_WIDTH = 42
MAX_DURATION = 6.0
PAUSE = 0.7
def lines(text):
return textwrap.wrap(
text, width=LINE_WIDTH, break_long_words=False, break_on_hyphens=False
)
def timestamp(seconds):
milliseconds = math.floor(seconds * 1000 + 0.5)
seconds, milliseconds = divmod(milliseconds, 1000)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
def convert(response):
if response.get("error"):
raise ValueError(f"Transcription failed: {response['error']}")
words = response.get("words")
if not isinstance(words, list) or (not words and response.get("text", "").strip()):
raise ValueError("SRT output requires word timestamps; the API returned none")
cues = []
current = []
previous_start = -1
def flush():
if current:
text = " ".join(word["word"] for word in current)
start = current[0]["start"]
end = min(max(word["end"] for word in current), start + MAX_DURATION)
# Avoid zero-length cues after rounding very short word durations.
end = max(end, math.floor(start * 1000 + 0.5) / 1000 + 0.001)
cues.append((start, end, "\n".join(lines(text))))
current.clear()
for item in words:
if not isinstance(item, dict) or not isinstance(item.get("word"), str):
raise ValueError("Invalid subtitle word: expected word text")
start, end = item.get("start"), item.get("end")
if (
any(
type(value) not in (int, float) or not math.isfinite(value)
for value in (start, end)
)
or start < 0
or end < start
):
raise ValueError("Invalid subtitle word: expected finite 0 <= start <= end")
if start < previous_start:
raise ValueError("Invalid subtitle words: timestamps are out of order")
previous_start = start
word = dict(item, word=" ".join(item["word"].split()))
if not word["word"]:
continue
if len(lines(word["word"])) > 2:
raise ValueError("Invalid subtitle word: text exceeds two lines")
if current:
candidate = " ".join(w["word"] for w in [*current, word])
previous = current[-1]
if (
len(lines(candidate)) > 2
or end - current[0]["start"] > MAX_DURATION
or start - previous["end"] >= PAUSE
or word.get("speaker") != previous.get("speaker")
or (
previous["end"] - current[0]["start"] >= 1.0
and re.search(r'[.!?…]["\u201d\u2019\u0027)]*$', previous["word"])
)
):
flush()
current.append(word)
flush()
return "".join(
f"{index}\n{timestamp(start)} --> {timestamp(end)}\n{text}\n\n"
for index, (start, end, text) in enumerate(cues, 1)
)
if __name__ == "__main__":
try:
with open(sys.argv[1], encoding="utf-8") as source:
result = convert(json.load(source))
except (ValueError, OSError) as error:
print(f"SRT conversion failed: {error}", file=sys.stderr)
sys.exit(1)
sys.stdout.write(result)