Files

474 lines
16 KiB
Python

"""
LLS / Sampling
==============
功能域:图像采样与去噪(对应功能分类总览第 3 节)
CATEGORY = "LLS/Sampling"
"""
from __future__ import annotations
import random
from ..repair import runtime as repair_runtime
from ..repair.repair_utils import normalize_model_info, normalize_repair_info, resolve_adapter_mode
from ..utils.model_info import (
FAMILY_DEFAULT_PRESET,
MODEL_FAMILY_CHOICES,
get_family_defaults,
get_sampling_preset,
infer_task_mode_from_latent,
info_to_json,
is_flux_family,
resolve_model_family,
)
# ---------- 防御性导入 ----------
try:
import comfy.sample as comfy_sample
except Exception as exc:
comfy_sample = None
_COMFY_SAMPLE_ERR = exc
else:
_COMFY_SAMPLE_ERR = None
try:
import comfy.samplers as comfy_samplers
except Exception as exc:
comfy_samplers = None
_COMFY_SAMPLERS_ERR = exc
else:
_COMFY_SAMPLERS_ERR = None
try:
import comfy.utils as comfy_utils
except Exception as exc:
comfy_utils = None
_COMFY_UTILS_ERR = exc
else:
_COMFY_UTILS_ERR = None
try:
import latent_preview
except Exception as exc:
latent_preview = None
_LATENT_PREVIEW_ERR = exc
else:
_LATENT_PREVIEW_ERR = None
try:
import torch
except Exception:
torch = None
try:
import node_helpers
except Exception:
node_helpers = None
# ---------- 预设定义 ----------
_QUALITY_PRESETS = [FAMILY_DEFAULT_PRESET, "Manual", "Fast", "Balanced", "High Quality"]
_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"]
_PRIMITIVE_NUMBER_INPUT = "STRING,FLOAT,INT"
_DENOISE_MODE_CHOICES = ["manual", "auto_from_repair"]
_ADAPTER_MODE_CHOICES = ["auto", "sd_classic", "flux", "sd3", "qwen", "zimage"]
def _get_samplers() -> list[str]:
if comfy_samplers is not None:
try:
return list(comfy_samplers.KSampler.SAMPLERS)
except Exception:
pass
return _DEFAULT_SAMPLERS
def _get_schedulers() -> list[str]:
if comfy_samplers is not None:
try:
return list(comfy_samplers.KSampler.SCHEDULERS)
except Exception:
pass
return _DEFAULT_SCHEDULERS
def _normalize_flux_guidance(value, fallback: float | None) -> float | None:
if value is None:
return fallback
if isinstance(value, bool):
raise RuntimeError("[LLS] flux_guidance must be a number, not BOOLEAN.")
if isinstance(value, str):
stripped = value.strip()
if not stripped:
return fallback
value = stripped
try:
normalized = float(value)
except (TypeError, ValueError) as exc:
raise RuntimeError(f"[LLS] Invalid flux_guidance value: {value!r}") from exc
if normalized < 0.0 or normalized > 100.0:
raise RuntimeError(f"[LLS] flux_guidance out of supported range [0.0, 100.0]: {normalized}")
return normalized
def _normalize_ksampler_compat_values(
denoise_mode,
adapter_mode,
flux_guidance,
model_family,
):
normalized_denoise_mode = str(denoise_mode or "").strip()
normalized_flux_guidance = str(flux_guidance or "").strip()
normalized_adapter_mode = str(adapter_mode or "").strip()
normalized_model_family = str(model_family or "").strip()
# Repair the short-lived widget-order regression so old and newly re-saved
# workflows can both be interpreted correctly.
if (
normalized_denoise_mode not in _DENOISE_MODE_CHOICES
and normalized_flux_guidance in _DENOISE_MODE_CHOICES
and normalized_model_family.lower() in _ADAPTER_MODE_CHOICES
):
shifted_flux_guidance = denoise_mode
denoise_mode = normalized_flux_guidance or "manual"
adapter_mode = normalized_model_family.lower() or "auto"
flux_guidance = shifted_flux_guidance
model_family = "Auto" if normalized_adapter_mode.lower() == "auto" else normalized_adapter_mode
return denoise_mode, adapter_mode, flux_guidance, model_family
if normalized_adapter_mode.lower() in _ADAPTER_MODE_CHOICES:
adapter_mode = normalized_adapter_mode.lower()
elif normalized_adapter_mode == "Auto":
adapter_mode = "auto"
if normalized_model_family == "auto":
model_family = "Auto"
return denoise_mode, adapter_mode, flux_guidance, model_family
def _apply_repair_model_patch(model, repair_meta):
if repair_meta is None:
return model
patch_name = str(repair_meta.get("model_patch") or "").strip().lower()
if patch_name == "differential_diffusion":
strength = float(repair_meta.get("model_patch_strength", 1.0))
return repair_runtime.apply_differential_diffusion(model, strength=strength)
return model
def _common_ksampler(
model,
seed: int,
steps: int,
cfg: float,
sampler_name: str,
scheduler: str,
positive,
negative,
latent,
denoise: float = 1.0,
disable_noise: bool = False,
start_step=None,
last_step=None,
force_full_denoise: bool = False,
):
if comfy_sample is None:
raise RuntimeError(
"[LLS] comfy.sample is not available. "
"Make sure this node runs inside a ComfyUI environment."
) from _COMFY_SAMPLE_ERR
if comfy_utils is None:
raise RuntimeError(
"[LLS] comfy.utils is not available. "
"Make sure this node runs inside a ComfyUI environment."
) from _COMFY_UTILS_ERR
if latent_preview is None:
raise RuntimeError(
"[LLS] latent_preview is not available. "
"Make sure this node runs inside a ComfyUI environment."
) from _LATENT_PREVIEW_ERR
if not isinstance(latent, dict) or "samples" not in latent:
raise RuntimeError(
"[LLS] latent_image must be a LATENT dict containing the 'samples' tensor."
)
latent_image = latent["samples"]
fix_empty_latent_channels = getattr(comfy_sample, "fix_empty_latent_channels", None)
if callable(fix_empty_latent_channels):
try:
latent_image = fix_empty_latent_channels(
model,
latent_image,
latent.get("downscale_ratio_spacial", None),
)
except TypeError:
latent_image = fix_empty_latent_channels(model, latent_image)
if disable_noise:
if torch is None:
raise RuntimeError("[LLS] PyTorch is required when disable_noise is enabled.")
noise = torch.zeros(
latent_image.size(),
dtype=latent_image.dtype,
layout=latent_image.layout,
device="cpu",
)
else:
batch_index = latent.get("batch_index")
noise = comfy_sample.prepare_noise(latent_image, seed, batch_index)
noise_mask = latent.get("noise_mask")
callback = latent_preview.prepare_callback(model, steps)
disable_pbar = not getattr(comfy_utils, "PROGRESS_BAR_ENABLED", True)
sampled = comfy_sample.sample(
model,
noise,
steps,
cfg,
sampler_name,
scheduler,
positive,
negative,
latent_image,
denoise=denoise,
disable_noise=disable_noise,
start_step=start_step,
last_step=last_step,
force_full_denoise=force_full_denoise,
noise_mask=noise_mask,
callback=callback,
disable_pbar=disable_pbar,
seed=seed,
)
out = latent.copy()
out["samples"] = sampled
return out
# ---------- 节点类 ----------
class LLSSimpleKSampler:
"""
简化版 KSampler。
内部复用 ComfyUI 原生采样能力,不重写采样算法。
支持 quality_preset 一键切换采样参数,并输出 sample_info 串。
"""
CATEGORY = "LLS/Sampling"
FUNCTION = "sample"
RETURN_TYPES = ("LATENT", "STRING")
RETURN_NAMES = ("latent", "sample_info")
DESCRIPTION = (
"Basic KSampler node. Reuses ComfyUI's native sampling pipeline. "
"quality_preset overrides steps/cfg/denoise when not 'Manual'. "
"seed=-1 generates a random seed."
)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"latent_image": ("LATENT",),
"quality_preset": (_QUALITY_PRESETS, {"default": FAMILY_DEFAULT_PRESET}),
"seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFFFFFFFFFF}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"sampler_name": (_get_samplers(), {"default": "euler_ancestral"}),
"scheduler": (_get_schedulers(), {"default": "karras"}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"denoise_mode": (_DENOISE_MODE_CHOICES, {"default": "manual"}),
"adapter_mode": (_ADAPTER_MODE_CHOICES, {"default": "auto"}),
"flux_guidance": (
_PRIMITIVE_NUMBER_INPUT,
{"default": 3.5, "widgetType": "FLOAT", "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.1},
),
"model_family": (MODEL_FAMILY_CHOICES, {"default": "Auto"}),
},
"optional": {
"repair_info": ("LLS_REPAIR_INFO",),
"guidance_stack": ("LLS_GUIDANCE_STACK",),
"model_info": ("STRING",),
},
}
@classmethod
def VALIDATE_INPUTS(cls, flux_guidance=None, input_types=None):
received_type = None
if isinstance(input_types, dict):
received_type = input_types.get("flux_guidance")
if received_type is not None and received_type not in {"STRING", "FLOAT", "INT"}:
return f"flux_guidance only accepts STRING/FLOAT/INT inputs, got {received_type}."
try:
_normalize_flux_guidance(flux_guidance, fallback=3.5)
except RuntimeError as exc:
return str(exc)
return True
def sample(
self,
model,
positive,
negative,
latent_image,
quality_preset: str,
seed: int,
steps: int,
cfg: float,
sampler_name: str,
scheduler: str,
denoise: float,
denoise_mode: str = "manual",
adapter_mode: str = "auto",
flux_guidance=3.5,
model_family: str = "Auto",
repair_info=None,
guidance_stack=None,
model_info=None,
):
if comfy_sample is None:
raise RuntimeError(
"[LLS] comfy.sample is not available. "
"Make sure this node runs inside a ComfyUI environment."
) from _COMFY_SAMPLE_ERR
if comfy_samplers is None:
raise RuntimeError(
"[LLS] comfy.samplers is not available. "
"Make sure this node runs inside a ComfyUI environment."
) from _COMFY_SAMPLERS_ERR
repair_meta = normalize_repair_info(repair_info) if repair_info is not None else None
model_meta = normalize_model_info(model_info)
denoise_mode, adapter_mode, flux_guidance, model_family = _normalize_ksampler_compat_values(
denoise_mode,
adapter_mode,
flux_guidance,
model_family,
)
family = resolve_model_family(model_family, model=model)
if repair_meta is not None and repair_meta.get("model_family") not in {"UNKNOWN", "", None}:
family = str(repair_meta["model_family"])
elif model_meta.get("model_family") not in {"UNKNOWN", "", None}:
family = str(model_meta["model_family"])
effective_adapter = resolve_adapter_mode(adapter_mode, family)
if effective_adapter == "sd3":
raise RuntimeError("[LLS] SD3 repair-aware sampling is not implemented yet.")
if effective_adapter == "qwen":
raise RuntimeError("[LLS] QWEN repair-aware sampling is not implemented yet.")
if effective_adapter == "zimage":
raise RuntimeError("[LLS] ZIMAGE repair-aware sampling is not implemented yet.")
defaults = get_family_defaults(family)
default_flux_guidance = defaults.get("default_guidance")
effective_task_mode = infer_task_mode_from_latent(latent_image)
if quality_preset == FAMILY_DEFAULT_PRESET:
steps = int(defaults["default_steps"])
cfg = float(defaults["default_cfg"])
sampler_name = str(defaults["default_sampler"])
scheduler = str(defaults["default_scheduler"])
denoise = float(defaults["default_denoise"])
if default_flux_guidance is not None:
flux_guidance = float(default_flux_guidance)
else:
preset = get_sampling_preset(defaults, quality_preset)
if preset is not None:
steps = int(preset["steps"])
cfg = float(preset["cfg"])
sampler_name = str(preset["sampler_name"])
scheduler = str(preset["scheduler"])
denoise = float(preset["denoise"])
if preset.get("guidance") is not None:
flux_guidance = float(preset["guidance"])
flux_guidance = _normalize_flux_guidance(flux_guidance, fallback=default_flux_guidance)
actual_denoise = float(denoise)
if repair_meta is not None and denoise_mode == "auto_from_repair":
actual_denoise = float(repair_meta.get("recommended_denoise", denoise))
if is_flux_family(family):
guidance_value = flux_guidance
else:
guidance_value = None
if is_flux_family(family) and node_helpers is not None and guidance_value is not None:
try:
positive = node_helpers.conditioning_set_values(positive, {"guidance": guidance_value})
negative = node_helpers.conditioning_set_values(negative, {"guidance": guidance_value})
except Exception:
pass
# seed = -1 时随机生成
actual_seed = seed
if seed == -1:
actual_seed = random.randint(0, 0xFFFFFFFFFFFFFFFF)
sample_model = _apply_repair_model_patch(model, repair_meta)
# 复用 ComfyUI 原生 common_ksampler
try:
result_latent = _common_ksampler(
model=sample_model,
seed=actual_seed,
steps=steps,
cfg=cfg,
sampler_name=sampler_name,
scheduler=scheduler,
positive=positive,
negative=negative,
latent=latent_image,
denoise=actual_denoise,
)
except Exception as exc:
raise RuntimeError(
f"[LLS] KSampler failed: {exc}"
) from exc
sample_info = info_to_json(
{
"seed": actual_seed,
"steps": steps,
"cfg": cfg,
"guidance": guidance_value,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": actual_denoise,
"quality_preset": quality_preset,
"family": family,
"task_mode": effective_task_mode,
"repair_mode": repair_meta is not None,
"repair_scope": repair_meta.get("repair_scope") if repair_meta else None,
"repair_kernel": repair_meta.get("repair_kernel") if repair_meta else None,
"repair_backend": repair_meta.get("backend_name") if repair_meta else None,
"repair_execution_path": repair_meta.get("execution_path") if repair_meta else None,
"repair_model_patch": repair_meta.get("model_patch") if repair_meta else None,
"guidance_used": bool(guidance_stack),
}
)
return (result_latent, sample_info)
# ---------- 注册表 ----------
NODE_CLASS_MAPPINGS: dict[str, type] = {
"LLSSimpleKSampler": LLSSimpleKSampler,
}
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
"LLSSimpleKSampler": "LLS Simple KSampler",
}