feat(hardcode-subs): add subtitle burn-in helper
This commit is contained in:
Executable
+101
@@ -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()
|
||||||
Reference in New Issue
Block a user