@optima-chat/gen-cli 1.2.1 → 2.1.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 (42) hide show
  1. package/.claude/skills/digital-human/SKILL.md +195 -0
  2. package/.claude/skills/digital-human/references/avatar-catalog.md +47 -0
  3. package/.claude/skills/digital-human/references/edit.md +198 -0
  4. package/.claude/skills/digital-human/references/generate.md +246 -0
  5. package/.claude/skills/digital-human/references/manage.md +126 -0
  6. package/.claude/skills/digital-human/references/train.md +291 -0
  7. package/.claude/skills/gen/SKILL.md +33 -26
  8. package/.claude/skills/video-edit/SKILL.md +99 -17
  9. package/.claude/skills/video-gen/SKILL.md +34 -108
  10. package/.claude/skills/video-translate/SKILL.md +254 -11
  11. package/dist/commands/asr.d.ts.map +1 -1
  12. package/dist/commands/asr.js +6 -3
  13. package/dist/commands/asr.js.map +1 -1
  14. package/dist/commands/avatar.d.ts +12 -2
  15. package/dist/commands/avatar.d.ts.map +1 -1
  16. package/dist/commands/avatar.js +27 -9
  17. package/dist/commands/avatar.js.map +1 -1
  18. package/dist/commands/doctor.d.ts +14 -4
  19. package/dist/commands/doctor.d.ts.map +1 -1
  20. package/dist/commands/doctor.js +59 -38
  21. package/dist/commands/doctor.js.map +1 -1
  22. package/dist/commands/image.d.ts.map +1 -1
  23. package/dist/commands/image.js +13 -6
  24. package/dist/commands/image.js.map +1 -1
  25. package/dist/commands/tryon.d.ts.map +1 -1
  26. package/dist/commands/tryon.js +6 -3
  27. package/dist/commands/tryon.js.map +1 -1
  28. package/dist/commands/tts.d.ts.map +1 -1
  29. package/dist/commands/tts.js +6 -3
  30. package/dist/commands/tts.js.map +1 -1
  31. package/dist/commands/video-translate.d.ts.map +1 -1
  32. package/dist/commands/video-translate.js +10 -6
  33. package/dist/commands/video-translate.js.map +1 -1
  34. package/dist/commands/video.d.ts +19 -0
  35. package/dist/commands/video.d.ts.map +1 -1
  36. package/dist/commands/video.js +292 -12
  37. package/dist/commands/video.js.map +1 -1
  38. package/dist/utils/output.d.ts +9 -2
  39. package/dist/utils/output.d.ts.map +1 -1
  40. package/dist/utils/output.js +9 -2
  41. package/dist/utils/output.js.map +1 -1
  42. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ # Generate Talking-Head Video
2
+
3
+ 用已有分身(用户自训的 或 公共池的)合成口播视频。
4
+
5
+ **关键设计**:每条视频独立 workspace 目录(`~/digital-human/videos/<video-id>/`),segments 分段保存,改一段不用全重生。详见 [edit.md](edit.md)。
6
+
7
+ ## 主流程: 用 X 做视频说 Y
8
+
9
+ ### 1. 确定 X 是什么
10
+
11
+ 按 [SKILL.md "LLM 如何区分用户输入是名字还是 external_id"](../SKILL.md) 表格判断:
12
+ - 名字 → 在 `~/digital-human/avatars.md` "名字"列模糊匹配
13
+ - 新格式 external_id (`dh-[a-z0-9]{8}`) → 直接走 §2
14
+ - 存量 ASCII slug (例 `dr-wang`) → 走 [manage.md "存量分身"](manage.md) 流程
15
+ - 歧义 → 反问消歧
16
+
17
+ 匹配结果:
18
+ - 1 个匹配 → 取该行 external_id,走 §2
19
+ - 多个匹配 → 反问消歧("你说的是'张医生'还是'张主任'?")
20
+ - 0 个匹配 → 反问"没找到'X',要训新的吗?"(走 [train.md](train.md))
21
+
22
+ ### 2. 反查 voice_id + avatar_id
23
+
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` 自动反查,所以**必须先查**:
25
+
26
+ ```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]')
30
+ ```
31
+
32
+ ### 3. 创建 video 工作目录
33
+
34
+ ```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"
49
+ ```
50
+
51
+ 参考 `video-gen` skill 同款 disambiguation pattern。
52
+
53
+ ### 4. 拆段(text > 50 字时)
54
+
55
+ text ≤ 50 字 → 跳过拆段,直接当 1 段处理(单段也走 segments 流程,保持一致性)。
56
+
57
+ text > 50 字 → 按分隔符自然分割,每段 ≤ 50 字 / ≤ 10 秒(spec §13.3 硬约束)。
58
+
59
+ **拆段分隔符优先级**(从强到弱依次尝试):
60
+
61
+ 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
+
82
+ ### 5. 写 meta.md(初版)
83
+
84
+ ```markdown
85
+ # Video: <slug from text or topic>
86
+
87
+ | 字段 | 值 |
88
+ |---|---|
89
+ | video-id | <VIDEO_ID> |
90
+ | avatar | <display_name> (external_id: <external_id>) |
91
+ | 创建时间 | <YYYY-MM-DD HH:MM> |
92
+ | 当前版本 | final-v1.mp4 |
93
+
94
+ ## 总文本
95
+
96
+ > <用户给的完整 text>
97
+
98
+ ## Segments
99
+
100
+ | seg | text | 时长 | 文件 | 最后更新 |
101
+ |---|---|---|---|---|
102
+ | 01 | "大家好,我是张医生。" | ~3s | segments/seg-01.mp4 | <时间> |
103
+ | 02 | "今天给大家推荐..." | ~4s | segments/seg-02.mp4 | <时间> |
104
+ | ... |
105
+ ```
106
+
107
+ > **不写 voice_id / avatar_id UUID** 到 meta.md —— 用户能看到这个文件,UUID 不冒泡;需要时 LLM 重新 `gen avatar profile` 反查。
108
+
109
+ ### 6. 逐段生成 mp4
110
+
111
+ ```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
118
+ ```
119
+
120
+ 每段独立 mp4,文件名跟 text 文件零填充对齐(seg-01.txt ↔ seg-01.mp4)。
121
+
122
+ ### 7. ffmpeg concat → final-v1.mp4(两步走 + 透明告知)
123
+
124
+ ```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
135
+ ```
136
+
137
+ **禁止**:静默切换不告知用户(决策表 #15)。走 fallback 时**必须**在 §9 报告里加一句"段间 codec 兼容问题,自动重编码合成"。
138
+
139
+ ### 8. 写 history.md
140
+
141
+ ```bash
142
+ cat > history.md <<EOF
143
+ # History
144
+
145
+ - **$(date +'%Y-%m-%d %H:%M')** [v1] 初版生成 (N 段)
146
+ EOF
147
+ ```
148
+
149
+ ### 9. 报告 final-v1.mp4 给用户
150
+
151
+ > "✅ 视频生成完成: $VIDEO_DIR/final-v1.mp4
152
+ > 共 N 段,每段独立保存在 segments/。
153
+ > 想改哪一段说一声,只重生那一段就行(不用全重做)。"
154
+
155
+ 如果走了 ffmpeg fallback(`CONCAT_MODE=re-encode`),多一句:
156
+ > "⚠️ 段间 codec 兼容问题,自动重编码合成(多花了 ~ X 秒,画质轻微损失)。"
157
+
158
+ ---
159
+
160
+ ## 没指定分身但要"做个口播视频"
161
+
162
+ 走"公共 avatar"分支:
163
+
164
+ 1. 展示 [avatar-catalog.md](avatar-catalog.md) 公共 avatar / voice 清单(用 preview 图或名称,不冒泡 UUID)
165
+ 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`):
170
+ ```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
176
+ ```
177
+ 5. 后续 §7 ffmpeg concat + §8 history + §9 报告同主流程
178
+
179
+ 公共 avatar 视频的 `meta.md` "avatar" 字段写 `公共: Abigail (id 不冒泡)` 即可。
180
+
181
+ ---
182
+
183
+ ## 可选参数(`gen video --provider heygen` 路径专用)
184
+
185
+ | 参数 | 说明 | 默认值 |
186
+ |---|---|---|
187
+ | `--avatar-style <style>` | normal \| circle \| closeUp | normal |
188
+ | `--voice-speed <n>` | 语速 0.5-1.5 | 1.0 |
189
+ | `--voice-emotion <emotion>` | Excited \| Friendly \| Serious \| Soothing \| Broadcaster | — |
190
+ | `--bg-color <hex>` | 背景色 | — |
191
+ | `--bg-image <url>` | 背景图 URL | — |
192
+ | `--caption` | 启用字幕 | — |
193
+ | `--title <title>` | 视频标题 | — |
194
+
195
+ `gen avatar video` 不支持以上 flag(只接受 `--avatar` / `--voice` / `--text` / `--title` / `-o`)。
196
+
197
+ ---
198
+
199
+ ## 训完立即 demo(可选)
200
+
201
+ 如果用户在原始训练请求里说了想"看效果 / 做一段试试 / 给我个 demo":
202
+
203
+ 训完直接走 §主流程,text 用用户给的或默认 `"大家好,我是 X"`(X = 名字)。
204
+
205
+ **降级**:如果 trained avatar 还在 still-processing(`gen avatar video` 返回错误)→ 用公共 avatar + trained voice 合成,告诉用户:
206
+ > "分身的视觉部分还在最后定型(几十分钟内会好),先用通用形象 + 你的声音演示。**这条 demo 仅证明 voice 训成,不展示真分身效果** — 几小时后用 'X 做个视频说 Y' 重试就能用真分身了。"
207
+
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 还没定型的应急方案,语义不同。
211
+
212
+ ---
213
+
214
+ ## Advanced: mix-and-match(参考实现,**不**进 SKILL.md 主流程)
215
+
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
+ ```
230
+
231
+ LLM 主动告诉用户:"这是参考实现,未来若呼声高再进主流程。"
232
+
233
+ ---
234
+
235
+ ## Common Mistakes
236
+
237
+ | ❌ Mistake | ✅ 正确做法 | 为什么 |
238
+ |---|---|---|
239
+ | 直接 `gen avatar video --external-id dh-...` | 必须先 `gen avatar profile` 拿 voice_id+avatar_id 再调 | doctor.ts:151-152 显示 `--avatar` + `--voice` 是 required;`--external-id` flag 不存在 |
240
+ | 把 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` | 一致性让用户后续"改这段"路径统一,不需要"短文本是另一种流程"的特殊知识 |
@@ -0,0 +1,126 @@
1
+ # Manage Avatars & Videos
2
+
3
+ 管理 `~/digital-human/` 工作空间下的分身(`avatars.md`)+ 生成过的视频(`videos/<id>/`)+ 训练素材(`training/<id>/`)。
4
+
5
+ ## "我的分身有哪些"
6
+
7
+ 直接 Read `~/digital-human/avatars.md` 渲染给用户。
8
+
9
+ 文件不存在 → "你还没训过分身,要训一个吗?"(走 [train.md](train.md))
10
+
11
+ 文件存在但只有表头(无数据行)→ 同上。
12
+
13
+ ## "我的视频有哪些" / "列我的视频" / "做过哪些视频"
14
+
15
+ 按 mtime 降序列 `~/digital-human/videos/`:
16
+
17
+ ```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
28
+ ```
29
+
30
+ 渲染给用户(markdown 表格更友好):
31
+
32
+ | video-id | 分身 | 总文本(前 30 字) | 段数 | 当前版本 |
33
+ |---|---|---|---|---|
34
+ | 20260515-1430 | 张医生 | "大家好,我是张医生。今天介..." | 4 | final-v2.mp4 |
35
+ | 20260514-0945 | 李老师 | "..." | 6 | final-v1.mp4 |
36
+
37
+ 文件夹不存在 / 空 → "你还没生成过视频,要做一条吗?"(走 [generate.md](generate.md))
38
+
39
+ ## "把张医生改成张主任"
40
+
41
+ ## "把张医生改成张主任"
42
+
43
+ 1. Read `~/digital-human/avatars.md`
44
+ 2. 在"名字"列找 `张医生`
45
+ 3. Edit 该行"名字"列的 cell:`张医生` → `张主任`
46
+ 4. 反馈"已改名,external_id 不变,以后可以用'张主任'引用了"
47
+
48
+ 如果用户给的旧名字找不到 → 反问"没找到叫'张医生'的分身,你现在的分身有:[列出]"
49
+
50
+ ## "删了张医生"
51
+
52
+ 1. Read `~/digital-human/avatars.md`
53
+ 2. 找到"张医生"行,记下该行所有字段(名字 / external_id / 训练时间 / preview URL)
54
+ 3. **二次确认 + 显式列出恢复锚点**:
55
+ > "确认删除'张医生'? **注意**:这是对你视角的硬删除 —
56
+ > - 你失去所有引用路径(external_id 从你视野消失 + 没有 list 端点)
57
+ > - 后端 DB 行成为 orphan 数据,**找回需要联系运维直接查 DB**
58
+ > - 运维查 DB 需要锚点,以下信息删除后你将看不到,**建议先记下来或截图**:
59
+ > - 名字:`张医生`
60
+ > - external_id:`dh-a7f2k9m1`
61
+ > - 训练时间:`2026-05-14`
62
+ > - preview:`https://...preview.jpg`
63
+ > 真要删吗?"
64
+ 4. 用户确认 → 删该行
65
+ 5. 反馈"已从你的列表删除'张医生'。如需找回请向运维提供上述 4 个锚点(名字 / external_id / 训练时间 / preview URL)。"
66
+
67
+ 如果用户给的名字找不到 → 同上反问。
68
+
69
+ ## 存量分身(用户在前 SPEC 落地之前训过 `dr-wang` 等)
70
+
71
+ 用户说"用 dr-wang 做视频"但 `~/digital-human/avatars.md` "名字"列没有 `dr-wang`:
72
+
73
+ 1. LLM 判断 `dr-wang` 是纯 ASCII + 含 `-` + 不在名字列 → 怀疑是存量 slug
74
+ 2. LLM 试着 `gen avatar profile dr-wang`(后端单查仍支持任意 external_id)
75
+ 3. **找到**(返回非 null) → 询问:
76
+ > "我发现你有个分身叫 'dr-wang',要起个友好名字加到你的列表吗?(以后可以用'张医生'这种说法引用,而不是记英文 slug)"
77
+ - 用户给名字 → 追加一行到 `avatars.md`:`| <用户给的名字> | dr-wang | <profile.created_at 或当天> | <preview_image_url> |`
78
+ - 用户说"不用" → 直接走生成流程,但下次还会反问
79
+ 4. **找不到** → 反问"没训过 dr-wang,要训吗?"
80
+
81
+ 这是**自动迁移路径**,不强制,用户继续用 dr-wang 也行。
82
+
83
+ ## "删了视频 20260515-1430"
84
+
85
+ 视频删除跟分身删除不同 —— 视频 workspace 是用户私有目录,直接删干净,**没有**后端 DB 行残留:
86
+
87
+ 1. 确认 video-id 存在(`ls ~/digital-human/videos/<id>`)
88
+ 2. 二次确认:
89
+ > "确认删除视频 `<id>`($N_SEGS 段, 当前 final-vX.mp4)?
90
+ > 这会删除整个目录(segments / final-v* / meta.md / history.md),**不可恢复**。"
91
+ 3. 用户确认 → `rm -rf ~/digital-human/videos/<id>`
92
+ 4. 反馈"已删除视频 `<id>`。"
93
+
94
+ 跟分身删除的对比:分身有后端 DB 行,删 markdown 行 = orphan;视频纯客户端,删目录 = 真删干净。
95
+
96
+ ## "改某段视频"
97
+
98
+ 走 [edit.md](edit.md)。
99
+
100
+ ## 磁盘占用查看
101
+
102
+ 用户问"我用了多少磁盘"/"workspace 多大" → 让 ta 自己跑:
103
+
104
+ ```bash
105
+ du -sh ~/digital-human/
106
+ du -sh ~/digital-human/training/*/
107
+ du -sh ~/digital-human/videos/*/
108
+ ```
109
+
110
+ 每个 training/<id>/ ~ 50-200 MB(主要是 source-video);每个 videos/<id>/ ~ 8-20 MB。
111
+
112
+ v1 不做自动清理。用户嫌占空间:
113
+ - 删旧视频:`rm -rf ~/digital-human/videos/<old-id>`
114
+ - 删某分身训练素材:`rm -rf ~/digital-human/training/<external_id>/`(但保留 `avatars.md` 那一行,以后还能用 ID 生成视频,只是不能重训了)
115
+
116
+ ## "我有哪些分身做过 X"(进阶筛选)
117
+
118
+ v1 不支持。用户问就告诉:"workspace 不索引'分身 → 视频'反向关系。但可以 `grep` workspace:
119
+ ```bash
120
+ grep -l 'external_id: dh-a7f2k9m1' ~/digital-human/videos/*/meta.md
121
+ ```
122
+ 找出该分身做过的所有视频。如需要常态化筛选请告诉我们。"
123
+
124
+ ## LLM 读 avatars.md 时的健全性校验
125
+
126
+ 每次 Read 后必须做一遍。**规则详见 [SKILL.md "工作空间" 段](../SKILL.md#工作空间)**,单源真值,本段不复述以免 drift。
@@ -0,0 +1,291 @@
1
+ # Training Workflow
2
+
3
+ 按顺序执行,每步必走。把用户上传的真人视频训成数字分身(voice + avatar),写入 `~/digital-human/avatars.md`。
4
+
5
+ ## 0. 工作空间初始化(首次训练)
6
+
7
+ ```bash
8
+ mkdir -p ~/digital-human/{training,videos}
9
+ ```
10
+
11
+ 如 `~/digital-human/avatars.md` 不存在,**创建**含 SKILL.md 模板的空表(见 SKILL.md "工作空间"段)。
12
+
13
+ `training/` 用于归档每个分身的训练源素材(原视频 / 提取音频 / 训练元数据);`videos/` 用于每条生成视频的 segments + final。这两个目录的存在让用户后续可以**改某段视频不用全重生**(见 [edit.md](edit.md))、**重训不用重传视频**(下文 §4.5)。
14
+
15
+ ## 1. 询问名字
16
+
17
+ LLM 主动问:
18
+
19
+ > "训完叫什么名字?(中英文皆可,1-40 字,以后用这个名字找它)"
20
+
21
+ 用户答 → 直接作为"名字"列的值,**不做任何 normalize / 转换 / 拒绝**。
22
+
23
+ 约束(由 LLM 兜底):
24
+ - 长度 1-40 字符(Unicode 可见字符,禁控制字符 `\x00-\x1f`)
25
+ - 不能跟 `~/digital-human/avatars.md` 现有"名字"列重复
26
+
27
+ **重名处理**:
28
+ - 反问"你已经有'张医生'了,起个别的(例如'张医生2'/'张主任')"
29
+ - **不**提供"覆盖"选项(v1 不支持,见 [workspace SPEC §6 #6](https://github.com/Optima-Chat/optima-agent/blob/main/docs/superpowers/specs/2026-05-14-digital-human-workspace-naming.md))
30
+
31
+ ## 2. 内部生成 external_id
32
+
33
+ **由 LLM 在本地生成**,不依赖后端兜底:
34
+
35
+ ```bash
36
+ EXTERNAL_ID="dh-$(openssl rand -hex 5 | base32 | tr 'A-Z' 'a-z' | tr -d '=' | head -c 8)"
37
+ # 例: "dh-a7f2k9m1"
38
+ # 注: 全大写,跨段(§4.5 / §5 / §6 / §6.5)统一用 $EXTERNAL_ID 引用,
39
+ # bash 大小写敏感,混用会取到空值导致路径错误
40
+ ```
41
+
42
+ **碰撞处理**(8 字节 base32 ≈ 64 bit 熵,碰撞概率低但非零,后端 `external_id` 有 UNIQUE 约束 `migrate.ts:44`):
43
+ - onboard insert 失败且 error_code 表明唯一冲突 → 重新生成 + 重试
44
+ - **最多重试 3 次**,3 次失败 → 报错给用户
45
+
46
+ ## 3. 视频规格检查
47
+
48
+ ```bash
49
+ ffprobe -v quiet -print_format json -show_format -show_streams <video-path>
50
+ ```
51
+
52
+ 记录:
53
+ - `duration` 总时长
54
+ - `width` × `height` 分辨率
55
+ - `audio` 流是否存在 + sample_rate / channels
56
+
57
+ 判定:
58
+
59
+ | 项 | 阈值 | 不达标动作 |
60
+ |---|---|---|
61
+ | 视频时长 | < 30s | 中止;要求重传 ≥ 30s 视频 |
62
+ | 视频时长 | 30-119s | 警告"voice OK,但 avatar 质量可能受影响,推荐 ≥ 2min" |
63
+ | 分辨率 | < 720p | 警告但继续 |
64
+ | 音频流 | 缺失 | 中止;视频里没声音怎么训 voice |
65
+
66
+ **不前置检测多人 vs 单人** — ffprobe 拿不到 speaker count。多人混杂等 voice clone fail 时按 `error_message` 反馈。
67
+
68
+ ## 4. 背景判断 + 决策
69
+
70
+ spec §13.4 关键约束:训练系统会把整段视频画面(包含背景)整体作为 avatar look 内容,video gen 时**无法用 `background` 参数替换**。
71
+
72
+ **直接问用户三选一**(不要抽帧 + Read 图判断 — 视觉判断不稳定且贵):
73
+
74
+ > "训练前确认:这段视频的背景是?
75
+ > A. **白墙 / 棚拍 / 绿幕 / 单色背景**(干净)
76
+ > B. **标准固定场景**(诊室 / 办公桌 / 医院前台 — 这个背景将永远 baked 进分身,后续无法换)
77
+ > C. **户外 / 商场 / 多人 / 杂乱**(脏)"
78
+
79
+ | 用户选 | 决策 |
80
+ |---|---|
81
+ | A | `--clean-bg` |
82
+ | B | 二次确认:"分身的所有未来视频背景都会是这个场景,可接受吗?" → 接受用 `--accept-baked-bg`;不接受 → 让用户重拍 A 类背景 |
83
+ | C | 强烈劝退,推荐重拍干净背景。如用户坚持产出 → `--accept-baked-bg` + 明确告诉产出视频背景永远是录制时场景 |
84
+
85
+ **禁止**:
86
+ - ❌ silent 默认 `--clean-bg`(错判脏背景为干净 → 产出脏数据)
87
+ - ❌ 自己抽帧用图像识别判断(视觉判断不稳定 + 浪费 token + 用户答更准)
88
+
89
+ ## 4.5 归档源视频(必须在 §5 拆音频之前)
90
+
91
+ 把用户上传的视频复制到 workspace,后续步骤全部从复制好的副本走:
92
+
93
+ ```bash
94
+ mkdir -p ~/digital-human/training/$EXTERNAL_ID
95
+ cp "<用户上传的视频路径>" ~/digital-human/training/$EXTERNAL_ID/source-video.mp4
96
+ ```
97
+
98
+ **为什么必须先复制**:
99
+ - Claude Code session 里 attach 的视频通常在临时目录(`/tmp/...` 或类似),**session 结束就消失**
100
+ - 后续步骤(拆音频、ffprobe、onboard 上传)都依赖这个文件存在,临时附件中途消失会全部失败
101
+ - 先 cp 到 workspace,等于把素材"焊死"在本地,后续就算 session 关了重启也能续做
102
+
103
+ **为什么用 `cp` 而不是 `ln`(hardlink)或 `mv`(移动)**:
104
+ - `mv` 删原文件 → 用户原始路径丢失,违和
105
+ - `ln` 跨 filesystem 会失败,而且加 LLM 出错面(决策详见 SPEC `2026-05-15-digital-human-segment-persistence.md` 决策 #13)
106
+ - `cp` 简单可靠,磁盘代价可接受(2-5 min 1080p 视频 ~ 50-200 MB,个人开发机能扛)
107
+
108
+ **用户原始路径不动**,只是我们这边存一份副本。
109
+
110
+ ## 5. 提音频(从复制好的 source-video.mp4)
111
+
112
+ `gen avatar onboard` 接受 `<audio-file> <video-file>` 两个文件参数。从 §4.5 复制好的 `source-video.mp4` 提音频(**不再从用户原始路径提**,因为原始路径可能中途消失):
113
+
114
+ ```bash
115
+ AUDIO_PATH=~/digital-human/training/$EXTERNAL_ID/source-audio.wav
116
+ ffmpeg -i ~/digital-human/training/$EXTERNAL_ID/source-video.mp4 \
117
+ -vn -acodec pcm_s16le -ar 44100 -ac 1 -t 90 "$AUDIO_PATH"
118
+ ```
119
+
120
+ 取前 90 秒单声道 WAV,够 voice clone。路径固定 = `training/<external_id>/source-audio.wav`,不用 `mktemp`(单分身一个 audio,无并发覆盖问题)。
121
+
122
+ ## 6. 触发训练(onboard)
123
+
124
+ ```bash
125
+ gen avatar onboard "$AUDIO_PATH" ~/digital-human/training/$EXTERNAL_ID/source-video.mp4 \
126
+ --external-id "$EXTERNAL_ID" \
127
+ --clean-bg # 或 --accept-baked-bg(二选一,不能省)
128
+ ```
129
+
130
+ > **不要传 `--display-name`** — 后端 CLI [`doctor.ts:36`](file:///C:/Users/zy/optima-gen/packages/cli/src/commands/doctor.ts) 不支持这个 flag,commander 会拒绝未知选项报错。display_name 唯一存在于 `~/digital-human/avatars.md`,backend 完全不感知。
131
+
132
+ 返回 JSON:
133
+ ```json
134
+ { "external_id": "dh-a7f2k9m1", "voice_task_id": "<uuid>", "avatar_task_id": "<uuid>", "status": "pending" }
135
+ ```
136
+
137
+ 如返回唯一冲突错误 → 回到 §2 重新生成 external_id,重试(最多 3 次)。
138
+
139
+ ## 6.5 写 train-meta.md(归档训练元数据)
140
+
141
+ onboard 提交成功后,写训练元数据到 workspace:
142
+
143
+ ```bash
144
+ cat > ~/digital-human/training/$EXTERNAL_ID/train-meta.md <<EOF
145
+ # Training: $DISPLAY_NAME (external_id: $EXTERNAL_ID)
146
+
147
+ | 字段 | 值 |
148
+ |---|---|
149
+ | display_name | $DISPLAY_NAME |
150
+ | external_id | $EXTERNAL_ID |
151
+ | 训练时间 | $(date +%Y-%m-%d) |
152
+ | voice_task_id | $VOICE_TASK_ID |
153
+ | avatar_task_id | $AVATAR_TASK_ID |
154
+ | 背景决策 | $BG_DECISION (--clean-bg / --accept-baked-bg) |
155
+ | 源视频时长 | (从 §3 ffprobe 输出取) |
156
+ | 源视频分辨率 | (同上) |
157
+ | preview_image_url | (训完 §8 拉到后回填这一行) |
158
+ EOF
159
+ ```
160
+
161
+ **为什么单独写文件**:
162
+ - avatars.md 是分身索引(用户看 + LLM 用),只放最关键字段(名字 / external_id / 训练时间 / preview)
163
+ - train-meta.md 是分身**完整训练记录**,含 task_ids / 背景决策 / 源视频 specs 等用户不关心但**重训时关键**的信息
164
+
165
+ ## 7. Poll 直到完成
166
+
167
+ ```bash
168
+ # 30 min 上限(avatar train 慢边界);5s 间隔
169
+ TIMEOUT=1800
170
+ INTERVAL=5
171
+ ELAPSED=0
172
+ while [ $ELAPSED -lt $TIMEOUT ]; do
173
+ V_STATUS=$(gen task get $VOICE_TASK | jq -r '.status')
174
+ A_STATUS=$(gen task get $AVATAR_TASK | jq -r '.status')
175
+ echo "[t+${ELAPSED}s] voice=$V_STATUS avatar=$A_STATUS"
176
+
177
+ V_DONE=false; A_DONE=false
178
+ [[ "$V_STATUS" =~ ^(completed|failed)$ ]] && V_DONE=true
179
+ [[ "$A_STATUS" =~ ^(completed|failed)$ ]] && A_DONE=true
180
+ $V_DONE && $A_DONE && break
181
+
182
+ sleep $INTERVAL
183
+ ELAPSED=$((ELAPSED + INTERVAL))
184
+ done
185
+ ```
186
+
187
+ > **Fallback**:如果 `gen task get` 不可用,直接打后端 endpoint:
188
+ > ```bash
189
+ > curl -s -H "Authorization: Bearer $OPTIMA_TOKEN" "$GENERATION_API/api/task/$TASK_ID" | jq
190
+ > ```
191
+
192
+ 期望耗时:
193
+ - voice clone:30s - 2 min
194
+ - avatar train:5 - 30 min
195
+ - spec §13.2:trained look 在训练完成后还有短期 race(几分钟到 1 小时);**onboard 阶段不阻塞**
196
+
197
+ 任一 fail → 看 `error_message`:
198
+ - voice clone fail → 大概率音频质量(噪声 / 多人混杂)→ 提醒用户重传清晰音频
199
+ - avatar train fail → 视频问题(时长 / 分辨率 / 内容)→ 给具体 error 让用户判断
200
+
201
+ ## 8. 拉 profile 拿 preview
202
+
203
+ 两个 task 都 completed 后:
204
+
205
+ ```bash
206
+ gen avatar profile "$EXTERNAL_ID"
207
+ ```
208
+
209
+ 返回(关键字段):
210
+ ```json
211
+ {
212
+ "external_id": "dh-a7f2k9m1",
213
+ "heygen_voice_id": "f7a8ea9f...",
214
+ "heygen_avatar_look_ids": ["04182555..."],
215
+ "voice_status": "complete",
216
+ "avatar_status": "completed",
217
+ "preview_image_url": "https://...preview.jpg"
218
+ }
219
+ ```
220
+
221
+ > DB 字段名 `heygen_*` 是历史命名,代码层不动。**LLM 给用户报告时不冒泡 UUID**(只展示名字 + preview)。
222
+
223
+ **回填 `train-meta.md`**:把 §6.5 写的 train-meta.md 里 `preview_image_url` 那行补上真值(profile 拿到的 URL)。
224
+
225
+ (可选)下载 preview 缓存到本地:
226
+ ```bash
227
+ curl -s -o ~/digital-human/training/$EXTERNAL_ID/preview.jpg "$PREVIEW_URL"
228
+ ```
229
+ 加快"列我的分身"渲染速度,避免每次都打外网。
230
+
231
+ ## 9. 追加一行到 `avatars.md`
232
+
233
+ ```bash
234
+ TODAY=$(date +%Y-%m-%d) # 真实日期,不凭印象
235
+ ```
236
+
237
+ 往 `~/digital-human/avatars.md` 末尾追加一行(用 Edit/Read 工具操作 markdown 表):
238
+
239
+ ```
240
+ | <用户给的名字> | <external_id> | <TODAY> | <preview_image_url> |
241
+ ```
242
+
243
+ ## 10. 给用户报告
244
+
245
+ > "✅ 张医生 训练完成 [preview 图]
246
+ > 训练素材已存档到 `~/digital-human/training/张医生/`,以后想重训不用再上传视频。
247
+ > 以后说'用张医生做视频说...'即可。"
248
+
249
+ **不输出** voice_id / avatar_id / external_id(UUID 全部不冒泡)。
250
+
251
+ ---
252
+
253
+ ## Edge cases
254
+
255
+ ### 1. 用户没给视频但说"训练 X"
256
+
257
+ 反问:"X 的视频附件没看到,把视频发我(2-5 min,正面拍摄,干净背景)。"
258
+ **不要** 凭印象搜索 X 的公开视频去训(法律 + 同意书风险)。
259
+
260
+ ### 2. 用户上传多个视频
261
+
262
+ 主动列出最近上传的视频文件(文件名 + 时长),**让用户选哪一个用于训练**。不要假设最后一个 = 训练用。
263
+
264
+ ### 3. 用户给了视频但意图模糊
265
+
266
+ 视频可能是"复刻这条视频" / "把这条视频翻译" / "训分身"。如果不明确是后者,**不要触发本 skill**。问一句:
267
+ > "你想要 (a) 用这条视频训练 X 的数字分身(以后用分身做新视频),还是 (b) 复刻这条视频做产品宣传 / (c) 把视频翻译成其他语言?"
268
+
269
+ 分别走 digital-human / video-gen / video-gen 视频翻译。
270
+
271
+ ### 4. 用户中途改主意
272
+
273
+ - 想换视频 → 旧 task 让它跑完(训练资源已扣,不阻塞);用户拿新参数重新走 §1-§7
274
+ - 想取消 → `gen task cancel <task_id>` 各自 cancel(尽力而为,上游可能已 in-flight 不可中止)
275
+
276
+ ---
277
+
278
+ ## Common Mistakes
279
+
280
+ | ❌ Mistake | ✅ 正确做法 | 为什么 |
281
+ |---|---|---|
282
+ | silent 默认 `--clean-bg`(§4) | 用户三选一 → 显式 `--clean-bg` 或 `--accept-baked-bg` | 错判脏背景为干净 → baked 进 avatar look,后续 video 永远换不掉背景(spec §13.4) |
283
+ | 抽帧用图像识别判断背景(§4) | 直接问用户三选一 | 视觉判断不稳定 + 浪费 token + 用户答更准 |
284
+ | 让用户起 ASCII slug 当名字(§1) | 用户起的就是友好名字,external_id 由 LLM 内部生成 | 让用户记英文 slug 反人性;workspace 文件已经隔离了 ASCII 约束 |
285
+ | 给 `gen avatar onboard` 传 `--display-name`(§6) | 不要传,commander 会拒未知选项报错 | 后端 CLI 没这个 flag([doctor.ts:36](file:///C:/Users/zy/optima-gen/packages/cli/src/commands/doctor.ts));display_name 只在 workspace markdown |
286
+ | 凭印象搜公开视频去训(§Edge 1) | 反问用户上传视频 | 法律 + 同意书风险,无授权训练他人形象 |
287
+ | 假设最后一个上传的视频 = 训练用(§Edge 2) | 列出所有让用户选 | 错训不该训的人 |
288
+ | 视频含糊意图直接触发 skill(§Edge 3) | 三岔反问 (a) 训分身 / (b) 复刻 / (c) 翻译 | 错走训练流程浪费训练配额(训练成本 > 复刻) |
289
+ | 没视频但有意图就调 onboard | 反问"把视频发我",并明确质量要求 | onboard 没视频会立即失败,白调一次 |
290
+ | 训完报告里写 voice_id / avatar_id UUID | 只展示名字 + preview | UUID 不冒泡是核心 UX 原则 |
291
+ | 跳过 §9 写 avatars.md | 训完必须写一行 | 不写下次会话用户说"用张医生"找不到,得记 dh-* slug |