"""FFmpeg 合成:帧序列 + SRT 字幕 + 背景音乐 → MP4。""" from __future__ import annotations import random import subprocess import sys from pathlib import Path # 匠厂统一字幕样式 —— 所有 skill 录屏共用,做品牌一致性 # FontSize 14:1080p 屏幕约占 1.3% 高度,演示视频留足画面呼吸感 # Outline 2:14 号字配 Outline 3 会显胖,2 已足够在浅色背景上保持可读 # 修改请评估对所有 skill 录屏的影响。 _JIANGCHANG_SUBTITLE_STYLE = ( "FontName=Arial,FontSize=14," "PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000," "Outline=2,Shadow=1,Alignment=2" ) 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}'" f":force_style='{_JIANGCHANG_SUBTITLE_STYLE}'" ) # 视频和音频都放进 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)