---
name: comfyui-keyframes-video-generation
description: 通过公网网关使用 ComfyUI API 进行 6 关键帧首尾帧视频生成。覆盖 6 张图片上传、首尾帧工作流提交、队列/任务状态查询、MP4 视频下载的完整流程。
version: "1.0"
category: video-generation
triggers:
  - 关键帧视频
  - 首尾帧视频
  - 6张图片视频
  - 多关键帧
  - comfyui keyframes
  - WanFirstLastFrameToVideo
  - 帧间过渡
  - keyframe video
  - 首尾生成
---

::: info 下载原始 Skill 文件
```bash
curl https://ai.ospreyai.cn/docs/raw/skills/comfyui-keyframes-video-generation.md -o comfyui-keyframes-video-generation.md
```
:::

## 对话式接入

本 Skill 文件可被 AI 助手（Claude Code、Cursor、ChatGPT 等）学习，通过自然语言对话完成关键帧视频生成。

在 AI 对话中发送以下指令即可：

```
学习：https://ai.ospreyai.cn/docs/raw/skills/comfyui-keyframes-video-generation.md，保存为本地的技能 skills
```

> 更多接入方式和使用示例详见 [API 文档 — AI 助手对话式接入](/api#ai-助手对话式接入)。

---

# ComfyUI 6 关键帧首尾帧视频生成

## Overview

通过公网网关 `https://ai.ospreyai.cn` 使用 ComfyUI 将 **6 张关键帧图片** 生成一段连贯的过渡视频。

使用 **Wan 2.2 First-Last-Frame-to-Video (FLF2V)** 工作流，将 6 张关键帧拆分为 5 个首尾帧过渡段，每段生成 25 帧过渡动画，最终合并为一段完整视频（720×720, 24fps, ~5 秒）。

核心特性：
- **首尾帧控制**：每段视频精确从 start_image 过渡到 end_image
- **双 UNET 采样**：高噪声 + 低噪声模型分阶段采样，画质更稳定
- **LightX2V 4 步加速**：LoRA 加速仅需 4 步采样/段
- **5 段自动拼接**：6 张图 → 5 段过渡 → ImageBatch 合并 → 单视频输出

所有 API 均需 Bearer Token 鉴权（`Authorization: Bearer sk-xxx`）。

## Quick Start

```bash
export GW="https://ai.ospreyai.cn"
export API_KEY="sk-your-api-key"

# 1. 上传 6 张关键帧图片
for i in 1 2 3 4 5 6; do
  curl -s -H "Authorization: Bearer $API_KEY" -X POST "$GW/api/v1/upload" \
    -F "image=@frame_${i}.png" -F "overwrite=true"
done

# 2. 提交首尾帧视频工作流（完整 JSON 见下方 Route A）
curl -s -H "Authorization: Bearer $API_KEY" -X POST "$GW/api/v1/ai/video/generate" \
  -H "Content-Type: application/json" \
  -d '{"prompt":{...}}'

# 3. 查询任务状态
curl -s -H "Authorization: Bearer $API_KEY" "$GW/api/v1/ai/tasks/{prompt_id}"

# 4. 下载 MP4 视频
curl -s -H "Authorization: Bearer $API_KEY" \
  "$GW/api/v1/ai/image/view/?filename=output.mp4&subfolder=video&type=output" \
  -o output.mp4
```

## Task Routing

| 场景 | 动作 |
|------|------|
| 首次生成关键帧视频 | → Route A: Upload & Generate |
| 需要查看任务是否完成 | → Route B: Check Status |
| 需要获取或下载 MP4 | → Route C: Download |
| 需要调优提示词、帧数或尺寸 | → Route D: Tune Parameters |
| 需要排查服务、节点、模型或文件问题 | → Route E: Troubleshoot |

## Route A: Upload & Generate

### 服务信息

- 网关地址: `https://ai.ospreyai.cn`
- 上传接口: `POST /api/v1/upload`
- 提交接口: `POST /api/v1/ai/video/generate`
- 鉴权方式: `Authorization: Bearer sk-xxx`

### 基本流程

1. 上传 6 张关键帧图片到 ComfyUI
2. 提交首尾帧视频工作流
3. 工作流架构：
   - 6 × `LoadImage` — 加载 6 张关键帧图片
   - 5 × `WanFirstLastFrameToVideo` — 每段首尾帧过渡生成（每段 25 帧）
   - 5 × 双 `KSamplerAdvanced` — 高噪声(0→2步) + 低噪声(2→10000步) 两阶段采样
   - 4 × `ImageBatch` — 将 5 段视频帧合并为一条序列
   - `CreateVideo` — 创建最终视频（24fps）
   - `SaveVideo` — 保存为 MP4
   - 每段还包含 CLIPLoader、UNETLoader(×2)、LoraLoader(×2)、ModelSamplingSD3(×2)、VAELoader、VAEDecode、CLIPTextEncode(×2)

### Step 1: 上传 6 张关键帧图片

```bash
for i in 1 2 3 4 5 6; do
  echo "Uploading frame_${i}.png..."
  curl -s -H "Authorization: Bearer $API_KEY" -X POST "$GW/api/v1/upload" \
    -F "image=@/path/to/frame_${i}.png" \
    -F "overwrite=true"
done
```

**响应（每次上传）：**

```json
{"name": "frame_1.png", "subfolder": "", "type": "input"}
```

> 记住每个返回的 `name` 值，工作流中 6 个 LoadImage 节点需要使用。

最大文件大小: 50MB。推荐使用 PNG 格式，6 张图片尺寸应一致（建议 720×720）。

### Step 2: 提交首尾帧视频工作流

```bash
curl -s -H "Authorization: Bearer $API_KEY" -X POST "$GW/api/v1/ai/video/generate" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": {
      "1": {"inputs": {"image": "frame_1.png"}, "class_type": "LoadImage"},
      "2": {"inputs": {"image": "frame_2.png"}, "class_type": "LoadImage"},
      "3": {"inputs": {"image": "frame_3.png"}, "class_type": "LoadImage"},
      "4": {"inputs": {"image": "frame_4.png"}, "class_type": "LoadImage"},
      "5": {"inputs": {"image": "frame_5.png"}, "class_type": "LoadImage"},
      "6": {"inputs": {"image": "frame_6.png"}, "class_type": "LoadImage"},

      "100": {"inputs": {"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "type": "wan", "device": "default"}, "class_type": "CLIPLoader"},
      "101": {"inputs": {"text": "cat turn around", "clip": ["100", 0]}, "class_type": "CLIPTextEncode"},
      "102": {"inputs": {"text": "色调艳丽，过曝，静态，细节模糊不清，字幕，风格，作品，画作，画面，静止，整体发灰，最差质量，低质量，JPEG压缩残留，丑陋的，残缺的，多余的手指，画得不好的手部，画得不好的脸部，畸形的，毁容的，形态畸变的肢体，手指融合，静止不动的画面，杂乱的背景，三条腿，背景人很多，倒着走", "clip": ["100", 0]}, "class_type": "CLIPTextEncode"},
      "103": {"inputs": {"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "104": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", "strength_model": 1.0, "model": ["103", 0]}, "class_type": "LoraLoaderModelOnly"},
      "105": {"inputs": {"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "106": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", "strength_model": 1.0, "model": ["105", 0]}, "class_type": "LoraLoaderModelOnly"},
      "107": {"inputs": {"shift": 5.0, "model": ["104", 0]}, "class_type": "ModelSamplingSD3"},
      "108": {"inputs": {"shift": 5.0, "model": ["106", 0]}, "class_type": "ModelSamplingSD3"},
      "109": {"inputs": {"width": 720, "height": 720, "length": 25, "batch_size": 1, "positive": ["101", 0], "negative": ["102", 0], "vae": ["114", 0], "start_image": ["1", 0], "end_image": ["2", 0]}, "class_type": "WanFirstLastFrameToVideo"},
      "110": {"inputs": {"add_noise": "enable", "noise_seed": 42, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 0, "end_at_step": 2, "return_with_leftover_noise": "enable", "model": ["107", 0], "positive": ["109", 0], "negative": ["109", 1], "latent_image": ["109", 2]}, "class_type": "KSamplerAdvanced"},
      "111": {"inputs": {"add_noise": "disable", "noise_seed": 0, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 2, "end_at_step": 10000, "return_with_leftover_noise": "disable", "model": ["108", 0], "positive": ["109", 0], "negative": ["109", 1], "latent_image": ["110", 0]}, "class_type": "KSamplerAdvanced"},
      "112": {"inputs": {"samples": ["111", 0], "vae": ["114", 0]}, "class_type": "VAEDecode"},
      "114": {"inputs": {"vae_name": "wan_2.1_vae.safetensors"}, "class_type": "VAELoader"},

      "200": {"inputs": {"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "type": "wan", "device": "default"}, "class_type": "CLIPLoader"},
      "201": {"inputs": {"text": "", "clip": ["200", 0]}, "class_type": "CLIPTextEncode"},
      "202": {"inputs": {"text": "色调艳丽，过曝，静态，细节模糊不清，字幕，风格，作品，画作，画面，静止，整体发灰，最差质量，低质量，JPEG压缩残留，丑陋的，残缺的，多余的手指，画得不好的手部，画得不好的脸部，畸形的，毁容的，形态畸变的肢体，手指融合，静止不动的画面，杂乱的背景，三条腿，背景人很多，倒着走", "clip": ["200", 0]}, "class_type": "CLIPTextEncode"},
      "203": {"inputs": {"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "204": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", "strength_model": 1.0, "model": ["203", 0]}, "class_type": "LoraLoaderModelOnly"},
      "205": {"inputs": {"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "206": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", "strength_model": 1.0, "model": ["205", 0]}, "class_type": "LoraLoaderModelOnly"},
      "207": {"inputs": {"shift": 5.0, "model": ["204", 0]}, "class_type": "ModelSamplingSD3"},
      "208": {"inputs": {"shift": 5.0, "model": ["206", 0]}, "class_type": "ModelSamplingSD3"},
      "209": {"inputs": {"width": 720, "height": 720, "length": 25, "batch_size": 1, "positive": ["201", 0], "negative": ["202", 0], "vae": ["214", 0], "start_image": ["2", 0], "end_image": ["3", 0]}, "class_type": "WanFirstLastFrameToVideo"},
      "210": {"inputs": {"add_noise": "enable", "noise_seed": 42, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 0, "end_at_step": 2, "return_with_leftover_noise": "enable", "model": ["207", 0], "positive": ["209", 0], "negative": ["209", 1], "latent_image": ["209", 2]}, "class_type": "KSamplerAdvanced"},
      "211": {"inputs": {"add_noise": "disable", "noise_seed": 0, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 2, "end_at_step": 10000, "return_with_leftover_noise": "disable", "model": ["208", 0], "positive": ["209", 0], "negative": ["209", 1], "latent_image": ["210", 0]}, "class_type": "KSamplerAdvanced"},
      "212": {"inputs": {"samples": ["211", 0], "vae": ["214", 0]}, "class_type": "VAEDecode"},
      "214": {"inputs": {"vae_name": "wan_2.1_vae.safetensors"}, "class_type": "VAELoader"},

      "300": {"inputs": {"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "type": "wan", "device": "default"}, "class_type": "CLIPLoader"},
      "301": {"inputs": {"text": "", "clip": ["300", 0]}, "class_type": "CLIPTextEncode"},
      "302": {"inputs": {"text": "色调艳丽，过曝，静态，细节模糊不清，字幕，风格，作品，画作，画面，静止，整体发灰，最差质量，低质量，JPEG压缩残留，丑陋的，残缺的，多余的手指，画得不好的手部，画得不好的脸部，畸形的，毁容的，形态畸变的肢体，手指融合，静止不动的画面，杂乱的背景，三条腿，背景人很多，倒着走", "clip": ["300", 0]}, "class_type": "CLIPTextEncode"},
      "303": {"inputs": {"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "304": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", "strength_model": 1.0, "model": ["303", 0]}, "class_type": "LoraLoaderModelOnly"},
      "305": {"inputs": {"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "306": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", "strength_model": 1.0, "model": ["305", 0]}, "class_type": "LoraLoaderModelOnly"},
      "307": {"inputs": {"shift": 5.0, "model": ["304", 0]}, "class_type": "ModelSamplingSD3"},
      "308": {"inputs": {"shift": 5.0, "model": ["306", 0]}, "class_type": "ModelSamplingSD3"},
      "309": {"inputs": {"width": 720, "height": 720, "length": 25, "batch_size": 1, "positive": ["301", 0], "negative": ["302", 0], "vae": ["314", 0], "start_image": ["3", 0], "end_image": ["4", 0]}, "class_type": "WanFirstLastFrameToVideo"},
      "310": {"inputs": {"add_noise": "enable", "noise_seed": 42, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 0, "end_at_step": 2, "return_with_leftover_noise": "enable", "model": ["307", 0], "positive": ["309", 0], "negative": ["309", 1], "latent_image": ["309", 2]}, "class_type": "KSamplerAdvanced"},
      "311": {"inputs": {"add_noise": "disable", "noise_seed": 0, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 2, "end_at_step": 10000, "return_with_leftover_noise": "disable", "model": ["308", 0], "positive": ["309", 0], "negative": ["309", 1], "latent_image": ["310", 0]}, "class_type": "KSamplerAdvanced"},
      "312": {"inputs": {"samples": ["311", 0], "vae": ["314", 0]}, "class_type": "VAEDecode"},
      "314": {"inputs": {"vae_name": "wan_2.1_vae.safetensors"}, "class_type": "VAELoader"},

      "400": {"inputs": {"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "type": "wan", "device": "default"}, "class_type": "CLIPLoader"},
      "401": {"inputs": {"text": "", "clip": ["400", 0]}, "class_type": "CLIPTextEncode"},
      "402": {"inputs": {"text": "色调艳丽，过曝，静态，细节模糊不清，字幕，风格，作品，画作，画面，静止，整体发灰，最差质量，低质量，JPEG压缩残留，丑陋的，残缺的，多余的手指，画得不好的手部，画得不好的脸部，畸形的，毁容的，形态畸变的肢体，手指融合，静止不动的画面，杂乱的背景，三条腿，背景人很多，倒着走", "clip": ["400", 0]}, "class_type": "CLIPTextEncode"},
      "403": {"inputs": {"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "404": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", "strength_model": 1.0, "model": ["403", 0]}, "class_type": "LoraLoaderModelOnly"},
      "405": {"inputs": {"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "406": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", "strength_model": 1.0, "model": ["405", 0]}, "class_type": "LoraLoaderModelOnly"},
      "407": {"inputs": {"shift": 5.0, "model": ["404", 0]}, "class_type": "ModelSamplingSD3"},
      "408": {"inputs": {"shift": 5.0, "model": ["406", 0]}, "class_type": "ModelSamplingSD3"},
      "409": {"inputs": {"width": 720, "height": 720, "length": 25, "batch_size": 1, "positive": ["401", 0], "negative": ["402", 0], "vae": ["414", 0], "start_image": ["4", 0], "end_image": ["5", 0]}, "class_type": "WanFirstLastFrameToVideo"},
      "410": {"inputs": {"add_noise": "enable", "noise_seed": 42, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 0, "end_at_step": 2, "return_with_leftover_noise": "enable", "model": ["407", 0], "positive": ["409", 0], "negative": ["409", 1], "latent_image": ["409", 2]}, "class_type": "KSamplerAdvanced"},
      "411": {"inputs": {"add_noise": "disable", "noise_seed": 0, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 2, "end_at_step": 10000, "return_with_leftover_noise": "disable", "model": ["408", 0], "positive": ["409", 0], "negative": ["409", 1], "latent_image": ["410", 0]}, "class_type": "KSamplerAdvanced"},
      "412": {"inputs": {"samples": ["411", 0], "vae": ["414", 0]}, "class_type": "VAEDecode"},
      "414": {"inputs": {"vae_name": "wan_2.1_vae.safetensors"}, "class_type": "VAELoader"},

      "500": {"inputs": {"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "type": "wan", "device": "default"}, "class_type": "CLIPLoader"},
      "501": {"inputs": {"text": "", "clip": ["500", 0]}, "class_type": "CLIPTextEncode"},
      "502": {"inputs": {"text": "色调艳丽，过曝，静态，细节模糊不清，字幕，风格，作品，画作，画面，静止，整体发灰，最差质量，低质量，JPEG压缩残留，丑陋的，残缺的，多余的手指，画得不好的手部，画得不好的脸部，畸形的，毁容的，形态畸变的肢体，手指融合，静止不动的画面，杂乱的背景，三条腿，背景人很多，倒着走", "clip": ["500", 0]}, "class_type": "CLIPTextEncode"},
      "503": {"inputs": {"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "504": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", "strength_model": 1.0, "model": ["503", 0]}, "class_type": "LoraLoaderModelOnly"},
      "505": {"inputs": {"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
      "506": {"inputs": {"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", "strength_model": 1.0, "model": ["505", 0]}, "class_type": "LoraLoaderModelOnly"},
      "507": {"inputs": {"shift": 5.0, "model": ["504", 0]}, "class_type": "ModelSamplingSD3"},
      "508": {"inputs": {"shift": 5.0, "model": ["506", 0]}, "class_type": "ModelSamplingSD3"},
      "509": {"inputs": {"width": 720, "height": 720, "length": 25, "batch_size": 1, "positive": ["501", 0], "negative": ["502", 0], "vae": ["514", 0], "start_image": ["5", 0], "end_image": ["6", 0]}, "class_type": "WanFirstLastFrameToVideo"},
      "510": {"inputs": {"add_noise": "enable", "noise_seed": 42, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 0, "end_at_step": 2, "return_with_leftover_noise": "enable", "model": ["507", 0], "positive": ["509", 0], "negative": ["509", 1], "latent_image": ["509", 2]}, "class_type": "KSamplerAdvanced"},
      "511": {"inputs": {"add_noise": "disable", "noise_seed": 0, "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "start_at_step": 2, "end_at_step": 10000, "return_with_leftover_noise": "disable", "model": ["508", 0], "positive": ["509", 0], "negative": ["509", 1], "latent_image": ["510", 0]}, "class_type": "KSamplerAdvanced"},
      "512": {"inputs": {"samples": ["511", 0], "vae": ["514", 0]}, "class_type": "VAEDecode"},
      "514": {"inputs": {"vae_name": "wan_2.1_vae.safetensors"}, "class_type": "VAELoader"},

      "601": {"inputs": {"image1": ["112", 0], "image2": ["212", 0]}, "class_type": "ImageBatch"},
      "602": {"inputs": {"image1": ["601", 0], "image2": ["312", 0]}, "class_type": "ImageBatch"},
      "603": {"inputs": {"image1": ["602", 0], "image2": ["412", 0]}, "class_type": "ImageBatch"},
      "604": {"inputs": {"image1": ["603", 0], "image2": ["512", 0]}, "class_type": "ImageBatch"},
      "700": {"inputs": {"fps": 24, "images": ["604", 0]}, "class_type": "CreateVideo"},
      "701": {"inputs": {"filename_prefix": "video/keyframes", "format": "auto", "codec": "auto", "video-preview": "", "video": ["700", 0]}, "class_type": "SaveVideo"}
    }
  }'
```

**响应：**

```json
{"prompt_id": "a1b2c3d4-...", "number": 140, "node_errors": {}}
```

> `node_errors` 为空对象 `{}` 表示工作流校验通过。如有错误会在此列出。

**使用前需修改：**

| 节点 | 字段 | 修改为 |
|------|------|--------|
| 1 | image | 关键帧 1 上传返回的 `name` 值 |
| 2 | image | 关键帧 2 上传返回的 `name` 值 |
| 3 | image | 关键帧 3 上传返回的 `name` 值 |
| 4 | image | 关键帧 4 上传返回的 `name` 值 |
| 5 | image | 关键帧 5 上传返回的 `name` 值 |
| 6 | image | 关键帧 6 上传返回的 `name` 值 |
| 101 | text | 第 1 段正向提示词（英文动作描述，如 `"cat turn around"`） |
| 201 | text | 第 2 段正向提示词（可留空 `""`） |
| 301 | text | 第 3 段正向提示词（可留空 `""`） |
| 401 | text | 第 4 段正向提示词（可留空 `""`） |
| 501 | text | 第 5 段正向提示词（可留空 `""`） |
| 701 | filename_prefix | 输出文件名前缀，如 `"video/my_keyframes"` |

### 段（Segment）结构说明

5 段使用完全相同的节点架构，仅输入图片和正向提示词不同：

| 段 | 节点前缀 | 起始帧 | 结束帧 | 正向提示词节点 |
|----|----------|--------|--------|---------------|
| 1 | 100-114 | LoadImage 1 (frame_1) | LoadImage 2 (frame_2) | 101 |
| 2 | 200-214 | LoadImage 2 (frame_2) | LoadImage 3 (frame_3) | 201 |
| 3 | 300-314 | LoadImage 3 (frame_3) | LoadImage 4 (frame_4) | 301 |
| 4 | 400-414 | LoadImage 4 (frame_4) | LoadImage 5 (frame_5) | 401 |
| 5 | 500-514 | LoadImage 5 (frame_5) | LoadImage 6 (frame_6) | 501 |

> 注意：相邻段共享关键帧图片。例如 frame_2 既是段 1 的 end_image，也是段 2 的 start_image。

### 每段节点说明

| 节点后缀 | class_type | 功能 |
|----------|-----------|------|
| x00 | CLIPLoader | 加载 Wan CLIP 文本编码器 |
| x01 | CLIPTextEncode | 正向提示词编码（英文动作描述） |
| x02 | CLIPTextEncode | 负向提示词编码（中文质量排除词） |
| x03 | UNETLoader | 加载高噪声 UNet 模型 |
| x04 | LoraLoaderModelOnly | 加载高噪声 LoRA（4 步加速） |
| x05 | UNETLoader | 加载低噪声 UNet 模型 |
| x06 | LoraLoaderModelOnly | 加载低噪声 LoRA（4 步加速） |
| x07 | ModelSamplingSD3 | 高噪声模型采样偏移（shift=5） |
| x08 | ModelSamplingSD3 | 低噪声模型采样偏移（shift=5） |
| x09 | WanFirstLastFrameToVideo | 首尾帧转视频核心节点 |
| x10 | KSamplerAdvanced | 高噪声采样（步骤 0→2） |
| x11 | KSamplerAdvanced | 低噪声采样（步骤 2→10000） |
| x12 | VAEDecode | VAE 解码 latent → 像素帧 |
| x14 | VAELoader | 加载 VAE 模型 |

### 合并与输出节点

| 节点 | class_type | 功能 |
|------|-----------|------|
| 601 | ImageBatch | 合并段 1 + 段 2 的帧 |
| 602 | ImageBatch | 合并 (段1+2) + 段 3 的帧 |
| 603 | ImageBatch | 合并 (段1+2+3) + 段 4 的帧 |
| 604 | ImageBatch | 合并 (段1+2+3+4) + 段 5 的帧 |
| 700 | CreateVideo | 创建视频，设置帧率 24fps |
| 701 | SaveVideo | 保存视频为 MP4 |

### 工作流连接图

```
段1: LoadImage(1)─┐
                  ├→ WanFLF(109) → KSamplerHigh(110) → KSamplerLow(111) → VAEDecode(112) ─┐
     LoadImage(2)─┘                                                                        │
                                                                                           ├→ ImageBatch(601) ─┐
段2: LoadImage(2)─┐                                                                        │                   │
                  ├→ WanFLF(209) → KSamplerHigh(210) → KSamplerLow(211) → VAEDecode(212) ─┘                   │
     LoadImage(3)─┘                                                                                           │
                                                                                           ├→ ImageBatch(602) ─┐
段3: LoadImage(3)─┐                                                                        │                   │
                  ├→ WanFLF(309) → ... → VAEDecode(312) ─────────────────────────────────┘                   │
     LoadImage(4)─┘                                                                                           │
                                                                                           ├→ ImageBatch(603) ─┐
段4: LoadImage(4)─┐                                                                        │                   │
                  ├→ WanFLF(409) → ... → VAEDecode(412) ─────────────────────────────────┘                   │
     LoadImage(5)─┘                                                                                           │
                                                                                           ├→ ImageBatch(604)
段5: LoadImage(5)─┐                                                                        │                   │
                  ├→ WanFLF(509) → ... → VAEDecode(512) ─────────────────────────────────┘                   │
     LoadImage(6)─┘                                                                                           │
                                                                                                             ↓
                                                                                             CreateVideo(700) → SaveVideo(701)
```

每段内部模型链：
```
UNETLoader(x03) → LoRA_High(x04) → ModelSampling(x07) ──→ KSamplerHigh(x10)
UNETLoader(x05) → LoRA_Low(x06)  → ModelSampling(x08) ──→ KSamplerLow(x11)
CLIPLoader(x00) → CLIPTextEncode_Pos(x01) ─┐
                  CLIPTextEncode_Neg(x02) ─┤→ WanFirstLastFrameToVideo(x09)
VAELoader(x14) ────────────────────────────┘
```

### 关键参数

| 节点 | 字段 | 说明 | 示例值 |
|------|------|------|--------|
| 1-6 | image | 6 张关键帧图片文件名 | `"frame_1.png"` ... `"frame_6.png"` |
| 101 | text | 第 1 段正向提示词（英文动作描述） | `"cat turn around"` |
| 201-501 | text | 第 2-5 段正向提示词（可留空） | `""` |
| x09 | width / height | 视频尺寸 | 720 × 720 |
| x09 | length | 每段帧数 | 25 |
| 700 | fps | 最终视频帧率 | 24 |
| x10 | noise_seed | 随机种子（不同值生成不同过渡） | 42 |
| 701 | filename_prefix | 输出文件名前缀 | `"video/keyframes"` |

### 输出视频参数

- **分辨率**: 720 × 720
- **每段帧数**: 25 帧 × 5 段 = 125 帧
- **帧率**: 24 fps
- **时长**: 约 5.2 秒（125 / 24）
- **格式**: MP4
- **输出路径**: `subfolder=video`, `type=output`

## Route B: Check Status

### 查询队列

```bash
curl -s -H "Authorization: Bearer $API_KEY" "$GW/api/v1/ai/queue"
```

**响应：**

```json
{
  "queue_running": [[140, "a1b2c3d4-...", {...}]],
  "queue_pending": []
}
```

- `queue_running` 不为空 → 任务正在执行
- `queue_pending` 不为空 → 任务排队等待

### 查询任务状态

```bash
curl -s -H "Authorization: Bearer $API_KEY" "$GW/api/v1/ai/tasks/{prompt_id}"
```

**进行中：**

```json
{
  "a1b2c3d4-...": {
    "status": {"status_str": "success", "completed": false},
    "outputs": {}
  }
}
```

**已完成：**

```json
{
  "a1b2c3d4-...": {
    "status": {"status_str": "success", "completed": true},
    "outputs": {
      "701": {
        "images": [{"filename": "keyframes_00001_.mp4", "subfolder": "video", "type": "output"}],
        "animated": [true]
      }
    }
  }
}
```

> 关键帧视频生成通常需要 1-5 分钟（5 段 × 每段约 15-30 秒，取决于 GPU 和队列情况）。

## Route C: Download

下载前先从任务状态中获取输出文件信息（`filename`, `subfolder`, `type`），然后通过查看接口下载。

```bash
# 下载 MP4 视频（注意 subfolder=video）
curl -s -H "Authorization: Bearer $API_KEY" \
  "$GW/api/v1/ai/image/view/?filename=keyframes_00001_.mp4&subfolder=video&type=output" \
  -o output.mp4
```

**关键点：**

- 视频输出的 `subfolder` 为 `video`（不是空字符串）
- 视频输出的 `type` 为 `output`
- 下载的是 MP4 格式，可直接播放

### Python 调用示例

```python
import requests
import time

GW = "https://ai.ospreyai.cn"
API_KEY = "sk-your-api-key"
headers = {"Authorization": f"Bearer {API_KEY}"}

# 1. 上传 6 张关键帧图片
image_names = []
for i in range(1, 7):
    with open(f"frame_{i}.png", "rb") as f:
        resp = requests.post(f"{GW}/api/v1/upload", headers=headers,
                             files={"image": f}, data={"overwrite": "true"})
        name = resp.json()["name"]
        image_names.append(name)
        print(f"Uploaded frame {i}: {name}")

# 2. 构建并提交工作流（完整 prompt 同上方 curl 示例）
# 替换节点 1-6 的 image 字段和节点 101 的 text 字段
prompt = {"prompt": {
    # ... 完整工作流 JSON，替换以下字段：
    # 节点 1-6: image 字段替换为 image_names[0]~[5]
    # 节点 101: 第 1 段正向提示词
    # 节点 201-501: 其余段正向提示词（可留空）
    # 节点 701: filename_prefix 设置输出文件名
}}
resp = requests.post(f"{GW}/api/v1/ai/video/generate", headers=headers,
                     json=prompt)
prompt_id = resp.json()["prompt_id"]
print(f"Task submitted: {prompt_id}")

# 3. 轮询任务状态
while True:
    resp = requests.get(f"{GW}/api/v1/ai/tasks/{prompt_id}", headers=headers)
    data = resp.json()
    task = data.get(prompt_id, {})
    status = task.get("status", {})
    if status.get("completed"):
        print("Task completed!")
        break
    print(f"Status: {status.get('status_str', 'unknown')}...")
    time.sleep(10)

# 4. 下载视频
outputs = task.get("outputs", {}).get("701", {})
video_info = outputs.get("images", [{}])[0]
resp = requests.get(f"{GW}/api/v1/ai/image/view/", headers=headers,
                    params={"filename": video_info["filename"],
                            "subfolder": video_info.get("subfolder", ""),
                            "type": video_info.get("type", "output")})
with open("keyframes_output.mp4", "wb") as f:
    f.write(resp.content)
print(f"Downloaded: keyframes_output.mp4 ({len(resp.content)} bytes)")
```

## Route D: Tune Parameters

| 参数 | 节点 | 建议 |
|------|------|------|
| 关键帧图片 | 1-6 | 推荐 PNG，尺寸一致（建议 720×720），清晰度越高越好 |
| 正向提示词（段 1） | 101 text | 英文动作描述，如 `"cat turn around"` |
| 正向提示词（段 2-5） | 201-501 text | 可留空；也可分别为每段写动作描述 |
| 负向提示词 | x02 text | 默认可保持，避免修改 |
| 视频尺寸 | x09 width/height | 默认 720×720；支持 640×640、832×480 等，需为 16 的倍数 |
| 每段帧数 | x09 length | 默认 25（≈1s@24fps）；增大则每段过渡更长 |
| 最终帧率 | 700 fps | 默认 24；增大则视频更流畅但更快播完 |
| 随机种子 | x10 noise_seed | 不同值生成不同过渡动画；相同种子 + 相同参数可复现 |
| 采样步数 | x10/x11 steps | LightX2V 加速模式下固定 4 步，不建议修改 |
| 采样偏移 | x07/x08 shift | 默认 5.0；增大偏移更锐利，减小更平滑 |
| 关键帧数量 | — | 当前工作流固定 6 张；增减需添加/删除整段节点组 |

### 调整关键帧数量

当前工作流固定 6 张关键帧 → 5 段过渡。如需调整数量：

- **增加关键帧**：复制一个完整的段节点组（如 200-214），修改 LoadImage 引用，在 ImageBatch 链中增加一个合并节点
- **减少关键帧**：删除对应段节点组，缩短 ImageBatch 链

> 提示：每增加一段，生成时间约增加 15-30 秒。

## Route E: Troubleshoot

| 问题 | 排查方法 |
|------|---------|
| 上传图片失败 | 检查文件大小（≤50MB）、格式（推荐 PNG）、Bearer Token 是否有效 |
| 工作流提交报 `value_not_in_list` | 模型文件名不正确，检查 UNet/CLIP/VAE/LoRA 名称是否与服务器一致 |
| 工作流提交报 `return_type_mismatch` | 节点间连接类型不匹配，检查节点输出→输入的链接是否正确 |
| 工作流报 ImageBatch 类型错误 | 确认 VAEDecode 输出连接到 ImageBatch 的 image1/image2 |
| 任务长时间未完成 | 5 段生成需要较长时间（1-5分钟），检查 `/api/v1/ai/queue` 确认是否在运行 |
| 下载视频返回空 | 检查 `subfolder` 是否为 `video`（不是空字符串） |
| 视频段之间有明显跳变 | 确保相邻段共享同一关键帧（如 frame_2 同时是段 1 end 和段 2 start） |
| 401 鉴权失败 | 检查 Bearer Token 是否有效：`curl -H "Authorization: Bearer sk-xxx" $GW/api/v1/ai/queue` |
| 429 请求被限流 | AI 接口 10次/分/IP，稍后重试 |

### 内网直连 vs 公网网关

| | 内网直连 | 公网网关 |
|---|---|---|
| 地址 | `192.168.1.236:8188` | `ai.ospreyai.cn` |
| 鉴权 | 无 | `Authorization: Bearer sk-xxx` |
| 上传路径 | `/upload/image` | `/api/v1/upload` |
| 提交路径 | `/prompt` | `/api/v1/ai/video/generate` |
| 任务查询 | `/history/{id}` | `/api/v1/ai/tasks/{id}` |
| 队列查询 | `/queue` | `/api/v1/ai/queue` |
| 下载路径 | `/view?filename=...` | `/api/v1/ai/image/view/?filename=...&subfolder=video&type=output` |
| 限流 | 无 | 10次/分/IP |

## Verification Checklist

- [ ] 6 张关键帧图片上传成功，各自返回 `name` 值
- [ ] 工作流提交成功，`node_errors` 为空
- [ ] 任务在 `/api/v1/ai/queue` 中执行完成
- [ ] 任务状态 `completed: true`，outputs 包含节点 701 的 MP4 文件信息
- [ ] 成功下载 MP4 文件（注意 `subfolder=video`）
- [ ] 视频可正常播放，段间过渡自然
- [ ] 视频分辨率为 720×720，帧率 24fps
