@optima-chat/gen-cli 2.3.0 → 2.4.0

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 (34) hide show
  1. package/.claude/skills/digital-human/SKILL.md +131 -17
  2. package/.claude/skills/digital-human/references/edit.md +121 -100
  3. package/.claude/skills/digital-human/references/generate.md +256 -124
  4. package/.claude/skills/digital-human/references/manage.md +28 -17
  5. package/.claude/skills/digital-human/references/train.md +47 -62
  6. package/.claude/skills/gen/SKILL.md +13 -0
  7. package/.claude/skills/motion-control/SKILL.md +68 -0
  8. package/.claude/skills/video-compose/SKILL.md +144 -0
  9. package/.claude/skills/video-compose/scripts/video_compose.py +272 -0
  10. package/.claude/skills/video-edit/SKILL.md +62 -1
  11. package/.claude/skills/video-gen/SKILL.md +72 -6
  12. package/.claude/skills/video-translate/SKILL.md +205 -69
  13. package/assets/video-compose/bgm-library/SOURCES.md +25 -0
  14. package/assets/video-compose/bgm-library/calm/calm-01.mp3 +0 -0
  15. package/assets/video-compose/bgm-library/calm/calm-02.mp3 +0 -0
  16. package/assets/video-compose/bgm-library/dramatic/dramatic-01.mp3 +0 -0
  17. package/assets/video-compose/bgm-library/dramatic/dramatic-02.mp3 +0 -0
  18. package/assets/video-compose/bgm-library/energetic/energetic-01.mp3 +0 -0
  19. package/assets/video-compose/bgm-library/energetic/energetic-02.mp3 +0 -0
  20. package/assets/video-compose/bgm-library/sad/sad-01.mp3 +0 -0
  21. package/assets/video-compose/bgm-library/sad/sad-02.mp3 +0 -0
  22. package/assets/video-compose/bgm-library/upbeat/upbeat-01.mp3 +0 -0
  23. package/assets/video-compose/bgm-library/upbeat/upbeat-02.mp3 +0 -0
  24. package/assets/video-compose/bgm-library/warm/warm-01.mp3 +0 -0
  25. package/assets/video-compose/bgm-library/warm/warm-02.mp3 +0 -0
  26. package/assets/video-compose/voice-samples/01-/346/270/251/346/232/226/345/260/221/345/245/263.mp3 +0 -0
  27. package/assets/video-compose/voice-samples/02-/347/224/234/347/276/216/345/245/263/345/243/260.mp3 +0 -0
  28. package/assets/video-compose/voice-samples/03-/347/224/234/347/276/216/345/205/203/346/260/224.mp3 +0 -0
  29. package/assets/video-compose/voice-samples/04-/346/270/205/347/224/234/345/260/221/345/245/263.mp3 +0 -0
  30. package/assets/video-compose/voice-samples/05-/345/276/241/345/247/220.mp3 +0 -0
  31. package/assets/video-compose/voice-samples/06-/346/210/220/347/206/237/347/237/245/346/200/247.mp3 +0 -0
  32. package/assets/video-compose/voice-samples/07-/345/245/263/344/270/273/346/222/255.mp3 +0 -0
  33. package/assets/video-compose/voice-samples/CATALOG.md +9 -0
  34. package/package.json +3 -2
@@ -1,8 +1,32 @@
1
- # Generate Talking-Head Video
1
+ # Generate Talking-Head Video(音频先行流程)
2
2
 
3
3
  用已有分身(用户自训的 或 公共池的)合成口播视频。
4
4
 
5
- **关键设计**:每条视频独立 workspace 目录(`~/digital-human/videos/<video-id>/`),segments 分段保存,改一段不用全重生。详见 [edit.md](edit.md)。
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 一律按上表翻译。
6
30
 
7
31
  ## 主流程: 用 X 做视频说 Y
8
32
 
@@ -21,65 +45,58 @@
21
45
 
22
46
  ### 2. 反查 voice_id + avatar_id
23
47
 
24
- 后端 CLI [`doctor.ts:151-152`](file:///C:/Users/zy/optima-gen/packages/cli/src/commands/doctor.ts) 要求 `gen avatar video` 传 `--avatar` + `--voice` 两个 UUID,不支持 `--external-id` 自动反查,所以**必须先查**:
48
+ 后端 CLI 要求传 `--avatar` + `--voice` 两个 UUID,不支持 `--external-id` 自动反查,所以**必须先查**:
25
49
 
26
50
  ```bash
27
- PROFILE=$(gen avatar profile "$external_id")
28
- VOICE_ID=$(echo "$PROFILE" | jq -r '.heygen_voice_id')
29
- AVATAR_ID=$(echo "$PROFILE" | jq -r '.heygen_avatar_look_ids[0]')
51
+ gen avatar profile <external_id>
30
52
  ```
31
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
+
32
60
  ### 3. 创建 video 工作目录
33
61
 
62
+ VIDEO_ID = 纯时间戳 `YYYYMMDD-HHMM`(决策表 #11,无 slug v1)。**你(LLM)知道当前时间,直接写出这个 id**,不要用 `$(date)`。
63
+
64
+ 先 `ls ~/digital-human/videos/` 看有没有同名目录;有的话给 id 追加 `-2`/`-3`。然后单条命令建目录:
65
+
34
66
  ```bash
35
- # 纯时间戳 (决策表 #11,无 slug v1)
36
- VIDEO_ID="$(date +%Y%m%d-%H%M)"
37
-
38
- # 同一分钟内多次生成 → 自动追加 -2/-3 后缀避免覆盖
39
- # (不能直接 mkdir -p,因 mkdir -p 静默复用已存在目录会覆盖 segments)
40
- n=2
41
- while [ -d ~/digital-human/videos/$VIDEO_ID ]; do
42
- VIDEO_ID="$(date +%Y%m%d-%H%M)-$n"
43
- n=$((n+1))
44
- done
45
-
46
- VIDEO_DIR=~/digital-human/videos/$VIDEO_ID
47
- mkdir -p "$VIDEO_DIR/segments"
48
- cd "$VIDEO_DIR"
67
+ mkdir -p ~/digital-human/videos/20260531-1430/segments
49
68
  ```
50
69
 
51
- 参考 `video-gen` skill 同款 disambiguation pattern。
70
+ (后续命令里 `~/digital-human/videos/20260531-1430` 这个路径直接写全,不要存 `$VIDEO_DIR` 变量。)
52
71
 
53
- ### 4. 拆段(text > 50 字时)
72
+ ### 4. 拆段(按句,用于逐段试听)
54
73
 
55
- text 50 跳过拆段,直接当 1 段处理(单段也走 segments 流程,保持一致性)
74
+ **拆段目的变了**:不再是规避"长 prompt trained avatar 挂死"(那是旧的逐段视频约束;音频驱动单条渲染已实测能吃长文本)。现在拆段**纯粹为了让用户逐段试听 + 只重生不满意的那一句**。所以按**自然句**拆,一句一段,粒度便于复听 / 重 roll
56
75
 
57
- text > 50 字 按分隔符自然分割,每段 50 字 / ≤ 10 秒(spec §13.3 硬约束)
76
+ text 很短(一两句)也走 segments 流程(单段也存),保持一致性。
58
77
 
59
78
  **拆段分隔符优先级**(从强到弱依次尝试):
60
79
 
61
80
  1. **句末标点**:`。`(中文句号)/ `.`(英文句号)/ `?` / `?` / `!` / `!`
62
- 2. **句中标点**(若 1 拆出的段仍 > 50 字):`,` / `,` / `;` / `;` / `:` / `:`
63
- 3. **空格**( 2 仍不行):按空格切,适用英文 / 长句拼接
64
- 4. **硬切兜底**(若上述全无):按字符数 45-50 切,**避免在词中间切**(中文按字符 OK;英文遇到字母连续 → 退回到上一个空格)
65
-
66
- 例:
67
- - "今天给大家推荐一款超级好用又便宜还能解决各种皮肤问题的护肤霜" (29 字,无标点)
68
- 50 字,不用拆,作为 seg-01 整段
69
- - "今天给大家推荐一款超级好用又便宜还能解决各种皮肤问题的护肤霜它真的太棒了你们一定要试试" (45 字,无标点)
70
- 50 字,整段
71
- - "今天给大家推荐一款超级好用又便宜还能解决各种皮肤问题的护肤霜它真的太棒了你们一定要试试我自己用了两个月效果惊人" (54 字,无标点)
72
- 超 50 字,无标点 → fallback 硬切到 45 字附近:`"...护肤霜它真的太棒了你们一定要试试" (35 字) + "我自己用了两个月效果惊人" (12 字)`
73
-
74
- **写每段 text 文件**:
75
- ```bash
76
- # 2 位零填充,支持到 99 段
77
- echo "大家好,我是张医生。" > segments/seg-01.txt
78
- echo "今天给大家推荐一款..." > segments/seg-02.txt
79
- # ...
80
- ```
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 回来。
81
94
 
82
- ### 5. 写 meta.md(初版)
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 "列我的视频"都靠这字段路由。
83
100
 
84
101
  ```markdown
85
102
  # Video: <slug from text or topic>
@@ -89,7 +106,7 @@ echo "今天给大家推荐一款..." > segments/seg-02.txt
89
106
  | video-id | <VIDEO_ID> |
90
107
  | avatar | <display_name> (external_id: <external_id>) |
91
108
  | 创建时间 | <YYYY-MM-DD HH:MM> |
92
- | 当前版本 | final-v1.mp4 |
109
+ | 当前版本 | (audio generating) |
93
110
 
94
111
  ## 总文本
95
112
 
@@ -97,90 +114,223 @@ echo "今天给大家推荐一款..." > segments/seg-02.txt
97
114
 
98
115
  ## Segments
99
116
 
100
- | seg | text | 时长 | 文件 | 最后更新 |
117
+ | seg | text | 音频时长 | 音频文件 | 最后更新 |
101
118
  |---|---|---|---|---|
102
- | 01 | "大家好,我是张医生。" | ~3s | segments/seg-01.mp4 | <时间> |
103
- | 02 | "今天给大家推荐..." | ~4s | segments/seg-02.mp4 | <时间> |
119
+ | 01 | "大家好,我是张医生。" | ~3s | segments/seg-01.wav | <时间> |
120
+ | 02 | "今天给大家推荐..." | ~4s | segments/seg-02.wav | <时间> |
104
121
  | ... |
105
122
  ```
106
123
 
107
124
  > **不写 voice_id / avatar_id UUID** 到 meta.md —— 用户能看到这个文件,UUID 不冒泡;需要时 LLM 重新 `gen avatar profile` 反查。
108
125
 
109
- ### 6. 逐段生成 mp4
126
+ ### 6. 逐段生成音频(**用 LLM 并行 tool_use,不要 shell 循环**)
127
+
128
+ 每段用 `gen voice preview` 出**音频**(纯音频试听,**不烧视频额度、不单独计费**)。`gen voice preview` 是**同步**的——一条命令跑完就拿到那段 .wav + 它的时长。所以:
129
+
130
+ **做法:你(LLM)一次性发出 N 个并行 bash 调用,每个就一条命令**,形如:
110
131
 
111
132
  ```bash
112
- for SEG_TXT in segments/seg-*.txt; do
113
- N=$(basename "$SEG_TXT" .txt | sed 's/seg-//')
114
- gen avatar video --avatar "$AVATAR_ID" --voice "$VOICE_ID" \
115
- --text "$(cat "$SEG_TXT")" \
116
- -o "segments/seg-$N.mp4"
117
- done
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 重新生成
118
173
  ```
119
174
 
120
- 每段独立 mp4,文件名跟 text 文件零填充对齐(seg-01.txt seg-01.mp4)。
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 内改段(只重生音频,免费 / 极廉,不版本化)
121
184
 
122
- ### 7. ffmpeg concat → final-v1.mp4(两步走 + 透明告知)
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 变量——沙箱里它没被安全赋过值):
123
215
 
124
216
  ```bash
125
- printf "file '%s'\n" segments/seg-*.mp4 > concat.txt
126
-
127
- # Step 1: 试 -c copy(快,无画质损失)
128
- if ffmpeg -f concat -safe 0 -i concat.txt -c copy final-v1.mp4 2>/dev/null; then
129
- CONCAT_MODE="stream-copy"
130
- else
131
- # Step 2: fallback 重编码
132
- ffmpeg -f concat -safe 0 -i concat.txt -c:v libx264 -preset fast -c:a aac final-v1.mp4
133
- CONCAT_MODE="re-encode"
134
- fi
217
+ rm -rf ~/digital-human/videos/20260531-1430
135
218
  ```
136
219
 
137
- **禁止**:静默切换不告知用户(决策表 #15)。走 fallback 时**必须**在 §9 报告里加一句"段间 codec 兼容问题,自动重编码合成"
220
+ 回到 §3 用新 VIDEO_ID + **原 text** 重建(不再问用户 text)
138
221
 
139
- ### 8. history.md
222
+ ### 6.8 session 续接
223
+
224
+ 用户开新 session 说"出视频 X" / "刚才那条出片" / "继续 X 视频":
140
225
 
141
226
  ```bash
142
- cat > history.md <<EOF
143
- # History
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 同款,沙箱安全):
144
243
 
145
- - **$(date +'%Y-%m-%d %H:%M')** [v1] 初版生成 (N 段)
146
- EOF
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
147
265
  ```
148
266
 
149
- ### 9. 报告 final-v1.mp4 给用户
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 抽象)。
150
270
 
151
- > "✅ 视频生成完成: $VIDEO_DIR/final-v1.mp4
152
- > 共 N 段,每段独立保存在 segments/。
153
- > 想改哪一段说一声,只重生那一段就行(不用全重做)。"
271
+ **Step 3: 用整条音频单条渲染 avatar 视频**(audio-driven,无 scene 边界):
154
272
 
155
- 如果走了 ffmpeg fallback(`CONCAT_MODE=re-encode`),多一句:
156
- > "⚠️ 段间 codec 兼容问题,自动重编码合成(多花了 ~ X 秒,画质轻微损失)。"
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
+ > 想改哪句,说一声——我重生那段音频给你听,确认后重渲一次。"
157
307
 
158
308
  ---
159
309
 
160
- ## 没指定分身但要"做个口播视频"
310
+ ## 没指定分身但要"做个口播视频"(公共 avatar)
161
311
 
162
- 走"公共 avatar"分支:
312
+ 公共 avatar 不是用户自己的声音,**不走音频先行试听**(没有"确认音色"的意义)。但仍**单条渲染、不拆段**——这是另一半的卡顿修复(拆段拼接才卡)。
163
313
 
164
314
  1. 展示 [avatar-catalog.md](avatar-catalog.md) 公共 avatar / voice 清单(用 preview 图或名称,不冒泡 UUID)
165
315
  2. 让用户选一个 avatar + 一个 voice
166
- 3. **是否走 workspace 持久化**:
167
- - 用户给的 text > 50 字 → 拆段,走 workspace(VIDEO_ID 同 §3)
168
- - text ≤ 50 字 → 同样走 workspace(单段也存),不冒泡 UUID
169
- 4. 调用(注意:公共 avatar 用 `gen video --provider heygen`,**不是** `gen avatar video`):
316
+ 3. workspace 持久化(VIDEO_ID 同 §3),但**整段一次渲染**(text 不拆;单条渲染已实测能吃长文本)。**一条命令、文本内联**(别用 `\` 续行换行、别 `$(cat)`——沙箱拦,见 §0):
170
317
  ```bash
171
- gen video --provider heygen \
172
- --avatar <公共 avatar id> \
173
- --voice <公共 voice id> \
174
- --text "$(cat segments/seg-NN.txt)" \
175
- -o segments/seg-NN.mp4
318
+ gen video --provider heygen --avatar <公共 avatar id> --voice <公共 voice id> --text "大家好,今天给大家介绍……(整段文案内联在这里)" -o ~/digital-human/videos/<VIDEO_ID>/final-v1.mp4
176
319
  ```
177
- 5. 后续 §7 ffmpeg concat + §8 history + §9 报告同主流程
320
+ (文案是用户给的、你手头就有,直接写进 `--text`;太长也照样内联成一行,不要拆文件再 `cat`。)
321
+ 4. meta.md state 直接 `(rendering)` → `final-v1.mp4`(无音频 review 阶段);§8 history + §9 报告同主流程
178
322
 
179
323
  公共 avatar 视频的 `meta.md` "avatar" 字段写 `公共: Abigail (id 不冒泡)` 即可。
180
324
 
325
+ > 公共池若想也走"逐段试听",需要先有该 voice 的 `gen voice preview` + 后端公共-avatar 音频驱动支持——**v2 暂不做**,公共路径保持单条 text 渲染。
326
+
181
327
  ---
182
328
 
183
- ## 可选参数(`gen video --provider heygen` 路径专用)
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`)**:
184
334
 
185
335
  | 参数 | 说明 | 默认值 |
186
336
  |---|---|---|
@@ -188,47 +338,29 @@ EOF
188
338
  | `--voice-speed <n>` | 语速 0.5-1.5 | 1.0 |
189
339
  | `--voice-emotion <emotion>` | Excited \| Friendly \| Serious \| Soothing \| Broadcaster | — |
190
340
  | `--bg-color <hex>` | 背景色 | — |
191
- | `--bg-image <url>` | 背景图 URL | — |
192
341
  | `--caption` | 启用字幕 | — |
193
342
  | `--title <title>` | 视频标题 | — |
194
343
 
195
- `gen avatar video` 不支持以上 flag(只接受 `--avatar` / `--voice` / `--text` / `--title` / `-o`)。
344
+ `gen avatar video`(trained avatar)接受 `--avatar` / `--voice` / `--text`(文本驱动)**或** `--avatar` / `--audio`(音频驱动)/ `--title` / `-o`。
196
345
 
197
346
  ---
198
347
 
199
348
  ## 训完立即 demo(可选)
200
349
 
201
- 如果用户在原始训练请求里说了想"看效果 / 做一段试试 / 给我个 demo":
202
-
203
- 训完直接走 §主流程,text 用用户给的或默认 `"大家好,我是 X"`(X = 名字)。
350
+ 如果用户在原始训练请求里说了想"看效果 / 做一段试试 / 给我个 demo":训完直接走 §主流程,text 用用户给的或默认 `"大家好,我是 X"`(X = 名字)。短 demo 也走音频先行(让用户先听音色)。
204
351
 
205
- **降级**:如果 trained avatar 还在 still-processing(`gen avatar video` 返回错误)→ 用公共 avatar + trained voice 合成,告诉用户:
206
- > "分身的视觉部分还在最后定型(几十分钟内会好),先用通用形象 + 你的声音演示。**这条 demo 仅证明 voice 训成,不展示真分身效果** — 几小时后用 'X 做个视频说 Y' 重试就能用真分身了。"
352
+ **降级**:如果 trained avatar 还在 still-processing(`gen avatar video` 返回错误)→ 音频段照常出(voice 已训好),但视频改用公共 avatar + 该音频单条渲染应急,告诉用户:
353
+ > "分身的视觉部分还在最后定型(几十分钟内会好),先用通用形象演示。**这条 demo 仅证明 voice 训成,不展示真分身效果** — 几小时后用 'X 做个视频说 Y' 重试就能用真分身了。"
207
354
 
208
- 降级时 voice trained 的,avatar 选 [avatar-catalog.md](avatar-catalog.md) 里任一公共 avatar
209
-
210
- > **注意区分**:这是 trained avatar still-processing 期间的**临时降级**(待定型后自动恢复用真 avatar),**不算** §"Advanced: mix-and-match"。Mix-and-match 是用户主动要求"用 X 的声音 + 公共 Y 形象"的长期混搭意图;本段是 voice 已训好但 avatar 还没定型的应急方案,语义不同。
355
+ > **注意区分**:这是 still-processing 期间的**临时降级**,不算 mix-and-match
211
356
 
212
357
  ---
213
358
 
214
359
  ## Advanced: mix-and-match(参考实现,**不**进 SKILL.md 主流程)
215
360
 
216
- > [workspace SPEC §2](https://github.com/Optima-Chat/optima-agent/blob/main/docs/superpowers/specs/2026-05-14-digital-human-workspace-naming.md) 已声明 mix-and-match 是非目标。下面是参考实现,**不**写进 LLM 的引导路径,用户主动要求时再让 advanced agent 临时执行。
217
-
218
- 例:"用张医生的声音 + 公共 Aditya 形象":
219
-
220
- 1. Read `~/digital-human/avatars.md` 找张医生的 external_id
221
- 2. `gen avatar profile <id>` 反查 voice_id
222
- 3. 调:
223
- ```bash
224
- gen video --provider heygen \
225
- --voice <张医生.voice_id> \
226
- --avatar Aditya_public_4 \
227
- --text "..." \
228
- -o segments/seg-NN.mp4
229
- ```
361
+ > [workspace SPEC §2](../../../../docs/superpowers/specs/2026-05-14-digital-human-workspace-naming.md) 已声明 mix-and-match 是非目标。用户主动要求时再让 advanced agent 临时执行。
230
362
 
231
- LLM 主动告诉用户:"这是参考实现,未来若呼声高再进主流程。"
363
+ 例:"用张医生的声音 + 公共 Aditya 形象":逐段用张医生 voice 出音频 → 拼接 → `gen video --provider heygen --avatar Aditya_public_4 --audio full-audio.mp3`(若公共路径支持 --audio;否则用 `--voice <张医生.voice_id> --text` 单条文本渲染)。LLM 主动告诉用户:"这是参考实现,未来若呼声高再进主流程。"
232
364
 
233
365
  ---
234
366
 
@@ -236,11 +368,11 @@ LLM 主动告诉用户:"这是参考实现,未来若呼声高再进主流程。"
236
368
 
237
369
  | ❌ Mistake | ✅ 正确做法 | 为什么 |
238
370
  |---|---|---|
239
- | 直接 `gen avatar video --external-id dh-...` | 必须先 `gen avatar profile` voice_id+avatar_id 再调 | doctor.ts:151-152 显示 `--avatar` + `--voice` 是 required;`--external-id` flag 不存在 |
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 杂音 |
240
375
  | 把 voice_id / avatar_id UUID 报给用户 | 只回 mp4 路径 + "用 X 做了" | UUID 不冒泡是核心 UX 原则 |
241
- | text > 50 字直接 send | 客户端拆段,每段 50 / 10 | 长 prompt 会让 trained avatar 挂死(spec §13.3) |
242
- | 公共"做个口播"用 `gen avatar video` | `gen video --provider heygen` | `gen avatar video` 仅用于已 onboard 训练分身 |
243
- | 默认走 mix-and-match | 默认是"X 做视频" 整套用,mix advanced 用户主动要 | mix 主流程会让 UX 复杂化 |
244
- | segments 写 ephemeral 目录(`/tmp`)而非 workspace | 写到 `~/digital-human/videos/<id>/segments/` | 用户后续想改一段,workspace 是单一真值;`/tmp` session 后丢失 |
245
- | ffmpeg fallback 静默切换不告知 | 走 re-encode 必须在报告里明示"自动重编码,画质轻微损失" | 用户看到画质变了 / 这次慢了会卡 debug,透明告知避免认知负担 |
246
- | 单段视频(text ≤ 50 字)跳过 workspace | 也写 `videos/<id>/segments/seg-01.{txt,mp4}` + `final-v1.mp4` | 一致性让用户后续"改这段"路径统一,不需要"短文本是另一种流程"的特殊知识 |
376
+ | 公共"做个口播"也拆段 | 公共走 `gen video --provider heygen` **单条** text 渲染 | 公共不是用户声音、不试听;但单条渲染仍要(防卡顿) |
377
+ | segments ephemeral 目录(`/tmp`) | 写到 `~/digital-human/videos/<id>/segments/` | 用户后续想改一句,workspace 是单一真值 |
378
+ | `gen avatar video --audio` "暂未开放"当成 bug 反复重试 | 这是后端灰度状态,告知用户稍后可用,不重试 | 音频驱动有 feature flag,未上线时正常拒绝 |
@@ -12,34 +12,45 @@
12
12
 
13
13
  ## "我的视频有哪些" / "列我的视频" / "做过哪些视频"
14
14
 
15
- mtime 降序列 `~/digital-human/videos/`:
15
+ > ⚠️ **沙箱 bash 须知**(同 [generate.md §0](generate.md)):agent-runtime 的 bash 软拒绝 `for`/`while` 循环、`$( )`、`| grep`/`| sed`/`| awk`/`| wc` 管道、`>` 重定向。所以**别写 `for d in $(ls ...)` 循环**逐目录抓字段——靠 **LLM 自己迭代**:一条 `ls` 列目录,再对每个目录用 **Read tool** 读 meta.md,字段(分身 / 当前版本 / 总文本 / 段数)你读出来自己提取、自己映射 emoji,**不靠 shell**。
16
+
17
+ **步骤**:
16
18
 
17
19
  ```bash
18
- for d in $(ls -t ~/digital-human/videos/); do
19
- cd ~/digital-human/videos/$d
20
- # 读 meta.md 提取关键字段
21
- AVATAR=$(grep -E '^\| avatar \|' meta.md | sed 's/.*| //' | sed 's/ |$//')
22
- VERSION=$(grep -E '^\| 当前版本 \|' meta.md | sed 's/.*| //' | sed 's/ |$//')
23
- TEXT_PREVIEW=$(awk '/^## 总文本/{getline; getline; print}' meta.md | head -c 30)
24
- N_SEGS=$(ls segments/seg-*.txt 2>/dev/null | wc -l)
25
- # 渲染
26
- echo "- $d ($AVATAR): \"$TEXT_PREVIEW...\" [$N_SEGS 段, $VERSION]"
27
- done
20
+ ls -t ~/digital-human/videos/
28
21
  ```
29
22
 
23
+ 对列出的每个 `<video-id>`,**用 Read tool** 读 `~/digital-human/videos/<video-id>/meta.md`,从中读出:`avatar`(分身)、`当前版本`、`总文本`前 30 字、segments 表行数(= 段数)。**按"当前版本"值映射 state emoji**:
24
+
25
+ | "当前版本" 值 | 状态标注 |
26
+ |---|---|
27
+ | `(audio generating)` | 🚧 音频生成中 (中断,可续接) |
28
+ | `(audio-review pending)` | ⏳ 待试听确认 (audio-review pending) |
29
+ | `(rendering)` | 🎬 渲染中 |
30
+ | `(generating)` / `(review pending)`(旧 v1 字面量) | 📼 旧流程视频 (v1,改动需整条重生) |
31
+ | `final-vN.mp4` | ✅ final-vN.mp4 |
32
+ | 其它 | ❓ 未知 state |
33
+
30
34
  渲染给用户(markdown 表格更友好):
31
35
 
32
- | video-id | 分身 | 总文本(前 30 字) | 段数 | 当前版本 |
36
+ | video-id | 分身 | 总文本(前 30 字) | 段数 | 状态 |
33
37
  |---|---|---|---|---|
34
- | 20260515-1430 | 张医生 | "大家好,我是张医生。今天介..." | 4 | final-v2.mp4 |
35
- | 20260514-0945 | 李老师 | "..." | 6 | final-v1.mp4 |
38
+ | 20260515-1430 | 张医生 | "大家好,我是张医生。今天介..." | 4 | final-v2.mp4 |
39
+ | 20260515-1620 | 李老师 | "今天讲..." | 5 | ⏳ 待试听确认 (audio-review pending) |
40
+ | 20260516-0915 | 王主任 | "..." | 3 | 🚧 音频生成中 (中断,可续接) |
41
+ | 20260514-0945 | 张医生 | "..." | 6 | ✅ final-v1.mp4 |
42
+
43
+ **用户暗示 + 操作引导**(给 LLM 的内部规则,**对用户**只输出自然语言话术,**不**报 §-编号):
44
+ - 看到 ⏳ → user-facing 提示 "想继续把 X 的音频听一下、确认出片吗?"(LLM 内部跳 [generate.md §6.5 重展试听 checkpoint](generate.md))
45
+ - 看到 🚧 → user-facing 提示 "想续生剩余音频段吗?"(LLM 内部跳 [generate.md §6.8 续生流程](generate.md))
46
+ - 看到 🎬 → user-facing 提示 "X 还在渲染,稍等;若中断了我可以重渲"(LLM 内部跳 [generate.md §6.8 → (rendering) 重渲](generate.md))
47
+ - 看到 📼 → 旧流程(v1 逐段视频)视频;用户说"改"时 [edit.md §1.5 Step 0](edit.md) 会拦下、引导整条重新生成(新旧不兼容)
48
+ - 看到 ✅ → 视频已完成,用户说"改 seg-N"时由 [edit.md §1.5 state check](edit.md) 路由
36
49
 
37
50
  文件夹不存在 / 空 → "你还没生成过视频,要做一条吗?"(走 [generate.md](generate.md))
38
51
 
39
52
  ## "把张医生改成张主任"
40
53
 
41
- ## "把张医生改成张主任"
42
-
43
54
  1. Read `~/digital-human/avatars.md`
44
55
  2. 在"名字"列找 `张医生`
45
56
  3. Edit 该行"名字"列的 cell:`张医生` → `张主任`
@@ -86,7 +97,7 @@ done
86
97
 
87
98
  1. 确认 video-id 存在(`ls ~/digital-human/videos/<id>`)
88
99
  2. 二次确认:
89
- > "确认删除视频 `<id>`($N_SEGS 段, 当前 final-vX.mp4)?
100
+ > "确认删除视频 `<id>`(<段数> 段, 当前 final-vX.mp4)?
90
101
  > 这会删除整个目录(segments / final-v* / meta.md / history.md),**不可恢复**。"
91
102
  3. 用户确认 → `rm -rf ~/digital-human/videos/<id>`
92
103
  4. 反馈"已删除视频 `<id>`。"