diff --git a/CHANGELOG.md b/CHANGELOG.md index f66df22..baa3be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 🆕 Version 1.3.0 — MODEL In→Out LoRA Stack & Qwen Loader Docs + +Date: 2025-11-19 + +Changes: +- Added new node `IAMCCS_WanLoRAStackModelIO` ("LoRA Stack (Model In→Out) WAN") for direct multi-LoRA application to an incoming MODEL (WAN 2.2 / Flow / Standard). +- Preserves WAN key remap + optional chaining via existing `IAMCCS_WanLoRAStack` (use optional `lora` input to extend beyond 4 slots). +- Updated `README.md` with explicit low VRAM instructions for Qwen Image LoRA loader and dependency checklist (`ComfyUI-nunchaku`, `ComfyUI-QwenImageLoraLoader`). +- Bumped versions (`version.json`, `pyproject.toml`) to 1.3.0. +- Neutralized deprecated Save&Load DragCrop code (frontend/backend) — removed from active registration. + +Notes: +- Existing workflows using the older two-node stack + apply pattern continue to work unchanged. +- Use `IAMCCS_WanLoRAStackModelIO` to simplify WAN 2.2 graphs or reduce node count before samplers. + +--- ## 🆕 Version 1.2.3 — Stackable LoRA Input - Added optional `lora` input to IAMCCS_WanLoRAStack node diff --git a/README.md b/README.md index 1d3ee88..cb9a7d3 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,47 @@ ### Category: ComfyUI Custom Nodes ### Main Feature: Fix for LoRA loading in native WANAnimate workflows -Version: 1.2.3 +Version: 1.3.0 -# UPDATE VERSION 1-2-3 +# UPDATE VERSION 1-3-0 -## 🆕 Version 1.2.3 — New input lora - add another StackLoraModel (concatenate) + Extended Wan 2.1 Compatibility +## 🆕 Version 1.3.0 — New MODEL IO LoRA Stack + Qwen Loader Instructions -Version: 1.2.1 +Highlights: +- Added `LoRA Stack (Model In→Out) WAN` node: directly applies up to 4 WAN / Flow / Standard LoRAs to an incoming MODEL and outputs a patched MODEL (ideal for WAN 2.2 workflows where a single node step is preferred). +- Qwen Image LoRA (IAMCCS QwenImgLoadFix) – Updated for 1.3.0: a fixed Qwen Image LoRA loader with improved offload controls and UX. +- Extended internal WAN key remapping for seamless WAN 2.2 (Flow) + WAN 2.1 cross-compatibility. +- Documentation updated with explicit Qwen Image LoRA loader prerequisites for low VRAM users (nunchaku based). +- Version bump across project files. + +### New Node: LoRA Stack (Model In→Out) WAN + +![Node piece no_7](assets/lora_stack_model_I_O.png)lora_stack_model_I_O.png + +Use this node when you already have a base MODEL loaded (WAN 2.2, Flow, SDXL, etc.) and want a single pass application of multiple LoRAs without an intermediate stack/output hand-off. It mirrors the behavior of the classic stack + apply pair but merges them for simpler graphs (especially animation or chained sampler pipelines). + +Inputs: +- `model`: base diffusion MODEL. +- `lora1..lora4` + `strength1..strength4` (skips if "no" or strength == 0.0) +- `model_type`: choose `flow`, `wan2x`, or `standard` to control remapping logic. +- Optional `lora` (LORA) input: allows concatenating a previously built stack from `IAMCCS_WanLoRAStack` for more than 4 total LoRAs. + +Output: +- Patched `MODEL` ready for samplers / video pipelines. + +Recommended Use (WAN 2.2 workflows): +1. Load base WAN 2.2 / LightX2V model. +2. Add `LoRA Stack (Model In→Out) WAN` and select up to 4 LoRAs. +3. (Optional) Chain a classic `IAMCCS_WanLoRAStack` into the optional `lora` input if you need >4. +4. Connect output to KSampler / Animate nodes. + +Why this node: Eliminates one extra node hop, reduces graph complexity and clarifies model lineage in large animation workflows. + +## Previous Versions + +### Version 1.2.3 — New input lora - add another StackLoraModel (concatenate) + Extended Wan 2.1 Compatibility + +### Version 1.2.1 # UPDATE VERSION 1-2-1 @@ -71,7 +105,7 @@ Ideal for WANAnimate, WANVideo, or any Flow-based cinematic model. ![Node piece no_3](assets/ensemble.png) -# New version 1.2.3!! Lora concatenate!! You can add another Lora stack to the node! +# LoRA Concatenation (1.2.3) ![Node piece no_4](assets/lora_concatenatel.png)lora_concatenatel.png @@ -128,3 +162,58 @@ This modular architecture makes LoRA management in WANAnimate flexible, transpar Buy Me A Coffee + +--- + +## Qwen Image LoRA (IAMCCS QwenImgLoadFix) – Updated for 1.3.0 + +This repo ships a fixed Qwen Image LoRA loader with improved offload controls and UX. + +![Node piece no_5](assets/LORAQWEN.png)LORAQWEN.png + +### Prerequisites (Low VRAM Friendly) +Required: +- ComfyUI (≥ 0.3.0) +- `ComfyUI-nunchaku` (Qwen Image / Sana transformer implementation) +- `ComfyUI-QwenImageLoraLoader` (wrappers.qwenimage module with `ComfyQwenImageWrapper`) +- Qwen Image model weights placed as per nunchaku loader instructions +- LoRA files (`.safetensors`) in `ComfyUI/models/loras` + +Recommended for low VRAM systems: +- Enable `offload_auto_tune` (auto scales transformer block residency) +- Set `offload_policy=disable` only if you experience instability during composition (keeps everything on GPU while LoRAs active) +- Keep `offload_num_blocks_on_gpu` low (1–2) if VRAM < 12 GB + +### Node Overview +`IAMCCS QwenImgLoadFix` +- Wraps a `NunchakuQwenImageTransformer2DModel` / `NunchakuSanaTransformer2DModel` if not already wrapped. +- Adds LoRA via internal wrapper (`model_wrapper.loras.append(...)`) with strength preset. +- Supports composition modes: + - `append`: legacy shape-changing approach + - `merge_v2`: in-place delta merge (preferred; no rank expansion) +- Offload Controls: dynamic, rebuild or disable strategies; pin-memory toggle; auto-tune; VRAM margin. +- Device Safety: ensures params & buffers migrate to correct CUDA device before forward. + + + +### Dependency Checklist +| Component | Purpose | +|-----------|---------| +| ComfyUI-nunchaku | Provides transformer class loaded by base model node | +| ComfyUI-QwenImageLoraLoader | Supplies `wrappers/qwenimage.py` used for wrapping | +| IAMCCS-nodes | Adds fixed loader + WAN LoRA stack system | +| LoRA safetensors | User-provided style/character adapters | + +If `wrappers/qwenimage.py` is not found the node will raise an import error. Ensure `ComfyUI-QwenImageLoraLoader` repository resides in `custom_nodes/`. + +### Quick Start (Low VRAM Scenario) +1. Load Qwen Image model (nunchaku loader node). +2. Place `IAMCCS QwenImgLoadFix` directly after it. +3. Select `lora_name` & preset (start with 0.76–1.00 for natural balance). +4. Set `composition_mode=merge_v2` unless you need legacy behavior. +5. Keep `offload_auto_tune=ON`; reduce `offload_num_blocks_on_gpu` if memory errors occur. +6. Run sampler; adjust preset or switch to a higher strength if effect too weak. + +### External Requirements Recap +This loader will not function standalone — both `ComfyUI-nunchaku` and `ComfyUI-QwenImageLoraLoader` must be installed, and the Qwen Image weights must be correctly placed. The IAMCCS node auto-wraps only if it detects a supported transformer class. If wrapping fails, verify repository folder names match expected conventions. + diff --git a/__init__.py b/__init__.py index 6f2372a..ebe3f39 100644 --- a/__init__.py +++ b/__init__.py @@ -1,19 +1,41 @@ # ========================================================== -# __init__.py — Registro nodi IAMCCS LoRA -# Versione pulita: mantiene solo i nodi principali +# __init__.py — Registro nodi IAMCCS +# Versione estesa: include LoRA + Qwen Bridge Conditioning # ========================================================== +# Apply safety monkeypatches on import (no-op if target not present) +from . import iamccs_qwen_monkeypatch # noqa: F401 + from .iamccs_wan_lora_stack import ( IAMCCS_WanLoRAStack, IAMCCS_ModelWithLoRA, ) +from .iamccs_wan_lora_stack_simple import ( + IAMCCS_WanLoRAStackModelIO, +) + +# Qwen Image LoRA loader (fixed copy) +from .iamccs_qwen_lora_loader import ( + IAMCCS_QwenImageLoraLoader, +) + +# Nodi principali NODE_CLASS_MAPPINGS = { "IAMCCS_WanLoRAStack": IAMCCS_WanLoRAStack, "IAMCCS_ModelWithLoRA": IAMCCS_ModelWithLoRA, + "IAMCCS_WanLoRAStackModelIO": IAMCCS_WanLoRAStackModelIO, + "IAMCCS_qwenloraloader": IAMCCS_QwenImageLoraLoader, } NODE_DISPLAY_NAME_MAPPINGS = { "IAMCCS_WanLoRAStack": "LoRA Stack (WAN-style remap)", "IAMCCS_ModelWithLoRA": "Apply LoRA to MODEL (Native)", + "IAMCCS_WanLoRAStackModelIO": "LoRA Stack (Model In→Out) WAN", + "IAMCCS_qwenloraloader": "IAMCCS QwenImgLoraLoaderFix", } + +# Web directory for JavaScript extensions +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/assets/LORAQWEN.png b/assets/LORAQWEN.png new file mode 100644 index 0000000..5418481 Binary files /dev/null and b/assets/LORAQWEN.png differ diff --git a/assets/lora_stack_model_I_O.png b/assets/lora_stack_model_I_O.png new file mode 100644 index 0000000..94c1f4b Binary files /dev/null and b/assets/lora_stack_model_I_O.png differ diff --git a/iamccs_qwen_lora_loader.py b/iamccs_qwen_lora_loader.py new file mode 100644 index 0000000..f2546cb --- /dev/null +++ b/iamccs_qwen_lora_loader.py @@ -0,0 +1,254 @@ +""" +IAMCCS Qwen Image LoRA Loader/Stack +A Nunchaku Qwen Image LoRA loader node +compatible with current `nunchaku` where Qwen image transformer is aliased +as `NunchakuSanaTransformer2DModel`. +""" + +import copy +import logging +import os +import sys + +# Support both old/new nunchaku class names (alias to a common name) +try: + from nunchaku import NunchakuQwenImageTransformer2DModel +except Exception: + from nunchaku import NunchakuSanaTransformer2DModel as NunchakuQwenImageTransformer2DModel + +import folder_paths + +# Logging +log_level = os.getenv("LOG_LEVEL", "INFO").upper() +logging.basicConfig(level=getattr(logging, log_level, logging.INFO), format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def _get_wrappers_module(): + """Dynamically load wrappers.qwenimage from ComfyUI-QwenImageLoraLoader.""" + import importlib.util + # Try to locate the sibling custom node folder + # typical structure: .../custom_nodes/ComfyUI-QwenImageLoraLoader + base_custom_nodes = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + qwen_node_dir = os.path.join(base_custom_nodes, "ComfyUI-QwenImageLoraLoader") + + # Fallback: if running outside expected layout, try current sys.path entries + candidate_dirs = [qwen_node_dir] + [p for p in sys.path if isinstance(p, str) and p.endswith("ComfyUI-QwenImageLoraLoader")] + + wrappers_path = None + for d in candidate_dirs: + wp = os.path.join(d, "wrappers", "qwenimage.py") + if os.path.exists(wp): + wrappers_path = wp + break + + if not wrappers_path: + raise ImportError("Cannot locate ComfyUI-QwenImageLoraLoader/wrappers/qwenimage.py") + + spec = importlib.util.spec_from_file_location("wrappers.qwenimage", wrappers_path) + if spec is None or spec.loader is None: + raise ImportError(f"Failed to load module spec for {wrappers_path}") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # type: ignore[attr-defined] + return mod + + +class IAMCCS_QwenImageLoraLoader: + """ + Load and apply a single LoRA to a Nunchaku Qwen Image model. + """ + @classmethod + def IS_CHANGED(cls, **kwargs): + """ComfyUI calls IS_CHANGED with widget values only; avoid positional args warnings. + Build a hash from model reference (stringified) and LoRA parameters. + """ + import hashlib + m = hashlib.sha256() + model = kwargs.get("model") + if model is not None: + m.update(str(model).encode()) + lora_name = kwargs.get("lora_name", "") + m.update(lora_name.encode()) + preset = str(kwargs.get("lora_strength_preset", "1.00")) + m.update(preset.encode()) + return m.hexdigest() + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ( + "MODEL", + { + "tooltip": "The diffusion model the LoRA will be applied to. Make sure the model is loaded by a Nunchaku Qwen Image loader.", + }, + ), + "lora_name": ( + folder_paths.get_filename_list("loras"), + {"tooltip": "The file name of the LoRA."}, + ), + "lora_strength_preset": ( + ["0.25", "0.50", "0.76", "1.00", "1.25", "1.50"], + { + "default": "1.00", + "tooltip": "Preset strength values.", + }, + ), + "composition_mode": ( + ["append", "merge_v2"], + { + "default": "merge_v2", + "tooltip": "How to apply LoRAs: 'append' (original behavior, may change shapes) or 'merge_v2' (in-place delta, no rank expansion).", + }, + ), + "offload_policy": ( + ["rebuild", "disable"], + { + "default": "rebuild", + "tooltip": "CPU offload with LoRAs: 'rebuild' re-enables offload after composing; 'disable' keeps it off while LoRAs are active.", + }, + ), + "offload_num_blocks_on_gpu": ( + "INT", + { + "default": 1, + "min": 1, + "max": 64, + "step": 1, + "tooltip": "How many transformer blocks stay on GPU when offload is enabled (higher = more VRAM, more speed).", + }, + ), + "offload_use_pin_memory": ( + "BOOLEAN", + { + "default": False, + "tooltip": "Use pinned host memory for offload transfers (can improve bandwidth, uses more system RAM).", + }, + ), + "offload_auto_tune": ( + "BOOLEAN", + { + "default": True, + "tooltip": "Automatically choose offload settings based on free VRAM (overrides saved settings).", + }, + ), + "vram_margin_gb": ( + "FLOAT", + { + "default": 4.0, + "min": 0.0, + "max": 16.0, + "step": 0.25, + "tooltip": "VRAM margin used when cpu_offload_setting='auto' to decide enabling offload for composition.", + }, + ), + } + } + + RETURN_TYPES = ("MODEL",) + OUTPUT_TOOLTIPS = ("The modified diffusion model.",) + FUNCTION = "load_lora" + TITLE = "IAMCCS QwenImgLoraLoaderFix" + CATEGORY = "IAMCCS/Nunchaku" + DESCRIPTION = "Apply a single LoRA to a Nunchaku Qwen Image model." + + def load_lora(self, model, lora_name: str, lora_strength_preset: str = "1.00", composition_mode: str = "merge_v2", offload_policy: str = "rebuild", offload_num_blocks_on_gpu: int = 1, offload_use_pin_memory: bool = False, offload_auto_tune: bool = True, vram_margin_gb: float = 4.0): + # Resolve effective strength from preset or default + import math + strength_val: float = 1.0 + try: + strength_val = float(lora_strength_preset) + except Exception: + strength_val = 1.0 + if not math.isfinite(strength_val): + strength_val = 1.0 + if abs(strength_val) < 1e-5: + return (model,) + + # Coerce offload_num_blocks_on_gpu to a sane integer (avoid NaN/None/inf) + import math as _math + try: + _tmp_val = float(offload_num_blocks_on_gpu) + if not _math.isfinite(_tmp_val): + _tmp_val = 1.0 + if _tmp_val < 1: + _tmp_val = 1.0 + if _tmp_val > 64: + _tmp_val = 64.0 + offload_num_blocks_on_gpu = int(_tmp_val) + except Exception: + offload_num_blocks_on_gpu = 1 + + # Advanced toggle removed: always respect user-provided widget values + + model_wrapper = model.model.diffusion_model + + wrappers_module = _get_wrappers_module() + ComfyQwenImageWrapper = wrappers_module.ComfyQwenImageWrapper + + # Debug logging + model_wrapper_type_name = type(model_wrapper).__name__ + model_wrapper_module = type(model_wrapper).__module__ + logger.info(f"🔍 Model wrapper type: '{model_wrapper_type_name}'") + logger.info(f"🔍 Model wrapper module: {model_wrapper_module}") + + if hasattr(model_wrapper, 'model') and hasattr(model_wrapper, 'loras'): + logger.info("✅ Model is already wrapped (detected via attributes)") + transformer = model_wrapper.model + elif ( + model_wrapper_type_name in ("NunchakuQwenImageTransformer2DModel", "NunchakuSanaTransformer2DModel") + or model_wrapper_type_name.endswith("NunchakuQwenImageTransformer2DModel") + or model_wrapper_type_name.endswith("NunchakuSanaTransformer2DModel") + ): + logger.info("🔧 Wrapping Nunchaku*Qwen/Sana* Transformer with ComfyQwenImageWrapper") + wrapped_model = ComfyQwenImageWrapper( + model_wrapper, + getattr(model_wrapper, 'config', {}), + None, + {}, + "auto", + vram_margin_gb, + lora_offload_policy=offload_policy, + offload_num_blocks_on_gpu=offload_num_blocks_on_gpu, + offload_use_pin_memory=offload_use_pin_memory, + offload_auto_tune=offload_auto_tune, + ) + # Forward composition mode flag (attribute-based to avoid strict kwargs requirements) + try: + setattr(wrapped_model, "lora_composition_mode", composition_mode) + except Exception: + pass + model.model.diffusion_model = wrapped_model + model_wrapper = wrapped_model + transformer = model_wrapper.model + else: + logger.error(f"❌ Model type mismatch! Type: {model_wrapper_type_name}, Module: {model_wrapper_module}") + raise TypeError( + f"This LoRA loader works with Nunchaku Qwen Image models; got {model_wrapper_type_name}." + ) + + # Remove expensive deepcopy (caused device divergence under offload); mutate in place + lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) + try: + setattr(model_wrapper, "lora_composition_mode", composition_mode) + except Exception: + pass + model_wrapper.loras.append((lora_path, strength_val)) + + # Ensure wrapper model resides fully on the original device (avoid mixed cpu/cuda modules) + try: + target_device = next(transformer.parameters()).device + if target_device.type == "cuda": + for p in transformer.parameters(): + if p.device != target_device: + p.data = p.data.to(target_device) + for b in transformer.buffers(): + if b.device != target_device: + b.data = b.data.to(target_device) + except Exception: + pass + + logger.info(f"LoRA added: {lora_name} (strength={strength_val})") + return (model,) + + diff --git a/iamccs_qwen_monkeypatch.py b/iamccs_qwen_monkeypatch.py new file mode 100644 index 0000000..5daa54b --- /dev/null +++ b/iamccs_qwen_monkeypatch.py @@ -0,0 +1,56 @@ +import logging + + +def _patch_qwen_wrapper_device_sync(): + try: + from importlib import import_module + mod = import_module("ComfyUI-QwenImageLoraLoader.wrappers.qwenimage") + ComfyQwenImageWrapper = getattr(mod, "ComfyQwenImageWrapper", None) + if ComfyQwenImageWrapper is None: + return False + except Exception: + return False + + if getattr(ComfyQwenImageWrapper, "_iamccs_device_sync_patched", False): + return True + + orig_execute = getattr(ComfyQwenImageWrapper, "_execute_model", None) + if orig_execute is None: + return False + + def _execute_model_patched(self, x, timestep, context, guidance, control, transformer_options, **kwargs): + # Ensure all params/buffers are on the same device as input x (prevents CPU/CUDA mix) + try: + dev = getattr(x, "device", None) + if dev is not None and dev.type in ("cuda", "cpu") and hasattr(self, "model") and self.model is not None: + needs_move = False + # Quick scan: if any param/buffer is on a different device, move whole model once + for _, p in self.model.named_parameters(recurse=True): + if p.device.type != dev.type: + needs_move = True + break + if not needs_move: + for _, b in self.model.named_buffers(recurse=True): + if b.device.type != dev.type: + needs_move = True + break + if needs_move: + try: + self.model.to(dev) + except Exception: + pass + except Exception: + pass + return orig_execute(self, x, timestep, context, guidance, control, transformer_options, **kwargs) + + setattr(ComfyQwenImageWrapper, "_execute_model", _execute_model_patched) + setattr(ComfyQwenImageWrapper, "_iamccs_device_sync_patched", True) + logging.getLogger(__name__).info("[IAMCCS] Applied device-sync monkeypatch to ComfyQwenImageWrapper") + return True + + +# Execute at import time +try: + _patch_qwen_wrapper_device_sync() +except Exception: + pass diff --git a/iamccs_wan_lora_stack_simple.py b/iamccs_wan_lora_stack_simple.py new file mode 100644 index 0000000..5100ae6 --- /dev/null +++ b/iamccs_wan_lora_stack_simple.py @@ -0,0 +1,112 @@ +# iamccs_wan_lora_stack_simple.py +# =============================================================== +# IAMCCS_WanLoRAStackModelIO +# Multi-LoRA loader (WAN-style remap) that takes MODEL in and outputs MODEL +# =============================================================== + +import logging +import comfy.utils +import comfy.sd +import folder_paths + +from .iamccs_wan_lora_stack import ( + standardize_wan_lora_keys, + SuppressOptionalKeysFilter, +) + + +class IAMCCS_WanLoRAStackModelIO: + @classmethod + def INPUT_TYPES(cls): + lora_list = folder_paths.get_filename_list("loras") + ["no"] + return { + "required": { + "model": ("MODEL",), + "lora1": (lora_list, {"default": "no"}), + "strength1": ("FLOAT", {"default": 1.0, "min": -5.0, "max": 5.0, "step": 0.01}), + "lora2": (lora_list, {"default": "no"}), + "strength2": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}), + "lora3": (lora_list, {"default": "no"}), + "strength3": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}), + "lora4": (lora_list, {"default": "no"}), + "strength4": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}), + "model_type": (["wan2x", "flow", "standard"], {"default": "flow"}), + }, + "optional": { + # Allow chaining in externally prepared LORA stacks if provided (optional) + "lora": ("LORA",), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "apply_stack" + CATEGORY = "IAMCCS/LoRA" + + def _build_lora_entries(self, lora1, strength1, lora2, strength2, lora3, strength3, lora4, strength4, model_type): + loras = [] + for name, strength in [ + (lora1, strength1), + (lora2, strength2), + (lora3, strength3), + (lora4, strength4), + ]: + if not name or name == "no" or strength == 0.0: + continue + path = folder_paths.get_full_path_or_raise("loras", name) + sd = comfy.utils.load_torch_file(path, safe_load=True) + if model_type != "standard": + sd = standardize_wan_lora_keys(sd) + loras.append({"name": name, "strength": strength, "state_dict": sd}) + return loras + + def apply_stack(self, model, + lora1, strength1, + lora2, strength2, + lora3, strength3, + lora4, strength4, + model_type="flow", + lora=None): + model_out = model + + loras = self._build_lora_entries( + lora1, strength1, + lora2, strength2, + lora3, strength3, + lora4, strength4, + model_type, + ) + + if lora is not None and isinstance(lora, list): + loras.extend(lora) + + if not loras: + logging.warning("[IAMCCS_WanLoRAStackModelIO] ⚠ No LoRA selected; returning input model unchanged") + return (model_out,) + + logger = logging.getLogger() + optional_filter = SuppressOptionalKeysFilter() + logger.addFilter(optional_filter) + + try: + for entry in loras: + sd = entry["state_dict"] + strength = entry["strength"] + model_out, _ = comfy.sd.load_lora_for_models(model_out, None, sd, strength, 0) + logging.info(f"[IAMCCS_WanLoRAStackModelIO] ✅ '{entry['name']}' strength={strength}") + + if optional_filter.suppressed_count > 0: + keys_types = ", ".join(sorted(optional_filter.suppressed_keys)) + logging.info(f"[IAMCCS_WanLoRAStackModelIO] ℹ {optional_filter.suppressed_count} optional keys not present in LORA ({keys_types})") + finally: + logger.removeFilter(optional_filter) + + return (model_out,) + + +NODE_CLASS_MAPPINGS = { + "IAMCCS_WanLoRAStackModelIO": IAMCCS_WanLoRAStackModelIO, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "IAMCCS_WanLoRAStackModelIO": "LoRA Stack (Model In→Out) WAN", +} diff --git a/pyproject.toml b/pyproject.toml index ac8684a..748158e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "iamccs-wan-lora-fixer-stack" -version = "1.2.3" -description = "Nodo IAMCCS per ComfyUI che applica fino a 4 LoRA (WAN 2.1 / 2.2 compatibili) con fix automatico e input/output MODEL. Aggiornamento v1.2.3: ADD Lora input for Lora stack adding." +version = "1.3.0" +description = "IAMCCS multi-LoRA stack & MODEL IO WAN remap + Qwen Image LoRA loader improvements. v1.3.0 adds direct MODEL in→out LoRA stack and updated low VRAM Qwen instructions." license = { text = "MIT" } authors = [ { name = "Carmine Cristallo Scalzi (IAMCCS)", email = "info@carminecristalloscalzi.com" } diff --git a/version.json b/version.json index 960651e..fd1091c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "name": "iamccs-wan-lora-fixer-stack", - "version": "1.2.3", + "version": "1.3.0", "author": "Carmine Cristallo Scalzi (IAMCCS)", - "description": "Nodo IAMCCS per ComfyUI che applica fino a 8 LoRA in stack (concatenabili), compatibile con WAN 2.2 (Flow). Aggiornamento per compatibilità estesa a WAN 2.1." + "description": "IAMCCS nodes for ComfyUI: multi-LoRA stack + direct MODEL IO WAN/Flow remap, Qwen Image LoRA loader improvements. v1.3.0 adds MODEL in→out stack node and updated low VRAM Qwen instructions. WAN 2.1 + 2.2 compatible." }