Files
worldmodel/research/math/podcast2/build_video2.py
T
gaojie 920439e044
Sync to site1 / sync (push) Has been cancelled
Add presentation slides for OU process and Mehler formula
2026-06-03 04:08:01 +08:00

115 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""把 OU/Mehler 幻灯片 + 已有播客音频合成为 MP4(复用第一篇流程)。
流程:读已有音频时长 → 按每句字符占比分配到 12 页 → Chrome 截 12 页 → ffmpeg 合成。
用法:python3 build_video2.py
"""
import json
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
SCRIPT_JSON = os.path.join(HERE, "ou_mehler-script.json")
SLIDE_HTML = os.path.join(HERE, "slides", "OU 过程与 Mehler 公式.html")
OUT_DIR = os.path.join(HERE, "output")
SHOT_DIR = os.path.join(OUT_DIR, "shots")
AUDIO_IN = os.path.join(OUT_DIR, "ou_mehler-podcast.mp3")
VIDEO_OUT = os.path.join(OUT_DIR, "ou_mehler-video.mp4")
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
NUM_PAGES = 12
# 句(1-based) -> 页(1-based)。共 44 句、12 页。
# 1 封面 / 2 核心问题 / 3 OU直觉 / 4 离散定义 / 5 三性质 / 6 Mehler / 7 核心推论
# 8 数值例子 / 9 对齐损失 / 10 衰减图 / 11 小结 / 12 尾页
LINE_TO_PAGE = {
1: 1, 2: 1, # 封面
3: 2, # 核心问题
4: 3, 5: 3, 6: 3, # OU 物理直觉
7: 4, 8: 4, 9: 4, # 离散定义
10: 5, 11: 5, 12: 5, 13: 5, 14: 5, 15: 5, # 三性质
16: 6, 17: 6, 18: 6, 19: 6, 20: 6, # Mehler 公式
21: 7, 22: 7, 23: 7, # 核心推论
24: 8, 25: 8, 26: 8, 27: 8, 28: 8, 29: 8, # 数值例子
30: 9, 31: 9, 32: 9, 33: 9, 34: 9, # 对齐损失
35: 10, 36: 10, # 衰减图示
37: 11, 38: 11, 39: 11, 40: 11, 41: 11, # 小结
42: 12, 43: 12, 44: 12, # 尾页
}
def run(cmd):
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def ffprobe_dur(path):
out = subprocess.check_output(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", path])
return float(out.strip())
def step1_page_durations():
total = ffprobe_dur(AUDIO_IN)
lines = json.load(open(SCRIPT_JSON, encoding="utf-8"))["lines"]
page_chars = {p: 0 for p in range(1, NUM_PAGES + 1)}
for i, line in enumerate(lines, 1):
page_chars[LINE_TO_PAGE.get(i, NUM_PAGES)] += len(line["paragraph"].strip())
grand = sum(page_chars.values())
page_dur = {p: total * page_chars[p] / grand for p in page_chars}
page_dur[NUM_PAGES] += (total - sum(page_dur.values()))
print(f"音频总时长 {total:.2f}s,分配到 {NUM_PAGES} 页:")
for p in range(1, NUM_PAGES + 1):
print(f" page {p:>2}: {page_dur[p]:6.2f}s ({page_chars[p]} 字)")
return page_dur
def step2_screenshots():
os.makedirs(SHOT_DIR, exist_ok=True)
file_url = "file://" + SLIDE_HTML.replace(" ", "%20")
for p in range(1, NUM_PAGES + 1):
out = os.path.join(SHOT_DIR, f"page_{p:02d}.png")
run([CHROME, "--headless=new", "--disable-gpu", "--hide-scrollbars",
"--force-device-scale-factor=1", "--window-size=1920,1080",
f"--screenshot={out}", "--virtual-time-budget=2800",
f"{file_url}?page={p}&clean=1"])
if not os.path.exists(out):
raise RuntimeError(f"截图失败 page {p}")
print(f"截图 page {p:02d}")
def step3_video(page_dur):
tmp_list = os.path.join(SHOT_DIR, "vlist.txt")
with open(tmp_list, "w") as lf:
for p in range(1, NUM_PAGES + 1):
img = os.path.join(SHOT_DIR, f"page_{p:02d}.png")
dur = max(page_dur[p], 0.8)
vid = os.path.join(SHOT_DIR, f"v_{p:02d}.mp4")
run(["ffmpeg", "-y", "-loop", "1", "-i", img, "-t", f"{dur:.3f}",
"-r", "30", "-pix_fmt", "yuv420p",
"-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,"
"pad=1920:1080:(ow-iw)/2:(oh-ih)/2",
"-c:v", "libx264", "-preset", "medium", "-crf", "20", vid])
lf.write(f"file '{vid}'\n")
print(f"页视频 {p:02d} {dur:.2f}s ✓")
silent = os.path.join(SHOT_DIR, "silent.mp4")
run(["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", tmp_list, "-c", "copy", silent])
run(["ffmpeg", "-y", "-i", silent, "-i", AUDIO_IN,
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-shortest", VIDEO_OUT])
print(f"\n✅ 视频已输出:{VIDEO_OUT} ({ffprobe_dur(VIDEO_OUT):.1f}s)")
def main():
if not os.path.exists(AUDIO_IN):
print(f"ERROR: 找不到音频 {AUDIO_IN}", file=sys.stderr); sys.exit(1)
print("=== 步骤 1/3:分配每页时长 ===")
page_dur = step1_page_durations()
print("\n=== 步骤 2/3:截取 12 页 ===")
step2_screenshots()
print("\n=== 步骤 3/3ffmpeg 合成 ===")
step3_video(page_dur)
if __name__ == "__main__":
main()