故事情景教学视频制作
0. 定位与取舍
定位:把课文、古诗、故事快速做成带配音的教学视频——速度优先的轻量链路,
全流程自包含(配音、出图、H3 生成、合成四条链路全部由本技能自带脚本完成,
不调用任何其它技能)。
链路设计(速度优先的取舍):
| 维度 | 本技能的取舍 |
|---|---|
| 通道 | 固定 metaso H3(S12) |
| 配音 | 分支A 逐段 GEM-3.1-TTS 全量配音;分支B 另加每角色一段参考音频——只用于驱动口型,绝不进成品(S15) |
| 段落产物 | H3 出画面(自带音轨默认丢弃、环境音可选保留)+ GEM-3.1 原声后期混入(两分支同) |
| 验收 | 不跑重型机器验收链;保留闸①(静帧 + 试听音频逐条人工过目)、闸②(逐段成片 30 秒内过目) + Step 6 三项 ffprobe 客观验收 |
| 确认闸 | 三道(参考图+参考音频 → 成片确认 → 交付) |
| 适合 | 快速出片、测试、草稿、日常教学 |
代价(要知道放弃了什么):无时间轴精调(切点不受静音区约束)、无响度标定、
无字幕-语音逐字对轴(字幕按标点拆短语、单行轮播显示,非逐字同步)。追求精修时再按需补工序。
> ⚠️ 门控是默认,不得擅自代选(2026-10-09 教师指令,最高优先级):体裁判型(A/B/C)、画风、音色(参考音频)、配音等一切需要用户决策的设定,必须通过对应闸口或 AskUserQuestion 让用户确认,AI 不得自作主张选定。唯一例外:用户在本次对话中明确说出「全自动运行」(或等效表述)时,方可由 AI 代选并直接出成果。 用户没有明确提出要求的,一律走门控——把记忆里「技能测试全自动」之类的偏好套到真实任务上,视为违规。
> ⚠️ 只允许 metaso 通道(2026-10-09 教师指令):本技能固定 metaso 通道,脚本 h3_video.py 只对接 metaso——--provider 仅接受 metaso、密钥只认 METASO_API_KEY(不读 .env)。任何环节都不得调用其它上游通道。
0.5 两条分支(先选分支再开工;分支按「段」选,不是按「片」选)
| 分支A 纯朗诵/旁白版 | 分支B 分角色配音版 | |
|---|---|---|
| 判据 | 画面里的人不该开口(旁白是画外音) | 画面里的角色在说话(对白/独白) |
| 典型 | 古诗朗诵、散文朗读、讲解型 | 故事、情景剧、多角色对话 |
| H3 收音频 | 不收(audio: "none") |
收(该角色的 GEM-3.1 参考音频) |
| 提示词含台词 | 绝不含(含了就驱动嘴动) | 必须含(<d>[Chinese]…</d> + <Audio 1>) |
| 画面人物口型 | 闭口、不跟读 | 口型同步 |
| 配音来源 | GEM-3.1 TTS 全程配音(单一音色) | GEM-3.1 TTS 分角色配音(每角色一音色) |
| 成品音轨 | 后期混入 GEM-3.1 原声 | 后期混入 GEM-3.1 原声(H3 音轨丢弃) |
| 已验证 | ✅《卖炭翁》2026-10-10 | ✅《卖火柴的小女孩》《咏鹅》《绝句》2026-10-09 |
一句话判据:画面里的人该不该开口说话?不该 → 分支A;该 → 分支B。
⚠️ 分支是「逐段开关」,不是「整片二选一」
H3 按段生成,同一部片子里可以逐段切换分支。混合型作品(如《卖火柴的小女孩》:
旁白 + 小女孩 + 奶奶对白)就是逐段混用——旁白段走分支A(audio:none + 提示词无台词),
角色段走分支B(传参考音频 + 提示词含台词)。在 plan.json 里逐段标注 mode 即可(见 §2)。
共同下游(两分支完全一致)
GEM-3.1 TTS 逐段配音并实测时长
↓
H3 出画面(分支A 不收音频 / 分支B 收角色参考音频)
↓
丢弃 H3 自带音轨
↓
ffmpeg 混入 GEM-3.1 原声(atempo 对齐画面时长)
↓
封面蒙板 + 楷体字幕 + concat
两条分支只差中间一行:H3 收不收音频、提示词含不含台词。其余流程完全相同。
> ⚠️ 成品配音一律 GEM-3.1 原声(2026-10-10 教师指令):H3 收到参考音频后走的是
> r2va 重渲染——不是透传原声,而是用自有语音引擎重说一遍:采样率 24k→32k、
> 声道 mono→stereo、并叠加空间混响(教师实测听到的「回响」)。因此分支B 也必须
> 丢弃 H3 音轨、后期混入 GEM-3.1 分角色原声;「要口型同步」是付这个代价的唯一理由。
1. 核心机制(必读)
分支B:参考音频驱动 → 画面 + 口型一步出齐
metaso H3 参考生模式下,把「参考图 + 提示词 + 参考音频 + 台词」一起发给 H3——
参考音频只借音色(<Audio 1> 声明),台词写在 <d>[Chinese]…</d> 里,
H3 用参考音色把台词说出来,画面角色口型同步。
三要素缺一不可(详见 references/prompt-v2.md):
1. 参考音频真的传了(--ref-audio);
2. 提示词有 <Audio 1> 音色参考声明 + 无禁人声句;
3. 提示词有 <d> 台词,且按旁白/角色分流写口型规则。
参考音频用哪一段:两种策略,二选一(须经闸① 与用户确认,S11/S13)
| 策略 | 做法 | 适合 |
|---|---|---|
| ① 固定试听片(传统) | 每角色一段 5~8s 试听片,全片固定,只借音色 | 音色稳定性优先、角色多、台词短 |
| ② 逐段原声(推荐,口型更准) | 把该段台词的 GEM-3.1 原声直接当该段参考音频(时长须落在 metaso 硬限 2~15s 内) | 对轴精度优先、长台词 |
策略② 的原理:H3 听到的就是最终要混进去的那条音频的时间轴,口型与成品音轨天然对齐,
后期替换音轨时几乎不需要 atempo 拉伸。两种策略不得混着用在同一角色上——音色会跳。
plan.json 侧的实现差别:策略① 靠顶层 ref_audio 表按 role 查;策略② 在该段写
"audio": "tts_out/<key>.wav"(显式优先于 ref_audio 表)。
分支A:不收音频 + 提示词无台词 = 人物闭口
H3 只要读到「有人在念这句台词」就会驱动嘴动——2026-10-10《卖炭翁》实测:
即便 audio:"none" 不传任何音频,只要提示词里残留 <d> 台词行,画面人物照样对口型。
反过来,把台词行删掉后,清晰面部特写也能闭口(无需改成背影/不露脸构图)。
所以分支A 的规则是做减法:
1. audio: "none"(不传参考音频);
2. 提示词删除台词行(<d>)与 <Audio 1> 声明行;
3. 提示词保留/补回禁人声句 no human voice, no speech, no singing;
4. 加一句闭口约束(保险):All on-screen characters remain completely silent with their lips gently closed.
> ⚠️ gen_prompts.py 是分支B 的派生器(它做的正是"删禁人声句 + 加 <Audio 1> +
> 加台词块"这三处改造)。分支A 的段不要对它跑 gen_prompts.py——会正好把台词加回来、
> 把禁人声句删掉,直接导致人物对口型。分支A 一律用 gen_v2_A.py 派生(它从静帧提示词
> 逐字继承 + 追加闭口约束,并内置四项自检)——不要手写 v2,手写必漏删台词。
2. 项目数据源:plan.json
所有脚本读同一份 plan.json(放项目根目录),这是唯一需要手工准备的数据文件:
{
"project": "卖火柴的小女孩",
"workdir": ".",
"h3_dir": "h3",
"ref_audio": {
"VO": "tts_out/audition_narrator_m.wav",
"小女孩": "tts_out/audition_girl.wav",
"奶奶": "tts_out/audition_grandma3.wav"
},
"segments": [
{"key": "cover", "image": "stills/still01.png",
"source_prompt": "prompts/cover.txt", "prompt": "prompts_v2/cover.txt",
"role": "VO", "speaker_en": "The narrator",
"line": "《卖火柴的小女孩》。安徒生。",
"duration": 6, "sub": false,
"is_cover": true,
"voice_wav": "tts_out/cover.wav"},
{"key": "01a", "image": "stills/still01a.png",
"source_prompt": "prompts/seg01a.txt", "prompt": "prompts_v2/seg01a.txt",
"role": "VO", "speaker_en": "The narrator",
"line": "天冷极了,下着雪,又快黑了。",
"duration": 8, "sub": true},
{"key": "02a", "image": "stills/still02a.png",
"source_prompt": "prompts/seg02a.txt", "prompt": "prompts_v2/seg02a.txt",
"mode": "B", "role": "小女孩", "speaker_en": "The little girl",
"line": "奶奶!", "duration": 5, "sub": true,
"audio": "tts_out/audition_girl.wav"}
]
}
字段说明:
mode(分支开关,逐段标注,见 §0.5):"A"= 纯朗诵/旁白版(不收音频、
提示词无台词、人物闭口);"B"= 分角色配音版(收参考音频、提示词含台词、口型同步)。
不写则按role推断:role为空/"VO"→ A;role为角色名 → B
(判定实现在_common.seg_branch(),全部脚本共用同一份;推断结果会打印出来,
不会静默)。混合型作品逐段混用(旁白段 A、角色段 B),建议显式写上。role空或"VO"= 旁白(自动用 ref_audio.VO,画外音闭口);
其它值 = 角色名(自动查 ref_audio 表,口型同步);audio= 该段的参考音频(显式路径优先于按 role 查表;"none"强制不传)。
仅生成阶段用(喂 H3 驱动口型)——分支A 一律不传,写不写都不影响(脚本强制剔除);voice_wav= 该段的成品配音(混入成片的人声,相对 workdir)。缺省时读
<tts_dir>/<key>.wav(即与段 key 同名)。封面段建议显式写(如tts_out/cover.wav);
两者语义完全不同,不要混用(历史事故:封面把试听片写进audio,成片封面念了试听句);prompt(v2 输出路径,隐性必填):gen_segments 读它提交 H3、finalize 不读;
缺省时 gen_prompts 退回prompts_v2/seg<key>.txt;sub: false该段不烧字幕;duration4~30 整数秒;
> 上面 JSON 三类段各示一例:封面段(is_cover + voice_wav)、分支A 旁白段(无音频)、
> 分支B 角色段(audio 参考音频 + 提示词含台词)。分支A 段的 v2 提示词不含台词块。
is_cover: true 封面段专用——finalize / assemble 只经 --cover(蒙板版)拼入成片,
循环里会跳过该段,不加此标记成片会出现两个封面(实测教训)。脚本对 key=="cover"
的段做兜底处理并打印告警,但请一律显式写 is_cover: true(判据统一在
_common.seg_is_cover())。
note(可选)= 朗读/读音提示(英文),仅分支B 有效(gen_prompts 写进台词块框架句,
<d> 之外);分支A 不读该字段,读音要在 TTS 侧用 --style 钉(如 怒号的 号 读 háo)。
封面段配音:finalize.py 找封面配音的顺序是——① 封面段 voice_wav;
②(老 plan 兼容)封面段 audio;③ <tts_dir>/cover.wav。三者都无则告警并按
静音封面拼入(违反 S18)。建议显式写 voice_wav,别写 audio。
顶层可选字段(其余脚本会读):
- workdir(示例写 ".",即 plan.json 所在目录;换机不用改)
- h3_dir(默认 h3):逐段 mp4 目录;
- subs_dir(默认 _subs):assemble.py 的字幕中间产物目录;
- 段级 prompt_dir(默认 prompts_v2):gen_prompts.py 在段缺 prompt 时的输出目录;
- design_bible(定稿描述符,见 Step 2a):{"人物名": "锁定英文描述", "景物名": "锁定英文描述"}。
路径基准(全脚本统一):workdir 为绝对路径则原样使用;为相对路径则相对
plan.json 所在目录解析;不写 workdir 时取 plan.json 所在目录
(实现在 _common.resolve_base())。gen_segments / gen_prompts / gen_v2_A /
finalize / assemble / make_stills_pdf 六者一致。
静帧集(图文册 PDF,闸③ 交付物)相关可选字段(供 make_stills_pdf.py --plan 直读):
- 顶层 stills_pdf:{"title": "《篇名》— 静帧集", "sub": "作者|体裁|用途|画风|画幅", "cover_note": ["说明行1", ...]}(缺省时脚本按 project 自动兜底);
- 每幕段可选 scene_name(幕名,短,如「轻轻的我走了」)与 scene_kind(场景·镜头,如「河畔小径 · 停泊小舟」,显示在标题条右端);缺省时幕名由 line 去标点取前 8 字兜底,时长取 duration,提示词取 prompt 文件解析。
3. 工作流
Step 1 定类与分镜(**逐段选分支 A/B**)──▶ Step 2a 跨段元素定稿 ──▶ Step 2 参考图 ──▶【闸① 参考图+配音确认】
──▶ Step 3A 分支A 逐段配音 / Step 3B 分支B 参考音频 ──▶ Step 4 提示词v2+plan.json
──▶ Step 5 metaso H3 逐段生成 ──▶【闸② 成片确认】
──▶ Step 6 合成(丢弃 H3 音轨 + 混入 GEM 原声 + 封面蒙板 + 字幕
+ make_stills_pdf.py 生成静帧集图文册 PDF) ──▶【闸③ 交付确认】
Step 1 定类与分镜
体裁判型沿用三分类(A 叙事讲解 / B 朗读朗诵 / C 多角色情景剧)与
分镜方法(beat 三轨、提示词三字段制),规则见 references/prompt-writing.md。
简化点:
- 每段时长 4~30s 自由估算,不需要按配音实测端点排;
- C 型角色段设计画面时必须让说话角色在屏(口型同步的前提);
- 分支A 的画面不必回避清晰面部特写(2026-10-10 纠正):早期误判为「闭口约束压不住特写」,
真因是提示词残留台词。只要提示词无台词,清晰面部特写照样闭口——
无需为此牺牲构图改成背影/过肩。
Step 2a 跨段元素定稿(文本锁 + 锚点图,多段共现场景必做)
只要有一个元素会跨段出现,就先定稿——否则同一人物/景物在各段各写各的、
长相各异(实测:《茅屋为秋风所破歌》杜甫五段五个样)。本步对任何跨段元素
通用,只是元素类型不同(人物 / 景物 / 道具);"人像定稿"与"锚点图"是同一
步的两个产物,适用场合不同(见下表)。
两个产物(务必分清):
- ① 文本锁(design_bible 条目)— 必做,一致性的真正引擎
每个跨段元素一句固定英文描述(长相/服饰/材质/配色逐项写死),存入
plan.json 的design_bible。所有含该元素的静帧/视频提示词,必须逐字嵌入
这段描述(不意译——改写即漂移)。 - ② 锚点图 / 定稿图(
stills/anchor_<名>.png)— 条件产出,给"人"看的
该元素的一张标准参考图,作用只有两个:(a) 给用户确认"就长这样"、
(b) 作为视觉基准供你肉眼核对下游静帧。它不参与机器传播。
什么时候出锚点图(决策表):
| 场合 | 文本锁 | 锚点图 | 说明 |
|---|---|---|---|
| 跨段人物(有具体长相) | 必做 | 必做(人物设定图:三视图+面部特写) | 人脸最难一致,必须出图给人确认+视觉基准 |
| 单一地点 / 跨段主场景 | 必做 | 建议做(场景锚点图) | 一个主场景贯穿全片,先出一张确认色调/构图 |
| 跨段道具 / 标志物 | 必做 | 可选 | 简单物件文字锁足够,复杂才出图 |
| 无跨段元素(纯一次性单景) | 不需要 | 不需要 | 逐段直出即可 |
二者兼具 = 既有跨段人物又有跨段主场景(如杜甫立于破屋前)→ 人物设定图
与场景锚点图都出。
> ⚠️ 关键限制:本技能出图工具 scripts/gen_image.py 只支持 --ref-url(URL),
> 本地文件不能作图生图参考。因此锚点图不会被机器自动"传染"给下游静帧——
> 跨段一致性 100% 靠逐字嵌入的文本锁保证,锚点图只是人眼基准。别指望出张
> 锚点图下游就自动长一样。
操作步骤:① 列跨段元素清单 → ② 写 design_bible 文本锁 → ③(按决策表)
出锚点图给用户确认 → ④ 所有含该元素的静帧/视频提示词逐字嵌入文本锁 →
⑤ 再出分镜静帧。
> 🎨 画风句与角色锚定的默认参考(v1.4.0):references/style-presets.md 收纳了成对的
> 画风基准句 + 角色/场景锚定写法——预设1「儿童绘本风·卡通明快」为默认(学前~小学
> 中段童话/寓言),另有水墨/水彩/纪实等预设。开工时逐字复制进 design_bible,
> 省去从零编写、杜绝措辞漂移(跨段一致性的真正引擎仍是逐字文本锁)。
> 续作 / 下篇(如《卖炭翁》下篇):先从上一部的 plan.json 原样继承
> design_bible(画风句 + 人物描述逐字沿用),再只补充本片新增元素。
> 否则上下篇会出现"同一个人两张脸、两种画风"——这类不一致一旦出了片,
> 修法是整段重生成,代价远高于开工前继承一下。
Step 2 参考图(本技能自带出图脚本)
- 凡含跨段元素的段,一律先过 Step 2a 定稿(文本锁 + 按决策表决定是否出锚点图),
该段静帧/视频提示词逐字嵌入 design_bible 锁定描述符; - 画风句取自
references/style-presets.md预设(默认「儿童绘本风·卡通明快」),逐字写进每段提示词; - 无跨段元素的段,直接逐段出分镜静帧(
stills/stillNN.png); - 出图用本技能自带
scripts/gen_image.py(默认-m I25S,2K,16:9); - 静帧提示词 = 该段视频提示词的静态化改写(删运镜句/音轨字段);
- 图片 >30MB 时 gen_segments.py 会自动转 JPEG 并逐档缩小(metaso 单图上限 30MB);
- 封面段也出一张参考图(key=cover)。
Step 3A 分支A:逐段完整配音 + 实测时长(配音先行、以声定时)
分支A 不做"每角色试听片",而是每段合成完整配音并实测时长,画面时长据此对齐:
# 逐段合成 GEM-3.1 配音(音色须经闸口确认)
# ⚠️ --name 必须等于该段的 key(finalize.py 找 <tts_dir>/<key>.wav)
python scripts/tts.py speak --text "该段台词" \
--voice 毅恒 --name 01a --out tts_out
# 脚本会打印「实测时长」并写入 tts_out/01a.json 的 duration 字段
# → H3 duration = clamp(round(t), 4, 30)
要点:
- --name = 段 key 是硬约束(如 01a、cover),否则 finalize.py 报
[MISS] 01a: GEM 配音不存在 并 ABORT 出片;不想同名就在该段写 voice_wav 指过去。
- 配音先行、以声定时——先有声音长度,再定 H3 画面时长(超 30s 则该段拆句);
tts.py speak 完成时会直接打印实测时长与建议的 duration,照它填 plan 即可
(不需要另建时长清单文件)。
- 生成画面时 audio:"none",H3 音轨后期丢弃;最终混入的就是这一版 GEM 原声。
- 封面段也要配音(2026-10-10 教师指正):key=cover 段的 line(片名+作者,
如「《再别康桥》,徐志摩」)同样要合成这一版 GEM 配音并混入封面——蒙板大标题只替代
字幕显示,不等于免配音;封面静音属不合格。封面音轨可保留极轻 H3 环境音作底(6.0b)。
Step 3B 分支B:每角色参考音频(5~8s)+ 逐段完整配音
分支B 需要两套音频,各司其职:
1. 参考音频(给 H3 驱动口型 + 借音色):按 §1 的二选一策略——固定 5~8s 试听片,
或直接用该段台词的 GEM 原声(时长须 ≤15s,口型与成品音轨天然对齐);
2. 成品配音(给人听):每角色的每段台词仍要用 GEM-3.1 单独合成,
命名 <key>.wav,后期混入时替换掉 H3 的重渲染音轨(S15)。
# 策略①:每角色合成一段 5~8s 试听片,全片固定(本技能自带 scripts/tts.py)
python scripts/tts.py speak --text "第一句台词" \
--voice 程砚 --name audition_narrator_m --out tts_out
# 策略②:直接拿该段成品配音当参考(同一份文件两用,最省事也最准)
python scripts/tts.py speak --text "该段台词" --voice 程砚 --name 01a --out tts_out
铁律:
| # | 规则 |
| --- | --- |
| 1 | 同一角色全片用同一条参考音频策略(① 或 ②,选定后不换;每条 2~15 秒) |
| 2 | 时长 2~15 秒(metaso 硬限,超限整批被拒;ffprobe 实测) |
| 3 | 选段必须读同目录 .json 的 voice_id,绝不凭文件名猜(_m=male 男声的教训) |
| 4 | 音色确定后中途不换;策略① 下参考音频内容无关紧要(只借音色) |
【闸①】参考图 + 参考音频一起呈给用户(静帧逐张 + 每角色一条试听音频),
通过后才开始生成视频。
Step 4 提示词 v2 + plan.json
- 写好 plan.json(每段 key/image/source_prompt/role/line/duration/sub);
- 按分支选派生器(两个脚本做的事相反,用错即翻车):
分支B 段 → gen_prompts.py(原始文件不动):
python scripts/gen_prompts.py --plan plan.json
自动完成三处改造(详见 references/prompt-v2.md):删禁人声句、加
<Audio 1> 声明、按 role 分流追加台词块(旁白=off-screen voiceover+闭口 /
角色=自然口型);段带 note 字段时,读音/朗读提示只写入台词块框架句
(<d> 之外)。派生后按该文档第五节清单 30 秒自检。
分支A 段 → gen_v2_A.py(手写 v2 易漏删台词,一律用脚本派生):
python scripts/gen_v2_A.py --plan plan.json [--only s2,s3] [--force]
从段的 source_prompt(静帧提示词)逐字继承画面描述——这样文本锁
不会在"静帧→视频"两版之间漂移,再追加运镜句、环境音、闭口约束;
段的 camera / soundscape 字段可逐段覆盖运镜与环境音(缺省给保守值)。
soundscape 值得认真写:H3 会照它生成环境音轨,质感有时相当好——
写得好就能在 Step 6.0b 选择保留(只写自然环境声,别写语音类内容,见 S14)。
脚本内置四项自检并打印 PASS/FAIL(无 <d>、无 <Audio 1>、保留禁人声、
有闭口约束),任何一项 FAIL 都不得提交 H3。
--force 在 gen_v2_A 里只有一个含义:强制把「判定为分支B」的段也按分支A 派生
(正常选择下不该用)。注意它总会覆盖已存在的 v2 产物——gen_prompts 的 --force
则是「重写已存在文件」,两者语义不同,别混记。
Step 5 metaso H3 逐段生成
export METASO_API_KEY=mk-... # 密钥只认环境变量,不读 .env
python scripts/gen_segments.py --plan plan.json # 全部缺的段
python scripts/gen_segments.py --plan plan.json --dry-run # 先核对命令
- 串行(metaso 并发=1),429 自动退避;幂等——中断/失败后直接重跑,
已成功段自动跳过; - 每段把参考图 + v2 提示词 +(分支B 才加)参考音频 + duration 发给 H3,
H3 返回带音轨的 mp4——该音轨后期默认丢弃(其中环境音效可按 6.0b 保留),
成品只用其画面(S15); - ⚠️ 幂等的反面陷阱:改了提示词想重生成某段时,先把该段 mp4 改名归档
(mv h3/01a.mp4 h3/01a_v1.mp4,满足 S6 的版本链),否则脚本见文件已存在就跳过,
白改一场(S17); - create 断线 ≠ 失败:任务已扣费在跑,用
python scripts/h3_video.py status --provider metaso --task-id <id> --download
续查下载,严禁重新 create; - metaso 只有 768P/2K;时长 4~30s;预计速度 1~4 分钟/段(视排队;顺畅时段可能 30~60s)。
【闸②】成片确认:把各段 mp4(或快速 concat 的草稿)呈给用户,逐段
确认台词念对、口型正确、画面合格。不合格的段:改 v2 提示词(--force
重派生)或换参考音频,把该段 mp4 改名归档后重跑 gen_segments(只重生成该段)。
Step 6 合成(丢弃 H3 音轨 + 混入 GEM 原声 + 封面蒙板 + 字幕 + 静帧集图文册 PDF)
6.0 混入 GEM-3.1 原声(S15——两条分支都做):H3 每段 mp4 自带音轨默认丢弃
(其中的环境音效可选保留,见 6.0b),
改用 Step 3A/3B 合成的 GEM-3.1 原声。
⚠️ 先测实际时长再对齐(S16):H3 返回的 mp4 实际时长不等于请求的
duration(实测请求 9s 返回 9.4s)。所以:
- 用 ffprobe 实测每段 mp4 的画面时长,不要用 plan.json 里的请求值;
atempo = 配音时长 ÷ 画面实测时长(ffmpeg 允许域 0.5~2.0;本技能实际只在 0.9~1.1
这个窄带内使用,超出即按 S20 改冻结/补静音)——配音比画面短时 atempo<1(减速铺满),
比画面长时 atempo>1(加速压进);- 禁用
-shortest:它会按较短的那轨截断,实测会把旁白最后一个字切掉。
ffmpeg -i h3/01a.mp4 -i tts_out/01a.wav -filter_complex \
"[1:a]atempo=<配音时长÷画面实测时长>[a]" -map 0:v -map "[a]" -c:v copy -c:a aac 01a_mixed.mp4
⚠️ 对齐方式按错配大小二选一(S20,2026-10-10 教师认可):上面的 atempo 只适合小幅错配
(atempo 落在 0.9~1.1,即 |配音−画面| ≲ 10%)。错配较大时不要用 atempo 变速——配音需
加速 >1.2 会明显变调(如 12.9s 旁白压进 8s 画面 = 1.6×,听感急促失真)、拖慢 <0.9 则拖沓。
此时改用冻结画面尾帧 / 配音末尾补静音,配音零变速、零裁剪:
# 配音比画面长(旁白读不完)→ 冻结画面尾帧到配音长度(画面持住、旁白自然读完)
ffmpeg -i h3/01a.mp4 -i tts_out/01a.wav -filter_complex \
"[0:v]tpad=stop_mode=clone:stop_duration=<配音−画面>[v];[1:a]apad[a]" \
-map "[v]" -map "[a]" -t <max(配音测长,画面测长)> \
-c:v libx264 -crf 18 -c:a aac -ar 24000 -ac 1 01a_mixed.mp4
- 终片段时长
T = max(画面实测, 配音实测);画面不足处tpad=stop_mode=clone冻结尾帧补足,
配音不足处apad补静音,-t T统一截齐——两条路都不动配音语速;- 代价:长旁白段画面会持住(静帧)数秒——可接受(镜头 hold 收尾,绘本风格里尤其自然),
换来的是保住了配音自然语速(教师实测认可); - 一步跑完「混音+楷体字幕+concat」 → 用
scripts/finalize.py(推荐;默认即此冻结对齐法)。
finalize.py不使用 atempo——它对任何错配一律「冻结尾帧 + 补静音」,因为实测下来
保配音自然语速比压时长更重要(上面的 atempo 写法只在手工微调时用)。
三项验收(出片前必测,全部用 ffprobe/ffmpeg 客观判定):
| # | 项目 | 判据 | 不合格说明 |
|---|---|---|---|
| 1 | 音轨规格(S15) | aac / 24000Hz / 1ch |
测出 32000Hz + 2ch = 误用 H3 重渲染音轨(带空间混响),回头查 S15 |
| 2 | 闭口旁证(分支A) | 单段 H3 音轨 mean_volume 低于 -30 dB |
读到 -16~-19 dB = H3 自己在念台词,提示词仍残留台词行(S14) |
| 3 | 旁白完整 | 配音末字未被截(对齐后音轨时长 = 画面时长) | 用了 -shortest 或时长取了请求值而非实测值(S16) |
> 第 2 项是唯一能客观判定"人物有没有被驱动说话"的机器手段——画面上嘴动与否
> 靠肉眼不可靠,但 H3 只要生成了语音,音轨能量就高出 13~16 dB,一测即知。
> 判据实测来源:含台词段 -16.2~-19.2 dB;清台词段 -30.2~-52 dB。
6.0b 可选:保留 H3 生成的环境音效
H3 会按提示词的 overall_soundscape 自行生成环境音(风声、雪声、车轮、市集等),
质感有时相当好,可以按需保留——默认丢弃只是"只要人声"的保守选择,不是禁令。
前提(必检):保留前先跑第 2 项验收,确认该段 H3 音轨 mean_volume < -30 dB。
否则混进来的不是环境音,而是 H3 自己念的台词(带空间混响,正是 S15 要杜绝的东西)。
做法:把 6.0 的「丢弃 H3 音轨」换成 amix——环境音作底层、GEM 人声在上,
输出仍强制 -ar 24000 -ac 1 保住 S15 硬指标:
ffmpeg -i h3/01a.mp4 -i tts_out/01a.wav -filter_complex \
"[1:a]atempo=<配音时长÷画面实测时长>[voice];[0:a]volume=0.3[amb];\
[voice][amb]amix=inputs=2:duration=first:normalize=0[a]" \
-map 0:v -map "[a]" -c:v copy -c:a aac -ar 24000 -ac 1 01a_mixed.mp4
volume=0.3是环境音起始值(人声为主),听感偏弱/偏强再调;- 音效要 H3 生成的,不要自己合成:ffmpeg 噪声(
anoisesrc/apulsator)拼出来的
风声/车轮声质感差,2026-10-10 实测已被否决; - 提示词里不要写语音类内容来"制造"音效——那会驱动人物嘴动(S14)。
正确写法是overall_soundscape只描述自然环境声。
# 6.1 封面蒙板大标题(PIL 蒙板+overlay,零字体坑;标题用雅黑)
python scripts/make_cover.py --video h3/cover.mp4 \
--title "《卖火柴的小女孩》" --subtitle "安徒生 童话" --out h3/cover_titled.mp4
# 6.2 楷体字幕烧录 + 全片 concat(封面不烧字幕,蒙板标题已替代)
python scripts/finalize.py --plan plan.json \
--cover h3/cover_titled.mp4 --out final_卖火柴的小女孩_最终版.mp4
- 字幕(
finalize.py默认实现):楷体(KaiTi)白字、底部居中,不整段堆屏——把整段按标点拆成「单行短语」,用enable='between(t,start,end)'一次只显示一行、按该段时长顺序轮播切换(不做逐句精确对轴);字幕下衬反色半透明底(black@0.5)+ 极细浅色描边,兼顾可读性与画面美观; - 中文长句必须拆短语:按标点(,。;:、!?)断句后逐条轮播;禁止按固定字数硬断——会把「DNA」「蛋白质」等词劈开。单条字数阈值须大于该段最长的无标点子句(本项目取 24 字);
- 细描边的做法:
drawtext单层的边框色 = 填充色,无法单独指定描边颜色,故用双层drawtext叠加——浅色层略大 + 深色半透明层略小,露出的环宽 = 两层boxborderw之差(差 1~2px 即为细边;本项目 1px 细边=白层 8 / 黑层 7); - 半透明底不一定"看着透":可辨性取决于背后亮度——叠在亮背景上明显透,叠在暗背景上天然显实(
black@0.7更易被误判成"完全不透明")。想要稳定可见的半透明用black@0.5,并主动向用户说明这一物理特性; - 封面段
sub:false:蒙板大标题(雅黑)即片名作者,不重复烧旁白字幕(但封面仍须配音朗读片名+作者并混入,见 Step 3A / S18); - 样式可调:
--font-size(字幕)、make_cover 的--box-w/--title-size/--alpha等。
【闸③】交付确认:present_files 成片 + 静帧集 PDF 一并交付。
(2026-10-10 教师两次指正:静帧集是交付物之一,不能只给成片;且须汇总为一个 PDF 文件,
不是散列 PNG)。
「静帧集」的合格形态 = 图文册(不是纯图拼贴!):第 1 页为封面(整体静帧 + 说明行),
其后每幕一页 = 深蓝标题条(篇名 +「第 NN 幕 幕名」+ 右侧「场景·镜头 · Ns」)/ 定稿静帧 /
「台词」行 / 「图生视频提示词 PROMPT」框。格式基准 = 参考件《春晓·静帧集.pdf》,
由 make_stills_pdf.py 生成(见 §6)。
输出 <项目目录>/静帧集_<篇名>.pdf;页序封面在前、各幕按 plan 段序。
用户提出单段修改时回 Step 5 只重生成该段,再 assemble 重合成(字幕产物幂等重建)。
4. 目录约定
<项目目录>/
├── plan.json # 唯一数据源
├── prompts/ # 原始分镜提示词(v1,不动)
├── prompts_v2/ # 派生提示词(分支B: gen_prompts.py / 分支A: gen_v2_A.py)
├── stills/ # 参考图/静帧
├── 静帧集_<篇名>.pdf # 闸③ 交付物:静帧集图文册 PDF(make_stills_pdf.py 产出)
├── tts_out/ # 成品配音 <key>.wav(finalize 按此名取)+ 参考音频 + 同名 .json(voice_id/duration)
├── h3/ # 逐段 mp4(gen_segments.py 产物)+ summary.json
├── _subs/ # 字幕中间产物(assemble.py)
└── final_*.mp4 # 成片
> 本技能自带资源:references/style-presets.md(画风预设库,含默认「儿童绘本风」)、
> scripts/finalize.py(Step 6 一站式合成)。本技能不附带任何图片资产——SkillHub
> 技能包只收文本资产、二进制媒体(.png/.jpg/.svg 等)一律 400 拒收,故画风参考一律以
> 文本形态入库(见 style-presets.md 的画风基准句 + 角色锚定句),无需、也不应存放样图。
5. 铁律(简化版只保留这几条,违反必翻车)
> 铁律讲「不许做什么」;方法论讲「怎么做对」——换通道/接新模型的 8 项评估清单、
> 参数语义 A/B 法、响应三层检查、试片验收三层,见 references/api-integration.md;
> 改造本技能自身(快照、批量改文本、回滚)的纪律见 references/maintenance.md。
| # | 铁律 |
|---|---|
| S1 | (仅分支B)v2 提示词三要素缺一不可:删禁人声句 + <Audio 1> 声明 + <d> 台词。漏任何一样:乱说话/无人声/复读参考音频。分支A 恰好相反——见 S14 |
| S2 | 口型分流:旁白段必须 says in an off-screen voiceover + lips completely closed;角色段台词归屏幕内角色。写反了口型就错 |
| S3 | 参考音频 2~15 秒(metaso 硬限);同一角色在全片用同一种策略——固定试听片(音色最稳)或逐段台词原声(口型最准),选定不混用;选段读 voice_id 不猜文件名 |
| S4 | 提示词整份原样传,任何 strip/截断都会删掉 <Audio 1> 和 <d> |
| S5 | create 断线 ≠ 失败:status 续查下载,严禁重新 create(重复扣费) |
| S6 | 已生成文件不覆盖:改提示词重生成前,旧 mp4/旧提示词先改名归档(版本链) |
| S7 | metaso 串行(并发=1);只有 768P/2K;密钥只认 METASO_API_KEY(或 --api-key-file/--key),不落盘 |
| S8 | 命名自解释:final_<片名>_最终版.mp4 式递增,不用「终版/完整版」 |
| S9 | (仅分支B)v1 提示词(外部 TTS 路线)的禁人声句会关闭 H3 人声通道——复用旧项目提示词时必须走 gen_prompts.py 派生,不得直接用 v1 原文件提交。分支A 禁止走 gen_prompts.py(见 S14),改用 gen_v2_A.py |
| S10 | Windows/后台调 ffmpeg 必加 -nostdin;字幕滤镜(subtitles / drawtext)的 filename= / fontfile= 一律用相对路径——C:/... 里的冒号会破坏 filtergraph 解析,整条字幕链静默失效(不报错、字幕与底框直接不渲染)。finalize.py 已内置「把字体复制到工作目录(_prep/simkai.ttf)再用相对路径引用」,勿改回绝对路径 |
| S11 | 门控是默认(2026-10-09 教师指令):判型/画风/音色/配音等用户决策项必须经闸口或 AskUserQuestion 确认,仅当用户明确说出「全自动运行」才可代选跳过;未明确要求一律走门控 |
| S12 | 只允许 metaso 通道(2026-10-09 教师指令):h3_video.py 只对接 metaso、--provider 仅收 metaso、密钥只认 METASO_API_KEY(不读 .env);任何环节禁止调用其它上游通道。将来若更换上游通道,须先过 references/api-integration.md 第 1 节的 8 项评估清单(尤其 1.1 能力实测),逐项验完再动代码——2026-10-23 曾两次未评估就改代码,白花试片费后整体回滚 |
| S13 | 分支按段选,判据唯一(§0.5):画面里的人该不该开口说话?不该 → 分支A(audio:"none" + 提示词无台词 + 闭口);该 → 分支B(传参考音频 + 提示词含台词 + 口型同步)。混合型作品逐段混用,plan.json 逐段标 mode,且分支选择属用户决策项,须经闸口/AskUserQuestion 确认(受 S11 门控约束) |
| S14 | 分支A 提示词禁台词(2026-10-10《卖炭翁》实测教训):分支A 段应写 audio:"none"(脚本按 _common.seg_branch() 判定为 A 后会强制剔除参考音频,但仍建议显式写清),且 v2 提示词不含 <d> 台词行、不含 <Audio 1> 声明行——残留台词行会让画面人物对口型,与传不传音频无关。分支A 禁止跑 gen_prompts.py(它按分支B 派生,会把台词加回来、把禁人声句删掉),一律用 gen_v2_A.py |
| S15 | 成品人声一律 GEM-3.1 原声(2026-10-10 教师指令):H3 音轨默认丢弃,后期混入 GEM-3.1 TTS 原声(atempo 对齐画面时长)。H3 收到参考音频走的是 r2va 重渲染——重采样 24k→32k、mono→stereo、叠加空间混响,不是 GEM 原声(教师实测的"回响"即此因)。要口型同步才付此代价。例外:经音量检测确认无语音的 H3 环境音轨,可在 Step 6.0b 选择保留混入(只添氛围,不替人声)——但成片仍须满足 24kHz 单声道 |
| S16 | 时长以实测为准,禁用 -shortest(2026-10-10):H3 返回时长≠ 请求 duration(请求 9s 实返 9.4s)。合成前必须 ffprobe 实测每段画面时长,atempo = 配音时长÷画面实测时长(ffmpeg 允许 0.5~2.0,本技能实际只用 0.9~1.1,见 S20)。用 -shortest 会切掉旁白最后一个字 |
| S17 | 改提示词重生成前先把该段 mp4 改名归档(不是删):gen_segments.py 幂等(已存在则跳过),不归档就白改一场、还误以为改了没用;改名归档同时满足 S6 的版本链要求(mv h3/01a.mp4 h3/01a_v1.mp4) |
| S18 | 封面段要配音、交付须带静帧集 PDF(2026-10-10 教师指正):① 封面段(key=cover)虽标 sub:false(免字幕,蒙板标题替代),但仍须合成 GEM 配音朗读片名+作者并混入,封面静音不合格;②【闸③】present_files 须成片 + 静帧集一并呈交,不能只给成片;③ 静帧集必须汇总为一个 PDF 文件(make_stills_pdf.py 产出),不是散列 PNG —— 散 PNG 交付不合格 |
| S19 | 静帧集=图文册,非拼图(2026-10-10 教师二次指正):静帧集 PDF 必须是「封面页 + 每幕一页(标题条/静帧/台词/图生视频提示词)」的图文册,格式对齐参考件《春晓·静帧集.pdf》与 make_stills_pdf.py;页序封面在前、各幕按 plan 段序;图生视频提示词须取提示词原文(prompts_v2/*.txt,不可手工转述);改 prompt 重出静帧后重跑脚本刷新 PDF,勿手工拼图;默认对图片压缩(长边 2000px/JPEG q88)压到数 MB,无损嵌入会达 60MB+ |
| S20 | 时长错配优先「冻结尾帧/补静音」,慎用大幅 atempo(2026-10-10 教师认可):画面与配音时长不一致时——小幅错配(|配音−画面| ≲10%,atempo 0.9~1.1)才用 atempo 微调;配音明显长于画面(需加速 >1.2 才压得进)不要变速,改 tpad=stop_mode=clone 冻结画面尾帧到配音长度(画面持住、旁白自然读完);配音明显短于画面则不要拖慢,改 apad 末尾补静音。终片段时长 = max(画面实测, 配音实测)。根因预防见 Step 3A「配音先行、以声定时」。一步实现用 scripts/finalize.py |
| S21 | 密钥解析:系统环境变量优先于 .env,旧兼容名会遮蔽新专用名(2026-10-23 实测):gen_image.py 的 get_key 先读环境变量 IMAGE_API_KEY→GEM_IMAGE_KEY→DUOMI6699_IMAGE_KEY→DUOMI_API_KEY,tts.py 先读 TTS_API_KEY→DUOMI_API_KEY→GEM_TTS_API_KEY→API_KEY;若系统里存有旧的 DUOMI_API_KEY(无图/音权限),它会压过 .env 里新写的 IMAGE_API_KEY/TTS_API_KEY,症状是"明明配了 key 却 403"。出图/配音一律显式 --key,或在命令前前缀注入(IMAGE_API_KEY=sk-xxx python scripts/xxx.py …,只对本次子进程生效);.env 必须无 BOM(见排障表)。注意 tts.py 的 .env 只认 API_KEY/TTS_API_KEY,写入 DUOMI_API_KEY= 不会被读 |
| S22 | 外部生成模型「三证合一」才算可用(2026-10-23 实测教训):①元数据——调平台的模型元数据接口拿到完整参数表(name/type/required/options/description),不看文档页措辞(SPA 抓不到、同一参数两处说明可能互相矛盾);②能否建单——真实提交一次拿到 task_id;③出片实测——ffprobe 量规格 + 抽帧人眼核对,有音频还要人耳/ASR。/v1/models 只能证明「模型存在」,不能证明「能用」,更不能当参数说明用(实测该清单漏列了实际可用的主模型,照它判断会得出完全相反的结论)。要查线路白名单用专门的「可用线路/密钥配置」接口 |
| S23 | 失败三分法,且绝不自动换模型/换通道:①未建单被拒(400/401/403 或 200+业务码)=不扣费,改参数或改配置后重提;②建单后任务失败=平台自动退款,改提示词/换素材后重提;③建单成功但本地没取到结果(断线/超时/退出码非 0)=钱已花、结果在平台,只能 status --task-id 续查,严禁重新 create(即 S5)。并发超限属①可退避重试。能力、成本、口径不同者一律不得自动替换——悄悄换模型会让用户以为在跑 A 实际在跑 B,价格与语义同时跑偏,日志里那句提示挡不住;要显式失败并给出解法。唯一可自动重试的是完全等价的同一请求 |
| S24 | 平台配置类错误:不重试,直接给操作路径:401(密钥错)、403 ip_not_allowed(IP 白名单)、key_config/「该模型暂未开通」(密钥未勾选模型)、「渠道临时下线」、api_edit_allowed=false(只能人工改)——这几类脚本侧无解,重试只是白等。错误信息必须写清「去哪、点什么」(如「控制台 → API 密钥 → 勾选模型及线路 / 渠道策略改综合最优」)。同理 --provider 不可用时,报错必须附恢复办法,不能只说「不支持」 |
| S25 | --dry-run 必须零副作用(2026-10-23 修复):dry-run 只打印、不写盘。所有写文件动作(summary.json、状态锁、缓存)都放在 if not dry_run: 之后——旧版 gen_segments.py 无条件写 summary,一次「只看看」的 dry-run 会把真实项目已有的段状态覆盖成一堆 dryrun 记录,事后无法判断哪些段真生成过 |
6. 脚本速查
| 脚本 | 用途 | 关键参数 |
|---|---|---|
gen_image.py |
文生图(Step 2 出图):出分镜静帧/封面参考图/锚点图,按真实文件头纠正扩展名 | --prompt/--prompt-file -m I25S\|BP -r 1K\|2K\|4K --ar --ref-url --dry-run --selftest --balance |
tts.py |
配音合成(Step 3 出人声):GEM-3.1-TTS 单条/多角色/批量合成,落 <out>/<name>.wav(含 .json 记 voice_id) |
speak --text --voice --name --out voices dialogue batch status selftest |
gen_prompts.py |
(仅分支B) v1→v2 提示词派生(三处改造) | --plan --only --force |
gen_v2_A.py |
(仅分支A) 从静帧提示词派生 v2 + 四项自检(S14)。--force 只表示「强行处理判定为分支B 的段」 |
--plan --only --force |
make_stills_pdf.py |
静帧集 PDF(图文册)(闸③ 交付物):封面页 + 各幕页(标题条/静帧/台词/图生视频提示词)。兼容 A/B 两种 v2 提示词格式(A 带字段标签 / B 裸描述);长提示词自动降号以保证提示词原文完整(对齐 S19)。--plan 与 --stills/--out 一起用,见下方命令 |
--plan --stills(必填)--out(必填)--img-max-px --jpg-quality --config |
gen_segments.py |
metaso H3 逐段生成(串行/幂等/断线提示) | --plan --only --dry-run |
make_cover.py |
封面蒙板大标题(PIL+overlay) | --video --title --subtitle --out 及样式参数 |
assemble.py |
楷体字幕烧录+concat 合成(-c:a copy,保留 H3 音轨——仅当已另行混好音轨时用) |
--plan --cover --out --font-size --skip-subs |
finalize.py |
Step 6 一站式(推荐):丢弃 H3 音轨+混入 GEM 原声(S20 冻结对齐)+楷体字幕+concat,并回显音轨验收 | --plan --cover --out --tts-dir --font-size --prep-dir --no-subs |
h3_video.py |
H3 底层调用(metaso 官方 v2 兼容协议;selftest 可离线自检) |
create --provider metaso status --task-id |
静帧集 PDF 完整命令(--stills、--out 均为必填,缺一个 argparse 直接报错):
python scripts/make_stills_pdf.py --plan plan.json \
--stills stills --out "静帧集_卖火柴的小女孩.pdf"
其余脚本的一次性串跑(Step 6 闸③ 前):
python scripts/gen_v2_A.py --plan plan.json # 分支A 段提示词(离线,秒级)
python scripts/gen_prompts.py --plan plan.json # 分支B 段提示词(自动跳过分支A 段)
python scripts/gen_segments.py --plan plan.json --dry-run # 核对命令(零副作用)
python scripts/finalize.py --plan plan.json --cover h3/cover_titled.mp4 --out final_片名_最终版.mp4
6.1 自带资源与自备项
全流程自包含——本技能不调用任何其它技能,四条链路(出图 / 配音 / H3 生成 / 合成)
全部由包内脚本完成:
scripts/_common.py:ffmpeg/ffprobe 定位链 + 中文字体 fallback(所有脚本共享);- 出图:
gen_image.py(文生图,默认-m I25S、2K、16:9); - 配音:
tts.py(GEM-3.1-TTS:单条 speak / 多角色 dialogue / 批量 batch / 查询 voices); - H3 生成:
h3_video.py(底层)+gen_segments.py(逐段调度); - 提示词:
gen_prompts.py(分支B 派生)/gen_v2_A.py(分支A 派生); - 合成:
make_cover.py/assemble.py/finalize.py/make_stills_pdf.py; references/:prompt-v2.md(分支B 提示词规则)、prompt-writing.md(分镜与提示词写法)、
api-h3.md(metaso H3 接口规格)、style-presets.md(画风预设库)、
api-integration.md(外部生成模型 API 对接方法论:换通道 8 项评估清单、参数语义 A/B 法、
响应三层检查、失败三分法、试片验收三层——换通道或接新模型前必读)、
maintenance.md(技能维护纪律:改前快照、批量改文本的写法、从会话记录回滚、改完验收)、
voices.json(运行态缓存,tts.py 首次拉取音色列表后写入;已在.skillhubignore排除,不入包)。
需你自备的外部项(非技能,而是运行环境与服务账号):
| 用途 | 需要什么 | 提供方式 |
|---|---|---|
| H3 视频生成 | metaso 平台账号 | 环境变量 METASO_API_KEY(或 --api-key-file <文件>、--key <值>;不读 .env、不落盘) |
| 配音 / 出图 | 图/音模型平台账号 | 环境变量 TTS_API_KEY(配音)、IMAGE_API_KEY(出图),或本技能目录 .env(不入包) |
| 音频/视频合成 | ffmpeg / ffprobe | 系统安装,或环境变量 FFMPEG/FFPROBE 指定(脚本自动定位) |
Python 依赖:标准库 + Pillow(make_cover.py)、reportlab(make_stills_pdf.py);
其余脚本零第三方依赖;PyMuPDF(fitz)仅用于静帧集 PDF 的回读验收(可选)。
7. 排障速查
| 症状 | 根因 | 处置 |
|---|---|---|
| 某段复读参考音频原话 | 提示词缺 <d> 台词或没传台词 |
查 v2 文件与 --ref-audio 是否都传了 |
整批报 duration must be between 2 and 15 seconds |
参考音频超 15s | 换 2~15s 的同音色音频(ffprobe 实测) |
| 某段无人声/乱说话 | 段落既无 <d> 又无禁人声句 |
该段补 role+line 重派生(--force) |
| 旁白口型同步给了画面角色 | 缺 off-screen voiceover 或闭口句 |
按规则改台词块重生成该段 |
报 original_size 解析错误 |
subtitles 滤镜用了绝对路径 | 用脚本内置的相对路径写法 |
| 成片出现两个封面 | plan 里 cover 段没加 is_cover: true,assemble 循环把原始 cover.mp4 也拼了进去 |
cover 段加 "is_cover": true 重跑 assemble |
| 同一人物各段长相不一 | 没做 Step 2a 定稿(缺 design_bible 文本锁),各段人物描述各写各的 | 补 design_bible 文本锁,逐字嵌入所有相关静帧/视频提示词后重生成 |
| 多音字读错声调(如 怒号读 hào,应为 háo) | H3 按默认音读 | 分支B:plan 该段加 note 读音提示(英文,写在 <d> 之外,勿在台词里夹注音),重生成该段。2026-10-09 茅屋项目 s1「怒号→háo」实测成功。分支A 不吃 note(gen_v2_A 不读该字段),读音要在 TTS 侧用 --style 钉,再重出画面 |
| gen_segments 全部报 [FAIL] 但 mp4 实际已生成 | h3_video 把 --out 当目录用,产物嵌套在 <key>.mp4/<taskid>.mp4,脚本 stat 到目录误判失败 |
已在 v1.1.0 修复(normalize_out 自动提升嵌套产物);旧版脚本切勿手工 rm -rf 嵌套目录——先确认复制成功再删 |
| 图片被拒 | >30MB | gen_segments.py 自动转 JPEG 并逐档缩小;或手动压缩 |
提交/上传阶段 SSL: UNEXPECTED_EOF_WHILE_READING |
走了本机 HTTP 代理 | 绕代理直连后再跑:export no_proxy="metaso.cn" NO_PROXY="metaso.cn" + unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY |
报 403 且含 ip_not_allowed |
密钥设了 IP 白名单(报错里带平台看到的来源 IP,不扣费) | 到平台控制台 →「API 密钥」清空白名单或加入该 IP 后重试(S24:别重试,重试无收益) |
| 报「创建返回中没有 task_id」 | 多半是 HTTP 200 的响应体里带业务码(如 code:403 + key_config「该模型暂未开通」),不是网络问题 |
看报错里的「原始返回」。h3_video.py 已内置 biz_error() 识别 200-body 业务码并给出操作路径;若仍出现此提示,把原始返回发给平台排查(S22/S24) |
| 同一模型,文档说支持参考音频,实际不做口型 | 文档措辞与真实能力不一致(元数据写「参考音色」,另一处写「仅作节奏参考,不做对口型」) | 以试片为准(抽帧看嘴型),不信文档措辞;能力缺失无法靠调参弥补,回选型阶段(见 references/api-integration.md 1.1) |
--dry-run 之后 summary.json 变成一堆 dryrun |
v1.4.1 及更早的无条件写盘 bug | v1.7.0 已修(S25);若旧版跑过,按 plan 段序重跑生成即可恢复状态 |
| 成片旁白最后一个字被切掉 | 用了 -shortest,或对齐时用了 plan 里的请求时长而非实测时长 |
按 S16:ffprobe 实测画面时长,atempo = 配音时长÷画面实测时长,去掉 -shortest |
| 配音明显比画面长,压不进去(或 atempo 加速后变调) | 先定了 H3 时长、后知配音更长,错配大(如 12.9s 旁白压进 8s 画面=1.6×) | 按 S20:别用 atempo 硬压,改 tpad=stop_mode=clone 冻结画面尾帧到配音长度(或直接用 finalize.py)。根因预防见 Step 3A「配音先行、以声定时」 |
| 改了提示词重生成,画面却没变 | gen_segments.py 幂等,见旧 mp4 已存在就跳过 | 按 S17:先把该段 mp4 改名归档(mv h3/01a.mp4 h3/01a_v1.mp4)再重跑 |
finalize.py 报 [MISS] 01a: GEM 配音不存在(最后 ABORT) |
配音文件名与 plan 的段 key 不同名(如 --name seg_01) |
用 tts.py speak --name 01a 重命名/重合成,或在该段写 voice_wav 指向实际文件 |
| TTS 读完正文后幻觉续诵额外内容(且整体超慢,如 27s 念 10 个字) | 文本用空格分隔标题(如「青玉案 元夕,辛弃疾。」)触发模型脑补续诵;间隔号「·」版本正常 | 2026-10-16《青玉案·元夕》实测:空格版 27.76s 且幻觉出额外诗句,改「青玉案·元夕,辛弃疾。」后 4.36s 干净。含生僻词/间隔号的标题一律用「·」,禁用空格分隔;合成后务必抽听/转写首尾段确认无续诵 |
| ASR 转写疑似读错字(如「元夕」被转写为「元系」) | 多为 ASR 对句尾音调的误识,不一定是 TTS 读错 | 先用对照句验证模型读音(「夕阳西下」「元宵佳节,元夕之夜」转写全对即证模型无误),勿冤判 TTS 反复重合成 |
| 分支A 段 H3 音轨 mean_volume 高于 -30dB(如 -22.8dB) | 该段 soundscape 写了热闹环境声(烟火、箫鼓、人群),环境音本身能量高,不等于有人声 |
提取该段音轨转写:为空即通过(2026-10-16 实测 cover 烟火 -22.8dB、seg03 箫鼓 -24.5dB,转写均空);有文字才按 S14 查提示词残留台词 |
| 字幕/底框根本不出现,且无任何报错 | drawtext/subtitles 的 fontfile/filename 用了绝对路径,C:/ 的冒号破坏 filtergraph 解析(整条字幕链被静默丢弃) |
按 S10 改相对路径:把字体复制到工作目录(如 _prep/simkai.ttf)再用相对引用;或直接用 finalize.py(已内置) |
配了 IMAGE_API_KEY/TTS_API_KEY 仍报 403 无权限 |
系统环境变量里的旧兼容名 DUOMI_API_KEY 遮蔽了 .env 的专用名(见 S21) |
显式 --key,或命令前缀注入 IMAGE_API_KEY=sk-xxx python …;或清理系统里旧的 DUOMI_API_KEY |
.env 首行密钥读不到 |
文件带 UTF-8 BOM,首行键名变成 \ufeffIMAGE_API_KEY,与脚本查找的键名不匹配 |
用 Python 以 encoding="utf-8" 无 BOM 重写(Write / PowerShell Set-Content 默认会带 BOM);或读取端改 utf-8-sig |
| 报「模型 X 不在密钥白名单」 | 密钥本身有效但没开通该图像/语音模型;或模型名变体后缀不精确匹配(tt-image-2.5 ≠ tt-image-2.5-sunburst) |
先 --balance 确认密钥有效(能查余额即有效);到开放 API 控制台 →「API 密钥」把渠道策略改「综合最优」(一改即绕开逐个勾选),或勾选该模型及其线路;模型名以控制台白名单的精确名称为准 |
ASS 字幕设了 BorderStyle=3 却不出底框 |
部分 libass 版本对该模式不渲染底框(实测完全不画,即便不透明黑底也不显示) | 不用 ASS 预置底框,改用 drawtext 的 box=1:boxcolor=black@0.5:boxborderw=N(finalize.py 即此法) |