Add presentation slides for OU process and Mehler formula
Sync to site1 / sync (push) Has been cancelled

This commit is contained in:
gaojie
2026-06-03 04:08:01 +08:00
parent 5de86388db
commit 920439e044
39 changed files with 2385 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
#!/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()