Files
ModelTC-ComfyUI-Lightx2vWra…/lightx2v_nodes/universal_bridge.py
T

1039 lines
35 KiB
Python

"""Universal bridge between LightX2V and ComfyUI for automatic adaptation."""
import json
import os
import torch
import numpy as np
from pathlib import Path
from typing import Any, Dict, List, Union, Optional
from PIL import Image
import tempfile
from easydict import EasyDict
from .config import get_available_attn_ops, get_available_quant_ops
from ..lightx2v.lightx2v.utils.set_config import get_default_config
from ..lightx2v.lightx2v.infer import init_runner
class LightX2VBridge:
"""Universal bridge for LightX2V and ComfyUI integration."""
def __init__(self):
self._model_registry = None
self._config_registry = None
self._quantization_registry = None
self._runners = {} # Cache initialized runners
@property
def model_registry(self):
"""Lazy load model registry."""
if self._model_registry is None:
self._model_registry = self._discover_models()
return self._model_registry
@property
def config_registry(self):
"""Lazy load config registry."""
if self._config_registry is None:
self._config_registry = self._discover_configs()
return self._config_registry
@property
def quantization_registry(self):
"""Lazy load quantization registry."""
if self._quantization_registry is None:
self._quantization_registry = self._discover_quantization_configs()
return self._quantization_registry
def _discover_models(self) -> List[str]:
"""Discover available model classes from LightX2V."""
return ["wan2.1", "hunyuan", "wan2.1_audio"]
def _discover_configs(self) -> Dict[str, Dict]:
"""Discover all available config files for single-card inference."""
configs = {}
base_dir = Path(__file__).parent.parent / "lightx2v" / "configs"
if base_dir.exists():
for config_file in base_dir.rglob("*.json"):
# Create a descriptive key
relative_path = config_file.relative_to(base_dir)
path_str = str(relative_path)
# 排除分布式推理和部署相关的配置
if any(exclude in path_str for exclude in ["deploy", "causvid", "skyreels", "cogvideox"]):
continue
# 排除量化配置(单独处理)
if "quantization" in path_str:
continue
# 创建配置信息
config_info = {
"path": config_file,
"model_type": relative_path.parts[0], # wan, hunyuan, etc.
"task": self._extract_task_from_path(path_str),
"is_quantized": False,
"description": self._generate_config_description(relative_path),
}
key = str(relative_path).replace("/", "_").replace(".json", "")
configs[key] = config_info
return configs
def _discover_quantization_configs(self) -> Dict[str, Dict]:
"""Discover quantization configurations."""
quant_configs = {}
base_dir = Path(__file__).parent.parent / "lightx2v" / "configs" / "quantization"
if base_dir.exists():
for config_file in base_dir.rglob("*.json"):
relative_path = config_file.relative_to(base_dir.parent)
path_str = str(relative_path)
config_info = {
"path": config_file,
"model_type": relative_path.parts[1], # wan, hunyuan, etc.
"task": self._extract_task_from_path(path_str),
"is_quantized": True,
"description": self._generate_config_description(relative_path, is_quantized=True),
}
key = f"quant_{str(relative_path).replace('/', '_').replace('.json', '')}"
quant_configs[key] = config_info
return quant_configs
def _extract_task_from_path(self, path_str: str) -> str:
"""Extract task type from config path."""
if "i2v" in path_str:
return "i2v"
elif "t2v" in path_str:
return "t2v"
else:
return "unknown"
def _generate_config_description(self, relative_path: Path, is_quantized: bool = False) -> str:
"""Generate human-readable description for config."""
parts = relative_path.parts
model_type = parts[0] if not is_quantized else parts[1]
task = self._extract_task_from_path(str(relative_path))
desc = f"{model_type.upper()} {task.upper()}"
if is_quantized:
desc += " (Quantized)"
return desc
def get_quantization_model_path(self, model_type: str, mm_type: str) -> str:
"""Get quantization model path based on model type and mm_type."""
# 定义量化模型的固定路径结构
base_path = Path(__file__).parent.parent / "lightx2v" / "models" / "quantized"
# 根据mm_type确定具体的模型文件名
mm_type_to_filename = {
"W-int8-channel-sym-A-int8-channel-sym-dynamic-Vllm": "int8_dynamic_vllm.safetensors",
"W-int8-channel-sym-A-fp16-dynamic-Vllm": "int8_fp16_dynamic_vllm.safetensors",
"W-fp16-A-fp16-dynamic-Vllm": "fp16_dynamic_vllm.safetensors",
}
filename = mm_type_to_filename.get(mm_type, "default_quantized.safetensors")
model_path = base_path / model_type / filename
return str(model_path)
def load_config(self, config_path: Union[str, Path]) -> Dict:
"""Load a config file."""
with open(config_path, "r") as f:
return json.load(f)
def get_runner(self, config: EasyDict):
"""Get or create a runner for the given config."""
# Create a unique key for this configuration
key = f"{config.model_cls}_{config.model_path}_{hash(str(config))}"
if key not in self._runners:
self._runners[key] = init_runner(config)
return self._runners[key]
def clear_cache(self):
"""Clear cached runners to free memory."""
self._runners.clear()
torch.cuda.empty_cache()
class InputOutputConverter:
"""Convert between ComfyUI and LightX2V formats."""
@staticmethod
def comfy_image_to_pil(comfy_image: torch.Tensor) -> Image.Image:
"""Convert ComfyUI image tensor to PIL Image.
ComfyUI format: [B, H, W, C] float32 0-1
"""
# Take first image from batch
image_np = (comfy_image[0].cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(image_np)
@staticmethod
def comfy_audio_to_path(comfy_audio: Dict) -> str:
"""Convert ComfyUI audio to file path."""
# ComfyUI audio format: {"waveform": tensor, "sample_rate": int}
waveform = comfy_audio["waveform"]
sample_rate = comfy_audio["sample_rate"]
# Save to temporary file
import torchaudio
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
torchaudio.save(tmp.name, waveform, sample_rate)
return tmp.name
@staticmethod
def video_to_latent(video_path: str) -> Dict[str, Any]:
"""Convert video path to ComfyUI latent format."""
# For now, return the path wrapped in latent format
# This allows downstream nodes to handle the video
return {"samples": video_path, "type": "video", "format": "path"}
@staticmethod
def tensor_to_latent(video_tensor: torch.Tensor) -> Dict[str, Any]:
"""Convert video tensor to ComfyUI latent format."""
return {"samples": video_tensor, "type": "video", "format": "tensor"}
class ConfigManager:
"""Manage configuration merging and parameter mapping."""
# Mapping from ComfyUI parameter names to LightX2V config keys
PARAM_MAPPING = {
"steps": "infer_steps",
"cfg_scale": "sample_guide_scale",
"seed": "seed",
"height": "target_height",
"width": "target_width",
"video_length": "target_video_length",
}
# 用户可配置的参数(在ComfyUI中显示)
USER_CONFIGURABLE_PARAMS = {
"steps": {"type": "INT", "default": -1, "min": 1, "max": 200, "tooltip": "推理步数 (-1使用默认值)"},
"cfg_scale": {"type": "FLOAT", "default": -1, "min": 0.1, "max": 30, "step": 0.1, "tooltip": "CFG引导强度 (-1使用默认值)"},
"seed": {"type": "INT", "default": -1, "min": -1, "max": 2**32 - 1, "tooltip": "随机种子 (-1随机)"},
"height": {"type": "INT", "default": -1, "min": 64, "max": 2048, "step": 8, "tooltip": "视频高度 (-1使用默认值)"},
"width": {"type": "INT", "default": -1, "min": 64, "max": 2048, "step": 8, "tooltip": "视频宽度 (-1使用默认值)"},
"video_length": {"type": "INT", "default": -1, "min": 1, "max": 300, "tooltip": "视频帧数 (-1使用默认值)"},
"sample_shift": {"type": "INT", "default": -1, "min": 0, "max": 20, "tooltip": "采样偏移 (-1使用默认值)"},
}
@classmethod
def create_config(
cls,
model_cls: str,
model_path: str,
task: str,
base_config: Dict,
overrides: Dict,
quantization_config: Optional[Dict] = None,
) -> EasyDict:
"""Create a complete config for LightX2V runner."""
# Start with default config
config = get_default_config()
# Add required fields
config.update(
{
"model_cls": model_cls,
"model_path": model_path,
"task": task,
"mode": "infer",
}
)
# Apply base config from file
config.update(base_config)
# Apply quantization config if provided
if quantization_config:
config.update(quantization_config)
# 自动设置量化模型路径
if "mm_config" in quantization_config and "mm_type" in quantization_config["mm_config"]:
mm_type = quantization_config["mm_config"]["mm_type"]
config["dit_quantized_ckpt"] = cls._get_quantization_model_path(model_cls, mm_type)
# Apply ComfyUI overrides
for comfy_key, value in overrides.items():
if value is not None and value != -1: # -1 means use default
if comfy_key in cls.PARAM_MAPPING:
lightx2v_key = cls.PARAM_MAPPING[comfy_key]
config[lightx2v_key] = value
else:
# Direct mapping for unknown parameters
config[comfy_key] = value
return EasyDict(config)
@classmethod
def _get_quantization_model_path(cls, model_cls: str, mm_type: str) -> str:
"""Get quantization model path based on model class and mm_type."""
bridge = LightX2VBridge()
return bridge.get_quantization_model_path(model_cls, mm_type)
@classmethod
def parse_custom_config(cls, custom_config_str: str) -> Dict:
"""Parse custom config string."""
if not custom_config_str.strip():
return {}
try:
return json.loads(custom_config_str)
except json.JSONDecodeError as e:
print(f"Failed to parse custom config: {e}")
return {}
class LightX2VConfigBuilder:
"""Build LightX2V configuration from various sources."""
def __init__(self):
self.bridge = LightX2VBridge()
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for the config builder."""
bridge = LightX2VBridge()
# Get available models and configs
model_choices = bridge.model_registry
# 构建配置选项
config_choices = ["custom"]
config_descriptions = {}
# 添加普通配置
for key, config_info in bridge.config_registry.items():
config_choices.append(key)
config_descriptions[key] = config_info["description"]
required_inputs = {
"model_cls": (model_choices, {"tooltip": "选择模型类型"}),
"model_path": (
"STRING",
{"default": "", "tooltip": "模型权重路径(留空使用默认路径)"},
),
"task": (
["t2v", "i2v"],
{
"default": "t2v",
"tooltip": "任务类型: 文本到视频或图像到视频",
},
),
"config_preset": (
config_choices,
{
"default": "custom",
"tooltip": "配置预设或'custom'自定义",
},
),
}
# 添加用户可配置参数
for param_name, param_config in ConfigManager.USER_CONFIGURABLE_PARAMS.items():
required_inputs[param_name] = (
param_config["type"],
{
"default": param_config["default"],
"min": param_config["min"],
"max": param_config["max"],
"tooltip": param_config["tooltip"],
},
)
if "step" in param_config:
required_inputs[param_name][1]["step"] = param_config["step"]
optional_inputs = {
"custom_config": (
"STRING",
{
"multiline": True,
"default": "{}",
"tooltip": "自定义配置(JSON格式)",
},
),
"quantization_config": (
"LIGHTX2V_CONFIG",
{"tooltip": "量化配置"},
),
"attention_config": (
"LIGHTX2V_CONFIG",
{"tooltip": "注意力机制配置"},
),
"caching_config": (
"LIGHTX2V_CONFIG",
{"tooltip": "缓存配置"},
),
"lora_path": (
"STRING",
{"default": "", "tooltip": "LoRA权重路径"},
),
"strength_model": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.1,
"tooltip": "LoRA模型强度",
},
),
}
return {
"required": required_inputs,
"optional": optional_inputs,
}
RETURN_TYPES = ("LIGHTX2V_CONFIG",)
RETURN_NAMES = ("config",)
FUNCTION = "build_config"
CATEGORY = "LightX2V/Config"
def build_config(
self,
model_cls,
model_path,
task,
config_preset,
steps,
cfg_scale,
seed,
height,
width,
video_length,
sample_shift,
custom_config="{}",
quantization_config=None,
attention_config=None,
caching_config=None,
lora_path="",
strength_model=1.0,
**kwargs,
):
"""Build configuration for LightX2V inference."""
# 加载基础配置
if config_preset == "custom":
base_config = {}
else:
config_info = self.bridge.config_registry.get(config_preset)
if config_info:
base_config = self.bridge.load_config(config_info["path"])
else:
raise ValueError(f"配置预设 '{config_preset}' 未找到")
# 解析自定义配置
custom_cfg = ConfigManager.parse_custom_config(custom_config)
base_config.update(custom_cfg)
# 合并其他配置
if quantization_config:
base_config.update(quantization_config)
if attention_config:
base_config.update(attention_config)
if caching_config:
base_config.update(caching_config)
# 准备覆盖参数
overrides = {
"seed": seed if seed != -1 else None,
"steps": steps,
"cfg_scale": cfg_scale,
"height": height,
"width": width,
"video_length": video_length,
"sample_shift": sample_shift,
"lora_path": lora_path if lora_path else None,
"strength_model": strength_model,
}
# 创建最终配置
config = ConfigManager.create_config(model_cls, model_path, task, base_config, overrides)
return (config,)
class LightX2VQuantizationConfig:
"""Configuration node for quantization settings."""
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for quantization config."""
bridge = LightX2VBridge()
# Get available quantization configs
quant_choices = ["custom"]
for key, config_info in bridge.quantization_registry.items():
quant_choices.append(key)
return {
"required": {
"quantization_preset": (
quant_choices,
{
"default": "custom",
"tooltip": "量化配置预设",
},
),
},
"optional": {
"mm_type": (
[
"W-int8-channel-sym-A-int8-channel-sym-dynamic-Vllm",
"W-int8-channel-sym-A-fp16-dynamic-Vllm",
"W-fp16-A-fp16-dynamic-Vllm",
],
{
"default": "W-int8-channel-sym-A-int8-channel-sym-dynamic-Vllm",
"tooltip": "量化类型",
},
),
"dit_quantized_ckpt": (
"STRING",
{"default": "", "tooltip": "量化模型路径"},
),
},
}
RETURN_TYPES = ("LIGHTX2V_CONFIG",)
RETURN_NAMES = ("quantization_config",)
FUNCTION = "build_quantization_config"
CATEGORY = "LightX2V/Config"
def build_quantization_config(
self,
quantization_preset,
mm_type="W-int8-channel-sym-A-int8-channel-sym-dynamic-Vllm",
dit_quantized_ckpt="",
):
"""Build quantization configuration."""
bridge = LightX2VBridge()
if quantization_preset == "custom":
config = {
"mm_config": {"mm_type": mm_type},
}
if dit_quantized_ckpt:
config["dit_quantized_ckpt"] = dit_quantized_ckpt
else:
config_info = bridge.quantization_registry.get(quantization_preset)
if config_info:
config = bridge.load_config(config_info["path"])
else:
raise ValueError(f"量化配置 '{quantization_preset}' 未找到")
return (config,)
class LightX2VAttentionConfig:
"""Configuration node for attention mechanisms."""
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for attention config."""
return {
"required": {
"self_attn_1_type": (
["flash_attn3", "sage_attn2", "radial_attn", "sparge_attn"],
{
"default": "flash_attn3",
"tooltip": "自注意力类型",
},
),
"cross_attn_1_type": (
["flash_attn3", "sage_attn2", "radial_attn", "sparge_attn"],
{
"default": "flash_attn3",
"tooltip": "交叉注意力1类型",
},
),
"cross_attn_2_type": (
["flash_attn3", "sage_attn2", "radial_attn", "sparge_attn"],
{
"default": "flash_attn3",
"tooltip": "交叉注意力2类型",
},
),
},
}
RETURN_TYPES = ("LIGHTX2V_CONFIG",)
RETURN_NAMES = ("attention_config",)
FUNCTION = "build_attention_config"
CATEGORY = "LightX2V/Config"
def build_attention_config(
self,
self_attn_1_type,
cross_attn_1_type,
cross_attn_2_type,
):
"""Build attention configuration."""
config = {
"self_attn_1_type": self_attn_1_type,
"cross_attn_1_type": cross_attn_1_type,
"cross_attn_2_type": cross_attn_2_type,
}
return (config,)
class LightX2VCachingConfig:
"""Configuration node for caching mechanisms."""
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for caching config."""
return {
"required": {
"feature_caching": (
["None", "Tea", "Ada", "Custom"],
{
"default": "None",
"tooltip": "特征缓存类型",
},
),
},
"optional": {
"teacache_thresh": (
"FLOAT",
{
"default": 0.26,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "TeaCache阈值",
},
),
"use_ret_steps": (
"BOOLEAN",
{
"default": True,
"tooltip": "使用返回步骤",
},
),
"coefficients": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "系数配置(JSON格式)",
},
),
},
}
RETURN_TYPES = ("LIGHTX2V_CONFIG",)
RETURN_NAMES = ("caching_config",)
FUNCTION = "build_caching_config"
CATEGORY = "LightX2V/Config"
def build_caching_config(
self,
feature_caching,
teacache_thresh=0.26,
use_ret_steps=True,
coefficients="",
):
"""Build caching configuration."""
config = {}
if feature_caching != "None":
config["feature_caching"] = feature_caching
if feature_caching == "Tea":
config["teacache_thresh"] = teacache_thresh
config["use_ret_steps"] = use_ret_steps
if coefficients:
try:
config["coefficients"] = json.loads(coefficients)
except json.JSONDecodeError:
print(f"Failed to parse coefficients: {coefficients}")
return (config,)
class LightX2VInference:
"""LightX2V inference node that accepts configuration and runs generation."""
def __init__(self):
self.bridge = LightX2VBridge()
self.converter = InputOutputConverter()
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for the inference node."""
return {
"required": {
"config": ("LIGHTX2V_CONFIG", {"tooltip": "LightX2V配置"}),
"prompt": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "生成提示词",
},
),
"negative_prompt": (
"STRING",
{"multiline": True, "default": "", "tooltip": "负面提示词"},
),
},
"optional": {
"image": ("IMAGE", {"tooltip": "i2v任务的输入图像"}),
"audio": (
"AUDIO",
{"tooltip": "音频驱动生成的输入音频"},
),
},
}
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("video",)
FUNCTION = "generate"
CATEGORY = "LightX2V/Inference"
def generate(
self,
config,
prompt,
negative_prompt,
image=None,
audio=None,
**kwargs,
):
"""Generate video using LightX2V."""
# 添加提示词到配置
config.prompt = prompt
config.negative_prompt = negative_prompt
# 处理i2v任务的图像输入
if config.task == "i2v" and image is not None:
# 保存图像到临时文件
pil_image = self.converter.comfy_image_to_pil(image)
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
pil_image.save(tmp.name)
config.image_path = tmp.name
# 处理音频输入
if audio is not None and "audio" in config.model_cls:
audio_path = self.converter.comfy_audio_to_path(audio)
config.audio_path = audio_path
# 创建临时配置文件(set_config需要)
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
json.dump(config.to_dict(), tmp)
config.config_json = tmp.name
# 获取或创建runner
try:
runner = self.bridge.get_runner(config)
# 运行生成
result = runner.run_pipeline()
# 转换输出
if hasattr(result, "save_video_path"):
return (self.converter.video_to_latent(result.save_video_path),)
else:
# 假设张量输出
return (self.converter.tensor_to_latent(result),)
finally:
# 清理临时文件
if hasattr(config, "image_path") and os.path.exists(config.image_path):
os.unlink(config.image_path)
if hasattr(config, "audio_path") and os.path.exists(config.audio_path):
os.unlink(config.audio_path)
if hasattr(config, "config_json") and os.path.exists(config.config_json):
os.unlink(config.config_json)
class LightX2VConfigViewer:
"""View and display LightX2V configuration details."""
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for the config viewer."""
return {
"required": {
"config": ("LIGHTX2V_CONFIG", {"tooltip": "LightX2V配置"}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("config_info",)
FUNCTION = "view_config"
CATEGORY = "LightX2V/Util"
def view_config(self, config):
"""View configuration details."""
if hasattr(config, "to_dict"):
config_dict = config.to_dict()
else:
config_dict = dict(config)
config_str = json.dumps(config_dict, indent=2, ensure_ascii=False)
print(f"LightX2V Configuration:\n{config_str}")
return (config_str,)
class UniversalLightX2VNode:
"""Universal ComfyUI node for all LightX2V models (Legacy compatibility)."""
def __init__(self):
self.bridge = LightX2VBridge()
self.converter = InputOutputConverter()
@classmethod
def INPUT_TYPES(cls):
"""Define inputs for the node."""
bridge = LightX2VBridge()
# Get available models and configs
model_choices = bridge.model_registry
# 构建配置选项
config_choices = ["custom"]
config_descriptions = {}
# 添加普通配置
for key, config_info in bridge.config_registry.items():
config_choices.append(key)
config_descriptions[key] = config_info["description"]
# 添加量化配置
for key, config_info in bridge.quantization_registry.items():
config_choices.append(key)
config_descriptions[key] = config_info["description"]
# 构建用户可配置参数
required_inputs = {
"model_cls": (model_choices, {"tooltip": "选择模型类型"}),
"model_path": (
"STRING",
{"default": "", "tooltip": "模型权重路径(留空使用默认路径)"},
),
"task": (
["t2v", "i2v"],
{
"default": "t2v",
"tooltip": "任务类型: 文本到视频或图像到视频",
},
),
"config_preset": (
config_choices,
{
"default": "custom",
"tooltip": "配置预设或'custom'自定义",
},
),
"prompt": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "生成提示词",
},
),
"negative_prompt": (
"STRING",
{"multiline": True, "default": "", "tooltip": "负面提示词"},
),
}
# 添加用户可配置参数
for param_name, param_config in ConfigManager.USER_CONFIGURABLE_PARAMS.items():
required_inputs[param_name] = (
param_config["type"],
{
"default": param_config["default"],
"min": param_config["min"],
"max": param_config["max"],
"tooltip": param_config["tooltip"],
},
)
if "step" in param_config:
required_inputs[param_name][1]["step"] = param_config["step"]
optional_inputs = {
"image": ("IMAGE", {"tooltip": "i2v任务的输入图像"}),
"audio": (
"AUDIO",
{"tooltip": "音频驱动生成的输入音频"},
),
"custom_config": (
"STRING",
{
"multiline": True,
"default": "{}",
"tooltip": "自定义配置(JSON格式)",
},
),
"lora_path": (
"STRING",
{"default": "", "tooltip": "LoRA权重路径"},
),
"strength_model": (
"FLOAT",
{
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.1,
"tooltip": "LoRA模型强度",
},
),
}
return {
"required": required_inputs,
"optional": optional_inputs,
}
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("video",)
FUNCTION = "generate"
CATEGORY = "LightX2V/Universal"
def generate(
self,
model_cls,
model_path,
task,
config_preset,
prompt,
negative_prompt,
steps,
cfg_scale,
seed,
height,
width,
video_length,
sample_shift,
image=None,
audio=None,
custom_config="{}",
lora_path="",
strength_model=1.0,
**kwargs,
):
"""Generate video using LightX2V."""
# 加载基础配置
if config_preset == "custom":
base_config = {}
quantization_config = None
else:
# 检查是否为量化配置
if config_preset in self.bridge.quantization_registry:
config_info = self.bridge.quantization_registry[config_preset]
base_config = self.bridge.load_config(config_info["path"])
quantization_config = base_config
else:
config_info = self.bridge.config_registry.get(config_preset)
if config_info:
base_config = self.bridge.load_config(config_info["path"])
quantization_config = None
else:
raise ValueError(f"配置预设 '{config_preset}' 未找到")
# 解析自定义配置
custom_cfg = ConfigManager.parse_custom_config(custom_config)
base_config.update(custom_cfg)
# 准备覆盖参数
overrides = {
"seed": seed if seed != -1 else None,
"steps": steps,
"cfg_scale": cfg_scale,
"height": height,
"width": width,
"video_length": video_length,
"sample_shift": sample_shift,
"lora_path": lora_path if lora_path else None,
"strength_model": strength_model,
"prompt": prompt,
"negative_prompt": negative_prompt,
}
# 处理i2v任务的图像输入
if task == "i2v" and image is not None:
# 保存图像到临时文件
pil_image = self.converter.comfy_image_to_pil(image)
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
pil_image.save(tmp.name)
overrides["image_path"] = tmp.name
# 处理音频输入
if audio is not None and "audio" in model_cls:
audio_path = self.converter.comfy_audio_to_path(audio)
overrides["audio_path"] = audio_path
# 创建最终配置
config = ConfigManager.create_config(model_cls, model_path, task, base_config, overrides, quantization_config)
# 创建临时配置文件(set_config需要)
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
json.dump(base_config, tmp)
config.config_json = tmp.name
# 获取或创建runner
try:
runner = self.bridge.get_runner(config)
# 运行生成
result = runner.run_pipeline()
# 转换输出
if hasattr(result, "save_video_path"):
return (self.converter.video_to_latent(result.save_video_path),)
else:
# 假设张量输出
return (self.converter.tensor_to_latent(result),)
finally:
# 清理临时文件
if "image_path" in overrides and os.path.exists(overrides["image_path"]):
os.unlink(overrides["image_path"])
if "audio_path" in overrides and os.path.exists(overrides["audio_path"]):
os.unlink(overrides["audio_path"])
if hasattr(config, "config_json") and os.path.exists(config.config_json):
os.unlink(config.config_json)
# Node class mapping
NODE_CLASS_MAPPINGS = {
# Modular nodes
"LightX2VConfigBuilder": LightX2VConfigBuilder,
"LightX2VQuantizationConfig": LightX2VQuantizationConfig,
"LightX2VAttentionConfig": LightX2VAttentionConfig,
"LightX2VCachingConfig": LightX2VCachingConfig,
"LightX2VInference": LightX2VInference,
"LightX2VConfigViewer": LightX2VConfigViewer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
# Modular nodes
"LightX2VConfigBuilder": "LightX2V Config Builder",
"LightX2VQuantizationConfig": "LightX2V Quantization Config",
"LightX2VAttentionConfig": "LightX2V Attention Config",
"LightX2VCachingConfig": "LightX2V Caching Config",
"LightX2VInference": "LightX2V Inference",
"LightX2VConfigViewer": "LightX2V Config Viewer",
}