起因是把一篇 6500 字的长文(一名阿里老员工的劳动仲裁自述)转成一条能发抖音/视频号的竖屏解说视频。最终产出 4’17”、1080×1920、带硬字幕和旁白垫乐的成片。本文整理完整方案、工具选型和踩过的坑,供下次直接抄。
一、管线总览
原文 ──① 需求拷问──▶ 设计决策树(形态/画幅/风格/口径/结构/声音/字幕)
──② 脚本先行──▶ 旁白脚本 + 分镜表(确认后才开始烧生成额度)
──③ 配音────▶ 分段 TTS(每场景一个音频文件 = 时间轴锚点)
──④ 素材────▶ AI 视频片段(情绪镜头) + PIL 手绘卡(制度/文字内容)
──⑤ 字幕────▶ 按音频真实时长 + 像素宽度断行 → 字幕条 PNG
── 合成────▶ 逐场景渲染(可缓存)→ concat filter + overlay 单遍 → BGM 混音
动手前有一件事最值得做:把模糊需求拆成决策树逐个拍板——成片还是单条片段?竖屏还是横屏?写实还是信息图风?实名还是匿名?第一人称还是第三人称?字幕怎么烧?这些决定互相咬合,跳着问会返工。本次的关键决策:
| 决策 | 选择 | 理由 |
|---|---|---|
| 镜头构成 | 混合:AI 片段只给情绪镜头,制度性内容全部做”证据卡” | 省 40%+ 生成额度;AI 画中文条款必翻车 |
| 实名口径 | 旁白用职位代称,画面文字仅英文名,无 logo | 平台审核与商誉风险的折中 |
| 结构 | 倒叙钩子(数字反差→权限全灰)→ 时间线 → 排比收尾 | 前 15 秒留人 |
| 字幕 | PIL 字幕条 + ffmpeg overlay 硬字幕 | 本机 ffmpeg 无 libass/drawtext |
| 旁白 | edge-tts · 云健 · rate=-8% | 与百炼 CosyVoice A/B 试听后胜出 |
内容分工原则(最有复用价值的一条)
- AI 视频片段:只用于”人/场景/情绪”镜头(会议室、灰掉的图标、仲裁委台阶),prompt 里写明
no text, no logos。 - PIL 手绘卡:一切含精确中文文字的画面——法条引用、系统界面复原、对话气泡、数据对比、人物关系图。程序绘制文字 100% 可控。
- 卡片配 zoompan 缓推(Ken Burns)避免”PPT 感”。
一句话分工:Pillow 管”画”,ffmpeg 管”动”,AI 视频管”活”,TTS 管”说”。
二、工具评估
| 工具 | 用途 | 评价 |
|---|---|---|
| VideoGen(wan30) | 文生视频 720×1280 | 可用;自带音轨(大坑,见五.1);风格指令执行偏保守(要”扁平2D”给了写实);色调稳定 |
| edge-tts | 旁白 | 免费、稳定、中文播报感好;mp3 头部元数据偶不可信,以解码后为准 |
| 百炼 CosyVoice v3 | 旁白备选 | HTTP 一次调用出签名 URL,质量可用 |
| Pillow | 证据卡/字幕/封面/片名 | 本次真正的”视觉主力”,见第三节 |
| ffmpeg 8.1(精简构建) | 全流程 | 缺 subtitles/drawtext 滤镜;overlay + enable 时间窗方案完全够用 |
| — | 为此项目引入 npm 视频框架性价比为负,未采用 |
三、Pillow:用代码做平面设计
Pillow 是 Python 标准图像处理库(pip install pillow)。它不是绘图软件,而是一套用代码画图的 API:给定坐标和颜色,画矩形/圆角矩形/圆/线段、按指定字体字号写文字、测量文字像素宽度、逐像素操作、导出 PNG。本次所有含精确中文的文字画面都是它生成的——效果等同 Figma 设计稿,但”设计稿”写在代码里:可批量、改一行重出图、文字永远清晰。
| 成片里的东西 | Pillow 能力 |
|---|---|
| 证据卡 ×6(数据对比、系统界面复原、对话气泡+人物关系图、时间线、结尾卡) | 渐变背景(画 1×1920 竖条再 resize 全屏)、rounded_rectangle 面板、色块条形图、textlength 测宽后居中/右对齐、按像素宽度给长引文断行 |
| 硬字幕 ×77 | 每句渲染成 1080×160 透明 PNG(半透明底条+双画伪加粗);Pillow 只画字条,“何时显示”由 ffmpeg overlay enable='between(t,起,止)' 控制 |
| 片名浮层 / 封面 | 透明底 PNG + 阴影偏移字;封面同卡片画法 |
| 字体 | macOS 自带 STHeiti Medium/Light.ttc,ImageFont.truetype(路径, 字号, index=0) |
四、背景钢琴垫乐:零依赖程序合成
BGM 不是下载的音乐也不是 AI 生成,而是纯 Python(仅标准库 math/struct/wave)的加法合成:
- 和声:Am→F→C→G 四和弦循环,每和弦 8 秒;
- 发声:基频正弦 + 2/3 次泛音(×0.45/×0.18)模拟钢琴;
- 包络:40ms 起音防爆音 + 双指数衰减;和弦内 0.18s 琶音错开 + 低八度铺底;
- 响度:峰值归一 -16dBFS 安静音床,混音
amix normalize=0保旁白电平,结尾 3s 淡出。
这是通用方案吗?作为兜底:是;作为默认:不是。 优点是零版权风险、时长任意、参数即改;局限是正弦”钢琴”经不起细听,BGM 一旦被听出来就露怯。升级路径:① 免版权真实钢琴曲(Pixabay Music 等,注意 CC 署名)——首选;② AI 音乐生成(Suno/Udio,注意商用授权);③ fluidsynth + SoundFont 真采样。经验法则:旁白密集型解说压到 -16dBFS 以下够用;音乐需要”被听见”的视频必须换真实音源。
五、踩坑实录(按严重度)
-
AI 视频片段自带音轨,抢掉旁白 ⚠️ 最隐蔽 症状:片段场景”有字幕没配音”、BGM 时断时续。 根因:
ffmpeg -i clip.mp4 -i vo.mp3不写-map时默认选”最佳音频流”,片段自带的 128kbps 音轨赢了旁白 mp3。 修复:显式-map 0:v:0 -map 1:a:0。凡是多输入合成,永远显式 map。 -
concat demuxer +
-c copy时间戳漂移 症状:13 个场景各自时长正确,拼接后 256s 变 435s,后续解码报错。 根因:各场景容器/流时长元数据不一致(tpad、-t 截断所致),copy 拼接把错误时间戳累加。 修复:改用 concat filter(重编码、重生成时间戳),并与字幕 overlay 合并为单遍。 -
filter_complex 输入索引差一
-i是成对参数,用len(inputs_list)数输入个数会翻倍。按”场景数 N → 图片输入从 N+1 起”显式计算。 -
生成额度耗尽(403) 8 段视频 + 试听把额度打满,1 个片段无法重做。对策:降级为 PIL 卡片——该场景反而成了全片信息密度最高的画面之一。经验:预留 1–2 次重做额度;卡片永远是兜底方案。
-
视频产物 URL 延迟可用(NoSuchKey) 任务显示完成但签名 URL 404——对象存储 finalize 有延迟。换新 URL 稍后重试即可,别过早判定失败。
-
时长估算偏差 中文旁白约 4.3 字/秒(-8% 语速),1000 字 ≈ 4 分钟,远超”2’30–3‘“预期。写脚本时先按
字数÷4.3估时长。 -
字体缺字形:STHeiti 无 ✕(U+2715)/⏱/,渲染成方块。改用
×(U+00D7) 等 CJK 兼容字符;发布前逐卡目检。 -
字幕断行:按字符数切会切出”两个核/心OKR”。按像素宽度贪心装箱(先切标点 token,超宽再按”的/——“细切)。
六、关键参数(下次直接抄)
- 场景时长 = 旁白真实时长 + 0.45s 尾垫;每场景首尾 0.25s 黑场淡入淡出(音频 afade 同步)
- 卡片运镜:
scale=2160:3840, zoompan z=min(1+0.00035*on,1.12) 居中, s=1080x1920:fps=30 - 片段适配:
factor=min(场景时长/片段时长, 1.3)(最多放慢 1.3×),不足则tpad=clone冻帧 - 字幕:46px 黑体、最大宽 880px、底部
overlay=0:H-260、半透明圆角底条 - 编码:libx264 crf19 medium;成片 1080×1920@30
七、Checklist
- 脚本+分镜先过确认,再消耗生成额度(样音用脚本第一段做 A/B)
- 先做决策树拷问:画幅/风格/实名口径/字幕方案/额度预算
- 含文字的镜头一律程序绘制,AI 只画”无字”画面
- 所有 ffmpeg 多输入命令显式
-map - 拼接用 concat filter,不用 demuxer copy
- 每段 TTS 解码后验证真实时长;字幕时间轴以音频为准
- 抽帧检查:每场景至少 1 帧 + 片段场景音量检测
- 交付五件套:成片 mp4 / 封面 / srt / 发布文案 / 脚本文档
本文整理自一次完整的 AI 辅助视频制作实践,管线代码(cards.py / subs.py / bgm.py / assemble.py)均在项目目录中,下一篇长文视频直接复用。