"""FFmpeg 合成:帧序列 + SRT 字幕 + 背景音乐 → MP4。""" from __future__ import annotations import random import subprocess import sys from pathlib import Path def compose_video( frames_dir: str, fps: int, subtitle_path: str, music_dir: str, output_path: str, music_volume: float = 0.15, ) -> str: frames_dir = Path(frames_dir) subtitle_path = Path(subtitle_path) output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) # 随机选一首背景音乐 music_files = list(Path(music_dir).rglob("*.mp3")) if not music_files: raise FileNotFoundError(f"music_dir 下没有找到 MP3 文件: {music_dir}") music_file = random.choice(music_files) print(f"[screencast] 背景音乐: {music_file.name}") # FFmpeg 字幕路径在 Windows 下需要转义冒号(subtitles 滤镜内部语法) srt_path_str = str(subtitle_path).replace("\\", "/").replace(":", "\\:") subtitle_filter = ( f"subtitles='{srt_path_str}'" ":force_style='FontName=Arial,FontSize=28," "PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000," "Outline=2,Shadow=1,Alignment=2'" ) # 视频和音频都放进 filter_complex,避免 -vf 与 -filter_complex 混用报错 filter_complex = ( f"[0:v]{subtitle_filter}[vout];" f"[1:a]volume={music_volume},apad[aout]" ) cmd = [ "ffmpeg", "-y", "-framerate", str(fps), "-i", str(frames_dir / "frame_%06d.png"), "-i", str(music_file), "-filter_complex", filter_complex, "-map", "[vout]", "-map", "[aout]", "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-c:a", "aac", "-b:a", "128k", "-shortest", "-pix_fmt", "yuv420p", str(output_path), ] print(f"[screencast] 运行 FFmpeg 合成...") result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace") if result.returncode != 0: print(result.stderr, file=sys.stderr) raise RuntimeError(f"FFmpeg 合成失败,退出码 {result.returncode}") print(f"[screencast] 输出: {output_path}") return str(output_path)