From bc30806feefd4772f198b6daac2986987d599bda Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:38:05 -0700 Subject: [PATCH 01/10] test: Add tests for new tools and error handling --- .github/workflows/code-quality.yml | 61 ++++- .github/workflows/tests.yml | 352 ++++++++++++++++++++++++++--- 2 files changed, 375 insertions(+), 38 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index bd4e7a0..ba4418b 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -74,6 +74,19 @@ jobs: from kikotools.tools.resolution_calculator.logic import extract_dimensions from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode as NodeClass + # Test Width Height Selector imports + from kikotools.tools.width_height_selector import WidthHeightSelectorNode + from kikotools.tools.width_height_selector.logic import get_preset_dimensions + from kikotools.tools.width_height_selector.presets import PRESET_OPTIONS, PRESET_METADATA + + # Test Sampler Combo imports + from kikotools.tools.sampler_combo import SamplerComboNode + from kikotools.tools.sampler_combo.logic import get_sampler_combo, SAMPLERS, SCHEDULERS + + # Test Seed History imports + from kikotools.tools.seed_history import SeedHistoryNode + from kikotools.tools.seed_history.logic import generate_random_seed, validate_seed_value + print('✓ All module imports successful') " @@ -190,12 +203,56 @@ jobs: # Check that nodes have proper ComfyUI interface required_attrs = ['INPUT_TYPES', 'RETURN_TYPES', 'RETURN_NAMES', 'FUNCTION', 'CATEGORY'] + + # Test Resolution Calculator Node for attr in required_attrs: if not hasattr(ResolutionCalculatorNode, attr): - print(f'❌ Node missing required attribute: {attr}') + print(f'❌ ResolutionCalculatorNode missing required attribute: {attr}') sys.exit(1) - print('✓ All architecture checks passed') + # Test Width Height Selector Node + from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode + + if issubclass(WidthHeightSelectorNode, ComfyAssetsBaseNode): + print('✓ WidthHeightSelectorNode properly inherits from base class') + else: + print('❌ WidthHeightSelectorNode does not inherit from base class') + sys.exit(1) + + for attr in required_attrs: + if not hasattr(WidthHeightSelectorNode, attr): + print(f'❌ WidthHeightSelectorNode missing required attribute: {attr}') + sys.exit(1) + + # Test Sampler Combo Node + from kikotools.tools.sampler_combo.node import SamplerComboNode + + if issubclass(SamplerComboNode, ComfyAssetsBaseNode): + print('✓ SamplerComboNode properly inherits from base class') + else: + print('❌ SamplerComboNode does not inherit from base class') + sys.exit(1) + + for attr in required_attrs: + if not hasattr(SamplerComboNode, attr): + print(f'❌ SamplerComboNode missing required attribute: {attr}') + sys.exit(1) + + # Test Seed History Node + from kikotools.tools.seed_history.node import SeedHistoryNode + + if issubclass(SeedHistoryNode, ComfyAssetsBaseNode): + print('✓ SeedHistoryNode properly inherits from base class') + else: + print('❌ SeedHistoryNode does not inherit from base class') + sys.exit(1) + + for attr in required_attrs: + if not hasattr(SeedHistoryNode, attr): + print(f'❌ SeedHistoryNode missing required attribute: {attr}') + sys.exit(1) + + print('✓ All architecture checks passed for all tools') " - name: Check test coverage expectations diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eab24e9..cae7ec0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -78,67 +78,318 @@ jobs: print('🎉 All tests passed!') " - - name: Test error handling + - name: Test Width Height Selector run: | python -c " import sys import os sys.path.insert(0, os.getcwd()) - from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode + # Test Width Height Selector imports + from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode + from kikotools.tools.width_height_selector.presets import PRESET_OPTIONS, PRESET_METADATA + from kikotools.tools.width_height_selector.logic import get_preset_dimensions - node = ResolutionCalculatorNode() + print('✓ Width Height Selector imports successful') - # Test error handling - try: - node.calculate_resolution(2.0) # No input provided - assert False, 'Should have raised ValueError' - except ValueError: - print('✓ Error handling test passed') + # Test preset structure + assert len(PRESET_OPTIONS) > 0 + assert 'custom' in PRESET_OPTIONS + assert len(PRESET_METADATA) > 0 + print('✓ Preset structure tests passed') - # Test invalid scale factor - try: - node.calculate_resolution(0.0) # Invalid scale - assert False, 'Should have raised ValueError' - except ValueError: - print('✓ Scale factor validation test passed') + # Test node interface + node = WidthHeightSelectorNode() + input_types = node.INPUT_TYPES() + assert 'required' in input_types + assert 'preset' in input_types['required'] + assert 'width' in input_types['required'] + assert 'height' in input_types['required'] + print('✓ Node interface tests passed') - print('✓ All error handling tests passed') + # Test formatted presets + preset_options = input_types['required']['preset'][0] + assert 'custom' in preset_options + formatted_count = len([opt for opt in preset_options if ' - ' in opt and 'MP' in opt]) + assert formatted_count > 0 + print(f'✓ Found {formatted_count} formatted presets') + + # Test dimension calculation + result = node.get_dimensions('1024×1024', 512, 512) + assert result == (1024, 1024) + print('✓ Dimension calculation tests passed') + + # Test formatted preset dimensions + formatted_preset = '1024×1024 - 1:1 (1.1MP) - SDXL' + result = node.get_dimensions(formatted_preset, 512, 512) + assert result == (1024, 1024) + print('✓ Formatted preset tests passed') + + # Test preset extraction + extracted = node._extract_preset_name(formatted_preset) + assert extracted == '1024×1024' + print('✓ Preset extraction tests passed') + + print('🎉 All Width Height Selector tests passed!') " - - name: Test ComfyUI integration readiness + - name: Test Sampler Combo run: | python -c " import sys import os sys.path.insert(0, os.getcwd()) + # Test Sampler Combo imports + from kikotools.tools.sampler_combo.node import SamplerComboNode + from kikotools.tools.sampler_combo.logic import ( + get_sampler_combo, validate_sampler_settings, SAMPLERS, SCHEDULERS + ) + + print('✓ Sampler Combo imports successful') + + # Test node interface + node = SamplerComboNode() + input_types = node.INPUT_TYPES() + assert 'required' in input_types + assert 'sampler_name' in input_types['required'] + assert 'scheduler' in input_types['required'] + assert 'steps' in input_types['required'] + assert 'cfg' in input_types['required'] + print('✓ Sampler Combo interface tests passed') + + # Test return types + assert node.RETURN_TYPES == (SAMPLERS, SCHEDULERS, 'INT', 'FLOAT') + assert node.RETURN_NAMES == ('sampler_name', 'scheduler', 'steps', 'cfg') + assert node.CATEGORY == 'ComfyAssets' + print('✓ Sampler Combo return types tests passed') + + # Test sampler combo functionality + result = node.get_sampler_combo('euler', 'normal', 20, 7.0) + assert result == ('euler', 'normal', 20, 7.0) + print('✓ Sampler combo functionality tests passed') + + # Test validation + assert validate_sampler_settings('euler', 'normal', 20, 7.0) == True + print('✓ Sampler validation tests passed') + + # Test available samplers and schedulers + samplers = node.get_available_samplers() + schedulers = node.get_available_schedulers() + assert len(samplers) > 0 + assert len(schedulers) > 0 + assert 'euler' in samplers + assert 'normal' in schedulers + print(f'✓ Found {len(samplers)} samplers and {len(schedulers)} schedulers') + + print('🎉 All Sampler Combo tests passed!') + " + + - name: Test Seed History + run: | + python -c " + import sys + import os + sys.path.insert(0, os.getcwd()) + + # Test Seed History imports + from kikotools.tools.seed_history.node import SeedHistoryNode + from kikotools.tools.seed_history.logic import ( + generate_random_seed, validate_seed_value, sanitize_seed_value + ) + + print('✓ Seed History imports successful') + + # Test node interface + node = SeedHistoryNode() + input_types = node.INPUT_TYPES() + assert 'required' in input_types + assert 'seed' in input_types['required'] + print('✓ Seed History interface tests passed') + + # Test return types + assert node.RETURN_TYPES == ('INT',) + assert node.RETURN_NAMES == ('seed',) + assert node.CATEGORY == 'ComfyAssets' + print('✓ Seed History return types tests passed') + + # Test seed output functionality + result = node.output_seed(12345) + assert result == (12345,) + print('✓ Seed output functionality tests passed') + + # Test seed validation + assert validate_seed_value(12345) == True + assert validate_seed_value(-1) == False + print('✓ Seed validation tests passed') + + # Test seed generation + new_seed = generate_random_seed() + assert isinstance(new_seed, int) + assert validate_seed_value(new_seed) == True + print('✓ Seed generation tests passed') + + # Test seed sanitization + clean_seed = sanitize_seed_value(12345) + assert clean_seed == 12345 + print('✓ Seed sanitization tests passed') + + # Test node helper methods + assert node.is_seed_in_range(12345) == True + assert node.is_seed_in_range(-1) == False + assert node.get_default_seed() == 12345 + print('✓ Seed helper methods tests passed') + + print('🎉 All Seed History tests passed!') + " + + - name: Test error handling for all tools + run: | + python -c " + import sys + import os + sys.path.insert(0, os.getcwd()) + + print('=== Testing Error Handling for All Tools ===') + + # Test Resolution Calculator error handling from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode + res_node = ResolutionCalculatorNode() - # Test ComfyUI interface requirements - node_class = ResolutionCalculatorNode + try: + res_node.calculate_resolution(2.0) # No input provided + assert False, 'Should have raised ValueError' + except ValueError: + print('✓ Resolution Calculator error handling test passed') - # Check required class attributes - assert hasattr(node_class, 'INPUT_TYPES') - assert hasattr(node_class, 'RETURN_TYPES') - assert hasattr(node_class, 'RETURN_NAMES') - assert hasattr(node_class, 'FUNCTION') - assert hasattr(node_class, 'CATEGORY') + try: + res_node.calculate_resolution(0.0) # Invalid scale + assert False, 'Should have raised ValueError' + except ValueError: + print('✓ Resolution Calculator scale factor validation test passed') - # Check INPUT_TYPES structure - input_types = node_class.INPUT_TYPES() + # Test Width Height Selector error handling + from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode + wh_node = WidthHeightSelectorNode() + + # Test invalid preset fallback + result = wh_node.get_dimensions('invalid_preset', 800, 600) + assert result == (800, 600) # Should fallback to custom dimensions + print('✓ Width Height Selector invalid preset handling test passed') + + # Test Sampler Combo error handling + from kikotools.tools.sampler_combo.node import SamplerComboNode + sampler_node = SamplerComboNode() + + # Test with invalid sampler (should use safe defaults) + result = sampler_node.get_sampler_combo('invalid_sampler', 'normal', 20, 7.0) + assert result == ('euler', 'normal', 20, 7.0) # Safe defaults + print('✓ Sampler Combo invalid input handling test passed') + + # Test Seed History error handling + from kikotools.tools.seed_history.node import SeedHistoryNode + seed_node = SeedHistoryNode() + + # Test invalid seed value (should use fallback) + result = seed_node.output_seed(-1) # Invalid negative seed + assert result == (12345,) # Fallback seed + print('✓ Seed History invalid seed handling test passed') + + print('🎉 All error handling tests passed for all tools!') + " + + - name: Test ComfyUI integration readiness for all tools + run: | + python -c " + import sys + import os + sys.path.insert(0, os.getcwd()) + + print('=== Testing ComfyUI Integration for All Tools ===') + + # Test Resolution Calculator + from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode + res_class = ResolutionCalculatorNode + + assert hasattr(res_class, 'INPUT_TYPES') + assert hasattr(res_class, 'RETURN_TYPES') + assert hasattr(res_class, 'RETURN_NAMES') + assert hasattr(res_class, 'FUNCTION') + assert hasattr(res_class, 'CATEGORY') + + input_types = res_class.INPUT_TYPES() assert 'required' in input_types assert 'optional' in input_types assert 'scale_factor' in input_types['required'] assert 'image' in input_types['optional'] assert 'latent' in input_types['optional'] - # Check return types - assert node_class.RETURN_TYPES == ('INT', 'INT') - assert node_class.RETURN_NAMES == ('width', 'height') - assert node_class.CATEGORY == 'ComfyAssets' + assert res_class.RETURN_TYPES == ('INT', 'INT') + assert res_class.RETURN_NAMES == ('width', 'height') + assert res_class.CATEGORY == 'ComfyAssets' + print('✓ Resolution Calculator ComfyUI integration passed') - print('✓ ComfyUI integration readiness tests passed') + # Test Width Height Selector + from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode + wh_class = WidthHeightSelectorNode + + assert hasattr(wh_class, 'INPUT_TYPES') + assert hasattr(wh_class, 'RETURN_TYPES') + assert hasattr(wh_class, 'RETURN_NAMES') + assert hasattr(wh_class, 'FUNCTION') + assert hasattr(wh_class, 'CATEGORY') + + input_types = wh_class.INPUT_TYPES() + assert 'required' in input_types + assert 'preset' in input_types['required'] + assert 'width' in input_types['required'] + assert 'height' in input_types['required'] + + assert wh_class.RETURN_TYPES == ('INT', 'INT') + assert wh_class.RETURN_NAMES == ('width', 'height') + assert wh_class.CATEGORY == 'ComfyAssets' + print('✓ Width Height Selector ComfyUI integration passed') + + # Test Sampler Combo + from kikotools.tools.sampler_combo.node import SamplerComboNode + sampler_class = SamplerComboNode + + assert hasattr(sampler_class, 'INPUT_TYPES') + assert hasattr(sampler_class, 'RETURN_TYPES') + assert hasattr(sampler_class, 'RETURN_NAMES') + assert hasattr(sampler_class, 'FUNCTION') + assert hasattr(sampler_class, 'CATEGORY') + + input_types = sampler_class.INPUT_TYPES() + assert 'required' in input_types + assert 'sampler_name' in input_types['required'] + assert 'scheduler' in input_types['required'] + assert 'steps' in input_types['required'] + assert 'cfg' in input_types['required'] + + assert sampler_class.CATEGORY == 'ComfyAssets' + print('✓ Sampler Combo ComfyUI integration passed') + + # Test Seed History + from kikotools.tools.seed_history.node import SeedHistoryNode + seed_class = SeedHistoryNode + + assert hasattr(seed_class, 'INPUT_TYPES') + assert hasattr(seed_class, 'RETURN_TYPES') + assert hasattr(seed_class, 'RETURN_NAMES') + assert hasattr(seed_class, 'FUNCTION') + assert hasattr(seed_class, 'CATEGORY') + + input_types = seed_class.INPUT_TYPES() + assert 'required' in input_types + assert 'seed' in input_types['required'] + + assert seed_class.RETURN_TYPES == ('INT',) + assert seed_class.RETURN_NAMES == ('seed',) + assert seed_class.CATEGORY == 'ComfyAssets' + print('✓ Seed History ComfyUI integration passed') + + print('🎉 All tools ComfyUI integration readiness tests passed!') " test-package-structure: @@ -163,14 +414,37 @@ jobs: test -d kikotools/base || (echo "kikotools/base directory missing" && exit 1) test -d kikotools/tools || (echo "kikotools/tools directory missing" && exit 1) test -d kikotools/tools/resolution_calculator || (echo "resolution_calculator directory missing" && exit 1) + test -d kikotools/tools/width_height_selector || (echo "width_height_selector directory missing" && exit 1) + test -d kikotools/tools/sampler_combo || (echo "sampler_combo directory missing" && exit 1) + test -d kikotools/tools/seed_history || (echo "seed_history directory missing" && exit 1) test -d tests || (echo "tests directory missing" && exit 1) test -d examples || (echo "examples directory missing" && exit 1) + test -d web || (echo "web directory missing" && exit 1) # Check key files test -f kikotools/__init__.py || (echo "kikotools/__init__.py missing" && exit 1) test -f kikotools/base/base_node.py || (echo "base_node.py missing" && exit 1) - test -f kikotools/tools/resolution_calculator/node.py || (echo "node.py missing" && exit 1) - test -f kikotools/tools/resolution_calculator/logic.py || (echo "logic.py missing" && exit 1) + + # Resolution Calculator files + test -f kikotools/tools/resolution_calculator/node.py || (echo "resolution_calculator node.py missing" && exit 1) + test -f kikotools/tools/resolution_calculator/logic.py || (echo "resolution_calculator logic.py missing" && exit 1) + + # Width Height Selector files + test -f kikotools/tools/width_height_selector/node.py || (echo "width_height_selector node.py missing" && exit 1) + test -f kikotools/tools/width_height_selector/logic.py || (echo "width_height_selector logic.py missing" && exit 1) + test -f kikotools/tools/width_height_selector/presets.py || (echo "width_height_selector presets.py missing" && exit 1) + + # Sampler Combo files + test -f kikotools/tools/sampler_combo/node.py || (echo "sampler_combo node.py missing" && exit 1) + test -f kikotools/tools/sampler_combo/logic.py || (echo "sampler_combo logic.py missing" && exit 1) + + # Seed History files + test -f kikotools/tools/seed_history/node.py || (echo "seed_history node.py missing" && exit 1) + test -f kikotools/tools/seed_history/logic.py || (echo "seed_history logic.py missing" && exit 1) + + # Web files + test -f web/width_height_swap.js || (echo "width_height_swap.js missing" && exit 1) + test -f web/seed_history_ui.js || (echo "seed_history_ui.js missing" && exit 1) echo "✓ Package structure tests passed" @@ -181,9 +455,15 @@ jobs: - name: Test documentation completeness run: | - # Check documentation files + # Check documentation files for all tools test -f examples/documentation/resolution_calculator.md || (echo "Resolution calculator docs missing" && exit 1) - test -f examples/workflows/resolution_calculator_example.json || (echo "Example workflow missing" && exit 1) + test -f examples/workflows/resolution_calculator_example.json || (echo "Resolution calculator workflow missing" && exit 1) + test -f examples/documentation/width_height_selector.md || (echo "Width height selector docs missing" && exit 1) + test -f examples/workflows/width_height_selector_example.json || (echo "Width height selector workflow missing" && exit 1) + test -f examples/documentation/sampler_combo.md || (echo "Sampler combo docs missing" && exit 1) + test -f examples/workflows/sampler_combo_example.json || (echo "Sampler combo workflow missing" && exit 1) + test -f examples/documentation/seed_history.md || (echo "Seed history docs missing" && exit 1) + test -f examples/workflows/seed_history_example.json || (echo "Seed history workflow missing" && exit 1) # Check README has key sections grep -q "Installation" README.md || (echo "README missing Installation section" && exit 1) From ce8c36f309965fc1ae783ab324faf675d5b9475b Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:38:21 -0700 Subject: [PATCH 02/10] refactor(node): enhance preset metadata handling --- kikotools/tools/width_height_selector/node.py | 129 ++++++- .../tools/width_height_selector/presets.py | 333 +++++++++++++----- 2 files changed, 352 insertions(+), 110 deletions(-) diff --git a/kikotools/tools/width_height_selector/node.py b/kikotools/tools/width_height_selector/node.py index 8ca80c4..eb8b96f 100644 --- a/kikotools/tools/width_height_selector/node.py +++ b/kikotools/tools/width_height_selector/node.py @@ -11,7 +11,10 @@ from .logic import ( from .presets import ( PRESET_OPTIONS, PRESET_DESCRIPTIONS, + PRESET_METADATA, get_model_recommendation, + get_preset_metadata, + get_presets_by_model_group, ) @@ -26,13 +29,26 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): @classmethod def INPUT_TYPES(cls): """Define the input types for the ComfyUI node.""" - # Get all preset options excluding the custom tuple - preset_keys = [key for key in PRESET_OPTIONS.keys()] + # Create formatted preset options with metadata + preset_options = ["custom"] # Custom first + + # Add formatted presets with metadata + for preset_name in PRESET_OPTIONS.keys(): + if preset_name != "custom": + metadata = PRESET_METADATA.get(preset_name) + if metadata: + formatted_option = ( + f"{preset_name} - {metadata.aspect_ratio} " + f"({metadata.megapixels:.1f}MP) - {metadata.model_group}" + ) + preset_options.append(formatted_option) + else: + preset_options.append(preset_name) return { "required": { "preset": ( - preset_keys, + preset_options, { "default": "custom", "tooltip": "Select from optimized resolution presets or use " @@ -78,7 +94,7 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): Get width and height dimensions with preset and swap support. Args: - preset: Selected preset name or "custom" + preset: Selected preset name or formatted preset string width: Custom width value height: Custom height value @@ -86,8 +102,11 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): Tuple of (width, height) """ try: + # Extract original preset name from formatted string if needed + original_preset = self._extract_preset_name(preset) + # Get base dimensions from preset or custom input - final_width, final_height = get_preset_dimensions(preset, width, height) + final_width, final_height = get_preset_dimensions(original_preset, width, height) # Sanitize dimensions to ensure they meet ComfyUI requirements final_width, final_height = sanitize_dimensions(final_width, final_height) @@ -110,6 +129,37 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): ) self.handle_error(error_msg) return (1024, 1024) + + def _extract_preset_name(self, formatted_preset: str) -> str: + """ + Extract the original preset name from a formatted preset string. + + Args: + formatted_preset: Either original preset name or formatted string + + Returns: + Original preset name + """ + # If it's already "custom", return as-is + if formatted_preset == "custom": + return formatted_preset + + # If it contains formatting metadata, extract the resolution part + if " - " in formatted_preset: + # Format is: "1024×1024 - 1:1 (1.0MP) - SDXL" + # Extract the first part (resolution) + resolution_part = formatted_preset.split(" - ")[0] + + # Verify this is a valid preset name + if resolution_part in PRESET_OPTIONS: + return resolution_part + + # If no formatting or not found, check if it's directly a valid preset + if formatted_preset in PRESET_OPTIONS: + return formatted_preset + + # Default to "custom" if we can't parse it + return "custom" def get_preset_info(self, preset: str) -> str: """ @@ -124,14 +174,12 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): if preset == "custom": return "Custom dimensions - use the width and height inputs below" - if preset in PRESET_DESCRIPTIONS: - return PRESET_DESCRIPTIONS[preset] - - # Fallback for unknown presets - if preset in PRESET_OPTIONS: - width, height = PRESET_OPTIONS[preset] - aspect_ratio = calculate_aspect_ratio(width, height) - return f"{preset} - {aspect_ratio} aspect ratio" + metadata = get_preset_metadata(preset) + if metadata.width > 0: # Valid metadata + return ( + f"{preset} - {metadata.aspect_ratio} ({metadata.megapixels:.1f}MP) - " + f"{metadata.description}" + ) return f"Unknown preset: {preset}" @@ -152,19 +200,22 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): Validate node inputs. Args: - preset: Preset name + preset: Preset name or formatted preset string width: Width value height: Height value Returns: True if inputs are valid """ + # Extract original preset name + original_preset = self._extract_preset_name(preset) + # Check if preset exists or is custom - if preset != "custom" and preset not in PRESET_OPTIONS: + if original_preset != "custom" and original_preset not in PRESET_OPTIONS: return False # For custom preset, validate dimensions - if preset == "custom": + if original_preset == "custom": if not validate_dimensions(width, height): return False @@ -195,6 +246,52 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): return PRESET_OPTIONS[preset] return (0, 0) + @classmethod + def get_presets_by_model(cls, model_group: str) -> dict: + """ + Get all presets for a specific model group with metadata. + + Args: + model_group: Model group name ("SDXL", "FLUX", "Ultra-Wide") + + Returns: + Dictionary of presets with metadata + """ + return get_presets_by_model_group(model_group) + + @classmethod + def get_preset_metadata_static(cls, preset: str) -> dict: + """ + Get metadata for a preset as a dictionary. + + Args: + preset: Preset name + + Returns: + Dictionary with metadata information + """ + metadata = get_preset_metadata(preset) + return { + "width": metadata.width, + "height": metadata.height, + "aspect_ratio": metadata.aspect_ratio, + "aspect_decimal": metadata.aspect_decimal, + "megapixels": metadata.megapixels, + "model_group": metadata.model_group, + "category": metadata.category, + "description": metadata.description, + } + + @classmethod + def get_model_groups(cls) -> list: + """ + Get list of available model groups. + + Returns: + List of model group names + """ + return list(set(metadata.model_group for metadata in PRESET_METADATA.values())) + def __str__(self) -> str: """String representation of the node.""" return f"WidthHeightSelectorNode(presets={len(PRESET_OPTIONS)})" diff --git a/kikotools/tools/width_height_selector/presets.py b/kikotools/tools/width_height_selector/presets.py index a146f22..b498aed 100644 --- a/kikotools/tools/width_height_selector/presets.py +++ b/kikotools/tools/width_height_selector/presets.py @@ -1,114 +1,241 @@ """Preset definitions for Width Height Selector.""" -from typing import Dict, Tuple +from typing import Dict, Tuple, NamedTuple +from fractions import Fraction -# SDXL optimized presets (~1 megapixel, dimensions divisible by 8) + +class PresetMetadata(NamedTuple): + """Metadata for a resolution preset.""" + width: int + height: int + aspect_ratio: str + aspect_decimal: float + megapixels: float + model_group: str + category: str + description: str + + +def calculate_aspect_ratio(width: int, height: int) -> Tuple[str, float]: + """Calculate aspect ratio as string and decimal.""" + fraction = Fraction(width, height) + decimal = width / height + return f"{fraction.numerator}:{fraction.denominator}", decimal + + +# Enhanced preset definitions with full metadata +PRESET_METADATA: Dict[str, PresetMetadata] = { + # SDXL Presets - Square + "1024×1024": PresetMetadata( + 1024, 1024, "1:1", 1.0, 1.05, "SDXL", "Square", + "SDXL base resolution - perfect square" + ), + + # SDXL Presets - Portrait + "896×1152": PresetMetadata( + 896, 1152, "7:9", 0.778, 1.03, "SDXL", "Portrait", + "SDXL portrait 7:9 - moderate portrait" + ), + "832×1216": PresetMetadata( + 832, 1216, "13:19", 0.684, 1.01, "SDXL", "Portrait", + "SDXL portrait 13:19 - standard portrait" + ), + "768×1344": PresetMetadata( + 768, 1344, "4:7", 0.571, 1.03, "SDXL", "Portrait", + "SDXL portrait 4:7 - tall portrait" + ), + "640×1536": PresetMetadata( + 640, 1536, "5:12", 0.417, 0.98, "SDXL", "Portrait", + "SDXL portrait 5:12 - very tall portrait" + ), + + # SDXL Presets - Landscape + "1152×896": PresetMetadata( + 1152, 896, "9:7", 1.286, 1.03, "SDXL", "Landscape", + "SDXL landscape 9:7 - moderate landscape" + ), + "1216×832": PresetMetadata( + 1216, 832, "19:13", 1.462, 1.01, "SDXL", "Landscape", + "SDXL landscape 19:13 - standard landscape" + ), + "1344×768": PresetMetadata( + 1344, 768, "7:4", 1.750, 1.03, "SDXL", "Landscape", + "SDXL landscape 7:4 - wide landscape" + ), + "1536×640": PresetMetadata( + 1536, 640, "12:5", 2.400, 0.98, "SDXL", "Landscape", + "SDXL landscape 12:5 - very wide landscape" + ), + + # FLUX Presets - High Quality + "1920×1080": PresetMetadata( + 1920, 1080, "16:9", 1.778, 2.07, "FLUX", "Cinematic", + "FLUX Full HD 16:9 - best quality/speed balance" + ), + "1536×1536": PresetMetadata( + 1536, 1536, "1:1", 1.0, 2.36, "FLUX", "Square", + "FLUX high-res square - premium quality" + ), + "1280×768": PresetMetadata( + 1280, 768, "5:3", 1.667, 0.98, "FLUX", "Cinematic", + "FLUX 5:3 landscape - cinematic wide" + ), + "768×1280": PresetMetadata( + 768, 1280, "3:5", 0.600, 0.98, "FLUX", "Portrait", + "FLUX 3:5 portrait - mobile optimized" + ), + + # FLUX Presets - Alternative + "1440×1080": PresetMetadata( + 1440, 1080, "4:3", 1.333, 1.56, "FLUX", "Classic", + "FLUX 4:3 classic - traditional aspect ratio" + ), + "1080×1440": PresetMetadata( + 1080, 1440, "3:4", 0.750, 1.56, "FLUX", "Portrait", + "FLUX 3:4 portrait - classic portrait" + ), + "1728×1152": PresetMetadata( + 1728, 1152, "3:2", 1.500, 1.99, "FLUX", "Photography", + "FLUX 3:2 photo - photography standard" + ), + "1152×1728": PresetMetadata( + 1152, 1728, "2:3", 0.667, 1.99, "FLUX", "Portrait", + "FLUX 2:3 portrait - portrait photography" + ), + + # Ultra-Wide Presets - Landscape + "2560×1080": PresetMetadata( + 2560, 1080, "64:27", 2.370, 2.76, "Ultra-Wide", "Gaming", + "Ultra-wide 64:27 - gaming/panoramic" + ), + "2048×768": PresetMetadata( + 2048, 768, "8:3", 2.667, 1.57, "Ultra-Wide", "Cinematic", + "Wide cinematic 8:3 - movie aspect" + ), + "1792×768": PresetMetadata( + 1792, 768, "7:3", 2.333, 1.38, "Ultra-Wide", "Panoramic", + "Panoramic 7:3 - landscape vista" + ), + "2304×768": PresetMetadata( + 2304, 768, "3:1", 3.000, 1.77, "Ultra-Wide", "Banner", + "Banner 3:1 - extreme wide banner" + ), + + # Ultra-Wide Presets - Portrait + "1080×2560": PresetMetadata( + 1080, 2560, "27:64", 0.422, 2.76, "Ultra-Wide", "Mobile", + "Mobile ultra-tall 27:64 - modern phones" + ), + "768×2048": PresetMetadata( + 768, 2048, "3:8", 0.375, 1.57, "Ultra-Wide", "Vertical", + "Vertical cinematic 3:8 - portrait video" + ), + "768×1792": PresetMetadata( + 768, 1792, "3:7", 0.429, 1.38, "Ultra-Wide", "Vertical", + "Vertical panoramic 3:7 - tall vista" + ), + "768×2304": PresetMetadata( + 768, 2304, "1:3", 0.333, 1.77, "Ultra-Wide", "Banner", + "Vertical banner 1:3 - extreme tall banner" + ), +} + +# Legacy compatibility - maintain old preset dictionaries SDXL_PRESETS: Dict[str, Tuple[int, int]] = { - # Square - "1024×1024": (1024, 1024), # 1:1 - Base SDXL resolution - # Portrait ratios - "896×1152": (896, 1152), # 7:9 - Moderate portrait - "832×1216": (832, 1216), # 13:19 - Standard portrait - "768×1344": (768, 1344), # 4:7 - Tall portrait - "640×1536": (640, 1536), # 5:12 - Very tall portrait - # Landscape ratios - "1152×896": (1152, 896), # 9:7 - Moderate landscape - "1216×832": (1216, 832), # 19:13 - Standard landscape - "1344×768": (1344, 768), # 7:4 - Wide landscape - "1536×640": (1536, 640), # 12:5 - Very wide landscape + k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" } -# FLUX optimized presets (higher resolution, flexible ratios) FLUX_PRESETS: Dict[str, Tuple[int, int]] = { - # Recommended high-quality resolutions - "1920×1080": (1920, 1080), # 16:9 - Full HD landscape - "1536×1536": (1536, 1536), # 1:1 - High-res square - "1280×768": (1280, 768), # 5:3 - Wide landscape - "768×1280": (768, 1280), # 3:5 - Tall portrait - # Alternative quality resolutions - "1440×1080": (1440, 1080), # 4:3 - Classic aspect ratio - "1080×1440": (1080, 1440), # 3:4 - Classic portrait - "1728×1152": (1728, 1152), # 3:2 - Photography standard - "1152×1728": (1152, 1728), # 2:3 - Portrait photography + k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" } -# Ultra-wide and modern aspect ratios ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = { - # Ultra-wide landscape (21:9 and variants) - "2560×1080": (2560, 1080), # 64:27 - Ultra-wide gaming - "2048×768": (2048, 768), # 8:3 - Wide cinematic - "1792×768": (1792, 768), # 7:3 - Panoramic - # Ultra-wide portrait - "1080×2560": (1080, 2560), # 27:64 - Mobile ultra-tall - "768×2048": (768, 2048), # 3:8 - Vertical cinematic - "768×1792": (768, 1792), # 3:7 - Vertical panoramic - # Extreme ratios - "2304×768": (2304, 768), # 3:1 - Banner landscape - "768×2304": (768, 2304), # 1:3 - Banner portrait + k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" } # Combined preset options for ComfyUI dropdown PRESET_OPTIONS: Dict[str, Tuple[int, int]] = { "custom": (0, 0), # Special case for custom dimensions - **SDXL_PRESETS, - **FLUX_PRESETS, - **ULTRA_WIDE_PRESETS, + **{k: (v.width, v.height) for k, v in PRESET_METADATA.items()} } -# Organized preset categories for better UX +# Enhanced preset categories organized by model groups and aspect ratios PRESET_CATEGORIES = { "Custom": ["custom"], - "SDXL Square": ["1024×1024"], - "SDXL Portrait": ["896×1152", "832×1216", "768×1344", "640×1536"], - "SDXL Landscape": ["1152×896", "1216×832", "1344×768", "1536×640"], - "FLUX Recommended": ["1920×1080", "1536×1536", "1280×768", "768×1280"], - "FLUX Alternative": ["1440×1080", "1080×1440", "1728×1152", "1152×1728"], - "Ultra-Wide Landscape": ["2560×1080", "2048×768", "1792×768", "2304×768"], - "Ultra-Wide Portrait": ["1080×2560", "768×2048", "768×1792", "768×2304"], + + # SDXL Categories + "SDXL Square": [k for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" and v.category == "Square"], + "SDXL Portrait": [k for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" and v.category == "Portrait"], + "SDXL Landscape": [k for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" and v.category == "Landscape"], + + # FLUX Categories + "FLUX Square": [k for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Square"], + "FLUX Portrait": [k for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Portrait"], + "FLUX Cinematic": [k for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Cinematic"], + "FLUX Classic": [k for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Classic"], + "FLUX Photography": [k for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Photography"], + + # Ultra-Wide Categories + "Ultra-Wide Gaming": [k for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Gaming"], + "Ultra-Wide Cinematic": [k for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Cinematic"], + "Ultra-Wide Panoramic": [k for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Panoramic"], + "Ultra-Wide Mobile": [k for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Mobile"], + "Ultra-Wide Vertical": [k for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Vertical"], + "Ultra-Wide Banner": [k for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Banner"], } -# Preset descriptions for tooltips -PRESET_DESCRIPTIONS = { - # SDXL presets - "1024×1024": "SDXL base resolution - perfect square", - "896×1152": "SDXL portrait 7:9 - moderate portrait", - "832×1216": "SDXL portrait 13:19 - standard portrait", - "768×1344": "SDXL portrait 4:7 - tall portrait", - "640×1536": "SDXL portrait 5:12 - very tall portrait", - "1152×896": "SDXL landscape 9:7 - moderate landscape", - "1216×832": "SDXL landscape 19:13 - standard landscape", - "1344×768": "SDXL landscape 7:4 - wide landscape", - "1536×640": "SDXL landscape 12:5 - very wide landscape", - # FLUX presets - "1920×1080": "FLUX Full HD 16:9 - best quality/speed balance", - "1536×1536": "FLUX high-res square - premium quality", - "1280×768": "FLUX 5:3 landscape - cinematic wide", - "768×1280": "FLUX 3:5 portrait - mobile optimized", - "1440×1080": "FLUX 4:3 classic - traditional aspect ratio", - "1080×1440": "FLUX 3:4 portrait - classic portrait", - "1728×1152": "FLUX 3:2 photo - photography standard", - "1152×1728": "FLUX 2:3 portrait - portrait photography", - # Ultra-wide presets - "2560×1080": "Ultra-wide 64:27 - gaming/panoramic", - "2048×768": "Wide cinematic 8:3 - movie aspect", - "1792×768": "Panoramic 7:3 - landscape vista", - "2304×768": "Banner 3:1 - extreme wide banner", - "1080×2560": "Mobile ultra-tall 27:64 - modern phones", - "768×2048": "Vertical cinematic 3:8 - portrait video", - "768×1792": "Vertical panoramic 3:7 - tall vista", - "768×2304": "Vertical banner 1:3 - extreme tall banner", -} +# Legacy compatibility - preset descriptions +PRESET_DESCRIPTIONS = {k: v.description for k, v in PRESET_METADATA.items()} -# Model-specific recommendations +# Model-specific recommendations with metadata MODEL_RECOMMENDATIONS = { - "SDXL": list(SDXL_PRESETS.keys()), - "FLUX": list(FLUX_PRESETS.keys()), - "Ultra-Wide": list(ULTRA_WIDE_PRESETS.keys()), + "SDXL": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"], + "FLUX": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"], + "Ultra-Wide": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"], } +# New metadata-aware helper functions +def get_presets_by_model_group(model_group: str) -> Dict[str, PresetMetadata]: + """Get all presets for a specific model group.""" + return {k: v for k, v in PRESET_METADATA.items() if v.model_group == model_group} + + +def get_presets_by_aspect_ratio(aspect_ratio: str) -> Dict[str, PresetMetadata]: + """Get all presets with a specific aspect ratio.""" + return {k: v for k, v in PRESET_METADATA.items() if v.aspect_ratio == aspect_ratio} + + +def get_presets_by_category(category: str) -> Dict[str, PresetMetadata]: + """Get all presets in a specific category.""" + return {k: v for k, v in PRESET_METADATA.items() if v.category == category} + + +def get_preset_metadata(preset_name: str) -> PresetMetadata: + """Get metadata for a specific preset.""" + return PRESET_METADATA.get(preset_name, PresetMetadata( + 0, 0, "1:1", 1.0, 0.0, "Custom", "Custom", "Custom dimensions" + )) + def get_preset_category(preset_name: str) -> str: """Get the category for a given preset name.""" + metadata = PRESET_METADATA.get(preset_name) + if metadata: + return metadata.category for category, presets in PRESET_CATEGORIES.items(): if preset_name in presets: return category @@ -117,21 +244,17 @@ def get_preset_category(preset_name: str) -> str: def get_model_recommendation(preset_name: str) -> str: """Get model recommendation for a given preset.""" - if preset_name in SDXL_PRESETS: - return "Optimized for SDXL" - elif preset_name in FLUX_PRESETS: - return "Optimized for FLUX" - elif preset_name in ULTRA_WIDE_PRESETS: - return "Modern ultra-wide ratios" - else: - return "Custom dimensions" + metadata = PRESET_METADATA.get(preset_name) + if metadata: + return f"Optimized for {metadata.model_group}" + return "Custom dimensions" def validate_preset_dimensions() -> bool: """Validate that all presets meet ComfyUI requirements.""" - all_presets = {**SDXL_PRESETS, **FLUX_PRESETS, **ULTRA_WIDE_PRESETS} - - for preset_name, (width, height) in all_presets.items(): + for preset_name, metadata in PRESET_METADATA.items(): + width, height = metadata.width, metadata.height + # Check divisible by 8 if width % 8 != 0 or height % 8 != 0: print( @@ -147,6 +270,28 @@ def validate_preset_dimensions() -> bool: return True +# Additional validation for metadata consistency +def validate_metadata_consistency() -> bool: + """Validate metadata consistency and completeness.""" + for preset_name, metadata in PRESET_METADATA.items(): + # Verify aspect ratio calculation + expected_ratio, expected_decimal = calculate_aspect_ratio(metadata.width, metadata.height) + if abs(metadata.aspect_decimal - expected_decimal) > 0.001: + print(f"ERROR: {preset_name} aspect ratio mismatch: expected {expected_decimal:.3f}, got {metadata.aspect_decimal}") + return False + + # Verify megapixel calculation + expected_mp = (metadata.width * metadata.height) / 1_000_000 + if abs(metadata.megapixels - expected_mp) > 0.1: + print(f"ERROR: {preset_name} megapixel mismatch: expected {expected_mp:.2f}, got {metadata.megapixels}") + return False + + return True + + # Validate presets on import if not validate_preset_dimensions(): raise ValueError("Preset validation failed - check console for details") + +if not validate_metadata_consistency(): + raise ValueError("Metadata validation failed - check console for details") From 2d6fef8fb42fb3762374ee08f8dd0ec99f02108d Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:38:33 -0700 Subject: [PATCH 03/10] test: Add tests for formatted preset metadata handling --- .../unit/tools/test_width_height_selector.py | 291 +++++++++++++++++- 1 file changed, 280 insertions(+), 11 deletions(-) diff --git a/tests/unit/tools/test_width_height_selector.py b/tests/unit/tools/test_width_height_selector.py index 459d55e..2c2c063 100644 --- a/tests/unit/tools/test_width_height_selector.py +++ b/tests/unit/tools/test_width_height_selector.py @@ -8,9 +8,12 @@ from kikotools.tools.width_height_selector.logic import ( ) from kikotools.tools.width_height_selector.presets import ( PRESET_OPTIONS, + PRESET_METADATA, SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS, + get_preset_metadata, + get_presets_by_model_group, ) @@ -42,46 +45,92 @@ class TestWidthHeightSelectorNode: assert result == (1920, 1080) def test_sdxl_square_preset(self): - """Test SDXL square preset.""" + """Test SDXL square preset (supports both raw and formatted).""" + # Test raw preset result = self.node.get_dimensions( preset="1024×1024", width=512, # Should be ignored height=512, # Should be ignored ) assert result == (1024, 1024) + + # Test formatted preset + result = self.node.get_dimensions( + preset="1024×1024 - 1:1 (1.1MP) - SDXL", + width=512, # Should be ignored + height=512, # Should be ignored + ) + assert result == (1024, 1024) def test_sdxl_portrait_preset(self): - """Test SDXL portrait preset.""" + """Test SDXL portrait preset (supports both raw and formatted).""" + # Test raw preset result = self.node.get_dimensions(preset="832×1216", width=512, height=512) assert result == (832, 1216) + + # Test formatted preset if available + formatted_preset = "832×1216 - 13:19 (1.0MP) - SDXL" + result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + assert result == (832, 1216) def test_sdxl_landscape_preset(self): - """Test SDXL landscape preset.""" + """Test SDXL landscape preset (supports both raw and formatted).""" + # Test raw preset result = self.node.get_dimensions(preset="1216×832", width=512, height=512) assert result == (1216, 832) + + # Test formatted preset if available + formatted_preset = "1216×832 - 19:13 (1.0MP) - SDXL" + result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + assert result == (1216, 832) def test_flux_preset(self): - """Test FLUX preset.""" + """Test FLUX preset (supports both raw and formatted).""" + # Test raw preset result = self.node.get_dimensions(preset="1920×1080", width=512, height=512) assert result == (1920, 1080) + + # Test formatted preset + formatted_preset = "1920×1080 - 16:9 (2.1MP) - FLUX" + result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + assert result == (1920, 1080) def test_ultra_wide_preset(self): - """Test ultra-wide preset.""" + """Test ultra-wide preset (supports both raw and formatted).""" + # Test raw preset result = self.node.get_dimensions(preset="2560×1080", width=512, height=512) assert result == (2560, 1080) + + # Test formatted preset if available + formatted_preset = "2560×1080 - 64:27 (2.8MP) - Ultra-Wide" + result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + assert result == (2560, 1080) def test_all_presets_available(self): """Test that all presets are available in INPUT_TYPES.""" input_types = self.node.INPUT_TYPES() available_presets = input_types["required"]["preset"][0] - # Check that all major preset categories are available + # Check that custom is available assert "custom" in available_presets - assert "1024×1024" in available_presets # SDXL square - assert "832×1216" in available_presets # SDXL portrait - assert "1216×832" in available_presets # SDXL landscape - assert "1920×1080" in available_presets # FLUX - assert "2560×1080" in available_presets # Ultra-wide + + # Check that formatted presets are available (with metadata) + # Extract raw preset names from formatted options + raw_presets = [] + for option in available_presets: + if option == "custom": + raw_presets.append(option) + elif " - " in option: + raw_presets.append(option.split(" - ")[0]) + else: + raw_presets.append(option) + + # Check that all major preset categories are available + assert "1024×1024" in raw_presets # SDXL square + assert "832×1216" in raw_presets # SDXL portrait + assert "1216×832" in raw_presets # SDXL landscape + assert "1920×1080" in raw_presets # FLUX + assert "2560×1080" in raw_presets # Ultra-wide def test_invalid_preset_fallback(self): """Test handling of invalid preset.""" @@ -259,3 +308,223 @@ class TestEdgeCases: # Prime number dimensions ratio = calculate_aspect_ratio(1920, 1080) assert ratio == "16:9" + + +class TestPresetMetadata: + """Test preset metadata functionality.""" + + def test_preset_metadata_structure(self): + """Test that metadata has correct structure.""" + for preset_name, metadata in PRESET_METADATA.items(): + assert hasattr(metadata, 'width') + assert hasattr(metadata, 'height') + assert hasattr(metadata, 'aspect_ratio') + assert hasattr(metadata, 'aspect_decimal') + assert hasattr(metadata, 'megapixels') + assert hasattr(metadata, 'model_group') + assert hasattr(metadata, 'category') + assert hasattr(metadata, 'description') + + def test_metadata_aspect_ratios(self): + """Test that aspect ratios are correctly calculated.""" + for preset_name, metadata in PRESET_METADATA.items(): + expected_decimal = metadata.width / metadata.height + assert abs(metadata.aspect_decimal - expected_decimal) < 0.001 + + # Common aspect ratios should match expected values + if preset_name == "1024×1024": + assert metadata.aspect_ratio == "1:1" + assert metadata.aspect_decimal == 1.0 + elif preset_name == "1920×1080": + assert metadata.aspect_ratio == "16:9" + assert abs(metadata.aspect_decimal - 1.778) < 0.01 + + def test_metadata_megapixels(self): + """Test that megapixel calculations are correct.""" + for preset_name, metadata in PRESET_METADATA.items(): + expected_mp = (metadata.width * metadata.height) / 1_000_000 + assert abs(metadata.megapixels - expected_mp) < 0.1 + + def test_model_groups(self): + """Test that model groups are properly assigned.""" + sdxl_presets = get_presets_by_model_group("SDXL") + flux_presets = get_presets_by_model_group("FLUX") + ultra_wide_presets = get_presets_by_model_group("Ultra-Wide") + + assert len(sdxl_presets) > 0 + assert len(flux_presets) > 0 + assert len(ultra_wide_presets) > 0 + + # Check specific presets are in correct groups + assert "1024×1024" in [k for k, v in sdxl_presets.items()] + assert "1920×1080" in [k for k, v in flux_presets.items()] + assert "2560×1080" in [k for k, v in ultra_wide_presets.items()] + + def test_get_preset_metadata_function(self): + """Test get_preset_metadata function.""" + # Valid preset + metadata = get_preset_metadata("1024×1024") + assert metadata.width == 1024 + assert metadata.height == 1024 + assert metadata.model_group == "SDXL" + + # Invalid preset returns default + metadata = get_preset_metadata("invalid_preset") + assert metadata.width == 0 + assert metadata.height == 0 + assert metadata.model_group == "Custom" + + +class TestNodeMetadataIntegration: + """Test node integration with metadata.""" + + def setup_method(self): + """Set up test fixtures.""" + self.node = WidthHeightSelectorNode() + + def test_get_preset_info_with_metadata(self): + """Test that preset info includes metadata.""" + info = self.node.get_preset_info("1024×1024") + assert "1:1" in info # Aspect ratio + assert "1.0MP" in info or "1.1MP" in info # Megapixels + assert "SDXL" in info # Description + + def test_get_presets_by_model_static(self): + """Test static method for getting presets by model.""" + sdxl_presets = self.node.get_presets_by_model("SDXL") + assert isinstance(sdxl_presets, dict) + assert len(sdxl_presets) > 0 + + # Check that returned values are metadata objects + for preset_name, metadata in sdxl_presets.items(): + assert metadata.model_group == "SDXL" + + def test_get_preset_metadata_static(self): + """Test static method for getting preset metadata.""" + metadata_dict = self.node.get_preset_metadata_static("1920×1080") + + assert metadata_dict["width"] == 1920 + assert metadata_dict["height"] == 1080 + assert metadata_dict["aspect_ratio"] == "16:9" + assert metadata_dict["model_group"] == "FLUX" + + def test_get_model_groups(self): + """Test static method for getting model groups.""" + groups = self.node.get_model_groups() + assert "SDXL" in groups + assert "FLUX" in groups + assert "Ultra-Wide" in groups + + +class TestMetadataValidation: + """Test metadata validation functions.""" + + def test_dimensions_validation(self): + """Test dimensions validation from metadata.""" + from kikotools.tools.width_height_selector.presets import validate_preset_dimensions + assert validate_preset_dimensions() is True + + def test_metadata_consistency_validation(self): + """Test metadata consistency validation.""" + from kikotools.tools.width_height_selector.presets import validate_metadata_consistency + assert validate_metadata_consistency() is True + + +class TestFormattedPresets: + """Test formatted preset functionality.""" + + def setup_method(self): + """Set up test fixtures.""" + self.node = WidthHeightSelectorNode() + + def test_formatted_preset_generation(self): + """Test that INPUT_TYPES generates formatted presets.""" + input_types = self.node.INPUT_TYPES() + available_presets = input_types["required"]["preset"][0] + + # Should have custom first + assert available_presets[0] == "custom" + + # Should have formatted presets with metadata + formatted_count = 0 + for option in available_presets[1:]: # Skip custom + if " - " in option and "MP" in option: + formatted_count += 1 + + assert formatted_count > 0, "No formatted presets found" + assert formatted_count == len(PRESET_METADATA), "Not all presets are formatted" + + def test_preset_name_extraction(self): + """Test extraction of raw preset names from formatted strings.""" + test_cases = [ + ("custom", "custom"), + ("1024×1024 - 1:1 (1.1MP) - SDXL", "1024×1024"), + ("1920×1080 - 16:9 (2.1MP) - FLUX", "1920×1080"), + ("832×1216 - 13:19 (1.0MP) - SDXL", "832×1216"), + ("1024×1024", "1024×1024"), # Raw preset name + ("invalid_preset", "custom"), # Invalid fallback + ] + + for formatted_preset, expected in test_cases: + result = self.node._extract_preset_name(formatted_preset) + assert result == expected, f"Expected {expected}, got {result} for input {formatted_preset}" + + def test_formatted_preset_dimensions(self): + """Test that formatted presets return correct dimensions.""" + # Test with formatted preset string + formatted_preset = "1024×1024 - 1:1 (1.1MP) - SDXL" + result = self.node.get_dimensions(formatted_preset, 512, 512) + assert result == (1024, 1024) + + # Test with FLUX formatted preset + formatted_preset = "1920×1080 - 16:9 (2.1MP) - FLUX" + result = self.node.get_dimensions(formatted_preset, 512, 512) + assert result == (1920, 1080) + + def test_formatted_preset_validation(self): + """Test validation of formatted presets.""" + # Valid formatted preset + assert self.node.validate_inputs("1024×1024 - 1:1 (1.1MP) - SDXL", 1024, 1024) + + # Valid raw preset + assert self.node.validate_inputs("1024×1024", 1024, 1024) + + # Custom preset + assert self.node.validate_inputs("custom", 1024, 1024) + + # Invalid formatted preset should still work (fallback to custom) + assert self.node.validate_inputs("invalid - formatted", 1024, 1024) + + def test_backwards_compatibility(self): + """Test that raw preset names still work.""" + # Raw preset names should still work for backwards compatibility + raw_presets = ["1024×1024", "1920×1080", "832×1216"] + + for raw_preset in raw_presets: + if raw_preset in PRESET_OPTIONS: + result = self.node.get_dimensions(raw_preset, 512, 512) + expected = PRESET_OPTIONS[raw_preset] + assert result == expected, f"Raw preset {raw_preset} failed" + + def test_formatted_preset_metadata_accuracy(self): + """Test that formatted presets contain accurate metadata.""" + input_types = self.node.INPUT_TYPES() + formatted_presets = [opt for opt in input_types["required"]["preset"][0] if " - " in opt] + + for formatted_preset in formatted_presets: + # Extract components + parts = formatted_preset.split(" - ") + assert len(parts) == 3, f"Formatted preset should have 3 parts: {formatted_preset}" + + resolution = parts[0] + aspect_and_mp = parts[1] + model_group = parts[2] + + # Verify resolution exists in metadata + assert resolution in PRESET_METADATA, f"Resolution {resolution} not in metadata" + + # Verify metadata matches format + metadata = PRESET_METADATA[resolution] + assert metadata.model_group == model_group, f"Model group mismatch for {resolution}" + assert metadata.aspect_ratio in aspect_and_mp, f"Aspect ratio not in {aspect_and_mp}" + assert f"{metadata.megapixels:.1f}MP" in aspect_and_mp, f"Megapixels not in {aspect_and_mp}" From 9833ccd69428edd43c7cca40ff820036b224bdd9 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:38:58 -0700 Subject: [PATCH 04/10] refactor(web): add resolution extraction helper function --- web/width_height_swap.js | 68 ++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/web/width_height_swap.js b/web/width_height_swap.js index 4831107..5262b8d 100644 --- a/web/width_height_swap.js +++ b/web/width_height_swap.js @@ -12,6 +12,20 @@ app.registerExtension({ // Track button click state for visual feedback this.swapButtonPressed = false; + // Helper function to extract resolution from formatted preset string + this.extractResolutionFromPreset = function(presetValue) { + if (presetValue === "custom") return null; + + // If it contains formatting metadata, extract the resolution part + if (presetValue.includes(" - ")) { + // Format is: "1024×1024 - 1:1 (1.0MP) - SDXL" + return presetValue.split(" - ")[0]; + } + + // Otherwise assume it's already a raw resolution + return presetValue; + }; + // Override preset callback to update width/height widgets when preset changes const presetWidget = this.widgets.find(w => w.name === "preset"); if (presetWidget) { @@ -27,6 +41,9 @@ app.registerExtension({ const heightWidget = node.widgets.find(w => w.name === "height"); if (widthWidget && heightWidget && value !== "custom") { + // Extract raw resolution from formatted preset + const rawResolution = node.extractResolutionFromPreset(value); + // Define all available presets from our preset system const presetDimensions = { // SDXL Presets @@ -43,8 +60,8 @@ app.registerExtension({ "768×1792": [768, 1792], "768×2304": [768, 2304] }; - if (presetDimensions[value]) { - const [w, h] = presetDimensions[value]; + if (rawResolution && presetDimensions[rawResolution]) { + const [w, h] = presetDimensions[rawResolution]; widthWidget.value = w; heightWidget.value = h; @@ -71,39 +88,42 @@ app.registerExtension({ if (presetWidget.value !== "custom") { const currentPreset = presetWidget.value; + // Extract raw resolution from formatted preset + const rawResolution = this.extractResolutionFromPreset(currentPreset); + if (!rawResolution) return; + // Parse current preset dimensions (handle both × and x separators) let w, h; - if (currentPreset.includes('×')) { - [w, h] = currentPreset.split('×').map(v => parseInt(v)); - } else if (currentPreset.includes('x')) { - [w, h] = currentPreset.split('x').map(v => parseInt(v)); + if (rawResolution.includes('×')) { + [w, h] = rawResolution.split('×').map(v => parseInt(v)); + } else if (rawResolution.includes('x')) { + [w, h] = rawResolution.split('x').map(v => parseInt(v)); } else { return; // Invalid preset format } - const swappedPreset = `${h}×${w}`; + const swappedRawPreset = `${h}×${w}`; - // Define all available presets from our preset system - const availablePresets = [ - "custom", - // SDXL Presets - "1024×1024", "896×1152", "832×1216", "768×1344", "640×1536", - "1152×896", "1216×832", "1344×768", "1536×640", - // FLUX Presets - "1920×1080", "1536×1536", "1280×768", "768×1280", - "1440×1080", "1080×1440", "1728×1152", "1152×1728", - // Ultra-Wide Presets - "2560×1080", "2048×768", "1792×768", "2304×768", - "1080×2560", "768×2048", "768×1792", "768×2304" - ]; + // Find the formatted version of the swapped preset from available options + const availablePresets = presetWidget.options.values || presetWidget.options; + let swappedFormattedPreset = null; - if (availablePresets.includes(swappedPreset)) { - // Swapped preset exists, use it - presetWidget.value = swappedPreset; + for (const option of availablePresets) { + if (option === "custom") continue; + const extractedRes = this.extractResolutionFromPreset(option); + if (extractedRes === swappedRawPreset) { + swappedFormattedPreset = option; + break; + } + } + + if (swappedFormattedPreset) { + // Swapped preset exists, use the formatted version + presetWidget.value = swappedFormattedPreset; widthWidget.value = h; heightWidget.value = w; if (presetWidget.callback) { - presetWidget.callback(swappedPreset, this, presetWidget); + presetWidget.callback(swappedFormattedPreset, this, presetWidget); } if (widthWidget.callback) { widthWidget.callback(h, this, widthWidget); From 5af7a564091ae562764bc6dcd33993df43a0225a Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:39:07 -0700 Subject: [PATCH 05/10] docs: Add Sampler Combo documentation file --- examples/documentation/sampler_combo.md | 208 ++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 examples/documentation/sampler_combo.md diff --git a/examples/documentation/sampler_combo.md b/examples/documentation/sampler_combo.md new file mode 100644 index 0000000..17d3336 --- /dev/null +++ b/examples/documentation/sampler_combo.md @@ -0,0 +1,208 @@ +# Sampler Combo Documentation + +## Overview + +The Sampler Combo is a unified ComfyUI node that combines sampler, scheduler, steps, and CFG settings into a single interface. It reduces workflow complexity while ensuring compatible parameter combinations and providing optimization recommendations. + +## Features + +### 🎯 **Unified Configuration** +- Single node for all sampling parameters +- Compatible sampler + scheduler combinations +- Optimized steps and CFG recommendations +- Reduced workflow complexity + +### 🧠 **Smart Recommendations** +- Scheduler suggestions based on selected sampler +- Optimal steps range for each sampler +- CFG scale recommendations +- Compatibility warnings and suggestions + +### ✅ **Built-in Validation** +- Parameter validation and sanitization +- Error handling with safe defaults +- Performance optimization hints +- Real-time compatibility checking + +### 📊 **Analysis Tools** +- Combo configuration analysis +- Performance assessment +- Optimization recommendations +- Compatibility scoring + +## Node Interface + +### Inputs +- **sampler_name**: Dropdown with available sampling algorithms +- **scheduler**: Dropdown with compatible schedulers +- **steps**: Integer slider (1-100 steps) +- **cfg**: Float slider (0.0-20.0 CFG scale) + +### Outputs +- **sampler_name**: Selected sampler algorithm +- **scheduler**: Selected scheduler algorithm +- **steps**: Number of sampling steps +- **cfg**: CFG scale value + +## Available Samplers + +### Primary Samplers +| Sampler | Type | Speed | Quality | Best For | +|---------|------|-------|---------|----------| +| euler | Deterministic | Fast | Good | General use | +| euler_ancestral | Stochastic | Fast | Good | Creative variation | +| heun | Higher-order | Medium | Better | Quality focus | +| dpm_2 | Multi-step | Medium | Good | Balanced | +| dpm_2_ancestral | Stochastic | Medium | Good | Creative quality | +| lms | Linear | Fast | Good | Simple scenes | +| dpm_fast | Optimized | Very Fast | Good | Speed priority | +| dpm_adaptive | Adaptive | Variable | Best | Automatic tuning | + +### Advanced Samplers +| Sampler | Type | Speed | Quality | Best For | +|---------|------|-------|---------|----------| +| dpmpp_2s_ancestral | Advanced | Medium | Better | High quality | +| dpmpp_2m | Optimized | Fast | Better | Speed + quality | +| dpmpp_2m_sde | Stochastic | Medium | Best | Maximum quality | +| dpmpp_3m_sde | Latest | Medium | Best | Cutting edge | +| ddim | Classic | Fast | Good | Compatibility | +| uni_pc | Unified | Fast | Better | Efficiency | + +## Available Schedulers + +### Linear Schedulers +- **normal**: Standard linear schedule +- **linear**: Basic linear distribution +- **sgm_uniform**: Uniform distribution + +### Advanced Schedulers +- **karras**: Karras noise schedule (recommended) +- **exponential**: Exponential decay +- **polyexponential**: Polynomial exponential +- **beta**: Beta distribution schedule + +### Specialized Schedulers +- **cosine**: Cosine annealing schedule +- **simple**: Simplified schedule +- **ddim_uniform**: DDIM uniform schedule +- **laplace**: Laplace distribution + +## Optimization Guidelines + +### Recommended Combinations + +#### Speed Optimized +``` +Sampler: euler or dpm_fast +Scheduler: normal or simple +Steps: 15-25 +CFG: 6.0-8.0 +``` + +#### Quality Optimized +``` +Sampler: dpmpp_2m_sde or dpmpp_3m_sde +Scheduler: karras +Steps: 25-35 +CFG: 7.0-9.0 +``` + +#### Balanced +``` +Sampler: dpmpp_2m or heun +Scheduler: karras or normal +Steps: 20-30 +CFG: 7.0-8.5 +``` + +### Steps Recommendations + +| Sampler Type | Min Steps | Optimal | Max Steps | +|--------------|-----------|---------|-----------| +| Fast (euler, dpm_fast) | 10 | 20 | 30 | +| Standard (heun, dpm_2) | 15 | 25 | 40 | +| Advanced (dpmpp_*) | 20 | 30 | 50 | +| Adaptive | 10 | 25 | 100 | + +### CFG Scale Guidelines + +| Content Type | CFG Range | Recommended | +|--------------|-----------|-------------| +| Photorealistic | 5.0-8.0 | 7.0 | +| Artistic/Stylized | 7.0-12.0 | 9.0 | +| Abstract/Creative | 8.0-15.0 | 11.0 | +| Text/Details | 10.0-20.0 | 13.0 | + +## Usage Examples + +### Basic Configuration +``` +sampler_name: euler +scheduler: normal +steps: 20 +cfg: 7.0 +``` + +### High Quality Setup +``` +sampler_name: dpmpp_2m_sde +scheduler: karras +steps: 30 +cfg: 8.0 +``` + +### Speed Priority +``` +sampler_name: dpm_fast +scheduler: simple +steps: 15 +cfg: 6.5 +``` + +## Advanced Features + +### Compatibility Analysis +The node provides real-time analysis of parameter compatibility: +- Scheduler compatibility with selected sampler +- Steps optimization for sampler type +- CFG scale recommendations +- Performance impact assessment + +### Error Handling +- Invalid samplers default to 'euler' +- Invalid schedulers default to 'normal' +- Out-of-range steps clamped to valid range +- Invalid CFG values sanitized to safe defaults + +### Performance Tips +1. **Use Karras scheduler** for most samplers (better quality) +2. **Start with 20-30 steps** for most use cases +3. **Keep CFG 6.0-9.0** for realistic images +4. **Try dpmpp_2m** for best speed/quality balance +5. **Use euler** for fastest generation + +## Troubleshooting + +### Common Issues +- **Slow generation**: Try euler or dpm_fast samplers +- **Poor quality**: Increase steps or try dpmpp_2m_sde +- **Overcooked images**: Lower CFG scale +- **Underdetailed**: Increase CFG or steps +- **Artifacts**: Try karras scheduler or different sampler + +### Performance Optimization +- **GPU Memory**: Lower steps if running out of VRAM +- **Speed**: Use euler + normal + 15-20 steps +- **Quality**: Use dpmpp_2m_sde + karras + 25-30 steps +- **Compatibility**: Stick to euler/heun for broad model support + +## Integration + +The Sampler Combo node outputs are compatible with all standard ComfyUI sampling nodes: +- KSampler +- KSamplerAdvanced +- Custom sampling workflows +- Upscaling pipelines +- Img2img workflows + +Connect the outputs directly to your sampling node inputs for streamlined configuration. \ No newline at end of file From c7e02a45651715dae1573c1f0fba785df9f94bdd Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:39:19 -0700 Subject: [PATCH 06/10] feat(examples): add sampler combo workflow example --- examples/workflows/sampler_combo_example.json | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 examples/workflows/sampler_combo_example.json diff --git a/examples/workflows/sampler_combo_example.json b/examples/workflows/sampler_combo_example.json new file mode 100644 index 0000000..1c0cd44 --- /dev/null +++ b/examples/workflows/sampler_combo_example.json @@ -0,0 +1,317 @@ +{ + "last_node_id": 10, + "last_link_id": 10, + "nodes": [ + { + "id": 1, + "type": "SamplerCombo", + "pos": [100, 100], + "size": [300, 200], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "sampler_name", + "type": "COMBO", + "links": [1], + "shape": 3, + "slot_index": 0 + }, + { + "name": "scheduler", + "type": "COMBO", + "links": [2], + "shape": 3, + "slot_index": 1 + }, + { + "name": "steps", + "type": "INT", + "links": [3], + "shape": 3, + "slot_index": 2 + }, + { + "name": "cfg", + "type": "FLOAT", + "links": [4], + "shape": 3, + "slot_index": 3 + } + ], + "properties": { + "Node name for S&R": "SamplerCombo" + }, + "widgets_values": [ + "dpmpp_2m", + "karras", + 25, + 7.5 + ], + "title": "Sampler Combo" + }, + { + "id": 2, + "type": "CheckpointLoaderSimple", + "pos": [100, 350], + "size": [315, 98], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [5], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [6, 7], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [8], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "flux1-dev.safetensors" + ] + }, + { + "id": 3, + "type": "CLIPTextEncode", + "pos": [450, 100], + "size": [400, 200], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 6 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [9], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "A beautiful sunset over mountains, highly detailed, photorealistic" + ], + "title": "Positive Prompt" + }, + { + "id": 4, + "type": "CLIPTextEncode", + "pos": [450, 350], + "size": [400, 200], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 7 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [10], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "blurry, low quality, distorted" + ], + "title": "Negative Prompt" + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [100, 500], + "size": [315, 106], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [11], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 6, + "type": "KSampler", + "pos": [900, 100], + "size": [315, 262], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 5 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 9 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 10 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 11 + }, + { + "name": "sampler_name", + "type": "COMBO", + "link": 1 + }, + { + "name": "scheduler", + "type": "COMBO", + "link": 2 + }, + { + "name": "steps", + "type": "INT", + "link": 3 + }, + { + "name": "cfg", + "type": "FLOAT", + "link": 4 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [12], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 12345, + "randomize", + 20, + 7.0, + "euler", + "normal", + 1.0 + ], + "title": "KSampler (using Sampler Combo)" + }, + { + "id": 7, + "type": "VAEDecode", + "pos": [1250, 100], + "size": [210, 46], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 12 + }, + { + "name": "vae", + "type": "VAE", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + } + ], + "links": [ + [1, 1, 0, 6, 4, "COMBO"], + [2, 1, 1, 6, 5, "COMBO"], + [3, 1, 2, 6, 6, "INT"], + [4, 1, 3, 6, 7, "FLOAT"], + [5, 2, 0, 6, 0, "MODEL"], + [6, 2, 1, 3, 0, "CLIP"], + [7, 2, 1, 4, 0, "CLIP"], + [8, 2, 2, 7, 1, "VAE"], + [9, 3, 0, 6, 1, "CONDITIONING"], + [10, 4, 0, 6, 2, "CONDITIONING"], + [11, 5, 0, 6, 3, "LATENT"], + [12, 6, 0, 7, 0, "LATENT"] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.8264462809917355, + "offset": [ + -49.66116272043009, + -10.826446280991736 + ] + } + }, + "version": 0.4 +} \ No newline at end of file From f04020b7284c48b54245f56f40261dacc5ec99ef Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:48:13 -0700 Subject: [PATCH 07/10] refactor(node): Simplify input validation logic --- kikotools/tools/sampler_combo/node.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/kikotools/tools/sampler_combo/node.py b/kikotools/tools/sampler_combo/node.py index d883bb9..141f906 100644 --- a/kikotools/tools/sampler_combo/node.py +++ b/kikotools/tools/sampler_combo/node.py @@ -87,12 +87,16 @@ class SamplerComboNode(ComfyAssetsBaseNode): """ try: # Validate inputs - self.validate_inputs( - sampler_name=sampler_name, - scheduler=scheduler, - steps=steps, - cfg=cfg, - ) + if not validate_sampler_settings(sampler_name, scheduler, steps, cfg): + # Log the validation error but don't raise + import logging + logger = logging.getLogger(__name__) + logger.error( + f"{self.__class__.__name__}: Invalid sampler settings: sampler={sampler_name}, " + f"scheduler={scheduler}, steps={steps}, cfg={cfg}. " + f"Using safe defaults: euler, normal, 20 steps, CFG 7.0" + ) + return ("euler", "normal", 20, 7.0) # Process and return the combo result = get_sampler_combo(sampler_name, scheduler, steps, cfg) @@ -106,11 +110,12 @@ class SamplerComboNode(ComfyAssetsBaseNode): except Exception as e: # Handle any unexpected errors gracefully - error_msg = ( - f"Error processing sampler combo: {str(e)}. " + import logging + logger = logging.getLogger(__name__) + logger.error( + f"{self.__class__.__name__}: Error processing sampler combo: {str(e)}. " f"Using safe defaults: euler, normal, 20 steps, CFG 7.0" ) - self.handle_error(error_msg) return ("euler", "normal", 20, 7.0) def validate_inputs( From 549d2dc0148217809146f2616dae5af84b86cdd3 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:54:34 -0700 Subject: [PATCH 08/10] style: Reformat code for better readability --- kikotools/tools/sampler_combo/node.py | 7 +- kikotools/tools/width_height_selector/node.py | 26 +- .../tools/width_height_selector/presets.py | 404 +++++++++++++----- 3 files changed, 321 insertions(+), 116 deletions(-) diff --git a/kikotools/tools/sampler_combo/node.py b/kikotools/tools/sampler_combo/node.py index 141f906..09cfe7b 100644 --- a/kikotools/tools/sampler_combo/node.py +++ b/kikotools/tools/sampler_combo/node.py @@ -90,10 +90,12 @@ class SamplerComboNode(ComfyAssetsBaseNode): if not validate_sampler_settings(sampler_name, scheduler, steps, cfg): # Log the validation error but don't raise import logging + logger = logging.getLogger(__name__) logger.error( - f"{self.__class__.__name__}: Invalid sampler settings: sampler={sampler_name}, " - f"scheduler={scheduler}, steps={steps}, cfg={cfg}. " + f"{self.__class__.__name__}: Invalid sampler settings: " + f"sampler={sampler_name}, scheduler={scheduler}, " + f"steps={steps}, cfg={cfg}. " f"Using safe defaults: euler, normal, 20 steps, CFG 7.0" ) return ("euler", "normal", 20, 7.0) @@ -111,6 +113,7 @@ class SamplerComboNode(ComfyAssetsBaseNode): except Exception as e: # Handle any unexpected errors gracefully import logging + logger = logging.getLogger(__name__) logger.error( f"{self.__class__.__name__}: Error processing sampler combo: {str(e)}. " diff --git a/kikotools/tools/width_height_selector/node.py b/kikotools/tools/width_height_selector/node.py index eb8b96f..316689f 100644 --- a/kikotools/tools/width_height_selector/node.py +++ b/kikotools/tools/width_height_selector/node.py @@ -4,13 +4,11 @@ from typing import Tuple from ...base.base_node import ComfyAssetsBaseNode from .logic import ( get_preset_dimensions, - calculate_aspect_ratio, validate_dimensions, sanitize_dimensions, ) from .presets import ( PRESET_OPTIONS, - PRESET_DESCRIPTIONS, PRESET_METADATA, get_model_recommendation, get_preset_metadata, @@ -31,7 +29,7 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): """Define the input types for the ComfyUI node.""" # Create formatted preset options with metadata preset_options = ["custom"] # Custom first - + # Add formatted presets with metadata for preset_name in PRESET_OPTIONS.keys(): if preset_name != "custom": @@ -104,9 +102,11 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): try: # Extract original preset name from formatted string if needed original_preset = self._extract_preset_name(preset) - + # Get base dimensions from preset or custom input - final_width, final_height = get_preset_dimensions(original_preset, width, height) + final_width, final_height = get_preset_dimensions( + original_preset, width, height + ) # Sanitize dimensions to ensure they meet ComfyUI requirements final_width, final_height = sanitize_dimensions(final_width, final_height) @@ -129,35 +129,35 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): ) self.handle_error(error_msg) return (1024, 1024) - + def _extract_preset_name(self, formatted_preset: str) -> str: """ Extract the original preset name from a formatted preset string. - + Args: formatted_preset: Either original preset name or formatted string - + Returns: Original preset name """ # If it's already "custom", return as-is if formatted_preset == "custom": return formatted_preset - + # If it contains formatting metadata, extract the resolution part if " - " in formatted_preset: # Format is: "1024×1024 - 1:1 (1.0MP) - SDXL" # Extract the first part (resolution) resolution_part = formatted_preset.split(" - ")[0] - + # Verify this is a valid preset name if resolution_part in PRESET_OPTIONS: return resolution_part - + # If no formatting or not found, check if it's directly a valid preset if formatted_preset in PRESET_OPTIONS: return formatted_preset - + # Default to "custom" if we can't parse it return "custom" @@ -209,7 +209,7 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode): """ # Extract original preset name original_preset = self._extract_preset_name(preset) - + # Check if preset exists or is custom if original_preset != "custom" and original_preset not in PRESET_OPTIONS: return False diff --git a/kikotools/tools/width_height_selector/presets.py b/kikotools/tools/width_height_selector/presets.py index b498aed..fa58042 100644 --- a/kikotools/tools/width_height_selector/presets.py +++ b/kikotools/tools/width_height_selector/presets.py @@ -6,6 +6,7 @@ from fractions import Fraction class PresetMetadata(NamedTuple): """Metadata for a resolution preset.""" + width: int height: int aspect_ratio: str @@ -27,175 +28,364 @@ def calculate_aspect_ratio(width: int, height: int) -> Tuple[str, float]: PRESET_METADATA: Dict[str, PresetMetadata] = { # SDXL Presets - Square "1024×1024": PresetMetadata( - 1024, 1024, "1:1", 1.0, 1.05, "SDXL", "Square", - "SDXL base resolution - perfect square" + 1024, + 1024, + "1:1", + 1.0, + 1.05, + "SDXL", + "Square", + "SDXL base resolution - perfect square", ), - # SDXL Presets - Portrait "896×1152": PresetMetadata( - 896, 1152, "7:9", 0.778, 1.03, "SDXL", "Portrait", - "SDXL portrait 7:9 - moderate portrait" + 896, + 1152, + "7:9", + 0.778, + 1.03, + "SDXL", + "Portrait", + "SDXL portrait 7:9 - moderate portrait", ), "832×1216": PresetMetadata( - 832, 1216, "13:19", 0.684, 1.01, "SDXL", "Portrait", - "SDXL portrait 13:19 - standard portrait" + 832, + 1216, + "13:19", + 0.684, + 1.01, + "SDXL", + "Portrait", + "SDXL portrait 13:19 - standard portrait", ), "768×1344": PresetMetadata( - 768, 1344, "4:7", 0.571, 1.03, "SDXL", "Portrait", - "SDXL portrait 4:7 - tall portrait" + 768, + 1344, + "4:7", + 0.571, + 1.03, + "SDXL", + "Portrait", + "SDXL portrait 4:7 - tall portrait", ), "640×1536": PresetMetadata( - 640, 1536, "5:12", 0.417, 0.98, "SDXL", "Portrait", - "SDXL portrait 5:12 - very tall portrait" + 640, + 1536, + "5:12", + 0.417, + 0.98, + "SDXL", + "Portrait", + "SDXL portrait 5:12 - very tall portrait", ), - # SDXL Presets - Landscape "1152×896": PresetMetadata( - 1152, 896, "9:7", 1.286, 1.03, "SDXL", "Landscape", - "SDXL landscape 9:7 - moderate landscape" + 1152, + 896, + "9:7", + 1.286, + 1.03, + "SDXL", + "Landscape", + "SDXL landscape 9:7 - moderate landscape", ), "1216×832": PresetMetadata( - 1216, 832, "19:13", 1.462, 1.01, "SDXL", "Landscape", - "SDXL landscape 19:13 - standard landscape" + 1216, + 832, + "19:13", + 1.462, + 1.01, + "SDXL", + "Landscape", + "SDXL landscape 19:13 - standard landscape", ), "1344×768": PresetMetadata( - 1344, 768, "7:4", 1.750, 1.03, "SDXL", "Landscape", - "SDXL landscape 7:4 - wide landscape" + 1344, + 768, + "7:4", + 1.750, + 1.03, + "SDXL", + "Landscape", + "SDXL landscape 7:4 - wide landscape", ), "1536×640": PresetMetadata( - 1536, 640, "12:5", 2.400, 0.98, "SDXL", "Landscape", - "SDXL landscape 12:5 - very wide landscape" + 1536, + 640, + "12:5", + 2.400, + 0.98, + "SDXL", + "Landscape", + "SDXL landscape 12:5 - very wide landscape", ), - # FLUX Presets - High Quality "1920×1080": PresetMetadata( - 1920, 1080, "16:9", 1.778, 2.07, "FLUX", "Cinematic", - "FLUX Full HD 16:9 - best quality/speed balance" + 1920, + 1080, + "16:9", + 1.778, + 2.07, + "FLUX", + "Cinematic", + "FLUX Full HD 16:9 - best quality/speed balance", ), "1536×1536": PresetMetadata( - 1536, 1536, "1:1", 1.0, 2.36, "FLUX", "Square", - "FLUX high-res square - premium quality" + 1536, + 1536, + "1:1", + 1.0, + 2.36, + "FLUX", + "Square", + "FLUX high-res square - premium quality", ), "1280×768": PresetMetadata( - 1280, 768, "5:3", 1.667, 0.98, "FLUX", "Cinematic", - "FLUX 5:3 landscape - cinematic wide" + 1280, + 768, + "5:3", + 1.667, + 0.98, + "FLUX", + "Cinematic", + "FLUX 5:3 landscape - cinematic wide", ), "768×1280": PresetMetadata( - 768, 1280, "3:5", 0.600, 0.98, "FLUX", "Portrait", - "FLUX 3:5 portrait - mobile optimized" + 768, + 1280, + "3:5", + 0.600, + 0.98, + "FLUX", + "Portrait", + "FLUX 3:5 portrait - mobile optimized", ), - # FLUX Presets - Alternative "1440×1080": PresetMetadata( - 1440, 1080, "4:3", 1.333, 1.56, "FLUX", "Classic", - "FLUX 4:3 classic - traditional aspect ratio" + 1440, + 1080, + "4:3", + 1.333, + 1.56, + "FLUX", + "Classic", + "FLUX 4:3 classic - traditional aspect ratio", ), "1080×1440": PresetMetadata( - 1080, 1440, "3:4", 0.750, 1.56, "FLUX", "Portrait", - "FLUX 3:4 portrait - classic portrait" + 1080, + 1440, + "3:4", + 0.750, + 1.56, + "FLUX", + "Portrait", + "FLUX 3:4 portrait - classic portrait", ), "1728×1152": PresetMetadata( - 1728, 1152, "3:2", 1.500, 1.99, "FLUX", "Photography", - "FLUX 3:2 photo - photography standard" + 1728, + 1152, + "3:2", + 1.500, + 1.99, + "FLUX", + "Photography", + "FLUX 3:2 photo - photography standard", ), "1152×1728": PresetMetadata( - 1152, 1728, "2:3", 0.667, 1.99, "FLUX", "Portrait", - "FLUX 2:3 portrait - portrait photography" + 1152, + 1728, + "2:3", + 0.667, + 1.99, + "FLUX", + "Portrait", + "FLUX 2:3 portrait - portrait photography", ), - # Ultra-Wide Presets - Landscape "2560×1080": PresetMetadata( - 2560, 1080, "64:27", 2.370, 2.76, "Ultra-Wide", "Gaming", - "Ultra-wide 64:27 - gaming/panoramic" + 2560, + 1080, + "64:27", + 2.370, + 2.76, + "Ultra-Wide", + "Gaming", + "Ultra-wide 64:27 - gaming/panoramic", ), "2048×768": PresetMetadata( - 2048, 768, "8:3", 2.667, 1.57, "Ultra-Wide", "Cinematic", - "Wide cinematic 8:3 - movie aspect" + 2048, + 768, + "8:3", + 2.667, + 1.57, + "Ultra-Wide", + "Cinematic", + "Wide cinematic 8:3 - movie aspect", ), "1792×768": PresetMetadata( - 1792, 768, "7:3", 2.333, 1.38, "Ultra-Wide", "Panoramic", - "Panoramic 7:3 - landscape vista" + 1792, + 768, + "7:3", + 2.333, + 1.38, + "Ultra-Wide", + "Panoramic", + "Panoramic 7:3 - landscape vista", ), "2304×768": PresetMetadata( - 2304, 768, "3:1", 3.000, 1.77, "Ultra-Wide", "Banner", - "Banner 3:1 - extreme wide banner" + 2304, + 768, + "3:1", + 3.000, + 1.77, + "Ultra-Wide", + "Banner", + "Banner 3:1 - extreme wide banner", ), - # Ultra-Wide Presets - Portrait "1080×2560": PresetMetadata( - 1080, 2560, "27:64", 0.422, 2.76, "Ultra-Wide", "Mobile", - "Mobile ultra-tall 27:64 - modern phones" + 1080, + 2560, + "27:64", + 0.422, + 2.76, + "Ultra-Wide", + "Mobile", + "Mobile ultra-tall 27:64 - modern phones", ), "768×2048": PresetMetadata( - 768, 2048, "3:8", 0.375, 1.57, "Ultra-Wide", "Vertical", - "Vertical cinematic 3:8 - portrait video" + 768, + 2048, + "3:8", + 0.375, + 1.57, + "Ultra-Wide", + "Vertical", + "Vertical cinematic 3:8 - portrait video", ), "768×1792": PresetMetadata( - 768, 1792, "3:7", 0.429, 1.38, "Ultra-Wide", "Vertical", - "Vertical panoramic 3:7 - tall vista" + 768, + 1792, + "3:7", + 0.429, + 1.38, + "Ultra-Wide", + "Vertical", + "Vertical panoramic 3:7 - tall vista", ), "768×2304": PresetMetadata( - 768, 2304, "1:3", 0.333, 1.77, "Ultra-Wide", "Banner", - "Vertical banner 1:3 - extreme tall banner" + 768, + 2304, + "1:3", + 0.333, + 1.77, + "Ultra-Wide", + "Banner", + "Vertical banner 1:3 - extreme tall banner", ), } # Legacy compatibility - maintain old preset dictionaries SDXL_PRESETS: Dict[str, Tuple[int, int]] = { - k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" + k: (v.width, v.height) + for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" } FLUX_PRESETS: Dict[str, Tuple[int, int]] = { - k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" + k: (v.width, v.height) + for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" } ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = { - k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" + k: (v.width, v.height) + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" } # Combined preset options for ComfyUI dropdown PRESET_OPTIONS: Dict[str, Tuple[int, int]] = { "custom": (0, 0), # Special case for custom dimensions - **{k: (v.width, v.height) for k, v in PRESET_METADATA.items()} + **{k: (v.width, v.height) for k, v in PRESET_METADATA.items()}, } # Enhanced preset categories organized by model groups and aspect ratios PRESET_CATEGORIES = { "Custom": ["custom"], - # SDXL Categories - "SDXL Square": [k for k, v in PRESET_METADATA.items() - if v.model_group == "SDXL" and v.category == "Square"], - "SDXL Portrait": [k for k, v in PRESET_METADATA.items() - if v.model_group == "SDXL" and v.category == "Portrait"], - "SDXL Landscape": [k for k, v in PRESET_METADATA.items() - if v.model_group == "SDXL" and v.category == "Landscape"], - + "SDXL Square": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" and v.category == "Square" + ], + "SDXL Portrait": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" and v.category == "Portrait" + ], + "SDXL Landscape": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "SDXL" and v.category == "Landscape" + ], # FLUX Categories - "FLUX Square": [k for k, v in PRESET_METADATA.items() - if v.model_group == "FLUX" and v.category == "Square"], - "FLUX Portrait": [k for k, v in PRESET_METADATA.items() - if v.model_group == "FLUX" and v.category == "Portrait"], - "FLUX Cinematic": [k for k, v in PRESET_METADATA.items() - if v.model_group == "FLUX" and v.category == "Cinematic"], - "FLUX Classic": [k for k, v in PRESET_METADATA.items() - if v.model_group == "FLUX" and v.category == "Classic"], - "FLUX Photography": [k for k, v in PRESET_METADATA.items() - if v.model_group == "FLUX" and v.category == "Photography"], - + "FLUX Square": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Square" + ], + "FLUX Portrait": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Portrait" + ], + "FLUX Cinematic": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Cinematic" + ], + "FLUX Classic": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Classic" + ], + "FLUX Photography": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "FLUX" and v.category == "Photography" + ], # Ultra-Wide Categories - "Ultra-Wide Gaming": [k for k, v in PRESET_METADATA.items() - if v.model_group == "Ultra-Wide" and v.category == "Gaming"], - "Ultra-Wide Cinematic": [k for k, v in PRESET_METADATA.items() - if v.model_group == "Ultra-Wide" and v.category == "Cinematic"], - "Ultra-Wide Panoramic": [k for k, v in PRESET_METADATA.items() - if v.model_group == "Ultra-Wide" and v.category == "Panoramic"], - "Ultra-Wide Mobile": [k for k, v in PRESET_METADATA.items() - if v.model_group == "Ultra-Wide" and v.category == "Mobile"], - "Ultra-Wide Vertical": [k for k, v in PRESET_METADATA.items() - if v.model_group == "Ultra-Wide" and v.category == "Vertical"], - "Ultra-Wide Banner": [k for k, v in PRESET_METADATA.items() - if v.model_group == "Ultra-Wide" and v.category == "Banner"], + "Ultra-Wide Gaming": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Gaming" + ], + "Ultra-Wide Cinematic": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Cinematic" + ], + "Ultra-Wide Panoramic": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Panoramic" + ], + "Ultra-Wide Mobile": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Mobile" + ], + "Ultra-Wide Vertical": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Vertical" + ], + "Ultra-Wide Banner": [ + k + for k, v in PRESET_METADATA.items() + if v.model_group == "Ultra-Wide" and v.category == "Banner" + ], } # Legacy compatibility - preset descriptions @@ -205,9 +395,12 @@ PRESET_DESCRIPTIONS = {k: v.description for k, v in PRESET_METADATA.items()} MODEL_RECOMMENDATIONS = { "SDXL": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"], "FLUX": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"], - "Ultra-Wide": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"], + "Ultra-Wide": [ + k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" + ], } + # New metadata-aware helper functions def get_presets_by_model_group(model_group: str) -> Dict[str, PresetMetadata]: """Get all presets for a specific model group.""" @@ -226,9 +419,10 @@ def get_presets_by_category(category: str) -> Dict[str, PresetMetadata]: def get_preset_metadata(preset_name: str) -> PresetMetadata: """Get metadata for a specific preset.""" - return PRESET_METADATA.get(preset_name, PresetMetadata( - 0, 0, "1:1", 1.0, 0.0, "Custom", "Custom", "Custom dimensions" - )) + return PRESET_METADATA.get( + preset_name, + PresetMetadata(0, 0, "1:1", 1.0, 0.0, "Custom", "Custom", "Custom dimensions"), + ) def get_preset_category(preset_name: str) -> str: @@ -254,7 +448,7 @@ def validate_preset_dimensions() -> bool: """Validate that all presets meet ComfyUI requirements.""" for preset_name, metadata in PRESET_METADATA.items(): width, height = metadata.width, metadata.height - + # Check divisible by 8 if width % 8 != 0 or height % 8 != 0: print( @@ -275,17 +469,25 @@ def validate_metadata_consistency() -> bool: """Validate metadata consistency and completeness.""" for preset_name, metadata in PRESET_METADATA.items(): # Verify aspect ratio calculation - expected_ratio, expected_decimal = calculate_aspect_ratio(metadata.width, metadata.height) + expected_ratio, expected_decimal = calculate_aspect_ratio( + metadata.width, metadata.height + ) if abs(metadata.aspect_decimal - expected_decimal) > 0.001: - print(f"ERROR: {preset_name} aspect ratio mismatch: expected {expected_decimal:.3f}, got {metadata.aspect_decimal}") + print( + f"ERROR: {preset_name} aspect ratio mismatch: " + f"expected {expected_decimal:.3f}, got {metadata.aspect_decimal}" + ) return False - + # Verify megapixel calculation expected_mp = (metadata.width * metadata.height) / 1_000_000 if abs(metadata.megapixels - expected_mp) > 0.1: - print(f"ERROR: {preset_name} megapixel mismatch: expected {expected_mp:.2f}, got {metadata.megapixels}") + print( + f"ERROR: {preset_name} megapixel mismatch: " + f"expected {expected_mp:.2f}, got {metadata.megapixels}" + ) return False - + return True From be0c70eab15b91f121af6175492cd421a7037028 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:54:47 -0700 Subject: [PATCH 09/10] style(test_width_height_selector): format code for readability --- .../unit/tools/test_width_height_selector.py | 124 +++++++++++------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/tests/unit/tools/test_width_height_selector.py b/tests/unit/tools/test_width_height_selector.py index 2c2c063..18c8ad7 100644 --- a/tests/unit/tools/test_width_height_selector.py +++ b/tests/unit/tools/test_width_height_selector.py @@ -53,7 +53,7 @@ class TestWidthHeightSelectorNode: height=512, # Should be ignored ) assert result == (1024, 1024) - + # Test formatted preset result = self.node.get_dimensions( preset="1024×1024 - 1:1 (1.1MP) - SDXL", @@ -67,10 +67,12 @@ class TestWidthHeightSelectorNode: # Test raw preset result = self.node.get_dimensions(preset="832×1216", width=512, height=512) assert result == (832, 1216) - + # Test formatted preset if available formatted_preset = "832×1216 - 13:19 (1.0MP) - SDXL" - result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + result = self.node.get_dimensions( + preset=formatted_preset, width=512, height=512 + ) assert result == (832, 1216) def test_sdxl_landscape_preset(self): @@ -78,10 +80,12 @@ class TestWidthHeightSelectorNode: # Test raw preset result = self.node.get_dimensions(preset="1216×832", width=512, height=512) assert result == (1216, 832) - + # Test formatted preset if available formatted_preset = "1216×832 - 19:13 (1.0MP) - SDXL" - result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + result = self.node.get_dimensions( + preset=formatted_preset, width=512, height=512 + ) assert result == (1216, 832) def test_flux_preset(self): @@ -89,10 +93,12 @@ class TestWidthHeightSelectorNode: # Test raw preset result = self.node.get_dimensions(preset="1920×1080", width=512, height=512) assert result == (1920, 1080) - + # Test formatted preset formatted_preset = "1920×1080 - 16:9 (2.1MP) - FLUX" - result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + result = self.node.get_dimensions( + preset=formatted_preset, width=512, height=512 + ) assert result == (1920, 1080) def test_ultra_wide_preset(self): @@ -100,10 +106,12 @@ class TestWidthHeightSelectorNode: # Test raw preset result = self.node.get_dimensions(preset="2560×1080", width=512, height=512) assert result == (2560, 1080) - + # Test formatted preset if available formatted_preset = "2560×1080 - 64:27 (2.8MP) - Ultra-Wide" - result = self.node.get_dimensions(preset=formatted_preset, width=512, height=512) + result = self.node.get_dimensions( + preset=formatted_preset, width=512, height=512 + ) assert result == (2560, 1080) def test_all_presets_available(self): @@ -113,7 +121,7 @@ class TestWidthHeightSelectorNode: # Check that custom is available assert "custom" in available_presets - + # Check that formatted presets are available (with metadata) # Extract raw preset names from formatted options raw_presets = [] @@ -124,7 +132,7 @@ class TestWidthHeightSelectorNode: raw_presets.append(option.split(" - ")[0]) else: raw_presets.append(option) - + # Check that all major preset categories are available assert "1024×1024" in raw_presets # SDXL square assert "832×1216" in raw_presets # SDXL portrait @@ -316,21 +324,21 @@ class TestPresetMetadata: def test_preset_metadata_structure(self): """Test that metadata has correct structure.""" for preset_name, metadata in PRESET_METADATA.items(): - assert hasattr(metadata, 'width') - assert hasattr(metadata, 'height') - assert hasattr(metadata, 'aspect_ratio') - assert hasattr(metadata, 'aspect_decimal') - assert hasattr(metadata, 'megapixels') - assert hasattr(metadata, 'model_group') - assert hasattr(metadata, 'category') - assert hasattr(metadata, 'description') + assert hasattr(metadata, "width") + assert hasattr(metadata, "height") + assert hasattr(metadata, "aspect_ratio") + assert hasattr(metadata, "aspect_decimal") + assert hasattr(metadata, "megapixels") + assert hasattr(metadata, "model_group") + assert hasattr(metadata, "category") + assert hasattr(metadata, "description") def test_metadata_aspect_ratios(self): """Test that aspect ratios are correctly calculated.""" for preset_name, metadata in PRESET_METADATA.items(): expected_decimal = metadata.width / metadata.height assert abs(metadata.aspect_decimal - expected_decimal) < 0.001 - + # Common aspect ratios should match expected values if preset_name == "1024×1024": assert metadata.aspect_ratio == "1:1" @@ -350,11 +358,11 @@ class TestPresetMetadata: sdxl_presets = get_presets_by_model_group("SDXL") flux_presets = get_presets_by_model_group("FLUX") ultra_wide_presets = get_presets_by_model_group("Ultra-Wide") - + assert len(sdxl_presets) > 0 assert len(flux_presets) > 0 assert len(ultra_wide_presets) > 0 - + # Check specific presets are in correct groups assert "1024×1024" in [k for k, v in sdxl_presets.items()] assert "1920×1080" in [k for k, v in flux_presets.items()] @@ -367,7 +375,7 @@ class TestPresetMetadata: assert metadata.width == 1024 assert metadata.height == 1024 assert metadata.model_group == "SDXL" - + # Invalid preset returns default metadata = get_preset_metadata("invalid_preset") assert metadata.width == 0 @@ -388,13 +396,13 @@ class TestNodeMetadataIntegration: assert "1:1" in info # Aspect ratio assert "1.0MP" in info or "1.1MP" in info # Megapixels assert "SDXL" in info # Description - + def test_get_presets_by_model_static(self): """Test static method for getting presets by model.""" sdxl_presets = self.node.get_presets_by_model("SDXL") assert isinstance(sdxl_presets, dict) assert len(sdxl_presets) > 0 - + # Check that returned values are metadata objects for preset_name, metadata in sdxl_presets.items(): assert metadata.model_group == "SDXL" @@ -402,12 +410,12 @@ class TestNodeMetadataIntegration: def test_get_preset_metadata_static(self): """Test static method for getting preset metadata.""" metadata_dict = self.node.get_preset_metadata_static("1920×1080") - + assert metadata_dict["width"] == 1920 assert metadata_dict["height"] == 1080 assert metadata_dict["aspect_ratio"] == "16:9" assert metadata_dict["model_group"] == "FLUX" - + def test_get_model_groups(self): """Test static method for getting model groups.""" groups = self.node.get_model_groups() @@ -421,12 +429,18 @@ class TestMetadataValidation: def test_dimensions_validation(self): """Test dimensions validation from metadata.""" - from kikotools.tools.width_height_selector.presets import validate_preset_dimensions + from kikotools.tools.width_height_selector.presets import ( + validate_preset_dimensions, + ) + assert validate_preset_dimensions() is True def test_metadata_consistency_validation(self): """Test metadata consistency validation.""" - from kikotools.tools.width_height_selector.presets import validate_metadata_consistency + from kikotools.tools.width_height_selector.presets import ( + validate_metadata_consistency, + ) + assert validate_metadata_consistency() is True @@ -441,16 +455,16 @@ class TestFormattedPresets: """Test that INPUT_TYPES generates formatted presets.""" input_types = self.node.INPUT_TYPES() available_presets = input_types["required"]["preset"][0] - + # Should have custom first assert available_presets[0] == "custom" - + # Should have formatted presets with metadata formatted_count = 0 for option in available_presets[1:]: # Skip custom if " - " in option and "MP" in option: formatted_count += 1 - + assert formatted_count > 0, "No formatted presets found" assert formatted_count == len(PRESET_METADATA), "Not all presets are formatted" @@ -464,10 +478,12 @@ class TestFormattedPresets: ("1024×1024", "1024×1024"), # Raw preset name ("invalid_preset", "custom"), # Invalid fallback ] - + for formatted_preset, expected in test_cases: result = self.node._extract_preset_name(formatted_preset) - assert result == expected, f"Expected {expected}, got {result} for input {formatted_preset}" + assert ( + result == expected + ), f"Expected {expected}, got {result} for input {formatted_preset}" def test_formatted_preset_dimensions(self): """Test that formatted presets return correct dimensions.""" @@ -485,13 +501,13 @@ class TestFormattedPresets: """Test validation of formatted presets.""" # Valid formatted preset assert self.node.validate_inputs("1024×1024 - 1:1 (1.1MP) - SDXL", 1024, 1024) - + # Valid raw preset assert self.node.validate_inputs("1024×1024", 1024, 1024) - + # Custom preset assert self.node.validate_inputs("custom", 1024, 1024) - + # Invalid formatted preset should still work (fallback to custom) assert self.node.validate_inputs("invalid - formatted", 1024, 1024) @@ -499,7 +515,7 @@ class TestFormattedPresets: """Test that raw preset names still work.""" # Raw preset names should still work for backwards compatibility raw_presets = ["1024×1024", "1920×1080", "832×1216"] - + for raw_preset in raw_presets: if raw_preset in PRESET_OPTIONS: result = self.node.get_dimensions(raw_preset, 512, 512) @@ -509,22 +525,34 @@ class TestFormattedPresets: def test_formatted_preset_metadata_accuracy(self): """Test that formatted presets contain accurate metadata.""" input_types = self.node.INPUT_TYPES() - formatted_presets = [opt for opt in input_types["required"]["preset"][0] if " - " in opt] - + formatted_presets = [ + opt for opt in input_types["required"]["preset"][0] if " - " in opt + ] + for formatted_preset in formatted_presets: # Extract components parts = formatted_preset.split(" - ") - assert len(parts) == 3, f"Formatted preset should have 3 parts: {formatted_preset}" - + assert ( + len(parts) == 3 + ), f"Formatted preset should have 3 parts: {formatted_preset}" + resolution = parts[0] aspect_and_mp = parts[1] model_group = parts[2] - + # Verify resolution exists in metadata - assert resolution in PRESET_METADATA, f"Resolution {resolution} not in metadata" - + assert ( + resolution in PRESET_METADATA + ), f"Resolution {resolution} not in metadata" + # Verify metadata matches format metadata = PRESET_METADATA[resolution] - assert metadata.model_group == model_group, f"Model group mismatch for {resolution}" - assert metadata.aspect_ratio in aspect_and_mp, f"Aspect ratio not in {aspect_and_mp}" - assert f"{metadata.megapixels:.1f}MP" in aspect_and_mp, f"Megapixels not in {aspect_and_mp}" + assert ( + metadata.model_group == model_group + ), f"Model group mismatch for {resolution}" + assert ( + metadata.aspect_ratio in aspect_and_mp + ), f"Aspect ratio not in {aspect_and_mp}" + assert ( + f"{metadata.megapixels:.1f}MP" in aspect_and_mp + ), f"Megapixels not in {aspect_and_mp}" From 965ad60c74d7f25b1acce890d9c06518e46e6d0b Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 15 Jun 2025 08:59:02 -0700 Subject: [PATCH 10/10] refactor(node): use logging for error handling --- kikotools/tools/seed_history/node.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/kikotools/tools/seed_history/node.py b/kikotools/tools/seed_history/node.py index fcb3739..cc070d7 100644 --- a/kikotools/tools/seed_history/node.py +++ b/kikotools/tools/seed_history/node.py @@ -53,8 +53,13 @@ class SeedHistoryNode(ComfyAssetsBaseNode): try: # Validate and sanitize the seed if not validate_seed_value(seed): - self.handle_error( - f"Invalid seed value: {seed}. Using fallback seed 12345." + # Log the validation error but don't raise + import logging + + logger = logging.getLogger(__name__) + logger.error( + f"{self.__class__.__name__}: Invalid seed value: {seed}. " + f"Using fallback seed 12345." ) return (12345,) @@ -64,8 +69,13 @@ class SeedHistoryNode(ComfyAssetsBaseNode): except Exception as e: # Handle any unexpected errors gracefully - error_msg = f"Error processing seed: {str(e)}. Using fallback seed 12345." - self.handle_error(error_msg) + import logging + + logger = logging.getLogger(__name__) + logger.error( + f"{self.__class__.__name__}: Error processing seed: {str(e)}. " + f"Using fallback seed 12345." + ) return (12345,) def generate_new_seed(self) -> int: