diff --git a/.gitignore b/.gitignore index 7a60b85..d1355f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ *.pyc +example/ diff --git a/example/comfyui_resolutionselectorplus/.github/workflows/publish.yml b/example/comfyui_resolutionselectorplus/.github/workflows/publish.yml deleted file mode 100644 index 665bf07..0000000 --- a/example/comfyui_resolutionselectorplus/.github/workflows/publish.yml +++ /dev/null @@ -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 }} diff --git a/example/comfyui_resolutionselectorplus/.gitignore b/example/comfyui_resolutionselectorplus/.gitignore deleted file mode 100644 index b6b3ddf..0000000 --- a/example/comfyui_resolutionselectorplus/.gitignore +++ /dev/null @@ -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 diff --git a/example/comfyui_resolutionselectorplus/.tracking b/example/comfyui_resolutionselectorplus/.tracking deleted file mode 100644 index b3a38b6..0000000 --- a/example/comfyui_resolutionselectorplus/.tracking +++ /dev/null @@ -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 \ No newline at end of file diff --git a/example/comfyui_resolutionselectorplus/LICENSE b/example/comfyui_resolutionselectorplus/LICENSE deleted file mode 100644 index 0074bf8..0000000 --- a/example/comfyui_resolutionselectorplus/LICENSE +++ /dev/null @@ -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. diff --git a/example/comfyui_resolutionselectorplus/README.md b/example/comfyui_resolutionselectorplus/README.md deleted file mode 100644 index f3b7746..0000000 --- a/example/comfyui_resolutionselectorplus/README.md +++ /dev/null @@ -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 -``` - diff --git a/example/comfyui_resolutionselectorplus/__init__.py b/example/comfyui_resolutionselectorplus/__init__.py deleted file mode 100644 index 1210d0b..0000000 --- a/example/comfyui_resolutionselectorplus/__init__.py +++ /dev/null @@ -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'] \ No newline at end of file diff --git a/example/comfyui_resolutionselectorplus/node_preview.png b/example/comfyui_resolutionselectorplus/node_preview.png deleted file mode 100644 index 2d9b981..0000000 Binary files a/example/comfyui_resolutionselectorplus/node_preview.png and /dev/null differ diff --git a/example/comfyui_resolutionselectorplus/pyproject.toml b/example/comfyui_resolutionselectorplus/pyproject.toml deleted file mode 100644 index 53f72fc..0000000 --- a/example/comfyui_resolutionselectorplus/pyproject.toml +++ /dev/null @@ -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 = "" \ No newline at end of file diff --git a/example/comfyui_resolutionselectorplus/resolution_selector.py b/example/comfyui_resolutionselectorplus/resolution_selector.py deleted file mode 100644 index 4b338d2..0000000 --- a/example/comfyui_resolutionselectorplus/resolution_selector.py +++ /dev/null @@ -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", -} diff --git a/example/comfyui_resolutionselectorplus/test_resolution_selector.py b/example/comfyui_resolutionselectorplus/test_resolution_selector.py deleted file mode 100644 index a085d49..0000000 --- a/example/comfyui_resolutionselectorplus/test_resolution_selector.py +++ /dev/null @@ -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) diff --git a/example/comfyui_resolutionselectorplus/web/js/resolution_selector.js b/example/comfyui_resolutionselectorplus/web/js/resolution_selector.js deleted file mode 100644 index 73770eb..0000000 --- a/example/comfyui_resolutionselectorplus/web/js/resolution_selector.js +++ /dev/null @@ -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]; - } - } - } - } -});