@optima-chat/gen-cli 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +12 -3
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/video.d.ts.map +1 -1
- package/dist/commands/video.js +26 -2
- package/dist/commands/video.js.map +1 -1
- 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 -662
- 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 -72
- 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,547 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: video-translate
|
|
3
|
-
description: "把单段口播视频翻译成 English / Thai (Thailand) / Malay (Malaysia) / Vietnamese (Vietnam) 之一,出片自带译音(自动克隆原说话人)+ 烧录目标语花体字幕 + 默认 BGM ducking。触发场景:用户说翻译这个视频/做泰语版/英文版/越南语版/马来语版/视频本地化/换语言/dub。"
|
|
4
|
-
version: 1.0.3
|
|
5
|
-
owner_repo: Optima-Chat/optima-gen
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# video-translate
|
|
9
|
-
|
|
10
|
-
把口播视频本地化:MiniMax 预置明亮女声译音 + 烧录目标语字幕 + BGM ducking(默认开)。**3 步主流程**(+ Step 0 预检 + Step 4 可选清理)。
|
|
11
|
-
|
|
12
|
-
## 适用语言
|
|
13
|
-
|
|
14
|
-
- `English`
|
|
15
|
-
- `Thai (Thailand)`
|
|
16
|
-
- `Malay (Malaysia)`
|
|
17
|
-
- `Vietnamese (Vietnam)`
|
|
18
|
-
|
|
19
|
-
其他语言用户说出来,**先 echo 警告**:"本 skill v1 只支持 4 语,其他语种支持中,可改用 `gen video-translate --lang <X>` 拿基础翻译(无花体字幕)"
|
|
20
|
-
|
|
21
|
-
## 前提
|
|
22
|
-
|
|
23
|
-
- 源视频 URL 公网可访问(或先让用户上传到 Optima → 拿 URL)
|
|
24
|
-
- 容器有:`gen` CLI(`@optima-chat/optima-gen` ≥ latest)、`video-translate` CLI(`@optima-chat/video-translate-tools` ≥ 1.0.9,`--style` 需 1.0.9)、`ffmpeg`、`curl`、`jq`
|
|
25
|
-
- 后端 2026-05 从 HeyGen 切换为 MiniMax pipeline(同接口,~100× 便宜)。CLI 接口未变,旧 `--mode` / `--dynamic-duration` flag 已无效会被忽略
|
|
26
|
-
|
|
27
|
-
## 输入
|
|
28
|
-
|
|
29
|
-
| 参数 | 必填 | 说明 |
|
|
30
|
-
|---|---|---|
|
|
31
|
-
| `URL` | ✅ | 公网可访问的源视频 URL |
|
|
32
|
-
| `LANG` | ✅ | 人类可读语言名(见上面 4 语) |
|
|
33
|
-
| `TAG` | ✅ | 对应的两字母 tag(`en` / `th` / `ms` / `vi`) |
|
|
34
|
-
| `BGM` | ⬜ | 自定义 BGM 文件路径(覆盖默认)。不传则用 npm 包内置 `bgm/default.mp3`(22s clean instrumental,自动 loop)|
|
|
35
|
-
| `NO_BGM` | ⬜ | 设非空值则跳过 BGM(出片只有人声 + 原视频 BGM 残留)|
|
|
36
|
-
| `VOICE` | ⬜ | MiniMax voice_id(从下面 ## Voice Catalog 选)。**不传 = `Portuguese_FriendlyNeighbor`** 默认(友好邻居女声,广谱适用——卖货 / 教程 / demo 都不违和)|
|
|
37
|
-
| `NAME` | ⬜ | 工作区名,默认从 URL 末段推 |
|
|
38
|
-
| `STYLE` | ⬜ | 字幕风格,**不传 = `classic`(原款)**。可选 `pop-soft` / `pop-3d` / `pop-hl` / `anton` / `luckyguy`,见 Step 0.6。需 `video-translate-tools` ≥ 1.0.9 |
|
|
39
|
-
|
|
40
|
-
## 3 步主流程(+ Step 0 预检 + Step 4 可选清理)
|
|
41
|
-
|
|
42
|
-
### Step 0:声明 + 工作区 + URL 预处理 + 源视频音量预检
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
## ⚠ URL 必须是公网 https URL,不能是本地路径 /home/aiuser/...
|
|
46
|
-
## gen video-translate 只接受 https URL(后端要下载源视频)
|
|
47
|
-
## 如果是本地路径,先用 chat 系统的 file API 签 URL
|
|
48
|
-
if [[ ! "$URL" =~ ^https?:// ]]; then
|
|
49
|
-
echo "INFO: 本地路径 '$URL',需要上传拿 https URL"
|
|
50
|
-
TOKEN=$(jq -r '.access_token' ~/.optima/token.json)
|
|
51
|
-
## shell.optima.onl 的 file signing endpoint(具体路径以系统实际为准,
|
|
52
|
-
## 这里给出常见模式;如果不行,根据 stderr 报错调整)
|
|
53
|
-
SIGNED=$(curl -sS -X POST "https://shell.optima.onl/api/files/sign" \
|
|
54
|
-
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
55
|
-
-d "{\"path\":\"$URL\"}" 2>&1 | jq -r '.data.url // .url // empty' 2>/dev/null)
|
|
56
|
-
if [ -z "$SIGNED" ]; then
|
|
57
|
-
echo "ERR: 拿不到 signed URL,把 '$URL' 上传到 chat 系统拿 https URL 再来"
|
|
58
|
-
exit 1
|
|
59
|
-
fi
|
|
60
|
-
URL="$SIGNED"
|
|
61
|
-
echo "URL → $URL"
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
## URL 末段可能含 query string(S3 预签名 url 含 token)— 先去掉再 basename
|
|
65
|
-
NAME="${NAME:-$(echo "$URL" | cut -d'?' -f1 | xargs basename | sed 's/\.[^.]*$//' | sed 's/[^A-Za-z0-9_-]/_/g')}"
|
|
66
|
-
WORK="./videos/${NAME}.work"
|
|
67
|
-
mkdir -p "$WORK"
|
|
68
|
-
|
|
69
|
-
## 音量预检 — Whisper ASR 在 < -25dB 会返回 0 segments(adapter 抛 "source audio too quiet (<-25dB)" 错误)
|
|
70
|
-
## ffmpeg 输出在 stderr,必须 2>&1
|
|
71
|
-
ffmpeg -i "$URL" -af volumedetect -f null - 2>&1 | grep mean_volume
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
如果 `mean_volume < -25dB`,提示用户:"源视频音量太低(< −25dB),翻译服务大概率会报无人声。建议先用 `ffmpeg -i in.mp4 -af 'volume=20dB,acompressor=threshold=-20dB:ratio=4' -c:v copy out.mp4` 放大后再翻译。"
|
|
75
|
-
|
|
76
|
-
### Step 0.5:音色 — 默认**自动双音色**(LLM 标注 A/B),**不问用户**
|
|
77
|
-
|
|
78
|
-
后端默认行为(2026-05 起):**自动双音色** — 翻译时 LLM 给每个 cue 标 speaker A 或 B:
|
|
79
|
-
- **A = `Russian_CrazyQueen`**:反应方 / 提问 / 惊呼("等等!"/ "多少钱?"/ "天哪")
|
|
80
|
-
- **B = `Portuguese_FriendlyNeighbor`**:卖家 / 介绍 / 推销("正品"/ "玻璃材质"/ "今天下单送")
|
|
81
|
-
|
|
82
|
-
对话型视频(赵大大式带货)→ **双女声自然对话感**。单口播视频 → LLM 全标 A,自动退回单音色,**不会瞎切**。**SKILL 不询问音色**,直接 backend 自动判断。
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
## 默认走 backend 自动双音色,不需要在 Step 1 显式传 --voice
|
|
86
|
-
VOICE=""
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
#### 仅当用户**主动要求换音色**时,才列下面备选并等回复:
|
|
90
|
-
|
|
91
|
-
**用户想全用单一音色**(关掉双音色,例如"全用一个声音"/ "我不要双音色"):
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
SINGLE_VOICE_FLAG="--single-voice" ## Step 1 传给 gen video-translate
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**用户想指定具体音色**(例如"用男声"/ "换个更可爱的"):
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
你可以从这 6 个预置音色挑(传 --voice 后所有 cue 都用该 voice,关闭双音色):
|
|
101
|
-
1. Friendly - 友好邻居广谱女声(F) — 默认 B (卖家)
|
|
102
|
-
2. CrazyQueen - 充满活力 + 狂野(F) — 默认 A (反应方)
|
|
103
|
-
3. Sweet - 甜美年轻(F) — 美妆/母婴/温和卖货
|
|
104
|
-
4. Lovely - 可爱俏皮(F) — Z 世代/活泼带货
|
|
105
|
-
5. Trustworthy - 美式磁性沉稳男声(M) — 科技/汽车/正经 demo
|
|
106
|
-
6. Aussie - 澳式阳光男声(M) — 短视频/运动/快消
|
|
107
|
-
|
|
108
|
-
回复数字或名字。
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
| 用户回复 | VOICE 变量值 |
|
|
112
|
-
|---|---|
|
|
113
|
-
| `1` / `Friendly` | **`VOICE=Portuguese_FriendlyNeighbor`** |
|
|
114
|
-
| `2` / `CrazyQueen` / `狂野` | **`VOICE=Russian_CrazyQueen`** |
|
|
115
|
-
| `3` / `Sweet` / `甜美` | **`VOICE=Sweet_Girl`** |
|
|
116
|
-
| `4` / `Lovely` / `可爱` | **`VOICE=lovely_girl`** |
|
|
117
|
-
| `5` / `Trustworthy` / `磁性` / `沉稳` | **`VOICE=English_Trustworthy_Man`** |
|
|
118
|
-
| `6` / `Aussie` / `澳` / `阳光` | **`VOICE=English_Aussie_Bloke`** |
|
|
119
|
-
|
|
120
|
-
#### 严禁
|
|
121
|
-
|
|
122
|
-
- ❌ **主动问"你要哪个音色"** — 默认双音色够好,问了反而增加摩擦
|
|
123
|
-
- ❌ **用 AskUserQuestion 工具**(只有用户主动要求换才列文字菜单)
|
|
124
|
-
- ❌ **加 "克隆原声" 选项** — MiniMax 预置音色管线 v1 不做 voice clone
|
|
125
|
-
|
|
126
|
-
### Step 0.6:字幕风格 — 默认 `classic`(原款),**不主动问**
|
|
127
|
-
|
|
128
|
-
`render-ass` 支持 `--style`(需 `video-translate-tools` ≥ 1.0.9)。**默认不传 = `classic`,与原来的花体字幕完全一致**,默认流程零变化。
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
## 默认走原款,不显式传 --style
|
|
132
|
-
STYLE=""
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
#### 仅当用户**主动要求换字幕风格**时(如"字幕换个风格"/"字幕太单调"/"用 anton"),才列菜单并等回复:
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
字幕风格(回复数字或名字):
|
|
139
|
-
1. classic - 原款:白字黑边 + 粉色关键词(默认)
|
|
140
|
-
2. pop-soft - 立体软影:原款 + 柔和投影,更立体
|
|
141
|
-
3. pop-3d - 立体彩影:洋红 3D 硬投影 + 亮黄关键词,最潮
|
|
142
|
-
4. pop-hl - 关键词亮填:原款 + 关键词亮黄实填
|
|
143
|
-
5. anton - 条形:高条形粗体,现代利落
|
|
144
|
-
6. luckyguy - 圆润:圆润漫画体,活泼
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
| 用户回复 | STYLE |
|
|
148
|
-
|---|---|
|
|
149
|
-
| `1` / classic / 原款 | `STYLE=""`(=classic) |
|
|
150
|
-
| `2` / pop-soft / 立体软影 | `STYLE=pop-soft` |
|
|
151
|
-
| `3` / pop-3d / 立体彩影 / 潮 | `STYLE=pop-3d` |
|
|
152
|
-
| `4` / pop-hl / 关键词亮填 | `STYLE=pop-hl` |
|
|
153
|
-
| `5` / anton / 条形 | `STYLE=anton` |
|
|
154
|
-
| `6` / luckyguy / 圆润 | `STYLE=luckyguy` |
|
|
155
|
-
|
|
156
|
-
#### 严禁
|
|
157
|
-
|
|
158
|
-
- ❌ **主动问"你要哪个字幕风格"** — 默认 `classic` 够好,问了增加摩擦(同音色逻辑)
|
|
159
|
-
- ❌ **用 AskUserQuestion 工具**(只有用户主动要求换才列文字菜单)
|
|
160
|
-
- ❌ 把菜单外的名字直传 `--style` — render-ass 会 warn 回退 classic,应先在表里映射成合法值
|
|
161
|
-
|
|
162
|
-
> 风格只改配色/描边/阴影/关键词,**字体仍按语言兜底**(th=Sarabun、vi=Noto;anton 例外,越南语用 Anton)。所有风格对 4 语都安全,不会豆腐块。
|
|
163
|
-
|
|
164
|
-
### Step 1:翻译(用现成 CLI)
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
RAW_DIR="$WORK/raw"
|
|
168
|
-
mkdir -p "$RAW_DIR"
|
|
169
|
-
gen video-translate \
|
|
170
|
-
--video-url "$URL" \
|
|
171
|
-
--lang "$LANG" \
|
|
172
|
-
${VOICE:+--voice "$VOICE"} \
|
|
173
|
-
${SINGLE_VOICE_FLAG:-} \
|
|
174
|
-
-o "$RAW_DIR" \
|
|
175
|
-
> "$WORK/gen.json"
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
`gen video-translate` 自动轮询。**完成时间随 cue 数线性增长**(adapter 段间 1.5s sleep 防 RPM):
|
|
179
|
-
- < 30s 视频(~10 cues):2-3 min
|
|
180
|
-
- 1 min 视频(~20 cues):3-5 min
|
|
181
|
-
- 5 min 视频(~50 cues):**10-15 min**(注意 BullMQ worker slot 占用)
|
|
182
|
-
- 10 min+ 视频:**不推荐**,接近 30 min poll timeout 上限
|
|
183
|
-
|
|
184
|
-
失败 / 超时见末尾错误表。
|
|
185
|
-
|
|
186
|
-
幂等:`$WORK/gen.json` 存在则跳。
|
|
187
|
-
|
|
188
|
-
**⚠ 提取字段必须用 `.data.` 前缀**:gen-cli 的 `outputSuccess` 把所有字段包装在 `{success: true, data: {...}}` 结构里。**不带 `.data.` 永远读不到字段,SKILL 会卡死**:
|
|
189
|
-
|
|
190
|
-
```bash
|
|
191
|
-
## gen-cli 输出格式: {"success": true, "data": {"task_id": "...", "audio_url": "...", "caption_url": "..."}}
|
|
192
|
-
AUDIO_URL=$(jq -r '.data.audio_url // empty' "$WORK/gen.json")
|
|
193
|
-
CAP_URL=$(jq -r '.data.caption_url // empty' "$WORK/gen.json")
|
|
194
|
-
[ -n "$AUDIO_URL" ] && [ -n "$CAP_URL" ] || {
|
|
195
|
-
echo "ERR: gen.json 缺 .data.audio_url 或 .data.caption_url 字段。原始 response:"
|
|
196
|
-
cat "$WORK/gen.json"
|
|
197
|
-
exit 1
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Step 2:下载译音 + SRT,渲染 ASS
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
AUDIO="$WORK/translated_audio.wav"
|
|
205
|
-
[ -s "$AUDIO" ] || curl -sSL --retry 1 "$AUDIO_URL" -o "$AUDIO"
|
|
206
|
-
|
|
207
|
-
SRT="$WORK/caption.srt"
|
|
208
|
-
[ -s "$SRT" ] || curl -sSL --retry 1 "$CAP_URL" -o "$SRT"
|
|
209
|
-
|
|
210
|
-
## SRT 必须非空 — 后端偶发返回 0 字节 SRT(Whisper 无语音 / 上传失败),后续 sync 检查会拿到空 end_time
|
|
211
|
-
[ -s "$SRT" ] || { echo "ERR: $SRT 空或损坏,gen.json 内 caption_url 可能已 expire,重跑 Step 1"; exit 1; }
|
|
212
|
-
|
|
213
|
-
ASS="$WORK/subs.ass"
|
|
214
|
-
TRANS="$WORK/translations.json"
|
|
215
|
-
video-translate render-ass \
|
|
216
|
-
--srt "$SRT" \
|
|
217
|
-
--lang "$TAG" \
|
|
218
|
-
${STYLE:+--style "$STYLE"} \
|
|
219
|
-
--translations "$TRANS" \
|
|
220
|
-
--out "$ASS"
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
用户可手编 `$TRANS`(给关键词加 `**word**` 触发粉色 KW 描边)后删 `$ASS` 重跑这步。
|
|
224
|
-
|
|
225
|
-
### Step 2.5:A/V sync 预检(必跑,挡掉调试不收敛的常见根因)
|
|
226
|
-
|
|
227
|
-
**为什么必跑**:用户反馈"字幕和配音调试多次不同频"。绝大多数情况不是字幕样式问题,而是后端 audio 时长跟 SRT 末尾时间戳本身就对不上(MiniMax adaptive-speed 调整不完美时会有 < 1s 累积漂移),或者跟原视频时长差距过大。**不在 mux 前查,用户只能盲调,改 10 次还是错。**
|
|
228
|
-
|
|
229
|
-
```bash
|
|
230
|
-
## 原视频本地化(Step 3 也需要,提前到这里让 sync 预检能算 VIDEO_DUR)
|
|
231
|
-
ORIG_VIDEO="$URL"
|
|
232
|
-
if [[ "$URL" =~ ^https?:// ]]; then
|
|
233
|
-
ORIG_VIDEO="$WORK/orig.mp4"
|
|
234
|
-
[ -f "$ORIG_VIDEO" ] || curl -sSL "$URL" -o "$ORIG_VIDEO"
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
## audio 实际时长(秒,保留 3 位小数)
|
|
238
|
-
AUDIO_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$AUDIO")
|
|
239
|
-
|
|
240
|
-
## SRT 最后一行 cue 的 end 时间(秒)
|
|
241
|
-
SRT_END=$(awk '
|
|
242
|
-
/-->/ { t=$3; gsub(",", ".", t); split(t, p, ":"); end = p[1]*3600 + p[2]*60 + p[3] }
|
|
243
|
-
END { print end }
|
|
244
|
-
' "$SRT")
|
|
245
|
-
|
|
246
|
-
## 原视频时长(用于检测 audio 超出/不足)
|
|
247
|
-
VIDEO_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$ORIG_VIDEO" 2>/dev/null || echo "0")
|
|
248
|
-
|
|
249
|
-
printf "AV-sync: audio=%.3fs srt_end=%.3fs video=%.3fs\n" "$AUDIO_DUR" "$SRT_END" "$VIDEO_DUR"
|
|
250
|
-
|
|
251
|
-
## 1. audio vs SRT — 这是字幕看起来 "晚" 或 "早" 收尾的直接原因
|
|
252
|
-
DELTA_AS=$(awk -v a="$AUDIO_DUR" -v s="$SRT_END" 'BEGIN { d=a-s; if (d<0) d=-d; print d }')
|
|
253
|
-
if awk "BEGIN { exit !($DELTA_AS > 0.5) }"; then
|
|
254
|
-
echo "ERR: audio 与 SRT 末时间差 ${DELTA_AS}s (>0.5s)。"
|
|
255
|
-
echo " 根因多半是后端本次输出 audio/SRT 不同步,继续 mux 字幕会全段漂。"
|
|
256
|
-
echo " remediation: rm $WORK/gen.json $AUDIO $SRT && 重跑 Step 1 拿新一对 audio+SRT。"
|
|
257
|
-
exit 1
|
|
258
|
-
fi
|
|
259
|
-
|
|
260
|
-
## 2. audio vs video — 字幕和声音一致但跟画面不齐
|
|
261
|
-
if [ "$VIDEO_DUR" != "0" ]; then
|
|
262
|
-
DELTA_AV=$(awk -v a="$AUDIO_DUR" -v v="$VIDEO_DUR" 'BEGIN { d=a-v; if (d<0) d=-d; print d }')
|
|
263
|
-
if awk "BEGIN { exit !($DELTA_AV > 1.0) }"; then
|
|
264
|
-
echo "INFO: audio 与原视频时长差 ${DELTA_AV}s。mux 会按 --raw 时长裁/补,字幕跟音同步但画面可能错位。"
|
|
265
|
-
fi
|
|
266
|
-
fi
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**触发条件 + 决策**:
|
|
270
|
-
- `DELTA_AS > 0.5s` → **默认停**,提示用户重跑 Step 1。盲跑 mux 然后再调字幕样式没用,根因在后端 TTS 输出。
|
|
271
|
-
- `DELTA_AV > 1.0s` → 只是 info,不挡;但要在 Step 3 出片汇报里如实告诉用户。
|
|
272
|
-
- 两条都 ≤ 阈值 → 直通 Step 3。
|
|
273
|
-
|
|
274
|
-
幂等:这个 block 没有副作用,可以反复跑。
|
|
275
|
-
|
|
276
|
-
### Step 3:Mux 出片
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
FINAL="./videos/${NAME}_${TAG}.mp4"
|
|
280
|
-
|
|
281
|
-
## --orig-video 关键:用用户原视频画面(MiniMax pipeline 不生成新视频,只换音轨,这里把原画面+新音轨合一)
|
|
282
|
-
## ORIG_VIDEO 通常已在 Step 2.5 落到本地;这里幂等兜底,允许只想换字幕样式时跳着复跑 Step 3
|
|
283
|
-
ORIG_VIDEO="${ORIG_VIDEO:-$URL}"
|
|
284
|
-
if [[ "$ORIG_VIDEO" =~ ^https?:// ]]; then
|
|
285
|
-
ORIG_VIDEO="$WORK/orig.mp4"
|
|
286
|
-
[ -f "$ORIG_VIDEO" ] || curl -sSL "$URL" -o "$ORIG_VIDEO"
|
|
287
|
-
fi
|
|
288
|
-
|
|
289
|
-
video-translate mux \
|
|
290
|
-
--raw "$AUDIO" \
|
|
291
|
-
--orig-video "$ORIG_VIDEO" \
|
|
292
|
-
--ass "$ASS" \
|
|
293
|
-
${BGM:+--bgm "$BGM"} \
|
|
294
|
-
${NO_BGM:+--no-bgm} \
|
|
295
|
-
--work "$WORK" \
|
|
296
|
-
--out "$FINAL"
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**关键**:管线匹配本地水杯/zhaodada 批量出片行为:
|
|
300
|
-
- `--orig-video` = 用户原视频画面(MiniMax 不动画面,只换音轨)
|
|
301
|
-
- `--raw` = 后端译音(audio_url 下载的 wav)
|
|
302
|
-
- 字幕烧 + BGM ducking 走 mux 内置
|
|
303
|
-
|
|
304
|
-
**BGM + 花体字幕样式 = 默认产出的一部分,不是可选项。** 不要问用户"要不要加",不要在结尾说"如需 BGM/字幕样式可补充"。`video-translate mux` 默认就用 `bgm/default.mp3` + Path-B 描边样式;`render-ass --lang $TAG` 默认按语言选字体(Bangers / Sarabun / Noto Sans)。不要绕过 `mux` 自己手写 ffmpeg(会丢 BGM + 字幕样式 + 音轨规范化)。
|
|
305
|
-
|
|
306
|
-
如果 `mux` 真的失败,先把完整 stderr 给用户看(不要默默用 `ffmpeg -i ... -c copy` 兜底出"裸版"),让用户决定下一步。
|
|
307
|
-
|
|
308
|
-
## 出片汇报口径
|
|
309
|
-
|
|
310
|
-
只描述事实("xxx_th.mp4 翻译完成,泰语花体字幕 + BGM ducking 已烧录")。**禁止**任何形式的"如需 X 可以告诉我"尾巴 — BGM 和字幕样式不是可选 add-on,这种话术暗示没做。
|
|
311
|
-
|
|
312
|
-
### Step 4(可选):清理
|
|
313
|
-
|
|
314
|
-
```bash
|
|
315
|
-
## 出片成功且用户确认满意后,删中间产物释放空间
|
|
316
|
-
rm -rf "$WORK"
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## 批量模式(多语种,一段视频翻多语)
|
|
322
|
-
|
|
323
|
-
**触发条件**:用户一次请求 ≥2 种目标语言("翻译成英/泰/越/马 4 国语")。
|
|
324
|
-
|
|
325
|
-
**不要按单语流程循环跑 N 次**——会有两个致命问题:
|
|
326
|
-
1. wall time = N × 单次 (~5min/lang × 4 = 20min)
|
|
327
|
-
2. chat bash 队列被 N 条阻塞命令塞满,后续命令(mux/SFX)全卡"准备中"
|
|
328
|
-
|
|
329
|
-
改用 **单条 bash 内 N 路 subshell 并发**:chat 队列只占 1 slot。
|
|
330
|
-
|
|
331
|
-
> ⚠ **MiniMax RPM 限速注意**:MiniMax 新账户 RPM 默认 1-2,4 路并发会触发后端段间 retry,实际 wall time 可能并不省 vs 串行。如果发现 batch 比单语 ×4 还慢,考虑升级 MiniMax tier 或改回串行。Phase 1 默认仍走并发(代码路径已验证)。
|
|
332
|
-
|
|
333
|
-
### Step B0:URL 预处理 + workspace + 音量预检 + 源视频下载(只跑一次)
|
|
334
|
-
|
|
335
|
-
```bash
|
|
336
|
-
## ⚠ URL 预处理:同单语 Step 0,本地路径必须先上传拿 https URL
|
|
337
|
-
if [[ ! "$URL" =~ ^https?:// ]]; then
|
|
338
|
-
TOKEN=$(jq -r '.access_token' ~/.optima/token.json)
|
|
339
|
-
SIGNED=$(curl -sS -X POST "https://shell.optima.onl/api/files/sign" \
|
|
340
|
-
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
341
|
-
-d "{\"path\":\"$URL\"}" 2>&1 | jq -r '.data.url // .url // empty' 2>/dev/null)
|
|
342
|
-
if [ -z "$SIGNED" ]; then
|
|
343
|
-
echo "ERR: 拿不到 signed URL for '$URL'"; exit 1
|
|
344
|
-
fi
|
|
345
|
-
URL="$SIGNED"
|
|
346
|
-
fi
|
|
347
|
-
|
|
348
|
-
NAME="${NAME:-$(echo "$URL" | cut -d'?' -f1 | xargs basename | sed 's/\.[^.]*$//' | sed 's/[^A-Za-z0-9_-]/_/g')}"
|
|
349
|
-
mkdir -p "./videos/${NAME}.batch"
|
|
350
|
-
|
|
351
|
-
## 音量预检(同单语 Step 0)
|
|
352
|
-
ffmpeg -i "$URL" -af volumedetect -f null - 2>&1 | grep mean_volume
|
|
353
|
-
|
|
354
|
-
## 源视频下载一次,所有 mux 共用(避免 N 次重复下载)
|
|
355
|
-
ORIG_VIDEO="./videos/${NAME}.batch/orig.mp4"
|
|
356
|
-
[ -f "$ORIG_VIDEO" ] || curl -sSL "$URL" -o "$ORIG_VIDEO"
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### Step B0.5:voice 默认 backend 预置,不问
|
|
360
|
-
|
|
361
|
-
同单语 Step 0.5,默认 `VOICE=""` 走后端 `Portuguese_FriendlyNeighbor`。仅当用户主动要求换音色才列菜单 + 设 `VOICE`。所有语种 subshell 共用同一个 `VOICE`。
|
|
362
|
-
|
|
363
|
-
字幕风格同理(Step 0.6):默认 `STYLE=""`(=classic 原款),仅用户主动要求才设;所有语种共用同一个 `STYLE`,已在下方 per-lang 模板的 `render-ass` 里以 `${STYLE:+--style "$STYLE"}` 透传。
|
|
364
|
-
|
|
365
|
-
### Step B1:并发派出所有翻译(后台跑,~5 秒返回)
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
## 用户实际要的语种,从下面 4 个里选(不要的注释掉)
|
|
369
|
-
LANG_LIST=(
|
|
370
|
-
"English:en"
|
|
371
|
-
"Thai (Thailand):th"
|
|
372
|
-
"Malay (Malaysia):ms"
|
|
373
|
-
"Vietnamese (Vietnam):vi"
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
for entry in "${LANG_LIST[@]}"; do
|
|
377
|
-
LANG="${entry%:*}"
|
|
378
|
-
TAG="${entry##*:}"
|
|
379
|
-
WORK="./videos/${NAME}_${TAG}.work"
|
|
380
|
-
mkdir -p "$WORK/raw"
|
|
381
|
-
|
|
382
|
-
## 后台启动 gen video-translate,各自写 gen.json,bash 立即继续
|
|
383
|
-
nohup gen video-translate \
|
|
384
|
-
--video-url "$URL" --lang "$LANG" \
|
|
385
|
-
${VOICE:+--voice "$VOICE"} \
|
|
386
|
-
-o "$WORK/raw" > "$WORK/gen.json" 2>&1 &
|
|
387
|
-
|
|
388
|
-
echo "$TAG: launched (PID $!) -> $WORK/gen.json"
|
|
389
|
-
done
|
|
390
|
-
echo "=== all submitted, returning immediately ==="
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
这一步 5 秒返回。**关键**:用 `nohup ... &` 后台启动,bash exit 后进程继续(尽量 survive 短暂 idle / 容器升级)。每个进程会在 MiniMax pipeline 完成后把 audio_url/caption_url 写入对应的 gen.json。
|
|
394
|
-
|
|
395
|
-
### Step B2-B5:逐语种等结果 → 出片(每个一条独立 bash)
|
|
396
|
-
|
|
397
|
-
**重点:每个语种用单独的 bash 命令处理,不要再用 `&` 并发**。这样:
|
|
398
|
-
- chat 看到每个语种独立完成、独立报告
|
|
399
|
-
- 第 1 个 bash 等翻译(~5-8 min),后续每个 bash 几乎瞬间完成(因为 4 个翻译是并行跑的,后续几个早已 done)
|
|
400
|
-
- 失败隔离:1 个失败不影响其他
|
|
401
|
-
|
|
402
|
-
通用 per-lang 模板(把 `$TAG` 换成 en / th / ms / vi 各跑一遍):
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
TAG=en ## ← 换语种就改这个
|
|
406
|
-
WORK="./videos/${NAME}_${TAG}.work"
|
|
407
|
-
FINAL="./videos/${NAME}_${TAG}.mp4"
|
|
408
|
-
|
|
409
|
-
## 等 gen.json 写完(或失败)
|
|
410
|
-
## ⚠ gen-cli 用 outputSuccess 包装在 {success, data: {...}} 里,字段在 .data.* 下
|
|
411
|
-
DEADLINE=$(( $(date +%s) + 1800 )) ## 30min 上限,跟后端 poll timeout 对齐
|
|
412
|
-
while true; do
|
|
413
|
-
## Case 1: 成功 → .data.audio_url 非 null
|
|
414
|
-
if jq -e '.data.audio_url' "$WORK/gen.json" >/dev/null 2>&1; then
|
|
415
|
-
break
|
|
416
|
-
fi
|
|
417
|
-
## Case 2: 业务失败 → .data.error_message(outputSuccess with status=failed)
|
|
418
|
-
if jq -e '.data.error_message' "$WORK/gen.json" >/dev/null 2>&1; then
|
|
419
|
-
echo "[$TAG] FAIL: $(jq -r '.data.error_message' "$WORK/gen.json")"
|
|
420
|
-
exit 1
|
|
421
|
-
fi
|
|
422
|
-
## Case 3: CLI 异常 → .error.code/.error.message(outputError 抛出)
|
|
423
|
-
if jq -e '.error' "$WORK/gen.json" >/dev/null 2>&1; then
|
|
424
|
-
echo "[$TAG] FAIL: $(jq -r '.error.code + \": \" + .error.message' "$WORK/gen.json")"
|
|
425
|
-
exit 1
|
|
426
|
-
fi
|
|
427
|
-
## Case 4: 30min 超时(MiniMax pipeline 一般 3-8min,30min 还没就是真挂了)
|
|
428
|
-
if [ "$(date +%s)" -gt "$DEADLINE" ]; then
|
|
429
|
-
echo "[$TAG] TIMEOUT 30min,gen.json 内容:"; cat "$WORK/gen.json"; exit 1
|
|
430
|
-
fi
|
|
431
|
-
sleep 15
|
|
432
|
-
done
|
|
433
|
-
|
|
434
|
-
## 拿到 audio_url + caption_url(必须带 .data. 前缀)
|
|
435
|
-
AUDIO_URL=$(jq -r '.data.audio_url' "$WORK/gen.json")
|
|
436
|
-
CAP_URL=$(jq -r '.data.caption_url' "$WORK/gen.json")
|
|
437
|
-
|
|
438
|
-
## 下载 + render-ass
|
|
439
|
-
curl -sSL --retry 1 "$AUDIO_URL" -o "$WORK/translated_audio.wav"
|
|
440
|
-
curl -sSL --retry 1 "$CAP_URL" -o "$WORK/caption.srt"
|
|
441
|
-
[ -s "$WORK/caption.srt" ] || { echo "[$TAG] ERR: SRT 空/损坏"; exit 1; }
|
|
442
|
-
video-translate render-ass \
|
|
443
|
-
--srt "$WORK/caption.srt" --lang "$TAG" \
|
|
444
|
-
${STYLE:+--style "$STYLE"} \
|
|
445
|
-
--translations "$WORK/translations.json" \
|
|
446
|
-
--out "$WORK/subs.ass"
|
|
447
|
-
|
|
448
|
-
## A/V sync 预检(同单语 Step 2.5,挡后端偶发 audio/SRT 不同步)
|
|
449
|
-
AUDIO_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$WORK/translated_audio.wav")
|
|
450
|
-
SRT_END=$(awk '
|
|
451
|
-
/-->/ { t=$3; gsub(",", ".", t); split(t, p, ":"); end = p[1]*3600 + p[2]*60 + p[3] }
|
|
452
|
-
END { print end }
|
|
453
|
-
' "$WORK/caption.srt")
|
|
454
|
-
DELTA_AS=$(awk -v a="$AUDIO_DUR" -v s="$SRT_END" 'BEGIN { d=a-s; if (d<0) d=-d; print d }')
|
|
455
|
-
printf "[%s] AV-sync: audio=%.3fs srt_end=%.3fs delta=%.3fs\n" "$TAG" "$AUDIO_DUR" "$SRT_END" "$DELTA_AS"
|
|
456
|
-
if awk "BEGIN { exit !($DELTA_AS > 0.5) }"; then
|
|
457
|
-
echo "[$TAG] WARN: audio/SRT 时间差 >0.5s,跳过本语 mux。删 $WORK/gen.json 重跑 Step B1 该 lang。"
|
|
458
|
-
exit 1
|
|
459
|
-
fi
|
|
460
|
-
|
|
461
|
-
## mux 出片
|
|
462
|
-
video-translate mux \
|
|
463
|
-
--raw "$WORK/translated_audio.wav" \
|
|
464
|
-
--orig-video "$ORIG_VIDEO" \
|
|
465
|
-
--ass "$WORK/subs.ass" \
|
|
466
|
-
${BGM:+--bgm "$BGM"} ${NO_BGM:+--no-bgm} \
|
|
467
|
-
--work "$WORK" \
|
|
468
|
-
--out "$FINAL"
|
|
469
|
-
|
|
470
|
-
echo "[$TAG] DONE -> $FINAL"
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
**Agent 执行节奏**:
|
|
474
|
-
1. 跑 Step B1(1 条 bash,5 秒返回,**立刻告知用户"4 个翻译派出去了"**)
|
|
475
|
-
2. 跑 per-lang 模板 `TAG=en`(1 条 bash,~12 min,出片后**立刻告知用户"English 出片 ✓"**)
|
|
476
|
-
3. 跑 per-lang 模板 `TAG=th`(1 条 bash,~30 秒,出片后**立刻告知用户"Thai 出片 ✓"**)
|
|
477
|
-
4. 跑 per-lang 模板 `TAG=ms`(~30 秒)
|
|
478
|
-
5. 跑 per-lang 模板 `TAG=vi`(~30 秒)
|
|
479
|
-
|
|
480
|
-
**总 wall time 不变(~13 min)**,但用户每 30 秒看到一个出片,不是 12 min 黑盒后突然 4 个一起出。
|
|
481
|
-
|
|
482
|
-
输出:`./videos/${NAME}_en.mp4` / `_th.mp4` / `_ms.mp4` / `_vi.mp4`
|
|
483
|
-
|
|
484
|
-
### 单语 vs 渐进批量对比
|
|
485
|
-
|
|
486
|
-
| | 单语循环(❌ 不要) | 渐进批量(✓) |
|
|
487
|
-
|---|---|---|
|
|
488
|
-
| 源视频下载 | N 次重下 | 1 次共用 |
|
|
489
|
-
| 翻译调用 | 串行 N × 5min | 并发(受 MiniMax RPM 限速影响) |
|
|
490
|
-
| chat 队列占 | N 条阻塞 bash | 1 条 submit + N 条快速 poll |
|
|
491
|
-
| 失败隔离 | 一个挂全停 | per-lang subshell + bash 独立 |
|
|
492
|
-
| 用户感知 | 黑盒 ~20 min | 第 1 个 ~5-8 min 出,后续每 ~30 秒 1 个 |
|
|
493
|
-
| **wall time(N=4)** | **~20 min** | **~8-12 min**(实际看 MiniMax tier) |
|
|
494
|
-
|
|
495
|
-
### 失败处理
|
|
496
|
-
|
|
497
|
-
- 单 lang 失败:per-lang bash `exit 1`,agent 报告该语种失败但**继续跑下一个语种的 bash**
|
|
498
|
-
- 用户对失败的单 lang re-run → 删 `$WORK/gen.json` 重跑 Step B1 单语 + per-lang 模板即可
|
|
499
|
-
- MiniMax 余额不足 / RPM 限速持续失败 → 当 lang 个失败处理,不阻塞其他 lang
|
|
500
|
-
|
|
501
|
-
## 错误处理
|
|
502
|
-
|
|
503
|
-
| 失败 | 处理 |
|
|
504
|
-
|---|---|
|
|
505
|
-
| URL 不通 | 重传或上传到 Optima 拿新 URL,不消耗翻译服务 credits |
|
|
506
|
-
| 源视频音量过低 | 让用户先用 ffmpeg `volume=20dB,acompressor` 放大后重传 |
|
|
507
|
-
| 后端 `status: failed` 包含 "no speaker" / "0 segments" | 源视频音量过低,同上处理 |
|
|
508
|
-
| MiniMax `1008 insufficient balance` | MiniMax 账户余额不足,运维充值后重跑 |
|
|
509
|
-
| MiniMax `1002 rate limit exceeded` 持续失败 | RPM tier 不够;后端有指数退避兜底,长视频可能超时 → 联系运维升级 tier |
|
|
510
|
-
| 后端其他失败 | 透出 task_id,提示用 `gen task get <id>` 查最新;干净退出 |
|
|
511
|
-
| 翻译 >30min 超时 | 同上,可能任务还在 running |
|
|
512
|
-
| `gen.json` 缺 `.audio_url` / `.caption_url` | gen 后端契约可能改字段名。打 `cat $WORK/gen.json` 看实际字段 |
|
|
513
|
-
| `curl <caption_url>` 失败 | S3 presigned URL 24 小时 expire。retry 1 次后仍失败 → 重跑 step 1 |
|
|
514
|
-
| `video-translate render-ass` SRT 解析失败 | 显示 SRT 头 20 行,不重试 |
|
|
515
|
-
| `video-translate mux` 字体 ☐ | 检查 `fc-match Bangers / Sarabun / "Noto Sans"` 是否精确返回 |
|
|
516
|
-
| Step 2.5 报 "audio 与 SRT 末时间差 > 0.5s" | MiniMax adaptive-speed 本次大幅偏离,**字幕调样式没用**。删 `$WORK/gen.json $AUDIO $SRT`,重跑 Step 1 |
|
|
517
|
-
|
|
518
|
-
## 不做
|
|
519
|
-
|
|
520
|
-
- ❌ 性别自动检测(默认 `Portuguese_FriendlyNeighbor` 女声,用户想换主动说)
|
|
521
|
-
- ❌ 硬字幕(burnt-in)抹除 — 源视频有的话出片会双语
|
|
522
|
-
- ❌ 自动加粗关键词 — SRT 直入无粉色,用户手编 translations.json
|
|
523
|
-
- ❌ 双说话人差异化音色 — MiniMax v1 单音色 default,手动传 speakers JSON 才能分轨(SKILL v1 不暴露)
|
|
524
|
-
- ❌ 克隆原说话人音色 — MiniMax voice clone API 是独立流程,v1 不上
|
|
525
|
-
- ❌ 一次多语言 — 一次一种语言
|
|
526
|
-
|
|
527
|
-
## 参考
|
|
528
|
-
|
|
529
|
-
- 设计 SPEC:https://github.com/Optima-Chat/video-translate/blob/main/SPEC.md (v3.1)
|
|
530
|
-
- 实测踩坑(gotchas):同上 §Gotchas
|
|
531
|
-
|
|
532
|
-
---
|
|
533
|
-
|
|
534
|
-
## Voice Catalog
|
|
535
|
-
|
|
536
|
-
下面 6 个 voice 是 MiniMax 预置音色,**任选一个,所有支持语言(en/th/ms/vi)都能说**(底层 `speech-02-turbo` + `language_boost` 跨语自适应)。POC 验证(2026-05)在卖货场景下跨 4 语均自然,Whisper 反向验证语种识别 > 90%。
|
|
537
|
-
|
|
538
|
-
| # | voice_id | 风格定位 | 适用场景 |
|
|
539
|
-
|---|---|---|---|
|
|
540
|
-
| 1 | `Portuguese_FriendlyNeighbor` | 充满活力的友好邻居(F) | **默认** — 销售 / 教程 / demo,广谱适用 |
|
|
541
|
-
| 2 | `Russian_CrazyQueen` | 充满活力 + 狂野不可预测(F) | 反应方 / 惊呼 / 高能开场 |
|
|
542
|
-
| 3 | `Sweet_Girl` | 甜美年轻(F) | 美妆 / 母婴 / 温和卖货 |
|
|
543
|
-
| 4 | `lovely_girl` | 可爱俏皮(F) | Z 世代 / 活泼带货 |
|
|
544
|
-
| 5 | `English_Trustworthy_Man` | 美式磁性沉稳(M,带通用美式口音) | 科技 / 汽车 / 严肃产品 demo |
|
|
545
|
-
| 6 | `English_Aussie_Bloke` | 阳光开朗(M,澳式口音) | 短视频 / 运动 / 快消 |
|
|
546
|
-
|
|
547
|
-
> 维护说明:从 MiniMax `POST /v1/get_voice` 接口可拉全量 303 个预置音色。挑跨语 voice 优先 `English_` / `Russian_` / `Portuguese_` / `Sweet_` / `lovely_` / `Indonesian_` 前缀(描述含 "活力 / 甜美 / 俏皮 / Trustworthy / Bloke" 关键词的);其他 `Chinese_` / `Korean_` / `Japanese_` 前缀的强单语音色硬说外语会有明显口音。试新 voice 必须先用 `text="测试一句"` × 4 语单独调一次听感再批量上(参考 [[feedback_api_accept_neq_use]]:API 接受 ≠ 输出可用)。
|