17 Commits
Author SHA1 Message Date
erosDiffusion eb5bd4dc43 Update README.md 2025-12-11 12:44:59 +01:00
Enrico 3256626436 move byproducts to trash 2025-12-09 20:43:18 +01:00
Enrico b784fcd797 Merge branch 'master' of https://github.com/erosDiffusion/ComfyUI-EulerDiscreteScheduler 2025-12-09 20:36:14 +01:00
Enrico 1840e5147b Fix: Explicitly register schedulers in KSampler.SCHEDULERS for RES4LYF compatibility 2025-12-09 20:29:14 +01:00
Enrico a3b87ec38b vq-scheduler 2025-12-09 20:25:26 +01:00
erosDiffusion 20baa09b85 Update README.md 2025-12-09 13:27:22 +01:00
Enrico adf491a3fa multiple changes extract metadata, compat 2025-12-03 09:59:08 +01:00
erosDiffusion 9ae7940aee Update README for version 1.0.6 changes 2025-12-02 14:47:45 +01:00
Enrico 96a0736b2f 1.0.7 nunchaku qwen and tiled diffusion fixes. update readme , bump version 2025-12-02 14:43:26 +01:00
Enrico ccc7c7b54c posttible fix for qwen nunchaku 2025-12-02 14:26:23 +01:00
Enrico b0ee7aa43b Merge branch 'master' of https://github.com/erosDiffusion/ComfyUI-EulerDiscreteScheduler 2025-12-01 23:01:20 +01:00
erosDiffusion ac7a096d63 Update README.md 2025-12-01 22:57:16 +01:00
erosDiffusion 9a248b4a65 Update README.md 2025-12-01 22:55:48 +01:00
Enrico 430a8a2d6e 1.0.6 2025-12-01 22:05:39 +01:00
erosDiffusion 15942353cd Merge pull request #3 from Meettya/patch-1
Update pyproject.toml
2025-12-01 21:37:41 +01:00
Karpich Dmitry 9dbea3a1ea Update pyproject.toml
Add dependencies and remove empty lines
2025-12-01 22:13:13 +03:00
Enrico dae4db9ca9 1.0.5 2025-12-01 00:20:16 +01:00
15 changed files with 851 additions and 16 deletions
+30 -2
View File
@@ -35,7 +35,14 @@ Example output (more below)
- **Advanced/experimental**: - **Advanced/experimental**:
1. Add **FlowMatch Euler Discrete Scheduler (Custom)** node to your workflow 1. Add **FlowMatch Euler Discrete Scheduler (Custom)** node to your workflow
2. Connect its SIGMAS output to **SamplerCustom** node's sigmas input 2. Connect its SIGMAS output to **SamplerCustom** node's sigmas input
3. Adjust parameters to control the sampling behavior 3. Adjust parameters to control the sampling behavior, you have ALL the parameters to play with.
## Troubleshoot
- if the scheduler does not appear when you have res4lyf package installed you can try:
-- workaround 1: adding an samplerCustom node and connect the sigmas to a basicScheduler node. this way the scheduler should be available in the list
-- workaround 2: disable res4lyf if you don't need that
-- workaround 3 use the flowmatch scheduler (custom) and connect to the sigmas of the samplerCustom.
- if your install fails you might have to use the correct version of peft package, some users reported this as issue, check startup logs and install the proper version
## Tech bits: ## Tech bits:
@@ -53,8 +60,29 @@ More examples:
<img width="1536" height="1088" alt="image" src="https://github.com/user-attachments/assets/1931af7e-1b3e-47c9-ac20-27add5135a71" /> <img width="1536" height="1088" alt="image" src="https://github.com/user-attachments/assets/1931af7e-1b3e-47c9-ac20-27add5135a71" />
## Changelog ## Changelog
**1.0.8**
- attempt fixing incompatibility with res4lyf by adding the scheduler to the list.
**1.0.7**
- nunchaku qwen patch fix, tiled diffusion patch fix
users reported issues with dimensions not being handled correctly, this should fix it.
**1.0.6**
- updated example
- updated pyproject deps (diffusers)
**1.0.5**
- remove bad practice of forking diffusers install on error (requirements.txt and does not rollback your diffusers if available)
**1.0.4** **1.0.4**
add start and end step by Etupa, with some fixes
- add start and end step by Etupa, with some fixes (can be used for image to image or restart sampling)
<img width="2880" height="960" alt="node_unknown" src="https://github.com/user-attachments/assets/247cb5ab-241f-43ce-b9d4-61c56ccb3711" />
**1.0.3** **1.0.3**
+118 -10
View File
@@ -14,14 +14,32 @@ import numpy as np # <-- Required for robust slicing of PyTorch tensors
try: try:
from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler
except ImportError as e:
print("=" * 80)
print("ERROR: Failed to import FlowMatchEulerDiscreteScheduler from diffusers")
print("Please ensure dependencies are installed by running:")
print(" pip install -r requirements.txt")
print("=" * 80)
raise ImportError(
"FlowMatchEulerDiscreteScheduler not found. "
"Please install dependencies from requirements.txt"
) from e
try:
from diffusers import VQDiffusionScheduler
except ImportError: except ImportError:
import subprocess VQDiffusionScheduler = None
import sys print("[FlowMatch Scheduler] Warning: VQDiffusionScheduler not found in diffusers.")
subprocess.check_call([sys.executable, "-m", "pip", "install", "diffusers"])
from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler
from comfy.samplers import SchedulerHandler, SCHEDULER_HANDLERS, SCHEDULER_NAMES from comfy.samplers import SchedulerHandler, SCHEDULER_HANDLERS, SCHEDULER_NAMES
# Import Nunchaku compatibility patches (auto-applies on import)
try:
from . import nunchaku_compat
except Exception as e:
print(f"[FlowMatch Scheduler] Warning: Could not load Nunchaku compatibility: {e}")
# Default config for registering in ComfyUI # Default config for registering in ComfyUI
default_config = { default_config = {
"base_image_seq_len": 256, "base_image_seq_len": 256,
@@ -46,12 +64,50 @@ def flow_match_euler_scheduler_handler(model_sampling, steps):
sigmas = scheduler.sigmas sigmas = scheduler.sigmas
return sigmas return sigmas
# Register the scheduler in ComfyUI def vq_diffusion_scheduler_handler(model_sampling, steps):
if VQDiffusionScheduler is None:
raise ImportError("VQDiffusionScheduler is not available.")
# VQDiffusionScheduler requires num_vec_classes.
print("[FlowMatch Scheduler] WARNING: VQDiffusionScheduler is for discrete models (VQ-Diffusion).")
print("It does not produce 'sigmas' for continuous diffusion.")
print("Returning dummy linear sigmas to prevent crash, but sampling will likely fail with standard models.")
# Dummy initialization
# scheduler = VQDiffusionScheduler(num_vec_classes=4096, num_train_timesteps=1000)
# Return dummy sigmas
sigmas = torch.linspace(1.0, 0.0, steps + 1)
if hasattr(model_sampling, 'device'):
sigmas = sigmas.to(model_sampling.device)
return sigmas
# Register the schedulers in ComfyUI
if "FlowMatchEulerDiscreteScheduler" not in SCHEDULER_HANDLERS: if "FlowMatchEulerDiscreteScheduler" not in SCHEDULER_HANDLERS:
handler = SchedulerHandler(handler=flow_match_euler_scheduler_handler, use_ms=True) handler = SchedulerHandler(handler=flow_match_euler_scheduler_handler, use_ms=True)
SCHEDULER_HANDLERS["FlowMatchEulerDiscreteScheduler"] = handler SCHEDULER_HANDLERS["FlowMatchEulerDiscreteScheduler"] = handler
SCHEDULER_NAMES.append("FlowMatchEulerDiscreteScheduler") SCHEDULER_NAMES.append("FlowMatchEulerDiscreteScheduler")
# Explicitly add to KSampler.SCHEDULERS to ensure compatibility with nodes
# that might replace the list object (like RES4LYF)
try:
from comfy.samplers import KSampler
if "FlowMatchEulerDiscreteScheduler" not in KSampler.SCHEDULERS:
KSampler.SCHEDULERS.append("FlowMatchEulerDiscreteScheduler")
except ImportError:
pass
# if "VQDiffusionScheduler" not in SCHEDULER_HANDLERS:
# SCHEDULER_HANDLERS["VQDiffusionScheduler"] = SchedulerHandler(handler=vq_diffusion_scheduler_handler, use_ms=True)
# SCHEDULER_NAMES.append("VQDiffusionScheduler")
# try:
# from comfy.samplers import KSampler
# if "VQDiffusionScheduler" not in KSampler.SCHEDULERS:
# KSampler.SCHEDULERS.append("VQDiffusionScheduler")
# except ImportError:
# pass
class FlowMatchEulerSchedulerNode: class FlowMatchEulerSchedulerNode:
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
@@ -63,13 +119,13 @@ class FlowMatchEulerSchedulerNode:
"max": 10000, "max": 10000,
"tooltip": "Total number of diffusion steps to generate the full sigma schedule." "tooltip": "Total number of diffusion steps to generate the full sigma schedule."
}), }),
"start_at_step": ("INT", { # <-- NEW INPUT "start_at_step": ("INT", {
"default": 0, "default": 0,
"min": 0, "min": 0,
"max": 10000, "max": 10000,
"tooltip": "The starting step (index) of the sigma schedule to use. Set to 0 to start at the beginning (first step)." "tooltip": "The starting step (index) of the sigma schedule to use. Set to 0 to start at the beginning (first step)."
}), }),
"end_at_step": ("INT", { # <-- NEW INPUT "end_at_step": ("INT", {
"default": 9999, "default": 9999,
"min": 0, "min": 0,
"max": 10000, "max": 10000,
@@ -81,6 +137,7 @@ class FlowMatchEulerSchedulerNode:
}), }),
"base_shift": ("FLOAT", { "base_shift": ("FLOAT", {
"default": 0.5, "default": 0.5,
"step": 0.01,
"tooltip": "Stabilizes generation. Higher values = more consistent/predictable outputs. Z-Image-Turbo uses default 0.5." "tooltip": "Stabilizes generation. Higher values = more consistent/predictable outputs. Z-Image-Turbo uses default 0.5."
}), }),
"invert_sigmas": (["disable", "enable"], { "invert_sigmas": (["disable", "enable"], {
@@ -93,6 +150,7 @@ class FlowMatchEulerSchedulerNode:
}), }),
"max_shift": ("FLOAT", { "max_shift": ("FLOAT", {
"default": 1.15, "default": 1.15,
"step": 0.01,
"tooltip": "Maximum variation allowed. Higher = more exaggerated/stylized results. Z-Image-Turbo uses default 1.15." "tooltip": "Maximum variation allowed. Higher = more exaggerated/stylized results. Z-Image-Turbo uses default 1.15."
}), }),
"num_train_timesteps": ("INT", { "num_train_timesteps": ("INT", {
@@ -101,10 +159,12 @@ class FlowMatchEulerSchedulerNode:
}), }),
"shift": ("FLOAT", { "shift": ("FLOAT", {
"default": 3.0, "default": 3.0,
"step": 0.01,
"tooltip": "Global timestep schedule shift. Z-Image-Turbo uses 3.0 for optimal performance with the Turbo model." "tooltip": "Global timestep schedule shift. Z-Image-Turbo uses 3.0 for optimal performance with the Turbo model."
}), }),
"shift_terminal": ("FLOAT", { "shift_terminal": ("FLOAT", {
"default": 0.0, "default": 0.0,
"step": 0.01,
"tooltip": "End value for shifted schedule. Set to 0.0 to disable. Advanced parameter for timestep schedule control." "tooltip": "End value for shifted schedule. Set to 0.0 to disable. Advanced parameter for timestep schedule control."
}), }),
"stochastic_sampling": (["disable", "enable"], { "stochastic_sampling": (["disable", "enable"], {
@@ -147,8 +207,8 @@ class FlowMatchEulerSchedulerNode:
def create( def create(
self, self,
steps, steps,
start_at_step, # <-- New parameter start_at_step,
end_at_step, # <-- New parameter end_at_step,
base_image_seq_len, base_image_seq_len,
base_shift, base_shift,
invert_sigmas, invert_sigmas,
@@ -216,10 +276,58 @@ class FlowMatchEulerSchedulerNode:
return (sigmas_sliced,) return (sigmas_sliced,)
class VQDiffusionSchedulerNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"num_vec_classes": ("INT", {"default": 4096, "min": 1, "max": 65536, "tooltip": "Number of vector classes for VQ model."}),
"num_train_timesteps": ("INT", {"default": 1000}),
}
}
RETURN_TYPES = ("SIGMAS",)
RETURN_NAMES = ("sigmas",)
FUNCTION = "create"
CATEGORY = "sampling/schedulers"
DESCRIPTION = "VQ Diffusion Scheduler (Experimental). For VQ-Diffusion models. Returns dummy sigmas for compatibility."
def create(self, steps, num_vec_classes, num_train_timesteps):
if VQDiffusionScheduler is None:
raise ImportError("VQDiffusionScheduler not found.")
print("[FlowMatch Scheduler] Creating VQDiffusionScheduler (Experimental)")
print("[FlowMatch Scheduler] WARNING: Returning dummy sigmas. This scheduler is for discrete latent models.")
# We don't actually use the scheduler to generate sigmas because it can't.
# We just return the dummy sigmas.
sigmas = torch.linspace(1.0, 0.0, steps + 1)
# Default to CPU, KSampler will move it if needed or we can try to detect
# But here we don't have model context easily.
return (sigmas,)
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"FlowMatchEulerDiscreteScheduler (Custom)": FlowMatchEulerSchedulerNode, "FlowMatchEulerDiscreteScheduler (Custom)": FlowMatchEulerSchedulerNode,
# "VQDiffusionScheduler": VQDiffusionSchedulerNode,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"FlowMatchEulerDiscreteScheduler (Custom)": "FlowMatch Euler Discrete Scheduler (Custom)", "FlowMatchEulerDiscreteScheduler (Custom)": "FlowMatch Euler Discrete Scheduler (Custom)",
} # "VQDiffusionScheduler": "VQ Diffusion Scheduler (Experimental)",
}
from .extract_metadata_node import NODE_CLASS_MAPPINGS as METADATA_NODE_MAPPINGS
from .extract_metadata_node import NODE_DISPLAY_NAME_MAPPINGS as METADATA_DISPLAY_MAPPINGS
NODE_CLASS_MAPPINGS.update(METADATA_NODE_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(METADATA_DISPLAY_MAPPINGS)
# Import Nunchaku nodes
try:
from .nunchaku_compat import NODE_CLASS_MAPPINGS as NUNCHAKU_NODES
from .nunchaku_compat import NODE_DISPLAY_NAME_MAPPINGS as NUNCHAKU_NAMES
NODE_CLASS_MAPPINGS.update(NUNCHAKU_NODES)
NODE_DISPLAY_NAME_MAPPINGS.update(NUNCHAKU_NAMES)
except Exception as e:
print(f"[FlowMatch Scheduler] Could not load Nunchaku nodes: {e}")
+106
View File
@@ -0,0 +1,106 @@
import torch
import os
import json
from PIL import Image, ImageOps
import folder_paths
import numpy as np
class ImageMetadataExtractor:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
return {"required":
{"image": (sorted(files), {"image_upload": True})},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT", "INT", "STRING")
RETURN_NAMES = ("image", "positive_prompt", "width", "height", "filename")
FUNCTION = "extract_metadata"
CATEGORY = "utils"
def extract_metadata(self, image):
image_path = folder_paths.get_annotated_filepath(image)
img = Image.open(image_path)
output_image = ImageOps.exif_transpose(img)
output_image = output_image.convert("RGB")
output_image = np.array(output_image).astype(np.float32) / 255.0
output_image = torch.from_numpy(output_image)[None,]
positive_prompt = ""
width = 0
height = 0
# Extract from 'prompt' (API format) which is what ComfyUI uses for execution
if 'prompt' in img.info:
try:
prompt = json.loads(img.info['prompt'])
# 1. Find Positive Prompt
# Strategy: Find KSampler -> positive input -> CLIPTextEncode -> text
ksampler_nodes = []
for node_id, node in prompt.items():
class_type = node.get('class_type', '')
if 'KSampler' in class_type or 'SamplerCustom' in class_type:
ksampler_nodes.append(node)
for ksampler in ksampler_nodes:
inputs = ksampler.get('inputs', {})
if 'positive' in inputs:
positive_link = inputs['positive']
if isinstance(positive_link, list): # It's a link [node_id, slot_index]
positive_node_id = str(positive_link[0])
if positive_node_id in prompt:
positive_node = prompt[positive_node_id]
if positive_node.get('class_type') == 'CLIPTextEncode':
positive_prompt = positive_node.get('inputs', {}).get('text', "")
break # Found it
# Fallback: Look for any CLIPTextEncode with "positive" in title/meta if not found via KSampler
if not positive_prompt:
candidates = []
for node_id, node in prompt.items():
if node.get('class_type') == 'CLIPTextEncode':
title = node.get('_meta', {}).get('title', '').lower()
text = node.get('inputs', {}).get('text', "")
if 'positive' in title and 'negative' not in title:
candidates.append(text)
# Also consider just long text if no clear title match
elif len(text) > 50:
candidates.append(text)
# Pick the longest candidate if any found
if candidates:
positive_prompt = max(candidates, key=len)
# 2. Find Width/Height
# Strategy: Find EmptyLatentImage
for node_id, node in prompt.items():
if node.get('class_type') == 'EmptyLatentImage':
width = node.get('inputs', {}).get('width', 0)
height = node.get('inputs', {}).get('height', 0)
break
# Fallback: Look for width/height in any node if still 0
if width == 0 or height == 0:
for node_id, node in prompt.items():
inputs = node.get('inputs', {})
if 'width' in inputs and 'height' in inputs:
width = inputs['width']
height = inputs['height']
break
except Exception as e:
print(f"Error parsing metadata: {e}")
return (output_image, positive_prompt, width, height, image)
# Node registration
NODE_CLASS_MAPPINGS = {
"ImageMetadataExtractor": ImageMetadataExtractor
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageMetadataExtractor": "Load Image ErosDiffusion"
}
+317
View File
@@ -0,0 +1,317 @@
# Nunchaku Qwen Direct Model Patcher
#
# This module monkey-patches Nunch aku models at runtime to fix dimension mismatches
# It installs a wrapper around model.apply_model that detects and fixes tensor shapes
import torch
import logging
logger = logging.getLogger(__name__)
_original_module_call = None
_original_tiled_call = None
_patch_applied = False
def is_nunchaku_qwen_model(model):
"""Detect if the model is a Nunchaku Qwen model"""
try:
if hasattr(model, 'diffusion_model'):
dm = model.diffusion_model
if hasattr(dm, 'txt_norm') and hasattr(dm.txt_norm, 'normalized_shape'):
return True
return False
except Exception:
return False
def get_expected_txt_dim(model):
"""Get the expected text encoder dimension from txt_norm"""
try:
if hasattr(model, 'diffusion_model'):
dm = model.diffusion_model
if hasattr(dm, 'txt_norm') and hasattr(dm.txt_norm, 'normalized_shape'):
return dm.txt_norm.normalized_shape[0]
return None
except Exception:
return None
def patched_apply_model(original_func):
"""Wrapper for model.apply_model that fixes dimension mismatches"""
projection_cache = {}
def wrapper(self, *args, **kwargs):
if not is_nunchaku_qwen_model(self):
return original_func(self, *args, **kwargs)
expected_dim = get_expected_txt_dim(self)
if expected_dim is None:
return original_func(self, *args, **kwargs)
context = kwargs.get('context', None)
if context is not None and context.shape[-1] != expected_dim:
actual_dim = context.shape[-1]
print(f"[Nunchaku Compat] Fixing dimension: {actual_dim} -> {expected_dim}")
cache_key = (actual_dim, expected_dim, context.device, context.dtype)
if cache_key not in projection_cache:
projection = torch.nn.Linear(actual_dim, expected_dim, bias=False,
device=context.device, dtype=context.dtype)
with torch.no_grad():
if actual_dim > expected_dim:
projection.weight.data = torch.eye(expected_dim, actual_dim,
device=context.device, dtype=context.dtype)
else:
projection.weight.data = torch.zeros(expected_dim, actual_dim,
device=context.device, dtype=context.dtype)
projection.weight.data[:actual_dim, :] = torch.eye(actual_dim,
device=context.device, dtype=context.dtype)
projection_cache[cache_key] = projection
context = projection_cache[cache_key](context)
kwargs['context'] = context
return original_func(self, *args, **kwargs)
return wrapper
def patch_diffusion_model_forward(original_forward):
"""Wrap diffusion_model's forward/__call__ to fix encoder_hidden_states"""
projection_cache = {}
def wrapper(self, *args, **kwargs):
# Check if this is a Nunchaku model by looking for txt_norm
if not (hasattr(self, 'txt_norm') and hasattr(self.txt_norm, 'normalized_shape')):
return original_forward(self, *args, **kwargs)
expected_dim = self.txt_norm.normalized_shape[0]
# Extract encoder_hidden_states from kwargs
# The diffusion model is called with: diffusion_model(xc, t, context=context, ...)
# which maps to: forward(hidden_states, encoder_hidden_states, ...)
# So 'context' kwarg becomes encoder_hidden_states parameter
encoder_hidden_states = None
param_name = None
# Try common parameter names
for name in ['context', 'encoder_hidden_states', 'text_embeds']:
if name in kwargs:
encoder_hidden_states = kwargs[name]
param_name = name
break
# If not in kwargs, it might be in args (positional)
# Typical signature: forward(self, hidden_states, encoder_hidden_states, timestep, ...)
if encoder_hidden_states is None and len(args) > 1:
# args[0] = hidden_states, args[1] = encoder_hidden_states
if isinstance(args[1], torch.Tensor) and len(args[1].shape) >= 2:
encoder_hidden_states = args[1]
param_name = 'args[1]'
# Check and fix dimension mismatch
if encoder_hidden_states is not None and isinstance(encoder_hidden_states, torch.Tensor):
# encoder_hidden_states should be shape [batch, seq_len, dim]
if len(encoder_hidden_states.shape) >= 2:
actual_dim = encoder_hidden_states.shape[-1]
if actual_dim != expected_dim:
print(f"[Nunchaku Compat] Fixing {param_name}: shape={list(encoder_hidden_states.shape)}, {actual_dim} -> {expected_dim}")
cache_key = (actual_dim, expected_dim, encoder_hidden_states.device, encoder_hidden_states.dtype)
if cache_key not in projection_cache:
projection = torch.nn.Linear(actual_dim, expected_dim, bias=False,
device=encoder_hidden_states.device,
dtype=encoder_hidden_states.dtype)
with torch.no_grad():
if actual_dim > expected_dim:
projection.weight.data = torch.eye(expected_dim, actual_dim,
device=encoder_hidden_states.device,
dtype=encoder_hidden_states.dtype)
projection_cache[cache_key] = projection
print(f"[Nunchaku Compat] Created projection layer {actual_dim}->{expected_dim}")
encoder_hidden_states = projection_cache[cache_key](encoder_hidden_states)
# Update the parameter
if param_name in kwargs:
kwargs[param_name] = encoder_hidden_states
elif param_name == 'args[1]':
args = list(args)
args[1] = encoder_hidden_states
args = tuple(args)
return original_forward(self, *args, **kwargs)
return wrapper
# Global variables to store original functions
_original_module_call = None
_original_tiled_call = None
def apply_nunchaku_patches():
"""Apply monkey patches to fix Nunchaku compatibility issues"""
global _patch_applied, _original_module_call, _original_tiled_call
if _patch_applied:
print("[Nunchaku Compat] Patches already applied")
return
try:
# We need to patch at the diffusion_model level, not model.apply_model
# The patch will be applied when models are loaded
import torch.nn as nn
# Store original if not already stored
if _original_module_call is None:
_original_module_call = nn.Module.__call__
# Patch torch.nn.Module's __call__ for modules that have txt_norm
# This is tricky - we'll patch specific Nunchaku model classes when we detect them
def patched_module_call(self, *args, **kwargs):
# ONLY patch Nunchaku diffusion models - very specific detection
# Must have all three: txt_norm, txt_in, img_in (unique to Nunchaku Qwen)
is_nunchaku_diffusion = (
hasattr(self, 'txt_norm') and
hasattr(self, 'txt_in') and
hasattr(self, 'img_in') and
hasattr(self, 'transformer_blocks') # Extra check to ensure it's the diffusion model
)
if is_nunchaku_diffusion:
# This is a NunchakuQwenImageTransformer2DModel
if not hasattr(self, '_nunchaku_patched'):
print(f"[Nunchaku Compat] Detected and patching Nunchaku diffusion model: {type(self).__name__}")
self._nunchaku_patched = True
return patch_diffusion_model_forward(_original_module_call)(self, *args, **kwargs)
# For all other modules (VAE, etc.), use original __call__ without modification
return _original_module_call(self, *args, **kwargs)
nn.Module.__call__ = patched_module_call
# Also patch TiledDiffusion if present
try:
import sys
if 'ComfyUI-TiledDiffusion.tiled_diffusion' in sys.modules:
tiled_diff = sys.modules['ComfyUI-TiledDiffusion.tiled_diffusion']
if hasattr(tiled_diff, 'TiledDiffusion'):
if _original_tiled_call is None:
_original_tiled_call = tiled_diff.TiledDiffusion.__call__
def patched_tiled_call(self, model_function, kwargs):
"""Wrap TiledDiffusion to handle 5D tensors from Qwen Image models"""
x_in = kwargs.get('input', None)
# Check if we have a 5D tensor
if x_in is not None and len(x_in.shape) == 5:
# Shape is [N, C, F, H, W], squeeze F dimension if it's 1
N, C, F, H, W = x_in.shape
if F == 1:
print(f"[Nunchaku Compat] TiledDiffusion: Squeezing 5D tensor {list(x_in.shape)} -> 4D")
kwargs['input'] = x_in.squeeze(2) # Remove F dimension
# Call original with 4D tensor
result = _original_tiled_call(self, model_function, kwargs)
# Restore 5D shape if result is 4D
if isinstance(result, torch.Tensor) and len(result.shape) == 4:
result = result.unsqueeze(2) # Add F dimension back
print(f"[Nunchaku Compat] TiledDiffusion: Restored to 5D shape {list(result.shape)}")
return result
else:
print(f"[Nunchaku Compat] TiledDiffusion: Warning - 5D tensor with F={F} (not 1), cannot safely squeeze")
return _original_tiled_call(self, model_function, kwargs)
tiled_diff.TiledDiffusion.__call__ = patched_tiled_call
print("[Nunchaku Compat] Successfully patched TiledDiffusion for 5D tensor support")
except Exception as e:
print(f"[Nunchaku Compat] Could not patch TiledDiffusion (not installed or incompatible): {e}")
print("[Nunchaku Compat] Successfully installed Nunchaku compatibility patches")
_patch_applied = True
except Exception as e:
print(f"[Nunchaku Compat] Error applying patches: {e}")
import traceback
traceback.print_exc()
def remove_nunchaku_patches():
"""Remove Nunchaku compatibility patches"""
global _patch_applied, _original_module_call, _original_tiled_call
if not _patch_applied:
print("[Nunchaku Compat] Patches not applied, nothing to remove")
return
try:
import torch.nn as nn
# Restore nn.Module.__call__
if _original_module_call is not None:
nn.Module.__call__ = _original_module_call
print("[Nunchaku Compat] Restored original nn.Module.__call__")
# Restore TiledDiffusion.__call__
try:
import sys
if 'ComfyUI-TiledDiffusion.tiled_diffusion' in sys.modules:
tiled_diff = sys.modules['ComfyUI-TiledDiffusion.tiled_diffusion']
if hasattr(tiled_diff, 'TiledDiffusion') and _original_tiled_call is not None:
tiled_diff.TiledDiffusion.__call__ = _original_tiled_call
print("[Nunchaku Compat] Restored original TiledDiffusion.__call__")
except Exception as e:
print(f"[Nunchaku Compat] Error restoring TiledDiffusion: {e}")
_patch_applied = False
print("[Nunchaku Compat] Successfully removed Nunchaku compatibility patches")
except Exception as e:
print(f"[Nunchaku Compat] Error removing patches: {e}")
import traceback
traceback.print_exc()
class NunchakuQwenPatches:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mode": (["enable", "disable"], {"default": "enable"}),
},
"optional": {
"model": ("MODEL",),
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("MODEL", "IMAGE",)
RETURN_NAMES = ("model", "image",)
FUNCTION = "execute"
CATEGORY = "utils"
def execute(self, mode, model=None, image=None):
if mode == "enable":
apply_nunchaku_patches()
else:
remove_nunchaku_patches()
return (model, image)
NODE_CLASS_MAPPINGS = {
"NunchakuQwenPatches": NunchakuQwenPatches
}
NODE_DISPLAY_NAME_MAPPINGS = {
"NunchakuQwenPatches": "Nunchaku Qwen Patches"
}
+3 -4
View File
@@ -1,8 +1,9 @@
[project] [project]
name = "erosdiffusion-eulerflowmatchingdiscretescheduler" name = "erosdiffusion-eulerflowmatchingdiscretescheduler"
description = "Noise Free images with Euler Discrete Scheduler in ComfyUI with Z-Image or other models" description = "Noise Free images with Euler Discrete Scheduler in ComfyUI with Z-Image or other models"
version = "1.0.4" version = "1.0.8"
license = {file = "LICENSE.TXT"} license = {file = "LICENSE.TXT"}
dependencies = ["diffusers"]
# classifiers = [ # classifiers = [
# # For OS-independent nodes (works on all operating systems) # # For OS-independent nodes (works on all operating systems)
# "Operating System :: OS Independent", # "Operating System :: OS Independent",
@@ -16,7 +17,6 @@ license = {file = "LICENSE.TXT"}
# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support # "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support
# ] # ]
[project.urls] [project.urls]
Repository = "https://github.com/erosDiffusion/ComfyUI-EulerDiscreteScheduler" Repository = "https://github.com/erosDiffusion/ComfyUI-EulerDiscreteScheduler"
# Used by Comfy Registry https://registry.comfy.org # Used by Comfy Registry https://registry.comfy.org
@@ -29,4 +29,3 @@ DisplayName = "ComfyUI-EulerFlowMatchingDiscreteScheduler"
Icon = "💜" Icon = "💜"
includes = [] includes = []
# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility # "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility
+13
View File
@@ -0,0 +1,13 @@
# Requirements
## Requirement 1
**Date**: 2025-12-04
**Description**: Create code to expose VQVAE scheduler (VQDiffusionScheduler) in the schedulers list and as a node to see if it can be used for sampling.
**Branch**: feature/req1-vq-scheduler
**Status**: In Progress
## Requirement 2
**Date**: 2025-12-09
**Description**: Investigate and fix compatibility issue with RES4LYF custom node package where flowmatcheeulerdiscretescheduler disappears from ksampler list.
**Branch**: feature/req2-res4lyf-compat
**Status**: Completed
+122
View File
@@ -0,0 +1,122 @@
================================================================================
Image: ComfyUI-zit_00006_.png
Dimensions: 0x0
Prompt:
胶片摄影,镜头语言,淡彩,暗调,氛围低光,个性视角,身穿朝鲜族民族服饰的少女,灵动俏皮,朦胧梦幻,高颜值,既视感,现场感,情绪氛围感拉满,透视感,暗朦,光朦,泛白,褪色,漏光,粒子高噪点,胶片颗粒质感,层次丰富,写意,朦胧美学,光的美学,lomo效果,超现实,高级感,杰作,
pornmaster bukkake, white cum on her face, white cum on her hair, White cum covered her hair, white cum covered her face. White cum covered her clothes
濃稠半透明精液從她的臉滴落,液體拉絲滴落,
================================================================================
Image: ComfyUI_00030_.png
Dimensions: 1792x1120
Prompt:
A low-angle cinematic shot in a dense bamboo forest at golden hour, sunbeams filtering diagonally through the tall green bamboo canopy to cast sharp, dramatic shadows on the moss-covered ground. A young East Asian swordswoman in her mid-twenties, with long flowing black hair escaping her topknot, wears a traditional red silk martial arts robe edged with gold thread embroidery; her expression is intensely focused, muscles tensed in her arms and legs. She is captured mid-action during a horizontal sword slash: body twisted dynamically forward, left foot planted firmly on emerald moss, right arm fully extended holding a polished steel jian sword with a black lacquered hilt, the blade angled sharply downward. Bamboo stalks surround her, their smooth green surfaces textured with vertical grooves and dew drops catching the light; fallen bamboo leaves scatter the forest floor, partially covered in soft velvety moss. Mist drifts faintly near the ground, with background bamboo softly blurred to create depth. Color palette emphasizes vibrant jade greens of the bamboo, warm amber sunlight, and the rich crimson of the robe, accented by the metallic gleam of the sword. Highly detailed textures include the woven silk fabric rippling with movement, the sword's reflective edge, and the rough bark of nearby bamboo trunks.
================================================================================
Image: Comfy Edit ZimageVs_00004_.png
Dimensions: 0x0
Prompt:
================================================================================
Image: ComfyUI_00041_.png
Dimensions: 0x0
Prompt:
================================================================================
Image: ComfyUI-zit_00060_.png
Dimensions: 0x0
Prompt:
masterpiece, best quality, photo realistic, 8k, a cybernetic woman with flowing nanotech ink tattoos animating across her skin, glossy black fluid moving like circuitry, sleek tech-editorial mood , hyperdetailed, dramatic lighting, cinematic shot, ultra detailed, intricate details, cinematic, photorealistic, masterpiece
================================================================================
Image: ComfyUI_00008_.png
Dimensions: 512x512
Prompt:
['7', 0]
================================================================================
Image: ComfyUI-sqnd-multi-flux_00013_.png
Dimensions: 0x0
Prompt:
================================================================================
Image: ComfyUI-sqnd-multi-zimage_00013_.png
Dimensions: 0x0
Prompt:
================================================================================
Image: ComfyUI-zit_00028_.png
Dimensions: 0x0
Prompt:
A close-up, explicit portrait of a 25-year-old beautiful woman in an ancient Egyptian royal sleep chamber at night. She has long black hair and a curvy figure with saggy, hanging breasts, visible nipples, and natural pubic hair. She wears elaborate golden body jewelry, including an underbra, armlets, thighlets, and a crotchless thong. The scene is captured from multiple angles—from behind, straight on, and from below—with dramatic foreshortening, focusing on her buttocks and visible labia and clitoral hood. The atmosphere is intimate, lit by candlelight with a warm, dim glow, casting soft shadows across her body and the silk bed she rests on. detailed labia, clitoral hood, visible pussy, from below, from behind
================================================================================
Image: ComfyUI_00050_.png
Dimensions: 1024x1024
Prompt:
tranin passing, anime style, 4k ultra resolution, flat shading.
================================================================================
Image: ComfyUI-FlowmatchEuler-Simple_00004_.png
Dimensions: 1280x960
Prompt:
A low-angle cinematic shot in a dense bamboo forest at golden hour, sunbeams filtering diagonally through the tall green bamboo canopy to cast sharp, dramatic shadows on the moss-covered ground. A young East Asian swordswoman in her mid-twenties, with long flowing black hair escaping her topknot, wears a traditional red silk martial arts robe edged with gold thread embroidery; her expression is intensely focused, muscles tensed in her arms and legs. She is captured mid-action during a horizontal sword slash: body twisted dynamically forward, left foot planted firmly on emerald moss, right arm fully extended holding a polished steel jian sword with a black lacquered hilt, the blade angled sharply downward. Bamboo stalks surround her, their smooth green surfaces textured with vertical grooves and dew drops catching the light; fallen bamboo leaves scatter the forest floor, partially covered in soft velvety moss. Mist drifts faintly near the ground, with background bamboo softly blurred to create depth. Color palette emphasizes vibrant jade greens of the bamboo, warm amber sunlight, and the rich crimson of the robe, accented by the metallic gleam of the sword. Highly detailed textures include the woven silk fabric rippling with movement, the sword's reflective edge, and the rough bark of nearby bamboo trunks.
================================================================================
Image: ComfyUI-zit_00042_.png
Dimensions: 0x0
Prompt:
镜头从高处拍摄,在一片宁静花园的斑驳光线下,一位纤细的女子优雅地坐在一张磨损的石凳上,藤蔓和花朵环绕四周。她的身形纤细,但胸部巨大且圆润,胸部远远大于角色的头部,超巨乳,自然地向下垂。她微微前倾,双手叠放在膝上,嘴角带着淡淡的微笑,头微微倾向阳光。光线柔和地温暖地照在她裸露的肌肤上,勾勒出她身体的每一处曲线和轻柔的重力拉伸。镜头从女子侧面拍摄,
================================================================================
Image: ComfyUI-zit_00032_.png
Dimensions: 0x0
Prompt:
a lomo photograph of a striking portrait of a naked woman with a detailed dragon tattoo on her back standing in front of a window, with her hand on the window sill, facing away from the camera but looking back, enveloped in the shadows of a dark room with an ethereal red glow cast from a neon light outside at night. her long wavy dark purple hair is tied back in a pony tail. her back is arched accentuating her equisite hour glass figure. the neon-lit sign and night time cityscape outside the window casts a red hue over the inside of the dimly lit apartment illuminating her back to reveal the dragon tattoo. the photograph has large dark vignetting and was shot on 35mm film with visible film grain and color splashing throughout the frame. while the woman is sharply in focus, the edges of the composition are soft, with a shallow depth of field, excellent bokeh. the film frame border can be seen in the image. the photograph was shot with a canon f1 using 800 iso film. dramatic cinematic lighting. the neon sign has chinese characters. light leaks and film borders visible. 4ft3rd4rk
================================================================================
Image: ComfyUI_00049_.png
Dimensions: 768x1280
Prompt:
tranin passing, anime style, 4k ultra resolution, flat shading.
================================================================================
Image: ComfyUI-zit_00077_.png
Dimensions: 0x0
Prompt:
masterpiece, best quality, photo realistic, 8k, a woman wearing a sculptural translucent mask carved from pure lightbeams, refracting prismatic colors across her face, futuristic beauty campaign energy , hyperdetailed, dramatic lighting, cinematic shot, ultra detailed, intricate details, cinematic, photorealistic, masterpiece
================================================================================
Image: ComfyUI_00046_.png
Dimensions: 0x0
Prompt:
Gothic Glamour. "Back to the Future" Delorean car rushes through the mysterious night forest through the fog glowing in the moonlight.High detail, 10-bit color rendering, large-scale image.Half-turned to the viewer,action pose.Cinematic realism, high contrast, surround light, exceptional detail, 8k,. on the plate the text "F.M.E.D.S". a partly visible indication on a wooden signe reads "Eros Diffusion" with an arrow pointing backwards to wards the car. the driver is just a shadow inside the car and not well lit.
================================================================================
Image: ComfyUI-zit_00056_.png
Dimensions: 0x0
Prompt:
masterpiece, best quality, photo realistic, 8k, a serene Japanese onsen scene with an otaku-styled woman relaxing in steaming mineral water, soft lantern light reflecting off wooden bath walls, subtle anime-inspired accessories, gentle mist rising around her, cherry blossoms drifting in the air, tranquil mountain backdrop, elegant editorial composition , hyperdetailed, dramatic lighting, cinematic shot, ultra detailed, intricate details, cinematic, photorealistic, masterpiece
================================================================================
Image: ComfyUI-zit_00026_.png
Dimensions: 0x0
Prompt:
You are an assistant... <Prompt Start> Hyper-realistic cinematic shot of a nude bio-mechanical Asian woman m3tsumi1, her silicon skin is completely revealed. She sits leaning against a crumbling stucco wall, its texture rough and weathered. The wall is overtaken by nature, with creeping vines, vibrant wildflowers, and dense foliage bursting through cracks. The scene is set centuries after a devastating post-apocalyptic battle. The woman's exposed silicon parts show signs of wounds, and battle damage, with subtle LED lights flickering weakly in her circuitry. Dirt and grime coat her form, emphasizing the passage of time. Shafts of golden sunlight filter through the overgrown canopy above, casting dappled shadows across the scene. The atmosphere is one of eerie beauty and abandoned technology reclaimed by nature. Ultra-detailed textures, dramatic lighting, and a muted color palette dominated by earth tones and metallic hues. 8K resolution, photorealistic rendering, cinematic composition.
================================================================================
Image: \
Dimensions: 768x1280
Prompt:
================================================================================
Image: ComfyUI_00052_.png
Dimensions: 1024x1024
Prompt:
tranin passing, anime style, 4k ultra resolution, flat shading.
+18
View File
@@ -0,0 +1,18 @@
try:
from diffusers import VQDiffusionScheduler
import inspect
print("VQDiffusionScheduler found!")
print("Init signature:")
print(inspect.signature(VQDiffusionScheduler.__init__))
# Also check config defaults if possible
scheduler = VQDiffusionScheduler()
print("\nDefault config:")
print(scheduler.config)
except ImportError:
print("VQDiffusionScheduler not found in diffusers.")
except Exception as e:
print(f"Error: {e}")
+21
View File
@@ -0,0 +1,21 @@
try:
from diffusers import VQDiffusionScheduler
import torch
scheduler = VQDiffusionScheduler(num_vec_classes=4096, num_train_timesteps=100)
print("Scheduler created successfully.")
print(f"Has sigmas attribute? {hasattr(scheduler, 'sigmas')}")
scheduler.set_timesteps(10)
print("Timesteps set to 10.")
print(f"Timesteps: {scheduler.timesteps}")
if hasattr(scheduler, 'sigmas'):
print(f"Sigmas: {scheduler.sigmas}")
else:
print("No 'sigmas' attribute found. This scheduler might not be compatible with ComfyUI's standard sampler loop which expects sigmas.")
except Exception as e:
print(f"Error: {e}")
BIN
View File
Binary file not shown.
+58
View File
@@ -0,0 +1,58 @@
import os
import random
import sys
# Add custom node directory to path so we can import the class
sys.path.append(os.path.dirname(__file__))
# Mock folder_paths for the node to work
import folder_paths
folder_paths.get_annotated_filepath = lambda x: x # Just return the path as is
from extract_metadata_node import ImageMetadataExtractor
def main():
output_dir = r"D:\ComfyUI7\ComfyUI\output"
output_file = "batch_test_results.txt"
# Get all image files
all_files = [os.path.join(output_dir, f) for f in os.listdir(output_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
if not all_files:
print(f"No images found in {output_dir}")
return
# Select 20 random images
num_samples = min(20, len(all_files))
selected_files = random.sample(all_files, num_samples)
extractor = ImageMetadataExtractor()
print(f"Testing on {num_samples} images...")
with open(output_file, "w", encoding="utf-8") as f:
for i, file_path in enumerate(selected_files):
try:
# We need to bypass the folder_paths.get_annotated_filepath call inside the node
# by mocking it, or just passing the absolute path if our mock above works.
# The node calls folder_paths.get_annotated_filepath(image)
# Our mock returns x, so we pass the full path.
prompt, width, height = extractor.extract_metadata(file_path)
separator = "=" * 80
entry = f"{separator}\nImage: {os.path.basename(file_path)}\nDimensions: {width}x{height}\nPrompt:\n{prompt}\n"
f.write(entry + "\n")
print(f"Processed {i+1}/{num_samples}: {os.path.basename(file_path)}")
except Exception as e:
error_msg = f"Error processing {os.path.basename(file_path)}: {e}\n"
f.write(error_msg)
print(error_msg)
print(f"Done. Results saved to {output_file}")
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
import sys
import os
import importlib.util
# Add the parent directory to sys.path so we can import EulerDiscrete as a module
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.append(parent_dir)
try:
# Import EulerDiscrete as a module
import EulerDiscrete
print("Successfully imported EulerDiscrete package.")
mappings = EulerDiscrete.NODE_CLASS_MAPPINGS
print("\nChecking NODE_CLASS_MAPPINGS:")
has_flow_match = "FlowMatchEulerDiscreteScheduler (Custom)" in mappings
has_vq = "VQDiffusionScheduler" in mappings
if has_flow_match:
print("✅ FlowMatchEulerDiscreteScheduler (Custom) found.")
else:
print("❌ FlowMatchEulerDiscreteScheduler (Custom) NOT found!")
if has_vq:
print("✅ VQDiffusionScheduler found.")
else:
print("❌ VQDiffusionScheduler NOT found!")
if has_flow_match and has_vq:
print("\nSUCCESS: Both schedulers are present.")
else:
print("\nFAILURE: Missing schedulers.")
sys.exit(1)
except Exception as e:
print(f"\nERROR: Import failed: {e}")
# Print traceback for more details
import traceback
traceback.print_exc()
sys.exit(1)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB