BREAKING CHANGE: Node categories now use emoji-based organization Add 6 new xyz-helper nodes adapted from comfyui-essentials-nodes: - FluxSamplerParams: FLUX-optimized parameter generator with batch support - LoRAFolderBatch: Batch process multiple LoRAs from folders - PlotParameters: Visualize parameter effects with graphs - SamplerSelectHelper: Intelligent sampler selection with recommendations - SchedulerSelectHelper: Optimal scheduler selection for samplers - TextEncodeSamplerParams: Combined text encoding and parameter management Changes: - Port and enhance nodes from comfyui-essentials (now in maintenance mode) - Add comprehensive documentation with attribution to original author (cubiq) - Create example workflows for xyz-helpers tools - Update all node categories to use emoji-based organization - Fix all unit tests to pass with new category system - Update README with xyz-helpers section and attribution Attribution: xyz-helpers adapted from github.com/cubiq/ComfyUI_essentials All tests passing (318 pass, 2 skip)
308 lines
9.8 KiB
Python
308 lines
9.8 KiB
Python
"""Width Height Selector node for ComfyUI."""
|
||
|
||
from typing import Tuple
|
||
from ...base.base_node import ComfyAssetsBaseNode
|
||
from .logic import (
|
||
get_preset_dimensions,
|
||
validate_dimensions,
|
||
sanitize_dimensions,
|
||
)
|
||
from .presets import (
|
||
PRESET_OPTIONS,
|
||
PRESET_METADATA,
|
||
get_model_recommendation,
|
||
get_preset_metadata,
|
||
get_presets_by_model_group,
|
||
)
|
||
|
||
|
||
class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||
"""
|
||
Width Height Selector node for selecting image dimensions.
|
||
|
||
Provides preset-based dimension selection with swap functionality,
|
||
optimized for SDXL and FLUX models with comprehensive aspect ratio support.
|
||
"""
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
"""Define the input types for the ComfyUI node."""
|
||
# Create formatted preset options with metadata
|
||
preset_options = ["custom"] # Custom first
|
||
|
||
# Add formatted presets with metadata
|
||
for preset_name in PRESET_OPTIONS.keys():
|
||
if preset_name != "custom":
|
||
metadata = PRESET_METADATA.get(preset_name)
|
||
if metadata:
|
||
formatted_option = (
|
||
f"{preset_name} - {metadata.aspect_ratio} "
|
||
f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
|
||
)
|
||
preset_options.append(formatted_option)
|
||
else:
|
||
preset_options.append(preset_name)
|
||
|
||
return {
|
||
"required": {
|
||
"preset": (
|
||
preset_options,
|
||
{
|
||
"default": "custom",
|
||
"tooltip": "Select from optimized resolution presets or use "
|
||
"custom dimensions. SDXL presets are ~1MP, FLUX presets are "
|
||
"higher resolution, Ultra-wide presets support modern "
|
||
"aspect ratios.",
|
||
},
|
||
),
|
||
"width": (
|
||
"INT",
|
||
{
|
||
"default": 1024,
|
||
"min": 64,
|
||
"max": 8192,
|
||
"step": 8,
|
||
"tooltip": "Custom width in pixels (must be multiple of 8). "
|
||
"Used when preset is 'custom' or as fallback for invalid "
|
||
"presets.",
|
||
},
|
||
),
|
||
"height": (
|
||
"INT",
|
||
{
|
||
"default": 1024,
|
||
"min": 64,
|
||
"max": 8192,
|
||
"step": 8,
|
||
"tooltip": "Custom height in pixels (must be multiple of 8). "
|
||
"Used when preset is 'custom' or as fallback for invalid "
|
||
"presets.",
|
||
},
|
||
),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("INT", "INT")
|
||
RETURN_NAMES = ("width", "height")
|
||
FUNCTION = "get_dimensions"
|
||
CATEGORY = "ComfyAssets/🖼️ Resolution"
|
||
|
||
def get_dimensions(self, preset: str, width: int, height: int) -> Tuple[int, int]:
|
||
"""
|
||
Get width and height dimensions with preset and swap support.
|
||
|
||
Args:
|
||
preset: Selected preset name or formatted preset string
|
||
width: Custom width value
|
||
height: Custom height value
|
||
|
||
Returns:
|
||
Tuple of (width, height)
|
||
"""
|
||
try:
|
||
# Extract original preset name from formatted string if needed
|
||
original_preset = self._extract_preset_name(preset)
|
||
|
||
# Get base dimensions from preset or custom input
|
||
final_width, final_height = get_preset_dimensions(
|
||
original_preset, width, height
|
||
)
|
||
|
||
# Sanitize dimensions to ensure they meet ComfyUI requirements
|
||
final_width, final_height = sanitize_dimensions(final_width, final_height)
|
||
|
||
# Validate final dimensions
|
||
if not validate_dimensions(final_width, final_height):
|
||
# This should not happen after sanitization, but handle gracefully
|
||
self.handle_error(
|
||
f"Generated invalid dimensions: {final_width}×{final_height}. "
|
||
f"Using fallback dimensions 1024×1024."
|
||
)
|
||
final_width, final_height = 1024, 1024
|
||
|
||
return (final_width, final_height)
|
||
|
||
except Exception as e:
|
||
# Handle any unexpected errors gracefully
|
||
error_msg = (
|
||
f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
|
||
)
|
||
self.handle_error(error_msg)
|
||
return (1024, 1024)
|
||
|
||
def _extract_preset_name(self, formatted_preset: str) -> str:
|
||
"""
|
||
Extract the original preset name from a formatted preset string.
|
||
|
||
Args:
|
||
formatted_preset: Either original preset name or formatted string
|
||
|
||
Returns:
|
||
Original preset name
|
||
"""
|
||
# If it's already "custom", return as-is
|
||
if formatted_preset == "custom":
|
||
return formatted_preset
|
||
|
||
# If it contains formatting metadata, extract the resolution part
|
||
if " - " in formatted_preset:
|
||
# Format is: "1024×1024 - 1:1 (1.0MP) - SDXL"
|
||
# Extract the first part (resolution)
|
||
resolution_part = formatted_preset.split(" - ")[0]
|
||
|
||
# Verify this is a valid preset name
|
||
if resolution_part in PRESET_OPTIONS:
|
||
return resolution_part
|
||
|
||
# If no formatting or not found, check if it's directly a valid preset
|
||
if formatted_preset in PRESET_OPTIONS:
|
||
return formatted_preset
|
||
|
||
# Default to "custom" if we can't parse it
|
||
return "custom"
|
||
|
||
def get_preset_info(self, preset: str) -> str:
|
||
"""
|
||
Get descriptive information about a preset.
|
||
|
||
Args:
|
||
preset: Preset name
|
||
|
||
Returns:
|
||
Description string for the preset
|
||
"""
|
||
if preset == "custom":
|
||
return "Custom dimensions - use the width and height inputs below"
|
||
|
||
metadata = get_preset_metadata(preset)
|
||
if metadata.width > 0: # Valid metadata
|
||
return (
|
||
f"{preset} - {metadata.aspect_ratio} ({metadata.megapixels:.1f}MP) - "
|
||
f"{metadata.description}"
|
||
)
|
||
|
||
return f"Unknown preset: {preset}"
|
||
|
||
def get_model_optimization(self, preset: str) -> str:
|
||
"""
|
||
Get model optimization information for a preset.
|
||
|
||
Args:
|
||
preset: Preset name
|
||
|
||
Returns:
|
||
Optimization information string
|
||
"""
|
||
return get_model_recommendation(preset)
|
||
|
||
def validate_inputs(self, preset: str, width: int, height: int) -> bool:
|
||
"""
|
||
Validate node inputs.
|
||
|
||
Args:
|
||
preset: Preset name or formatted preset string
|
||
width: Width value
|
||
height: Height value
|
||
|
||
Returns:
|
||
True if inputs are valid
|
||
"""
|
||
# Extract original preset name
|
||
original_preset = self._extract_preset_name(preset)
|
||
|
||
# Check if preset exists or is custom
|
||
if original_preset != "custom" and original_preset not in PRESET_OPTIONS:
|
||
return False
|
||
|
||
# For custom preset, validate dimensions
|
||
if original_preset == "custom":
|
||
if not validate_dimensions(width, height):
|
||
return False
|
||
|
||
return True
|
||
|
||
@classmethod
|
||
def get_preset_list(cls) -> list:
|
||
"""
|
||
Get list of available presets for external use.
|
||
|
||
Returns:
|
||
List of preset names
|
||
"""
|
||
return list(PRESET_OPTIONS.keys())
|
||
|
||
@classmethod
|
||
def get_preset_dimensions_static(cls, preset: str) -> Tuple[int, int]:
|
||
"""
|
||
Static method to get preset dimensions without node instance.
|
||
|
||
Args:
|
||
preset: Preset name
|
||
|
||
Returns:
|
||
Tuple of (width, height) or (0, 0) if invalid
|
||
"""
|
||
if preset in PRESET_OPTIONS:
|
||
return PRESET_OPTIONS[preset]
|
||
return (0, 0)
|
||
|
||
@classmethod
|
||
def get_presets_by_model(cls, model_group: str) -> dict:
|
||
"""
|
||
Get all presets for a specific model group with metadata.
|
||
|
||
Args:
|
||
model_group: Model group name ("SDXL", "FLUX", "Ultra-Wide")
|
||
|
||
Returns:
|
||
Dictionary of presets with metadata
|
||
"""
|
||
return get_presets_by_model_group(model_group)
|
||
|
||
@classmethod
|
||
def get_preset_metadata_static(cls, preset: str) -> dict:
|
||
"""
|
||
Get metadata for a preset as a dictionary.
|
||
|
||
Args:
|
||
preset: Preset name
|
||
|
||
Returns:
|
||
Dictionary with metadata information
|
||
"""
|
||
metadata = get_preset_metadata(preset)
|
||
return {
|
||
"width": metadata.width,
|
||
"height": metadata.height,
|
||
"aspect_ratio": metadata.aspect_ratio,
|
||
"aspect_decimal": metadata.aspect_decimal,
|
||
"megapixels": metadata.megapixels,
|
||
"model_group": metadata.model_group,
|
||
"category": metadata.category,
|
||
"description": metadata.description,
|
||
}
|
||
|
||
@classmethod
|
||
def get_model_groups(cls) -> list:
|
||
"""
|
||
Get list of available model groups.
|
||
|
||
Returns:
|
||
List of model group names
|
||
"""
|
||
return list(set(metadata.model_group for metadata in PRESET_METADATA.values()))
|
||
|
||
def __str__(self) -> str:
|
||
"""String representation of the node."""
|
||
return f"WidthHeightSelectorNode(presets={len(PRESET_OPTIONS)})"
|
||
|
||
def __repr__(self) -> str:
|
||
"""Detailed string representation of the node."""
|
||
return (
|
||
f"WidthHeightSelectorNode("
|
||
f"presets={len(PRESET_OPTIONS)}, "
|
||
f"category='{self.CATEGORY}', "
|
||
f"function='{self.FUNCTION}'"
|
||
f")"
|
||
)
|