Merge pull request #5 from ComfyAssets/resolution-metadata

Resolution metadata
This commit is contained in:
Vito
2025-06-15 09:01:44 -07:00
committed by GitHub
10 changed files with 1838 additions and 197 deletions
+59 -2
View File
@@ -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
+316 -36
View File
@@ -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)
+208
View File
@@ -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.
@@ -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
}
+17 -9
View File
@@ -87,12 +87,18 @@ 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: "
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)
# Process and return the combo
result = get_sampler_combo(sampler_name, scheduler, steps, cfg)
@@ -106,11 +112,13 @@ 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(
+14 -4
View File
@@ -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:
+115 -18
View File
@@ -4,14 +4,15 @@ 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,
get_presets_by_model_group,
)
@@ -26,13 +27,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 +92,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 +100,13 @@ 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)
@@ -111,6 +130,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:
"""
Get descriptive information about a preset.
@@ -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)})"
+440 -93
View File
@@ -1,114 +1,435 @@
"""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 +438,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, metadata in PRESET_METADATA.items():
width, height = metadata.width, metadata.height
for preset_name, (width, height) in all_presets.items():
# Check divisible by 8
if width % 8 != 0 or height % 8 != 0:
print(
@@ -147,6 +464,36 @@ 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: "
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: "
f"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")
+308 -11
View File
@@ -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,7 +45,8 @@ 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
@@ -50,38 +54,91 @@ class TestWidthHeightSelectorNode:
)
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 +316,243 @@ 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}"
+44 -24
View File
@@ -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);