Full refresh with additional resolutions, custom options and latent output.

This commit is contained in:
BRADSEC
2025-12-06 19:26:29 +10:00
parent 4542f192f6
commit bb79e38a8b
11 changed files with 846 additions and 85 deletions
+3
View File
@@ -119,3 +119,6 @@ venv.bak/
Thumbs.db
ehthumbs.db
Desktop.ini
# Claude Code files
.claude/
+53 -24
View File
@@ -1,43 +1,72 @@
# ResolutionSelector for ComfyUI
# Resolution Selector for ComfyUI
A custom node for Stable Diffusion [ComfyUI](https://github.com/comfyanonymous/ComfyUI) to enable easy selection of image resolutions for SDXL SD15 SD21
Custom node for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) providing easy resolution and empty latent selection for image generation workflows.
- Select base SDXL resolution, width and height are returned as `INT` values which can be connected to latent image inputs or other inputs such as the `CLIPTextEncodeSDXL` `width, height, target_width, target_height`.
- Resolution list based off what is currently being used in the [Fooocus SDXL Web UI](https://github.com/lllyasviel/Fooocus).
- If using older models such as SD 1.5 or SD 2.1 use the `base_adjustment` dropdown. This will reduce the returned width and height values to suit the selected model whilst maintaining the image aspect ratio.
![Node Preview](node_preview.png)
```terminal
# Example
SDXL base_resolution 1024x1024
SD21 adjustment returns 768x768
SD15 adjustment returns 512x512
```
## Features
### Resolution Selector
- **Model-optimised resolution presets** (portrait, landscape, square)
- **Empty latent output** for direct KSampler connection
- **Custom width/height inputs** with **independent multiplier and latent**
### Installation
## Installation
```
```bash
# Change to the directory you installed ComfyUI
cd pathTo/ComfyUI
# Change to the custom_nodes directory ie.
# Change to the custom_nodes directory
cd custom_nodes
```
```terminal
# Clone the repo into custom_nodes
git clone https://github.com/bradsec/ComfyUI_ResolutionSelector.git
# Restart ComfyUI
```
### Usage after install
`Add Node > utils > Resolution Selector`
![node_example](resolution_selector_node.png)
## Usage
#### SDXL hookup example
![sdxl_hookup](sdxl_hookup.png)
### Resolution Selector
**Add Node:** `Add Node > utils > Resolution Selector`
**Inputs:**
- `model` (dropdown) - Select your image generation model or "All" for all resolutions
- `resolution` (dropdown) - Choose from model-optimized presets with aspect ratios (e.g., "1920x1080 (16:9 Landscape)")
- `resolution_multiplier` (dropdown) - Multiply preset resolution (1x, 2x, 3x, 4x)
- `custom_width` (optional) - Override with custom width (0-4096, step 8)
- `custom_height` (optional) - Override with custom height (0-4096, step 8)
- `custom_multiplier` (optional) - Independent multiplier for custom dimensions (1x, 2x, 3x, 4x)
**Outputs:**
- `width` (INT) - Preset resolution width in pixels
- `height` (INT) - Preset resolution height in pixels
- `latent` (LATENT) - Empty latent tensor for preset resolution
- `custom_width` (INT) - Custom width (0 if not set)
- `custom_height` (INT) - Custom height (0 if not set)
- `custom_latent` (LATENT) - Empty latent tensor for custom resolution
**Example Workflows:**
**Basic preset with multiplier:**
```
Resolution Selector (model: SDXL, resolution: "1024x1024 (1:1 Square)", multiplier: 2x)
→ width: 2048, height: 2048
→ latent → KSampler
```
**Custom dimensions with independent multiplier:**
```
Resolution Selector (custom_width: 1024, custom_height: 768, custom_multiplier: 2x)
→ custom_width: 2048, custom_height: 1536
→ custom_latent → KSampler
```
**Using "All" model option:**
```
Resolution Selector (model: All, resolution: "1920x1080 (16:9 Landscape)")
→ Shows all 34 unique resolutions from all models
```
#### SD15 hookup example
![sd15_hookup](sd15_hookup.png)
+3 -1
View File
@@ -1,3 +1,5 @@
from .resolution_selector import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

+3 -3
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_resolutionselector"
description = "A custom node for Stable Diffusion ComfyUI to enable easy selection of image resolutions for SDXL SD15 SD21"
version = "1.0.0"
name = "comfyui_resolutionselector""
description = "Custom node for ComfyUI providing easy resolution and empty latent selection for image generation workflows."
version = "2.0.0"
license = "MIT"
[project.urls]
+397 -57
View File
@@ -1,91 +1,431 @@
# Resolution presets
BASE_RESOLUTIONS = [
(1024, 1024),
(704, 1408),
(704, 1344),
(768, 1344),
(768, 1280),
(832, 1216),
(832, 1152),
(896, 1152),
(896, 1088),
(960, 1088),
(960, 1024),
(1024, 960),
(1088, 960),
(1088, 896),
(1152, 896),
(1152, 832),
(1216, 832),
(1280, 768),
(1344, 768),
(1344, 704),
(1408, 704),
(1472, 704),
(1536, 640),
(1600, 640),
(1664, 576),
(1728, 576)
]
import torch
try:
import comfy.model_management
COMFY_AVAILABLE = True
except ImportError:
COMFY_AVAILABLE = False
# Model-specific resolution presets with constraints
MODEL_RESOLUTIONS = {
"Flux": {
"square": [(512, 512), (768, 768), (1024, 1024), (1280, 1280), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(768, 1344), (832, 1216), (896, 1152), (928, 1664), (1024, 1536), (1024, 1792), (1080, 1920), (1152, 2048)],
"landscape": [(1344, 768), (1216, 832), (1152, 896), (1536, 1024), (1664, 928), (1792, 1024), (1920, 1080), (2048, 1152)],
"constraints": {"divisible_by": 16, "min": 256, "max": 2048}
},
"Qwen Image": {
"square": [(1024, 1024), (1280, 1280), (1328, 1328), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(928, 1664), (1024, 1536), (1080, 1920), (1140, 1472), (1152, 2048)],
"landscape": [(1472, 1140), (1536, 1024), (1664, 928), (1920, 1080), (2048, 1152)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
},
"Z-Image": {
"square": [(512, 512), (768, 768), (1024, 1024), (1280, 1280), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(720, 1280), (768, 1024), (1080, 1920), (1152, 2048)],
"landscape": [(1024, 768), (1280, 720), (1920, 1080), (2048, 1152)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
},
"SD 1.5": {
"square": [(512, 512), (768, 768), (1024, 1024), (1280, 1280), (1536, 1536)],
"portrait": [(512, 768), (512, 682), (512, 1024), (768, 1024), (768, 1344), (1080, 1920)],
"landscape": [(768, 512), (1024, 512), (1024, 768), (1344, 768), (1536, 512), (1920, 1080)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
},
"SDXL": {
"square": [(1024, 1024), (1280, 1280), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(640, 1536), (768, 1344), (832, 1216), (896, 1152), (1024, 1536), (1080, 1920), (1152, 2048)],
"landscape": [(1152, 896), (1216, 832), (1344, 768), (1536, 640), (1536, 1024), (1920, 1080), (2048, 1152)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
}
}
def gcd(a, b):
"""
Calculate greatest common divisor using Euclidean algorithm.
Args:
a (int): First number
b (int): Second number
Returns:
int: Greatest common divisor
"""
while b != 0:
a, b = b, a % b
return a
def calculate_aspect_ratio(width, height):
"""
Calculate simplified aspect ratio from dimensions, rounding to common ratios.
Args:
width (int): Width in pixels
height (int): Height in pixels
Returns:
str: Aspect ratio like "16:9" or "1:1"
"""
# Calculate actual ratio as decimal
actual_ratio = width / height
# Common aspect ratios (ratio_value, "width:height" string)
common_ratios = [
(1.0, "1:1"), # Square
(1.25, "5:4"), # 1.25
(1.33, "4:3"), # 1.333...
(1.5, "3:2"), # 1.5
(1.6, "16:10"), # 1.6
(1.78, "16:9"), # 1.777...
(2.0, "2:1"), # 2.0
(2.35, "21:9"), # 2.333... (ultrawide)
(2.4, "12:5"), # 2.4
(3.0, "3:1"), # 3.0
# Portrait ratios
(0.75, "3:4"), # 0.75
(0.67, "2:3"), # 0.666...
(0.625, "5:8"), # 0.625
(0.56, "9:16"), # 0.5625
(0.5, "1:2"), # 0.5
(0.42, "5:12"), # 0.4166...
]
# Find closest common ratio (within 5% tolerance)
tolerance = 0.05
for ratio_value, ratio_str in common_ratios:
if abs(actual_ratio - ratio_value) / ratio_value < tolerance:
return ratio_str
# If no close match, calculate GCD-based ratio
divisor = gcd(width, height)
ratio_width = width // divisor
ratio_height = height // divisor
return f"{ratio_width}:{ratio_height}"
def format_resolution(width, height):
"""
Format resolution tuple into display string with aspect ratio and orientation.
Uses fixed-width formatting for better alignment in dropdowns.
Args:
width (int): Width in pixels
height (int): Height in pixels
Returns:
str: Formatted string like "1920x1080 (16:9 Landscape)" with consistent spacing
"""
aspect_ratio = calculate_aspect_ratio(width, height)
if width == height:
orientation = "Square"
elif width < height:
orientation = "Portrait"
else:
orientation = "Landscape"
# Format with fixed width for better alignment (e.g., "1920x1080 ")
# Most resolutions are 4 digits, so we pad to 9 characters (4x4 + 'x')
resolution_str = f"{width}x{height}"
padded_resolution = resolution_str.ljust(13) # Pad to 13 chars for alignment
return f"{padded_resolution}({aspect_ratio} {orientation})"
def get_resolution_list(model_name):
"""
Generate ordered list of resolution strings for a specific model.
Args:
model_name (str): Name of the model (or "All" for all unique resolutions)
Returns:
list: Formatted resolution strings in order: square, portrait, landscape
"""
if model_name == "All":
return get_all_resolutions()
if model_name not in MODEL_RESOLUTIONS:
return []
model_data = MODEL_RESOLUTIONS[model_name]
resolutions = []
# Order: square first, then portrait, then landscape
for category in ["square", "portrait", "landscape"]:
for width, height in model_data[category]:
resolutions.append(format_resolution(width, height))
return resolutions
def get_default_resolution(model_name):
"""
Get the default/native resolution for a specific model.
Args:
model_name (str): Name of the model
Returns:
str: Formatted resolution string for the model's native resolution
"""
# Model-specific native/optimal resolutions
default_resolutions = {
"Flux": (1024, 1024), # Flux native
"Qwen Image": (1328, 1328), # Qwen native
"Z-Image": (1024, 1024), # Z-Image native
"SD 1.5": (512, 512), # SD 1.5 native
"SDXL": (1024, 1024), # SDXL native
"All": (1024, 1024), # Default for "All"
}
if model_name in default_resolutions:
width, height = default_resolutions[model_name]
return format_resolution(width, height)
# Fallback to 1024x1024
return format_resolution(1024, 1024)
def get_all_resolutions():
"""
Get all unique resolution strings across all models, sorted by dimensions.
Returns:
list: All unique resolution strings sorted by total pixels
"""
unique_resolutions = {}
# Collect all unique width×height pairs
for model_name in MODEL_RESOLUTIONS.keys():
model_data = MODEL_RESOLUTIONS[model_name]
for category in ["square", "portrait", "landscape"]:
for width, height in model_data[category]:
key = (width, height)
if key not in unique_resolutions:
unique_resolutions[key] = format_resolution(width, height)
# Sort by total pixels, then by width
sorted_resolutions = sorted(
unique_resolutions.items(),
key=lambda item: (item[0][0] * item[0][1], item[0][0])
)
return [res_str for _, res_str in sorted_resolutions]
def parse_resolution_string(resolution_str):
"""
Parse formatted resolution string back to width, height integers.
Args:
resolution_str (str): Format "1920x1080 (16:9 Landscape)" with possible padding
Returns:
tuple: (width, height)
Raises:
ValueError: If string format is invalid
"""
try:
# Format: "1920x1080 (16:9 Landscape)" - may have padding spaces
# Extract the dimension part (before the opening parenthesis)
dimension_part = resolution_str.split("(")[0].strip()
if not dimension_part or 'x' not in dimension_part:
raise ValueError(f"No dimension part found in: {resolution_str}")
# Split on 'x' and convert to integers
width, height = map(int, dimension_part.split("x"))
return (width, height)
except (ValueError, IndexError) as e:
raise ValueError(f"Invalid resolution format: {resolution_str}")
class ResolutionSelector:
"""
A node to provide a drop-down list of resolutions and returns two int values (width and height).
Enhanced resolution selector supporting multiple image generation models.
Provides model-specific resolution presets, custom dimension inputs, and empty latent output.
"""
def __init__(self):
pass
"""Initialize device for latent tensor generation."""
if COMFY_AVAILABLE:
self.device = comfy.model_management.intermediate_device()
else:
self.device = torch.device("cpu")
@classmethod
def INPUT_TYPES(cls):
"""
Return a dictionary which contains config for all input fields.
"""
# Create a list of resolution strings for the drop-down menu
resolution_strings = [
f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
Returns:
dict: Input configuration with required and optional fields
"""
model_list = ["All"] + list(MODEL_RESOLUTIONS.keys())
# Get all possible resolutions across all models (JavaScript will filter dynamically)
all_resolutions = get_all_resolutions()
return {
"required": {
"base_resolution": (resolution_strings,),
"base_adjustment": (["SDXL (None)", "SD21 (75%)", "SD15 (50%)"],),
"model": (model_list, {
"default": "SDXL"
}),
"resolution": (all_resolutions, {
"default": "1024x1024 (1:1 Square)"
}),
"resolution_multiplier": (["1x", "2x", "3x", "4x"], {
"default": "1x"
}),
},
"optional": {
"custom_width": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 8,
"display": "number"
}),
"custom_height": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 8,
"display": "number"
}),
"custom_multiplier": (["1x", "2x", "3x", "4x"], {
"default": "1x"
}),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
RETURN_TYPES = ("INT", "INT", "LATENT", "INT", "INT", "LATENT")
RETURN_NAMES = ("width", "height", "latent", "custom_width", "custom_height", "custom_latent")
FUNCTION = "select_resolution"
CATEGORY = 'utils'
CATEGORY = "utils"
def select_resolution(self, base_resolution, base_adjustment):
def select_resolution(self, model, resolution, resolution_multiplier="1x", custom_width=0, custom_height=0, custom_multiplier="1x"):
"""
Returns the width and height based on the selected resolution and adjustment.
Select and validate resolution, generate outputs.
Args:
base_resolution (str): Selected resolution in the format "width x height".
base_adjustment (str): Selected adjustment (resolution value reduction) based on SD version.
model (str): Selected model name
resolution (str): Selected preset resolution string
resolution_multiplier (str): Multiplier for resolution (1x-4x)
custom_width (int, optional): Custom width override
custom_height (int, optional): Custom height override
custom_multiplier (str, optional): Multiplier for custom dimensions (1x-4x)
Returns:
Tuple[int, int]: Adjusted width and height.
tuple: (width: int, height: int, latent: dict, custom_width: int, custom_height: int, custom_latent: dict)
"""
try:
width, height = map(int, base_resolution.split(' x '))
except ValueError:
raise ValueError("Invalid base_resolution format.")
# Parse multipliers (e.g., "2x" -> 2)
multiplier = int(resolution_multiplier.replace("x", ""))
custom_mult = int(custom_multiplier.replace("x", ""))
adjustment_factors = {
"SDXL (None)": 1, "SD21 (75%)": 0.75, "SD15 (50%)": 0.5}
factor = adjustment_factors.get(base_adjustment)
# Parse preset resolution
width, height = parse_resolution_string(resolution)
if factor is None:
raise ValueError("Invalid base_adjustment value.")
# Apply multiplier to preset resolution
width *= multiplier
height *= multiplier
width = int(width * factor)
height = int(height * factor)
# Generate latent for preset resolution
latent = self._generate_empty_latent(width, height)
return width, height
# Determine final custom dimensions
if custom_width > 0 and custom_height > 0:
# Use custom dimensions with custom multiplier
final_custom_width = custom_width * custom_mult
final_custom_height = custom_height * custom_mult
# Validate against model constraints (if not "All" model)
if model != "All":
self._validate_dimensions(model, final_custom_width, final_custom_height)
# Generate custom latent
custom_latent = self._generate_empty_latent(final_custom_width, final_custom_height)
return (width, height, latent, final_custom_width, final_custom_height, custom_latent)
else:
# No custom dimensions, return zeros and minimal empty custom latent
custom_latent = self._generate_empty_latent(1, 1)
return (width, height, latent, 0, 0, custom_latent)
def _validate_dimensions(self, model, width, height):
"""
Validate dimensions against model-specific constraints.
Args:
model (str): Model name
width (int): Width in pixels
height (int): Height in pixels
Raises:
ValueError: If dimensions violate model constraints
"""
if model not in MODEL_RESOLUTIONS:
return
constraints = MODEL_RESOLUTIONS[model]["constraints"]
divisible_by = constraints.get("divisible_by", 8)
min_dim = constraints.get("min", 64)
max_dim = constraints.get("max", 4096)
# Check divisibility
if width % divisible_by != 0:
raise ValueError(
f"{model} requires width divisible by {divisible_by}. "
f"Got {width} (remainder: {width % divisible_by})"
)
if height % divisible_by != 0:
raise ValueError(
f"{model} requires height divisible by {divisible_by}. "
f"Got {height} (remainder: {height % divisible_by})"
)
# Check bounds
if width < min_dim or width > max_dim:
raise ValueError(
f"{model} requires width between {min_dim} and {max_dim}. Got {width}"
)
if height < min_dim or height > max_dim:
raise ValueError(
f"{model} requires height between {min_dim} and {max_dim}. Got {height}"
)
def _generate_empty_latent(self, width, height, batch_size=1):
"""
Generate empty latent tensor for VAE input.
Args:
width (int): Image width in pixels
height (int): Image height in pixels
batch_size (int): Number of latent samples (default: 1)
Returns:
dict: LATENT dict with 'samples' key containing empty tensor
"""
# Latent space is 1/8 the image dimensions for SD-based models
latent_width = width // 8
latent_height = height // 8
# Shape: [batch_size, channels=4, height//8, width//8]
latent_tensor = torch.zeros(
[batch_size, 4, latent_height, latent_width],
device=self.device
)
return {"samples": latent_tensor}
NODE_CLASS_MAPPINGS = {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Test script for ResolutionSelector enhancements
"""
import sys
sys.path.insert(0, '.')
# Mock torch module for testing without ComfyUI environment
class MockTorch:
class device:
def __init__(self, name):
self.name = name
sys.modules['torch'] = MockTorch()
sys.modules['comfy'] = type('module', (), {'model_management': None})()
from resolution_selector import (
gcd,
calculate_aspect_ratio,
format_resolution,
get_resolution_list,
get_all_resolutions,
parse_resolution_string,
MODEL_RESOLUTIONS
)
def test_gcd():
"""Test GCD function"""
print("Testing GCD function:")
assert gcd(1920, 1080) == 120, "GCD of 1920, 1080 should be 120"
assert gcd(1024, 1024) == 1024, "GCD of 1024, 1024 should be 1024"
assert gcd(16, 9) == 1, "GCD of 16, 9 should be 1"
print(" ✓ GCD tests passed")
def test_aspect_ratio():
"""Test aspect ratio calculation"""
print("\nTesting aspect ratio calculation:")
assert calculate_aspect_ratio(1920, 1080) == "16:9", "1920x1080 should be 16:9"
assert calculate_aspect_ratio(1024, 1024) == "1:1", "1024x1024 should be 1:1"
assert calculate_aspect_ratio(1280, 720) == "16:9", "1280x720 should be 16:9"
assert calculate_aspect_ratio(1536, 1024) == "3:2", "1536x1024 should be 3:2"
print(" ✓ Aspect ratio tests passed")
def test_format_resolution():
"""Test resolution formatting"""
print("\nTesting resolution formatting:")
result = format_resolution(1920, 1080)
print(f" 1920x1080 → '{result}'")
# With padding, should be "1920x1080 (16:9 Landscape)" - 13 chars total for resolution part
assert "(16:9 Landscape)" in result, f"Should contain aspect ratio and orientation"
assert result.startswith("1920x1080"), f"Should start with resolution"
result = format_resolution(1024, 1024)
print(f" 1024x1024 → '{result}'")
assert "(1:1 Square)" in result, f"Should contain aspect ratio and orientation"
assert result.startswith("1024x1024"), f"Should start with resolution"
result = format_resolution(1080, 1920)
print(f" 1080x1920 → '{result}'")
assert "(9:16 Portrait)" in result, f"Should contain aspect ratio and orientation"
assert result.startswith("1080x1920"), f"Should start with resolution"
print(" ✓ Format resolution tests passed")
def test_parse_resolution():
"""Test resolution string parsing"""
print("\nTesting resolution string parsing:")
# Test with padding (as it will be in the actual dropdown)
width, height = parse_resolution_string("1920x1080 (16:9 Landscape)")
assert width == 1920 and height == 1080, "Should parse 1920x1080 with padding"
width, height = parse_resolution_string("1024x1024 (1:1 Square)")
assert width == 1024 and height == 1024, "Should parse 1024x1024 with padding"
# Test without padding (backward compatibility)
width, height = parse_resolution_string("1920x1080 (16:9 Landscape)")
assert width == 1920 and height == 1080, "Should parse 1920x1080 without padding"
print(" ✓ Parse resolution tests passed")
def test_model_resolutions():
"""Test model resolution lists"""
print("\nTesting model resolution lists:")
# Test individual model
flux_res = get_resolution_list("Flux")
print(f" Flux has {len(flux_res)} resolutions")
assert len(flux_res) > 0, "Flux should have resolutions"
assert any("1920x1080" in r for r in flux_res), "Flux should have 1920x1080"
# Test All model
all_res = get_resolution_list("All")
print(f" 'All' has {len(all_res)} unique resolutions")
assert len(all_res) > len(flux_res), "'All' should have more resolutions than individual models"
# Verify no duplicates in All
assert len(all_res) == len(set(all_res)), "'All' should have no duplicates"
print(" ✓ Model resolution tests passed")
def test_new_resolutions():
"""Test that new resolutions were added"""
print("\nTesting new resolutions up to 1920x1080:")
for model_name, model_data in MODEL_RESOLUTIONS.items():
resolutions = get_resolution_list(model_name)
has_1080p = any("1920x1080" in r or "1080x1920" in r for r in resolutions)
print(f" {model_name}: {len(resolutions)} resolutions, has 1080p: {has_1080p}")
print(" ✓ New resolutions verified")
def test_all_resolutions_unique():
"""Test that 'All' model returns unique resolutions"""
print("\nTesting 'All' model uniqueness:")
all_res = get_all_resolutions()
# Count occurrences
dimensions = []
for res in all_res:
width, height = parse_resolution_string(res)
dimensions.append((width, height))
# Check for duplicates
unique_dimensions = set(dimensions)
assert len(dimensions) == len(unique_dimensions), "Should have no duplicate dimensions"
print(f" ✓ All {len(all_res)} resolutions are unique")
if __name__ == "__main__":
print("=" * 60)
print("ResolutionSelector Enhancement Tests")
print("=" * 60)
try:
test_gcd()
test_aspect_ratio()
test_format_resolution()
test_parse_resolution()
test_model_resolutions()
test_new_resolutions()
test_all_resolutions_unique()
print("\n" + "=" * 60)
print("✓ ALL TESTS PASSED!")
print("=" * 60)
except AssertionError as e:
print(f"\n✗ TEST FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"\n✗ ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+236
View File
@@ -0,0 +1,236 @@
import { app } from "/scripts/app.js";
const MODEL_RESOLUTIONS = {
"Flux": {
"square": [[512, 512], [768, 768], [1024, 1024], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[768, 1344], [832, 1216], [896, 1152], [928, 1664], [1024, 1536], [1024, 1792], [1080, 1920], [1152, 2048]],
"landscape": [[1344, 768], [1216, 832], [1152, 896], [1536, 1024], [1664, 928], [1792, 1024], [1920, 1080], [2048, 1152]],
},
"Qwen Image": {
"square": [[1024, 1024], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[928, 1664], [1024, 1536], [1080, 1920], [1140, 1472], [1152, 2048]],
"landscape": [[1472, 1140], [1536, 1024], [1664, 928], [1920, 1080], [2048, 1152]],
},
"Z-Image": {
"square": [[512, 512], [768, 768], [1024, 1024], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[720, 1280], [768, 1024], [1080, 1920], [1152, 2048]],
"landscape": [[1024, 768], [1280, 720], [1920, 1080], [2048, 1152]],
},
"SD 1.5": {
"square": [[512, 512], [768, 768], [1024, 1024], [1280, 1280], [1536, 1536]],
"portrait": [[512, 768], [512, 682], [512, 1024], [768, 1024], [768, 1344], [1080, 1920]],
"landscape": [[768, 512], [1024, 512], [1024, 768], [1344, 768], [1536, 512], [1920, 1080]],
},
"SDXL": {
"square": [[1024, 1024], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[640, 1536], [768, 1344], [832, 1216], [896, 1152], [1024, 1536], [1080, 1920], [1152, 2048]],
"landscape": [[1152, 896], [1216, 832], [1344, 768], [1536, 640], [1536, 1024], [1920, 1080], [2048, 1152]],
}
};
function gcd(a, b) {
while (b !== 0) {
const temp = b;
b = a % b;
a = temp;
}
return a;
}
function calculateAspectRatio(width, height) {
// Calculate actual ratio as decimal
const actualRatio = width / height;
// Common aspect ratios [ratio_value, "width:height" string]
const commonRatios = [
[1.0, "1:1"], // Square
[1.25, "5:4"], // 1.25
[1.33, "4:3"], // 1.333...
[1.5, "3:2"], // 1.5
[1.6, "16:10"], // 1.6
[1.78, "16:9"], // 1.777...
[2.0, "2:1"], // 2.0
[2.35, "21:9"], // 2.333... (ultrawide)
[2.4, "12:5"], // 2.4
[3.0, "3:1"], // 3.0
// Portrait ratios
[0.75, "3:4"], // 0.75
[0.67, "2:3"], // 0.666...
[0.625, "5:8"], // 0.625
[0.56, "9:16"], // 0.5625
[0.5, "1:2"], // 0.5
[0.42, "5:12"], // 0.4166...
];
// Find closest common ratio (within 5% tolerance)
const tolerance = 0.05;
for (const [ratioValue, ratioStr] of commonRatios) {
if (Math.abs(actualRatio - ratioValue) / ratioValue < tolerance) {
return ratioStr;
}
}
// If no close match, calculate GCD-based ratio
const divisor = gcd(width, height);
const ratioWidth = width / divisor;
const ratioHeight = height / divisor;
return `${ratioWidth}:${ratioHeight}`;
}
function formatResolution(width, height) {
const aspectRatio = calculateAspectRatio(width, height);
let orientation;
if (width === height) {
orientation = "Square";
} else if (width < height) {
orientation = "Portrait";
} else {
orientation = "Landscape";
}
// Format with fixed width for better alignment
const resolutionStr = `${width}x${height}`;
const paddedResolution = resolutionStr.padEnd(13, ' '); // Pad to 13 chars for alignment
return `${paddedResolution}(${aspectRatio} ${orientation})`;
}
function getAllResolutions() {
const uniqueResolutions = new Map();
// Collect all unique width×height pairs
for (const modelName in MODEL_RESOLUTIONS) {
const modelData = MODEL_RESOLUTIONS[modelName];
for (const category of ["square", "portrait", "landscape"]) {
if (modelData[category]) {
for (const [w, h] of modelData[category]) {
const key = `${w}x${h}`;
if (!uniqueResolutions.has(key)) {
uniqueResolutions.set(key, { width: w, height: h, pixels: w * h });
}
}
}
}
}
// Sort by total pixels, then by width
const sorted = Array.from(uniqueResolutions.values())
.sort((a, b) => {
if (a.pixels !== b.pixels) return a.pixels - b.pixels;
return a.width - b.width;
});
return sorted.map(({ width, height }) => formatResolution(width, height));
}
function getResolutionsForModel(model) {
if (model === "All") {
return getAllResolutions();
}
const modelData = MODEL_RESOLUTIONS[model];
if (!modelData) return [];
const resolutions = [];
for (const category of ["square", "portrait", "landscape"]) {
if (modelData[category]) {
for (const [w, h] of modelData[category]) {
resolutions.push(formatResolution(w, h));
}
}
}
return resolutions;
}
function getDefaultResolution(model) {
// Model-specific native/optimal resolutions
const defaultResolutions = {
"Flux": [1024, 1024],
"Qwen Image": [1328, 1328],
"Z-Image": [1024, 1024],
"SD 1.5": [512, 512],
"SDXL": [1024, 1024],
"All": [1024, 1024]
};
if (defaultResolutions[model]) {
const [w, h] = defaultResolutions[model];
return formatResolution(w, h);
}
// Fallback
return formatResolution(1024, 1024);
}
app.registerExtension({
name: "ResolutionSelector.DynamicDropdown",
async nodeCreated(node) {
if (node.comfyClass !== "ResolutionSelector") return;
// Set wider default node width to prevent text cutoff
node.setSize([400, node.size[1]]);
const modelWidget = node.widgets.find(w => w.name === "model");
const resolutionWidget = node.widgets.find(w => w.name === "resolution");
if (!modelWidget || !resolutionWidget) {
console.error("ResolutionSelector: Required widgets not found");
return;
}
const origCallback = modelWidget.callback;
const updateResolutions = (modelValue) => {
const resolutions = getResolutionsForModel(modelValue);
if (resolutions.length === 0) {
console.warn(`ResolutionSelector: No resolutions found for model ${modelValue}`);
return;
}
resolutionWidget.options.values = resolutions;
// Set to model-specific default resolution
const defaultRes = getDefaultResolution(modelValue);
if (resolutions.includes(defaultRes)) {
resolutionWidget.value = defaultRes;
} else if (!resolutions.includes(resolutionWidget.value)) {
// Fallback to first resolution if default not found
resolutionWidget.value = resolutions[0];
}
node.setDirtyCanvas(true, true);
};
modelWidget.callback = function(value) {
if (origCallback) {
origCallback.apply(this, arguments);
}
updateResolutions(value);
};
setTimeout(() => {
updateResolutions(modelWidget.value);
}, 10);
},
async loadedGraphNode(node) {
if (node.comfyClass !== "ResolutionSelector") return;
const modelWidget = node.widgets.find(w => w.name === "model");
if (modelWidget) {
const resolutions = getResolutionsForModel(modelWidget.value);
const resolutionWidget = node.widgets.find(w => w.name === "resolution");
if (resolutionWidget && resolutions.length > 0) {
resolutionWidget.options.values = resolutions;
if (!resolutions.includes(resolutionWidget.value)) {
resolutionWidget.value = resolutions[0];
}
}
}
}
});