本节摘要:本节把 0.1 节的七段骨架缩成一条能跑的最小闭环。环境只要两样:Python 3.10 以上与 ffmpeg,外加 Pillow 画图。然后写一个三十余行的脚本:两段文字先用 Pillow 画成两张 1280 by 720 的字卡,再由 ffmpeg 的 concat 清单拼成各 2.5 秒的两镜,字幕用 SRT 文件烧录进画面,输出一条带字幕的五秒成片——全程本地完成,不依赖任何付费 API。跑通之后,本节给出「替换路线图」:最小闭环里的每个占位实现,在完整流水线里对应的真身是哪一节的哪个模块。0.1 节负责「看得懂」,本节负责「跑得通」。
全书的运行环境刻意保持朴素:Python 加 ffmpeg,没有重量级框架。组件清单如下:
| 组件 | 版本口径 | 用途 | 服务章节 |
|---|---|---|---|
| Python | 3.10 以上 | 流水线各段的胶水语言 | 全书 |
| ffmpeg | 6.x(以官网为准) | 拼接、转码、字幕烧录 | 第 0、5 章 |
| Pillow | 10.x 以上 | 生成字卡图片 | 本节 |
| moviepy | 2.x(以官方文档为准) | Python 剪辑 | 第 5 章 |
安装与自检(Windows 读者在 Git Bash 里执行;ffmpeg 若未入 PATH,到 ffmpeg 官网下载后把 bin 目录加进 PATH):
# bash —— 环境安装与自检 python -m pip install pillow python --version # 建议 3.10 以上 ffmpeg -version # 能打印版本即已安装 python -c "import PIL; print(PIL.__version__)"
四条命令都通过,环境就绪。本节不需要任何模型 API 密钥——这是刻意的设计:最小闭环的意义是验证「管线通」而不是「模型好」。
把七段加工压缩到一次能看完的规模:
两段文字 ──▶ [脚本段] 剧本清单(写死在代码里) │ ▼ 两张字卡 ──▶ [素材+生成段] Pillow 画图(占位:将来换成文生图/文生视频) │ ▼ concat 清单 ─▶ [剪辑段] ffmpeg 拼接(占位:将来换成 moviepy 时间线) │ ▼ 两行 SRT ───▶ [字幕段] 烧录进画面(占位:将来换成 Whisper 对齐) │ ▼ final.mp4 ──▶ [导出段] 五秒、30 帧、H.264
音轨段在最小闭环里缺席(无声成片),后半本第 6 章补齐。每个占位实现将来替换成哪个真模块,本节末尾给路线图。
一个文件,四十行以内:
# min_pipeline.py —— 最小流水线:两段文字、两张图、一条五秒成片(本地零 API 依赖) import subprocess from pathlib import Path from PIL import Image, ImageDraw, ImageFont WORK = Path("build") WORK.mkdir(exist_ok=True) FONT = "C:/Windows/Fonts/msyh.ttc" # macOS 可换成系统自带的中文字体路径 SEGMENTS = [ ("一句想法", "流水线的输入端"), ("一条成片", "流水线的输出端"), ] def make_frame(title, note, index): img = Image.new("RGB", (1280, 720), (24, 26, 32)) pen = ImageDraw.Draw(img) big = ImageFont.truetype(FONT, 96) small = ImageFont.truetype(FONT, 44) pen.text((640, 300), title, font=big, fill=(235, 235, 240), anchor="mm") pen.text((640, 470), note, font=small, fill=(140, 200, 255), anchor="mm") out = WORK / ("frame_%02d.png" % index) img.save(out) return out.as_posix() frames = [make_frame(title, note, i) for i, (title, note) in enumerate(SEGMENTS)] entries = [] for item in frames: entries.append("file '%s'\nduration 2.5\n" % item) entries.append("file '%s'\n" % frames[-1]) # concat 清单的已知细节:末帧需重复一次文件行 list_file = WORK / "list.txt" list_file.write_text("".join(entries), encoding="utf-8") srt_text = ( "1\n00:00:00,000 --> 00:00:02,500\n一句想法\n\n" "2\n00:00:02,500 --> 00:00:05,000\n一条成片\n\n" ) (WORK / "subs.srt").write_text(srt_text, encoding="utf-8") subprocess.run( [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", "list.txt", "-vf", "subtitles=subs.srt", "-r", "30", "final.mp4", ], check=True, cwd=WORK, ) print("成片已生成:", (WORK / "final.mp4").resolve())
三处值得停下来看的设计。其一,段间以文件为契约:字卡是 png 文件,拼接依据是 list.txt,字幕是 subs.srt,段与段之间不传内存状态——这就是 0.1 节「七段以文件衔接」的缩微版。其二,字幕走标准格式:SRT 是字幕段的通用语言,烧录滤镜一条命令完成,将来换成 Whisper 生成的 SRT,这一行不用改。其三,ffmpeg 在产物目录内以相对路径运行(cwd 参数),避开 Windows 盘符冒号在滤镜参数里的转义坑。
# bash —— 运行最小流水线并验收产物 python min_pipeline.py ls build # 应看到 frame_00.png、frame_01.png、list.txt、subs.srt、final.mp4 ffprobe -v error -show_entries format=duration -of csv=p=0 build/final.mp4
验收标准:final.mp4 存在,时长约 5 秒,播放可见两张字卡各 2.5 秒,底部字幕按时间码切换。常见故障三类:
| 症状 | 原因 | 处置 |
|---|---|---|
| ffmpeg 不是内部或外部命令 | 未安装或未入 PATH | 官网下载后把 bin 目录加入 PATH,重开终端 |
| 报错 OSError cannot open font | 字体路径不对 | 换成本机存在的中文字体文件路径 |
| 字幕乱码或不出 | SRT 编码或字体缺字形 | 确认 SRT 以 UTF-8 写出、字体含中文字形 |
最小闭环的价值在于它把每个占位实现的位置标清楚了。往完整流水线走,逐个替换:
| 最小闭环里的占位 | 完整流水线里的真身 | 落在 |
|---|---|---|
| 写死在代码里的两段文字 | LLM 结构化生成的剧本 JSON | 第 2.1 节 |
| Pillow 画的两张字卡 | 文生图 API 或素材库取图,再经图生视频成片段 | 第 3、4 章 |
| concat 拼两镜 | 分镜 JSON 驱动的 moviepy 时间线 | 第 5.2 节 |
| 手写的两行 SRT | Whisper 逐镜对齐自动生成 | 第 7 章(后半本) |
| 无声成片 | TTS 配音、音效与混音 | 第 6 章(后半本) |
| 手工敲一条命令 | Agent 编排的一条命令全流程 | 第 8 章(后半本) |
替换的次序建议按「素材与生成 → 剪辑 → 字幕 → 音轨」推进:先把画面的来源换成真模型(质量收益最大),再让剪辑吃上分镜 JSON,字幕与音轨随后。每替换一段,其余段不动——这条纪律在下一章升级为总论。
闭环跑通只是「能跑」,下一章回答「怎么跑得不散」——契约怎么定、目录怎么摆、失败怎么只重跑那一段。