Appearance
下载原始 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 助手对话式接入。
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.mp4Task 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
基本流程
- 上传 6 张关键帧图片到 ComfyUI
- 提交首尾帧视频工作流
- 工作流架构:
- 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)
- 6 ×
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