副标题:一份可以照着跑的 AI 视频生产指南,以及那些文档里根本不会写的坑
0. 做了什么
输入是一段《华严经·入法界品》的经文(善财童子参访根自在主童子,约 600 字),输出是一部 5 分 25 秒、32 镜、1080p30 的水墨/敦煌风动画短片,包含:
- 40+ 张 AI 生成的水墨插画(逐镜运镜)
- 双声部配音(旁白 + 童子对白,其中童子是真正克隆成功的音色)
- 纯 ffmpeg 滤波合成的环境声(风/浪/瀑/钟/低鸣)
- 硬字幕 + SRT + B 站封面 + 发布文案
整个流程没有真人参与音画制作,全部脚本化,改一个台本 JSON 就能重出全片。这篇文章把这个流水线、以及每一步踩过的坑完整记录下来。
先给结论速览(细节在后文展开):
| 环节 | 方案 | 耗时占比 | 坑密度 |
|---|---|---|---|
| 插画 | 即梦 CLI text2image 2k 免费通道 | ~20% | ★★ |
| 配音 | CosyVoice2 克隆 + MOSS 预置音色 | ~35% | ★★★★★ |
| 合成 | ffmpeg 两遍法(zoompan + xfade) | ~25% | ★★★★ |
| 叙事设计 | autothink3 思维链 8 轮推演 | ~10% | ★ |
| 验收 | SenseVoice ASR 抽验 + 抽帧 | ~10% | ★★ |
1. 总体架构:一个 JSON 是唯一真源
整个项目能反复重渲不出错,靠的是一个核心设计:segs.json 是唯一运行时真源。
json
[
{"img": "y6a_浪起之前", "spk": "S2",
"text": "什么叫内心的烦恼?眼睛贪恋景象......",
"file": "audio/v12_21.m4a", "dur": 14.2, "asr": 0.94,
"card": null, "amb": null},
{"img": "z15_瀑流藏界", "spk": "", "text": "",
"card": "瀑", "amb": "waterfall",
"file": "audio/amb/waterfall.m4a", "dur": 11.0},
...
]
每个元素 = 一镜。字段涵盖:底图、说话人、台词、音频文件、时长、单字卡、环境声类型。所有下游脚本(TTS、字幕、合成、烧录)都只读这一个文件:
segs.json ──┬─ tts_*.py → 逐镜配音(断点续跑,达标跳过)
├─ amb_*.py → 无语音镜的环境声合成
├─ film_*.py → 逐镜 zoompan → xfade 终剪 → 混音
├─ gen_subs.py → 字幕 PNG + SRT
└─ burn_subs.py → 烧录字幕版
这个设计换来的能力:换图 = 改一个字段重渲一镜;换配音 = 清一个字段重跑一段;整体时长自动重算。后期"重构"需求来了以后,只花了一天就切到全新的六幕结构------架构收益占了一半功劳。
2. 插画:即梦 CLI 的使用与"鬼脸治理"
2.1 基本用法
bash
dreamina text2image --prompt="宣纸水墨淡彩,俯瞰视角......留白七成" \
--ratio=16:9 --resolution_type=2k --generate_num=2
2k(2560×1440)走标准通道免费 ,但排队时间波动极大(1 分钟到 1.5 小时都有),脚本必须按"提交后立刻 list_task 拿 sid → 轮询"写。
两个必须知道的坑:
- 提交后回读常报
ret=1015,但任务实际已创建 。别盲目重试(会双倍占队列),用list_task确认。 list_task是新任务在前。提交后立刻查列表,第一个就是本次任务,按位置绑定 sid。
2.2 鬼脸治理:提示词工程
AI 画国风人物最容易出现"鬼脸"(五官扭曲、瞳孔异常)。实测有效的提示词策略:
- 能不给正脸就不给:背影、侧身四分之三、远景剪影;
- 必须给脸时明确写"五官端正清晰对称、淡墨点染眉眼不细描";
- 忌讳"双髻童子背影"在中等景别下会出现------两个发髻会被渲染成"熊耳朵",宁可用光头或斗笠;
- 纯风景隐喻镜头(沙、水、镜、浪)直接不画人。
每镜出 2-4 张候选、人工(或视觉模型)挑选,是零成本的质量手段。
3. 配音:一场关于"音色克隆"的完整祛魅
这是全项目最曲折、也最有价值的部分。需求:旁白一个音色,童子(讲法者)用一段指定视频里的音色(B 站某戏曲/广播剧视频,73 分钟)。
3.1 三种失败姿势(MOSS-TTSD references 通道)
SiliconFlow 的 fnlp/MOSS-TTSD-v0.5 支持 references 参数做零样本克隆。文档没写清楚,靠 400 报错码试错:
json
// ✅ 唯一能通过的结构:元素是对象、字段名 audio、参考必须是 wav
{"model":"fnlp/MOSS-TTSD-v0.5","input":"[S1]文本",
"references":[{"audio":"data:audio/wav;base64,...."}]}
// ❌ 音色名对象 → 20044 ❌ mp3 base64 → 20015 ❌ 字符串数组 → 20015
结构对了以后,克隆质量依然很差:同一段参考,ASR 相似度三次能跑出 0.58 / 0.04 / 0.53。当时我们以为是参考音频太脏,换窗口、降噪折腾了很久。
3.2 真正的根因:槽位映射
后来设计了一组对照实验才定位。MOSS 的 references 数组是按位置对应 [S1]/[S2] 说话人标签的:references[N] 就是 [SN] 的音色。
- 旧管线只传了 1 个参考(落在 S1 槽),却用
[S2]让模型说话------等于让模型对着一个没有音色样本的空槽自由发挥,输出全是 0.7~4.8 秒的残句; - 改成
[S1]+ 单参考后,输出基频立刻贴合参考(见下文基频验证),克隆真的发生了。
教训:接口文档不写的语义,要用 A/B 实验去打。
3.3 "参考音频太长"的证伪:长度扫描实验
用户提出"是不是参考太长导致的问题"。实测结果部分证伪:
| 参考长度 | 表现 |
|---|---|
| 5s | 内容可达 1.00 但易带歌唱杂质 |
| 8s | 最稳(r=0.94,无杂质) |
| 12-15s | 方差大 |
| 20s | 模型续写跑飞(一条冲到 63.9 秒) |
结论:8 秒左右最优,但比长度更重要的是参考的内容纯度------句首对齐(从完整句开始)、无音乐无唱腔。怎么找纯净水域?用基频稳定度扫描全片:
python
# 逐 40ms 帧、60-420Hz 自相关找基频;逐秒统计中位 f0 与离差
# f0 稳定 + 有真正静音间隙(无持续 BGM 填底)的窗口 = 好参考产地
ac = np.correlate(fr, fr, 'full')[fl-1:]; ac /= (ac[0]+1e-9)
lo, hi = int(sr/420), int(sr/60)
k = ac[lo:hi].argmax(); f0 = sr/(lo+k) if ac[lo+k] > 0.3 else 0
怎么验证"克隆出的声音就是参考的声音"? 听觉不可用(自动化流程),用基频当指纹:参考 f0 中位 206Hz,正确槽位的克隆输出 208-232Hz(≈参考),预置音色对照组只有 110Hz------这就是"音色真的转移了"的量化证据。
3.4 最终方案:CosyVoice2 官方克隆通道
MOSS 的内联 references 即便结构正确,方差仍然太大(1/6 达标)。切换到 CosyVoice2-0.5B 的官方克隆流程后一锤定音:
bash
# 第一步:上传参考音频,创建自定义音色(wav base64 + 与音频严格一致的转写文本)
curl -X POST https://api.siliconflow.cn/v1/uploads/audio/voice \
-d '{"model":"FunAudioLLM/CosyVoice2-0.5B","customName":"tongzi",
"audio":"data:audio/wav;base64,...",
"text":"便被万千的弟子围绕起来,围得水泄不通......"}'
# → {"uri":"speech:tongzi:xxx:yyy"}
# 第二步:用 uri 生成
curl -X POST https://api.siliconflow.cn/v1/audio/speech \
-d '{"model":"FunAudioLLM/CosyVoice2-0.5B","input":"文本","voice":"speech:tongzi:xxx:yyy"}'
生产实测:23 段台词全部通过(相似度 0.91-1.00),泛化验证 3 take 里 2 条内容满分。
3.5 语音 QA 自动化:不信耳朵信指标
自动化流程里每一条配音都要过四道门:
- 时长门:预期时长 = 汉字数 ÷ 语速常数,超出 0.5×, 2.0× 直接弃(TTS 模型会"续写");
- ASR 转写:SenseVoiceSmall 转写生成结果;
- 拼音相似度 :
pypinyin转无调拼音 +difflib.SequenceMatcher,阈值 ≥0.85; - 🎼 拒收:SenseVoice 检出歌唱/哼唱标记直接弃------这是配音里混入唱腔的最可靠信号。
每段最多 4-6 take 取最优,全程无人工。
3.6 意外发现:白话比文言稳定得多
项目中期把半文半白的台本改写成白话口语后,TTS 一次通过率从"需要多 take 挑优"变成"基本 take0 全过"(32/32)。文言翻译腔对 TTS 和 ASR 都是分布外文本------这个经验对所有中文语音项目适用:先为机器把文本"说人话",再谈音色。
4. 合成:ffmpeg 两遍法与那些报错都救不了你的坑
4.1 两遍法架构
一镜一图一缓推(Ken Burns),先逐镜渲染小 mp4,再用 xfade 链接成片:
逐镜: -loop 1 -t D -i img.png
→ scale=3840:2160, zoompan=...:d=1:s=1920x1080:fps=30 (2k 超采样防抖动)
→ h264_nvenc
终剪: N 路输入 → xfade=fade:duration=0.7:offset=... 链式叠化
→ 逐镜 adelay 对轴 → amix → loudnorm
音频对轴公式:start_i = Σ_{j<i}(dur_j + pad_j) − i × 0.7,旁白在 start_i + 0.5 入画。
4.2 坑清单(每一条都真实付出过时间)
① zoompan 的 d 参数
-loop 1 -t D 输入上用 zoompan 必须写 d=1。写成 d=N 会把帧数放大 N 倍------症状是单镜渲染 20 分钟不结束、文件无限膨胀。
② conda 版 ffmpeg 没有 libmp3lame / drawtext / libass
mp3 编码不可用(一律 aac/.m4a);字幕没法用滤镜烧------改用 PIL 渲染透明 PNG 字幕条,再 overlay 进视频:
python
d.text((x+2, y+3), line, font=f, fill=(15,12,8,160)) # 投影层
d.text((x, y), line, font=f, fill=(252,250,244,255),
stroke_width=3, stroke_fill=(22,17,12,235)) # 描边正文
# ffmpeg: overlay=0:0:enable='between(t,开始,结束)'
PIL 字幕反而更可控:均衡折行、行首标点规避、说话人彩色小标签,都是几行代码的事。
③ loudnorm 会把采样率拉到 96k/192k
混音链末尾必须 aresample=48000,否则成片音频规格异常。
④ 单帧图 overlay + fade = 全透明
片头标题卡最初用第二个输入 overlay + fade=t=in:st=1.0:alpha=1,结果完全不可见。原因:单帧图片输入只产生 t=0 的一帧,fade 从 st=1.0 起步意味着这一帧的 alpha 永远是 0。解法:标题/单字卡全部改成 PIL 预合成到底图上,再进 zoompan。
⑤ curl 传 JSON 用 --data @-
脚本里写成 --data - 会把字面量 - 当请求体发出去,API 静默报错------所有段全部"秒失败"时先查这个。
4.3 零素材环境声:纯滤波合成
幕五"旁白退场、只留天地声"的段落,没有音效素材库,全部用 ffmpeg 滤波合成:
bash
# 浪涌: 棕噪声 + 低通 + 慢 tremolo(呼吸感)
anoisesrc=color=brown:d=11, lowpass=f=460, tremolo=f=0.16:d=0.95, volume=1.2
# 风声: 带通更宽 + 更快的起伏
anoisesrc=color=brown:d=11, highpass=f=180, lowpass=f=950, tremolo=f=0.45:d=0.8
# 低鸣(阿赖耶底色): 双正弦叠加
sine=f=55, sine=f=82.4, amix=2
# 钟声: 高频正弦 + 长衰减 + adelay 错位
sine=f=1318:d=3, sine=f=1760:d=2.6(延迟1.7s), lowpass=f=4000
输出统一 apad → afade in/out → -t 定长,混音时与旁白同级进 amix。成本为零,氛围成立。
5. 叙事重构:让思维链替你"读懂"原文
初版是按经文顺序逐句解说的 33 镜,能看,但平:无高潮、节奏均质、"教学清单感"重。
重构没有拍脑袋,而是把原文骨架 + 旧版问题 + 约束条件写进 autothink3 思维链系统(Deepseek-v4 网关,8 轮,采样+元认知评分),跑出的核心洞察值回票价:
原文表面是"参访得法"的线性故事,内在是一条垂直下潜的隧道。
前三幕所有"能建城邑、善调仙药",在"境界风飘心海"面前,全是沙地上的印痕。
旧版的病根:把"下坠"拍成了"散步"。
由此推导出的六幕结构与情绪曲线 平---平---急---静---深---净:
| 幕 | 内容 | 声音设计 |
|---|---|---|
| 一 行 | 晨雾南行、虚空来讯 | 旁白极简 |
| 二 见 | 河洲万童聚沙,"塌了就再堆" | 游戏感 |
| 三 巧 | 百工快剪蒙太奇 → 沙城轰然坍塌 → 脚边一洼水 | 最快,骤落 |
| 四 静 | 水洼映出一生生老病死 | 骤停,全片"空"点 |
| 五 海 | 四喻单字卡:瀑·镜·风·浪,旁白完全退场 | 纯环境声 → 低鸣 |
| 六 金 | "如炼真金",单字卡"金" | 一粒沙落定的静 |
思维链也有跑偏的时候:第 3-8 轮滑向了"弹幕实时塑形业力沙塔"的交互艺术构想------很有想象力,但对预渲染视频完全不可实施。取舍纪律:取其结构论,弃其不可实施项。 人依然是编辑,思维链是高产的思想搭档。
6. 验收方法论:自动化流程里"对齐"必须自证
视频、语音、字幕三轴对齐不能靠构造时"应该对齐",要从成片反抽验证:
python
# 从成片按时间轴抽 6 秒音频 → ASR → 与台本比对拼音相似度
ffmpeg -ss {t0} -t 6 -i 成片.mp4 -vn -ac 1 -ar 16000 chk.wav
# 4-5 个采样点(开头/中段/转场/结尾)全部 ≥0.8 才算过
配合抽帧检查(字幕样式、单字卡、转场)和 volumedetect(环境声段落响度),三类问题各有一次被这套流程当场抓住:字幕说话人标签错位、片尾字幕与出处卡重叠、环境声过短被截断。
7. 产出与成本
| 交付物 | 规格 |
|---|---|
| 成片(烧字幕) | 324.9s,1080p30,~229MB |
| 成片(无字幕母版) | 同上,~252MB |
| SRT / 封面 / 发布文案 | 1920×1080 封面,PIL 合成 |
| 可复用生产脚本 | 10+ 个,全部参数化 |
计算成本:插画走免费通道;TTS/ASR 按 token 计费约几十元级;合成全部本地 NVENC。真正的大头是试错时间------而试错之所以能收敛,靠的是第 1 节的"单一真源"架构和第 3.5 节的自动 QA:每一次失败都变成一条可执行的规避规则。
8. 最后的三条元经验
- "接口没写的语义"要用 A/B 实验打出来。槽位映射那个 bug,文档里一个字都没有,三组对照实验十分钟定位------比读十遍文档有用。
- 自动化流水线的每一环都要有机器可读的验收指标(拼音相似度、时长门、🎼标记、基频指纹、成片反抽 ASR)。耳朵不可信也不可扩展,指标可以。
- AI 生成内容的质量上限,取决于你把"领域判断"翻译成"机器可执行约束"的能力。鬼脸治理是提示词约束,好参考音频是 f0 稳定度约束,叙事结构是情绪曲线约束------都是同一个动作。
附:全部生产脚本与台本结构在项目内 scripts/ 目录,本文所有代码片段可直接用于同类流水线。