@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
@@ -2,11 +2,14 @@
2
2
 
3
3
  按顺序执行,每步必走。把用户上传的真人视频训成数字分身(voice + avatar),写入 `~/digital-human/avatars.md`。
4
4
 
5
+ > ⚠️ **沙箱 bash 须知**(同 [generate.md §0](generate.md) / [SKILL.md Global Rule #7](../SKILL.md)):agent-runtime 的 bash **软拒绝** `$( )` 命令替换、`for`/`while` 循环、命令内换行(含 `\` 续行)、`;`/`&&`/`||` 串多条、`> file`/`>>`/heredoc(`<<EOF`)重定向、`| jq`/`| sed`/`| grep` 管道、`nohup &`。所以本文档:**取值靠 LLM 自己读单条命令的 stdout JSON**(不 `$()`/`jq`)、**写文件用 Write/Edit tool**(不 heredoc / `echo >`)、**轮询用单条命令逐次跑**(不 `while`)、**所有 id / 路径内联进命令**(不存 `$VAR`)。
6
+
5
7
  ## 0. 工作空间初始化(首次训练)
6
8
 
7
9
  ```bash
8
- mkdir -p ~/digital-human/{training,videos}
10
+ mkdir -p ~/digital-human/training ~/digital-human/videos
9
11
  ```
12
+ (`mkdir -p` 接受多个路径参数,一条命令建两个目录,不用 `{a,b}` brace 展开。)
10
13
 
11
14
  如 `~/digital-human/avatars.md` 不存在,**创建**含 SKILL.md 模板的空表(见 SKILL.md "工作空间"段)。
12
15
 
@@ -26,20 +29,13 @@ LLM 主动问:
26
29
 
27
30
  **重名处理**:
28
31
  - 反问"你已经有'张医生'了,起个别的(例如'张医生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))
32
+ - **不**提供"覆盖"选项(v1 不支持,见 [workspace SPEC §6 #6](../../../../docs/superpowers/specs/2026-05-14-digital-human-workspace-naming.md))
30
33
 
31
34
  ## 2. 内部生成 external_id
32
35
 
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
- ```
36
+ **由 LLM 自己生成**(你直接想一个,**别用 `$(openssl ...)`/管道**——沙箱拦):格式 `dh-` + 8 位随机小写字母数字(例 `dh-a7f2k9m1`)。生成后**记住这个值**,跨段(§4.5 / §5 / §6 / §6.5 / §8 / §9)在每条命令里**直接写全**,不存 `$EXTERNAL_ID` 变量(沙箱里没法安全赋值,且大小写混用会取空值导致路径错误)。
41
37
 
42
- **碰撞处理**(8 字节 base32 ≈ 64 bit 熵,碰撞概率低但非零,后端 `external_id` 有 UNIQUE 约束 `migrate.ts:44`):
38
+ **碰撞处理**(随机 8 位,碰撞概率低但非零,后端 `external_id` 有 UNIQUE 约束 `migrate.ts:44`):
43
39
  - onboard insert 失败且 error_code 表明唯一冲突 → 重新生成 + 重试
44
40
  - **最多重试 3 次**,3 次失败 → 报错给用户
45
41
 
@@ -88,11 +84,13 @@ spec §13.4 关键约束:训练系统会把整段视频画面(包含背景)整
88
84
 
89
85
  ## 4.5 归档源视频(必须在 §5 拆音频之前)
90
86
 
91
- 把用户上传的视频复制到 workspace,后续步骤全部从复制好的副本走:
87
+ 把用户上传的视频复制到 workspace,后续步骤全部从复制好的副本走(把 `<external_id>` 换成 §2 你生成的那个 id,**写全**,别用 `$EXTERNAL_ID`)。两条单命令:
92
88
 
93
89
  ```bash
94
- mkdir -p ~/digital-human/training/$EXTERNAL_ID
95
- cp "<用户上传的视频路径>" ~/digital-human/training/$EXTERNAL_ID/source-video.mp4
90
+ mkdir -p ~/digital-human/training/dh-a7f2k9m1
91
+ ```
92
+ ```bash
93
+ cp "<用户上传的视频路径>" ~/digital-human/training/dh-a7f2k9m1/source-video.mp4
96
94
  ```
97
95
 
98
96
  **为什么必须先复制**:
@@ -109,25 +107,23 @@ cp "<用户上传的视频路径>" ~/digital-human/training/$EXTERNAL_ID/source-
109
107
 
110
108
  ## 5. 提音频(从复制好的 source-video.mp4)
111
109
 
112
- `gen avatar onboard` 接受 `<audio-file> <video-file>` 两个文件参数。从 §4.5 复制好的 `source-video.mp4` 提音频(**不再从用户原始路径提**,因为原始路径可能中途消失):
110
+ `gen avatar onboard` 接受 `<audio-file> <video-file>` 两个文件参数。从 §4.5 复制好的 `source-video.mp4` 提音频(**不再从用户原始路径提**,因为原始路径可能中途消失)。**一条命令、路径写全、不换行**(`\` 续行沙箱拦):
113
111
 
114
112
  ```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"
113
+ ffmpeg -i ~/digital-human/training/dh-a7f2k9m1/source-video.mp4 -vn -acodec pcm_s16le -ar 44100 -ac 1 -t 90 ~/digital-human/training/dh-a7f2k9m1/source-audio.wav
118
114
  ```
119
115
 
120
116
  取前 90 秒单声道 WAV,够 voice clone。路径固定 = `training/<external_id>/source-audio.wav`,不用 `mktemp`(单分身一个 audio,无并发覆盖问题)。
121
117
 
122
118
  ## 6. 触发训练(onboard)
123
119
 
120
+ **一条命令、路径 + id 写全、不换行**(`\` 续行沙箱拦;`--clean-bg` / `--accept-baked-bg` 按 §4 决策二选一,不能省):
121
+
124
122
  ```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(二选一,不能省)
123
+ gen avatar onboard ~/digital-human/training/dh-a7f2k9m1/source-audio.wav ~/digital-human/training/dh-a7f2k9m1/source-video.mp4 --external-id dh-a7f2k9m1 --clean-bg
128
124
  ```
129
125
 
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 完全不感知。
126
+ > **不要传 `--display-name`** — 后端 CLI [`doctor.ts:36`](packages/cli/src/commands/doctor.ts) 不支持这个 flag,commander 会拒绝未知选项报错。display_name 唯一存在于 `~/digital-human/avatars.md`,backend 完全不感知。
131
127
 
132
128
  返回 JSON:
133
129
  ```json
@@ -138,55 +134,48 @@ gen avatar onboard "$AUDIO_PATH" ~/digital-human/training/$EXTERNAL_ID/source-vi
138
134
 
139
135
  ## 6.5 写 train-meta.md(归档训练元数据)
140
136
 
141
- onboard 提交成功后,写训练元数据到 workspace:
137
+ onboard 提交成功后,从它 stdout 的 JSON 里读 `voice_task_id` / `avatar_task_id`(自己读,别 `$()`/`jq`)。用 **Write tool** 写训练元数据到 `~/digital-human/training/<external_id>/train-meta.md`(**别用 `cat <<EOF` heredoc**——沙箱拦)。所有值(名字 / external_id / 两个 task_id / 背景决策 / 今天日期 / §3 ffprobe 的时长分辨率)你手头都有,**内联**进模板:
142
138
 
143
- ```bash
144
- cat > ~/digital-human/training/$EXTERNAL_ID/train-meta.md <<EOF
145
- # Training: $DISPLAY_NAME (external_id: $EXTERNAL_ID)
139
+ ```markdown
140
+ # Training: 张医生 (external_id: dh-a7f2k9m1)
146
141
 
147
142
  | 字段 | 值 |
148
143
  |---|---|
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) |
144
+ | display_name | 张医生 |
145
+ | external_id | dh-a7f2k9m1 |
146
+ | 训练时间 | 2026-06-01 |
147
+ | voice_task_id | <onboard 输出的 voice_task_id> |
148
+ | avatar_task_id | <onboard 输出的 avatar_task_id> |
149
+ | 背景决策 | --clean-bg(或 --accept-baked-bg) |
155
150
  | 源视频时长 | (从 §3 ffprobe 输出取) |
156
151
  | 源视频分辨率 | (同上) |
157
152
  | preview_image_url | (训完 §8 拉到后回填这一行) |
158
- EOF
159
153
  ```
160
154
 
155
+ (日期用你 LLM 知道的当天真实日期,别 `$(date)`。)
156
+
161
157
  **为什么单独写文件**:
162
158
  - avatars.md 是分身索引(用户看 + LLM 用),只放最关键字段(名字 / external_id / 训练时间 / preview)
163
159
  - train-meta.md 是分身**完整训练记录**,含 task_ids / 背景决策 / 源视频 specs 等用户不关心但**重训时关键**的信息
164
160
 
165
161
  ## 7. Poll 直到完成
166
162
 
163
+ **沙箱里没有 `while` 循环**——靠 LLM **逐次发单条命令**轮询(把 task_id 内联进命令,从输出 JSON 自己读 `status`,别 `$()`/`jq`)。每轮两条:
164
+
165
+ ```bash
166
+ gen task get <voice_task_id>
167
+ ```
167
168
  ```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
169
+ gen task get <avatar_task_id>
185
170
  ```
186
171
 
187
- > **Fallback**:如果 `gen task get` 不可用,直接打后端 endpoint:
172
+ - 读两条输出里的 `status`。两个都 `completed` → 进 §8;任一 `failed` 看 `error_message` 处理(见下)。
173
+ - 都还在 `pending`/`processing` → 等一会再来一轮。可选单条 `sleep 15`(**别** `sleep && gen ...` 串、别写 `while`),然后重发上面两条;或直接隔一轮再查。
174
+ - **上限 ~30 min**(avatar train 慢边界):LLM 自己记开始时间,超时仍未完成 → 告诉用户"训练超时,稍后用'X 训好了吗'再查"(state 不丢,§profile 可续查),不要无限轮询。
175
+
176
+ > **Fallback**:`gen task get` 不可用时,单条 curl 查后端(token / 域名内联,**别管道接 `| jq`**,直接看返回 JSON):
188
177
  > ```bash
189
- > curl -s -H "Authorization: Bearer $OPTIMA_TOKEN" "$GENERATION_API/api/task/$TASK_ID" | jq
178
+ > curl -s -H "Authorization: Bearer <token>" "<generation-api>/api/task/<task_id>"
190
179
  > ```
191
180
 
192
181
  期望耗时:
@@ -200,10 +189,10 @@ done
200
189
 
201
190
  ## 8. 拉 profile 拿 preview
202
191
 
203
- 两个 task 都 completed 后:
192
+ 两个 task 都 completed 后(external_id 内联):
204
193
 
205
194
  ```bash
206
- gen avatar profile "$EXTERNAL_ID"
195
+ gen avatar profile dh-a7f2k9m1
207
196
  ```
208
197
 
209
198
  返回(关键字段):
@@ -222,19 +211,15 @@ gen avatar profile "$EXTERNAL_ID"
222
211
 
223
212
  **回填 `train-meta.md`**:把 §6.5 写的 train-meta.md 里 `preview_image_url` 那行补上真值(profile 拿到的 URL)。
224
213
 
225
- (可选)下载 preview 缓存到本地:
214
+ (可选)下载 preview 缓存到本地(url + 路径内联,单条命令):
226
215
  ```bash
227
- curl -s -o ~/digital-human/training/$EXTERNAL_ID/preview.jpg "$PREVIEW_URL"
216
+ curl -s -o ~/digital-human/training/dh-a7f2k9m1/preview.jpg "<preview_image_url>"
228
217
  ```
229
218
  加快"列我的分身"渲染速度,避免每次都打外网。
230
219
 
231
220
  ## 9. 追加一行到 `avatars.md`
232
221
 
233
- ```bash
234
- TODAY=$(date +%Y-%m-%d) # 真实日期,不凭印象
235
- ```
236
-
237
- 往 `~/digital-human/avatars.md` 末尾追加一行(用 Edit/Read 工具操作 markdown 表):
222
+ 用 **Read + Edit tool** 往 `~/digital-human/avatars.md` 末尾追加一行(别用 `echo >>`)。日期用你 LLM 知道的**当天真实日期**(别 `$(date)`,别凭印象):
238
223
 
239
224
  ```
240
225
  | <用户给的名字> | <external_id> | <TODAY> | <preview_image_url> |
@@ -282,7 +267,7 @@ TODAY=$(date +%Y-%m-%d) # 真实日期,不凭印象
282
267
  | silent 默认 `--clean-bg`(§4) | 用户三选一 → 显式 `--clean-bg` 或 `--accept-baked-bg` | 错判脏背景为干净 → baked 进 avatar look,后续 video 永远换不掉背景(spec §13.4) |
283
268
  | 抽帧用图像识别判断背景(§4) | 直接问用户三选一 | 视觉判断不稳定 + 浪费 token + 用户答更准 |
284
269
  | 让用户起 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 |
270
+ | 给 `gen avatar onboard` 传 `--display-name`(§6) | 不要传,commander 会拒未知选项报错 | 后端 CLI 没这个 flag([doctor.ts:36](packages/cli/src/commands/doctor.ts));display_name 只在 workspace markdown |
286
271
  | 凭印象搜公开视频去训(§Edge 1) | 反问用户上传视频 | 法律 + 同意书风险,无授权训练他人形象 |
287
272
  | 假设最后一个上传的视频 = 训练用(§Edge 2) | 列出所有让用户选 | 错训不该训的人 |
288
273
  | 视频含糊意图直接触发 skill(§Edge 3) | 三岔反问 (a) 训分身 / (b) 复刻 / (c) 翻译 | 错走训练流程浪费训练配额(训练成本 > 复刻) |
@@ -284,6 +284,18 @@ gen task retry <task_id> # 重试失败的任务
284
284
 
285
285
  所有 `gen` 命令在失败时会返回结构化错误,顶层是 `success: false` + `error` 对象,**进程退出码为 1**。`error.message` 是后端已经处理过的中文人话(上游英文报错已脱敏/翻译),大多数情况下**可以原样透传给用户**,但要按下表选择对应处理方式(重试 / 透传 / 询问用户)。
286
286
 
287
+ ### 执行方式(前置条件,先看这条)
288
+
289
+ `gen` 命令**必须前台同步执行**,等命令真正退出后再判断结果(读 stdout 的 JSON envelope + 进程退出码)。同步等待 30s ~ 5min 是**正常的**(图片生成 + 上游 polling),不是卡住。
290
+
291
+ **禁止以下行为** —— 它们会让你**永远看不到 `success` / `failure` envelope**,从而陷入 `cat` / `sleep` / `Monitor` 死循环:
292
+
293
+ - ❌ 后台跑 `gen`(`&` / `nohup` / Bash `run_in_background:true` / 用 Task / Monitor 工具包一层)
294
+ - ❌ 给 Bash 设短 timeout(< 600000ms / < 10min)强行打断 `gen` —— 命令被 kill 后**没机会写出 envelope**,你看到的只是空输出
295
+ - ❌ 一边跑 `gen` 一边 `cat` 它的 `.output` 文件 / `sleep N` 等待 / `Monitor until [ -s file ]` —— `gen-cli` 走 buffered JSON 输出,**中途文件就是空**,空文件**既不代表失败也不代表"还在跑"**,只代表你不该 polling
296
+
297
+ 正确做法:**前台 Bash 跑 `gen image ...`,Bash `timeout` ≥ 600000ms**,命令退出后 stdout 一定有 `success:true` 或 `success:false` JSON envelope,按下面的对照表处理即可。
298
+
287
299
  ### 失败响应示例
288
300
 
289
301
  ```json
@@ -343,6 +355,7 @@ gen task retry <task_id> # 重试失败的任务
343
355
  ❌ 把 `"维护中"` 三个字直接砸给用户
344
356
  ❌ 任何失败都自动 retry 三五次造成账单浪费
345
357
  ❌ 看到 `success: false` 却跑去 `cat` 输出文件、`sleep` 等待、再调一次 `gen`——这就是把失败当"任务还在跑"。真实事故:上游返回 CONTENT_POLICY_VIOLATION 后 Agent 没识别失败,反复 polling+重试,用户被晾几分钟才看到错误(且每次都已计费)
358
+ ❌ 把 `gen` 跑后台(`run_in_background:true` / 包 Task / 短 Bash `timeout`)+ 用 `cat` / `sleep 30` / `Monitor until [ -s file ]` 轮询 `.output` 文件——`gen` 同步会跑几十秒到几分钟,期间 output 文件就是空,你 cat 出来"什么都没有"既不代表失败也不代表"还在跑"。等 Bash `timeout` 把 `gen` kill 掉,envelope 都没机会写出来,你**永远看不到 `success:false`**,陷入死循环。真实事故 2026-05-19:ai.optima.onl 复测 Disney prompt,Bash `timeout:120000` 把 gen 提前 kill,agent 看到空 `.output` 文件后开 `cat` / `sleep 30` / `Monitor until` polling,用户被晾几分钟才感知失败
346
359
  ❌ `CONTENT_POLICY_VIOLATION` 时擅自改 prompt 偷偷重试(用户失去控制,多等一次生成时间,且 Agent 无法确知触发哪条政策,改完大概率还是被拒;正确做法是把 `error.message` 透传给用户并询问怎么办)
347
360
  ❌ `PROVIDER_INSUFFICIENT_CREDITS` 时让用户去充值(这跟用户余额无关)
348
361
  ❌ **任何 `gen` 命令失败后,在 sandbox 里 `pip install` 重 ML 推理库(rembg / onnxruntime / torch / segmentation-models 等)做 fallback** —— 纯像素操作(PIL / cv2 的 crop / paste / resize / 简单 alpha 通道处理)不在此列。真实事故:grsai 超时后 LLM 自己装 rembg → 内存不足被 kill → 反复重试 → 用户等 20 分钟才出图。失败必须**诚实告知用户**,不要在 sandbox 里"自己想办法"。
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: motion-control
3
+ description: "用一段参考视频的动作驱动一张人物图,输出该人物按相同动作运动的视频(kling motion-control)。触发场景:用户给一张人物图 + 一段动作参考视频,说'让这个人按这个动作动起来'/'用这个视频的动作做我这张图的视频'/'motion transfer'/'动作克隆'/'让 X 跳这段舞'。范围:画面里有真人/角色按参考动作做身体运动;不是讲话(讲话走 digital-human)、不是产品演示(走 video-gen)。"
4
+ version: 1.0.0
5
+ owner_repo: Optima-Chat/optima-gen
6
+ ---
7
+
8
+ # Motion Control Skill
9
+
10
+ 把「一张人物图 + 一段动作参考视频」变成「人物图里的角色按参考动作动起来」的视频。kling-2.6/motion-control 驱动,720p,$0.10/秒。
11
+
12
+ > **CLI 版本要求**:`@optima-chat/gen-cli ≥ 2.2.0`(含 `gen video motion-control` 子命令)。报 `unknown command` → 引导用户升级,**不**自己拼接 HTTP 调用。
13
+
14
+ > **用户向输出原则**:status / 错误 / 总结里**不出现上游品牌名**,统一用「动作驱动视频生成中」「视频生成完成」等中性描述。
15
+
16
+ ## 不是这个 skill 的场景(先路由出去)
17
+
18
+ | 用户意图 | 走哪里 |
19
+ |---|---|
20
+ | 真人/数字人**讲话** | `digital-human` |
21
+ | **产品**视频(PDP / 社媒投放 / 复刻爆款) | `video-gen` |
22
+ | 纯文生 / 图生**非人物**视频(风景、动物) | `gen video`(基础视频) |
23
+ | 翻译已有口播视频 | `video-translate` |
24
+ | 剪辑已有视频 | `video-edit` |
25
+
26
+ motion-control **只管**:有具体人物图 + 想让 ta 按某段动作运动。
27
+
28
+ ## 输入要求(不满足先问)
29
+
30
+ - **人物图**:jpeg/png,≥300px,长宽比 2:5 到 5:2(即不能比 5:2 更窄或更宽)。**单一清晰主角**——背景人物 / 多人会被服务端拒。
31
+ - **参考视频**:mp4/mov/mkv,3-30 秒,≤100MB,**镜头里要有完整可识别的上半身**——服务端会做人体检测,没识别到上半身会以 `No complete upper body detected` 报错。
32
+ - **缺其中之一** → 问用户补齐,不要硬上。
33
+
34
+ ## 工作流(极简)
35
+
36
+ 1. **确认两个输入都有**(本地路径或 http(s) URL 都行)
37
+ 2. **告知预估成本**:
38
+ - 本地视频:`ffprobe` 取时长 `D`
39
+ - URL:必须问用户视频多长(CLI 不能自动 probe URL)
40
+ - 成本 = `D × $0.10`,告诉用户 + 等"继续"
41
+ 3. **执行**:`gen video motion-control --image <img> --reference-video <vid> [--character-orientation image|video] [--prompt "..."]`
42
+ - duration 不传 → CLI 自动 ffprobe 本地视频;URL 必须显式 `--duration`
43
+ - `character-orientation`:默认 `video`(输出时长 = 参考视频);用户说「保持角色形象优先」→ `image`(输出截到 ≤10s)
44
+ 4. **下载 + 展示**:CLI 自动 download 到 `./gen-output/motion_<id>.mp4`,告诉用户路径
45
+ 5. **失败处理**见下表
46
+
47
+ ## 错误处理
48
+
49
+ | 服务端错误 | 含义 + 处理 |
50
+ |---|---|
51
+ | `No complete upper body detected in the video` | 参考视频里没识别到完整上半身。让用户换段更清晰的动作视频,或截出有上半身的片段 |
52
+ | `file format not support` | 极少触发(已修,走 kie File Upload API)。再现 → 报 bug,不要自己重试 |
53
+ | `duration out of range` | duration 不在 3-30s(或 image-orientation 时超 10s)。截短参考视频或换 orientation 重试 |
54
+ | `INSUFFICIENT_CREDITS` | 用户积分不足,告知充值 |
55
+ | 任务超时 / 网络故障 | 用 `gen task get <id>` 查最新状态;服务端真正完成但 CLI poll 超时是正常的,结果还在 |
56
+
57
+ ## 不要做的事
58
+
59
+ - **不要自己跑 ffmpeg** 拼/剪/转码 motion-control 的输出——这个 skill 只管「生成那条 motion-control 视频」,后期编辑走 `video-edit`
60
+ - **不要给"改第 N 段"做支持**——motion-control 是单次原子生成,没有 storyboard / 多段概念。用户要改 → 重新生成一发
61
+ - **不要传 `--mode`**——服务端锁 720p,传了 CLI 会过但没意义
62
+ - **不要给商家用作产品视频**——product 视频走 `video-gen`,motion-control 不适合「让产品旋转一圈」这种纯物体场景(参考视频必须有人)
63
+
64
+ ## 相关工具
65
+
66
+ - `gen video motion-control` — 本 skill 唯一执行命令
67
+ - `gen task get <id>` — 查任务状态(轮询超时后用)
68
+ - `ffprobe` — 取本地视频时长,CLI 内部已用,skill 一般不直接调
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: video-compose
3
+ description: "把【多个素材片段 + 一段口播文案】自动合成一条可发布短视频——自动配音、按文案语义选片拼接、烧字幕、加 BGM。用户只需拍素材/用 AI 生成片段 + 写文案,剪辑配音字幕全交给 AI。
4
+
5
+ 必备前提:用户有 ≥1 个视频片段文件 + 一段口播文案(或愿意现写)。
6
+
7
+ 触发:用户给出多个视频片段 + 文案,并说'拼个视频'/'把这堆素材剪一下'/'给这段文案配视频'/'做条口播带画面的视频'/'素材+文案做成片'。
8
+
9
+ 不触发:只给 1 个口播 talking-head 视频要剪(用 video-edit);只给产品图要生成画面(用 video-gen);用户要【保留片段原声】拼接(用 video-edit,本 skill 会丢弃原声)。"
10
+ version: 1.0.0
11
+ owner_repo: Optima-Chat/optima-gen
12
+ ---
13
+
14
+ # video-compose — 素材 + 文案 → 成片
15
+
16
+ 用户给一堆片段 + 一段口播稿,你交付配好音、配好画面、带字幕和 BGM 的竖版成片。
17
+
18
+ > ⚠ **语义前提:片段被当作纯画面 b-roll,原声一律丢弃**,成片音频 = AI 配音 + BGM。若用户给的是带人声的口播片段、想**保留原声**拼接,那是 video-edit 的活——开工前跟用户确认一句"片段原声会去掉、全程用 AI 配音",避免出片后才发现声音没了。
19
+
20
+ **关键:选片这一步由你(Claude)亲自看帧判断**——脚本把每个 clip 抽 3 帧,你用 Read 看图,按文案语义写 `proposal.json`。不需要任何外部 vision API。
21
+
22
+ ## 工具
23
+
24
+ `python3 $CLAUDE_SKILL_DIR/scripts/video_compose.py <frames|build> <proj-dir>`
25
+ - 依赖(容器自带):`python3`、`ffmpeg`、`gen` CLI。
26
+ - 情感配音走 `gen tts --provider minimax`(key + 计费在后端 optima-generation,skill 不碰密钥)。
27
+
28
+ ## 工作目录
29
+
30
+ ```
31
+ <proj>/inputs/clips/*.mp4 素材(任意命名,按文件名排序得 clip id)
32
+ <proj>/inputs/script.txt 口播稿,每行一句 = 一个 segment
33
+ <proj>/work/ 中间产物(frames/ proposal.json subs.ass 等)
34
+ <proj>/final.mp4 成片
35
+ ```
36
+
37
+ ## Step 0:指令清单读回(≥ 2 个动作时必跑)
38
+
39
+ 用户一条消息里给多个要求(如"竖版 + 不要 BGM + 字幕大一点 + 压到 20 秒")时,**先拆成原子清单读回、等确认再动手**,不要边读边做。单一动作("拼个视频")跳过。
40
+ (理由同 video-edit:多指令直接执行易漏,漏了要等成片出来才发现,全流程重做。)
41
+
42
+ ## 主流程
43
+
44
+ ### 1. 建工程 + 落素材和文案
45
+ - 建 `<proj>/inputs/clips/` 和 `<proj>/inputs/script.txt`
46
+ - 把用户的片段拷进 clips/(命名随意,建议 `01.mp4 02.mp4 …` 便于引用)
47
+ - 文案写进 script.txt,**每行一句**。用户没给文案就先帮他写(看素材定主题),写完**先给用户确认文案**再继续。
48
+
49
+ ### 2. 抽帧
50
+ ```
51
+ python .../video_compose.py frames <proj>
52
+ ```
53
+ 产出 `work/frames/<clipid>_<tag>.jpg`(每片自适应抽 3~6 帧,约每 5s 一帧)+ `work/clips_manifest.json`。**manifest 里每帧带 `t`(秒)= 该子镜头在素材中的时间点**——写 proposal 时用它指定 `src_start`。
54
+
55
+ ### 3. 看帧 + 写 proposal.json(**你的核心判断**)
56
+ - 用 **Read 逐张看** `work/frames/` 里的帧,在心里给每个 clip 一句话描述(人物/动作/场景/有无烧死字幕)。
57
+ - 按 script 每句的语义,给它挑最贴合的 clip,写出 `work/proposal.json`:
58
+
59
+ ```json
60
+ {
61
+ "voice": "Chinese (Mandarin)_Warm_Girl",
62
+ "bgm_mood": "warm",
63
+ "assignments": [
64
+ { "segment_idx": 0, "text": "第一句原文", "clip": "01", "src_start": 5.2, "emotion": "sad", "speed": 0.92, "rationale": "为什么选它 + 为什么这个子镜头 + 为什么这个情绪" }
65
+ ]
66
+ }
67
+ ```
68
+
69
+ - **voice**:voice_id。**只用 `voice-samples/CATALOG.md` 里 7 个实测可用的音色**(标签 ↔ voice_id),别填没验证过的(错的 voice_id 会 `UPSTREAM_UNKNOWN: voice id not exist` 失败)。默认 `Chinese (Mandarin)_Warm_Girl`(温暖少女)。**具体音色让用户试听后定,见 §4。**
70
+ - **clip**:素材 id(manifest 里的 `id`)
71
+ - **src_start**:从该素材的**哪一秒**开始切(= 你选中那个子镜头帧的 `t`,看 manifest)。**这是避免重复镜头的关键**:同一 clip 给多句复用时,每句填**不同的 `t`**(如倒水特写 t=11.5、碰杯 t=20.7),脚本以该时刻为中心切,画面就不会重复。不填则脚本按复用顺序自动均匀错开(也不会重复,但选的子镜头不一定贴文案,所以建议显式填)。
72
+ - **emotion**:每句独立,九选一 `happy/sad/angry/fearful/disgusted/surprised/calm/fluent/whisper`,按文案情绪配
73
+ - **speed**:0.5–2.0。**这是抖音/TikTok/小红书短视频工具,默认就要快**——不写 speed 脚本按 `DEFAULT_SPEED=1.35` 配音(≈TikTok 口播节奏);想更冲可显式写 1.5;**只有明确要治愈/抒情慢节奏才写 ≤1.0**(如 0.9)。别让成片听起来拖沓。
74
+ - **bgm_mood / bgm**:见 §BGM(用户没指定就按文案情绪填 `bgm_mood`)
75
+
76
+ 选片规则:
77
+ - 首句优先用能建立场景/正面的镜头
78
+ - 时序上有视觉故事线就尊重它
79
+ - **clip 可复用,但复用时必须给不同的 `src_start`(选不同子镜头),否则画面重复**;也尽量别连续两句用同一 clip
80
+ - **每个 clip 通常有多个子镜头(manifest 多帧)——优先把不同子镜头分给不同句子,让素材都出镜,而不是反复用片头**
81
+ - 素材数 < 句子数时复用并提示用户素材偏少
82
+ - **若某 clip 帧里有烧死的字幕/水印**(见 §坑),尽量不选它承载关键句;非用不可则提示用户
83
+
84
+ 情绪配法:按文案的情感弧线给每句配 emotion,不要全句一个调。例:怅然开场 `sad` → 转折欣喜 `happy` → 高潮 `surprised` → 舒缓 `calm` → 暖心收尾 `happy`。
85
+
86
+ 节奏(平台风格):这是短视频,默认**快节奏**(像抖音/TikTok 口播)。文案**短句、强钩子、句子别太长**(一句太长配音就拖、画面也切得慢);语速默认 1.35,想更冲可更快。除非用户明确要慢节奏治愈片,否则别配出"念课文"那种拖沓感。
87
+
88
+ ### 4. 试听音色 / BGM + 看选片方案 → 用户拍板
89
+ **音色和 BGM 是创作决策,让用户自己听、自己定,不要替用户默认死。** 出片前:
90
+
91
+ > **素材路径**:BGM 库和音色样音随 `gen-cli` 一起发布(不在 skill 目录)。先解析一次:
92
+ > ```bash
93
+ > ASSETS=$(python3 $CLAUDE_SKILL_DIR/scripts/video_compose.py assets-dir)
94
+ > ```
95
+ > 下面用 `$ASSETS/voice-samples`、`$ASSETS/bgm-library/<mood>`。
96
+
97
+ 1. **音色试听**:把 `$ASSETS/voice-samples/*.mp3`(7 个音色样音,标签↔voice_id 见同目录 `CATALOG.md`)拷到 `<proj>/previews/`,把可播放的文件链接列给用户,让用户**听完选一个**。
98
+ 2. **BGM 试听**:按文案情绪先推荐一类(治愈→`warm`、种草→`upbeat`…),把该类(用户想多听就多拷几类)`$ASSETS/bgm-library/<mood>/*.mp3` 拷到 `<proj>/previews/` 给用户试听;用户可换类、指定某首,或**自己上传一首**(放 `inputs/bgm/`,优先级最高)。
99
+ 3. **选片方案**:把 assignments 摘要(每句哪个 clip + 理由)一并给用户过目。
100
+ 4. **给推荐默认**(音色=贴文案的一个、BGM=按情绪一类),用户想省事一句「就用默认」即可直接出片;想换就听了再定。
101
+
102
+ 用户拍板后:选定的 voice_id 写进 `proposal.voice`、BGM 写进 `bgm`(自带路径) 或 `bgm_mood`(情绪库),再:
103
+ ```
104
+ python3 $CLAUDE_SKILL_DIR/scripts/video_compose.py build <proj>
105
+ ```
106
+ 脚本做:逐句情感配音(带缓存,未改的句子不重跑)→ 每句按 `src_start` 用 `-ss` **精切对应子镜头**到该句时长(竖版 crop,clip 比该句短则**慢放填满、不 loop**;**同素材复用强制时间窗不重叠,防重复镜头**)→ 拼接 → 烧字幕 → BGM sidechain ducking(有 BGM 才加)→ `final.mp4`。
107
+
108
+ ### 5. 交付汇报
109
+ 报成片路径 + 时长 + 用了几个 clip + 配音字数。**不要提任何模型/服务名**,配音统一说"AI 配音"。
110
+
111
+ ## BGM(不锁死;用户没指定就按文案情绪自动配)
112
+ 来源优先级:
113
+ 1. `proposal["bgm"]` 显式路径(用户明确指定某首)
114
+ 2. `inputs/bgm/` 用户上传的音频
115
+ 3. `proposal["bgm_mood"]` → 从情绪库 `bgm-library/<mood>/` **随机挑一首**(用户没指定 BGM 时,**你按文案整体情绪填这个字段**)
116
+ 4. 都没有 → 仅人声
117
+
118
+ - 有 BGM 时自动 sidechain ducking(人声起时压低 BGM)
119
+ - **优先让用户试听后选**(见 §4 步骤 2);用户说「你定/随便」时才按文案情绪自动填 `bgm_mood`,**绝不留空**。情绪 → 目录:`warm`(治愈/温暖) `upbeat`(欢快) `sad`(伤感) `calm`(舒缓) `energetic`(高能/卖货) `dramatic`(戏剧/科技)
120
+ - 用户明确要某首 / 要换 → 用优先级 1、2
121
+ - ⚠ 库里只能放**可商用授权**的曲子(公开发布视频有版权要求);某情绪目录为空时该句跳过 BGM 并提示
122
+
123
+ ## 用户改方案怎么办
124
+ - 换某句的画面:改 `proposal.json` 那条的 `clip`(换素材)或 `src_start`(同素材换子镜头),重跑 `build`
125
+ - 嫌某两句画面重复:给它们填不同的 `src_start`(看 manifest 的帧 `t`),重跑 `build`
126
+ - 换情绪/音色:改 `emotion`/`voice`,重跑 `build`
127
+ - 换/加 BGM:丢文件进 `inputs/bgm/` 或改 `bgm` 路径,重跑 `build`
128
+ **配音有缓存**(engine/voice/emotion/speed/text 没变就复用),所以单纯换 BGM/字幕**不重新花钱跑配音**,秒出。`proposal.json` 就是反复调的抓手。
129
+
130
+ ## 坑(实跑踩过)
131
+
132
+ | 坑 | 处理 |
133
+ |---|---|
134
+ | **用户素材自带烧死字幕/水印** | 你看帧时若发现某片已有硬字幕(如原片烧了英文 caption),新字幕叠上去会重影。承载关键句时避开该片,或提示用户该片有原生字幕 |
135
+ | **clip 是横版** | 脚本默认 center-crop 到竖版会裁掉两侧;横版素材多时提示用户成片是竖版裁切 |
136
+ | **素材总时长 < 文案配音时长** | 不再 loop(loop=重复):单片比某句短→慢放填满;某片被多句复用但时长不够 distinct 画面→`[mix] ⚠` 告警。**要彻底不重复**:素材总时长应 ≥ 配音总时长,且每片别被复用超过它能切出的 distinct 段数;不够就提示用户补素材或精简文案 |
137
+ | **字幕样式/字体** | 默认白字黑边底部。字体走 env `VIDEO_COMPOSE_FONT`(默认 `Noto Sans CJK SC`)。prod shell 镜像(optima-ai-shell 根 `Dockerfile`,Ubuntu 22.04)已装 `fonts-noto-cjk`(提供 `Noto Sans CJK SC`)+ Source Han Sans SC,CJK 字体确认可用。`build` 前 `_preflight_font()` 会 `fc-match` 兜底,缺字体 fail-loud 不静默渲染豆腐块。换镜像/换 env 字体时按此 fc-match 验证 |
138
+ | **gen tts 报错** | 透传后端错误。`PROVIDER_INSUFFICIENT_CREDITS`=MiniMax 余额;`INVALID_INPUT`=emotion/voice 非法。配音失败不出片,不静默吞 |
139
+
140
+ ## 关键参数(scripts/video_compose.py 顶部)
141
+ - `W,H=1080,1920`(竖版)、`FPS=30`、`CRF=20`
142
+ - 配音:`gen tts --provider minimax`,密钥/计费在后端,skill 不碰密钥
143
+ - `VIDEO_COMPOSE_FONT`:字幕字体(容器需有对应 CJK 字体)
144
+ - BGM 不锁死:见 §BGM;情绪库随 `gen-cli` 打包,脚本自动解析(`$ASSETS/bgm-library/<mood>/`,见 §4 素材路径)