---
name: video-compose
description: 当用户说"合成视频"、"做 mp4"、"图片转视频"、"ffmpeg 拼视频"、"video-compose"，或在 workflow 里要把图片序列 + 可选音轨 + 可选字幕合成成 mp4 时使用。本 skill 是 ffmpeg 的 declarative 封装，所有参数（分辨率、字体、码率、转场）都可配置，不绑定特定视频格式。
triggers: 合成视频、做 mp4、图片转视频、ffmpeg 拼视频、video-compose、视频制作
---

# video-compose —— 通用 ffmpeg 视频合成

> 通用 skill，可被任何 workflow 调用。无场景假设（不绑定竖屏/横屏，不绑定语言）。

## 输入约定

调用方传一行 `key=value` 字段串，必填项 + 可选项混合：

```
images_glob=./assets/img_*.png audio=./assets/audio.mp3 srt=./assets/subs.srt output=./out/video.mp4 width=1080 height=1920 per_image_duration=3 fps=30 font=Microsoft YaHei font_size=42
```

### 必填

- **images_glob**：图片 glob 表达式（用 Glob 工具枚举，按文件名排序）
- **output**：目标 mp4 路径

### 可选

| 字段 | 默认 | 说明 |
|---|---|---|
| **audio** | （无） | 音轨 mp3/wav；不传则纯视频无声 |
| **srt** | （无） | 字幕文件；不传则不烧入字幕 |
| **width** × **height** | `1080` × `1920` | 输出分辨率（默认竖屏；横屏传 `1920` × `1080`） |
| **per_image_duration** | `3` | 每张图片显示秒数（如果给 audio，可能会被覆盖 —— 见下方"对齐音轨"） |
| **fps** | `30` | 帧率 |
| **font** | `Microsoft YaHei`（Win）/ `PingFang SC`（Mac）/ `Noto Sans CJK SC`（Linux） | 字幕字体 |
| **font_size** | `42` | 字幕字号 |
| **font_color** | `FFFFFF` | 字幕颜色（RGB hex） |
| **align_audio** | `auto` | `auto`=按音轨长度等分图片时长 / `fixed`=固定 `per_image_duration` / `cut`=超出部分截断 |
| **crf** | `23` | x264 质量（18–28，越低越清） |

## 标准执行流程

### 1. 前置检查

```bash
which ffmpeg >/dev/null || { echo "缺少 ffmpeg，请安装：winget install Gyan.FFmpeg / brew install ffmpeg / apt install ffmpeg"; exit 1; }
which ffprobe >/dev/null || { echo "缺少 ffprobe（通常和 ffmpeg 一起装）"; exit 1; }
```

### 2. 收集图片列表

用 Glob 工具按 `images_glob` 找文件 → 按文件名 ASCII 排序。如果只有 0 或 1 张图，**报错退出**（视频至少要 2 帧才有意义）。

### 3. 决定每张图时长

- 如果**有 audio** 且 `align_audio=auto`（默认）：
  - 用 `ffprobe -v error -show_entries format=duration -of csv=p=0 <audio>` 拿音轨时长 `T`
  - 单图时长 = `T / 图片数`
- 否则：每张图固定 `per_image_duration` 秒

### 4. 生成 concat 列表

ffmpeg concat demuxer 用法：

```bash
mkdir -p "$(dirname "$OUTPUT")"
LIST=/tmp/video_compose_list.txt
> "$LIST"
for f in $(ls $IMAGES_GLOB | sort); do
  echo "file '$f'" >> "$LIST"
  echo "duration $PER_IMG" >> "$LIST"
done
# ffmpeg quirk：最后一张图片要再列一次（不带 duration）才会被显示完整时长
LAST=$(ls $IMAGES_GLOB | sort | tail -n1)
echo "file '$LAST'" >> "$LIST"
```

### 5. 合成视频流（可选烧字幕）

无字幕版本：
```bash
ffmpeg -y -f concat -safe 0 -i "$LIST" \
  -vf "scale=${WIDTH}:${HEIGHT}:force_original_aspect_ratio=increase,crop=${WIDTH}:${HEIGHT},fps=${FPS}" \
  -c:v libx264 -crf ${CRF} -pix_fmt yuv420p /tmp/video_only.mp4
```

带字幕版本（filter 链尾加 subtitles 滤镜）：
```bash
ffmpeg -y -f concat -safe 0 -i "$LIST" \
  -vf "scale=${WIDTH}:${HEIGHT}:force_original_aspect_ratio=increase,crop=${WIDTH}:${HEIGHT},fps=${FPS},subtitles=${SRT}:force_style='Fontname=${FONT},Fontsize=${FONT_SIZE},PrimaryColour=&H00${FONT_COLOR}&,Outline=2,BorderStyle=1'" \
  -c:v libx264 -crf ${CRF} -pix_fmt yuv420p /tmp/video_only.mp4
```

> ⚠️ Windows 路径里反斜杠 `\` 会被 ffmpeg subtitles 滤镜当转义字符吃掉。**字幕路径强制用正斜杠 `/`**，或者 `\\:` 转义盘符冒号。

### 6. 合入音轨（如果有）

```bash
if [ -n "$AUDIO" ]; then
  ffmpeg -y -i /tmp/video_only.mp4 -i "$AUDIO" \
    -c:v copy -c:a aac -b:a 192k \
    -shortest "$OUTPUT"
else
  cp /tmp/video_only.mp4 "$OUTPUT"
fi
```

### 7. 校验产物

```bash
ffprobe -v error -show_entries stream=codec_type,duration -of default=noprint_wrappers=1 "$OUTPUT"
SIZE=$(stat -c%s "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT")
```

- 文件存在且 size > 100KB
- 至少有 1 个 video 流；如果传了 audio 还应该有 1 个 audio 流
- 总时长 > 1 秒

任一项不满足 → 报错并打印 ffprobe 输出供 debug。

### 8. 收尾输出

```
saved: <OUTPUT> (resolution=<WIDTH>x<HEIGHT>, duration=<DUR>s, size=<MB>MB)
```

## 字体路径速查

| OS | 字体名（filter 用） | 实际文件 |
|---|---|---|
| Windows | `Microsoft YaHei` 或 `SimHei` | `C:\Windows\Fonts\msyh.ttc` |
| macOS | `PingFang SC` | `/System/Library/Fonts/PingFang.ttc` |
| Linux | `Noto Sans CJK SC` | `/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc` |

如果 force_style 找不到字体，会 fallback 到 Arial（中文会变方块），用 `fc-list :lang=zh-cn` 看本机有哪些中文字体。

## 注意事项

- **ffmpeg 滤镜里的引号嵌套很难写**：双引号包围整个 `-vf` 值，里面用单引号；force_style 的 key=value 用 `=` 不要用空格
- subtitles 滤镜路径必须**用正斜杠**，盘符冒号转义成 `\\:`（Win 上 `C:/path/x.srt` → `C\\:/path/x.srt`）
- 不接受**纯静态图片**作输入 —— 至少 2 张才合理；如果调用方只有 1 张图，让它用 ffmpeg `-loop 1` 单独处理（不走本 skill）
- 不假设视频用途（短视频 / 长视频 / 横屏 / 竖屏 / 方形）—— 全部走参数
