Adds tools/screencast/ — a reusable screen-recording pipeline for skill demo videos. Wraps a pytest desktop E2E run with: - ScreenRecorder: mss-based full-screen frame capture (background thread) - SubtitleEngine: stdout keyword → timestamped SRT generation - compose_video: FFmpeg filter_complex merge (video + subtitles + BGM) - run_screencast: main orchestrator wiring all three layers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
72 lines
2.2 KiB
Python
72 lines
2.2 KiB
Python
"""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)
|