@optima-chat/gen-cli 2.2.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 (49) 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/dist/commands/doctor.d.ts +15 -0
  35. package/dist/commands/doctor.d.ts.map +1 -1
  36. package/dist/commands/doctor.js +74 -11
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/tts.d.ts.map +1 -1
  39. package/dist/commands/tts.js +19 -4
  40. package/dist/commands/tts.js.map +1 -1
  41. package/dist/commands/voice.d.ts +13 -0
  42. package/dist/commands/voice.d.ts.map +1 -1
  43. package/dist/commands/voice.js +280 -0
  44. package/dist/commands/voice.js.map +1 -1
  45. package/dist/services/generation-api.d.ts +16 -2
  46. package/dist/services/generation-api.d.ts.map +1 -1
  47. package/dist/services/generation-api.js +6 -0
  48. package/dist/services/generation-api.js.map +1 -1
  49. package/package.json +3 -2
@@ -7,7 +7,7 @@ owner_repo: Optima-Chat/optima-gen
7
7
 
8
8
  # video-translate
9
9
 
10
- 把口播视频本地化:HeyGen 自动克隆原说话人译音 + 烧录目标语字幕 + BGM ducking(默认开)。**3 步主流程**(+ Step 0 预检 + Step 4 可选清理)。
10
+ 把口播视频本地化:MiniMax 预置明亮女声译音 + 烧录目标语字幕 + BGM ducking(默认开)。**3 步主流程**(+ Step 0 预检 + Step 4 可选清理)。
11
11
 
12
12
  ## 适用语言
13
13
 
@@ -21,20 +21,21 @@ owner_repo: Optima-Chat/optima-gen
21
21
  ## 前提
22
22
 
23
23
  - 源视频 URL 公网可访问(或先让用户上传到 Optima → 拿 URL)
24
- - 容器有:`gen` CLI(`@optima-chat/optima-gen` ≥ latest)、`video-translate` CLI(`@optima-chat/video-translate-tools` ≥ 1.0.5)、`ffmpeg`、`curl`、`jq`
25
- - gen video-translate `--mode fast` / `--dynamic-duration` flag 在 latest 版本已存在;若 CLI 报 unknown flag → `npm i -g @optima-chat/optima-gen@latest` 升级
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
26
 
27
27
  ## 输入
28
28
 
29
29
  | 参数 | 必填 | 说明 |
30
30
  |---|---|---|
31
31
  | `URL` | ✅ | 公网可访问的源视频 URL |
32
- | `LANG` | ✅ | HeyGen 人类可读名(见上面 4 语) |
32
+ | `LANG` | ✅ | 人类可读语言名(见上面 4 语) |
33
33
  | `TAG` | ✅ | 对应的两字母 tag(`en` / `th` / `ms` / `vi`) |
34
34
  | `BGM` | ⬜ | 自定义 BGM 文件路径(覆盖默认)。不传则用 npm 包内置 `bgm/default.mp3`(22s clean instrumental,自动 loop)|
35
35
  | `NO_BGM` | ⬜ | 设非空值则跳过 BGM(出片只有人声 + 原视频 BGM 残留)|
36
- | `VOICE` | ⬜ | HeyGen stock voice_id(从下面 ## Voice Catalog 选)。**不传 = 克隆源说话人音色**(legacy 默认行为)|
36
+ | `VOICE` | ⬜ | MiniMax voice_id(从下面 ## Voice Catalog 选)。**不传 = `Portuguese_FriendlyNeighbor`** 默认(友好邻居女声,广谱适用——卖货 / 教程 / demo 都不违和)|
37
37
  | `NAME` | ⬜ | 工作区名,默认从 URL 末段推 |
38
+ | `STYLE` | ⬜ | 字幕风格,**不传 = `classic`(原款)**。可选 `pop-soft` / `pop-3d` / `pop-hl` / `anton` / `luckyguy`,见 Step 0.6。需 `video-translate-tools` ≥ 1.0.9 |
38
39
 
39
40
  ## 3 步主流程(+ Step 0 预检 + Step 4 可选清理)
40
41
 
@@ -42,7 +43,7 @@ owner_repo: Optima-Chat/optima-gen
42
43
 
43
44
  ```bash
44
45
  ## ⚠ URL 必须是公网 https URL,不能是本地路径 /home/aiuser/...
45
- ## gen video-translate 只接受 https URL(HeyGen 那边要拉的)
46
+ ## gen video-translate 只接受 https URL(后端要下载源视频)
46
47
  ## 如果是本地路径,先用 chat 系统的 file API 签 URL
47
48
  if [[ ! "$URL" =~ ^https?:// ]]; then
48
49
  echo "INFO: 本地路径 '$URL',需要上传拿 https URL"
@@ -60,55 +61,107 @@ if [[ ! "$URL" =~ ^https?:// ]]; then
60
61
  echo "URL → $URL"
61
62
  fi
62
63
 
63
- ## URL 末段可能含 query string(HeyGen / S3 预签名 url 含 token)— 先去掉再 basename
64
+ ## URL 末段可能含 query string(S3 预签名 url 含 token)— 先去掉再 basename
64
65
  NAME="${NAME:-$(echo "$URL" | cut -d'?' -f1 | xargs basename | sed 's/\.[^.]*$//' | sed 's/[^A-Za-z0-9_-]/_/g')}"
65
66
  WORK="./videos/${NAME}.work"
66
67
  mkdir -p "$WORK"
67
68
 
68
- ## 音量预检 — HeyGen 在 < -25dB 会报 "No speaker detected"
69
+ ## 音量预检 — Whisper ASR 在 < -25dB 会返回 0 segments(adapter 抛 "source audio too quiet (<-25dB)" 错误)
69
70
  ## ffmpeg 输出在 stderr,必须 2>&1
70
71
  ffmpeg -i "$URL" -af volumedetect -f null - 2>&1 | grep mean_volume
71
72
  ```
72
73
 
73
74
  如果 `mean_volume < -25dB`,提示用户:"源视频音量太低(< −25dB),翻译服务大概率会报无人声。建议先用 `ffmpeg -i in.mp4 -af 'volume=20dB,acompressor=threshold=-20dB:ratio=4' -c:v copy out.mp4` 放大后再翻译。"
74
75
 
75
- ### Step 0.5:让用户挑音色纯文本对话(不要用 AskUserQuestion)
76
+ ### Step 0.5:音色默认**自动双音色**(LLM 标注 A/B),**不问用户**
76
77
 
77
- **重要:不要用 AskUserQuestion 工具**(只支持 4 选项 + label-based 反查不可靠,试过失败)。**用纯文本输出 5 个选项,等用户文字回复**:
78
+ 后端默认行为(2026-05 起):**自动双音色** 翻译时 LLM 给每个 cue speaker A 或 B:
79
+ - **A = `Russian_CrazyQueen`**:反应方 / 提问 / 惊呼("等等!"/ "多少钱?"/ "天哪")
80
+ - **B = `Portuguese_FriendlyNeighbor`**:卖家 / 介绍 / 推销("正品"/ "玻璃材质"/ "今天下单送")
78
81
 
79
- agent 输出(逐字照搬,把 $LANG 换成实际目标语):
82
+ 对话型视频(赵大大式带货)→ **双女声自然对话感**。单口播视频 → LLM 全标 A,自动退回单音色,**不会瞎切**。**SKILL 不询问音色**,直接 backend 自动判断。
80
83
 
84
+ ```bash
85
+ ## 默认走 backend 自动双音色,不需要在 Step 1 显式传 --voice
86
+ VOICE=""
81
87
  ```
82
- 翻译到 $LANG,4 个语种共用一个音色。请回复编号(1-5)或名字:
83
88
 
84
- 1. Connie - 沉稳专业旁白(F) — preview: https://resource.heygen.ai/text_to_speech/locale=en-USmodel=eleven_multilingual_v2id=9FnNGNtwCeU9fyf6mFfDp8.mp3
85
- 2. Sophie - 温柔友好(F) — preview: https://resource.heygen.ai/text_to_speech/locale=model=eleven_multilingual_v2id=kte4EzDuRTnsnHkATe6tDK.mp3
86
- 3. Bruce - 中年浑厚(M) — preview: https://resource.heygen.ai/text_to_speech/locale=model=eleven_multilingual_v2id=2SdnapPUN7wvtCbkPSgdHV.mp3
87
- 4. Luca - 年轻活力(M) — preview: https://resource.heygen.ai/text_to_speech/locale=model=eleven_multilingual_v2id=FVKYscu8J8EVReBuZdPXnJ.mp3
88
- 5. 保留原声(克隆源说话人音色)
89
+ #### 仅当用户**主动要求换音色**时,才列下面备选并等回复:
90
+
91
+ **用户想全用单一音色**(关掉双音色,例如"全用一个声音"/ "我不要双音色"):
89
92
 
90
- 回复方式:数字 1-5,或者 "Connie"/"Sophie"/"Bruce"/"Luca"/"原声",或者 "随便"(我帮你随机挑)。
93
+ ```bash
94
+ SINGLE_VOICE_FLAG="--single-voice" ## Step 1 传给 gen video-translate
91
95
  ```
92
96
 
93
- #### 用户回复 → VOICE 变量(严格按下表)
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
+ ```
94
110
 
95
111
  | 用户回复 | VOICE 变量值 |
96
112
  |---|---|
97
- | `1` / `Connie` / `用 Connie` / 任何含 "Connie" | **`VOICE=d774d69075f24d1fb52a0dad145ba809`** |
98
- | `2` / `Sophie` / `用 Sophie` | **`VOICE=vakjM0uzzAxU4UiT0433`** |
99
- | `3` / `Bruce` / `用 Bruce` | **`VOICE=1LtsDD7yfTuX92TzjmJk`** |
100
- | `4` / `Luca` / `用 Luca` | **`VOICE=6HiVdeiuBdZbtcnukrQn`** |
101
- | `5` / `原声` / `克隆` / `用我自己的` / `keep original` | **`VOICE=`**(留空,不传 `--voice`) |
102
- | `随便` / `random` | 1-4 voice_id 里**随机挑一个**设 VOICE,**不要默认 5** |
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
+ #### 严禁
103
157
 
104
- #### 严禁(踩过两次了)
158
+ - ❌ **主动问"你要哪个字幕风格"** — 默认 `classic` 够好,问了增加摩擦(同音色逻辑)
159
+ - ❌ **用 AskUserQuestion 工具**(只有用户主动要求换才列文字菜单)
160
+ - ❌ 把菜单外的名字直传 `--style` — render-ass 会 warn 回退 classic,应先在表里映射成合法值
105
161
 
106
- - **用 AskUserQuestion 工具**(4 选项限制 + label 反查不可靠,会让用户选了 voice 你跑 clone)
107
- - ❌ **用户选了 1-4,你设 VOICE=空走 clone** — 跟选择不一致就是 bug
108
- - ❌ **不列全 5 个选项**(比如只列"用 voice / 原声"二选一,等同不让选)
109
- - ❌ **不告诉用户怎么回复**(必须明确说"回复数字或名字")
162
+ > 风格只改配色/描边/阴影/关键词,**字体仍按语言兜底**(th=Sarabun、vi=Noto;anton 例外,越南语用 Anton)。所有风格对 4 语都安全,不会豆腐块。
110
163
 
111
- ### Step 1:HeyGen 翻译(用现成 CLI)
164
+ ### Step 1:翻译(用现成 CLI)
112
165
 
113
166
  ```bash
114
167
  RAW_DIR="$WORK/raw"
@@ -116,14 +169,19 @@ mkdir -p "$RAW_DIR"
116
169
  gen video-translate \
117
170
  --video-url "$URL" \
118
171
  --lang "$LANG" \
119
- --mode fast \
120
- --dynamic-duration \
121
172
  ${VOICE:+--voice "$VOICE"} \
173
+ ${SINGLE_VOICE_FLAG:-} \
122
174
  -o "$RAW_DIR" \
123
175
  > "$WORK/gen.json"
124
176
  ```
125
177
 
126
- `gen video-translate` 自动轮询,5-15 min 完成。失败 / 超时见末尾错误表。
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
+ 失败 / 超时见末尾错误表。
127
185
 
128
186
  幂等:`$WORK/gen.json` 存在则跳。
129
187
 
@@ -149,27 +207,81 @@ AUDIO="$WORK/translated_audio.wav"
149
207
  SRT="$WORK/caption.srt"
150
208
  [ -s "$SRT" ] || curl -sSL --retry 1 "$CAP_URL" -o "$SRT"
151
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
+
152
213
  ASS="$WORK/subs.ass"
153
214
  TRANS="$WORK/translations.json"
154
215
  video-translate render-ass \
155
216
  --srt "$SRT" \
156
217
  --lang "$TAG" \
218
+ ${STYLE:+--style "$STYLE"} \
157
219
  --translations "$TRANS" \
158
220
  --out "$ASS"
159
221
  ```
160
222
 
161
223
  用户可手编 `$TRANS`(给关键词加 `**word**` 触发粉色 KW 描边)后删 `$ASS` 重跑这步。
162
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
+
163
276
  ### Step 3:Mux 出片
164
277
 
165
278
  ```bash
166
279
  FINAL="./videos/${NAME}_${TAG}.mp4"
167
280
 
168
- ## --orig-video 关键:用用户原视频画面(不用 HeyGen lip-sync 改过嘴型的画面)
169
- ## $URL 是用户原视频本地路径或 https URL
170
- ## 如果 $URL 是 https,先 curl 到本地 → 用本地路径
171
- ORIG_VIDEO="$URL"
172
- if [[ "$URL" =~ ^https?:// ]]; then
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
173
285
  ORIG_VIDEO="$WORK/orig.mp4"
174
286
  [ -f "$ORIG_VIDEO" ] || curl -sSL "$URL" -o "$ORIG_VIDEO"
175
287
  fi
@@ -185,8 +297,8 @@ video-translate mux \
185
297
  ```
186
298
 
187
299
  **关键**:管线匹配本地水杯/zhaodada 批量出片行为:
188
- - `--orig-video` = 用户原视频画面( lip-sync 改动)
189
- - `--raw` = HeyGen v3 译音(audio_url 下载的 wav)
300
+ - `--orig-video` = 用户原视频画面(MiniMax 不动画面,只换音轨)
301
+ - `--raw` = 后端译音(audio_url 下载的 wav)
190
302
  - 字幕烧 + BGM ducking 走 mux 内置
191
303
 
192
304
  **BGM + 花体字幕样式 = 默认产出的一部分,不是可选项。** 不要问用户"要不要加",不要在结尾说"如需 BGM/字幕样式可补充"。`video-translate mux` 默认就用 `bgm/default.mp3` + Path-B 描边样式;`render-ass --lang $TAG` 默认按语言选字体(Bangers / Sarabun / Noto Sans)。不要绕过 `mux` 自己手写 ffmpeg(会丢 BGM + 字幕样式 + 音轨规范化)。
@@ -211,10 +323,12 @@ rm -rf "$WORK"
211
323
  **触发条件**:用户一次请求 ≥2 种目标语言("翻译成英/泰/越/马 4 国语")。
212
324
 
213
325
  **不要按单语流程循环跑 N 次**——会有两个致命问题:
214
- 1. wall time = N × 单次 (~12min/lang × 4 = 48min)
326
+ 1. wall time = N × 单次 (~5min/lang × 4 = 20min)
215
327
  2. chat bash 队列被 N 条阻塞命令塞满,后续命令(mux/SFX)全卡"准备中"
216
328
 
217
- 改用 **单条 bash 内 N 路 subshell 并发**:chat 队列只占 1 slot,HeyGen 服务端并行处理,wall time ≈ 单次最慢 (~12min)
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 默认仍走并发(代码路径已验证)。
218
332
 
219
333
  ### Step B0:URL 预处理 + workspace + 音量预检 + 源视频下载(只跑一次)
220
334
 
@@ -242,11 +356,13 @@ ORIG_VIDEO="./videos/${NAME}.batch/orig.mp4"
242
356
  [ -f "$ORIG_VIDEO" ] || curl -sSL "$URL" -o "$ORIG_VIDEO"
243
357
  ```
244
358
 
245
- ### Step B0.5:voice 选一次,所有语种共用
359
+ ### Step B0.5:voice 默认 backend 预置,不问
360
+
361
+ 同单语 Step 0.5,默认 `VOICE=""` 走后端 `Portuguese_FriendlyNeighbor`。仅当用户主动要求换音色才列菜单 + 设 `VOICE`。所有语种 subshell 共用同一个 `VOICE`。
246
362
 
247
- 同单语 Step 0.5。`VOICE` 变量设一次后下方循环里所有 subshell 都继承,**所有语种用同一个 voice 出片**(用户只需挑一次)。
363
+ 字幕风格同理(Step 0.6):默认 `STYLE=""`(=classic 原款),仅用户主动要求才设;所有语种共用同一个 `STYLE`,已在下方 per-lang 模板的 `render-ass` 里以 `${STYLE:+--style "$STYLE"}` 透传。
248
364
 
249
- ### Step B1:并发派出所有 HeyGen 翻译(后台跑,~5 秒返回)
365
+ ### Step B1:并发派出所有翻译(后台跑,~5 秒返回)
250
366
 
251
367
  ```bash
252
368
  ## 用户实际要的语种,从下面 4 个里选(不要的注释掉)
@@ -266,7 +382,6 @@ for entry in "${LANG_LIST[@]}"; do
266
382
  ## 后台启动 gen video-translate,各自写 gen.json,bash 立即继续
267
383
  nohup gen video-translate \
268
384
  --video-url "$URL" --lang "$LANG" \
269
- --mode fast --dynamic-duration \
270
385
  ${VOICE:+--voice "$VOICE"} \
271
386
  -o "$WORK/raw" > "$WORK/gen.json" 2>&1 &
272
387
 
@@ -275,13 +390,13 @@ done
275
390
  echo "=== all submitted, returning immediately ==="
276
391
  ```
277
392
 
278
- 这一步 5 秒返回。**关键**:用 `nohup ... &` 后台启动,bash exit 后进程继续(尽量 survive 短暂 idle / 容器升级)。每个进程会在 HeyGen 翻译完成后把 audio_url/caption_url 写入对应的 gen.json。
393
+ 这一步 5 秒返回。**关键**:用 `nohup ... &` 后台启动,bash exit 后进程继续(尽量 survive 短暂 idle / 容器升级)。每个进程会在 MiniMax pipeline 完成后把 audio_url/caption_url 写入对应的 gen.json。
279
394
 
280
395
  ### Step B2-B5:逐语种等结果 → 出片(每个一条独立 bash)
281
396
 
282
397
  **重点:每个语种用单独的 bash 命令处理,不要再用 `&` 并发**。这样:
283
398
  - chat 看到每个语种独立完成、独立报告
284
- - 第 1 个 bash 等 HeyGen(~12 min),后续每个 bash 几乎瞬间完成(因为 4 个翻译是并行跑的,后续几个早已 done)
399
+ - 第 1 个 bash 等翻译(~5-8 min),后续每个 bash 几乎瞬间完成(因为 4 个翻译是并行跑的,后续几个早已 done)
285
400
  - 失败隔离:1 个失败不影响其他
286
401
 
287
402
  通用 per-lang 模板(把 `$TAG` 换成 en / th / ms / vi 各跑一遍):
@@ -293,7 +408,7 @@ FINAL="./videos/${NAME}_${TAG}.mp4"
293
408
 
294
409
  ## 等 gen.json 写完(或失败)
295
410
  ## ⚠ gen-cli 用 outputSuccess 包装在 {success, data: {...}} 里,字段在 .data.* 下
296
- DEADLINE=$(( $(date +%s) + 1800 )) ## 30min 上限,跟 HeyGen poll timeout 对齐
411
+ DEADLINE=$(( $(date +%s) + 1800 )) ## 30min 上限,跟后端 poll timeout 对齐
297
412
  while true; do
298
413
  ## Case 1: 成功 → .data.audio_url 非 null
299
414
  if jq -e '.data.audio_url' "$WORK/gen.json" >/dev/null 2>&1; then
@@ -309,7 +424,7 @@ while true; do
309
424
  echo "[$TAG] FAIL: $(jq -r '.error.code + \": \" + .error.message' "$WORK/gen.json")"
310
425
  exit 1
311
426
  fi
312
- ## Case 4: 30min 超时(HeyGen 一般 5-15min,30min 还没就是真挂了)
427
+ ## Case 4: 30min 超时(MiniMax pipeline 一般 3-8min,30min 还没就是真挂了)
313
428
  if [ "$(date +%s)" -gt "$DEADLINE" ]; then
314
429
  echo "[$TAG] TIMEOUT 30min,gen.json 内容:"; cat "$WORK/gen.json"; exit 1
315
430
  fi
@@ -323,11 +438,26 @@ CAP_URL=$(jq -r '.data.caption_url' "$WORK/gen.json")
323
438
  ## 下载 + render-ass
324
439
  curl -sSL --retry 1 "$AUDIO_URL" -o "$WORK/translated_audio.wav"
325
440
  curl -sSL --retry 1 "$CAP_URL" -o "$WORK/caption.srt"
441
+ [ -s "$WORK/caption.srt" ] || { echo "[$TAG] ERR: SRT 空/损坏"; exit 1; }
326
442
  video-translate render-ass \
327
443
  --srt "$WORK/caption.srt" --lang "$TAG" \
444
+ ${STYLE:+--style "$STYLE"} \
328
445
  --translations "$WORK/translations.json" \
329
446
  --out "$WORK/subs.ass"
330
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
+
331
461
  ## mux 出片
332
462
  video-translate mux \
333
463
  --raw "$WORK/translated_audio.wav" \
@@ -356,17 +486,17 @@ echo "[$TAG] DONE -> $FINAL"
356
486
  | | 单语循环(❌ 不要) | 渐进批量(✓) |
357
487
  |---|---|---|
358
488
  | 源视频下载 | N 次重下 | 1 次共用 |
359
- | HeyGen 调用 | 串行 N × 12min | 并发 max(12min) |
489
+ | 翻译调用 | 串行 N × 5min | 并发(受 MiniMax RPM 限速影响) |
360
490
  | chat 队列占 | N 条阻塞 bash | 1 条 submit + N 条快速 poll |
361
491
  | 失败隔离 | 一个挂全停 | per-lang subshell + bash 独立 |
362
- | 用户感知 | 黑盒 48 min | 第 1 个 ~12 min 出,后续每 ~30 秒 1 个 |
363
- | **wall time(N=4)** | **~48 min** | **~13 min(73%↓)** |
492
+ | 用户感知 | 黑盒 ~20 min | 第 1 个 ~5-8 min 出,后续每 ~30 秒 1 个 |
493
+ | **wall time(N=4)** | **~20 min** | **~8-12 min**(实际看 MiniMax tier) |
364
494
 
365
495
  ### 失败处理
366
496
 
367
497
  - 单 lang 失败:per-lang bash `exit 1`,agent 报告该语种失败但**继续跑下一个语种的 bash**
368
498
  - 用户对失败的单 lang re-run → 删 `$WORK/gen.json` 重跑 Step B1 单语 + per-lang 模板即可
369
- - HeyGen credit 不足报错 → 当 lang 个失败处理,不阻塞其他 lang
499
+ - MiniMax 余额不足 / RPM 限速持续失败 → 当 lang 个失败处理,不阻塞其他 lang
370
500
 
371
501
  ## 错误处理
372
502
 
@@ -374,20 +504,24 @@ echo "[$TAG] DONE -> $FINAL"
374
504
  |---|---|
375
505
  | URL 不通 | 重传或上传到 Optima 拿新 URL,不消耗翻译服务 credits |
376
506
  | 源视频音量过低 | 让用户先用 ffmpeg `volume=20dB,acompressor` 放大后重传 |
377
- | HeyGen `status: failed` "No speaker is detected" | 同源视频音量过低处理 |
378
- | HeyGen 其他失败 | 透出 task_id,提示用 `gen task get <id>` 查最新;干净退出 |
379
- | HeyGen >30min 超时 | 同上,可能任务还在 running |
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 |
380
512
  | `gen.json` 缺 `.audio_url` / `.caption_url` | gen 后端契约可能改字段名。打 `cat $WORK/gen.json` 看实际字段 |
381
- | `curl <caption_url>` 失败 | URL 7 expire。retry 1 次后仍失败 → 重跑 step 1 |
513
+ | `curl <caption_url>` 失败 | S3 presigned URL 24 小时 expire。retry 1 次后仍失败 → 重跑 step 1 |
382
514
  | `video-translate render-ass` SRT 解析失败 | 显示 SRT 头 20 行,不重试 |
383
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 |
384
517
 
385
518
  ## 不做
386
519
 
387
- - ❌ 性别自动检测(让用户自己选,见 Step 0.5)
520
+ - ❌ 性别自动检测(默认 `Portuguese_FriendlyNeighbor` 女声,用户想换主动说)
388
521
  - ❌ 硬字幕(burnt-in)抹除 — 源视频有的话出片会双语
389
522
  - ❌ 自动加粗关键词 — SRT 直入无粉色,用户手编 translations.json
390
- - ❌ 双说话人差异化音色 — HeyGen 自动 diarize 但用同一克隆音色
523
+ - ❌ 双说话人差异化音色 — MiniMax v1 单音色 default,手动传 speakers JSON 才能分轨(SKILL v1 不暴露)
524
+ - ❌ 克隆原说话人音色 — MiniMax voice clone API 是独立流程,v1 不上
391
525
  - ❌ 一次多语言 — 一次一种语言
392
526
 
393
527
  ## 参考
@@ -399,13 +533,15 @@ echo "[$TAG] DONE -> $FINAL"
399
533
 
400
534
  ## Voice Catalog
401
535
 
402
- 下面 4 个 voice 全部是 HeyGen catalog 里走 **ElevenLabs `eleven_multilingual_v2`** 模型的多语 voice,**任选一个,所有支持语言(en/th/ms/vi)都能说**。实测 Connie 4 语全部 Whisper 95%+ 语种识别置信度,音色保留 + 自动适配目标语言性别敬语(如泰语 ค่ะ/ครับ)。
536
+ 下面 6 个 voice MiniMax 预置音色,**任选一个,所有支持语言(en/th/ms/vi)都能说**(底层 `speech-02-turbo` + `language_boost` 跨语自适应)。POC 验证(2026-05)在卖货场景下跨 4 语均自然,Whisper 反向验证语种识别 > 90%。
403
537
 
404
- | # | voice_id | 名字 | 性别 | 风格定位 | preview(英文样本) |
405
- |---|---|---|---|---|---|
406
- | 1 | `d774d69075f24d1fb52a0dad145ba809` | Connie - Professional | F | 沉稳专业旁白 | https://resource.heygen.ai/text_to_speech/locale=en-USmodel=eleven_multilingual_v2id=9FnNGNtwCeU9fyf6mFfDp8.mp3 |
407
- | 2 | `vakjM0uzzAxU4UiT0433` | Sophie | F | 温柔友好 | https://resource.heygen.ai/text_to_speech/locale=model=eleven_multilingual_v2id=kte4EzDuRTnsnHkATe6tDK.mp3 |
408
- | 3 | `1LtsDD7yfTuX92TzjmJk` | Bruce | M | 中年浑厚 | https://resource.heygen.ai/text_to_speech/locale=model=eleven_multilingual_v2id=2SdnapPUN7wvtCbkPSgdHV.mp3 |
409
- | 4 | `6HiVdeiuBdZbtcnukrQn` | Luca | M | 年轻活力 | https://resource.heygen.ai/text_to_speech/locale=model=eleven_multilingual_v2id=FVKYscu8J8EVReBuZdPXnJ.mp3 |
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,澳式口音) | 短视频 / 运动 / 快消 |
410
546
 
411
- > 维护说明:换 voice 时,从 `GET /v2/voices` 里筛 `preview_audio` `multilingual` EL 系列。其它非 EL 系列的"English"标签 voice **大概率**不能跨语言,要实测验证。
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 接受 ≠ 输出可用)。
@@ -0,0 +1,25 @@
1
+ # BGM 情绪库 — 来源与授权
2
+
3
+ 所有曲目来自 **FreePD.com**,授权 **CC0 1.0(公共领域,可商用、零署名)**。
4
+ 原始仓库镜像:`SoundSafari/CC0-1.0-Music`(`freepd.com/` 目录,CC0-1.0)。
5
+
6
+ 本库文件已转码瘦身(30s、80kbps、mono、首尾淡入淡出)用于成片 BGM。
7
+
8
+ | 目录(情绪) | 文件 | FreePD 原曲 |
9
+ |---|---|---|
10
+ | warm | warm-01.mp3 | Aquarium |
11
+ | warm | warm-02.mp3 | Adding the Sun |
12
+ | calm | calm-01.mp3 | Amazing Grace |
13
+ | calm | calm-02.mp3 | Baltic Levity |
14
+ | upbeat | upbeat-01.mp3 | And Here We Go |
15
+ | upbeat | upbeat-02.mp3 | Backbeat |
16
+ | sad | sad-01.mp3 | After the End |
17
+ | sad | sad-02.mp3 | A Waltz For Naseem |
18
+ | energetic | energetic-01.mp3 | Action Strike |
19
+ | energetic | energetic-02.mp3 | Battle Ready |
20
+ | dramatic | dramatic-01.mp3 | Ancient Rite |
21
+ | dramatic | dramatic-02.mp3 | Alien Invasion |
22
+
23
+ 注:情绪分类按曲名启发式归类,未经逐曲试听校准,可按实际听感调整目录归属。
24
+ 扩库:往对应情绪目录丢入**可商用授权**的音频即可(脚本自动纳入随机池)。
25
+ CC0 全文:https://creativecommons.org/publicdomain/zero/1.0/
@@ -0,0 +1,9 @@
1
+ # 音色样音目录(label = voice_id)
2
+ # Claude:把这些 mp3 给用户试听,用户选定后把对应 voice_id 写进 proposal.voice
3
+ 01-温暖少女 = Chinese (Mandarin)_Warm_Girl
4
+ 02-甜美女声 = Chinese (Mandarin)_Sweet_Lady
5
+ 03-甜美元气 = female-tianmei
6
+ 04-清甜少女 = female-shaonv
7
+ 05-御姐 = female-yujie
8
+ 06-成熟知性 = female-chengshu
9
+ 07-女主播 = presenter_female
@@ -1,4 +1,19 @@
1
1
  import { Command } from 'commander';
2
+ export type DoctorVideoMode = {
3
+ mode: 'audio';
4
+ } | {
5
+ mode: 'text';
6
+ };
7
+ /**
8
+ * Decide audio-driven vs text-driven for `gen doctor video` from CLI options,
9
+ * enforcing the 二选一 contract (spec §4.2). Throws on invalid combos so the
10
+ * action surfaces a clean validation error before any upload/submit.
11
+ */
12
+ export declare function selectDoctorVideoMode(opts: {
13
+ voice?: string;
14
+ text?: string;
15
+ audio?: string;
16
+ }): DoctorVideoMode;
2
17
  /**
3
18
  * `gen doctor` namespace — historical naming (Phase 2 POC originated for medical
4
19
  * scenarios). Functionally identical to `gen avatar *` and kept as a
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAepC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,QAMrD;AAKD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,QAIxD"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,CAW9G;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,QAMrD;AAKD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,QAIxD"}