纯 ComfyUI 视频生成工作流方案设计
方案类型 : 纯 ComfyUI 工程 + 本地模型调用
适用场景 : 本地视频生成、批量处理、工作流自动化
技术栈: ComfyUI + Python API + 本地模型
一、方案概述
1.1 架构设计
┌─────────────────────────────────────────────────────────────┐
│ ComfyUI 单机架构 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTTP Server │ │ WebSocket │ │ Web UI │ │
│ │ (8188) │ │ Server │ │ (可视化) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
│ │ │ │
│ └─────────────────┼─────────────────────────────────┤
│ │ │
│ ┌────────────────────────▼───────────────────────────────┐│
│ │ ComfyUI 核心 ││
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
│ │ │ 工作流引擎 │ │ 节点系统 │ │ 任务调度 │ ││
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ ││
│ │ │ │ │ ││
│ │ ┌──────▼─────────────────┼──────────────────▼───────┐││
│ │ │ 节点执行引擎 │││
│ │ └───────────────────────────────────────────────────┘││
│ └──────────────────────────────┬────────────────────────┘│
│ │ │
│ ┌──────────────────────────────▼────────────────────────┐│
│ │ 本地模型文件 ││
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
│ │ │ LTX Video │ │ Wan 2.2 │ │ CogVideoX │ ││
│ │ │ 8.7GB │ │ 10GB │ │ 6GB │ ││
│ │ └──────────────┘ └──────────────┘ └──────────────┘ ││
│ └────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
Python API 调用脚本
1.2 核心组件
| 组件 | 功能 | 状态 |
|---|---|---|
| ComfyUI 服务 | 提供工作流执行环境 | ✅ 已安装 |
| HTTP API | 提供RESTful接口 | ✅ 可用 |
| WebSocket | 实时进度推送 | ✅ 可用 |
| 工作流引擎 | 解析和执行JSON工作流 | ✅ 可用 |
| 节点系统 | 支持100+种节点 | ✅ 可用 |
| 本地模型 | LTX/Wan/CogVideoX | ✅ 已下载 |
二、工作流设计
2.1 LTX Video 工作流
工作流结构
json
{
"1": {
"inputs": {
"text_encoder": "gemma_3_12B_it_fp4_mixed.safetensors",
"ckpt_name": "ltx-video-2b-v0.9.safetensors"
},
"class_type": "LTXAVTextEncoderLoader",
"_meta": {"title": "LTX 模型加载器"}
},
"2": {
"inputs": {
"text": "${prompt}",
"clip": ["1", 0]
},
"class_type": "CLIPTextEncode",
"_meta": {"title": "正向提示词"}
},
"3": {
"inputs": {
"text": "${negative_prompt}",
"clip": ["1", 0]
},
"class_type": "CLIPTextEncode",
"_meta": {"title": "负面提示词"}
},
"4": {
"inputs": {
"width": ${width},
"height": ${height},
"batch_size": 1,
"frame_count": ${frames}
},
"class_type": "EmptyLTXVLatentVideo",
"_meta": {"title": "空潜空间视频"}
},
"5": {
"inputs": {
"model": ["1", 0],
"add_noise": "enable",
"noise_seed": ${seed},
"steps": ${steps},
"cfg": ${cfg},
"sampler_name": "${sampler}",
"scheduler": "${scheduler}",
"positive": ["2", 0],
"negative": ["3", 0],
"latent_image": ["4", 0],
"start_at_step": 0,
"end_at_step": ${steps},
"return_noise": "disable"
},
"class_type": "KSamplerAdvanced",
"_meta": {"title": "高级采样器"}
},
"6": {
"inputs": {
"samples": ["5", 0],
"vae": ["1", 1]
},
"class_type": "VAEDecodeTiled",
"_meta": {"title": "分块 VAE 解码"}
},
"7": {
"inputs": {
"filename_prefix": "${filename_prefix}"
},
"class_type": "SaveVideo",
"_meta": {"title": "保存视频"}
}
}
节点说明
| 节点ID | 节点类型 | 功能 | 输入 |
|---|---|---|---|
| 1 | LTXAVTextEncoderLoader | 加载LTX模型和文本编码器 | 模型文件路径 |
| 2 | CLIPTextEncode | 编码正向提示词 | 提示词 + CLIP |
| 3 | CLIPTextEncode | 编码负面提示词 | 负面提示词 + CLIP |
| 4 | EmptyLTXVLatentVideo | 创建空潜空间视频 | 分辨率 + 帧数 |
| 5 | KSamplerAdvanced | 执行Diffusion采样 | 所有输入 + 参数 |
| 6 | VAEDecodeTiled | 分块解码潜空间 | 采样结果 + VAE |
| 7 | SaveVideo | 保存生成的视频 | 解码结果 |
2.2 Wan 2.2 工作流
关键节点差异
json
{
"1": {
"inputs": {
"ckpt_name": "wan2.2_fp16.safetensors"
},
"class_type": "CheckpointLoaderSimple"
},
"2": {
"inputs": {
"text": "${prompt}",
"width": ${width},
"height": ${height},
"video_length": ${frames}
},
"class_type": "WanTextToVideo"
},
"3": {
"inputs": {
"model": ["1", 0],
"positive": ["2", 0],
"steps": ${steps},
"cfg": ${cfg}
},
"class_type": "WanSampler"
}
}
2.3 CogVideoX 工作流
关键节点差异
json
{
"1": {
"inputs": {
"ckpt_name": "CogVideoX-2b.safetensors"
},
"class_type": "CogVideoXLoader"
},
"2": {
"inputs": {
"text": "${prompt}",
"num_frames": ${frames},
"image_size": ${width}
},
"class_type": "CogVideoXTextToVideo"
}
}
三、API 调用方案
3.1 基础 HTTP API
1. 发送生成请求
python
import requests
import json
# ComfyUI 服务地址
COMFYUI_URL = "http://127.0.0.1:8188"
# 加载工作流模板
with open('ltx_workflow.json', 'r') as f:
workflow = json.load(f)
# 填充参数
parameters = {
"prompt": "A beautiful sunset over the ocean, cinematic lighting",
"negative_prompt": "blurry, low quality, distorted",
"width": 1024,
"height": 576,
"frames": 100, # 4秒 @ 25fps
"steps": 20,
"cfg": 7.0,
"seed": 42,
"sampler": "euler",
"scheduler": "simple",
"filename_prefix": "sunset_video"
}
# 填充工作流
for node_id, node in workflow.items():
for key, value in node.get("inputs", {}).items():
if isinstance(value, str) and value.startswith("${"):
param_name = value[2:-1]
if param_name in parameters:
workflow[node_id]["inputs"][key] = parameters[param_name]
# 发送请求
response = requests.post(
f"{COMFYUI_URL}/prompt",
json={"prompt": workflow}
)
prompt_id = response.json()["prompt_id"]
print(f"✅ 任务已提交,ID: {prompt_id}")
2. 查询生成进度
python
import time
# 轮询查询结果
while True:
response = requests.get(f"{COMFYUI_URL}/history/{prompt_id}")
data = response.json()
if prompt_id in data:
result = data[prompt_id]
print(f"✅ 生成完成!")
# 提取结果
outputs = result.get("outputs", {})
for node_id, node_outputs in outputs.items():
for output_name, output_list in node_outputs.items():
if output_name == "video":
filename = output_list[0]["filename"]
print(f"📁 视频文件: {filename}")
print(f"📂 输出目录: ComfyUI/output/")
break
break
else:
print(f"⏳ 生成中... 已等待 {i * 2} 秒")
time.sleep(2)
i += 1
3.2 WebSocket 实时进度
python
import asyncio
import websockets
import json
async def watch_generation_progress(prompt_id):
"""监听生成进度"""
uri = f"ws://127.0.0.1:8188/ws?clientId={prompt_id}"
async with websockets.connect(uri) as websocket:
print(f"📡 已连接到 WebSocket: {prompt_id}")
while True:
message = await websocket.recv()
data = json.loads(message)
if data.get("type") == "status":
progress = data.get("data", {}).get("value", 0)
node = data.get("data", {}).get("node", "")
print(f"⏳ 节点 {node} 进度: {progress}%")
elif data.get("type") == "executing":
print(f"🔄 正在执行: {data.get('data', {}).get('node', '')}")
elif data.get("type") == "execution_success":
print(f"✅ 执行完成")
break
# 使用
asyncio.run(watch_generation_progress(prompt_id))
3.3 完整的 Python 调用类
python
import requests
import asyncio
import websockets
import json
import time
from pathlib import Path
from typing import Optional, Dict, Any
class ComfyUIVideoGenerator:
"""ComfyUI 视频生成器"""
def __init__(self, base_url: str = "http://127.0.0.1:8188"):
self.base_url = base_url
self.session = requests.Session()
def generate_video(
self,
workflow_path: str,
parameters: Dict[str, Any],
watch_progress: bool = True
) -> Dict[str, Any]:
"""
生成视频
Args:
workflow_path: 工作流 JSON 文件路径
parameters: 生成参数
watch_progress: 是否监听进度
Returns:
生成结果
"""
# 1. 加载工作流
with open(workflow_path, 'r') as f:
workflow = json.load(f)
# 2. 填充参数
filled_workflow = self._fill_parameters(workflow, parameters)
# 3. 发送请求
response = self.session.post(
f"{self.base_url}/prompt",
json={"prompt": filled_workflow}
)
prompt_id = response.json()["prompt_id"]
print(f"🚀 任务已提交: {prompt_id}")
# 4. 等待结果
if watch_progress:
asyncio.run(self._watch_progress(prompt_id))
else:
self._wait_for_completion(prompt_id)
# 5. 获取结果
result = self._get_result(prompt_id)
return {
"prompt_id": prompt_id,
"status": "completed",
"result": result
}
def _fill_parameters(
self,
workflow: Dict[str, Any],
parameters: Dict[str, Any]
) -> Dict[str, Any]:
"""填充工作流参数"""
filled_workflow = workflow.copy()
for node_id, node in filled_workflow.items():
for key, value in node.get("inputs", {}).items():
if isinstance(value, str) and value.startswith("${"):
param_name = value[2:-1]
if param_name in parameters:
filled_workflow[node_id]["inputs"][key] = parameters[param_name]
return filled_workflow
def _wait_for_completion(self, prompt_id: str):
"""等待生成完成"""
print("⏳ 等待生成完成...")
i = 0
while True:
response = self.session.get(f"{self.base_url}/history/{prompt_id}")
data = response.json()
if prompt_id in data:
print("✅ 生成完成!")
break
if i % 5 == 0: # 每10秒输出一次
print(f"⏳ 已等待 {i * 2} 秒...")
time.sleep(2)
i += 1
async def _watch_progress(self, prompt_id: str):
"""监听生成进度"""
uri = f"ws://127.0.0.1:8188/ws?clientId={prompt_id}"
async with websockets.connect(uri) as websocket:
print(f"📡 已连接到 WebSocket: {prompt_id}")
while True:
message = await websocket.recv()
data = json.loads(message)
if data.get("type") == "status":
progress = data.get("data", {}).get("value", 0)
node = data.get("data", {}).get("node", "")
print(f"⏳ {node}: {progress}%")
elif data.get("type") == "executing":
print(f"🔄 {data.get('data', {}).get('node', '')}")
elif data.get("type") == "execution_success":
print("✅ 执行完成")
break
def _get_result(self, prompt_id: str) -> Dict[str, Any]:
"""获取生成结果"""
response = self.session.get(f"{self.base_url}/history/{prompt_id}")
data = response.json()
result_data = data[prompt_id]
outputs = result_data.get("outputs", {})
# 提取视频文件信息
videos = []
for node_id, node_outputs in outputs.items():
for output_name, output_list in node_outputs.items():
if output_name in ["video", "output"]:
for item in output_list:
videos.append({
"filename": item["filename"],
"subfolder": item.get("subfolder", ""),
"type": item.get("type", "output")
})
return {
"videos": videos,
"output_dir": "ComfyUI/output/",
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
四、本地模型配置
4.1 模型文件结构
bash
ComfyUI/
├── models/
│ ├── checkpoints/ # 主模型目录
│ │ ├── ltx-video-2b-v0.9.safetensors # LTX 主模型 (8.7GB)
│ │ └── svd_xt.safetensors # SVD 模型 (1.5GB)
│ │
│ ├── text_encoders/ # 文本编码器目录
│ │ └── gemma_3_12B_it_fp4_mixed.safetensors # LTX 文本编码器 (8.8GB)
│ │
│ ├── clip_vision/ # CLIP Vision 目录
│ │ └── clip_vision_g.safetensors # CLIP Vision (1.6GB)
│ │
│ ├── loras/ # LoRA 目录(可选)
│ │ └── put_loras_here
│ │
│ └── embeddings/ # 嵌入向量目录(可选)
│ └── put_embeddings_or_textual_inversion_concepts_here
│
└── output/ # 输出目录
├── videos/ # 视频输出
└── images/ # 图片输出
4.2 模型下载脚本
python
#!/usr/bin/env python3
"""
模型下载脚本
自动下载所需模型文件
"""
import os
import requests
from pathlib import Path
from typing import Dict, Any
class ModelDownloader:
"""模型下载器"""
def __init__(self, models_dir: str = "ComfyUI/models"):
self.models_dir = Path(models_dir)
self.models_dir.mkdir(parents=True, exist_ok=True)
def download_file(
self,
url: str,
destination: Path,
chunk_size: int = 8192
):
"""下载文件"""
destination.parent.mkdir(parents=True, exist_ok=True)
print(f"📥 下载: {destination.name}")
print(f"🌐 URL: {url}")
# 显示文件大小
head_response = requests.head(url, allow_redirects=True)
total_size = int(head_response.headers.get('content-length', 0))
print(f"📏 文件大小: {total_size / (1024**3):.2f} GB")
# 流式下载
response = requests.get(url, stream=True)
response.raise_for_status()
downloaded = 0
with open(destination, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
f.write(chunk)
downloaded += len(chunk)
# 显示进度
percent = (downloaded / total_size) * 100
print(f"\r⏳ 进度: {percent:.1f}%", end='', flush=True)
print(f"\n✅ 下载完成: {destination.name}")
def download_ltx_models(self):
"""下载 LTX 模型"""
models = {
"ltx-video-2b-v0.9.safetensors": {
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.safetensors",
"dest": self.models_dir / "checkpoints"
},
"gemma_3_12B_it_fp4_mixed.safetensors": {
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/gemma_3_12B_it_fp4_mixed.safetensors",
"dest": self.models_dir / "text_encoders"
},
"clip_vision_g.safetensors": {
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/clip_vision_g.safetensors",
"dest": self.models_dir / "clip_vision"
}
}
for filename, config in models.items():
dest_path = config["dest"] / filename
self.download_file(config["url"], dest_path)
# 使用
if __name__ == "__main__":
downloader = ModelDownloader()
downloader.download_ltx_models()
4.3 模型验证脚本
python
#!/usr/bin/env python3
"""
模型验证脚本
检查模型文件是否完整
"""
import hashlib
from pathlib import Path
from typing import Dict, Optional
class ModelValidator:
"""模型验证器"""
def __init__(self, models_dir: str = "ComfyUI/models"):
self.models_dir = Path(models_dir)
def validate_ltx_models(self) -> bool:
"""验证 LTX 模型"""
required_models = {
"checkpoints/ltx-video-2b-v0.9.safetensors": 9_340_377_792, # 8.7GB
"text_encoders/gemma_3_12B_it_fp4_mixed.safetensors": 9_457_694_720, # 8.8GB
"clip_vision/clip_vision_g.safetensors": 1_723_286_016 # 1.6GB
}
all_valid = True
for model_path, expected_size in required_models.items():
full_path = self.models_dir / model_path
if not full_path.exists():
print(f"❌ 缺失: {model_path}")
all_valid = False
continue
actual_size = full_path.stat().st_size
size_diff = abs(actual_size - expected_size)
tolerance = expected_size * 0.01 # 允许1%误差
if size_diff > tolerance:
print(f"❌ 大小不匹配: {model_path}")
print(f" 期望: {expected_size / (1024**3):.2f} GB")
print(f" 实际: {actual_size / (1024**3):.2f} GB")
all_valid = False
else:
print(f"✅ 完整: {model_path} ({actual_size / (1024**3):.2f} GB)")
return all_valid
def get_model_summary(self) -> Dict[str, Any]:
"""获取模型摘要"""
summary = {
"total_size": 0,
"model_count": 0,
"models": []
}
for model_file in self.models_dir.rglob("*.safetensors"):
size = model_file.stat().st_size
summary["total_size"] += size
summary["model_count"] += 1
summary["models"].append({
"name": model_file.name,
"path": str(model_file.relative_to(self.models_dir)),
"size_gb": size / (1024**3),
"size_mb": size / (1024**2)
})
return summary
# 使用
if __name__ == "__main__":
validator = ModelValidator()
print("🔍 验证 LTX 模型...")
is_valid = validator.validate_ltx_models()
if is_valid:
print("\n✅ 所有模型文件完整!")
else:
print("\n❌ 部分模型文件缺失或损坏,请重新下载")
summary = validator.get_model_summary()
print(f"\n📊 模型摘要:")
print(f" 模型数量: {summary['model_count']}")
print(f" 总大小: {summary['total_size'] / (1024**3):.2f} GB")
五、批量处理方案
5.1 批量视频生成
python
import asyncio
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Any
class BatchVideoGenerator:
"""批量视频生成器"""
def __init__(self, max_concurrent: int = 2):
self.generator = ComfyUIVideoGenerator()
self.max_concurrent = max_concurrent
def generate_batch(
self,
prompts: List[str],
workflow_path: str,
common_params: Dict[str, Any],
individual_params: Optional[List[Dict[str, Any]]] = None
) -> List[Dict[str, Any]]:
"""
批量生成视频
Args:
prompts: 提示词列表
workflow_path: 工作流文件路径
common_params: 通用参数
individual_params: 每个提示词的独立参数
Returns:
生成结果列表
"""
results = []
with ThreadPoolExecutor(max_workers=self.max_concurrent) as executor:
# 提交任务
futures = {}
for i, prompt in enumerate(prompts):
params = common_params.copy()
params["prompt"] = prompt
if individual_params and i < len(individual_params):
params.update(individual_params[i])
params["filename_prefix"] = f"batch_{i:03d}"
future = executor.submit(
self.generator.generate_video,
workflow_path,
params,
watch_progress=False
)
futures[future] = i
# 收集结果
for future in as_completed(futures):
i = futures[future]
try:
result = future.result()
results.append(result)
print(f"✅ 任务 {i+1}/{len(prompts)} 完成")
except Exception as e:
print(f"❌ 任务 {i+1}/{len(prompts)} 失败: {e}")
results.append({
"index": i,
"status": "failed",
"error": str(e)
})
return results
# 使用示例
batch_generator = BatchVideoGenerator(max_concurrent=2)
prompts = [
"A beautiful sunset over the ocean",
"A rainy city street at night",
"A mountain landscape in autumn",
"A futuristic cyberpunk city",
"A peaceful garden with flowers"
]
results = batch_generator.generate_batch(
prompts=prompts,
workflow_path="ltx_workflow.json",
common_params={
"model": "ltx",
"duration": 4,
"resolution": "1024x576"
}
)
5.2 配置文件管理
python
import json
import yaml
from pathlib import Path
from typing import Dict, Any
class WorkflowConfig:
"""工作流配置管理"""
def __init__(self, config_dir: str = "configs"):
self.config_dir = Path(config_dir)
self.config_dir.mkdir(parents=True, exist_ok=True)
def load_config(self, config_name: str) -> Dict[str, Any]:
"""加载配置"""
config_path = self.config_dir / f"{config_name}.yaml"
with open(config_path, 'r') as f:
return yaml.safe_load(f)
def save_config(self, config_name: str, config: Dict[str, Any]):
"""保存配置"""
config_path = self.config_dir / f"{config_name}.yaml"
with open(config_path, 'w') as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
def create_ltx_config(self):
"""创建 LTX 配置"""
config = {
"model": "ltx",
"workflow": "ltx_workflow.json",
"default_parameters": {
"width": 1024,
"height": 576,
"frames": 100,
"steps": 20,
"cfg": 7.0,
"sampler": "euler",
"scheduler": "simple"
},
"presets": {
"cinematic": {
"prompt_suffix": ", cinematic lighting, 4k, highly detailed",
"cfg": 7.5
},
"anime": {
"prompt_suffix": ", anime style, vibrant colors, detailed",
"cfg": 8.0
},
"realistic": {
"prompt_suffix": ", photorealistic, highly detailed, 8k",
"cfg": 7.0
}
}
}
self.save_config("ltx", config)
# 使用
config_manager = WorkflowConfig()
# 创建配置
config_manager.create_ltx_config()
# 加载配置
ltx_config = config_manager.load_config("ltx")
print(f"📋 默认参数: {ltx_config['default_parameters']}")
# 应用预设
preset = ltx_config["presets"]["cinematic"]
prompt = "A beautiful sunset" + preset["prompt_suffix"]
cfg = preset["cfg"]
六、完整使用示例
6.1 单视频生成
python
#!/usr/bin/env python3
"""
单视频生成示例
"""
from comfyui_video_generator import ComfyUIVideoGenerator
# 创建生成器
generator = ComfyUIVideoGenerator()
# 生成参数
parameters = {
"prompt": "A beautiful sunset over the ocean, cinematic lighting",
"negative_prompt": "blurry, low quality, distorted",
"width": 1024,
"height": 576,
"frames": 100, # 4秒 @ 25fps
"steps": 20,
"cfg": 7.0,
"seed": 42,
"sampler": "euler",
"scheduler": "simple",
"filename_prefix": "sunset_video"
}
# 生成视频
result = generator.generate_video(
workflow_path="ltx_workflow.json",
parameters=parameters,
watch_progress=True
)
print(f"🎉 生成完成!")
print(f"📁 视频: {result['result']['videos'][0]['filename']}")
print(f"📂 路径: {result['result']['output_dir']}")
6.2 批量生成
python
#!/usr/bin/env python3
"""
批量视频生成示例
"""
from batch_video_generator import BatchVideoGenerator
# 创建批量生成器
batch_generator = BatchVideoGenerator(max_concurrent=2)
# 提示词列表
prompts = [
"A beautiful sunset over the ocean",
"A rainy city street at night",
"A mountain landscape in autumn",
"A futuristic cyberpunk city"
]
# 通用参数
common_params = {
"model": "ltx",
"resolution": "1024x576",
"duration": 4,
"fps": 25,
"steps": 20,
"cfg": 7.0
}
# 批量生成
results = batch_generator.generate_batch(
prompts=prompts,
workflow_path="ltx_workflow.json",
common_params=common_params
)
# 统计结果
success_count = sum(1 for r in results if r["status"] == "completed")
failed_count = len(results) - success_count
print(f"📊 批量生成完成:")
print(f" 总数: {len(results)}")
print(f" 成功: {success_count}")
print(f" 失败: {failed_count}")
6.3 命令行工具
python
#!/usr/bin/env python3
"""
ComfyUI 视频生成命令行工具
"""
import argparse
import json
from pathlib import Path
from comfyui_video_generator import ComfyUIVideoGenerator
def main():
parser = argparse.ArgumentParser(description="ComfyUI 视频生成工具")
parser.add_argument("--prompt", required=True, help="生成提示词")
parser.add_argument("--model", default="ltx", help="模型选择 (ltx/wan/cog)")
parser.add_argument("--workflow", help="工作流文件路径")
parser.add_argument("--resolution", default="1024x576", help="分辨率")
parser.add_argument("--duration", type=int, default=4, help="时长(秒)")
parser.add_argument("--steps", type=int, default=20, help="采样步数")
parser.add_argument("--cfg", type=float, default=7.0, help="CFG Scale")
parser.add_argument("--seed", type=int, default=-1, help="随机种子")
parser.add_argument("--output", help="输出文件名前缀")
args = parser.parse_args()
# 解析分辨率
width, height = map(int, args.resolution.split("x"))
frames = args.duration * 25 # 假设 25fps
# 参数字典
parameters = {
"prompt": args.prompt,
"width": width,
"height": height,
"frames": frames,
"steps": args.steps,
"cfg": args.cfg,
"seed": args.seed if args.seed >= 0 else -1,
"filename_prefix": args.output or "generated_video"
}
# 选择工作流
if args.workflow:
workflow_path = args.workflow
else:
workflow_path = f"{args.model}_workflow.json"
# 创建生成器
generator = ComfyUIVideoGenerator()
# 生成视频
result = generator.generate_video(
workflow_path=workflow_path,
parameters=parameters,
watch_progress=True
)
print(f"✅ 生成完成!")
print(f"📁 视频: {result['result']['videos'][0]['filename']}")
print(f"📂 路径: {result['result']['output_dir']}")
if __name__ == "__main__":
main()
使用示例:
bash
# 基础用法
python video_cli.py \
--prompt "A beautiful sunset over the ocean" \
--model ltx \
--resolution 1024x576 \
--duration 4
# 完整参数
python video_cli.py \
--prompt "A futuristic cyberpunk city at night" \
--model ltx \
--resolution 1280x720 \
--duration 8 \
--steps 25 \
--cfg 7.5 \
--seed 42 \
--output cyberpunk_city
# 使用自定义工作流
python video_cli.py \
--prompt "Anime style girl dancing" \
--workflow custom_workflow.json \
--output anime_girl
七、部署和启动
7.1 ComfyUI 启动脚本
bash
#!/bin/bash
# start-comfyui.sh
echo "🚀 启动 ComfyUI 服务..."
# 检查端口
if lsof -Pi :8188 -sTCP:LISTEN -t >/dev/null ; then
echo "⚠️ 端口 8188 已被占用"
exit 1
fi
# 设置环境
export PYTORCH_ENABLE_MPS_FALLBACK=1
export PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0
# 启动 ComfyUI
cd ComfyUI
echo "📍 工作目录: $(pwd)"
echo "📊 系统信息:"
echo " macOS: $(sw_vers -productVersion)"
echo " 芯片: $(sysctl -n machdep.cpu.brand_string)"
echo " 内存: $(system_profiler SPHardwareDataType | grep "Memory:" | awk '{print $2}')"
# 启动服务
echo ""
echo "🎯 启动 ComfyUI 服务..."
python main.py \
--listen 0.0.0.0 \
--port 8188 \
--output-directory ./output \
--disable-auto-launch \
--enable-cors-header "*"
7.2 系统服务配置
创建 ~/Library/LaunchAgents/com.comfyui.plist:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.comfyui</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/baohan/Desktop/Engineering-playbook/flowai/ComfyUI/main.py</string>
<string>--listen</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8188</string>
<string>--output-directory</string>
<string>./output</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/baohan/Desktop/Engineering-playbook/flowai/ComfyUI</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/comfyui.log</string>
<key>StandardErrorPath</key>
<string>/tmp/comfyui_error.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PYTORCH_ENABLE_MPS_FALLBACK</key>
<string>1</string>
<key>PYTORCH_MPS_HIGH_WATERMARK_RATIO</key>
<string>0.0</string>
</dict>
</dict>
</plist>
加载服务:
bash
# 加载服务
launchctl load ~/Library/LaunchAgents/com.comfyui.plist
# 启动服务
launchctl start com.comfyui
# 查看状态
launchctl list | grep comfyui
八、性能优化
8.1 模型预加载
python
import requests
def preload_models():
"""预加载模型到内存"""
print("🔧 预加载模型...")
# 发送预加载请求
response = requests.post(
"http://127.0.0.1:8188/model/load",
json={
"model": "ltx-video-2b-v0.9.safetensors",
"text_encoder": "gemma_3_12B_it_fp4_mixed.safetensors"
}
)
if response.status_code == 200:
print("✅ 模型预加载完成")
else:
print(f"❌ 预加载失败: {response.text}")
# 在 ComfyUI 启动后立即调用
preload_models()
8.2 内存管理
python
import psutil
def check_memory_usage():
"""检查内存使用"""
process = psutil.Process()
memory_info = process.memory_info()
rss_gb = memory_info.rss / (1024**3)
vms_gb = memory_info.vms / (1024**3)
print(f"📊 内存使用:")
print(f" RSS: {rss_gb:.2f} GB")
print(f" VMS: {vms_gb:.2f} GB")
# 检查系统内存
sys_memory = psutil.virtual_memory()
available_gb = sys_memory.available / (1024**3)
print(f" 可用: {available_gb:.2f} GB")
if available_gb < 10:
print("⚠️ 可用内存不足 10GB,建议释放一些任务")
# 在生成前检查
check_memory_usage()
8.3 性能监控
python
import time
import requests
def monitor_generation(prompt_id):
"""监控生成性能"""
start_time = time.time()
# 轮询查询
while True:
response = requests.get(f"http://127.0.0.1:8188/history/{prompt_id}")
data = response.json()
if prompt_id in data:
end_time = time.time()
duration = end_time - start_time
print(f"⏱️ 生成耗时: {duration:.2f} 秒")
print(f"⚡ 平均速度: {duration/4:.2f} 秒/秒")
# 获取视频信息
outputs = data[prompt_id].get("outputs", {})
for node_id, node_outputs in outputs.items():
for output_name, output_list in node_outputs.items():
if output_name == "video":
filename = output_list[0]["filename"]
file_size = output_list[0].get("size", 0)
print(f"📁 文件: {filename}")
print(f"📏 大小: {file_size / (1024**2):.2f} MB")
if file_size > 0:
speed = (file_size / (1024**2)) / duration
print(f"🚀 速度: {speed:.2f} MB/秒")
break
time.sleep(2)
九、故障排查
9.1 常见问题
问题 1: ComfyUI 无法启动
bash
# 检查端口
lsof -i :8188
# 检查权限
ls -la ComfyUI/
# 查看日志
tail -f /tmp/comfyui.log
问题 2: 模型加载失败
python
import requests
# 检查模型列表
response = requests.get("http://127.0.0.1:8188/object_info")
models = response.json()
print("📋 可用模型:")
for model_type, model_list in models.items():
print(f"\n{model_type}:")
for model in model_list:
print(f" - {model}")
问题 3: 生成失败
python
# 获取历史记录
response = requests.get("http://127.0.0.1:8188/history")
history = response.json()
print("📜 生成历史:")
for prompt_id, result in history.items():
status = result.get("status", {})
print(f"\n{prompt_id}:")
print(f" 状态: {status}")
print(f" 节点: {list(result.get('outputs', {}).keys())}")
十、总结
10.1 方案优势
| 优势 | 说明 |
|---|---|
| ✅ 简单易用 | 无需额外后端,直接调用 ComfyUI |
| ✅ 功能强大 | 支持100+种节点,扩展性强 |
| ✅ 可视化 | Web UI 直观,易于调试 |
| ✅ 本地化 | 完全本地运行,数据安全 |
| ✅ 性能优秀 | Mac M2/M3 优化,MPS 加速 |
10.2 技术栈
ComfyUI 服务
├── Python 3.10+
├── PyTorch 2.0+ (MPS)
├── HTTP/WebSocket API
└── 工作流 JSON
本地模型
├── LTX Video (8.7GB)
├── Wan 2.2 (10GB)
└── CogVideoX (6GB)
调用脚本
├── Python HTTP Client
├── WebSocket Client
└── 配置文件管理
10.3 使用场景
适用场景:
- ✅ 本地视频生成
- ✅ 批量处理
- ✅ 自动化工作流
- ✅ 离线环境
- ✅ 高性能需求
不适用场景:
- ❌ 需要 Web 服务(需要额外后端)
- ❌ 需要多用户协作(需要用户系统)
- ❌ 需要云端部署(需要分布式架构)
10.4 后续扩展
如需扩展为完整的服务,可以:
- 添加 FastAPI 后端封装
- 集成 Temporal 工作流
- 添加数据库存储
- 开发 Web 前端
- 实现多用户支持
结论:纯 ComfyUI 方案适合本地使用和批量处理,简单高效,功能强大。