chore: exclude example folder from git

This commit is contained in:
Your Name
2026-01-20 10:54:55 +07:00
parent e07c3984b3
commit 52ff20923b
12 changed files with 1 additions and 1113 deletions
+1
View File
@@ -1,2 +1,3 @@
__pycache__/
*.pyc
example/
@@ -1,26 +0,0 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'bradsec' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
@@ -1,125 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.env
.build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.coverage
.tox/
.nox/
.coverage.*
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# poetry
poetry.lock
# vscode
.vscode/
.settings/
*.code-workspace
# Sublime Text
*.sublime-workspace
# SFTP configuration
.sftp-config.json
# mypy
.mypy_cache/
# Pyre type checker
.pyre/
# Pytest cache
__pytest_cache__/
pytestdebuglog.txt
# Rope project settings
.ropeproject
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# macOS
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Claude Code files
.claude/
CLAUDE.md
@@ -1,10 +0,0 @@
.github/workflows/publish.yml
.gitignore
LICENSE
README.md
__init__.py
node_preview.png
pyproject.toml
resolution_selector.py
test_resolution_selector.py
web/js/resolution_selector.js
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 BRADSEC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,74 +0,0 @@
# Resolution Selector Plus for ComfyUI
Custom node for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) providing easy resolution and empty latent selection for image generation workflows.
![Node Preview](node_preview.png)
## 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
```bash
# Change to the directory you installed ComfyUI
cd pathTo/ComfyUI
# Change to the custom_nodes directory
cd custom_nodes
# Clone the repo into custom_nodes
git clone https://github.com/bradsec/ComfyUI_ResolutionSelectorPlus.git
# Restart ComfyUI
```
## Usage
### Resolution Selector Plus
**Add Node:** `Add Node > utils > Resolution Selector Plus`
**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)
- `batch_size` (number) - Number of latent samples to generate (1-64, default: 1)
- `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)
- `custom_batch` (optional) - Number of latent samples for custom resolution (1-64, default: 1)
**Outputs:**
- `width` (INT) - Preset resolution width in pixels
- `height` (INT) - Preset resolution height in pixels
- `latent` (LATENT) - Empty latent tensor for preset resolution (batch_size samples)
- `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 (custom_batch samples)
**Example Workflows:**
**Basic preset with multiplier:**
```
Resolution Selector Plus (model: SDXL, resolution: "1024x1024 (1:1 Square)", multiplier: 2x)
→ width: 2048, height: 2048
→ latent → KSampler
```
**Custom dimensions with independent multiplier:**
```
Resolution Selector Plus (custom_width: 1024, custom_height: 768, custom_multiplier: 2x)
→ custom_width: 2048, custom_height: 1536
→ custom_latent → KSampler
```
**Using "All" model option:**
```
Resolution Selector Plus (model: All, resolution: "1920x1080 (16:9 Landscape)")
→ Shows all 74 unique resolutions from all models
```
@@ -1,5 +0,0 @@
from .resolution_selector import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

@@ -1,14 +0,0 @@
[project]
name = "comfyui_resolutionselectorplus"
description = "Custom node for ComfyUI providing easy resolution and empty latent selection for image generation workflows."
version = "2.0.5"
license = {file = "LICENSE"}
[project.urls]
Repository = "https://github.com/bradsec/ComfyUI_ResolutionSelectorPlus"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "bradsec"
DisplayName = "ComfyUI_ResolutionSelectorPlus"
Icon = ""
@@ -1,447 +0,0 @@
import torch
try:
import comfy.model_management
COMFY_AVAILABLE = True
except ImportError:
COMFY_AVAILABLE = False
# Model-specific resolution presets with constraints
# Includes: model-optimized sizes + photo print (4x6, 5x7, 8x10) + digital/social + canvas art ratios
MODEL_RESOLUTIONS = {
"Flux": {
"square": [(512, 512), (768, 768), (1024, 1024), (1088, 1088), (1280, 1280), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(688, 2048), (768, 1344), (832, 1216), (896, 1152), (928, 1664), (1024, 1536), (1024, 1792), (1024, 2048), (1088, 1920), (1152, 2048), (1200, 1792), (1360, 2048), (1456, 2048), (1536, 2048), (1616, 2048), (1632, 2048), (1712, 2048)],
"landscape": [(1280, 720), (1344, 768), (1216, 832), (1152, 896), (1536, 1024), (1664, 928), (1792, 1024), (1792, 1200), (1920, 1088), (2048, 688), (2048, 1024), (2048, 1152), (2048, 1360), (2048, 1456), (2048, 1536), (2048, 1616), (2048, 1632), (2048, 1712)],
"constraints": {"divisible_by": 16, "min": 256, "max": 2048}
},
"Qwen Image": {
"square": [(1024, 1024), (1080, 1080), (1280, 1280), (1328, 1328), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(680, 2048), (928, 1664), (1024, 1536), (1024, 2048), (1080, 1920), (1140, 1472), (1152, 2048), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)],
"landscape": [(1280, 720), (1472, 1140), (1536, 1024), (1664, 928), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1152), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
},
"Z-Image": {
"square": [(512, 512), (768, 768), (1024, 1024), (1080, 1080), (1280, 1280), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(680, 2048), (720, 1280), (768, 1024), (1024, 2048), (1080, 1920), (1152, 2048), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)],
"landscape": [(1024, 768), (1280, 720), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1152), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
},
"SD 1.5": {
"square": [(512, 512), (768, 768), (1024, 1024), (1080, 1080), (1280, 1280), (1536, 1536)],
"portrait": [(512, 768), (512, 682), (512, 1024), (680, 2048), (768, 1024), (768, 1344), (1024, 2048), (1080, 1920), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)],
"landscape": [(768, 512), (1024, 512), (1024, 768), (1280, 720), (1344, 768), (1536, 512), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)],
"constraints": {"divisible_by": 8, "min": 256, "max": 2048}
},
"SDXL": {
"square": [(1024, 1024), (1080, 1080), (1280, 1280), (1536, 1536), (1920, 1920), (2048, 2048)],
"portrait": [(640, 1536), (680, 2048), (768, 1344), (832, 1216), (896, 1152), (1024, 1536), (1024, 2048), (1080, 1920), (1152, 2048), (1200, 1800), (1368, 2048), (1464, 2048), (1536, 2048), (1608, 2048), (1640, 2048), (1704, 2048)],
"landscape": [(1152, 896), (1216, 832), (1280, 720), (1344, 768), (1536, 640), (1536, 1024), (1800, 1200), (1920, 1080), (2048, 680), (2048, 1024), (2048, 1152), (2048, 1368), (2048, 1464), (2048, 1536), (2048, 1608), (2048, 1640), (2048, 1704)],
"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, using nearest common ratio.
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...
(0.33, "1:3"), # 0.333... (panoramic)
]
# Find closest common ratio by absolute difference
closest_ratio = min(common_ratios, key=lambda r: abs(actual_ratio - r[0]))
return closest_ratio[1]
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:
"""
Enhanced resolution selector supporting multiple image generation models.
Provides model-specific resolution presets, custom dimension inputs, and empty latent output.
"""
def __init__(self):
"""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.
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": {
"model": (model_list, {
"default": "SDXL"
}),
"resolution": (all_resolutions, {
"default": "1024x1024 (1:1 Square)"
}),
"resolution_multiplier": (["1x", "2x", "3x", "4x"], {
"default": "1x"
}),
"batch_size": ("INT", {
"default": 1,
"min": 1,
"max": 64,
"step": 1,
"display": "number"
}),
},
"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"
}),
"custom_batch": ("INT", {
"default": 1,
"min": 1,
"max": 64,
"step": 1,
"display": "number"
}),
}
}
RETURN_TYPES = ("INT", "INT", "LATENT", "INT", "INT", "LATENT")
RETURN_NAMES = ("width", "height", "latent", "custom_width", "custom_height", "custom_latent")
FUNCTION = "select_resolution"
CATEGORY = "utils"
def select_resolution(self, model, resolution, resolution_multiplier="1x", batch_size=1, custom_width=0, custom_height=0, custom_multiplier="1x", custom_batch=1):
"""
Select and validate resolution, generate outputs.
Args:
model (str): Selected model name
resolution (str): Selected preset resolution string
resolution_multiplier (str): Multiplier for resolution (1x-4x)
batch_size (int): Number of latent samples for preset resolution (default: 1)
custom_width (int, optional): Custom width override
custom_height (int, optional): Custom height override
custom_multiplier (str, optional): Multiplier for custom dimensions (1x-4x)
custom_batch (int, optional): Number of latent samples for custom resolution (default: 1)
Returns:
tuple: (width: int, height: int, latent: dict, custom_width: int, custom_height: int, custom_latent: dict)
"""
# Parse multipliers (e.g., "2x" -> 2)
multiplier = int(resolution_multiplier.replace("x", ""))
custom_mult = int(custom_multiplier.replace("x", ""))
# Parse preset resolution
width, height = parse_resolution_string(resolution)
# Apply multiplier to preset resolution
width *= multiplier
height *= multiplier
# Generate latent for preset resolution with batch size
latent = self._generate_empty_latent(width, height, batch_size)
# 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 with custom batch size
custom_latent = self._generate_empty_latent(final_custom_width, final_custom_height, custom_batch)
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, 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 = {
"ResolutionSelector": ResolutionSelector,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ResolutionSelector": "Resolution Selector Plus",
}
@@ -1,152 +0,0 @@
#!/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"
# Flux uses 16-pixel divisibility, so 1920x1080 becomes 1920x1088
assert any("1920x1088" in r for r in flux_res), "Flux should have 1920x1088 (Full HD adapted to 16px divisibility)"
# 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)
@@ -1,239 +0,0 @@
import { app } from "/scripts/app.js";
// Model-specific resolution presets with constraints
// Includes: model-optimized sizes + photo print (4x6, 5x7, 8x10) + digital/social + canvas art ratios
const MODEL_RESOLUTIONS = {
"Flux": {
"square": [[512, 512], [768, 768], [1024, 1024], [1088, 1088], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[688, 2048], [768, 1344], [832, 1216], [896, 1152], [928, 1664], [1024, 1536], [1024, 1792], [1024, 2048], [1088, 1920], [1152, 2048], [1200, 1792], [1360, 2048], [1456, 2048], [1536, 2048], [1616, 2048], [1632, 2048], [1712, 2048]],
"landscape": [[1280, 720], [1344, 768], [1216, 832], [1152, 896], [1536, 1024], [1664, 928], [1792, 1024], [1792, 1200], [1920, 1088], [2048, 688], [2048, 1024], [2048, 1152], [2048, 1360], [2048, 1456], [2048, 1536], [2048, 1616], [2048, 1632], [2048, 1712]],
},
"Qwen Image": {
"square": [[1024, 1024], [1080, 1080], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[680, 2048], [928, 1664], [1024, 1536], [1024, 2048], [1080, 1920], [1140, 1472], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
"landscape": [[1280, 720], [1472, 1140], [1536, 1024], [1664, 928], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
},
"Z-Image": {
"square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[680, 2048], [720, 1280], [768, 1024], [1024, 2048], [1080, 1920], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
"landscape": [[1024, 768], [1280, 720], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
},
"SD 1.5": {
"square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536]],
"portrait": [[512, 768], [512, 682], [512, 1024], [680, 2048], [768, 1024], [768, 1344], [1024, 2048], [1080, 1920], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
"landscape": [[768, 512], [1024, 512], [1024, 768], [1280, 720], [1344, 768], [1536, 512], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
},
"SDXL": {
"square": [[1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
"portrait": [[640, 1536], [680, 2048], [768, 1344], [832, 1216], [896, 1152], [1024, 1536], [1024, 2048], [1080, 1920], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
"landscape": [[1152, 896], [1216, 832], [1280, 720], [1344, 768], [1536, 640], [1536, 1024], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
}
};
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...
[0.33, "1:3"], // 0.333... (panoramic)
];
// Find closest common ratio by absolute difference
let closestRatio = commonRatios[0];
let minDiff = Math.abs(actualRatio - closestRatio[0]);
for (const ratio of commonRatios) {
const diff = Math.abs(actualRatio - ratio[0]);
if (diff < minDiff) {
minDiff = diff;
closestRatio = ratio;
}
}
return closestRatio[1];
}
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];
}
}
}
}
});