@optima-chat/gen-cli 2.6.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.
- package/package.json +1 -2
- package/.claude/skills/digital-human/SKILL.md +0 -309
- package/.claude/skills/digital-human/references/avatar-catalog.md +0 -47
- package/.claude/skills/digital-human/references/edit.md +0 -219
- package/.claude/skills/digital-human/references/generate.md +0 -378
- package/.claude/skills/digital-human/references/manage.md +0 -137
- package/.claude/skills/digital-human/references/train.md +0 -276
- package/.claude/skills/gen/SKILL.md +0 -366
- package/.claude/skills/motion-control/SKILL.md +0 -68
- package/.claude/skills/multigrid-poster/SKILL.md +0 -194
- package/.claude/skills/multigrid-poster/layouts/2x2.json +0 -34
- package/.claude/skills/multigrid-poster/layouts/3x3.json +0 -43
- package/.claude/skills/multigrid-poster/scripts/compose.py +0 -116
- package/.claude/skills/multigrid-poster/scripts/placeholder.png +0 -0
- package/.claude/skills/multigrid-poster/shared/fonts/MaShanZheng-Regular.ttf +0 -0
- package/.claude/skills/video-compose/SKILL.md +0 -144
- package/.claude/skills/video-compose/scripts/video_compose.py +0 -290
- package/.claude/skills/video-edit/SKILL.md +0 -332
- package/.claude/skills/video-gen/SKILL.md +0 -642
- package/.claude/skills/video-gen/references/cinematic-language.md +0 -158
- package/.claude/skills/video-gen/references/confirm-card.md +0 -49
- package/.claude/skills/video-gen/references/prompt-craft.md +0 -73
- package/.claude/skills/video-gen/templates/INDEX.md +0 -78
- package/.claude/skills/video-gen/templates/before-after-beauty.md +0 -183
- package/.claude/skills/video-gen/templates/drama-fmcg.md +0 -183
- package/.claude/skills/video-gen/templates/kol-reaction-food.md +0 -193
- package/.claude/skills/video-gen/templates/multi-point-apparel.md +0 -185
- package/.claude/skills/video-gen/templates/pain-solution-home.md +0 -184
- package/.claude/skills/video-gen/templates/pdp-360-showcase.md +0 -189
- package/.claude/skills/video-gen/templates/pdp-feature-highlight.md +0 -182
- package/.claude/skills/video-gen/templates/scene-digital.md +0 -183
- package/.claude/skills/video-translate/SKILL.md +0 -547
|
@@ -1,68 +0,0 @@
|
|
|
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 一般不直接调
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: multigrid-poster
|
|
3
|
-
description: "为商家生成小红书 2×2 四宫格 / 3×3 九宫格封面图。触发场景:做小红书封面 / 小红书首图 / 种草帖封面 / 爆款封面 / 四宫格 / 九宫格。一句话指令产出 1242×1660 成片,支持自然语言迭代(换版式 / 重抽某格 / 改文案)。本 skill 只生成封面图,搜索小红书笔记 / 分析博主请用 'xhs' skill。"
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
owner_repo: Optima-Chat/optima-gen
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# 小红书多宫格封面生成
|
|
9
|
-
|
|
10
|
-
帮电商商家用 AI 图 + 通用网格布局合成小红书封面。**一句话从意图到 1242×1660 成片**,支持 4 宫格 / 9 宫格两种版式。
|
|
11
|
-
|
|
12
|
-
## Global Rules
|
|
13
|
-
|
|
14
|
-
优先级高于任何 pipeline 步骤。
|
|
15
|
-
|
|
16
|
-
1. **User-facing 不出现模型名 / 服务名**
|
|
17
|
-
status / 成本 / 进度统一用"封面生成中 / 素材生成中 / 合成封面中"。`gen image` 作为 CLI 字面值可以,但不要把整条命令原文回显给用户。
|
|
18
|
-
|
|
19
|
-
2. **花钱前必走 COST-GATE**
|
|
20
|
-
任何 `gen image` 批量调用之前必走一次成本确认。**Fast-path、迭代、重试均无例外**。2×2 = 4 张,3×3 = 9 张,SKU 拉图 = 0 张。rate 按 `gen image` 每次 1 积分估。
|
|
21
|
-
|
|
22
|
-
3. **Per-post init 是任何 pipeline 第一步**
|
|
23
|
-
出图 / 合成执行前先建目录 + cd。否则 write 写错位置。迭代场景用 `{旧id}-vN`。
|
|
24
|
-
|
|
25
|
-
4. **Anti-fabrication**
|
|
26
|
-
未在本 skill 显式列出的命令 / flag / 参数,不允许凭印象拼。`gen image` / `commerce` / `compose.py` 子命令同样适用。
|
|
27
|
-
|
|
28
|
-
5. **不自动发帖**
|
|
29
|
-
只产 PNG,**绝对不调用**任何自动登录 / 发帖 / 上传命令。完成后给路径,用户自己下载手动发。
|
|
30
|
-
|
|
31
|
-
## 工作目录
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
~/multigrid-poster/
|
|
35
|
-
├── preferences.md
|
|
36
|
-
├── history.md
|
|
37
|
-
└── posters/{post-id}/
|
|
38
|
-
├── intent.md # 用户意图 + layout + 文案
|
|
39
|
-
├── cells/cell_0..N.png # 4 或 9 张素材
|
|
40
|
-
├── cover.png # 成片
|
|
41
|
-
└── cost.md
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
整个 `~/multigrid-poster/` 是一个 git repo。每步完成后 `git add -A && git commit`。
|
|
45
|
-
|
|
46
|
-
## 启动流程
|
|
47
|
-
|
|
48
|
-
1. **首次** (`ls ~/multigrid-poster/preferences.md` 不存在) → `mkdir -p ~/multigrid-poster/posters && cd ~/multigrid-poster && git init -b main`,创建空的 `preferences.md` (字段:merchant_id / brand_name / category / xhs_account_id / default_layout) 和 `history.md` (表头:date | post-id | layout | title | parent | status)。`.gitignore` 加 `posters/**/cells/*.png` 和 `posters/**/cover.png`。
|
|
49
|
-
2. **扫未完成**:`posters/` 下有 `intent.md` 但无 `cover.png` 且 < 7 天 → 提一次"你有 N 个封面没完成"。
|
|
50
|
-
3. **读 preferences.md / history.md**,继续主流程。
|
|
51
|
-
|
|
52
|
-
## 主流程
|
|
53
|
-
|
|
54
|
-
### Step 1: 选 layout
|
|
55
|
-
|
|
56
|
-
| 用户原话 | layout | cells |
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| 含"九宫格 / 9 格 / 9 张 / 清单 / N 款 / 礼物推荐 / 榜单" | **3×3** | 9 |
|
|
59
|
-
| 其他(包含"四宫格 / 4 格 / 4 张" 或没指定) | **2×2** | 4 |
|
|
60
|
-
|
|
61
|
-
### Step 2: Per-post init
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
# slug = 用户意图前 20 字内的 kebab-case
|
|
65
|
-
POST_ID="$(date +%Y%m%d-%H%M)-<slug>"
|
|
66
|
-
mkdir -p ~/multigrid-poster/posters/$POST_ID/cells
|
|
67
|
-
cd ~/multigrid-poster/posters/$POST_ID
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
迭代("换版式 / 重抽 / 改文案")时:轻迭代沿用旧目录,重迭代新建 `{旧id}-vN`。
|
|
71
|
-
|
|
72
|
-
### Step 3: 写文案
|
|
73
|
-
|
|
74
|
-
agent 自己写,不调外部生成器。约束:
|
|
75
|
-
- **title**:2 行 × 8-12 字 / 行(2×2 适合)或 2 行 × 6-10 字 / 行(3×3 标题挤)
|
|
76
|
-
- **caption**:2 行 × 15-20 字 / 行
|
|
77
|
-
- 硬禁:医疗 / 保健 / 绝对化用语(最 / 第一 / 唯一 / 100%)
|
|
78
|
-
|
|
79
|
-
写到 `intent.md`:
|
|
80
|
-
|
|
81
|
-
```markdown
|
|
82
|
-
# Intent
|
|
83
|
-
| 项目 | 值 |
|
|
84
|
-
|---|---|
|
|
85
|
-
| 用户原话 | <原话> |
|
|
86
|
-
| layout | 2x2 / 3x3 |
|
|
87
|
-
| title 行 1 | <8-12 字> |
|
|
88
|
-
| title 行 2 | <8-12 字> |
|
|
89
|
-
| caption 行 1 | <15-20 字> |
|
|
90
|
-
| caption 行 2 | <15-20 字> |
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
展示给用户:"标题'XXX / YYY',副标题'AAA / BBB'。OK 吗?"
|
|
94
|
-
- Fast-path(意图明确):告知,用户喊停才停
|
|
95
|
-
- 意图模糊:必须等确认
|
|
96
|
-
|
|
97
|
-
### Step 4: COST-GATE
|
|
98
|
-
|
|
99
|
-
**生成前必做**(包括 Fast-path / 迭代 / 重试):
|
|
100
|
-
|
|
101
|
-
> 即将生成封面(布局: 2x2 / 3x3),预计:
|
|
102
|
-
> - 素材调用: N 次(2×2=4 / 3×3=9 / SKU 拉图=0)
|
|
103
|
-
> - 预估耗时: ~X 分钟
|
|
104
|
-
> - 预估成本: ~Y 积分
|
|
105
|
-
>
|
|
106
|
-
> 继续?
|
|
107
|
-
|
|
108
|
-
用户说"继续 / 好" → 执行。"太贵 / 换便宜的" → 提议降级 (3×3 → 2×2,或 SKU 拉图)。不回应 → 等。
|
|
109
|
-
|
|
110
|
-
### Step 5: 出图
|
|
111
|
-
|
|
112
|
-
**默认走 AI 生图**。每个 cell 并行调一次:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
# 2×2: cell 尺寸 621×830;3×3: cell 尺寸 414×420
|
|
116
|
-
gen image "<subprompt>" -W <W> -H <H> -o ./cells/cell_<i>.png -s <seed> -f png
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
`<subprompt>` 由 agent 根据用户意图为每格独立设计(不同视角 / 不同 step / 不同场景 / 不同 SKU 等)。`<seed>` 用确定性 hash(POST_ID + cell_index),迭代复用同格 seed。
|
|
120
|
-
|
|
121
|
-
**SKU 拉图模式**(用户明确说"用我店里商品图"做 listicle):
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
commerce product list --limit 9
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
下载到 `./cells/cell_0..8.png`。商品 < 9 → 降级 2×2 取前 4。商品 < 4 → 报错。
|
|
128
|
-
|
|
129
|
-
**失败容忍**:单格生图失败 → 重试 1 次(换 seed)。两次失败 → 用 `${CLAUDE_SKILL_DIR}/scripts/placeholder.png` 占位,告知用户"第 N 格失败,先占位,要重抽直接说"。
|
|
130
|
-
|
|
131
|
-
### Step 6: 合成
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
python3 $CLAUDE_SKILL_DIR/scripts/compose.py \
|
|
135
|
-
--layout $CLAUDE_SKILL_DIR/layouts/2x2.json \
|
|
136
|
-
--cells ./cells/cell_0.png ./cells/cell_1.png ... \
|
|
137
|
-
--title-line "<title 行 1>" \
|
|
138
|
-
--title-line "<title 行 2>" \
|
|
139
|
-
--caption-line "<caption 行 1>" \
|
|
140
|
-
--caption-line "<caption 行 2>" \
|
|
141
|
-
--output ./cover.png
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
依赖:Pillow(容器自带)。失败常见原因:
|
|
145
|
-
|
|
146
|
-
| 错误 | 处理 |
|
|
147
|
-
|---|---|
|
|
148
|
-
| `cell 数量不对` | layout 要求 4 / 9,检查 `--cells` 参数 |
|
|
149
|
-
| `font not found` | 检查 `$CLAUDE_SKILL_DIR/shared/fonts/` 完整 |
|
|
150
|
-
| 中文显示方块 | 同上,字体没加载 |
|
|
151
|
-
|
|
152
|
-
### Step 7: 交付
|
|
153
|
-
|
|
154
|
-
写 `cost.md`,追加 `~/multigrid-poster/history.md` 一行,告知用户:
|
|
155
|
-
|
|
156
|
-
> 封面在 `~/multigrid-poster/posters/<POST_ID>/cover.png`,可以下载发帖了。
|
|
157
|
-
> 换版式 / 改文案 / 重抽某格直接告诉我。
|
|
158
|
-
|
|
159
|
-
用户说"好 / 完美" → preferences.md `Learned` 追加一条 → commit。
|
|
160
|
-
|
|
161
|
-
## 迭代
|
|
162
|
-
|
|
163
|
-
| 类型 | 重跑步骤 | 新目录 | 成本 |
|
|
164
|
-
|---|---|---|---|
|
|
165
|
-
| 换 layout(2×2 ↔ 3×3) | 文案 → 出图 → 合成 | 是(`-vN`) | 全成本 |
|
|
166
|
-
| 重抽全部 | 出图 → 合成 | 是 | 全素材 |
|
|
167
|
-
| 重抽单格 N | 出图(单格) → 合成 | 否 | 1 素材 |
|
|
168
|
-
| 改文案 | 合成 | 否 | 0 |
|
|
169
|
-
|
|
170
|
-
**每次迭代也走 COST-GATE**,即使 0 积分。
|
|
171
|
-
|
|
172
|
-
## 错误处理
|
|
173
|
-
|
|
174
|
-
| 故障 | 处理 |
|
|
175
|
-
|---|---|
|
|
176
|
-
| `gen image` 返回 failed | 重试 1 次换 seed → 仍失败用占位图 |
|
|
177
|
-
| 超配额 / 余额不足 | 告知用户,不自动降级 |
|
|
178
|
-
| `commerce product list` < 9 | 降级 2×2 取前 4 |
|
|
179
|
-
| 会话关闭 | 状态在文件系统 + git,下次接续 |
|
|
180
|
-
|
|
181
|
-
## 相关工具
|
|
182
|
-
|
|
183
|
-
- `gen image` — 文生图(详见 `gen` skill)
|
|
184
|
-
- `commerce merchant get` / `commerce product list` — 商家档案 / 商品(详见 `merchant` skill 和 `product` skill)
|
|
185
|
-
- `compose.py` — 本 skill 自带的 Pillow 渲染器
|
|
186
|
-
|
|
187
|
-
## 流程偏好
|
|
188
|
-
|
|
189
|
-
- **信息够就直接做(Fast-path)**
|
|
190
|
-
- **`intent.md` 是可追溯产物**
|
|
191
|
-
- **每步完成立刻 git commit**
|
|
192
|
-
- **生成过程零打扰**
|
|
193
|
-
- **迭代用 `-vN` 不覆盖**
|
|
194
|
-
- **新会话有未完成先告知一次**
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "通用 2×2 网格布局 — 适合 4 张 cell 的所有 intent (创业故事/对比测评/教程/场景)",
|
|
3
|
-
"canvas_size": [1242, 1660],
|
|
4
|
-
"cells": {
|
|
5
|
-
"positions": [[0, 0], [621, 0], [0, 830], [621, 830]],
|
|
6
|
-
"sizes": [[621, 830], [621, 830], [621, 830], [621, 830]]
|
|
7
|
-
},
|
|
8
|
-
"text_zones": {
|
|
9
|
-
"title": {
|
|
10
|
-
"_comment": "中央偏上 2 行标题 - 8-12 字 / 行最佳",
|
|
11
|
-
"font": "shared/fonts/MaShanZheng-Regular.ttf",
|
|
12
|
-
"size": 110,
|
|
13
|
-
"color": "#FFB940",
|
|
14
|
-
"stroke_w": 8,
|
|
15
|
-
"stroke_color": "#D63D3D",
|
|
16
|
-
"lines": [
|
|
17
|
-
{"position": [621, 480], "anchor": "mm"},
|
|
18
|
-
{"position": [621, 620], "anchor": "mm"}
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"caption": {
|
|
22
|
-
"_comment": "底部 2 行 caption - 15-20 字 / 行最佳",
|
|
23
|
-
"font": "shared/fonts/MaShanZheng-Regular.ttf",
|
|
24
|
-
"size": 78,
|
|
25
|
-
"color": "#FFB940",
|
|
26
|
-
"stroke_w": 6,
|
|
27
|
-
"stroke_color": "#D63D3D",
|
|
28
|
-
"lines": [
|
|
29
|
-
{"position": [621, 1340], "anchor": "mm"},
|
|
30
|
-
{"position": [621, 1450], "anchor": "mm"}
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "通用 3×3 网格布局 - 适合 9 张 cell (商品清单 / 多角度展示)",
|
|
3
|
-
"canvas_size": [1242, 1660],
|
|
4
|
-
"cells": {
|
|
5
|
-
"_comment": "9 格 414×420。顶部 200px 标题区,cells y=200..1460,底部 200px caption 区。3×420 + 200×2 = 1660 = canvas_h ✓",
|
|
6
|
-
"positions": [
|
|
7
|
-
[0, 200], [414, 200], [828, 200],
|
|
8
|
-
[0, 620], [414, 620], [828, 620],
|
|
9
|
-
[0, 1040], [414, 1040], [828, 1040]
|
|
10
|
-
],
|
|
11
|
-
"sizes": [
|
|
12
|
-
[414, 420], [414, 420], [414, 420],
|
|
13
|
-
[414, 420], [414, 420], [414, 420],
|
|
14
|
-
[414, 420], [414, 420], [414, 420]
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"text_zones": {
|
|
18
|
-
"title": {
|
|
19
|
-
"_comment": "顶部白边 2 行标题(y < 200 区间)",
|
|
20
|
-
"font": "shared/fonts/MaShanZheng-Regular.ttf",
|
|
21
|
-
"size": 78,
|
|
22
|
-
"color": "#FFB940",
|
|
23
|
-
"stroke_w": 6,
|
|
24
|
-
"stroke_color": "#D63D3D",
|
|
25
|
-
"lines": [
|
|
26
|
-
{"position": [621, 60], "anchor": "mm"},
|
|
27
|
-
{"position": [621, 150], "anchor": "mm"}
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
"caption": {
|
|
31
|
-
"_comment": "底部白边 2 行 caption(cells 结束于 y=1460,留 200px)",
|
|
32
|
-
"font": "shared/fonts/MaShanZheng-Regular.ttf",
|
|
33
|
-
"size": 60,
|
|
34
|
-
"color": "#FFB940",
|
|
35
|
-
"stroke_w": 5,
|
|
36
|
-
"stroke_color": "#D63D3D",
|
|
37
|
-
"lines": [
|
|
38
|
-
{"position": [621, 1530], "anchor": "mm"},
|
|
39
|
-
{"position": [621, 1610], "anchor": "mm"}
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""multigrid-poster compose — Pillow 版渲染器
|
|
3
|
-
|
|
4
|
-
输入:layout.json + N 张 cell 图片 + 标题文字 + caption 文字
|
|
5
|
-
输出:1242×1660 PNG(小红书封面标准)
|
|
6
|
-
|
|
7
|
-
用法:
|
|
8
|
-
python compose.py \
|
|
9
|
-
--layout <SKILL_DIR>/layouts/2x2.json \
|
|
10
|
-
--cells cell_0.png cell_1.png cell_2.png cell_3.png \
|
|
11
|
-
--title-line "26岁一个人创业" \
|
|
12
|
-
--title-line "跨境电商月入10w+" \
|
|
13
|
-
--caption-line "只需要一部手机就可以完成!" \
|
|
14
|
-
--caption-line "跨境人的必备app推荐" \
|
|
15
|
-
--output cover.png
|
|
16
|
-
|
|
17
|
-
依赖:Pillow (容器自带,无需额外安装)
|
|
18
|
-
字体:从 SKILL_DIR/shared/fonts/ 加载(layout.json 里指定)
|
|
19
|
-
"""
|
|
20
|
-
import argparse
|
|
21
|
-
import json
|
|
22
|
-
import sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from PIL import Image, ImageDraw, ImageFont
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def compose(layout_path: Path, cell_paths: list[Path],
|
|
28
|
-
title_lines: list[str], caption_lines: list[str],
|
|
29
|
-
output_path: Path) -> Path:
|
|
30
|
-
"""根据 layout.json 渲染海报。
|
|
31
|
-
|
|
32
|
-
layout.json 字段:
|
|
33
|
-
canvas_size [w, h]
|
|
34
|
-
cells.positions [[x,y], ...] cell 左上角
|
|
35
|
-
cells.sizes [[w,h], ...] cell 尺寸
|
|
36
|
-
text_zones.title {lines: [...], size, color, stroke_w, stroke_color, font}
|
|
37
|
-
text_zones.caption {同上}
|
|
38
|
-
"""
|
|
39
|
-
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
|
40
|
-
# 约定:layout 必须放在 <skill>/layouts/ 下,字体路径相对 <skill>/ 解析
|
|
41
|
-
skill_dir = layout_path.parent.parent
|
|
42
|
-
|
|
43
|
-
# 文本行数 vs layout 配置行数:行多了 zip 会静默截断,显式 fail-fast
|
|
44
|
-
# 错误信息用英文 — sys.exit 走 stderr,Windows GBK locale 下中文会乱码
|
|
45
|
-
for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
|
|
46
|
-
zone = layout.get("text_zones", {}).get(zone_key)
|
|
47
|
-
if not zone:
|
|
48
|
-
continue
|
|
49
|
-
max_lines = len(zone["lines"])
|
|
50
|
-
if len(lines) > max_lines:
|
|
51
|
-
sys.exit(f"too many {zone_key} lines: got {len(lines)}, layout supports {max_lines}")
|
|
52
|
-
|
|
53
|
-
# Canvas
|
|
54
|
-
canvas = Image.new("RGB", tuple(layout["canvas_size"]), "white")
|
|
55
|
-
|
|
56
|
-
# Cells
|
|
57
|
-
cell_count = len(layout["cells"]["positions"])
|
|
58
|
-
if len(cell_paths) != cell_count:
|
|
59
|
-
sys.exit(f"cell count mismatch: layout expects {cell_count}, got {len(cell_paths)}")
|
|
60
|
-
for cp, pos, size in zip(cell_paths,
|
|
61
|
-
layout["cells"]["positions"],
|
|
62
|
-
layout["cells"]["sizes"]):
|
|
63
|
-
img = Image.open(cp).convert("RGB").resize(tuple(size), Image.LANCZOS)
|
|
64
|
-
canvas.paste(img, tuple(pos))
|
|
65
|
-
|
|
66
|
-
draw = ImageDraw.Draw(canvas)
|
|
67
|
-
|
|
68
|
-
# Text zones (title + caption 通用绘制)
|
|
69
|
-
for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
|
|
70
|
-
if zone_key not in layout["text_zones"]:
|
|
71
|
-
continue
|
|
72
|
-
zone = layout["text_zones"][zone_key]
|
|
73
|
-
font_path = skill_dir / zone["font"]
|
|
74
|
-
font = ImageFont.truetype(str(font_path), zone["size"])
|
|
75
|
-
for text, line_cfg in zip(lines, zone["lines"]):
|
|
76
|
-
if not text:
|
|
77
|
-
continue
|
|
78
|
-
draw.text(
|
|
79
|
-
tuple(line_cfg["position"]), text,
|
|
80
|
-
fill=zone["color"], font=font,
|
|
81
|
-
stroke_width=zone.get("stroke_w", 0),
|
|
82
|
-
stroke_fill=zone.get("stroke_color", zone["color"]),
|
|
83
|
-
anchor=line_cfg.get("anchor", "la"),
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
canvas.save(output_path, optimize=True)
|
|
87
|
-
return output_path
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def main():
|
|
91
|
-
ap = argparse.ArgumentParser(description=__doc__,
|
|
92
|
-
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
93
|
-
ap.add_argument("--layout", required=True, type=Path,
|
|
94
|
-
help="layout.json 路径(如 SKILL_DIR/layouts/2x2.json)")
|
|
95
|
-
ap.add_argument("--cells", required=True, nargs="+", type=Path,
|
|
96
|
-
help="N 张 cell 图片路径,顺序对应 layout.cells.positions")
|
|
97
|
-
ap.add_argument("--title-line", action="append", default=[],
|
|
98
|
-
help="标题行(可重复)")
|
|
99
|
-
ap.add_argument("--caption-line", action="append", default=[],
|
|
100
|
-
help="底部 caption 行(可重复)")
|
|
101
|
-
ap.add_argument("--output", required=True, type=Path,
|
|
102
|
-
help="输出 PNG 路径")
|
|
103
|
-
args = ap.parse_args()
|
|
104
|
-
|
|
105
|
-
out = compose(
|
|
106
|
-
layout_path=args.layout,
|
|
107
|
-
cell_paths=args.cells,
|
|
108
|
-
title_lines=args.title_line,
|
|
109
|
-
caption_lines=args.caption_line,
|
|
110
|
-
output_path=args.output,
|
|
111
|
-
)
|
|
112
|
-
print(f"saved {out} ({out.stat().st_size // 1024} KB)")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if __name__ == "__main__":
|
|
116
|
-
main()
|
|
Binary file
|
|
Binary file
|
|
@@ -1,144 +0,0 @@
|
|
|
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 素材路径)
|