@optima-chat/gen-cli 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/commands/task.d.ts.map +1 -1
  2. package/dist/commands/task.js +12 -3
  3. package/dist/commands/task.js.map +1 -1
  4. package/dist/commands/video.d.ts.map +1 -1
  5. package/dist/commands/video.js +26 -2
  6. package/dist/commands/video.js.map +1 -1
  7. package/package.json +1 -2
  8. package/.claude/skills/digital-human/SKILL.md +0 -309
  9. package/.claude/skills/digital-human/references/avatar-catalog.md +0 -47
  10. package/.claude/skills/digital-human/references/edit.md +0 -219
  11. package/.claude/skills/digital-human/references/generate.md +0 -378
  12. package/.claude/skills/digital-human/references/manage.md +0 -137
  13. package/.claude/skills/digital-human/references/train.md +0 -276
  14. package/.claude/skills/gen/SKILL.md +0 -366
  15. package/.claude/skills/motion-control/SKILL.md +0 -68
  16. package/.claude/skills/multigrid-poster/SKILL.md +0 -194
  17. package/.claude/skills/multigrid-poster/layouts/2x2.json +0 -34
  18. package/.claude/skills/multigrid-poster/layouts/3x3.json +0 -43
  19. package/.claude/skills/multigrid-poster/scripts/compose.py +0 -116
  20. package/.claude/skills/multigrid-poster/scripts/placeholder.png +0 -0
  21. package/.claude/skills/multigrid-poster/shared/fonts/MaShanZheng-Regular.ttf +0 -0
  22. package/.claude/skills/video-compose/SKILL.md +0 -144
  23. package/.claude/skills/video-compose/scripts/video_compose.py +0 -290
  24. package/.claude/skills/video-edit/SKILL.md +0 -332
  25. package/.claude/skills/video-gen/SKILL.md +0 -662
  26. package/.claude/skills/video-gen/references/cinematic-language.md +0 -158
  27. package/.claude/skills/video-gen/references/confirm-card.md +0 -49
  28. package/.claude/skills/video-gen/references/prompt-craft.md +0 -72
  29. package/.claude/skills/video-gen/templates/INDEX.md +0 -78
  30. package/.claude/skills/video-gen/templates/before-after-beauty.md +0 -183
  31. package/.claude/skills/video-gen/templates/drama-fmcg.md +0 -183
  32. package/.claude/skills/video-gen/templates/kol-reaction-food.md +0 -193
  33. package/.claude/skills/video-gen/templates/multi-point-apparel.md +0 -185
  34. package/.claude/skills/video-gen/templates/pain-solution-home.md +0 -184
  35. package/.claude/skills/video-gen/templates/pdp-360-showcase.md +0 -189
  36. package/.claude/skills/video-gen/templates/pdp-feature-highlight.md +0 -182
  37. package/.claude/skills/video-gen/templates/scene-digital.md +0 -183
  38. package/.claude/skills/video-translate/SKILL.md +0 -547
@@ -1,219 +0,0 @@
1
- # Edit Segment(出片后改某段)
2
-
3
- 改**已出片**视频的某一句:重生那段音频 → 重拼整条 → 整条重渲染。
4
-
5
- > ⚠️ **沙箱 bash 须知**(同 [generate.md §0](generate.md)):agent-runtime 的 bash 是安全沙箱,**软拒绝** `$( )` 命令替换、`for`/`while` 循环、命令内换行、`;`/`&&`/`||` 串多条、`> file` 重定向、`| jq`/`| sed`/`| grep` 管道、`nohup &`。所以本文档的所有"取值 / 判断 / 写文件"都靠 **LLM tool_use**(Read / Write / Edit tool + 单条命令读 JSON 输出),**不靠 shell 拼**。下面 bash 块都是**单条命令**;凡需要变量 / 文件内容,LLM 先用 Read tool 读、把值**内联**进命令,不存 `$VAR`。
6
-
7
- > **音频先行下的成本模型**(跟旧版不同,务必理解):
8
- > - 改音频(重生那段 `seg-NN.wav`)= **免费 / 极廉**(voice preview 不烧视频额度)。
9
- > - 但成片是**单条连续渲染**的整条视频,**没法只换一段画面**——改任意一句都要**整条重渲一次**(烧一次视频额度)。
10
- > - 所以**绝大多数修改应该在出片前的音频试听阶段**([generate.md §6.5](generate.md))就改掉(那时改音频免费、还没渲染)。走到 edit.md 说明**成片后才发现要改**——能做,但要重渲一次,提醒用户。
11
-
12
- ## 触发
13
-
14
- 用户说类似:
15
- - "把刚才那条视频的第 2 段改成 X"
16
- - "重做第 2 段" / "seg-3 重新生成"
17
- - "刚才那条改一句话"
18
-
19
- 不在范围(走别的流程):
20
- - "换 avatar"(换分身)→ 等于新视频,走 [generate.md](generate.md)
21
- - "改总文本"(全部重写)→ 反问"要全新生成还是只改某句?"
22
- - "video 比例 / 时长 / 字幕等元参数"→ trained avatar 不支持,反问
23
-
24
- ## 流程
25
-
26
- ### 1. 找 video-id
27
-
28
- - 用户说"刚才那条" / "刚做的" / 不指定 → `ls -t ~/digital-human/videos/ | head -1` 取最新
29
- - 用户给了 video-id(例 `20260515-1430`)→ 直接定位
30
- - 用户给文本片段或主题 → 遍历 `videos/*/meta.md` "总文本"段模糊匹配
31
- - 0 / 多个匹配 → 反问消歧
32
-
33
- 如果 video 是 SPEC 落地前(无 workspace 目录)生成的 → 反问 "这条视频没有 workspace 记录,只能重新生成整条。要走 [generate.md](generate.md) 全新创建吗?"
34
-
35
- ### 1.5 State check(找到 video-id 后必走)
36
-
37
- **Step 0: 旧流程 / 无分段视频检测(必须在 state 路由之前跑)**
38
-
39
- v2(音频先行)跟 v1(逐段视频 + stitch)**不兼容**。prod 上可能有 v1 时期生成的视频(#118 的卡顿就是 v1 在用)。镜像升到 2.0.0 后,这些旧视频若流进新的音频改段路径会**静默出错**:`gen voice concat` 只扫 `seg-*.{wav,mp3}`,旧的 `seg-*.mp4` 段被排除 → 只剩重生那一段 → 渲出**只含被改段**的残片。所以先挡。
40
-
41
- **两条单命令查 + LLM 判断**(别写 `if`/`grep -q`/`$()`,沙箱拦):
42
-
43
- ```bash
44
- ls ~/digital-human/videos/<VIDEO_ID>/segments/
45
- ```
46
-
47
- 再 Read `~/digital-human/videos/<VIDEO_ID>/meta.md` 看"当前版本"字段。然后 LLM **自己判断**(不靠 shell 条件):
48
-
49
- - **`IS_OLD_FLOW`**(命中任一):`segments/` 里出现 `seg-*.mp4` 段文件,**或** meta.md "当前版本"是旧字面量 `(generating)` / `(review pending)`。
50
- - **`NO_SEGMENTS`**:没有 `segments/` 目录,或目录里没有任何 `seg-*.txt`(公共形象单条渲染不产分段)。
51
-
52
- | 命中 | 行为(user-facing,不报 §-编号) |
53
- |---|---|
54
- | `IS_OLD_FLOW` | 反问:"这条是旧流程(逐段视频)生成的,改某句得整条重新生成——新流程是音频驱动单条渲染,跟旧的逐段视频不兼容。要用 `<meta 里的分身>` + 原文重新生成吗?" 用户同意 → 走 [generate.md](generate.md)(复用 meta.md "总文本",不再问 text)。**绝不**让它流进下面的 `final-vN.mp4` 改段分支。 |
55
- | `NO_SEGMENTS`(且非 old-flow) | 反问:"这条是公共形象单条生成的,没有分段,改内容要整条重新生成,要吗?" 同意 → [generate.md](generate.md) 公共分支。 |
56
-
57
- 两者都不命中 → 继续下面 state 路由。
58
-
59
- Read `meta.md` 取"当前版本"字段,**按 state machine 分支**(state 即路由 source-of-truth,不靠用户 phrasing 猜):
60
-
61
- | "当前版本" 值 | 行为 |
62
- |---|---|
63
- | `final-vN.mp4` | ✅ 已出片,走本文档 §2-§8 主流程(重渲产 final-v<N+1>.mp4) |
64
- | `(audio-review pending)` | ⏳ **跳出 edit.md,跳转 [generate.md §6.5 checkpoint](generate.md)** —— 还没出片,用户说"改 seg-N"是 [§6.6 出片前改音频](generate.md)(免费、不渲染) |
65
- | `(audio generating)` | 🚧 音频段还没生完。**反问**:"这条还在生成音频,要先把剩余段生完吗?(生成中不稳定,没法改)" 用户同意 → 跳 [generate.md §6.8](generate.md) 续生。**话术不出现内部 §-编号** |
66
- | `(rendering)` | 🎬 视频正在渲染。反问"这条正在出片,等渲染完再改好吗?"——渲染中改段没意义 |
67
-
68
- **为什么硬分支**:同一句"改 seg-N",在不同 state 下正确动作完全不同。出片前(audio-review pending)改音频免费、不渲染;出片后(final-vN)改要整条重渲。不 state-aware 会乱花额度或产出不存在的版本引用。
69
-
70
- ### 2. 读现状
71
-
72
- 用 **Read tool** 读 `~/digital-human/videos/<VIDEO_ID>/meta.md` 和 `~/digital-human/videos/<VIDEO_ID>/history.md`(别 `cat`),再单条 `ls` 看分段:
73
-
74
- ```bash
75
- ls ~/digital-human/videos/<VIDEO_ID>/segments/
76
- ```
77
-
78
- LLM 给用户回顾:
79
- > "找到 `<VIDEO_ID>`(分身: 张医生,4 句,当前 final-v2.mp4)。想改哪一句?现有:
80
- > - seg-01: '大家好,我是张医生。'
81
- > - seg-02: '今天介绍一款 ...'
82
- > - seg-03: '它能帮你 ...'
83
- > - seg-04: '谢谢观看。'"
84
-
85
- ### 3. 判断改的是什么
86
-
87
- | 改的是 | 处理 |
88
- |---|---|
89
- | 单段 text | 走 §4(主流程) |
90
- | 多段 text | 逐段确认 + 逐段走 §4 的重生音频,最后**只重渲一次**(§6 合并重渲) |
91
- | 换 avatar / voice | 反问 "这等于新视频,本段流程不支持。要走 [generate.md](generate.md) 用 <新分身> 全新生成吗?" |
92
- | 段间顺序 | 反问 "重排需要重拼音频 + 重渲,本 skill v1 不支持。要手动 mv segments/* 改名后我帮你重拼重渲吗?" |
93
- | 段时长 / 字幕 | trained avatar 不支持调时长 / 字幕参数,reply 不支持 |
94
-
95
- ### 4. text 改动(主流程)
96
-
97
- 获取新 text + **二次确认带阈值**(决策表 #14):
98
-
99
- ```python
100
- # 伪代码
101
- old_text = read("segments/seg-<N>.txt")
102
- new_text = <用户给的新 text>
103
- edit_ratio = levenshtein(old_text, new_text) / max(len(old_text), len(new_text))
104
-
105
- if edit_ratio > 0.3:
106
- # 大改 → 反问确认(注意:这里要重渲整条视频,会烧一次额度)
107
- ask_user(f"确认改 seg-<N>?\n 旧: {old_text}\n 新: {new_text}\n (改音频免费,但成片要整条重渲一次)")
108
- if not confirmed:
109
- return
110
- # else (≤30%): typo/标点微调,静默继续
111
- ```
112
-
113
- 30% 阈值依据:错别字 / 标点 < 5% 静默;半句话 / 换语序 20-40% 触发确认;整段重写 > 50% 肯定确认。无 levenshtein 工具时用粗估 fallback。
114
-
115
- ### 5. 覆盖 segments/seg-NN.txt + 重生 seg-NN.wav(只重生音频,免费)
116
-
117
- 设改的是第 N 段(2 位零填充,例 `02`)。三步,全走 tool_use / 单条命令:
118
-
119
- 1. **覆盖 text**:用 **Write tool** 把新文本写进 `~/digital-human/videos/<VIDEO_ID>/segments/seg-02.txt`(别 `echo >`)。
120
- 2. **反查 voice_id**(meta.md 不存 UUID):Read `meta.md` 自己读出 external_id(形如 `dh-xxxxxxxx`,或老格式 `external_id: ...` 行),**不用 grep/sed**。再单条:
121
- ```bash
122
- gen avatar profile dh-xxxxxxxx
123
- ```
124
- 从它 stdout 的 JSON 里读 `heygen_voice_id`(出片要用的 `heygen_avatar_look_ids[0]` 也一并记下,§6 要),**别 `$()`/`jq` 截**。
125
- 3. **只重生这一段音频**(免费 / 极廉,不烧视频额度;新文本**内联**进 `--text`,你手里就有):
126
- ```bash
127
- gen voice preview --voice <heygen_voice_id> --text "<新 text>" --output ~/digital-human/videos/<VIDEO_ID>/segments/seg-02.wav
128
- ```
129
-
130
- **其他段音频不动**。多段改:对每个改的段各发**一条** `gen voice preview`(LLM 并行多条,不写 `for`/`nohup`),然后 §6 **只重渲一次**。
131
-
132
- > 可选:让用户先**听**新的 seg-N 音频确认,再 §6 重渲(省得渲完又不满意)。强烈推荐多段改 / 大改时先听。
133
-
134
- ### 6. 重拼音频 → 整条重渲 → final-v<下一版本>.mp4
135
-
136
- **版本号计算 + 上限清理**(LLM 看 `ls` 输出自己数,别 `sed`/`sort`/`wc`/`$(())`):
137
-
138
- ```bash
139
- ls ~/digital-human/videos/<VIDEO_ID>/final-v*.mp4
140
- ```
141
-
142
- - LLM 从输出里找最大版本号 `LAST`(例已有 final-v1/v2 → LAST=2),下一版 `NEXT = LAST + 1`(=3)。
143
- - **决策表 #12 上限 10**:输出里 final-v*.mp4 已有 **≥ 10 个** → 删最老的那个(单条命令、路径写全):
144
- ```bash
145
- rm ~/digital-human/videos/<VIDEO_ID>/final-v1.mp4
146
- ```
147
-
148
- **重拼音频 + 单条重渲**(两条单命令,值内联):
149
-
150
- ```bash
151
- gen voice concat --workspace-dir ~/digital-human/videos/<VIDEO_ID>/ --out ~/digital-human/videos/<VIDEO_ID>/full-audio.mp3
152
- ```
153
-
154
- ```bash
155
- gen avatar video --avatar <heygen_avatar_look_ids[0]> --audio ~/digital-human/videos/<VIDEO_ID>/full-audio.mp3 -o ~/digital-human/videos/<VIDEO_ID>/final-v<NEXT>.mp4
156
- ```
157
-
158
- - `gen voice concat` 重拼整条音频(各段已含新的 seg-NN.wav,自动扫 `segments/seg-*` 排序拼接)。**🔴 严禁手搓 `ffmpeg -f concat`**——即使撞 soft-deny 也绝不回退(同 [generate.md §7](generate.md))。
159
- - `<heygen_avatar_look_ids[0]>` 用 §5 第 2 步 `gen avatar profile` 输出里读到的值内联;`<NEXT>` 用上面算出的下一版本号。
160
-
161
- > 没有 `gen video stitch` 那一步了——音频先行下成片是单条渲染,不是拼接多段 mp4。
162
-
163
- **失败处理**:`gen avatar video --audio` 返回 `音频驱动 doctor-video 暂未开放` → 后端音频驱动未启用(灰度状态,不是用户错误)→ 告知"稍后可用",不重试。其他失败看 `error_message`,音频已拼好可直接重跑渲染。
164
-
165
- ### 7. 更新 meta.md + history.md
166
-
167
- 都用 **Edit tool** 改(别 `echo >>` / `cat <<EOF`,沙箱拦;时间用你 LLM 知道的当前时间内联,别 `$(date)`)。
168
-
169
- **meta.md**:表头"当前版本"→ `final-v<NEXT>.mp4`;Segments 表第 N 行"最后更新"→ `<当前时间> (v<NEXT>)`。
170
-
171
- **history.md** 末尾用 Edit 追加一行(保留旧记录):
172
- ```markdown
173
- - **<当前时间>** [v<NEXT>] 改了 seg-<N>: "<旧 text>" → "<新 text>" (<编辑距离比例 X%>,重渲整条)
174
- ```
175
-
176
- ### 8. 报告
177
-
178
- > "✅ 视频更新完成: ~/digital-human/videos/<VIDEO_ID>/final-v<NEXT>.mp4
179
- > 改动:seg-<N> 从 '<旧>' 改成 '<新>',重生了该段音频 + 整条重渲一次。
180
- > 历史版本 final-v1 ~ final-v<LAST> 仍在,想回退直接用旧版本。"
181
-
182
- 如果触发了 final-vN.mp4 删最老逻辑(数量 ≥ 10),加一句"📦 历史版本已满 10,自动清理了最早的 final-v<被删版本>.mp4"。
183
-
184
- ---
185
-
186
- ## Edge cases
187
-
188
- ### 1. 用户说"改第 N 段"但 N 不存在
189
- 例:只有 4 段,用户说"改第 7 段"。反问:"这条只有 4 句(seg-01 ~ seg-04),没有第 7 句。要改哪一句?"
190
-
191
- ### 2. final-v* 数量已达上限 10 + 用户改段
192
- 按 §6 自动删最老 final-v1.mp4,保留 latest + 9 个历史。**不问用户**,在 §8 报告里告知。
193
-
194
- ### 3. 用户改的 text 跟旧 text 完全一样(edit_ratio = 0)
195
- 反问:"新旧 text 一样,确认要重渲吗?(音频会重生,韵律可能有细微差异,但要烧一次重渲额度)" 默认推荐"不改"。
196
-
197
- ### 4. 用户说"改最后一段"/"改第一段"(相对引用)
198
- 单条 `ls ~/digital-human/videos/<VIDEO_ID>/segments/` 看分段,LLM **数** `seg-*.txt` 的个数得到最后一段号(例 4 个 → 最后是 `seg-04`),"第一段"= `seg-01`。别用 `$(ls ... | wc -l)` 拼。
199
-
200
- ### 5. 用户改完后说"算了,用回旧版本"
201
- - final-v<NEXT>.mp4 留着(磁盘占用接受)
202
- - 告诉用户旧版本 `~/digital-human/videos/<VIDEO_ID>/final-v<LAST>.mp4` 还在
203
- - 不做"标记 active 版本"功能(final-vN 并存)
204
-
205
- ---
206
-
207
- ## Common Mistakes
208
-
209
- | ❌ Mistake | ✅ 正确做法 | 为什么 |
210
- |---|---|---|
211
- | 出片后才让用户发现要改 | 引导用户在出片前 [音频试听阶段](generate.md) 改(免费、不渲染) | edit.md 改一句要整条重渲烧额度;音频阶段改免费 |
212
- | 改一句重生所有段音频 | 仅重生改的那段 `seg-NN.wav`,其他不动 | 其他段音频不变,重拼时复用 |
213
- | 多段改时每段都重渲一次 | 逐段重生音频,最后**只重渲一次**整条 | 视频是单条渲染,改 N 段也只渲一次 |
214
- | 用 `gen video stitch` / `gen avatar video --text` 出片 | 用 `gen voice concat` + `gen avatar video --audio` | 音频先行架构下成片是单条音频驱动渲染 |
215
- | 改段时静默不二次确认 | edit_ratio > 30% 反问(且提示要重渲);≤30% 静默 | 重渲烧额度不可逆 |
216
- | final 覆盖旧版本 | final-vN.mp4 并存,最多 10 个 | 用户对比 / 回退 |
217
- | 反查 voice/avatar 从 meta.md 读 | 永远 `gen avatar profile <external_id>` 反查 | meta.md 不存 UUID(UX 原则) |
218
- | 改段时把 video-id 改成新的 | 同一条视频改段复用原 video-id | video-id 是用户对这条视频的锚 |
219
- | `--audio` 报"暂未开放"当 bug 反复重试 | 后端灰度状态,告知稍后可用 | 音频驱动有 feature flag |
@@ -1,378 +0,0 @@
1
- # Generate Talking-Head Video(音频先行流程)
2
-
3
- 用已有分身(用户自训的 或 公共池的)合成口播视频。
4
-
5
- **关键设计(v2,音频先行)**:
6
- - **音频拆段、视频不拆**。脚本按句拆成 N 段,**逐段先出音频**给用户试听;用户确认音色 / 内容后,把各段音频拼成整条,**单条连续渲染**出成片。
7
- - **为什么**:实测把长视频拆成段再拼(逐段渲染 + ffmpeg/多场景拼接)会在段边界产生卡顿 / 冻帧;单条连续渲染最流畅。音频侧拆段则让用户能**只重生不满意的那一句**(音频试听免费 / 极廉),不用整条重来。
8
- - 每条视频独立 workspace 目录(`~/digital-human/videos/<video-id>/`),音频段 + 整条音频 + 成片都持久化。详见 [edit.md](edit.md)。
9
-
10
- > **关键不变量**:用户**逐段听过并确认**每段音频之前,**绝不进行任何视频渲染**(视频才烧额度)。音频段可反复单独重生(免费 / 极廉),视频只在最终确认后整条渲染一次——这是进入视频的**唯一闸门**。
11
-
12
- ## 0. 运行环境约束:bash 是**受限沙箱**(必读,否则会撞墙)
13
-
14
- agent-runtime 的 bash 有 baseline 安全策略(claude-code 移植,烤进镜像、部署关不掉)。下面这些**会被 soft-deny 拦**,本 SKILL 的所有命令都**不要用**:
15
-
16
- - ❌ `$( )` 命令替换(如 `VAR=$(cmd)`、`--text "$(cat f)"`)
17
- - ❌ `for ... do ... done` / `while` 循环,命令内换行,`;` / `&&` / `||` 串多条
18
- - ❌ `nohup ... &` 后台,`> file` / `>>` 重定向,`| jq` / `| sed` / `| wc` 管道
19
-
20
- **改用这些沙箱安全的等价做法:**
21
-
22
- | 想做的事 | ❌ 别写 | ✅ 改成 |
23
- |---|---|---|
24
- | 读文件内容喂给命令 | `--text "$(cat seg.txt)"` | 先用 **Read tool** 读出文本,再把内容**内联**进命令:`--text "大家好,我是张医生。"` |
25
- | 解析 CLI 的 JSON 输出 | `VAR=$(cmd \| jq -r .x)` | 直接跑 `cmd`(单条),**从它 stdout 的 JSON 里读**你要的字段,后续命令里内联用 |
26
- | 并发跑 N 段 | `for ...; nohup cmd & done` | **LLM 并行 tool_use**:一次性发 N 个独立 bash 调用,每个**只一条命令**,runtime 自动并发 |
27
- | 写文件 | `echo "x" > f` / `cat > f <<EOF` | 用 **Write tool** 写 |
28
-
29
- **核心心法**:**一条 bash 只跑一条简单命令**;循环 / 并发 / 取值 / 读写文件,统统交给 **LLM 自己的 tool_use 编排**(Read/Write/并行 bash),不要让 shell 做。下面各步的命令都按这个写;看到旧式 shell-magic 一律按上表翻译。
30
-
31
- ## 主流程: 用 X 做视频说 Y
32
-
33
- ### 1. 确定 X 是什么
34
-
35
- 按 [SKILL.md "LLM 如何区分用户输入是名字还是 external_id"](../SKILL.md) 表格判断:
36
- - 名字 → 在 `~/digital-human/avatars.md` "名字"列模糊匹配
37
- - 新格式 external_id (`dh-[a-z0-9]{8}`) → 直接走 §2
38
- - 存量 ASCII slug (例 `dr-wang`) → 走 [manage.md "存量分身"](manage.md) 流程
39
- - 歧义 → 反问消歧
40
-
41
- 匹配结果:
42
- - 1 个匹配 → 取该行 external_id,走 §2
43
- - 多个匹配 → 反问消歧("你说的是'张医生'还是'张主任'?")
44
- - 0 个匹配 → 反问"没找到'X',要训新的吗?"(走 [train.md](train.md))
45
-
46
- ### 2. 反查 voice_id + avatar_id
47
-
48
- 后端 CLI 要求传 `--avatar` + `--voice` 两个 UUID,不支持 `--external-id` 自动反查,所以**必须先查**:
49
-
50
- ```bash
51
- gen avatar profile <external_id>
52
- ```
53
-
54
- 跑这一条,从它 stdout 的 JSON 里读两个字段(**别用 `$()`/`jq` 截**,直接看输出):
55
- - `heygen_voice_id` → 后面音频段(`gen voice preview`)用,**内联**进 `--voice` 参数
56
- - `heygen_avatar_look_ids[0]` → 最终视频(audio-driven)用,**内联**进 `--avatar` 参数
57
-
58
- > 记住这两个值(在你后续命令里直接写出来),不要存 shell 变量——沙箱里 `VAR=$(...)` 会被拦。
59
-
60
- ### 3. 创建 video 工作目录
61
-
62
- VIDEO_ID = 纯时间戳 `YYYYMMDD-HHMM`(决策表 #11,无 slug v1)。**你(LLM)知道当前时间,直接写出这个 id**,不要用 `$(date)`。
63
-
64
- 先 `ls ~/digital-human/videos/` 看有没有同名目录;有的话给 id 追加 `-2`/`-3`。然后单条命令建目录:
65
-
66
- ```bash
67
- mkdir -p ~/digital-human/videos/20260531-1430/segments
68
- ```
69
-
70
- (后续命令里 `~/digital-human/videos/20260531-1430` 这个路径直接写全,不要存 `$VIDEO_DIR` 变量。)
71
-
72
- ### 4. 拆段(按句,用于逐段试听)
73
-
74
- **拆段目的变了**:不再是规避"长 prompt 让 trained avatar 挂死"(那是旧的逐段视频约束;音频驱动单条渲染已实测能吃长文本)。现在拆段**纯粹为了让用户逐段试听 + 只重生不满意的那一句**。所以按**自然句**拆,一句一段,粒度便于复听 / 重 roll。
75
-
76
- text 很短(一两句)→ 也走 segments 流程(单段也存),保持一致性。
77
-
78
- **拆段分隔符优先级**(从强到弱依次尝试):
79
-
80
- 1. **句末标点**:`。`(中文句号)/ `.`(英文句号)/ `?` / `?` / `!` / `!`
81
- 2. **句中标点**(若句子过长想再切):`,` / `,` / `;` / `;` / `:` / `:`
82
- 3. **空格**(英文 / 长拼接句)
83
- 4. 没有任何分隔符的超长句 → 整句作一段(音频无硬字数限制,单条渲染也能吃)
84
-
85
- > 旧版的"每段 ≤ 50 字"硬约束**已取消**——那是 text-driven 逐段视频的限制,音频先行下不适用。拆段只为试听粒度,不为绕字数。
86
-
87
- **写每段 text 文件**:用 **Write tool** 逐个写(2 位零填充,支持到 99 段),**别用 `echo > seg.txt`**(重定向沙箱拦,见 §0)。每段一个文件:
88
-
89
- - `~/digital-human/videos/<VIDEO_ID>/segments/seg-01.txt` ← 内容 `大家好,我是张医生。`
90
- - `~/digital-human/videos/<VIDEO_ID>/segments/seg-02.txt` ← 内容 `今天给大家推荐一款...`
91
- - ……(拆出几段写几个文件)
92
-
93
- > 这些 text 文件是 workspace 的真值,§6 出音频时文本你手头就有(直接内联进 `--text`),后续 §6.6 改词 / §6.8 续生才需要 Read 回来。
94
-
95
- ### 5. 写 meta.md(初版,state=audio generating)
96
-
97
- "当前版本"字段是 **state machine** 的 marker:`(audio generating)` → `(audio-review pending)` → `(rendering)` → `final-vN.mp4`。本步写第 1 阶段 placeholder `(audio generating)`,表示音频段还在生成。
98
-
99
- 跨 session 续接(§6.8)和 manage.md "列我的视频"都靠这字段路由。
100
-
101
- ```markdown
102
- # Video: <slug from text or topic>
103
-
104
- | 字段 | 值 |
105
- |---|---|
106
- | video-id | <VIDEO_ID> |
107
- | avatar | <display_name> (external_id: <external_id>) |
108
- | 创建时间 | <YYYY-MM-DD HH:MM> |
109
- | 当前版本 | (audio generating) |
110
-
111
- ## 总文本
112
-
113
- > <用户给的完整 text>
114
-
115
- ## Segments
116
-
117
- | seg | text | 音频时长 | 音频文件 | 最后更新 |
118
- |---|---|---|---|---|
119
- | 01 | "大家好,我是张医生。" | ~3s | segments/seg-01.wav | <时间> |
120
- | 02 | "今天给大家推荐..." | ~4s | segments/seg-02.wav | <时间> |
121
- | ... |
122
- ```
123
-
124
- > **不写 voice_id / avatar_id UUID** 到 meta.md —— 用户能看到这个文件,UUID 不冒泡;需要时 LLM 重新 `gen avatar profile` 反查。
125
-
126
- ### 6. 逐段生成音频(**用 LLM 并行 tool_use,不要 shell 循环**)
127
-
128
- 每段用 `gen voice preview` 出**音频**(纯音频试听,**不烧视频额度、不单独计费**)。`gen voice preview` 是**同步**的——一条命令跑完就拿到那段 .wav + 它的时长。所以:
129
-
130
- **做法:你(LLM)一次性发出 N 个并行 bash 调用,每个就一条命令**,形如:
131
-
132
- ```bash
133
- gen voice preview --voice <voice_id> --text "大家好,我是张医生。" --output ~/digital-human/videos/<VIDEO_ID>/segments/seg-01.wav
134
- ```
135
-
136
- - 每段文本从你 §4 拆好的内容**内联**进 `--text`(你手里就有这些句子,不用 `cat`/`$()` 去读)。
137
- - N 个调用**并行发出**(同一轮 tool_use 里多个 bash),runtime 自动并发——等价于以前的"并发派发",但**没有** `for` / `nohup &` / `$()` / `>` 重定向。
138
- - 幂等:已存在的 seg-NN.wav 不用重发。
139
- - 每条命令的输出里有该段的 `duration`、`audio_path` 等——**记下每段时长**(§6.5 要用,不必再 ffprobe)。
140
-
141
- > **严禁**:`for ... gen voice preview ... done` 循环、`nohup ... &`、`--text "$(cat ...)"`、`> seg.log`。这些在沙箱里全被拦(见 §0)。并发靠 LLM 并行发多条,不是 shell。
142
-
143
- 每段独立音频文件,文件名跟 text 零填充对齐(seg-01 ↔ seg-01.wav)。某段命令报错就单独重发那一段。
144
-
145
- ---
146
-
147
- ### 6.5 音频 review checkpoint(**核心 — 进入视频的唯一闸门**)
148
-
149
- > 实施 [SPEC 2026-05-30 digital-human-audio-first-flow](docs/2026-05-30-digital-human-audio-first-flow.md) §4.3。
150
- > **N==1 单段也走本 checkpoint** —— 音频先行下,单段也要让用户先听过音色 / 内容再出片(视频烧额度,听了再出最稳)。
151
-
152
- **Step 1: 更新 meta.md state**:`(audio generating)` → `(audio-review pending)`。
153
-
154
- **Step 2: 取每段时长**:用 §6 每条 `gen voice preview` 输出里已经返回的 `duration`(不用再 ffprobe;那是个 `for`+`$()` 循环,沙箱拦)。若确实漏了某段时长,单条 `ffprobe -v error -show_entries format=duration -of csv=p=0 ~/digital-human/videos/<VIDEO_ID>/segments/seg-NN.wav` 补一段即可(一次一条,别写循环)。
155
-
156
- **Step 3: 向用户逐段呈现音频供试听 + 列 segments 表**
157
-
158
- 把**每段文本 + 对应音频文件**都列出来,让用户逐条听:
159
-
160
- ```text
161
- ✅ N 段音频已生成,出视频前请逐段听一下(确认音色 + 内容):
162
-
163
- | seg | text | 音频时长 | 试听文件 |
164
- |---|---|---|---|
165
- | 01 | "大家好,我是张医生。" | 2.95s | segments/seg-01.wav |
166
- | 02 | "今天介绍一款..." | 4.12s | segments/seg-02.wav |
167
- | ... |
168
-
169
- 逐段听完怎么处理?
170
- - "都 OK,出视频吧" → 我把音频拼成整条,渲染最终视频
171
- - "seg-N 改成 X"(改词)/ "seg-N 重生一遍"(同词重 roll) → 只重生那一段音频(免费 / 极廉),改完再回来给你听
172
- - "都不要,重新搞" → 删了用同一段 text 重新生成
173
- ```
174
-
175
- **Note**:user-facing 文案**禁止泄露内部 §-编号**。用户看不懂章节号,用自然语言。
176
-
177
- **Step 4: LLM turn-end 等用户响应**
178
-
179
- CC chat 单 turn 内无法同步 block 等输入,必须 turn-end。state 由 meta.md `(audio-review pending)` 持久化,即使用户中途关 session,新 session 也能通过 §6.8 重展 checkpoint。
180
-
181
- > **再次强调闸门**:只有用户明确"都 OK 出视频"才进 §7。任意一段被指出要改 → 走 §6.6 只重生那段音频、回到本 checkpoint 重新听。**没听完确认前不渲染视频。**
182
-
183
- ### 6.6 Checkpoint 内改段(只重生音频,免费 / 极廉,不版本化)
184
-
185
- 跟 [edit.md](edit.md) 主流程区别:edit.md 改的是**已出片**的视频(要重渲染整条);本段是**出片前**改音频,只重生该段音频,**不触发任何视频渲染**。
186
-
187
- **完整 5 步**(state 不变,仍是 `(audio-review pending)`):
188
-
189
- 1. **编辑距离阈值检查**(沿用 [edit.md §4 决策 #14](edit.md)):
190
- - 计算 `levenshtein(旧 text, 新 text) / max(len(旧), len(新))`
191
- - `> 30%` → 反问"确认改吗?";`≤ 30%` → typo/标点微调,静默继续
192
- - (音频 preview 免费 / 极廉,这里阈值确认主要防误改内容,不是防烧钱)
193
- 2. **覆盖 `segments/seg-NN.txt`**(用 **Write tool** 写新文本,别用 `echo >`)+ **重生 `segments/seg-NN.wav`**(一条命令,新文本**内联**):
194
- - voice_id:若这是新 session、手头没有,先 Read `meta.md` 拿 external_id(形如 `dh-xxxxxxxx`)→ 跑 `gen avatar profile <external_id>` → 从输出读 `heygen_voice_id`(别 `$()`/`jq`)。
195
- ```bash
196
- gen voice preview --voice <voice_id> --text "<新 text>" --output ~/digital-human/videos/<VIDEO_ID>/segments/seg-02.wav
197
- ```
198
- (其他段音频不动;voice preview 无语速/语气调节,只能改词或同词重 roll)
199
- 3. **更新 meta.md** segments 表第 N 行"最后更新"列
200
- 4. **显式写 history.md** 一行:
201
- ```
202
- - **<当前时间>** [audio-review] 改了 seg-N 音频: '<旧>' → '<新>' (<编辑距离比例 X%>)
203
- ```
204
- `[audio-review]` 标记区分出片后的 `[v1]/[v2]`,跨 session 续接时能看到历史改动
205
- 5. **回 §6.5 checkpoint 重展**(防止改完 seg-2 又发现 seg-3 也要改的二次 edit)
206
-
207
- ### 6.7 用户说"都不要"——丢弃当前 video
208
-
209
- **二次确认**(rm -rf 不可恢复,跟 manage.md 删视频对齐):
210
-
211
- > "确认删除 `videos/<VIDEO_ID>/`?这条还没出片,音频段删了不可恢复。
212
- > 删完会用**同一段 text** 重新生成(每次音色 / 韵律会有细微差异,所以'重新搞'本身有意义)。"
213
-
214
- 用户确认 → 走(把 `<VIDEO_ID>` 这段路径**写全**,别用 `$VIDEO_ID` shell 变量——沙箱里它没被安全赋过值):
215
-
216
- ```bash
217
- rm -rf ~/digital-human/videos/20260531-1430
218
- ```
219
-
220
- 回到 §3 用新 VIDEO_ID + **原 text** 重建(不再问用户 text)。
221
-
222
- ### 6.8 跨 session 续接
223
-
224
- 用户开新 session 说"出视频 X" / "刚才那条出片" / "继续 X 视频":
225
-
226
- ```bash
227
- ls -t ~/digital-human/videos/ # 按 mtime 降序
228
- ```
229
-
230
- 逐目录 Read `meta.md` 取"当前版本",**按 state 路由**:
231
-
232
- | state | 行为 |
233
- |---|---|
234
- | `(audio generating)` | 音频段还没生完(可能 §6 跑一半中断)→ 走下面"续生音频流程" |
235
- | `(audio-review pending)` | 音频都生完,等用户确认 → **重展 §6.5 checkpoint** |
236
- | `(rendering)` | 已确认、视频在渲染(可能中断)→ 走 §7 重跑渲染(concat 已有则跳过,直接重渲) |
237
- | `(generating)` / `(review pending)`(旧 v1 字面量) | 旧流程(逐段视频)半成品,跟音频先行**不兼容**(段是 .mp4)→ 反问"这条是旧流程的半成品,续接方式已变,要用原文重新生成吗?" 同意 → §3 用原 text 全新生成,**不**走下面续生流程 |
238
- | `final-vN.mp4` | 已出片,跳过(不是未完成视频) |
239
-
240
- 匹配结果:1 个 → 按 state 决定;多个 → 反问"以下是未完成的:..."(列 state);0 个 → "没找到未完成的视频,要新建吗?"
241
-
242
- **`(audio generating)` 续生音频流程**(跟 §6 同款,沙箱安全):
243
-
244
- 1. `ls ~/digital-human/videos/<VIDEO_ID>/segments/` 看哪些 `seg-NN.txt` 还**缺**对应的 `seg-NN.wav`。
245
- 2. voice_id:Read `meta.md` 拿 external_id → `gen avatar profile <external_id>` → 从输出读 `heygen_voice_id`(别 `$()`/`jq`)。
246
- 3. 对**每个缺的段**,各发**一条** `gen voice preview`(LLM 并行多条,不写 `for`/`nohup`),文本从对应 `seg-NN.txt` 用 **Read tool** 读出来内联:
247
- ```bash
248
- gen voice preview --voice <voice_id> --text "<该段文本>" --output ~/digital-human/videos/<VIDEO_ID>/segments/seg-NN.wav
249
- ```
250
-
251
- **续生完成后**:全部 seg-NN.wav 存在 → 转 state 到 `(audio-review pending)` + 跳 §6.5 checkpoint 让用户逐段试听。**不**直接出片(用户没听过)。
252
-
253
- ---
254
-
255
- ### 7. 确认后:拼接音频 → 单条渲染 → final-v1.mp4
256
-
257
- 仅当 §6.5 用户明确"都 OK 出视频"才进本步。
258
-
259
- **Step 1: 更新 meta.md state**:`(audio-review pending)` → `(rendering)`。
260
-
261
- **Step 2: 把确认的各段音频拼成整条** —— **必须用 `gen voice concat`**(单条 CLI,内部用 concat filter 重编码、非 stream-copy,避免拼缝杂音;不会撞沙箱):
262
-
263
- ```bash
264
- gen voice concat --workspace-dir ~/digital-human/videos/<VIDEO_ID>/ --out ~/digital-human/videos/<VIDEO_ID>/full-audio.mp3
265
- ```
266
-
267
- > 自动扫 `segments/seg-*.{wav,mp3}` 按文件名自然排序拼接(Mode A)。从它 stdout 的 JSON 读 `duration_sec`(别 `$()`/`jq`)。
268
- >
269
- > 🔴 **严禁自己跑 `ffmpeg -f concat ...` 拼音频** —— 即使 §6 撞过 soft-deny,也**绝不**回退到手搓 ffmpeg。`gen voice concat` 就是为此封装的规范 wrapper;手搓 ffmpeg = 绕过抽象 + 易出拼缝问题。这是硬规矩(见 SKILL.md Global Rule 关于 anti-fabrication / 不绕 CLI 抽象)。
270
-
271
- **Step 3: 用整条音频单条渲染 avatar 视频**(audio-driven,无 scene 边界):
272
-
273
- ```bash
274
- gen avatar video --avatar <avatar_id> --audio ~/digital-human/videos/<VIDEO_ID>/full-audio.mp3 -o ~/digital-human/videos/<VIDEO_ID>/final-v1.mp4
275
- ```
276
-
277
- - 音频驱动:嘴型跟随 `full-audio.mp3`,成片声音 = 用户逐段确认过的那条,分毫不差。
278
- - **单条连续渲染**,不拆 scene → 无段边界卡顿。
279
-
280
- **渲染成功后,更新 meta.md "当前版本"**:`(rendering)` → `final-v1.mp4`。
281
-
282
- **失败处理**:
283
- - `gen avatar video --audio` 返回 `音频驱动 doctor-video 暂未开放` → 后端音频驱动未启用(feature flag off / 未上线)。**这是部署状态,不是用户错误**。告诉用户"音频驱动出片功能正在灰度,稍后可用",**不要**反复重试。
284
- - 其他渲染失败 → 看 `error_message`,必要时重跑本步(音频已拼好,直接重渲不用重做音频)。
285
-
286
- ### 8. 写 history.md
287
-
288
- 用 **Write/Edit tool** 写 `~/digital-human/videos/<VIDEO_ID>/history.md`(别用 `echo >>` / `cat <<EOF`,沙箱拦)。时间用你(LLM)知道的当前时间内联,别用 `$(date)`。
289
-
290
- - 若 history.md 已存在(§6.6 期间有过 `[audio-review]` 改动行)→ 用 Edit **追加**一行,保留旧记录:
291
- ```
292
- - **2026-06-01 14:30** [v1] 出片 (N 段音频拼接 + 单条渲染)
293
- ```
294
- - 否则用 Write 新建:
295
- ```
296
- # History
297
-
298
- - **2026-06-01 14:30** [v1] 出片 (N 段音频拼接 + 单条渲染)
299
- ```
300
- ```
301
-
302
- ### 9. 报告 final-v1.mp4 给用户
303
-
304
- > "✅ 视频生成完成: ~/digital-human/videos/<VIDEO_ID>/final-v1.mp4
305
- > 共 N 段音频拼成整条、单条连续渲染(无段间卡顿)。
306
- > 想改哪句,说一声——我重生那段音频给你听,确认后重渲一次。"
307
-
308
- ---
309
-
310
- ## 没指定分身但要"做个口播视频"(公共 avatar)
311
-
312
- 公共 avatar 不是用户自己的声音,**不走音频先行试听**(没有"确认音色"的意义)。但仍**单条渲染、不拆段**——这是另一半的卡顿修复(拆段拼接才卡)。
313
-
314
- 1. 展示 [avatar-catalog.md](avatar-catalog.md) 公共 avatar / voice 清单(用 preview 图或名称,不冒泡 UUID)
315
- 2. 让用户选一个 avatar + 一个 voice
316
- 3. 走 workspace 持久化(VIDEO_ID 同 §3),但**整段一次渲染**(text 不拆;单条渲染已实测能吃长文本)。**一条命令、文本内联**(别用 `\` 续行换行、别 `$(cat)`——沙箱拦,见 §0):
317
- ```bash
318
- gen video --provider heygen --avatar <公共 avatar id> --voice <公共 voice id> --text "大家好,今天给大家介绍……(整段文案内联在这里)" -o ~/digital-human/videos/<VIDEO_ID>/final-v1.mp4
319
- ```
320
- (文案是用户给的、你手头就有,直接写进 `--text`;太长也照样内联成一行,不要拆文件再 `cat`。)
321
- 4. meta.md state 直接 `(rendering)` → `final-v1.mp4`(无音频 review 阶段);§8 history + §9 报告同主流程
322
-
323
- 公共 avatar 视频的 `meta.md` "avatar" 字段写 `公共: Abigail (id 不冒泡)` 即可。
324
-
325
- > 公共池若想也走"逐段试听",需要先有该 voice 的 `gen voice preview` + 后端公共-avatar 音频驱动支持——**v2 暂不做**,公共路径保持单条 text 渲染。
326
-
327
- ---
328
-
329
- ## 可选参数
330
-
331
- **音频段(`gen voice preview`)**:`--text-type <text|ssml>`。**注:`voice preview` 不支持 `--voice-speed`/`--voice-emotion`**——音频先行下逐段音频暂无语速/语气调节(改词 / 重 roll 即可;真要语速/情绪是后续 CLI + 上游支持的 follow-up,别凭印象拼 flag,见 SKILL rule #4)。
332
-
333
- **公共 avatar 单段视频(`gen video --provider heygen`)**:
334
-
335
- | 参数 | 说明 | 默认值 |
336
- |---|---|---|
337
- | `--avatar-style <style>` | normal \| circle \| closeUp | normal |
338
- | `--voice-speed <n>` | 语速 0.5-1.5 | 1.0 |
339
- | `--voice-emotion <emotion>` | Excited \| Friendly \| Serious \| Soothing \| Broadcaster | — |
340
- | `--bg-color <hex>` | 背景色 | — |
341
- | `--caption` | 启用字幕 | — |
342
- | `--title <title>` | 视频标题 | — |
343
-
344
- `gen avatar video`(trained avatar)接受 `--avatar` / `--voice` / `--text`(文本驱动)**或** `--avatar` / `--audio`(音频驱动)/ `--title` / `-o`。
345
-
346
- ---
347
-
348
- ## 训完立即 demo(可选)
349
-
350
- 如果用户在原始训练请求里说了想"看效果 / 做一段试试 / 给我个 demo":训完直接走 §主流程,text 用用户给的或默认 `"大家好,我是 X"`(X = 名字)。短 demo 也走音频先行(让用户先听音色)。
351
-
352
- **降级**:如果 trained avatar 还在 still-processing(`gen avatar video` 返回错误)→ 音频段照常出(voice 已训好),但视频改用公共 avatar + 该音频单条渲染应急,告诉用户:
353
- > "分身的视觉部分还在最后定型(几十分钟内会好),先用通用形象演示。**这条 demo 仅证明 voice 训成,不展示真分身效果** — 几小时后用 'X 做个视频说 Y' 重试就能用真分身了。"
354
-
355
- > **注意区分**:这是 still-processing 期间的**临时降级**,不算 mix-and-match。
356
-
357
- ---
358
-
359
- ## Advanced: mix-and-match(参考实现,**不**进 SKILL.md 主流程)
360
-
361
- > [workspace SPEC §2](../../../../docs/superpowers/specs/2026-05-14-digital-human-workspace-naming.md) 已声明 mix-and-match 是非目标。用户主动要求时再让 advanced agent 临时执行。
362
-
363
- 例:"用张医生的声音 + 公共 Aditya 形象":逐段用张医生 voice 出音频 → 拼接 → `gen video --provider heygen --avatar Aditya_public_4 --audio full-audio.mp3`(若公共路径支持 --audio;否则用 `--voice <张医生.voice_id> --text` 单条文本渲染)。LLM 主动告诉用户:"这是参考实现,未来若呼声高再进主流程。"
364
-
365
- ---
366
-
367
- ## Common Mistakes
368
-
369
- | ❌ Mistake | ✅ 正确做法 | 为什么 |
370
- |---|---|---|
371
- | 没让用户听音频就出视频 | 必须先 §6.5 逐段试听 + 确认,才 §7 渲染 | 视频烧额度;音色 / 内容问题在音频阶段(免费)就该抓出来 |
372
- | 把视频拆段渲染再拼接 | 整条音频**单条渲染**(`gen avatar video --audio`) | 拆段拼接是卡顿根因;单条连续渲染最流畅 |
373
- | 用 `gen avatar video --text` 出 trained avatar 视频 | 走音频先行:`voice preview` 逐段 → `voice concat` → `avatar video --audio` | text 驱动跳过了试听闸门,且回到逐段问题 |
374
- | `gen voice concat` 用 mp3 `-c copy` 拼 | 用 `gen voice concat`(内部 concat filter 重编码) | stream-copy 拼 mp3 有 priming/gap 杂音 |
375
- | 把 voice_id / avatar_id UUID 报给用户 | 只回 mp4 路径 + "用 X 做了" | UUID 不冒泡是核心 UX 原则 |
376
- | 公共"做个口播"也拆段 | 公共走 `gen video --provider heygen` **单条** text 渲染 | 公共不是用户声音、不试听;但单条渲染仍要(防卡顿) |
377
- | segments 写 ephemeral 目录(`/tmp`) | 写到 `~/digital-human/videos/<id>/segments/` | 用户后续想改一句,workspace 是单一真值 |
378
- | `gen avatar video --audio` 报"暂未开放"当成 bug 反复重试 | 这是后端灰度状态,告知用户稍后可用,不重试 | 音频驱动有 feature flag,未上线时正常拒绝 |