feat: support create subtitles (.srt)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user