diff --git a/video_to_srt.py b/video_to_srt.py new file mode 100755 index 0000000..8a88d75 --- /dev/null +++ b/video_to_srt.py @@ -0,0 +1,468 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = ["httpx"] +# /// +import argparse +import base64 +import functools +import logging +import os +import re +import subprocess +import tempfile +import textwrap +import time +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path + +import httpx + +RETRY_STATUS_CODES = {429, 500, 502, 503, 504, 524, 529} +MAX_ATTEMPTS = 5 +MIN_SPLIT_DURATION = 1.0 +MIN_CHUNK_DURATION = 0.1 +DIALOGUE_LINE_LENGTH = 42 +MAX_DIALOGUE_GAP = 1.0 +MAX_DIALOGUE_DURATION = 7.0 +LINE_LENGTH = 42 +MAX_LINES = 2 +# lowercase tokens (en, tr) whose trailing period does not end a sentence +ABBREVIATIONS = {"mr", "mrs", "ms", "dr", "prof", "e.g", "i.e", "vs", "etc", "doç", "av", "sn", "vb", "bkz"} +# Finds candidate sentence boundaries: end punctuation, optional closers, then whitespace. +# Matches in "He left. Then" -> ". " and in 'He said "no." Then' -> '." '. +SENTENCE_END = re.compile( + r""" + [.!?…]+ # one or more end marks: "." "?!" "..." or the ellipsis character + [\"'”’)\]]* # closing quotes or brackets that belong to the finished sentence + \s+ # whitespace after it; one or more, so double spaces work too + """, + re.VERBOSE, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class Segment: + """A timed piece of transcript; times are in seconds, speaker is a diarization index if known.""" + + start: float + end: float + text: str + speaker: int | None = None + + +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Transcribe a video into an SRT file using an OpenRouter speech-to-text model.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("video", type=Path, help="Input video (or audio) file") + parser.add_argument("--output", "-o", type=Path, help="Output SRT path (default: