Files
Gin3601-LLS-node/nodes.py
T
2026-05-19 14:02:35 +08:00

304 lines
9.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
node/nodes.py — 根级共享工具与统一门面节点
==========================================
职责:
1. 提供可被各子包复用的通用工具函数。
2. 注册根级的 `LLS Universal Image Generator` 节点。
现有子包仍可继续从本文件导入工具函数;新增的统一节点只作为门面,
具体的 SD1.5 / SDXL / FLUX 差异全部下沉到 `lls_universal/` 后端适配器。
"""
from __future__ import annotations
import contextlib
import inspect
import importlib
from typing import Any, Iterable
# ---------- 防御性导入 ----------
try:
import torch
except Exception as exc:
torch = None
_TORCH_IMPORT_ERROR = exc
else:
_TORCH_IMPORT_ERROR = None
try:
import folder_paths
except Exception as exc:
folder_paths = None
_FOLDER_PATHS_IMPORT_ERROR = exc
else:
_FOLDER_PATHS_IMPORT_ERROR = None
try:
import comfy.model_management as model_management
import comfy.utils as comfy_utils
except Exception as exc:
model_management = None
comfy_utils = None
_COMFY_IMPORT_ERROR = exc
else:
_COMFY_IMPORT_ERROR = None
try:
comfy_core_nodes = importlib.import_module("nodes")
except Exception:
comfy_core_nodes = None
try:
from .lls_universal.dispatcher import get_backend
from .lls_universal.request import (
MODEL_FAMILY_CHOICES,
TASK_MODE_CHOICES,
LLSUniversalGenerationRequest,
)
except Exception as exc:
get_backend = None
MODEL_FAMILY_CHOICES = ("SD1.5", "SDXL", "FLUX")
TASK_MODE_CHOICES = ("txt2img",)
LLSUniversalGenerationRequest = None
_UNIVERSAL_IMPORT_ERROR = exc
else:
_UNIVERSAL_IMPORT_ERROR = None
try:
from .sampling.nodes import _get_samplers, _get_schedulers
except Exception:
_get_samplers = None
_get_schedulers = None
_DEFAULT_SAMPLERS = [
"euler",
"euler_ancestral",
"heun",
"dpm_2",
"dpm_2_ancestral",
"lms",
"dpm_fast",
"dpm_adaptive",
"dpmpp_2s_ancestral",
"dpmpp_sde",
"dpmpp_2m",
"dpmpp_2m_sde",
"ddim",
"uni_pc",
]
_DEFAULT_SCHEDULERS = ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform"]
_MAX_RESOLUTION = int(getattr(comfy_core_nodes, "MAX_RESOLUTION", 8192))
# ---------- 共享工具函数 ----------
def _torch_inference_mode():
"""返回 torch.inference_mode() context manager; torch 不可用时返回空上下文。"""
if torch is None or not hasattr(torch, "inference_mode"):
return contextlib.nullcontext()
return torch.inference_mode()
def _require_torch() -> None:
"""断言 torch 可用,否则抛出 RuntimeError。"""
if torch is None:
raise RuntimeError(
"PyTorch is not available in the current environment. "
"Load this custom node inside ComfyUI's Python environment."
) from _TORCH_IMPORT_ERROR
def _require_comfy_modules() -> None:
"""断言 ComfyUI 核心模块均可用,否则抛出 RuntimeError。"""
if folder_paths is None:
raise RuntimeError(
"ComfyUI folder_paths could not be imported. "
"Place this plugin directory under ComfyUI/custom_nodes/ and restart ComfyUI."
) from _FOLDER_PATHS_IMPORT_ERROR
if comfy_utils is None or model_management is None:
raise RuntimeError(
"ComfyUI runtime modules could not be imported. "
"This node must run inside a ComfyUI environment."
) from _COMFY_IMPORT_ERROR
def _clamp_image(image: Any) -> Any:
"""将图像 tensor clamp 到 [0.0, 1.0]。"""
if torch is None:
return image
return torch.clamp(image, 0.0, 1.0)
def _sorted_unique(items: Iterable[str]) -> list[str]:
"""去重并排序字符串列表(保持确定性顺序)。"""
return sorted(dict.fromkeys(items))
def _is_oom_exception(exc: Exception) -> bool:
"""判断异常是否为显存不足(OOM)。"""
oom_types: list[type[BaseException]] = []
if model_management is not None:
oom_type = getattr(model_management, "OOM_EXCEPTION", None)
if isinstance(oom_type, type):
oom_types.append(oom_type)
if torch is not None:
cuda = getattr(torch, "cuda", None)
oom_type = getattr(cuda, "OutOfMemoryError", None)
if isinstance(oom_type, type):
oom_types.append(oom_type)
if any(isinstance(exc, t) for t in oom_types):
return True
return "out of memory" in str(exc).lower()
def _make_progress_bar(steps: int):
"""创建 ComfyUI ProgressBar;comfy_utils 不可用时返回 None。"""
if comfy_utils is None or not hasattr(comfy_utils, "ProgressBar"):
return None
return comfy_utils.ProgressBar(steps)
def _supports_argument(fn: Any, argument_name: str) -> bool:
"""检查函数签名中是否包含指定参数名。"""
try:
signature = inspect.signature(fn)
except (TypeError, ValueError):
return False
return argument_name in signature.parameters
def _get_checkpoint_names() -> list[str]:
"""读取 ComfyUI checkpoints 列表;脱离 ComfyUI 时返回占位提示。"""
if folder_paths is None:
return ["(ComfyUI not available)"]
try:
names = folder_paths.get_filename_list("checkpoints")
except Exception:
names = []
return names if names else ["(no checkpoints found)"]
def _get_sampler_choices() -> list[str]:
if callable(_get_samplers):
try:
names = list(_get_samplers())
if names:
return names
except Exception:
pass
return _DEFAULT_SAMPLERS
def _get_scheduler_choices() -> list[str]:
if callable(_get_schedulers):
try:
names = list(_get_schedulers())
if names:
return names
except Exception:
pass
return _DEFAULT_SCHEDULERS
# ---------- 根级统一节点 ----------
class LLSUniversalImageGenerator:
"""
统一 txt2img 入口。
节点本身不拼接任何“万能采样流程”;
它只负责收集请求并交给 dispatcher 选择对应 backend。
"""
CATEGORY = "LLS/Image"
FUNCTION = "generate"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
DESCRIPTION = (
"Unified txt2img node that dispatches SD1.5, SDXL, and FLUX generation "
"to family-specific backend adapters."
)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_family": (list(MODEL_FAMILY_CHOICES), {"default": "SD1.5"}),
"task_mode": (list(TASK_MODE_CHOICES), {"default": "txt2img"}),
"model_name": (_get_checkpoint_names(),),
"positive_prompt": ("STRING", {"default": "", "multiline": True}),
"negative_prompt": ("STRING", {"default": "", "multiline": True}),
"width": ("INT", {"default": 1024, "min": 64, "max": _MAX_RESOLUTION, "step": 8}),
"height": ("INT", {"default": 1024, "min": 64, "max": _MAX_RESOLUTION, "step": 8}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"seed": (
"INT",
{
"default": -1,
"min": -1,
"max": 0xFFFFFFFFFFFFFFFF,
"control_after_generate": True,
},
),
"sampler_name": (_get_sampler_choices(), {"default": "euler"}),
"scheduler": (_get_scheduler_choices(), {"default": "normal"}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
def generate(
self,
model_family: str,
task_mode: str,
model_name: str,
positive_prompt: str,
negative_prompt: str,
width: int,
height: int,
steps: int,
cfg: float,
seed: int,
sampler_name: str,
scheduler: str,
denoise: float,
):
if get_backend is None or LLSUniversalGenerationRequest is None:
raise RuntimeError(
"[LLS] Universal generator modules could not be imported. "
"Check the lls_universal package for syntax or import errors."
) from _UNIVERSAL_IMPORT_ERROR
request = LLSUniversalGenerationRequest(
model_family=model_family,
task_mode=task_mode,
model_name=model_name,
positive_prompt=positive_prompt,
negative_prompt=negative_prompt,
width=width,
height=height,
steps=steps,
cfg=cfg,
seed=seed,
sampler_name=sampler_name,
scheduler=scheduler,
denoise=denoise,
).validate()
backend = get_backend(request.model_family)
image = backend.generate(request)
return (image,)
# ---------- 根级注册表 ----------
NODE_CLASS_MAPPINGS: dict[str, type] = {
"LLSUniversalImageGenerator": LLSUniversalImageGenerator,
}
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
"LLSUniversalImageGenerator": "LLS Universal Image Generator",
}