一庭草木 v2 — 自愈流水线
v2 关键改进(2026-07-11 实战验证通过)
改进 1:图片生成安全网(修图片阶段全挂场景)
不再直接调 mmx image generate,改用 scripts/gen_images.sh:
- 单图独立 3 次重试(间隔 5s)
- 已存在且 >10KB 自动跳过(断点续传)
- 一张失败不影响其他 7 张
- 退出码明确(0=全成功,1=部分失败,2=prompt 配置错)
改进 2:atempo 反向 bug 修复(修时长超限场景)
- 修复前(v1 错):ratio = target/d → 反向变速
- 修复后(v2 正):ratio = d/target → 正确方向,新时长=d/ratio
- 容差 ±0.1s 边界,避免 atempo 后误判超限再触发 LLM
- 实战:32.78s → 32.04s(077 石蒜)、34.75s → 32.04s(079 大丽花)
改进 3:LLM 失败兜底(修 MiniMax-Text-01 500 错误场景)
v1 静默失败 → v2 检测后改用强行 atempo 链式(±30% 容差)兜底,再不行才报清晰错误。
改进 4:状态机 + 断点续传(修中途断电场景)
每期生成 references/_state.json 记录 11 个 step 的完成时间戳。
make_video.py --resume 扫 _state.json + 产物,自动从第一个未完成的 step 续传。
改进 5:产物验证报告
补做任务新增 references/verify_report.json,含:
- 5 个必需文件存在性 + 大小
- video/audio 时长对齐(drift < 0.5s)
- voice/video 在 [28, 32] 区间
- 字幕 PNG 数 == subtitles.json 条数
- shots 数 ≥ 8
- 总状态 PASS/FAIL
目标
输入:植物名称(中文)(可选:角度/主题)
输出:完整 30s 抖音视频,自带封面、字幕、口播、8 张 B-roll、发布包。
out/{series}_{episode_no}_{plant}/
├── video.mp4 (最终成片)
├── voice.mp3 (口播音频)
├── footer.png (固定底栏)
├── cover_vertical.png (1080×1440)
├── cover_horizontal.png (1440×1080)
├── shots/001.png...008.png (8 张 AI 竖屏 B-roll 原图)
├── subpng/sub_01.png... (字幕 PNG 层)
└── references/
├── voice.txt (口播纯文本)
├── subtitles.json (字幕时间轴)
├── prompts_<plant>.txt (8 个差异化提示词)
├── publish.md (发布包)
├── voice_meta.json (TTS 时长元数据)
├── verify_report.json (v2 验证报告)
└── _state.json (v2 状态机)
series 默认 庭中一枝,episode_no 建议三位:001/002/003...
固定品牌规范(已锁定)
规范文件:references/spec.json
- 画幅:1080×1920 | 30fps | 30s
- 口播时长:duration_min=28s, duration_max=32s(硬限制)
- 印章:180×180, (72,96),安全边距启用
- 底栏:30秒 · 庭中一枝 + #000000 底条(25%透明度,高96px,圆角12,文字垂直居中+上移15px光学微调)
- 字幕:PNG字幕卡;字幕框 y0=H-430;与底栏间距≥50px
- 裁切:居中裁切(scale to cover + center crop)
- 配音:纯文本严格朗读(不使用 SSML),年轻聊天气质
工作流(v2,8 步)
Step 0:创建当期 episode 目录
python3 scripts/make_video.py "植物名" --series "庭中一枝" --episode_no "XXX" --init-only
- 创建
out/{dir}/shots/subpng/references/ - 立即退出;后续步骤全部写入该目录
- v2:自动写
_state.json[0_init]
Step 1a:生成 8 个差异化提示词(主 agent 用 LLM)
调用 MiniMax LLM 生成 8 张图的 prompt,保存到 references/prompts_<plant>.txt。
Prompt 模板:
**Role:** 你是一位精通《中国植物志》的分类学家,同时也是一位具备电影级审美(参考:安哲罗普洛斯式的迷雾长镜头、库布里克式对称光、塔可夫斯基的自然诗意)、擅长利用 MiniMax 模型特性的视觉导演。
**Task:** 请针对植物「{plant}」生成 8 组极致专业的 AI 生图提示词,用于 9:16 竖屏短视频创作。**重要:8组提示词相互独立,每一组将被单独送入生图模型,因此每一组提示词中都必须明确出现「{plant}」全称,严禁使用任何代词。**
**Core Requirements:**
1. **命名规则(CRITICAL):** 每一行必须明确包含「{plant}」全称。不得省略,不得使用代词。
2. **生物学真实性(CRITICAL):** 描述必须严格符合「{plant}」的实际性状。
3. **八大维度分镜(顺序固定):**
- ① 全景生态:生境类型与空间纵深感
- ② 中景姿态:整体生长结构、枝干走势
- ③ 单株特写:主体形态美,占画面1/2以上
- ④ 微距解剖:花药、气孔、腺鳞、刺突等(60-80字)
- ⑤ 光影剪影:逆光/侧逆光轮廓、电影氛围
- ⑥ 物候特征:特定季节状态(花期/果期/秋色)
- ⑦ 生境叙事:与古建/流水/岩石/动物的共生关系
- ⑧ 意境终章:留白、意象或中国传统色结尾
4. **MiniMax 垂直适配:** 强调竖构图9:16(仰拍、纵向延伸、画面上方留白、垂直构图)。中国色优先。
5. **输出格式约束:** 纯文本,每行以 `——` 起头,行末换行。禁止 Markdown、编号、主观词汇("漂亮""美丽"等)。
6. **长度与容错:** 每行 40-80 字(微距解剖可达 80 字),可用分号连接。
请直接输出 8 行提示词,不要任何解释。
输出格式:8 行,每行以 —— 起头,紧跟植物全称。
Step 1b:v2 用 gen_images.sh 而非直接 mmx
bash scripts/gen_images.sh "植物名" \
"references/prompts_<plant>.txt" \
"shots"
安全网保证:
- 单图 3 次重试(间隔 5s)
- 已存在且 >10KB 自动跳过(断点续传)
- 一张失败不影响其他 7 张
- 退出码:0=全成功,1=部分失败,2=prompts 格式错
绝对不要:
- 直接调用 mmx image generate(已废弃)
- 用 --n 2 批量
- 加 --prompt-optimizer/--aigc-watermark/--subject-ref
Step 2:写口播稿(主 agent 用 LLM)
写入 references/voice.txt:
- 字数:控制在 125-145 字区间
- 结尾固定:庭中草木,明日再看。(必须)
- 知识密度:只讲 1 个硬知识点
- 风格:年轻聊天,不背书
- 禁止:SSML/XML 标签、镜头指示、中文引号
Step 3:写字幕时间轴(基于 TTS WordBoundary)
主 agent 用 MiniMax LLM 写 references/subtitles.json:
- 每段 ≤ 12 字(硬性)
- 总字数 = voice.txt 字数
- 零缝隙:每段 end = 下一段 start - 0.001s
- 末段 end ≈ voice_dur(差值 < 0.05s,否则 Step 5b 误压缩)
TTS WordBoundary 获取代码:
import asyncio, edge_tts
async def get_word_timings(text, voice='zh-CN-YunxiNeural'):
submaker = edge_tts.SubMaker()
communicate = edge_tts.Communicate(text, voice=voice, boundary='WordBoundary')
async for msg in communicate.stream():
if msg['type'] == 'WordBoundary':
submaker.feed(msg)
return [(c.content, c.start.total_seconds(), c.end.total_seconds()) for c in submaker.cues]
必须验证 4 项(不全 PASS 则禁止输出):
# [1] 总字数 == voice.txt 字数
# [2] 每行 ≤ 12 字
# [3] 零缝隙(gap ≈ 0.001s)
# [4] 末段 end ≈ voice_dur(差值 < 0.05s)
Step 4:生成发布包(主 agent 用 LLM)
写入 references/publish.md:
**标题A:** 植物名
**标题B(古语风格,3 个备选):**
1. [古语风格标题,画面感]
2. ...
3. ...
**简介:** 50-100 字
**话题:** #庭中一枝 #一庭草木 #草木小识 #[植物名] #[特征词]
**置顶评论:** 20-30 字互动问题
Step 5a-6:跑 make_video.py(v2 自愈核心)
python3 scripts/make_video.py "植物名" --series "庭中一枝" --episode_no "XXX"
脚本自动处理:
1. TTS:edge-tts 不传 rate(否则 "No audio received")
2. 时长控制(v2 修正):ratio = d/target(v1 反向 bug 已修)
3. 容差:±0.1s 边界,避免误判超限
4. LLM 失败兜底:MiniMax-Text-01 500 时改用 atempo 链式(±30%)
5. 字幕压缩(Step 5b):仅在 |原始末段end - voice_dur| > 0.05s 时触发
6. 合成视频:filter_complex concat(不能用 concat demuxer,否则帧率误差)
--init-only:只创建目录
--keep-voice:保留现有 voice.mp3(如时长在区间)
--resume:从 _state.json 推断续传点
v2 状态写入:
- 完成后 5a_voice、5b_subs_sync、6_video 自动写状态
Step 7:生成封面
python3 scripts/make_cover.py "植物名" --episode-dir "out/<dir>"
| 文件 | 尺寸 | 比例 | 用途 |
|---|---|---|---|
cover_vertical.png |
1080×1440 | 3:4 | 抖音封面、小红书竖版 |
cover_horizontal.png |
1440×1080 | 4:3 | 微博、横屏视频封面 |
Step 8:v2 产物验证(必须执行)
读取 references/verify_report.json(v2 流程自动生成),或主 agent 自检:
- 5 个文件齐全:video.mp4, voice.mp3, cover_vertical.png, cover_horizontal.png, footer.png
- 8 shots + N subs PNG 与 N 字幕条对应
- video/audio 时长对齐(drift < 0.5s)
- voice/video 在 [28, 32] 区间
voice_meta.json变速 ≤ 10%(passes_10pct_limit=true)
如果 verify_report.json 不存在或 status != PASS,必须修复后才能发布。
补做任务协议
对失败的期(参考 out/HISTORY.md 第 077 期实战):
- 诊断:
ls out/庭中一枝_*/references/_state.json(如有) - 恢复策略(按失败模式):
- Step 1b 失败的(图片 0/N) → 重跑gen_images.sh(已存在自动跳过)
- Step 5/6 失败的 → 用--resume让 make_video.py 推断续传点
- Step 5a voice 时长超限的 → 脚本自动应用 atempo 修复(重跑即可) - 验证:补做完成必须跑
verify_report.json检查,全 PASS 才能算完成
常见问题
| 问题 | 排查 |
|---|---|
| 字幕盖住底栏 | 检查 spec.json subtitle.box_y0 是否为 1490(H-430) |
| 字幕和语音对不上 | 必须用 TTS WordBoundary,不得估算 |
| Step 5b 误压缩 | 末段 end ≈ voice_dur(差值 < 0.05s) |
| edge_tts "No audio received" | 不要传 rate 参数,用 FFmpeg atempo |
| 口播时长超 32 秒 | 必须重写 voice.txt,不能继续加速 |
| voice.txt 漏片尾 | 每次写完检查是否以 庭中草木,明日再看。结尾 |
| B-roll 和字幕不同步 | make_video.py 用 filter_complex concat |
| 视频音频偏差 >1s | 检查 concat 是否用 filter_complex |
| 字幕总字数 ≠ voice.txt | 字幕分拆与 voice.txt 不一致,必须修正 |
| 图片阶段 0/N 全失败 | 用 gen_images.sh(已修),不再直接 mmx |
| 跑到一半断(缺 video/封面) | 用 --resume 让 make_video.py 推断续传 |
v2 实战验证记录
- 077 石蒜:voice 32.78s → atempo 1.0244 → 32.04s ✅ PASS
- 079 大丽花:mmx 0/8 →
gen_images.sh全成 → atempo 1.0859 → 32.04s ✅ PASS - 080 夹竹桃:跑到一半断 → state 推断 → make_video.py 续传 → 28.58s ✅ PASS