Files
Jesse Rabek 21a5d24f8c Integrate ComfyUI sampler and scheduler constants into BokehKSampler
- Added logic to import and utilize sampler and scheduler names from ComfyUI's KSampler class.
- Implemented fallback mechanisms to ensure default sampler and scheduler names are available if ComfyUI is not present.
- Enhanced logging to provide feedback on the number of samplers and schedulers loaded, or warnings if the import fails.
2025-11-27 19:58:36 +01:00

940 lines
47 KiB
Python

import torch
import numpy as np
from typing import Optional, Tuple
import sys
import os
import logging
# Import bokeh adapter
from .bokeh_diffusion.adapter_flux import BokehFluxControlAdapter
from .bokeh_diffusion.utils import color_transfer_lab
MIN_BOKEH, MAX_BOKEH = 0, 30
# Configure logging - can be controlled via BOKEH_DEBUG environment variable
# Set BOKEH_DEBUG=1 for DEBUG level, BOKEH_DEBUG=2 for even more verbose
#_log_level = os.environ.get('BOKEH_DEBUG', '1')
_log_level = '1'
if _log_level == '2':
logging.basicConfig(level=logging.DEBUG, format='[BokehDiffusion] %(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
elif _log_level == '1':
logging.basicConfig(level=logging.INFO, format='[BokehDiffusion] %(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
else:
logging.basicConfig(level=logging.WARNING, format='[BokehDiffusion] %(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
def find_flux_transformer(model):
"""Find the FLUX transformer in a ComfyUI model"""
logger.info("=" * 80)
logger.info("Starting FLUX transformer search")
logger.info(f"Model type: {type(model)}")
logger.info(f"Model ID: {id(model)}")
# Helper function to check if an object is a FLUX transformer
def is_flux_transformer(obj, obj_name="unknown"):
if obj is None:
logger.debug(f" [is_flux_transformer] {obj_name}: Object is None")
return False
obj_type = str(type(obj))
logger.debug(f" [is_flux_transformer] {obj_name}: Type = {obj_type}")
# Check for FluxTransformer2DModel class name
if 'FluxTransformer2DModel' in obj_type:
logger.info(f" [is_flux_transformer] {obj_name}: ✓ MATCH - FluxTransformer2DModel detected!")
return True
# Check for attn_processors attribute (key indicator of FLUX transformer)
has_attn_procs = hasattr(obj, 'attn_processors')
has_set_attn = hasattr(obj, 'set_attn_processor')
logger.debug(f" [is_flux_transformer] {obj_name}: has attn_processors={has_attn_procs}, has set_attn_processor={has_set_attn}")
if has_attn_procs:
# Additional check: FLUX transformers have set_attn_processor method
if has_set_attn:
logger.info(f" [is_flux_transformer] {obj_name}: ✓ MATCH - Has attn_processors and set_attn_processor!")
return True
else:
logger.debug(f" [is_flux_transformer] {obj_name}: Has attn_processors but missing set_attn_processor")
logger.debug(f" [is_flux_transformer] {obj_name}: ✗ Not a transformer")
return False
# Try different possible locations for the transformer
# Most common: model.model.diffusion_model (ComfyUI ModelPatcher structure)
logger.info("Checking model.model structure...")
if hasattr(model, 'model'):
logger.info(f" model.model exists: {type(model.model)}")
logger.debug(f" model.model attributes: {[a for a in dir(model.model) if not a.startswith('__')][:20]}")
if hasattr(model.model, 'diffusion_model'):
logger.info(" Found model.model.diffusion_model")
flux_model = model.model.diffusion_model
flux_type = str(type(flux_model))
logger.info(f" flux_model type: {flux_type}")
# Check if it's a ComfyUI Flux wrapper (comfy.ldm.flux.model.Flux)
# In this case, the actual transformer is usually in flux_model.transformer
if 'comfy.ldm.flux' in flux_type or 'comfy.model_base.Flux' in flux_type:
logger.info(" ✓ Detected ComfyUI Flux wrapper - searching for transformer inside...")
# The actual FluxTransformer2DModel is typically in the 'transformer' attribute
logger.info(" Checking flux_model.transformer...")
if hasattr(flux_model, 'transformer'):
logger.info(" flux_model.transformer exists!")
transformer = flux_model.transformer
logger.info(f" transformer type: {type(transformer)}")
if is_flux_transformer(transformer, "flux_model.transformer"):
logger.info(" ✓ FOUND TRANSFORMER at flux_model.transformer!")
return transformer
else:
logger.warning(" flux_model.transformer exists but is_flux_transformer returned False")
else:
logger.warning(" flux_model.transformer attribute NOT FOUND")
# Check all direct attributes (including private ones)
logger.info(" Checking direct attributes: model, _model, transformer, _transformer...")
for attr_name in ['model', '_model', 'transformer', '_transformer']:
logger.debug(f" Checking attribute: {attr_name}")
if hasattr(flux_model, attr_name):
logger.info(f" {attr_name} exists!")
try:
inner_obj = getattr(flux_model, attr_name)
logger.debug(f" {attr_name} type: {type(inner_obj)}")
if inner_obj is not None:
if is_flux_transformer(inner_obj, f"flux_model.{attr_name}"):
logger.info(f" ✓ FOUND TRANSFORMER at flux_model.{attr_name}!")
return inner_obj
# Recursively check if it contains the transformer
if hasattr(inner_obj, 'attn_processors') or 'FluxTransformer' in str(type(inner_obj)):
logger.debug(f" {attr_name} has attn_processors or FluxTransformer in type")
# Check nested
if hasattr(inner_obj, 'transformer'):
logger.info(f" {attr_name}.transformer exists!")
nested = getattr(inner_obj, 'transformer')
if is_flux_transformer(nested, f"flux_model.{attr_name}.transformer"):
logger.info(f" ✓ FOUND TRANSFORMER at flux_model.{attr_name}.transformer!")
return nested
except (AttributeError, RuntimeError, TypeError) as e:
logger.debug(f" Error accessing {attr_name}: {e}")
continue
else:
logger.debug(f" {attr_name} does not exist")
# Check visible attributes first (double_blocks, etc.)
logger.info(" Checking visible attributes (double_blocks, etc.)...")
visible_attrs_to_check = ['double_blocks', 'single_blocks', 'transformer', 'model', '_model', '_transformer', 'diffusers_model', '_diffusers_model']
for attr_name in visible_attrs_to_check:
if hasattr(flux_model, attr_name):
logger.info(f" Checking {attr_name}...")
try:
attr_obj = getattr(flux_model, attr_name)
logger.info(f" {attr_name} type: {type(attr_obj)}")
if attr_obj is not None:
# If it's a ModuleList or similar, check items
if isinstance(attr_obj, (torch.nn.ModuleList, torch.nn.ModuleDict, list, tuple)):
logger.info(f" {attr_name} is a container with {len(attr_obj)} items")
# For double_blocks, check if any block contains a transformer
# Limit to first few items to avoid too much logging
items_to_check = min(len(attr_obj), 5) if attr_name == 'double_blocks' else len(attr_obj)
logger.info(f" Checking first {items_to_check} items in {attr_name}...")
for i in range(items_to_check):
item = attr_obj[i]
logger.debug(f" Checking {attr_name}[{i}]: {type(item)}")
# Check item itself
if is_flux_transformer(item, f"{attr_name}[{i}]"):
logger.info(f" ✓ FOUND TRANSFORMER in {attr_name}[{i}]!")
return item
# Check if the item has a transformer attribute
if hasattr(item, 'transformer'):
nested = getattr(item, 'transformer')
logger.debug(f" {attr_name}[{i}].transformer exists: {type(nested)}")
if is_flux_transformer(nested, f"{attr_name}[{i}].transformer"):
logger.info(f" ✓ FOUND TRANSFORMER in {attr_name}[{i}].transformer!")
return nested
# Check item's attributes for transformer-like objects
if hasattr(item, '__dict__'):
for sub_key, sub_value in item.__dict__.items():
if sub_value is not None and not isinstance(sub_value, (str, int, float, bool, list, dict, tuple)):
if 'transformer' in sub_key.lower() or 'flux' in str(type(sub_value)).lower():
logger.debug(f" {attr_name}[{i}].{sub_key}: {type(sub_value)}")
if is_flux_transformer(sub_value, f"{attr_name}[{i}].{sub_key}"):
logger.info(f" ✓ FOUND TRANSFORMER in {attr_name}[{i}].{sub_key}!")
return sub_value
else:
if is_flux_transformer(attr_obj, f"flux_model.{attr_name}"):
logger.info(f" ✓ FOUND TRANSFORMER in {attr_name}!")
return attr_obj
except Exception as e:
logger.debug(f" Error checking {attr_name}: {e}")
# Check if ComfyUI Flux model has the diffusers model stored somewhere
# Sometimes it's in __dict__ or accessible via specific methods
logger.info(" Checking for diffusers model in __dict__ or state_dict keys...")
try:
if hasattr(flux_model, '__dict__'):
for key, value in flux_model.__dict__.items():
if value is not None and not isinstance(value, (str, int, float, bool, list, dict, tuple)):
value_type = str(type(value))
if 'diffusers' in value_type.lower() or 'FluxTransformer' in value_type:
logger.info(f" Found potential diffusers model in __dict__[{key}]: {value_type}")
if is_flux_transformer(value, f"__dict__[{key}]"):
logger.info(f" ✓ FOUND TRANSFORMER in __dict__[{key}]!")
return value
except Exception as e:
logger.debug(f" Error checking __dict__: {e}")
# Check state_dict keys for hints about transformer structure
try:
if hasattr(flux_model, 'state_dict'):
state_keys = list(flux_model.state_dict().keys())[:30] # First 30 keys
logger.info(f" Sample state_dict keys (first 30): {state_keys}")
# Look for transformer-related keys
transformer_keys = [k for k in state_keys if 'transformer' in k.lower() or 'attn' in k.lower()]
if transformer_keys:
logger.info(f" Found transformer-related keys: {transformer_keys[:10]}")
except Exception as e:
logger.debug(f" Error checking state_dict: {e}")
# Search through all named modules/children to find the transformer
# ComfyUI might store it as a submodule
logger.info(" Searching through named_modules...")
transformer_candidates = []
attn_processor_candidates = []
interesting_modules = [] # Track interesting module types
try:
module_count = 0
for name, module in flux_model.named_modules():
module_count += 1
module_type = str(type(module))
module_class_name = type(module).__name__
# Track interesting module types for debugging
if module_count <= 50 or 'transformer' in name.lower() or 'flux' in module_class_name.lower():
interesting_modules.append((name, module_class_name, module_type))
# Check if it's a FluxTransformer2DModel
if 'FluxTransformer2DModel' in module_type:
logger.debug(f" Found FluxTransformer2DModel candidate: {name}")
transformer_candidates.append((name, module_type))
if is_flux_transformer(module, f"named_modules[{name}]"):
logger.info(f" ✓ FOUND TRANSFORMER in named_modules: {name} ({type(module)})!")
return module
# Check if it has attn_processors
elif hasattr(module, 'attn_processors') and hasattr(module, 'set_attn_processor'):
logger.debug(f" Found attn_processor candidate: {name}")
attn_processor_candidates.append((name, module_type))
if is_flux_transformer(module, f"named_modules[{name}]"):
logger.info(f" ✓ FOUND TRANSFORMER-LIKE in named_modules: {name} ({type(module)})!")
return module
logger.info(f" Searched {module_count} modules total")
logger.info(f" Found {len(transformer_candidates)} FluxTransformer2DModel candidates")
logger.info(f" Found {len(attn_processor_candidates)} attn_processor candidates")
# Log interesting modules for debugging
if interesting_modules:
logger.info(" Sample of interesting modules (first 50 or transformer/flux related):")
for name, class_name, mod_type in interesting_modules[:30]:
logger.info(f" {name}: {class_name}")
# Print candidate modules for debugging
if transformer_candidates:
logger.info(" FluxTransformer2DModel candidates:")
for name, mod_type in transformer_candidates[:20]: # Show first 20
logger.info(f" {name}: {mod_type}")
if attn_processor_candidates:
logger.info(" attn_processor candidates:")
for name, mod_type in attn_processor_candidates[:20]: # Show first 20
logger.info(f" {name}: {mod_type}")
except (AttributeError, RuntimeError, TypeError) as e:
logger.error(f" Error searching named_modules: {e}", exc_info=True)
pass
# Also check direct children
logger.info(" Checking direct children...")
try:
child_count = 0
for child in flux_model.children():
child_count += 1
logger.debug(f" Child {child_count}: {type(child)}")
if is_flux_transformer(child, f"children[{child_count}]"):
logger.info(f" ✓ FOUND TRANSFORMER in children: {type(child)}!")
return child
logger.info(f" Checked {child_count} direct children")
except (AttributeError, RuntimeError, TypeError) as e:
logger.error(f" Error searching children: {e}", exc_info=True)
pass
# Try searching through all attributes more thoroughly
# Check if any attribute contains FluxTransformer2DModel
logger.info(" Searching through all private attributes...")
try:
all_attrs = [a for a in dir(flux_model) if a.startswith('_') and not a.startswith('__')]
logger.debug(f" Found {len(all_attrs)} private attributes to check")
checked_count = 0
for attr_name in all_attrs:
checked_count += 1
if checked_count % 10 == 0:
logger.debug(f" Checked {checked_count}/{len(all_attrs)} attributes...")
try:
attr_obj = getattr(flux_model, attr_name, None)
if attr_obj is not None:
attr_type = str(type(attr_obj))
# Check if it's a FluxTransformer2DModel
if 'FluxTransformer2DModel' in attr_type:
logger.info(f" Found FluxTransformer2DModel in attribute: {attr_name} ({attr_type})")
if is_flux_transformer(attr_obj, f"attr[{attr_name}]"):
logger.info(f" ✓ FOUND TRANSFORMER in attribute: {attr_name}!")
return attr_obj
# Check if it has attn_processors
if hasattr(attr_obj, 'attn_processors') and hasattr(attr_obj, 'set_attn_processor'):
logger.info(f" Found transformer-like object in attribute: {attr_name} ({attr_type})")
if is_flux_transformer(attr_obj, f"attr[{attr_name}]"):
logger.info(f" ✓ FOUND TRANSFORMER in attribute: {attr_name}!")
return attr_obj
except (AttributeError, RuntimeError, TypeError) as e:
logger.debug(f" Error accessing {attr_name}: {e}")
continue
logger.info(f" Checked {checked_count} private attributes")
except Exception as e:
logger.error(f" Error searching attributes: {e}", exc_info=True)
pass
else:
# Not a ComfyUI wrapper, check if it's the transformer directly
logger.info(" Not a ComfyUI wrapper, checking if flux_model is transformer directly...")
if is_flux_transformer(flux_model, "flux_model (direct)"):
logger.info(" ✓ FOUND TRANSFORMER - flux_model is transformer directly!")
return flux_model
else:
logger.warning(" model.model.diffusion_model does not exist")
logger.info(" Checking model.model.transformer...")
if hasattr(model.model, 'transformer'):
logger.info(" model.model.transformer exists!")
transformer = model.model.transformer
if is_flux_transformer(transformer, "model.model.transformer"):
logger.info(" ✓ FOUND TRANSFORMER at model.model.transformer!")
return transformer
else:
logger.debug(" model.model.transformer does not exist")
# Sometimes the model.model itself might be the transformer
logger.info(" Checking if model.model is transformer directly...")
if is_flux_transformer(model.model, "model.model (direct)"):
logger.info(" ✓ FOUND TRANSFORMER - model.model is transformer directly!")
return model.model
else:
logger.warning(" model.model does not exist")
# Check for private _model attribute (some wrappers use this)
logger.info("Checking model._model...")
if hasattr(model, '_model'):
logger.info(" model._model exists!")
try:
inner_model = model._model
if inner_model is not None:
logger.info(f" _model type: {type(inner_model)}")
if hasattr(inner_model, 'diffusion_model'):
logger.info(" _model.diffusion_model exists!")
transformer = inner_model.diffusion_model
if is_flux_transformer(transformer, "model._model.diffusion_model"):
logger.info(" ✓ FOUND TRANSFORMER at model._model.diffusion_model!")
return transformer
if hasattr(inner_model, 'transformer'):
logger.info(" _model.transformer exists!")
transformer = inner_model.transformer
if is_flux_transformer(transformer, "model._model.transformer"):
logger.info(" ✓ FOUND TRANSFORMER at model._model.transformer!")
return transformer
if is_flux_transformer(inner_model, "model._model (direct)"):
logger.info(" ✓ FOUND TRANSFORMER - model._model is transformer directly!")
return inner_model
except (AttributeError, RuntimeError, TypeError) as e:
logger.debug(f" Error accessing _model: {e}")
pass
else:
logger.debug(" model._model does not exist")
# Check if model has a get_model() method (some ComfyUI wrappers)
logger.info("Checking model.get_model()...")
if hasattr(model, 'get_model'):
logger.info(" model.get_model() exists!")
try:
inner_model = model.get_model()
logger.info(f" get_model() returned: {type(inner_model)}")
if inner_model is not None:
if hasattr(inner_model, 'diffusion_model'):
logger.info(" inner_model.diffusion_model exists!")
transformer = inner_model.diffusion_model
if is_flux_transformer(transformer, "get_model().diffusion_model"):
logger.info(" ✓ FOUND TRANSFORMER at get_model().diffusion_model!")
return transformer
if hasattr(inner_model, 'transformer'):
logger.info(" inner_model.transformer exists!")
transformer = inner_model.transformer
if is_flux_transformer(transformer, "get_model().transformer"):
logger.info(" ✓ FOUND TRANSFORMER at get_model().transformer!")
return transformer
if is_flux_transformer(inner_model, "get_model() (direct)"):
logger.info(" ✓ FOUND TRANSFORMER - get_model() result is transformer directly!")
return inner_model
except (AttributeError, RuntimeError, TypeError) as e:
logger.debug(f" Error calling get_model(): {e}")
pass
else:
logger.debug(" model.get_model() does not exist")
# Direct access: model.diffusion_model
logger.info("Checking model.diffusion_model (direct)...")
if hasattr(model, 'diffusion_model'):
logger.info(" model.diffusion_model exists!")
transformer = model.diffusion_model
if is_flux_transformer(transformer, "model.diffusion_model (direct)"):
logger.info(" ✓ FOUND TRANSFORMER at model.diffusion_model!")
return transformer
else:
logger.debug(" model.diffusion_model does not exist")
# Direct access: model.transformer
logger.info("Checking model.transformer (direct)...")
if hasattr(model, 'transformer'):
logger.info(" model.transformer exists!")
transformer = model.transformer
if is_flux_transformer(transformer, "model.transformer (direct)"):
logger.info(" ✓ FOUND TRANSFORMER at model.transformer!")
return transformer
else:
logger.debug(" model.transformer does not exist")
# Last resort: check if model itself is the transformer
logger.info("Checking if model itself is transformer...")
if is_flux_transformer(model, "model (self)"):
logger.info(" ✓ FOUND TRANSFORMER - model itself is transformer!")
return model
# Recursive search as final fallback
logger.info("Starting recursive search as final fallback...")
def search_recursive(obj, depth=0, max_depth=3, visited=None, path=""):
if visited is None:
visited = set()
if depth > max_depth or id(obj) in visited:
return None
visited.add(id(obj))
logger.debug(f" [recursive] depth={depth}, path={path}, type={type(obj)}")
# Check if current object is the transformer
if is_flux_transformer(obj, f"recursive[{path}]"):
logger.info(f" ✓ FOUND TRANSFORMER in recursive search at: {path}!")
return obj
# Search in common attribute names
for attr_name in ['diffusion_model', 'transformer', 'model', '_model']:
if hasattr(obj, attr_name):
try:
attr_obj = getattr(obj, attr_name)
if attr_obj is not None and id(attr_obj) not in visited:
new_path = f"{path}.{attr_name}" if path else attr_name
result = search_recursive(attr_obj, depth + 1, max_depth, visited, new_path)
if result is not None:
return result
except (AttributeError, RuntimeError, TypeError):
continue
# Check for get_model() method
if hasattr(obj, 'get_model'):
try:
inner_model = obj.get_model()
if inner_model is not None and id(inner_model) not in visited:
new_path = f"{path}.get_model()" if path else "get_model()"
result = search_recursive(inner_model, depth + 1, max_depth, visited, new_path)
if result is not None:
return result
except (AttributeError, RuntimeError, TypeError):
pass
return None
transformer = search_recursive(model, path="model")
if transformer is not None:
logger.info(" ✓ FOUND TRANSFORMER via recursive search!")
return transformer
# Final debug summary
logger.error("=" * 80)
logger.error("FAILED TO FIND FLUX TRANSFORMER")
logger.error("=" * 80)
logger.error("Model structure summary:")
logger.error(f" Model type: {type(model)}")
logger.error(f" Model attributes: {[a for a in dir(model) if not a.startswith('__')][:30]}")
if hasattr(model, 'model'):
logger.error(f" model.model type: {type(model.model)}")
logger.error(f" model.model attributes: {[a for a in dir(model.model) if not a.startswith('__')][:30]}")
if hasattr(model.model, 'diffusion_model'):
flux_model = model.model.diffusion_model
flux_type = str(type(flux_model))
logger.error(f" model.model.diffusion_model type: {flux_type}")
logger.error(f" Has attn_processors: {hasattr(flux_model, 'attn_processors')}")
logger.error(f" Has set_attn_processor: {hasattr(flux_model, 'set_attn_processor')}")
# Check if it's a ComfyUI Flux wrapper
if 'comfy.ldm.flux' in flux_type or 'comfy.model_base.Flux' in flux_type:
logger.error(" Detected ComfyUI Flux wrapper")
# Check for transformer attribute
if hasattr(flux_model, 'transformer'):
transformer = flux_model.transformer
logger.error(f" flux_model.transformer exists: {type(transformer)}")
logger.error(f" Has attn_processors: {hasattr(transformer, 'attn_processors')}")
logger.error(f" Has set_attn_processor: {hasattr(transformer, 'set_attn_processor')}")
else:
logger.error(" flux_model.transformer attribute NOT FOUND")
# List all attributes that might be relevant
all_attrs = [attr for attr in dir(flux_model) if not attr.startswith('__')]
logger.error(f" All non-private attributes ({len(all_attrs)} total):")
for attr in all_attrs[:50]: # Show first 50
try:
attr_obj = getattr(flux_model, attr, None)
if attr_obj is not None and not isinstance(attr_obj, (str, int, float, bool, list, dict, tuple)):
logger.error(f" {attr}: {type(attr_obj)}")
except:
pass
logger.error("=" * 80)
return None
class BokehAdapterLoader:
"""Load the Bokeh Diffusion adapter from HuggingFace"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"adapter_name": ("STRING", {
"default": "atfortes/BokehDiffusion",
"multiline": False
}),
},
"optional": {
"bokeh_scale": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.1
}),
"lora_scale": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.1
}),
}
}
RETURN_TYPES = ("BOKEH_ADAPTER",)
RETURN_NAMES = ("adapter",)
FUNCTION = "load_adapter"
CATEGORY = "bokeh_diffusion"
def load_adapter(self, adapter_name: str, bokeh_scale: float = 1.0, lora_scale: float = 1.0):
"""Load the bokeh adapter configuration"""
adapter_info = {
"adapter_name": adapter_name,
"bokeh_scale": bokeh_scale,
"lora_scale": lora_scale,
"adapter": None # Will be loaded when model is available
}
return (adapter_info,)
class ApplyBokehAdapter:
"""Apply the Bokeh adapter to a FLUX model"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"adapter": ("BOKEH_ADAPTER",),
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "apply_adapter"
CATEGORY = "bokeh_diffusion"
def apply_adapter(self, model, adapter):
"""Apply the bokeh adapter to the model's transformer"""
logger.info("=" * 80)
logger.info("ApplyBokehAdapter.apply_adapter() called")
logger.info(f"Adapter config: {adapter}")
try:
# Find the transformer
logger.info("Step 1: Finding FLUX transformer...")
transformer = find_flux_transformer(model)
if transformer is None:
logger.error("Transformer is None - search failed!")
raise Exception("Could not find FLUX transformer in model. Make sure you're using a FLUX model.")
logger.info(f"✓ Transformer found: {type(transformer)}")
logger.info(f" Transformer ID: {id(transformer)}")
# Check if transformer has attn_processors (required for adapter)
logger.info("Step 2: Verifying transformer has attn_processors...")
if not hasattr(transformer, 'attn_processors'):
logger.error("Transformer does not have attn_processors attribute!")
raise Exception("Model transformer does not have attn_processors. This may not be a FLUX model.")
logger.info("✓ Transformer has attn_processors")
logger.debug(f" attn_processors type: {type(transformer.attn_processors)}")
if hasattr(transformer.attn_processors, '__len__'):
logger.debug(f" Number of processors: {len(transformer.attn_processors)}")
# Check for set_attn_processor method
if not hasattr(transformer, 'set_attn_processor'):
logger.error("Transformer does not have set_attn_processor method!")
raise Exception("Model transformer does not have set_attn_processor method. This may not be a FLUX model.")
logger.info("✓ Transformer has set_attn_processor method")
# Load the adapter if not already loaded
adapter_name = adapter["adapter_name"]
logger.info(f"Step 3: Loading adapter from {adapter_name}...")
if adapter["adapter"] is None:
logger.info(f" Adapter not yet loaded, loading now...")
try:
adapter_obj = BokehFluxControlAdapter.from_pretrained(
adapter_name,
base_model=transformer,
)
logger.info(" ✓ Adapter loaded from HuggingFace")
# Move to same device as transformer
logger.info("Step 4: Moving adapter to same device/dtype as transformer...")
try:
device = next(transformer.parameters()).device
dtype = next(transformer.parameters()).dtype
logger.info(f" Transformer device: {device}, dtype: {dtype}")
adapter_obj = adapter_obj.to(device=device, dtype=dtype)
logger.info(" ✓ Adapter moved to transformer device/dtype")
except Exception as e:
logger.warning(f" Error moving adapter to device: {e}")
logger.info(" Continuing anyway...")
adapter["adapter"] = adapter_obj
logger.info("✓ Bokeh adapter loaded and configured successfully")
except Exception as e:
logger.error(f" Failed to load adapter: {e}", exc_info=True)
raise
else:
logger.info(" Adapter already loaded, reusing existing instance")
adapter_obj = adapter["adapter"]
logger.info(f" Adapter object: {type(adapter_obj)}")
# Set scales
logger.info("Step 5: Setting adapter scales...")
bokeh_scale = adapter.get("bokeh_scale", 1.0)
lora_scale = adapter.get("lora_scale", 1.0)
logger.info(f" bokeh_scale: {bokeh_scale}, lora_scale: {lora_scale}")
if bokeh_scale != 1.0:
logger.info(f" Setting bokeh_scale to {bokeh_scale}")
adapter_obj.set_bokeh_scale(bokeh_scale)
if lora_scale != 1.0:
logger.info(f" Setting lora_scale to {lora_scale}")
adapter_obj.set_lora_scale(lora_scale)
# Store adapter in model for use during sampling
logger.info("Step 6: Storing adapter in model...")
model.bokeh_adapter = adapter_obj
model.bokeh_adapter_config = adapter
model._bokeh_transformer = transformer
logger.info("✓ Adapter stored in model")
logger.info("=" * 80)
logger.info("✓ Bokeh adapter applied successfully!")
logger.info("=" * 80)
return (model,)
except Exception as e:
logger.error("=" * 80)
logger.error("FAILED TO APPLY BOKEH ADAPTER")
logger.error("=" * 80)
logger.error(f"Error: {e}", exc_info=True)
logger.error("=" * 80)
raise Exception(f"Failed to apply bokeh adapter: {e}")
class BokehLevel:
"""Set bokeh level for generation"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"bokeh_level": ("FLOAT", {
"default": 15.0,
"min": MIN_BOKEH,
"max": MAX_BOKEH,
"step": 0.1,
"tooltip": "Bokeh level from 0 (sharp) to 30 (maximum blur)"
}),
},
"optional": {
"bokeh_pivot": ("FLOAT", {
"default": 15.0,
"min": MIN_BOKEH,
"max": MAX_BOKEH,
"step": 0.1,
"tooltip": "Pivot bokeh level for grounded generation (scene consistency)"
}),
"num_grounding_steps": ("INT", {
"default": 0,
"min": 0,
"max": 50,
"tooltip": "Number of grounding steps for scene consistency (0 = disabled, 18-24 recommended)"
}),
}
}
RETURN_TYPES = ("BOKEH_CONTROL",)
RETURN_NAMES = ("bokeh_control",)
FUNCTION = "create_control"
CATEGORY = "bokeh_diffusion"
def create_control(self, bokeh_level: float, bokeh_pivot: Optional[float] = None, num_grounding_steps: int = 0):
"""Create bokeh control parameters"""
# Normalize bokeh level (0-30 -> 0-1)
bokeh_target = bokeh_level / MAX_BOKEH
bokeh_pivot_norm = bokeh_pivot / MAX_BOKEH if bokeh_pivot is not None else None
control = {
"bokeh_target": bokeh_target,
"bokeh_pivot": bokeh_pivot_norm,
"num_grounding_steps": num_grounding_steps,
"is_grounded": num_grounding_steps > 0 and bokeh_pivot is not None
}
return (control,)
class BokehKSampler:
"""KSampler with bokeh control support for FLUX models"""
@classmethod
def INPUT_TYPES(cls):
# Try to import ComfyUI's sampler constants
try:
import comfy.samplers
# Get sampler and scheduler names from ComfyUI
# First try KSampler class attributes (most reliable)
sampler_names = ["euler"] # Default fallback
scheduler_names = ["normal"] # Default fallback
if hasattr(comfy.samplers, 'KSampler'):
ksampler_class = comfy.samplers.KSampler
if hasattr(ksampler_class, 'SAMPLERS') and isinstance(ksampler_class.SAMPLERS, (list, tuple)):
sampler_names = list(ksampler_class.SAMPLERS)
if hasattr(ksampler_class, 'SCHEDULERS') and isinstance(ksampler_class.SCHEDULERS, (list, tuple)):
scheduler_names = list(ksampler_class.SCHEDULERS)
# Fallback to module-level constants if class attributes not found
if sampler_names == ["euler"] and hasattr(comfy.samplers, 'KSAMPLER_NAMES'):
sampler_names = list(comfy.samplers.KSAMPLER_NAMES)
if scheduler_names == ["normal"] and hasattr(comfy.samplers, 'SCHEDULER_NAMES'):
scheduler_names = list(comfy.samplers.SCHEDULER_NAMES)
logger.debug(f"Loaded {len(sampler_names)} samplers and {len(scheduler_names)} schedulers from ComfyUI")
except (ImportError, AttributeError) as e:
# Fallback if ComfyUI is not available or constants missing
logger.warning(f"Could not load ComfyUI sampler constants: {e}. Using fallback lists.")
sampler_names = ["euler", "euler_ancestral", "heun", "dpm_2", "dpm_2_ancestral", "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_sde", "dpmpp_2m", "dpmpp_2m_sde", "ddpm", "lcm"]
scheduler_names = ["beta", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform"]
return {
"required": {
"model": ("MODEL",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"latent_image": ("LATENT",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 30, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"sampler_name": (sampler_names, {"default": sampler_names[0] if sampler_names else "euler"}),
"scheduler": (scheduler_names, {"default": scheduler_names[0] if scheduler_names else "beta"}),
"bokeh_control": ("BOKEH_CONTROL",),
},
"optional": {
"true_cfg": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"tooltip": "True CFG scale for negative prompt"
}),
}
}
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("latent",)
FUNCTION = "sample"
CATEGORY = "bokeh_diffusion/sampling"
def sample(self, model, positive, negative, latent_image, seed, steps, cfg, sampler_name, scheduler,
bokeh_control, true_cfg=1.0):
"""Sample with bokeh control"""
# Check if model has bokeh adapter
if not hasattr(model, 'bokeh_adapter'):
raise Exception("Model does not have bokeh adapter applied. Use ApplyBokehAdapter node first.")
bokeh_adapter = model.bokeh_adapter
transformer = model._bokeh_transformer
# Get bokeh control parameters
bokeh_target = bokeh_control["bokeh_target"]
bokeh_pivot = bokeh_control.get("bokeh_pivot")
num_grounding_steps = bokeh_control.get("num_grounding_steps", 0)
is_grounded = bokeh_control.get("is_grounded", False)
# Get latent
samples = latent_image["samples"]
device = samples.device
dtype = samples.dtype
# Prepare bokeh annotations
if is_grounded:
bokeh_ann = torch.tensor([bokeh_target, bokeh_pivot], dtype=dtype, device=device)
else:
bokeh_ann = torch.tensor([bokeh_target], dtype=dtype, device=device)
bokeh_ann = bokeh_ann.unsqueeze(1)
neg_bokeh_ann = torch.full_like(bokeh_ann, -1)
# Store bokeh info in transformer for use during forward pass
# The attention processors will access this via joint_attention_kwargs
transformer._bokeh_ann = bokeh_ann
transformer._neg_bokeh_ann = neg_bokeh_ann
transformer._is_grounded = is_grounded
transformer._num_grounding_steps = num_grounding_steps
transformer._true_cfg = true_cfg
transformer._current_step = 0
# Patch the transformer's forward method to inject bokeh annotations
original_forward = transformer.forward
def patched_forward(
hidden_states,
timestep=None,
encoder_hidden_states=None,
pooled_projections=None,
added_cond_kwargs=None,
return_dict=True,
**kwargs
):
# Get current step info
current_step = getattr(transformer, '_current_step', 0)
is_grounded = getattr(transformer, '_is_grounded', False)
num_grounding_steps = getattr(transformer, '_num_grounding_steps', 0)
bokeh_ann = getattr(transformer, '_bokeh_ann', None)
# Prepare joint_attention_kwargs with bokeh info
joint_attention_kwargs = kwargs.get('joint_attention_kwargs', {})
if bokeh_ann is not None:
perform_swap = is_grounded and (current_step < num_grounding_steps)
batch_swap_ids = [1, 1] if is_grounded else None
# Get bokeh embeddings from adapter
bokeh_embeds = bokeh_adapter.embedding_layer(bokeh_ann)
joint_attention_kwargs['bokeh_embeds'] = bokeh_embeds
joint_attention_kwargs['perform_swap'] = perform_swap
joint_attention_kwargs['batch_swap_ids'] = batch_swap_ids
kwargs['joint_attention_kwargs'] = joint_attention_kwargs
return original_forward(
hidden_states,
timestep=timestep,
encoder_hidden_states=encoder_hidden_states,
pooled_projections=pooled_projections,
added_cond_kwargs=added_cond_kwargs,
return_dict=return_dict,
**kwargs
)
transformer.forward = patched_forward
try:
# Use standard ComfyUI sampling
import comfy.samplers
import comfy.sample
# Get the sampler function
sampler = comfy.samplers.sampler_object(sampler_name)
# Create a callback to track steps for grounded generation
def step_callback(step, x0, x, total_steps):
transformer._current_step = step
# Sample using ComfyUI's sample function
# Note: This is a simplified version. Full implementation may need
# to handle negative prompts with bokeh control separately
disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
sampled = comfy.sample.sample(
model,
seed,
steps,
cfg,
sampler_name,
scheduler,
positive,
negative,
samples,
denoise=1.0,
disable_noise=False,
start_step=None,
last_step=None,
force_full_denoise=False,
callback=step_callback,
disable_pbar=disable_pbar,
seed_mode="scale_alike"
)
# Restore original forward
transformer.forward = original_forward
return (sampled,)
except ImportError:
# Fallback: if ComfyUI imports fail, return original samples
# This allows the node to be defined even if not in ComfyUI
print("Warning: ComfyUI not found. Bokeh control requires ComfyUI environment.")
transformer.forward = original_forward
return ({"samples": samples},)
except Exception as e:
# Restore original forward on error
transformer.forward = original_forward
raise Exception(f"Sampling failed: {e}")
# Node class mappings for ComfyUI
NODE_CLASS_MAPPINGS = {
"BokehAdapterLoader": BokehAdapterLoader,
"ApplyBokehAdapter": ApplyBokehAdapter,
"BokehLevel": BokehLevel,
"BokehKSampler": BokehKSampler,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BokehAdapterLoader": "Load Bokeh Adapter",
"ApplyBokehAdapter": "Apply Bokeh Adapter",
"BokehLevel": "Bokeh Level",
"BokehKSampler": "Bokeh KSampler",
}