---
name: video-compose
description: "把【多个素材片段 + 一段口播文案】自动合成一条可发布短视频——自动配音、按文案语义选片拼接、烧字幕、加 BGM。用户只需拍素材/用 AI 生成片段 + 写文案，剪辑配音字幕全交给 AI。

  必备前提：用户有 ≥1 个视频片段文件 + 一段口播文案（或愿意现写）。

  触发：用户给出多个视频片段 + 文案，并说'拼个视频'/'把这堆素材剪一下'/'给这段文案配视频'/'做条口播带画面的视频'/'素材+文案做成片'。

  不触发：只给 1 个口播 talking-head 视频要剪（用 video-edit）；只给产品图要生成画面（用 video-gen）；用户要【保留片段原声】拼接（用 video-edit，本 skill 会丢弃原声）。"
version: 1.0.0
owner_repo: Optima-Chat/optima-gen
---

# video-compose — 素材 + 文案 → 成片

用户给一堆片段 + 一段口播稿，你交付配好音、配好画面、带字幕和 BGM 的竖版成片。

> ⚠ **语义前提：片段被当作纯画面 b-roll，原声一律丢弃**，成片音频 = AI 配音 + BGM。若用户给的是带人声的口播片段、想**保留原声**拼接，那是 video-edit 的活——开工前跟用户确认一句"片段原声会去掉、全程用 AI 配音"，避免出片后才发现声音没了。

**关键：选片这一步由你（Claude）亲自看帧判断**——脚本把每个 clip 抽 3 帧，你用 Read 看图，按文案语义写 `proposal.json`。不需要任何外部 vision API。

## 工具

`python3 $CLAUDE_SKILL_DIR/scripts/video_compose.py <frames|build> <proj-dir>`
- 依赖（容器自带）：`python3`、`ffmpeg`、`gen` CLI。
- 情感配音走 `gen tts --provider minimax`（key + 计费在后端 optima-generation，skill 不碰密钥）。

## 工作目录

```
<proj>/inputs/clips/*.mp4    素材（任意命名，按文件名排序得 clip id）
<proj>/inputs/script.txt     口播稿，每行一句 = 一个 segment
<proj>/work/                 中间产物（frames/ proposal.json subs.ass 等）
<proj>/final.mp4             成片
```

## Step 0：指令清单读回（≥ 2 个动作时必跑）

用户一条消息里给多个要求（如"竖版 + 不要 BGM + 字幕大一点 + 压到 20 秒"）时，**先拆成原子清单读回、等确认再动手**，不要边读边做。单一动作（"拼个视频"）跳过。
（理由同 video-edit：多指令直接执行易漏，漏了要等成片出来才发现，全流程重做。）

## 主流程

### 1. 建工程 + 落素材和文案
- 建 `<proj>/inputs/clips/` 和 `<proj>/inputs/script.txt`
- 把用户的片段拷进 clips/（命名随意，建议 `01.mp4 02.mp4 …` 便于引用）
- 文案写进 script.txt，**每行一句**。用户没给文案就先帮他写（看素材定主题），写完**先给用户确认文案**再继续。

### 2. 抽帧
```
python .../video_compose.py frames <proj>
```
产出 `work/frames/<clipid>_<tag>.jpg`（每片自适应抽 3~6 帧，约每 5s 一帧）+ `work/clips_manifest.json`。**manifest 里每帧带 `t`（秒）= 该子镜头在素材中的时间点**——写 proposal 时用它指定 `src_start`。

### 3. 看帧 + 写 proposal.json（**你的核心判断**）
- 用 **Read 逐张看** `work/frames/` 里的帧，在心里给每个 clip 一句话描述（人物/动作/场景/有无烧死字幕）。
- 按 script 每句的语义，给它挑最贴合的 clip，写出 `work/proposal.json`：

```json
{
  "voice": "Chinese (Mandarin)_Warm_Girl",
  "bgm_mood": "warm",
  "assignments": [
    { "segment_idx": 0, "text": "第一句原文", "clip": "01", "src_start": 5.2, "emotion": "sad", "speed": 0.92, "rationale": "为什么选它 + 为什么这个子镜头 + 为什么这个情绪" }
  ]
}
```

- **voice**：voice_id。**只用 `voice-samples/CATALOG.md` 里 7 个实测可用的音色**（标签 ↔ voice_id），别填没验证过的（错的 voice_id 会 `UPSTREAM_UNKNOWN: voice id not exist` 失败）。默认 `Chinese (Mandarin)_Warm_Girl`（温暖少女）。**具体音色让用户试听后定，见 §4。**
- **clip**：素材 id（manifest 里的 `id`）
- **src_start**：从该素材的**哪一秒**开始切（= 你选中那个子镜头帧的 `t`，看 manifest）。**这是避免重复镜头的关键**：同一 clip 给多句复用时，每句填**不同的 `t`**（如倒水特写 t=11.5、碰杯 t=20.7），脚本以该时刻为中心切，画面就不会重复。不填则脚本按复用顺序自动均匀错开（也不会重复，但选的子镜头不一定贴文案，所以建议显式填）。
- **emotion**：每句独立，九选一 `happy/sad/angry/fearful/disgusted/surprised/calm/fluent/whisper`，按文案情绪配
- **speed**：0.5–2.0。**这是抖音/TikTok/小红书短视频工具，默认就要快**——不写 speed 脚本按 `DEFAULT_SPEED=1.35` 配音（≈TikTok 口播节奏）；想更冲可显式写 1.5；**只有明确要治愈/抒情慢节奏才写 ≤1.0**（如 0.9）。别让成片听起来拖沓。
- **bgm_mood / bgm**：见 §BGM（用户没指定就按文案情绪填 `bgm_mood`）

选片规则：
- 首句优先用能建立场景/正面的镜头
- 时序上有视觉故事线就尊重它
- **clip 可复用，但复用时必须给不同的 `src_start`（选不同子镜头），否则画面重复**；也尽量别连续两句用同一 clip
- **每个 clip 通常有多个子镜头（manifest 多帧）——优先把不同子镜头分给不同句子，让素材都出镜，而不是反复用片头**
- 素材数 < 句子数时复用并提示用户素材偏少
- **若某 clip 帧里有烧死的字幕/水印**（见 §坑），尽量不选它承载关键句；非用不可则提示用户

情绪配法：按文案的情感弧线给每句配 emotion，不要全句一个调。例：怅然开场 `sad` → 转折欣喜 `happy` → 高潮 `surprised` → 舒缓 `calm` → 暖心收尾 `happy`。

节奏（平台风格）：这是短视频，默认**快节奏**（像抖音/TikTok 口播）。文案**短句、强钩子、句子别太长**（一句太长配音就拖、画面也切得慢）；语速默认 1.35，想更冲可更快。除非用户明确要慢节奏治愈片，否则别配出"念课文"那种拖沓感。

### 4. 试听音色 / BGM + 看选片方案 → 用户拍板
**音色和 BGM 是创作决策，让用户自己听、自己定，不要替用户默认死。** 出片前：

> **素材路径**：BGM 库和音色样音随 `gen-cli` 一起发布（不在 skill 目录）。先解析一次：
> ```bash
> ASSETS=$(python3 $CLAUDE_SKILL_DIR/scripts/video_compose.py assets-dir)
> ```
> 下面用 `$ASSETS/voice-samples`、`$ASSETS/bgm-library/<mood>`。

1. **音色试听**：把 `$ASSETS/voice-samples/*.mp3`（7 个音色样音，标签↔voice_id 见同目录 `CATALOG.md`）拷到 `<proj>/previews/`，把可播放的文件链接列给用户，让用户**听完选一个**。
2. **BGM 试听**：按文案情绪先推荐一类（治愈→`warm`、种草→`upbeat`…），把该类（用户想多听就多拷几类）`$ASSETS/bgm-library/<mood>/*.mp3` 拷到 `<proj>/previews/` 给用户试听；用户可换类、指定某首，或**自己上传一首**（放 `inputs/bgm/`，优先级最高）。
3. **选片方案**：把 assignments 摘要（每句哪个 clip + 理由）一并给用户过目。
4. **给推荐默认**（音色=贴文案的一个、BGM=按情绪一类），用户想省事一句「就用默认」即可直接出片；想换就听了再定。

用户拍板后：选定的 voice_id 写进 `proposal.voice`、BGM 写进 `bgm`(自带路径) 或 `bgm_mood`(情绪库)，再：
```
python3 $CLAUDE_SKILL_DIR/scripts/video_compose.py build <proj>
```
脚本做：逐句情感配音（带缓存，未改的句子不重跑）→ 每句按 `src_start` 用 `-ss` **精切对应子镜头**到该句时长（竖版 crop，clip 比该句短则**慢放填满、不 loop**；**同素材复用强制时间窗不重叠，防重复镜头**）→ 拼接 → 烧字幕 → BGM sidechain ducking（有 BGM 才加）→ `final.mp4`。

### 5. 交付汇报
报成片路径 + 时长 + 用了几个 clip + 配音字数。**不要提任何模型/服务名**，配音统一说"AI 配音"。

## BGM（不锁死；用户没指定就按文案情绪自动配）
来源优先级：
1. `proposal["bgm"]` 显式路径（用户明确指定某首）
2. `inputs/bgm/` 用户上传的音频
3. `proposal["bgm_mood"]` → 从情绪库 `bgm-library/<mood>/` **随机挑一首**（用户没指定 BGM 时，**你按文案整体情绪填这个字段**）
4. 都没有 → 仅人声

- 有 BGM 时自动 sidechain ducking（人声起时压低 BGM）
- **优先让用户试听后选**（见 §4 步骤 2）；用户说「你定/随便」时才按文案情绪自动填 `bgm_mood`，**绝不留空**。情绪 → 目录：`warm`(治愈/温暖) `upbeat`(欢快) `sad`(伤感) `calm`(舒缓) `energetic`(高能/卖货) `dramatic`(戏剧/科技)
- 用户明确要某首 / 要换 → 用优先级 1、2
- ⚠ 库里只能放**可商用授权**的曲子（公开发布视频有版权要求）；某情绪目录为空时该句跳过 BGM 并提示

## 用户改方案怎么办
- 换某句的画面：改 `proposal.json` 那条的 `clip`（换素材）或 `src_start`（同素材换子镜头），重跑 `build`
- 嫌某两句画面重复：给它们填不同的 `src_start`（看 manifest 的帧 `t`），重跑 `build`
- 换情绪/音色：改 `emotion`/`voice`，重跑 `build`
- 换/加 BGM：丢文件进 `inputs/bgm/` 或改 `bgm` 路径，重跑 `build`
**配音有缓存**（engine/voice/emotion/speed/text 没变就复用），所以单纯换 BGM/字幕**不重新花钱跑配音**，秒出。`proposal.json` 就是反复调的抓手。

## 坑（实跑踩过）

| 坑 | 处理 |
|---|---|
| **用户素材自带烧死字幕/水印** | 你看帧时若发现某片已有硬字幕（如原片烧了英文 caption），新字幕叠上去会重影。承载关键句时避开该片，或提示用户该片有原生字幕 |
| **clip 是横版** | 脚本默认 center-crop 到竖版会裁掉两侧；横版素材多时提示用户成片是竖版裁切 |
| **素材总时长 < 文案配音时长** | 不再 loop（loop=重复）：单片比某句短→慢放填满；某片被多句复用但时长不够 distinct 画面→`[mix] ⚠` 告警。**要彻底不重复**：素材总时长应 ≥ 配音总时长，且每片别被复用超过它能切出的 distinct 段数；不够就提示用户补素材或精简文案 |
| **字幕样式/字体** | 默认白字黑边底部。字体走 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 验证 |
| **gen tts 报错** | 透传后端错误。`PROVIDER_INSUFFICIENT_CREDITS`=MiniMax 余额；`INVALID_INPUT`=emotion/voice 非法。配音失败不出片，不静默吞 |

## 关键参数（scripts/video_compose.py 顶部）
- `W,H=1080,1920`（竖版）、`FPS=30`、`CRF=20`
- 配音：`gen tts --provider minimax`，密钥/计费在后端，skill 不碰密钥
- `VIDEO_COMPOSE_FONT`：字幕字体（容器需有对应 CJK 字体）
- BGM 不锁死：见 §BGM；情绪库随 `gen-cli` 打包，脚本自动解析（`$ASSETS/bgm-library/<mood>/`，见 §4 素材路径）
