fix: update black line-length to 88 and reformat codebase

- Update pyproject.toml to use black's default line-length of 88
- This matches what the CI workflow expects (black --check without args)
- Reformat all Python files to comply with the new line length
- This will prevent CI failures due to formatting discrepancies
This commit is contained in:
Vito Sansevero
2025-08-01 09:45:43 -07:00
parent 932e30ade0
commit e84ec6721c
23 changed files with 394 additions and 124 deletions
+10 -6
View File
@@ -226,9 +226,11 @@ class HFHubLoraLoader:
lora_path = hf_hub_download(
repo_id=repo_id.strip(),
subfolder=None
if subfolder is None or subfolder.strip() == ""
else subfolder.strip(),
subfolder=(
None
if subfolder is None or subfolder.strip() == ""
else subfolder.strip()
),
filename=filename.strip(),
cache_dir=find_or_create_cache(),
)
@@ -281,9 +283,11 @@ class HFHubEmbeddingLoader:
):
hf_hub_download(
repo_id=repo_id.strip(),
subfolder=None
if subfolder is None or subfolder.strip() == ""
else subfolder.strip(),
subfolder=(
None
if subfolder is None or subfolder.strip() == ""
else subfolder.strip()
),
filename=filename.strip(),
local_dir=get_folder_paths("embeddings")[0],
)
+3 -1
View File
@@ -35,7 +35,9 @@ class ComfyAssetsBaseNode:
"""
pass
def handle_error(self, error_msg: str, exception: Optional[Exception] = None) -> None:
def handle_error(
self, error_msg: str, exception: Optional[Exception] = None
) -> None:
"""
Standardized error handling with logging
+6 -2
View File
@@ -4,7 +4,9 @@ import torch
from typing import Dict, Tuple
def create_empty_latent_batch(width: int, height: int, batch_size: int = 1) -> Dict[str, torch.Tensor]:
def create_empty_latent_batch(
width: int, height: int, batch_size: int = 1
) -> Dict[str, torch.Tensor]:
"""
Create empty latent tensor with batch support.
@@ -28,7 +30,9 @@ def create_empty_latent_batch(width: int, height: int, batch_size: int = 1) -> D
# Ensure dimensions are divisible by 8 (VAE requirement)
if width % 8 != 0 or height % 8 != 0:
raise ValueError(f"Width and height must be divisible by 8, got {width}x{height}")
raise ValueError(
f"Width and height must be divisible by 8, got {width}x{height}"
)
# Convert pixel dimensions to latent space (divide by 8)
latent_width = width // 8
+25 -8
View File
@@ -36,7 +36,8 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
metadata = PRESET_METADATA.get(preset_name)
if metadata:
formatted_option = (
f"{preset_name} - {metadata.aspect_ratio} " f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
f"{preset_name} - {metadata.aspect_ratio} "
f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
)
preset_options.append(formatted_option)
else:
@@ -85,7 +86,8 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
"min": 1,
"max": 64,
"step": 1,
"tooltip": "Number of empty latents to create in the batch. " "Useful for batch processing workflows.",
"tooltip": "Number of empty latents to create in the batch. "
"Useful for batch processing workflows.",
},
),
}
@@ -116,7 +118,9 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
original_preset = self._extract_preset_name(preset)
# Get base dimensions from preset or custom input
base_width, base_height = get_preset_dimensions(original_preset, width, height)
base_width, base_height = get_preset_dimensions(
original_preset, width, height
)
# Sanitize dimensions to ensure they meet requirements
final_width, final_height = sanitize_dimensions(base_width, base_height)
@@ -130,17 +134,23 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
# Validate final dimensions
if not validate_dimensions(final_width, final_height):
self.handle_error(f"Invalid dimensions after sanitization: {final_width}×{final_height}")
self.handle_error(
f"Invalid dimensions after sanitization: {final_width}×{final_height}"
)
# Validate batch size
if batch_size <= 0:
self.handle_error(f"Batch size must be positive, got {batch_size}")
if batch_size > 64:
self.log_info(f"Large batch size ({batch_size}) may use significant memory")
self.log_info(
f"Large batch size ({batch_size}) may use significant memory"
)
# Create the empty latent batch
latent_dict = create_empty_latent_batch(final_width, final_height, batch_size)
latent_dict = create_empty_latent_batch(
final_width, final_height, batch_size
)
# Log the operation
latent_height = final_height // 8
@@ -188,7 +198,9 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
# Default to "custom" if we can't parse it
return "custom"
def validate_inputs(self, preset: str, width: int, height: int, batch_size: int) -> bool:
def validate_inputs(
self, preset: str, width: int, height: int, batch_size: int
) -> bool:
"""
Validate node inputs.
@@ -279,7 +291,12 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
def __repr__(self) -> str:
"""Detailed string representation of the node."""
return f"EmptyLatentBatchNode(" f"category='{self.CATEGORY}', " f"function='{self.FUNCTION}'" f")"
return (
f"EmptyLatentBatchNode("
f"category='{self.CATEGORY}', "
f"function='{self.FUNCTION}'"
f")"
)
# Node class mappings for ComfyUI registration
+30 -9
View File
@@ -48,7 +48,9 @@ def get_save_image_path(
prefix_name = os.path.basename(filename_prefix)
# Sanitize only the filename part (not the directory path)
safe_prefix = prefix_name.replace(":", "_") # Only sanitize problematic chars for filenames
safe_prefix = prefix_name.replace(
":", "_"
) # Only sanitize problematic chars for filenames
safe_prefix = "".join(c for c in safe_prefix if c.isalnum() or c in "._-")
# Create unique filename with timestamp to avoid conflicts
@@ -114,7 +116,9 @@ def convert_tensor_to_pil(image_tensor: torch.Tensor) -> Image.Image:
return img
def create_png_metadata(prompt: Optional[Dict] = None, extra_pnginfo: Optional[Dict] = None) -> Optional[PngInfo]:
def create_png_metadata(
prompt: Optional[Dict] = None, extra_pnginfo: Optional[Dict] = None
) -> Optional[PngInfo]:
"""
Create PNG metadata with workflow information
@@ -242,7 +246,10 @@ def process_image_batch(
format_extensions = {"PNG": ".png", "JPEG": ".jpg", "WEBP": ".webp"}
if format_type not in format_extensions:
raise ValueError(f"Unsupported format: {format_type}. " f"Supported: {list(format_extensions.keys())}")
raise ValueError(
f"Unsupported format: {format_type}. "
f"Supported: {list(format_extensions.keys())}"
)
format_ext = format_extensions[format_type]
@@ -308,7 +315,9 @@ def process_image_batch(
return results, enhanced_data
def validate_save_inputs(images: torch.Tensor, format_type: str, quality: int, png_compress_level: int) -> None:
def validate_save_inputs(
images: torch.Tensor, format_type: str, quality: int, png_compress_level: int
) -> None:
"""
Validate inputs for image saving
@@ -326,19 +335,31 @@ def validate_save_inputs(images: torch.Tensor, format_type: str, quality: int, p
raise ValueError(f"images must be a torch.Tensor, got {type(images).__name__}")
if len(images.shape) != 4:
raise ValueError(f"images tensor must have 4 dimensions [batch, height, width, channels], " f"got {len(images.shape)}")
raise ValueError(
f"images tensor must have 4 dimensions [batch, height, width, channels], "
f"got {len(images.shape)}"
)
# Validate format
supported_formats = ["PNG", "JPEG", "WEBP"]
if format_type not in supported_formats:
raise ValueError(f"format must be one of {supported_formats}, got {format_type}")
raise ValueError(
f"format must be one of {supported_formats}, got {format_type}"
)
# Validate quality (for JPEG/WebP)
if format_type in ["JPEG", "WEBP"]:
if not isinstance(quality, int) or not (1 <= quality <= 100):
raise ValueError(f"quality must be an integer between 1 and 100, got {quality}")
raise ValueError(
f"quality must be an integer between 1 and 100, got {quality}"
)
# Validate PNG compression level
if format_type == "PNG":
if not isinstance(png_compress_level, int) or not (0 <= png_compress_level <= 9):
raise ValueError(f"png_compress_level must be an integer between 0 and 9, " f"got {png_compress_level}")
if not isinstance(png_compress_level, int) or not (
0 <= png_compress_level <= 9
):
raise ValueError(
f"png_compress_level must be an integer between 0 and 9, "
f"got {png_compress_level}"
)
+9 -3
View File
@@ -79,7 +79,8 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
"BOOLEAN",
{
"default": False,
"tooltip": "Use lossless WebP compression " "(ignores quality setting)",
"tooltip": "Use lossless WebP compression "
"(ignores quality setting)",
},
),
"popup": (
@@ -162,7 +163,10 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
# Log results
total_size = sum(data["file_size"] for data in enhanced_data)
self.log_info(f"Successfully saved {len(results)} images " f"(total size: {total_size / 1024:.1f} KB)")
self.log_info(
f"Successfully saved {len(results)} images "
f"(total size: {total_size / 1024:.1f} KB)"
)
# Return UI data for ComfyUI preview (clean) + enhanced data for our JS
return {
@@ -204,7 +208,9 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
# Additional node-specific validation
if not isinstance(webp_lossless, bool):
raise ValueError(f"webp_lossless must be a boolean, got {type(webp_lossless).__name__}")
raise ValueError(
f"webp_lossless must be a boolean, got {type(webp_lossless).__name__}"
)
if not isinstance(popup, bool):
raise ValueError(f"popup must be a boolean, got {type(popup).__name__}")
+25 -8
View File
@@ -28,7 +28,9 @@ def extract_dimensions(
if image is not None:
# IMAGE tensor format: [batch, height, width, channels]
if len(image.shape) != 4:
raise ValueError(f"Expected IMAGE tensor with 4 dimensions, got {len(image.shape)}")
raise ValueError(
f"Expected IMAGE tensor with 4 dimensions, got {len(image.shape)}"
)
_, height, width, _ = image.shape
return int(width), int(height)
@@ -40,7 +42,10 @@ def extract_dimensions(
samples = latent["samples"]
if len(samples.shape) != 4:
raise ValueError(f"Expected LATENT samples tensor with 4 dimensions, " f"got {len(samples.shape)}")
raise ValueError(
f"Expected LATENT samples tensor with 4 dimensions, "
f"got {len(samples.shape)}"
)
_, _, latent_height, latent_width = samples.shape
@@ -74,7 +79,9 @@ def ensure_divisible_by_8(width: int, height: int) -> Tuple[int, int]:
return int(new_width), int(new_height)
def calculate_scaled_dimensions(width: int, height: int, scale_factor: float) -> Tuple[int, int]:
def calculate_scaled_dimensions(
width: int, height: int, scale_factor: float
) -> Tuple[int, int]:
"""
Calculate new dimensions with scale factor and ensure divisible by 8
@@ -97,7 +104,9 @@ def calculate_scaled_dimensions(width: int, height: int, scale_factor: float) ->
return ensure_divisible_by_8(new_width, new_height)
def validate_scale_factor(scale_factor: float, min_scale: float = 0.1, max_scale: float = 8.0) -> None:
def validate_scale_factor(
scale_factor: float, min_scale: float = 0.1, max_scale: float = 8.0
) -> None:
"""
Validate scale factor is within reasonable bounds
@@ -110,13 +119,19 @@ def validate_scale_factor(scale_factor: float, min_scale: float = 0.1, max_scale
ValueError: If scale factor is out of bounds
"""
if not isinstance(scale_factor, (int, float)):
raise ValueError(f"Scale factor must be a number, got {type(scale_factor).__name__}")
raise ValueError(
f"Scale factor must be a number, got {type(scale_factor).__name__}"
)
if scale_factor < min_scale:
raise ValueError(f"Scale factor {scale_factor} is too small (minimum: {min_scale})")
raise ValueError(
f"Scale factor {scale_factor} is too small (minimum: {min_scale})"
)
if scale_factor > max_scale:
raise ValueError(f"Scale factor {scale_factor} is too large (maximum: {max_scale})")
raise ValueError(
f"Scale factor {scale_factor} is too large (maximum: {max_scale})"
)
def calculate_resolution_from_input(
@@ -146,6 +161,8 @@ def calculate_resolution_from_input(
original_width, original_height = extract_dimensions(image=image, latent=latent)
# Calculate scaled dimensions
new_width, new_height = calculate_scaled_dimensions(original_width, original_height, scale_factor)
new_width, new_height = calculate_scaled_dimensions(
original_width, original_height, scale_factor
)
return new_width, new_height
+28 -9
View File
@@ -42,7 +42,8 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
"max": 8.0,
"step": 0.1,
"display": "slider",
"tooltip": "Factor to scale the resolution by " "(e.g., 2.0 for 2x, 0.5 for half scale)",
"tooltip": "Factor to scale the resolution by "
"(e.g., 2.0 for 2x, 0.5 for half scale)",
},
),
},
@@ -87,11 +88,20 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
self.validate_inputs(scale_factor=scale_factor, image=image, latent=latent)
# Log the operation
input_type = "IMAGE" if image is not None else "LATENT" if latent is not None else "NONE"
self.log_info(f"Calculating resolution with scale_factor={scale_factor}, " f"input_type={input_type}")
input_type = (
"IMAGE"
if image is not None
else "LATENT" if latent is not None else "NONE"
)
self.log_info(
f"Calculating resolution with scale_factor={scale_factor}, "
f"input_type={input_type}"
)
# Calculate the resolution
width, height = calculate_resolution_from_input(scale_factor=scale_factor, image=image, latent=latent)
width, height = calculate_resolution_from_input(
scale_factor=scale_factor, image=image, latent=latent
)
# Log the result
self.log_info(f"Calculated resolution: {width}x{height}")
@@ -126,7 +136,9 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
# Validate scale factor type
if not isinstance(scale_factor, (int, float)):
raise ValueError(f"scale_factor must be a number, got {type(scale_factor).__name__}")
raise ValueError(
f"scale_factor must be a number, got {type(scale_factor).__name__}"
)
# Validate tensors using helper methods
if image is not None:
@@ -138,11 +150,14 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
def _validate_image_tensor(self, image: torch.Tensor) -> None:
"""Validate image tensor format"""
if not isinstance(image, torch.Tensor):
raise ValueError(f"image must be a torch.Tensor, got {type(image).__name__}")
raise ValueError(
f"image must be a torch.Tensor, got {type(image).__name__}"
)
if len(image.shape) != 4:
raise ValueError(
f"image tensor must have 4 dimensions " f"[batch, height, width, channels], got {len(image.shape)}"
f"image tensor must have 4 dimensions "
f"[batch, height, width, channels], got {len(image.shape)}"
)
def _validate_latent_dict(self, latent: Dict[str, torch.Tensor]) -> None:
@@ -155,11 +170,15 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
samples = latent["samples"]
if not isinstance(samples, torch.Tensor):
raise ValueError(f"latent['samples'] must be a torch.Tensor, " f"got {type(samples).__name__}")
raise ValueError(
f"latent['samples'] must be a torch.Tensor, "
f"got {type(samples).__name__}"
)
if len(samples.shape) != 4:
raise ValueError(
f"latent samples tensor must have 4 dimensions " f"[batch, channels, height, width], got {len(samples.shape)}"
f"latent samples tensor must have 4 dimensions "
f"[batch, channels, height, width], got {len(samples.shape)}"
)
@@ -65,7 +65,9 @@ class SamplerComboCompactNode(ComfyAssetsBaseNode):
FUNCTION = "get_combo"
CATEGORY = "ComfyAssets"
def get_combo(self, sampler: str, sched: str, steps: int, cfg: float) -> Tuple[object, str, int, float]:
def get_combo(
self, sampler: str, sched: str, steps: int, cfg: float
) -> Tuple[object, str, int, float]:
"""
Get compact sampler combo configuration.
+6 -2
View File
@@ -40,7 +40,9 @@ except ImportError:
]
def validate_sampler_settings(sampler_name: str, scheduler: str, steps: int, cfg: float) -> bool:
def validate_sampler_settings(
sampler_name: str, scheduler: str, steps: int, cfg: float
) -> bool:
"""
Validate sampler configuration settings.
@@ -81,7 +83,9 @@ def validate_sampler_settings(sampler_name: str, scheduler: str, steps: int, cfg
return False
def get_sampler_combo(sampler_name: str, scheduler: str, steps: int, cfg: float) -> Tuple[str, str, int, float]:
def get_sampler_combo(
sampler_name: str, scheduler: str, steps: int, cfg: float
) -> Tuple[str, str, int, float]:
"""
Process and return sampler combo settings.
+19 -6
View File
@@ -70,7 +70,9 @@ class SamplerComboNode(ComfyAssetsBaseNode):
FUNCTION = "get_sampler_combo"
CATEGORY = "ComfyAssets"
def get_sampler_combo(self, sampler_name: str, scheduler: str, steps: int, cfg: float) -> Tuple[object, str, int, float]:
def get_sampler_combo(
self, sampler_name: str, scheduler: str, steps: int, cfg: float
) -> Tuple[object, str, int, float]:
"""
Get sampler combo configuration.
@@ -117,7 +119,10 @@ class SamplerComboNode(ComfyAssetsBaseNode):
# Return sampler name for testing
sampler = result[0]
self.log_info(f"Configured sampler combo: {result[0]}, {result[1]}, " f"{result[2]} steps, CFG {result[3]}")
self.log_info(
f"Configured sampler combo: {result[0]}, {result[1]}, "
f"{result[2]} steps, CFG {result[3]}"
)
return (sampler, result[1], result[2], result[3])
@@ -139,7 +144,9 @@ class SamplerComboNode(ComfyAssetsBaseNode):
sampler = "euler"
return (sampler, "normal", 20, 7.0)
def validate_inputs(self, sampler_name: str, scheduler: str, steps: int, cfg: float) -> None:
def validate_inputs(
self, sampler_name: str, scheduler: str, steps: int, cfg: float
) -> None:
"""
Validate sampler combo inputs.
@@ -154,7 +161,8 @@ class SamplerComboNode(ComfyAssetsBaseNode):
"""
if not validate_sampler_settings(sampler_name, scheduler, steps, cfg):
self.handle_error(
f"Invalid sampler settings: sampler={sampler_name}, " f"scheduler={scheduler}, steps={steps}, cfg={cfg}"
f"Invalid sampler settings: sampler={sampler_name}, "
f"scheduler={scheduler}, steps={steps}, cfg={cfg}"
)
def get_scheduler_suggestions(self, sampler_name: str) -> list:
@@ -205,7 +213,9 @@ class SamplerComboNode(ComfyAssetsBaseNode):
"recommendation": f"Recommended range: {min_cfg}-{max_cfg} CFG",
}
def get_combo_analysis(self, sampler_name: str, scheduler: str, steps: int, cfg: float) -> dict:
def get_combo_analysis(
self, sampler_name: str, scheduler: str, steps: int, cfg: float
) -> dict:
"""
Analyze the sampler combo configuration and provide recommendations.
@@ -264,7 +274,10 @@ class SamplerComboNode(ComfyAssetsBaseNode):
def __str__(self) -> str:
"""String representation of the node."""
return f"SamplerComboNode(samplers={len(SAMPLERS)}, " f"schedulers={len(SCHEDULERS)})"
return (
f"SamplerComboNode(samplers={len(SAMPLERS)}, "
f"schedulers={len(SCHEDULERS)})"
)
def __repr__(self) -> str:
"""Detailed string representation of the node."""
+9 -3
View File
@@ -66,7 +66,9 @@ def sanitize_seed_value(seed: Any) -> int:
raise ValueError(f"Invalid seed value: {seed}") from e
def create_history_entry(seed: int, timestamp: Optional[float] = None) -> Dict[str, Any]:
def create_history_entry(
seed: int, timestamp: Optional[float] = None
) -> Dict[str, Any]:
"""
Create a standardized history entry for a seed.
@@ -87,7 +89,9 @@ def create_history_entry(seed: int, timestamp: Optional[float] = None) -> Dict[s
}
def filter_duplicate_seeds(history: List[Dict[str, Any]], new_seed: int, dedup_window_ms: int = 500) -> bool:
def filter_duplicate_seeds(
history: List[Dict[str, Any]], new_seed: int, dedup_window_ms: int = 500
) -> bool:
"""
Check if a seed should be filtered as a duplicate.
@@ -189,7 +193,9 @@ def format_time_ago(timestamp: float) -> str:
return f"{seconds}s ago"
def search_history_by_seed(history: List[Dict[str, Any]], seed: int) -> Optional[Dict[str, Any]]:
def search_history_by_seed(
history: List[Dict[str, Any]], seed: int
) -> Optional[Dict[str, Any]]:
"""
Search history for a specific seed value.
+10 -3
View File
@@ -28,7 +28,8 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
"default": 12345,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"tooltip": "Seed value for generation processes. " "History UI tracks all changes automatically.",
"tooltip": "Seed value for generation processes. "
"History UI tracks all changes automatically.",
},
),
}
@@ -56,7 +57,10 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
import logging
logger = logging.getLogger(__name__)
logger.error(f"{self.__class__.__name__}: Invalid seed value: {seed}. " f"Using fallback seed 12345.")
logger.error(
f"{self.__class__.__name__}: Invalid seed value: {seed}. "
f"Using fallback seed 12345."
)
return (12345,)
clean_seed = sanitize_seed_value(seed)
@@ -68,7 +72,10 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
import logging
logger = logging.getLogger(__name__)
logger.error(f"{self.__class__.__name__}: Error processing seed: {str(e)}. " f"Using fallback seed 12345.")
logger.error(
f"{self.__class__.__name__}: Error processing seed: {str(e)}. "
f"Using fallback seed 12345."
)
return (12345,)
def generate_new_seed(self) -> int:
@@ -5,7 +5,9 @@ from math import gcd
from .presets import PRESET_OPTIONS
def get_preset_dimensions(preset: str, custom_width: int, custom_height: int) -> Tuple[int, int]:
def get_preset_dimensions(
preset: str, custom_width: int, custom_height: int
) -> Tuple[int, int]:
"""
Get dimensions from preset name or use custom dimensions.
@@ -112,7 +114,9 @@ def sanitize_dimensions(width: int, height: int) -> Tuple[int, int]:
return width, height
def get_dimension_info(preset: str, width: int, height: int, swap_enabled: bool) -> dict:
def get_dimension_info(
preset: str, width: int, height: int, swap_enabled: bool
) -> dict:
"""
Get comprehensive dimension information including metadata.
@@ -201,7 +205,9 @@ def parse_dimension_string(dimension_str: str) -> Tuple[int, int]:
raise ValueError(f"Could not parse dimensions from {dimension_str}: {e}")
def get_optimal_scale_factor(current_width: int, current_height: int, target_width: int, target_height: int) -> float:
def get_optimal_scale_factor(
current_width: int, current_height: int, target_width: int, target_height: int
) -> float:
"""
Calculate optimal scale factor to get from current to target dimensions.
+14 -5
View File
@@ -36,7 +36,8 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
metadata = PRESET_METADATA.get(preset_name)
if metadata:
formatted_option = (
f"{preset_name} - {metadata.aspect_ratio} " f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
f"{preset_name} - {metadata.aspect_ratio} "
f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
)
preset_options.append(formatted_option)
else:
@@ -103,7 +104,9 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
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)
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)
@@ -112,7 +115,8 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
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."
f"Generated invalid dimensions: {final_width}×{final_height}. "
f"Using fallback dimensions 1024×1024."
)
final_width, final_height = 1024, 1024
@@ -120,7 +124,9 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
except Exception as e:
# Handle any unexpected errors gracefully
error_msg = f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
error_msg = (
f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
)
self.handle_error(error_msg)
return (1024, 1024)
@@ -170,7 +176,10 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
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"{preset} - {metadata.aspect_ratio} ({metadata.megapixels:.1f}MP) - "
f"{metadata.description}"
)
return f"Unknown preset: {preset}"
@@ -287,15 +287,21 @@ PRESET_METADATA: Dict[str, PresetMetadata] = {
# Legacy compatibility - maintain old preset dictionaries
SDXL_PRESETS: Dict[str, Tuple[int, int]] = {
k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"
k: (v.width, v.height)
for k, v in PRESET_METADATA.items()
if v.model_group == "SDXL"
}
FLUX_PRESETS: Dict[str, Tuple[int, int]] = {
k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"
k: (v.width, v.height)
for k, v in PRESET_METADATA.items()
if v.model_group == "FLUX"
}
ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = {
k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"
k: (v.width, v.height)
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide"
}
# Combined preset options for ComfyUI dropdown
@@ -308,28 +314,78 @@ PRESET_OPTIONS: Dict[str, Tuple[int, int]] = {
PRESET_CATEGORIES = {
"Custom": ["custom"],
# SDXL Categories
"SDXL Square": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" and v.category == "Square"],
"SDXL Portrait": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" and v.category == "Portrait"],
"SDXL Landscape": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" and v.category == "Landscape"],
"SDXL Square": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "SDXL" and v.category == "Square"
],
"SDXL Portrait": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "SDXL" and v.category == "Portrait"
],
"SDXL Landscape": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "SDXL" and v.category == "Landscape"
],
# FLUX Categories
"FLUX Square": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Square"],
"FLUX Portrait": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Portrait"],
"FLUX Cinematic": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Cinematic"],
"FLUX Classic": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Classic"],
"FLUX Photography": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Photography"],
"FLUX Square": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "FLUX" and v.category == "Square"
],
"FLUX Portrait": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "FLUX" and v.category == "Portrait"
],
"FLUX Cinematic": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "FLUX" and v.category == "Cinematic"
],
"FLUX Classic": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "FLUX" and v.category == "Classic"
],
"FLUX Photography": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "FLUX" and v.category == "Photography"
],
# Ultra-Wide Categories
"Ultra-Wide Gaming": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Gaming"],
"Ultra-Wide Gaming": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide" and v.category == "Gaming"
],
"Ultra-Wide Cinematic": [
k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Cinematic"
k
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide" and v.category == "Cinematic"
],
"Ultra-Wide Panoramic": [
k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Panoramic"
k
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide" and v.category == "Panoramic"
],
"Ultra-Wide Mobile": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide" and v.category == "Mobile"
],
"Ultra-Wide Mobile": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Mobile"],
"Ultra-Wide Vertical": [
k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Vertical"
k
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide" and v.category == "Vertical"
],
"Ultra-Wide Banner": [
k
for k, v in PRESET_METADATA.items()
if v.model_group == "Ultra-Wide" and v.category == "Banner"
],
"Ultra-Wide Banner": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Banner"],
}
# Legacy compatibility - preset descriptions
@@ -339,7 +395,9 @@ PRESET_DESCRIPTIONS = {k: v.description for k, v in PRESET_METADATA.items()}
MODEL_RECOMMENDATIONS = {
"SDXL": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"],
"FLUX": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"],
"Ultra-Wide": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"],
"Ultra-Wide": [
k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"
],
}
@@ -393,7 +451,9 @@ def validate_preset_dimensions() -> bool:
# Check divisible by 8
if width % 8 != 0 or height % 8 != 0:
print(f"ERROR: {preset_name} dimensions not divisible by 8: {width}×{height}")
print(
f"ERROR: {preset_name} dimensions not divisible by 8: {width}×{height}"
)
return False
# Check reasonable bounds
@@ -409,7 +469,9 @@ def validate_metadata_consistency() -> bool:
"""Validate metadata consistency and completeness."""
for preset_name, metadata in PRESET_METADATA.items():
# Verify aspect ratio calculation
expected_ratio, expected_decimal = calculate_aspect_ratio(metadata.width, metadata.height)
expected_ratio, expected_decimal = calculate_aspect_ratio(
metadata.width, metadata.height
)
if abs(metadata.aspect_decimal - expected_decimal) > 0.001:
print(
f"ERROR: {preset_name} aspect ratio mismatch: "
@@ -420,7 +482,10 @@ def validate_metadata_consistency() -> bool:
# Verify megapixel calculation
expected_mp = (metadata.width * metadata.height) / 1_000_000
if abs(metadata.megapixels - expected_mp) > 0.1:
print(f"ERROR: {preset_name} megapixel mismatch: " f"expected {expected_mp:.2f}, got {metadata.megapixels}")
print(
f"ERROR: {preset_name} megapixel mismatch: "
f"expected {expected_mp:.2f}, got {metadata.megapixels}"
)
return False
return True
+1 -1
View File
@@ -42,7 +42,7 @@ Icon = "https://avatars.githubusercontent.com/u/213204677?s=200"
includes = []
[tool.black]
line-length = 127
line-length = 88
target-version = ['py310']
include = '\.pyi?$'
extend-exclude = '''
+9 -3
View File
@@ -102,12 +102,18 @@ def assert_divisible_by_8(width: int, height: int) -> None:
assert height % 8 == 0, f"Height {height} must be divisible by 8"
def assert_reasonable_dimensions(width: int, height: int, min_size: int = 64, max_size: int = 8192) -> None:
def assert_reasonable_dimensions(
width: int, height: int, min_size: int = 64, max_size: int = 8192
) -> None:
"""
Helper function to assert dimensions are within reasonable bounds
"""
assert min_size <= width <= max_size, f"Width {width} out of reasonable range [{min_size}, {max_size}]"
assert min_size <= height <= max_size, f"Height {height} out of reasonable range [{min_size}, {max_size}]"
assert (
min_size <= width <= max_size
), f"Width {width} out of reasonable range [{min_size}, {max_size}]"
assert (
min_size <= height <= max_size
), f"Height {height} out of reasonable range [{min_size}, {max_size}]"
# Make helper functions available as pytest fixtures
+8 -2
View File
@@ -32,7 +32,10 @@ class TestComfyAssetsBaseNode:
node.handle_error("Test error message")
mock_logger.error.assert_called_once()
assert "ComfyAssetsBaseNode: Test error message" in mock_logger.error.call_args[0][0]
assert (
"ComfyAssetsBaseNode: Test error message"
in mock_logger.error.call_args[0][0]
)
def test_handle_error_with_exception_logs_exception(self):
"""Test error handling with original exception logs both messages"""
@@ -55,7 +58,10 @@ class TestComfyAssetsBaseNode:
node.log_info("Test information")
mock_logger.info.assert_called_once()
assert "ComfyAssetsBaseNode: Test information" in mock_logger.info.call_args[0][0]
assert (
"ComfyAssetsBaseNode: Test information"
in mock_logger.info.call_args[0][0]
)
def test_get_node_info_returns_metadata(self):
"""Test get_node_info returns correct metadata"""
+15 -5
View File
@@ -52,7 +52,9 @@ class TestKikoSaveImageLogic:
"""Test save path generation"""
with tempfile.TemporaryDirectory() as temp_dir:
# Test basic path generation
full_path, filename = get_save_image_path("test_prefix", 0, ".png", temp_dir)
full_path, filename = get_save_image_path(
"test_prefix", 0, ".png", temp_dir
)
assert full_path.startswith(temp_dir)
assert filename.startswith("test_prefix_")
@@ -211,20 +213,28 @@ class TestKikoSaveImageLogic:
images = torch.rand(1, 32, 32, 3)
# Quality out of range
with pytest.raises(ValueError, match="quality must be an integer between 1 and 100"):
with pytest.raises(
ValueError, match="quality must be an integer between 1 and 100"
):
validate_save_inputs(images, "JPEG", 0, 4)
with pytest.raises(ValueError, match="quality must be an integer between 1 and 100"):
with pytest.raises(
ValueError, match="quality must be an integer between 1 and 100"
):
validate_save_inputs(images, "JPEG", 101, 4)
def test_validate_save_inputs_invalid_compress_level(self):
"""Test validation with invalid PNG compression level"""
images = torch.rand(1, 32, 32, 3)
with pytest.raises(ValueError, match="png_compress_level must be an integer between 0 and 9"):
with pytest.raises(
ValueError, match="png_compress_level must be an integer between 0 and 9"
):
validate_save_inputs(images, "PNG", 90, -1)
with pytest.raises(ValueError, match="png_compress_level must be an integer between 0 and 9"):
with pytest.raises(
ValueError, match="png_compress_level must be an integer between 0 and 9"
):
validate_save_inputs(images, "PNG", 90, 10)
def test_save_image_with_format_png(self):
+24 -8
View File
@@ -56,9 +56,13 @@ class TestDimensionExtraction:
with pytest.raises(ValueError, match="Either image or latent must be provided"):
extract_dimensions()
def test_extract_dimensions_both_inputs_prefers_image(self, mock_image_tensor, mock_latent_tensor):
def test_extract_dimensions_both_inputs_prefers_image(
self, mock_image_tensor, mock_latent_tensor
):
"""Test that when both inputs provided, image takes precedence"""
width, height = extract_dimensions(image=mock_image_tensor, latent=mock_latent_tensor)
width, height = extract_dimensions(
image=mock_image_tensor, latent=mock_latent_tensor
)
# Should return image dimensions, not latent
assert width == 832
@@ -89,7 +93,9 @@ class TestScaledDimensionsCalculation:
original_width, original_height = 832, 1216
scale_factor = 1.5
new_width, new_height = calculate_scaled_dimensions(original_width, original_height, scale_factor)
new_width, new_height = calculate_scaled_dimensions(
original_width, original_height, scale_factor
)
# Check aspect ratio is preserved (within floating point precision)
original_ratio = original_width / original_height
@@ -101,7 +107,9 @@ class TestScaledDimensionsCalculation:
base_width, base_height = 1024, 1024
for scale_factor in sample_scale_factors:
width, height = calculate_scaled_dimensions(base_width, base_height, scale_factor)
width, height = calculate_scaled_dimensions(
base_width, base_height, scale_factor
)
expected_width = int(base_width * scale_factor)
expected_height = int(base_height * scale_factor)
@@ -205,7 +213,9 @@ class TestResolutionCalculatorNode:
"""Test node calculation with IMAGE input"""
node = ResolutionCalculatorNode()
width, height = node.calculate_resolution(scale_factor=2.0, image=mock_image_tensor)
width, height = node.calculate_resolution(
scale_factor=2.0, image=mock_image_tensor
)
# Original: 832x1216, 2x scale = 1664x2432
assert isinstance(width, int)
@@ -220,7 +230,9 @@ class TestResolutionCalculatorNode:
"""Test node calculation with LATENT input"""
node = ResolutionCalculatorNode()
width, height = node.calculate_resolution(scale_factor=1.5, latent=mock_latent_tensor)
width, height = node.calculate_resolution(
scale_factor=1.5, latent=mock_latent_tensor
)
# Original: 832x1216, 1.5x scale = 1248x1824
assert isinstance(width, int)
@@ -238,12 +250,16 @@ class TestResolutionCalculatorNode:
with pytest.raises(ValueError):
node.calculate_resolution(scale_factor=2.0)
def test_calculate_resolution_with_various_scale_factors(self, mock_image_tensor_square, sample_scale_factors):
def test_calculate_resolution_with_various_scale_factors(
self, mock_image_tensor_square, sample_scale_factors
):
"""Test calculation with various scale factors"""
node = ResolutionCalculatorNode()
for scale_factor in sample_scale_factors:
width, height = node.calculate_resolution(scale_factor=scale_factor, image=mock_image_tensor_square)
width, height = node.calculate_resolution(
scale_factor=scale_factor, image=mock_image_tensor_square
)
# All results should be integers divisible by 8
assert isinstance(width, int)
+3 -1
View File
@@ -331,7 +331,9 @@ class TestSamplerComboIntegration:
# Test that recommendations work with the node
for scheduler in suggestions[:2]: # Test first 2 suggestions
result = node.get_sampler_combo(sampler, scheduler, steps_rec["default"], cfg_rec["default"])
result = node.get_sampler_combo(
sampler, scheduler, steps_rec["default"], cfg_rec["default"]
)
assert result[0] == sampler
assert result[1] == scheduler
assert result[2] == steps_rec["default"]
+42 -14
View File
@@ -70,7 +70,9 @@ class TestWidthHeightSelectorNode:
# Test formatted preset if available
formatted_preset = "832×1216 - 13:19 (1.0MP) - SDXL"
result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512)
result = self.node.get_dimensions(
preset=formatted_preset, width=512, height=512
)
assert result == (832, 1216)
def test_sdxl_landscape_preset(self):
@@ -81,7 +83,9 @@ class TestWidthHeightSelectorNode:
# Test formatted preset if available
formatted_preset = "1216×832 - 19:13 (1.0MP) - SDXL"
result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512)
result = self.node.get_dimensions(
preset=formatted_preset, width=512, height=512
)
assert result == (1216, 832)
def test_flux_preset(self):
@@ -92,7 +96,9 @@ class TestWidthHeightSelectorNode:
# Test formatted preset
formatted_preset = "1920×1080 - 16:9 (2.1MP) - FLUX"
result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512)
result = self.node.get_dimensions(
preset=formatted_preset, width=512, height=512
)
assert result == (1920, 1080)
def test_ultra_wide_preset(self):
@@ -103,7 +109,9 @@ class TestWidthHeightSelectorNode:
# Test formatted preset if available
formatted_preset = "2560×1080 - 64:27 (2.8MP) - Ultra-Wide"
result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512)
result = self.node.get_dimensions(
preset=formatted_preset, width=512, height=512
)
assert result == (2560, 1080)
def test_all_presets_available(self):
@@ -135,7 +143,9 @@ class TestWidthHeightSelectorNode:
def test_invalid_preset_fallback(self):
"""Test handling of invalid preset."""
# Should fall back to custom dimensions
result = self.node.get_dimensions(preset="invalid_preset", width=800, height=600)
result = self.node.get_dimensions(
preset="invalid_preset", width=800, height=600
)
assert result == (800, 600)
@@ -258,14 +268,18 @@ class TestPresetDefinitions:
for preset_dict in [SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS]:
for preset_name, (width, height) in preset_dict.items():
assert width % 8 == 0, f"{preset_name} width {width} not divisible by 8"
assert height % 8 == 0, f"{preset_name} height {height} not divisible by 8"
assert (
height % 8 == 0
), f"{preset_name} height {height} not divisible by 8"
def test_preset_dimensions_within_limits(self):
"""Test that all preset dimensions are within acceptable limits."""
for preset_dict in [SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS]:
for preset_name, (width, height) in preset_dict.items():
assert 64 <= width <= 8192, f"{preset_name} width {width} out of range"
assert 64 <= height <= 8192, f"{preset_name} height {height} out of range"
assert (
64 <= height <= 8192
), f"{preset_name} height {height} out of range"
class TestEdgeCases:
@@ -467,7 +481,9 @@ class TestFormattedPresets:
for formatted_preset, expected in test_cases:
result = self.node._extract_preset_name(formatted_preset)
assert result == expected, f"Expected {expected}, got {result} for input {formatted_preset}"
assert (
result == expected
), f"Expected {expected}, got {result} for input {formatted_preset}"
def test_formatted_preset_dimensions(self):
"""Test that formatted presets return correct dimensions."""
@@ -509,22 +525,34 @@ class TestFormattedPresets:
def test_formatted_preset_metadata_accuracy(self):
"""Test that formatted presets contain accurate metadata."""
input_types = self.node.INPUT_TYPES()
formatted_presets = [opt for opt in input_types["required"]["preset"][0] if " - " in opt]
formatted_presets = [
opt for opt in input_types["required"]["preset"][0] if " - " in opt
]
for formatted_preset in formatted_presets:
# Extract components
parts = formatted_preset.split(" - ")
assert len(parts) == 3, f"Formatted preset should have 3 parts: {formatted_preset}"
assert (
len(parts) == 3
), f"Formatted preset should have 3 parts: {formatted_preset}"
resolution = parts[0]
aspect_and_mp = parts[1]
model_group = parts[2]
# Verify resolution exists in metadata
assert resolution in PRESET_METADATA, f"Resolution {resolution} not in metadata"
assert (
resolution in PRESET_METADATA
), f"Resolution {resolution} not in metadata"
# Verify metadata matches format
metadata = PRESET_METADATA[resolution]
assert metadata.model_group == model_group, f"Model group mismatch for {resolution}"
assert metadata.aspect_ratio in aspect_and_mp, f"Aspect ratio not in {aspect_and_mp}"
assert f"{metadata.megapixels:.1f}MP" in aspect_and_mp, f"Megapixels not in {aspect_and_mp}"
assert (
metadata.model_group == model_group
), f"Model group mismatch for {resolution}"
assert (
metadata.aspect_ratio in aspect_and_mp
), f"Aspect ratio not in {aspect_and_mp}"
assert (
f"{metadata.megapixels:.1f}MP" in aspect_and_mp
), f"Megapixels not in {aspect_and_mp}"