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:
+10
-6
@@ -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],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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__}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user