From b20433564d20ea83f4285994ae85b79ff4e467d9 Mon Sep 17 00:00:00 2001 From: Abdussamet Kocak Date: Thu, 25 Jun 2026 08:55:14 +0300 Subject: [PATCH] feat(hardcode-subs): add subtitle burn-in helper --- hardcode_subs.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100755 hardcode_subs.py diff --git a/hardcode_subs.py b/hardcode_subs.py new file mode 100755 index 0000000..197cd90 --- /dev/null +++ b/hardcode_subs.py @@ -0,0 +1,101 @@ +#!/usr/bin/env -S python3.12 + +from pathlib import Path +import logging +import subprocess +import argparse + +logger = logging.getLogger(__name__) + + +def hardcode_subs( + video_path: Path, + subtitle_path: Path, + lang: str, + save_path: Path | None = None, + resolution: str | None = None, + quality: float | None = None, + font_size: int = 40, +) -> Path: + if not save_path: + save_path = video_path.with_name(f"{video_path.stem}.{lang}.hardcoded.mp4") + vf_filters = [f"subtitles='{subtitle_path}:force_style=Fontsize={font_size}'"] + if resolution: + resolution_map = {"1080p": 1920, "720p": 1280, "480p": 854} + max_width = resolution_map.get(resolution) + if max_width: + vf_filters.append(f"scale='min({max_width},iw)':-2") + vf = ",".join(vf_filters) + crf = str(quality) if quality else "35" + cmd = [ + "ffmpeg", + "-y", + "-hwaccel", + "videotoolbox", + "-i", + str(video_path), + "-vf", + vf, + "-c:v", + "h264_videotoolbox", + "-crf", + crf, + "-c:a", + "copy", + str(save_path), + ] + logger.info(f"Hardcoding subtitles into video: {save_path.name}") + subprocess.run(cmd, check=True) + return save_path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Hardcode subtitles into a video.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("video", type=Path, help="Path to the video file") + parser.add_argument("subtitle", type=Path, help="Path to the subtitle file") + parser.add_argument("--out", "-o", type=Path, help="Path to the output video file") + parser.add_argument("--lang", default="en", help="Language code for subtitles") + parser.add_argument( + "--resolution", + choices=["1080p", "720p", "480p"], + help="Set output video resolution (max width)", + ) + parser.add_argument( + "--quality", + "-q", + type=float, + default=35, + help="Set CRF quality factor (lower is better, default 35)", + ) + parser.add_argument( + "--font-size", + type=int, + default=40, + help="Font size for hardcoded subtitles", + ) + return parser.parse_args() + + +def main(): + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") + args = parse_args() + if not args.video.exists(): + raise FileNotFoundError(f"Video file not found: {args.video}") + if not args.subtitle.exists(): + raise FileNotFoundError(f"Subtitle file not found: {args.subtitle}") + + save_path = args.out or args.video.with_name(f"{args.video.stem}.{args.lang}.hardcoded.mp4") + hardcoded_video = hardcode_subs( + video_path=args.video, + subtitle_path=args.subtitle, + lang=args.lang, + save_path=save_path, + resolution=args.resolution, + quality=args.quality, + font_size=args.font_size, + ) + logging.info(f"Hardcoded video saved at: {hardcoded_video}") + + +if __name__ == "__main__": + main()