Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd77d06ac9 | ||
|
|
cc725d27f6 | ||
|
|
4c3d3958d6 | ||
|
|
2c992b5c97 | ||
|
|
8628bc39bb | ||
|
|
3654867a21 | ||
|
|
85af1b38f9 | ||
|
|
03189afd85 | ||
|
|
69db6e12b4 | ||
|
|
fb9c313724 | ||
|
|
ffae4e9f21 | ||
|
|
24b257ea6d | ||
|
|
398cf27546 | ||
|
|
6c6c0e6abe | ||
|
|
bc1a34eef2 | ||
|
|
599981cd9a | ||
|
|
880f376e8e | ||
|
|
dcf2d679c1 | ||
|
|
965ad60c74 | ||
|
|
be0c70eab1 | ||
|
|
549d2dc014 | ||
|
|
f04020b728 | ||
|
|
c7e02a4565 | ||
|
|
5af7a56409 | ||
|
|
9833ccd694 | ||
|
|
2d6fef8fb4 | ||
|
|
ce8c36f309 | ||
|
|
bc30806fee | ||
|
|
36b861e778 | ||
|
|
5cf3977744 | ||
|
|
0b542eafbc |
@@ -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
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ComfyAssets' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
+316
-36
@@ -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)
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
.PHONY: help install test test-fast lint format type-check quality-check clean setup dev-test release-test
|
||||
|
||||
# Python and virtual environment setup
|
||||
PYTHON := python3
|
||||
VENV_DIR := venv
|
||||
VENV_BIN := $(VENV_DIR)/bin
|
||||
VENV_PYTHON := $(VENV_BIN)/python
|
||||
VENV_PIP := $(VENV_BIN)/pip
|
||||
|
||||
# Check if we're in a virtual environment, if not use venv
|
||||
ifeq ($(VIRTUAL_ENV),)
|
||||
PYTHON_CMD := $(VENV_PYTHON)
|
||||
PIP_CMD := $(VENV_PIP)
|
||||
else
|
||||
PYTHON_CMD := python
|
||||
PIP_CMD := pip
|
||||
endif
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "ComfyUI-KikoTools Development Commands"
|
||||
@@ -28,44 +44,48 @@ help:
|
||||
@echo " help - Show this help message"
|
||||
|
||||
# Setup and installation
|
||||
setup:
|
||||
@echo "Setting up ComfyUI-KikoTools development environment..."
|
||||
python -m venv venv
|
||||
@echo "Virtual environment created. Activate with:"
|
||||
@echo " source venv/bin/activate (Linux/Mac)"
|
||||
@echo " venv\\Scripts\\activate (Windows)"
|
||||
@echo "Then run: make install"
|
||||
setup: $(VENV_DIR)
|
||||
@echo "✅ ComfyUI-KikoTools development environment ready"
|
||||
@echo "Virtual environment created. Dependencies installed."
|
||||
|
||||
install:
|
||||
$(VENV_DIR):
|
||||
@echo "Creating virtual environment..."
|
||||
$(PYTHON) -m venv $(VENV_DIR)
|
||||
@echo "Installing development dependencies..."
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements-dev.txt
|
||||
$(VENV_PIP) install --upgrade pip
|
||||
$(VENV_PIP) install -r requirements-dev.txt
|
||||
@echo "✅ Virtual environment created and dependencies installed"
|
||||
|
||||
install: $(VENV_DIR)
|
||||
@echo "Installing/updating development dependencies..."
|
||||
$(PIP_CMD) install --upgrade pip
|
||||
$(PIP_CMD) install -r requirements-dev.txt
|
||||
@echo "✅ Dependencies installed"
|
||||
|
||||
# Code quality
|
||||
format:
|
||||
format: $(VENV_DIR)
|
||||
@echo "Formatting code with black..."
|
||||
black .
|
||||
$(PYTHON_CMD) -m black .
|
||||
@echo "✅ Code formatted"
|
||||
|
||||
lint:
|
||||
lint: $(VENV_DIR)
|
||||
@echo "Linting with flake8..."
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
$(PYTHON_CMD) -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=venv
|
||||
$(PYTHON_CMD) -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=venv
|
||||
@echo "✅ Linting completed"
|
||||
|
||||
type-check:
|
||||
type-check: $(VENV_DIR)
|
||||
@echo "Type checking with mypy..."
|
||||
mypy kikotools/ --ignore-missing-imports --no-strict-optional || true
|
||||
$(PYTHON_CMD) -m mypy kikotools/ --ignore-missing-imports --no-strict-optional || true
|
||||
@echo "✅ Type checking completed"
|
||||
|
||||
quality-check: format lint type-check
|
||||
@echo "✅ All quality checks completed"
|
||||
|
||||
# Testing
|
||||
dev-test:
|
||||
dev-test: $(VENV_DIR)
|
||||
@echo "Running quick development test..."
|
||||
@python -c "\
|
||||
@$(PYTHON_CMD) -c "\
|
||||
import sys, os; \
|
||||
sys.path.insert(0, os.getcwd()); \
|
||||
from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; \
|
||||
@@ -75,9 +95,9 @@ dev-test:
|
||||
print(f'✅ Development test passed! Result: {result[0]}x{result[1]}'); \
|
||||
"
|
||||
|
||||
test-fast:
|
||||
test-fast: $(VENV_DIR)
|
||||
@echo "Running core functionality tests..."
|
||||
@python -c "\
|
||||
@$(PYTHON_CMD) -c "\
|
||||
import sys, os; \
|
||||
sys.path.insert(0, os.getcwd()); \
|
||||
from kikotools.base import ComfyAssetsBaseNode; \
|
||||
@@ -146,13 +166,13 @@ ci: quality-check test
|
||||
@echo "✅ CI checks passed!"
|
||||
|
||||
# Tool-specific commands (can be extended for new tools)
|
||||
test-resolution-calculator:
|
||||
test-resolution-calculator: $(VENV_DIR)
|
||||
@echo "Testing Resolution Calculator specifically..."
|
||||
@python -c "import sys, os; sys.path.insert(0, os.getcwd()); from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; import torch; node = ResolutionCalculatorNode(); scenarios = [('SDXL Portrait', torch.randn(1, 1216, 832, 3), 1.5), ('FLUX Square', torch.randn(1, 1024, 1024, 3), 2.0), ('User Scenario', torch.randn(1, 1216, 832, 3), 1.53)]; [print(f'✅ {name}: {image.shape[2]}×{image.shape[1]} → {node.calculate_resolution(scale, image=image)[0]}×{node.calculate_resolution(scale, image=image)[1]} ({scale}x)') for name, image, scale in scenarios]; print('🎉 Resolution Calculator tests completed!')"
|
||||
@$(PYTHON_CMD) -c "import sys, os; sys.path.insert(0, os.getcwd()); from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; import torch; node = ResolutionCalculatorNode(); scenarios = [('SDXL Portrait', torch.randn(1, 1216, 832, 3), 1.5), ('FLUX Square', torch.randn(1, 1024, 1024, 3), 2.0), ('User Scenario', torch.randn(1, 1216, 832, 3), 1.53)]; [print(f'✅ {name}: {image.shape[2]}×{image.shape[1]} → {node.calculate_resolution(scale, image=image)[0]}×{node.calculate_resolution(scale, image=image)[1]} ({scale}x)') for name, image, scale in scenarios]; print('🎉 Resolution Calculator tests completed!')"
|
||||
|
||||
test-width-height-selector:
|
||||
test-width-height-selector: $(VENV_DIR)
|
||||
@echo "Testing Width Height Selector specifically..."
|
||||
@python -c "\
|
||||
@$(PYTHON_CMD) -c "\
|
||||
import sys, os; \
|
||||
sys.path.insert(0, os.getcwd()); \
|
||||
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode; \
|
||||
|
||||
@@ -76,6 +76,22 @@ Unified sampling configuration interface combining sampler, scheduler, steps, an
|
||||
- Reduce node clutter in workflows
|
||||
- Quick sampling parameter experimentation
|
||||
|
||||
#### 📦 Empty Latent Batch
|
||||
Advanced empty latent creation with preset support and batch processing capabilities.
|
||||
|
||||
- **Preset Integration**: 26 curated resolution presets with model optimization
|
||||
- **Batch Processing**: Create multiple empty latents (1-64) in a single operation
|
||||
- **Visual Swap Button**: Interactive blue button for quick dimension swapping
|
||||
- **Smart Validation**: Automatic dimension sanitization for VAE compatibility
|
||||
- **Memory Estimation**: Built-in memory usage calculation and warnings
|
||||
- **Model-Aware Presets**: SDXL (~1MP), FLUX (high-res), and Ultra-wide options
|
||||
|
||||
**Use Cases:**
|
||||
- Initialize batch processing workflows efficiently
|
||||
- Create consistent latent dimensions across model types
|
||||
- Optimize memory usage with batch size planning
|
||||
- Quick preset-based latent generation for different aspect ratios
|
||||
|
||||
### 🔧 Architecture Highlights
|
||||
|
||||
- **Modular Design**: Each tool is self-contained and independently testable
|
||||
@@ -152,6 +168,20 @@ Sampler Combo → KSampler → VAE Decode → Save Image
|
||||
**Output:** Complete sampling configuration in one node
|
||||
**Smart Features:** Recommendations and compatibility validation
|
||||
|
||||
### Empty Latent Batch Example
|
||||
|
||||
```
|
||||
Empty Latent Batch → KSampler → VAE Decode → Save Image
|
||||
📦 preset: "1024×1024" ↘ batch latents ↗
|
||||
batch_size: 4
|
||||
[swap button]
|
||||
```
|
||||
|
||||
**Preset:** SDXL Square (1024×1024)
|
||||
**Batch Size:** 4 empty latents
|
||||
**Output:** 4×4×128×128 latent tensor ready for sampling
|
||||
**Swap Button:** Click to switch to any available swapped preset
|
||||
|
||||
### Common Workflows
|
||||
|
||||
<details>
|
||||
@@ -191,7 +221,8 @@ Sampler Combo → KSampler → VAE Decode → Save Image
|
||||
| **Resolution Calculator** | Calculate upscaled dimensions with model optimization | ✅ Complete | [Docs](examples/documentation/resolution_calculator.md) |
|
||||
| **Width Height Selector** | Preset-based dimension selection with 26 curated options | ✅ Complete | [Docs](examples/documentation/width_height_selector.md) |
|
||||
| **Seed History** | Advanced seed tracking with interactive history management | ✅ Complete | [Docs](examples/documentation/seed_history.md) |
|
||||
| **Sampler Combo** | Unified sampling configuration with smart recommendations | ✅ Complete | [Usage Examples](#sampler-combo-example) |
|
||||
| **Sampler Combo** | Unified sampling configuration with smart recommendations | ✅ Complete | [Docs](examples/documentation/sampler_combo.md) |
|
||||
| **Empty Latent Batch** | Create empty latent batches with preset support | ✅ Complete | [Docs](examples/documentation/empty_latent_batch.md) |
|
||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||
|
||||
@@ -278,6 +309,37 @@ Sampler Combo → KSampler → VAE Decode → Save Image
|
||||
- Graceful error handling with safe defaults
|
||||
- Comprehensive tooltips for user guidance
|
||||
|
||||
#### Empty Latent Batch
|
||||
|
||||
**Inputs:**
|
||||
- `preset` (DROPDOWN): 26 preset options + custom with formatted metadata display
|
||||
- `width` (INT): 64-8192, step 8, default 1024
|
||||
- `height` (INT): 64-8192, step 8, default 1024
|
||||
- `batch_size` (INT): 1-64, default 1
|
||||
|
||||
**Outputs:**
|
||||
- `latent` (LATENT): Batch of empty latent tensors in ComfyUI format
|
||||
- `width` (INT): Final sanitized width (divisible by 8)
|
||||
- `height` (INT): Final sanitized height (divisible by 8)
|
||||
|
||||
**UI Features:**
|
||||
- Visual blue swap button with hover and click feedback
|
||||
- Intelligent preset switching when swapping dimensions
|
||||
- Memory usage estimation and warnings for large batches
|
||||
- Auto-update width/height widgets when presets change
|
||||
|
||||
**Batch Processing:**
|
||||
- Creates tensors with shape: [batch_size, 4, height//8, width//8]
|
||||
- Efficient memory allocation with torch.zeros
|
||||
- Validates batch size limits (1-64) with performance warnings
|
||||
- Compatible with all ComfyUI latent processing nodes
|
||||
|
||||
**Preset Integration:**
|
||||
- Full access to 26 curated resolution presets from Width Height Selector
|
||||
- Model-aware categorization (SDXL, FLUX, Ultra-wide)
|
||||
- Formatted display with aspect ratio and megapixel information
|
||||
- Intelligent fallback to custom dimensions for invalid presets
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Prerequisites
|
||||
@@ -398,9 +460,9 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
- **Nodes**: 4 (Resolution Calculator, Width Height Selector, Seed History, Sampler Combo)
|
||||
- **Nodes**: 5 (Resolution Calculator, Width Height Selector, Seed History, Sampler Combo, Empty Latent Batch)
|
||||
- **Presets**: 26 curated resolution presets
|
||||
- **Interactive Features**: 2 (Swap Button, History UI)
|
||||
- **Interactive Features**: 3 (Width/Height Swap Button, Seed History UI, Empty Latent Batch Swap Button)
|
||||
- **Test Coverage**: 100% (180+ comprehensive tests)
|
||||
- **Python Version**: 3.8+
|
||||
- **ComfyUI Compatibility**: Latest
|
||||
|
||||
+26
-2
@@ -3,12 +3,36 @@ ComfyUI-KikoTools: Modular collection of custom ComfyUI nodes
|
||||
All nodes are grouped under the "ComfyAssets" category
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from .kikotools import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# Tell ComfyUI where to find our JavaScript extensions
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
# Print startup message
|
||||
print("\033[94m[ComfyUI-KikoTools] Loaded with swap button support!\033[0m")
|
||||
|
||||
def get_version():
|
||||
"""Parse version from pyproject.toml"""
|
||||
try:
|
||||
pyproject_path = Path(__file__).parent / "pyproject.toml"
|
||||
if pyproject_path.exists():
|
||||
content = pyproject_path.read_text()
|
||||
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
|
||||
# Print startup message with loaded tools
|
||||
print()
|
||||
print(f"\033[94m[ComfyUI-KikoTools] Version:\033[0m {get_version()}")
|
||||
for node_key, display_name in NODE_DISPLAY_NAME_MAPPINGS.items():
|
||||
print(f"🫶 \033[94mLoaded:\033[0m {display_name}")
|
||||
print(f"\033[94mTotal: {len(NODE_CLASS_MAPPINGS)} tools loaded\033[0m")
|
||||
print()
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# Empty Latent Batch Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Empty Latent Batch is a ComfyUI node that creates empty latent tensors with batch support and preset integration. It combines the preset functionality of Width Height Selector with efficient batch processing capabilities, making it ideal for batch workflows and optimized generation pipelines.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 **Preset Integration**
|
||||
- **26 Curated Presets**: Full access to SDXL, FLUX, and Ultra-wide presets
|
||||
- **Formatted Display**: Shows aspect ratio, megapixels, and model group
|
||||
- **Smart Fallback**: Automatic fallback to custom dimensions for invalid presets
|
||||
- **Model Optimization**: Preset categories optimized for different model types
|
||||
|
||||
### 📦 **Batch Processing**
|
||||
- **Configurable Batch Size**: Create 1-64 empty latents in single operation
|
||||
- **Memory Efficient**: Uses torch.zeros for optimal memory allocation
|
||||
- **Batch Validation**: Prevents excessive memory usage with warnings
|
||||
- **ComfyUI Compatible**: Standard latent format for seamless integration
|
||||
|
||||
### 🔄 **Visual Swap Button**
|
||||
- **Interactive UI**: Blue swap button with hover and click feedback
|
||||
- **Preset-Aware Swapping**: Intelligent switching between matching presets
|
||||
- **Custom Dimension Support**: Simple value swapping for custom inputs
|
||||
- **Visual Feedback**: Button state changes during interaction
|
||||
|
||||
### ✅ **Smart Validation**
|
||||
- **Dimension Sanitization**: Automatic adjustment to divisible-by-8 constraint
|
||||
- **Memory Estimation**: Built-in memory usage calculation
|
||||
- **Error Handling**: Graceful handling of invalid inputs with helpful messages
|
||||
- **Logging**: Detailed operation logging for debugging
|
||||
|
||||
## Node Interface
|
||||
|
||||
### Inputs
|
||||
- **preset**: Dropdown with 26 formatted preset options + custom
|
||||
- **width**: Custom width (64-8192, step 8, default 1024)
|
||||
- **height**: Custom height (64-8192, step 8, default 1024)
|
||||
- **batch_size**: Number of latents to create (1-64, default 1)
|
||||
|
||||
### Outputs
|
||||
- **latent**: Dictionary containing batch of empty latent tensors
|
||||
- **width**: Final sanitized width (guaranteed divisible by 8)
|
||||
- **height**: Final sanitized height (guaranteed divisible by 8)
|
||||
|
||||
## Preset Reference
|
||||
|
||||
The Empty Latent Batch node uses the same 26 curated presets as the Width Height Selector:
|
||||
|
||||
### SDXL Presets (~1 Megapixel)
|
||||
Optimized for SDXL models with ~1MP resolution constraint.
|
||||
|
||||
### FLUX Presets (High Resolution)
|
||||
Higher resolution presets optimized for FLUX models with better quality/speed balance.
|
||||
|
||||
### Ultra-Wide Presets (Modern Ratios)
|
||||
Modern aspect ratios for ultra-wide and panoramic generation.
|
||||
|
||||
*For complete preset details, see [Width Height Selector Documentation](width_height_selector.md#preset-reference)*
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Empty Latent Creation
|
||||
1. **Select Preset**: Choose from dropdown (e.g., "1024×1024 - 1:1 (1.0MP) - SDXL")
|
||||
2. **Set Batch Size**: Enter desired number of latents (e.g., 4)
|
||||
3. **Connect Output**: Link latent output to KSampler or other processing nodes
|
||||
|
||||
### Custom Batch Creation
|
||||
1. **Set Preset**: Select "custom"
|
||||
2. **Enter Dimensions**: Input width and height manually
|
||||
3. **Set Batch Size**: Configure number of latents needed
|
||||
4. **Validation**: Automatic sanitization ensures compatibility
|
||||
|
||||
### Orientation Swapping
|
||||
1. **Choose Preset**: Any preset (e.g., "1920×1080")
|
||||
2. **Click Swap Button**: Blue button in bottom-right corner
|
||||
3. **Result**: Gets swapped preset if available, or custom dimensions with swapped values
|
||||
4. **Widget Update**: Width/height widgets automatically update
|
||||
|
||||
### Memory-Aware Batch Processing
|
||||
1. **Large Batch**: Set batch_size to 16 or higher
|
||||
2. **Memory Warning**: Node provides memory usage estimation
|
||||
3. **Optimization**: Choose appropriate resolution preset for available VRAM
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Batch Generation Pipeline
|
||||
```
|
||||
Empty Latent Batch → KSampler → VAE Decode → Save Image
|
||||
(batch_size: 4) ↓ ↓ ↓
|
||||
4 samples 4 images 4 files
|
||||
```
|
||||
- Create 4 empty latents at once
|
||||
- Process all through sampling
|
||||
- Generate 4 images in single operation
|
||||
- Efficient for parameter exploration
|
||||
|
||||
### Model Comparison Workflow
|
||||
```
|
||||
Empty Latent Batch → [Multiple KSamplers] → [Multiple VAE Decoders] → Compare Results
|
||||
(batch_size: 8) ↓ ↓ ↓
|
||||
Split batch Process variants Side-by-side
|
||||
```
|
||||
- Create consistent batch of empty latents
|
||||
- Split across different samplers/models
|
||||
- Compare results with identical starting conditions
|
||||
|
||||
### Upscaling Preparation
|
||||
```
|
||||
Empty Latent Batch → KSampler → VAE Decode → Resolution Calculator → Upscaler
|
||||
(832×1216, batch:4) ↓ ↓ ↓ ↓
|
||||
Sample Decode Calculate 2x Upscale batch
|
||||
```
|
||||
- Generate batch at base resolution
|
||||
- Calculate upscale dimensions
|
||||
- Process entire batch through upscaler
|
||||
|
||||
### Aspect Ratio Exploration
|
||||
```
|
||||
Empty Latent Batch → [Clone to multiple orientations] → Parallel Processing
|
||||
(1920×1080) ↓ ↓
|
||||
[Swap Button] → Portrait & Landscape versions Compare orientations
|
||||
```
|
||||
- Start with base preset
|
||||
- Use swap button to create orientation variants
|
||||
- Process both simultaneously
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Memory Estimation
|
||||
The node provides built-in memory estimation for batch operations:
|
||||
|
||||
```python
|
||||
# Example memory calculations
|
||||
Batch Size: 4, Resolution: 1024×1024
|
||||
Latent Tensor: 4 × 4 × 128 × 128 = 262,144 elements
|
||||
Memory Usage: 262,144 × 4 bytes = 1.0 MB per batch
|
||||
```
|
||||
|
||||
### Intelligent Preset Handling
|
||||
- **Formatted Display**: Shows full metadata in dropdown
|
||||
- **Original Extraction**: Extracts original preset name from formatted strings
|
||||
- **Validation**: Verifies preset exists before processing
|
||||
- **Fallback Logic**: Uses custom dimensions if preset is invalid
|
||||
|
||||
### Batch Size Optimization
|
||||
- **Performance Warnings**: Alerts for large batch sizes
|
||||
- **Memory Limits**: Prevents excessive memory allocation
|
||||
- **Hardware Awareness**: Considers available system resources
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
### Batch Size Selection
|
||||
- **Small Batches (1-4)**: Good for testing and development
|
||||
- **Medium Batches (5-16)**: Efficient for most production workflows
|
||||
- **Large Batches (17-64)**: Only for high-memory systems and specific use cases
|
||||
|
||||
### Preset Selection
|
||||
- **SDXL Projects**: Use SDXL presets for memory efficiency
|
||||
- **FLUX Projects**: Use FLUX presets for optimal quality
|
||||
- **Ultra-wide Projects**: Ensure sufficient VRAM for large resolutions
|
||||
- **Custom Projects**: Use custom dimensions for specific requirements
|
||||
|
||||
### Memory Management
|
||||
- Monitor memory usage with large batches
|
||||
- Use appropriate resolution presets for available VRAM
|
||||
- Consider splitting very large batches across multiple nodes
|
||||
- Clear GPU memory between large batch operations
|
||||
|
||||
### Workflow Integration
|
||||
- Always connect all three outputs (latent, width, height)
|
||||
- Use width/height outputs for downstream dimension calculations
|
||||
- Combine with Resolution Calculator for upscaling workflows
|
||||
- Leverage batch processing for efficient parameter exploration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **Out of Memory**: Reduce batch_size or use lower resolution presets
|
||||
- **Invalid Dimensions**: Node automatically sanitizes to valid values
|
||||
- **Preset Not Found**: Falls back to custom dimensions with warning
|
||||
- **Swap Button Not Working**: Ensure node is not collapsed and button is visible
|
||||
|
||||
### Performance Optimization
|
||||
- **Batch Size**: Start with smaller batches and increase as needed
|
||||
- **Resolution**: Use appropriate presets for your model and VRAM
|
||||
- **Memory Monitoring**: Watch for memory warnings and adjust accordingly
|
||||
- **Cleanup**: Clear unused tensors between large batch operations
|
||||
|
||||
### Error Handling
|
||||
- **Dimension Validation**: Automatic rounding to nearest valid values
|
||||
- **Batch Size Limits**: Clamped to 1-64 range with warnings
|
||||
- **Memory Allocation**: Graceful handling of insufficient memory
|
||||
- **Preset Fallbacks**: Automatic fallback to custom dimensions
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Latent Tensor Format
|
||||
- **Shape**: [batch_size, 4, height//8, width//8]
|
||||
- **Data Type**: torch.float32
|
||||
- **Initialization**: torch.zeros for clean empty state
|
||||
- **Memory Layout**: Contiguous tensor for optimal performance
|
||||
|
||||
### Validation Pipeline
|
||||
1. **Preset Extraction**: Parse formatted preset strings
|
||||
2. **Dimension Calculation**: Get base dimensions from preset or custom
|
||||
3. **Sanitization**: Ensure divisible-by-8 constraint
|
||||
4. **Batch Validation**: Check batch size limits
|
||||
5. **Memory Estimation**: Calculate expected memory usage
|
||||
6. **Tensor Creation**: Allocate and initialize latent tensor
|
||||
|
||||
### UI Integration
|
||||
- **JavaScript Extension**: Custom UI for swap button functionality
|
||||
- **Widget Synchronization**: Auto-update width/height when preset changes
|
||||
- **Visual Feedback**: Hover effects and click animations
|
||||
- **Event Handling**: Proper mouse event management
|
||||
|
||||
### Swap Button Implementation
|
||||
- **Position Calculation**: Dynamic positioning based on node size
|
||||
- **State Management**: Visual feedback for button interactions
|
||||
- **Preset Intelligence**: Smart switching between compatible presets
|
||||
- **Fallback Logic**: Custom dimension swapping when preset not available
|
||||
@@ -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.
|
||||
@@ -1,47 +1,259 @@
|
||||
{
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 2,
|
||||
"id": "41469b2d-d616-479d-879a-95cdc6074a37",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 5,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ResolutionCalculator",
|
||||
"pos": [100, 100],
|
||||
"size": {"0": 315, "1": 126},
|
||||
"pos": [
|
||||
60,
|
||||
430
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
126
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"name": "latent",
|
||||
"shape": 7,
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
2,
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"ver": "965ad60c74d7f25b1acce890d9c06518e46e6d0b",
|
||||
"Node name for S&R": "ResolutionCalculator",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
2.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-250,
|
||||
430
|
||||
],
|
||||
"size": [
|
||||
274.080078125,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "image", "type": "IMAGE", "link": null},
|
||||
{"name": "latent", "type": "LATENT", "link": null}
|
||||
],
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{"name": "width", "type": "INT", "links": [1], "slot_index": 0},
|
||||
{"name": "height", "type": "INT", "links": [2], "slot_index": 1}
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [2.0],
|
||||
"category": "ComfyAssets"
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"image-2025-06-13-105737.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "Display Int (rgthree)",
|
||||
"pos": [
|
||||
420,
|
||||
360
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"dir": 3,
|
||||
"name": "input",
|
||||
"type": "INT",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "rgthree-comfy",
|
||||
"ver": "1.0.2506081210",
|
||||
"Node name for S&R": "Display Int (rgthree)",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "Display Int (rgthree)",
|
||||
"pos": [
|
||||
430,
|
||||
530
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"dir": 3,
|
||||
"name": "input",
|
||||
"type": "INT",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "rgthree-comfy",
|
||||
"ver": "1.0.2506081210",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "Display Int (rgthree)"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Note",
|
||||
"pos": [450, 100],
|
||||
"size": {"0": 400, "1": 200},
|
||||
"pos": [
|
||||
-150,
|
||||
170
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {"text": "Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models."},
|
||||
"widgets_values": ["Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models."]
|
||||
"properties": {
|
||||
"text": "Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models.",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 3, 0, "INT"],
|
||||
[2, 1, 1, 3, 1, "INT"]
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
5,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
5,
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"ds": {
|
||||
"scale": 0.7972024500000015,
|
||||
"offset": [
|
||||
709.6391289161842,
|
||||
-90.0127389290854
|
||||
]
|
||||
},
|
||||
"links_added_by_ue": [],
|
||||
"frontendVersion": "1.21.7",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
{
|
||||
"id": "972425bd-9910-484d-ad09-f142f534fc61",
|
||||
"revision": 0,
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 13,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
120,
|
||||
390
|
||||
],
|
||||
"size": [
|
||||
425.27801513671875,
|
||||
180.6060791015625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
180,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "EmptyLatentImage",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
863,
|
||||
186
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
571
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "steps"
|
||||
},
|
||||
"link": 12
|
||||
},
|
||||
{
|
||||
"name": "cfg",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "cfg"
|
||||
},
|
||||
"link": 13
|
||||
},
|
||||
{
|
||||
"name": "sampler_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "sampler_name"
|
||||
},
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"name": "scheduler",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "scheduler"
|
||||
},
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "KSampler",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
555950987049067,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1,
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1209,
|
||||
188
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "VAEDecode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1451,
|
||||
189
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "SaveImage",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
120,
|
||||
180
|
||||
],
|
||||
"size": [
|
||||
422.84503173828125,
|
||||
164.31304931640625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SamplerCombo",
|
||||
"pos": [
|
||||
550,
|
||||
290
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "sampler_name",
|
||||
"type": "COMBO",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "scheduler",
|
||||
"type": "COMBO",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"type": "INT",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cfg",
|
||||
"type": "FLOAT",
|
||||
"slot_index": 3,
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"ver": "dcf2d679c1a1091c54c2aa8cc04724236723227d",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "SamplerCombo"
|
||||
},
|
||||
"widgets_values": [
|
||||
"dpmpp_2m",
|
||||
"karras",
|
||||
25,
|
||||
7.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
-220,
|
||||
180
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
3,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"sdxl_ckpt/realvisxlV40_v40LightningBakedvae.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
4,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
2,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
4,
|
||||
6,
|
||||
0,
|
||||
3,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
5,
|
||||
4,
|
||||
1,
|
||||
7,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
7,
|
||||
3,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
8,
|
||||
4,
|
||||
2,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
9,
|
||||
8,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
0,
|
||||
3,
|
||||
6,
|
||||
"COMBO"
|
||||
],
|
||||
[
|
||||
11,
|
||||
10,
|
||||
1,
|
||||
3,
|
||||
7,
|
||||
"COMBO"
|
||||
],
|
||||
[
|
||||
12,
|
||||
10,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
13,
|
||||
10,
|
||||
3,
|
||||
3,
|
||||
5,
|
||||
"FLOAT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": [],
|
||||
"ds": {
|
||||
"scale": 0.9740024562304554,
|
||||
"offset": [
|
||||
75.10666660323399,
|
||||
-252.18018113111526
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.21.7",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,192 +1,111 @@
|
||||
{
|
||||
"last_node_id": 8,
|
||||
"last_link_id": 7,
|
||||
"id": "972425bd-9910-484d-ad09-f142f534fc61",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 14,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "SeedHistory",
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
100,
|
||||
100
|
||||
120,
|
||||
390
|
||||
],
|
||||
"size": {
|
||||
"0": 280,
|
||||
"1": 320
|
||||
"size": [
|
||||
425.27801513671875,
|
||||
180.6060791015625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
180,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SeedHistory"
|
||||
},
|
||||
"widgets_values": [
|
||||
12345
|
||||
],
|
||||
"color": "#2a363b",
|
||||
"bgcolor": "#3f5159"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
100,
|
||||
450
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
2
|
||||
],
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
3
|
||||
],
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
4
|
||||
],
|
||||
"shape": 3
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "EmptyLatentImage",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"sd_xl_base_1.0.safetensors"
|
||||
512,
|
||||
512,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
450,
|
||||
100
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
5
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"a beautiful landscape with mountains and lakes, sunset, detailed, photorealistic"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
450,
|
||||
350
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
6
|
||||
],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"blurry, low quality, distorted, ugly, deformed"
|
||||
],
|
||||
"color": "#322",
|
||||
"bgcolor": "#533"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
900,
|
||||
100
|
||||
863,
|
||||
186
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
571
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 2
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 5
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
@@ -196,84 +115,89 @@
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"link": 1,
|
||||
"widget": {
|
||||
"name": "seed"
|
||||
}
|
||||
},
|
||||
"link": 14
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "steps"
|
||||
},
|
||||
"link": 12
|
||||
},
|
||||
{
|
||||
"name": "cfg",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "cfg"
|
||||
},
|
||||
"link": 13
|
||||
},
|
||||
{
|
||||
"name": "sampler_name",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "sampler_name"
|
||||
},
|
||||
"link": 10
|
||||
},
|
||||
{
|
||||
"name": "scheduler",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "scheduler"
|
||||
},
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
7
|
||||
],
|
||||
"shape": 3
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "KSampler",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
12345,
|
||||
231413202715030,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
1,
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [
|
||||
900,
|
||||
400
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
1024,
|
||||
1024,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1300,
|
||||
100
|
||||
1209,
|
||||
188
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
@@ -284,126 +208,437 @@
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 4
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [],
|
||||
"shape": 3
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "VAEDecode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1300,
|
||||
200
|
||||
1451,
|
||||
189
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
270
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 270
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveImage"
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "SaveImage",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
120,
|
||||
180
|
||||
],
|
||||
"size": [
|
||||
422.84503173828125,
|
||||
164.31304931640625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
-220,
|
||||
180
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
3,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"sdxl_ckpt/realvisxlV40_v40LightningBakedvae.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SamplerCombo",
|
||||
"pos": [
|
||||
550,
|
||||
360
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "sampler_name",
|
||||
"type": "COMBO",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "scheduler",
|
||||
"type": "COMBO",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"type": "INT",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cfg",
|
||||
"type": "FLOAT",
|
||||
"slot_index": 3,
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"ver": "dcf2d679c1a1091c54c2aa8cc04724236723227d",
|
||||
"Node name for S&R": "SamplerCombo",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"dpmpp_2m",
|
||||
"karras",
|
||||
25,
|
||||
7.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "SeedHistory",
|
||||
"pos": [
|
||||
550,
|
||||
100
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
220
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "seed",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
14
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"ver": "dcf2d679c1a1091c54c2aa8cc04724236723227d",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "SeedHistory"
|
||||
},
|
||||
"widgets_values": [
|
||||
893082183398485,
|
||||
"randomize",
|
||||
""
|
||||
],
|
||||
"color": "#2a363b",
|
||||
"bgcolor": "#3f5159",
|
||||
"hasBeenResized": true,
|
||||
"seedHistory": [
|
||||
{
|
||||
"seed": 893082183398485,
|
||||
"timestamp": 1750032481153,
|
||||
"dateString": "6/15/2025, 5:08:01 PM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236135,
|
||||
"timestamp": 1750014787315,
|
||||
"dateString": "6/15/2025, 12:13:07 PM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236134,
|
||||
"timestamp": 1750014721305,
|
||||
"dateString": "6/15/2025, 12:12:01 PM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236133,
|
||||
"timestamp": 1750012659402,
|
||||
"dateString": "6/15/2025, 11:37:39 AM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236132,
|
||||
"timestamp": 1750011284415,
|
||||
"dateString": "6/15/2025, 11:14:44 AM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236131,
|
||||
"timestamp": 1750011223416,
|
||||
"dateString": "6/15/2025, 11:13:43 AM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236130,
|
||||
"timestamp": 1750011181407,
|
||||
"dateString": "6/15/2025, 11:13:01 AM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236129,
|
||||
"timestamp": 1750005515384,
|
||||
"dateString": "6/15/2025, 9:38:35 AM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236128,
|
||||
"timestamp": 1750005462384,
|
||||
"dateString": "6/15/2025, 9:37:42 AM"
|
||||
},
|
||||
{
|
||||
"seed": 267914687236127,
|
||||
"timestamp": 1750005354713,
|
||||
"dateString": "6/15/2025, 9:35:54 AM"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
5,
|
||||
4,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
5,
|
||||
3,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
3,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
6,
|
||||
0,
|
||||
3,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
5,
|
||||
4,
|
||||
1,
|
||||
7,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
7,
|
||||
3,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
8,
|
||||
4,
|
||||
2,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
9,
|
||||
8,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
10,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
5,
|
||||
7,
|
||||
"COMBO"
|
||||
],
|
||||
[
|
||||
11,
|
||||
10,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
3,
|
||||
8,
|
||||
"COMBO"
|
||||
],
|
||||
[
|
||||
6,
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
12,
|
||||
10,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
3,
|
||||
5,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
7,
|
||||
5,
|
||||
13,
|
||||
10,
|
||||
3,
|
||||
3,
|
||||
6,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
14,
|
||||
11,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
"LATENT"
|
||||
3,
|
||||
4,
|
||||
"INT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": [],
|
||||
"ds": {
|
||||
"scale": 0.9090909090909091,
|
||||
"scale": 0.9740024562304554,
|
||||
"offset": [
|
||||
-45.45454545454545,
|
||||
-9.090909090909092
|
||||
2.9611945935278796,
|
||||
36.50023864466208
|
||||
]
|
||||
},
|
||||
"info": {
|
||||
"name": "Seed History Example",
|
||||
"author": "ComfyUI-KikoTools",
|
||||
"description": "Example workflow demonstrating the Seed History tool for tracking and managing seed values with an interactive UI.",
|
||||
"version": "1.0",
|
||||
"created": "2024-06-14",
|
||||
"modified": "2024-06-14"
|
||||
}
|
||||
"frontendVersion": "1.21.7",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ from .tools.resolution_calculator import ResolutionCalculatorNode
|
||||
from .tools.width_height_selector import WidthHeightSelectorNode
|
||||
from .tools.seed_history import SeedHistoryNode
|
||||
from .tools.sampler_combo import SamplerComboNode, SamplerComboCompactNode
|
||||
from .tools.empty_latent_batch import EmptyLatentBatchNode
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -15,6 +16,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SeedHistory": SeedHistoryNode,
|
||||
"SamplerCombo": SamplerComboNode,
|
||||
"SamplerComboCompact": SamplerComboCompactNode,
|
||||
"EmptyLatentBatch": EmptyLatentBatchNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -23,6 +25,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SeedHistory": "Seed History",
|
||||
"SamplerCombo": "Sampler Combo",
|
||||
"SamplerComboCompact": "Sampler Combo (Compact)",
|
||||
"EmptyLatentBatch": "Empty Latent Batch",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Empty Latent Batch tool for ComfyUI."""
|
||||
|
||||
from .node import EmptyLatentBatchNode
|
||||
|
||||
__all__ = ["EmptyLatentBatchNode"]
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Logic for creating empty latent tensors with batch support."""
|
||||
|
||||
import torch
|
||||
from typing import Dict, Tuple, Any
|
||||
|
||||
|
||||
def create_empty_latent_batch(
|
||||
width: int, height: int, batch_size: int = 1
|
||||
) -> Dict[str, torch.Tensor]:
|
||||
"""
|
||||
Create empty latent tensor with batch support.
|
||||
|
||||
Args:
|
||||
width: Width in pixels (will be divided by 8 for latent space)
|
||||
height: Height in pixels (will be divided by 8 for latent space)
|
||||
batch_size: Number of latents in the batch
|
||||
|
||||
Returns:
|
||||
Dictionary containing the latent samples tensor
|
||||
|
||||
Raises:
|
||||
ValueError: If dimensions are invalid
|
||||
"""
|
||||
# Validate inputs
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError(f"Width and height must be positive, got {width}x{height}")
|
||||
|
||||
if batch_size <= 0:
|
||||
raise ValueError(f"Batch size must be positive, got {batch_size}")
|
||||
|
||||
# Ensure dimensions are divisible by 8 (VAE requirement)
|
||||
if width % 8 != 0 or height % 8 != 0:
|
||||
raise ValueError(
|
||||
f"Width and height must be divisible by 8, got {width}x{height}"
|
||||
)
|
||||
|
||||
# Convert pixel dimensions to latent space (divide by 8)
|
||||
latent_width = width // 8
|
||||
latent_height = height // 8
|
||||
|
||||
# Create empty latent tensor
|
||||
# ComfyUI latent format: [batch, channels, height, width]
|
||||
# Standard VAE uses 4 channels
|
||||
latent_tensor = torch.zeros(batch_size, 4, latent_height, latent_width)
|
||||
|
||||
return {"samples": latent_tensor}
|
||||
|
||||
|
||||
def validate_dimensions(width: int, height: int) -> bool:
|
||||
"""
|
||||
Validate that dimensions are suitable for latent creation.
|
||||
|
||||
Args:
|
||||
width: Width in pixels
|
||||
height: Height in pixels
|
||||
|
||||
Returns:
|
||||
True if dimensions are valid
|
||||
"""
|
||||
# Check basic constraints
|
||||
if width <= 0 or height <= 0:
|
||||
return False
|
||||
|
||||
# Check divisibility by 8
|
||||
if width % 8 != 0 or height % 8 != 0:
|
||||
return False
|
||||
|
||||
# Check reasonable size limits (64x64 to 8192x8192)
|
||||
if width < 64 or height < 64:
|
||||
return False
|
||||
|
||||
if width > 8192 or height > 8192:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def sanitize_dimensions(width: int, height: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Sanitize dimensions to ensure they meet latent requirements.
|
||||
|
||||
Args:
|
||||
width: Input width
|
||||
height: Input height
|
||||
|
||||
Returns:
|
||||
Tuple of (sanitized_width, sanitized_height)
|
||||
"""
|
||||
# Ensure minimum dimensions
|
||||
width = max(64, width)
|
||||
height = max(64, height)
|
||||
|
||||
# Ensure maximum dimensions
|
||||
width = min(8192, width)
|
||||
height = min(8192, height)
|
||||
|
||||
# Round to nearest multiple of 8
|
||||
width = (width + 7) // 8 * 8
|
||||
height = (height + 7) // 8 * 8
|
||||
|
||||
return width, height
|
||||
@@ -0,0 +1,312 @@
|
||||
"""Empty Latent Batch node for ComfyUI."""
|
||||
|
||||
import torch
|
||||
from typing import Dict, Any, Tuple
|
||||
|
||||
from ...base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
create_empty_latent_batch,
|
||||
validate_dimensions,
|
||||
sanitize_dimensions,
|
||||
)
|
||||
from ..width_height_selector.logic import get_preset_dimensions
|
||||
from ..width_height_selector.presets import (
|
||||
PRESET_OPTIONS,
|
||||
PRESET_METADATA,
|
||||
get_model_recommendation,
|
||||
get_preset_metadata,
|
||||
get_presets_by_model_group,
|
||||
)
|
||||
|
||||
|
||||
class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Empty Latent Batch node for creating empty latent tensors with batch support.
|
||||
|
||||
Creates empty latent tensors with specified dimensions and batch size,
|
||||
compatible with ComfyUI's latent format for use with VAE and diffusion models.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""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":
|
||||
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_options,
|
||||
{
|
||||
"default": "custom",
|
||||
"tooltip": "Select from optimized resolution presets or use "
|
||||
"custom dimensions. SDXL presets are ~1MP, FLUX presets are "
|
||||
"higher resolution, Ultra-wide presets support modern "
|
||||
"aspect ratios.",
|
||||
},
|
||||
),
|
||||
"width": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 64,
|
||||
"max": 8192,
|
||||
"step": 8,
|
||||
"tooltip": "Custom width in pixels (must be multiple of 8). "
|
||||
"Used when preset is 'custom' or as fallback for invalid "
|
||||
"presets. This will be converted to latent space dimensions.",
|
||||
},
|
||||
),
|
||||
"height": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 64,
|
||||
"max": 8192,
|
||||
"step": 8,
|
||||
"tooltip": "Custom height in pixels (must be multiple of 8). "
|
||||
"Used when preset is 'custom' or as fallback for invalid "
|
||||
"presets. This will be converted to latent space dimensions.",
|
||||
},
|
||||
),
|
||||
"batch_size": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 64,
|
||||
"step": 1,
|
||||
"tooltip": "Number of empty latents to create in the batch. "
|
||||
"Useful for batch processing workflows.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT", "INT", "INT")
|
||||
RETURN_NAMES = ("latent", "width", "height")
|
||||
FUNCTION = "create_empty_latent"
|
||||
CATEGORY = "ComfyAssets"
|
||||
|
||||
def create_empty_latent(
|
||||
self, preset: str, width: int, height: int, batch_size: int
|
||||
) -> Tuple[Dict[str, torch.Tensor], int, int]:
|
||||
"""
|
||||
Create empty latent tensor with specified dimensions and batch size.
|
||||
|
||||
Args:
|
||||
preset: Selected preset name or formatted preset string
|
||||
width: Custom width value
|
||||
height: Custom height value
|
||||
batch_size: Number of latents in the batch
|
||||
|
||||
Returns:
|
||||
Tuple containing (latent dictionary with 'samples' tensor, 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
|
||||
base_width, base_height = get_preset_dimensions(
|
||||
original_preset, width, height
|
||||
)
|
||||
|
||||
# Sanitize dimensions to ensure they meet requirements
|
||||
final_width, final_height = sanitize_dimensions(base_width, base_height)
|
||||
|
||||
# Log if dimensions were changed from the base dimensions
|
||||
if final_width != base_width or final_height != base_height:
|
||||
self.log_info(
|
||||
f"Dimensions adjusted from {base_width}×{base_height} to "
|
||||
f"{final_width}×{final_height} to meet VAE requirements"
|
||||
)
|
||||
|
||||
# Validate final dimensions
|
||||
if not validate_dimensions(final_width, final_height):
|
||||
self.handle_error(
|
||||
f"Invalid dimensions after sanitization: {final_width}×{final_height}"
|
||||
)
|
||||
|
||||
# Validate batch size
|
||||
if batch_size <= 0:
|
||||
self.handle_error(f"Batch size must be positive, got {batch_size}")
|
||||
|
||||
if batch_size > 64:
|
||||
self.log_info(
|
||||
f"Large batch size ({batch_size}) may use significant memory"
|
||||
)
|
||||
|
||||
# Create the empty latent batch
|
||||
latent_dict = create_empty_latent_batch(
|
||||
final_width, final_height, batch_size
|
||||
)
|
||||
|
||||
# Log the operation
|
||||
latent_height = final_height // 8
|
||||
latent_width = final_width // 8
|
||||
self.log_info(
|
||||
f"Created empty latent batch: {batch_size}×4×{latent_height}×{latent_width} "
|
||||
f"(pixel dims: {final_width}×{final_height})"
|
||||
)
|
||||
|
||||
return (latent_dict, final_width, final_height)
|
||||
|
||||
except Exception as e:
|
||||
# Handle any unexpected errors gracefully
|
||||
error_msg = f"Error creating empty latent batch: {str(e)}"
|
||||
self.handle_error(error_msg, e)
|
||||
|
||||
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 validate_inputs(
|
||||
self, preset: str, width: int, height: int, batch_size: int
|
||||
) -> bool:
|
||||
"""
|
||||
Validate node inputs.
|
||||
|
||||
Args:
|
||||
preset: Preset name or formatted preset string
|
||||
width: Width value
|
||||
height: Height value
|
||||
batch_size: Batch size 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 original_preset != "custom" and original_preset not in PRESET_OPTIONS:
|
||||
return False
|
||||
|
||||
# Get dimensions from preset or use custom
|
||||
base_width, base_height = get_preset_dimensions(original_preset, width, height)
|
||||
|
||||
# Check dimension validity (after sanitization)
|
||||
sanitized_width, sanitized_height = sanitize_dimensions(base_width, base_height)
|
||||
if not validate_dimensions(sanitized_width, sanitized_height):
|
||||
return False
|
||||
|
||||
# Check batch size
|
||||
if batch_size <= 0 or batch_size > 64:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_latent_info(self, width: int, height: int, batch_size: int) -> str:
|
||||
"""
|
||||
Get descriptive information about the latent that will be created.
|
||||
|
||||
Args:
|
||||
width: Width in pixels
|
||||
height: Height in pixels
|
||||
batch_size: Batch size
|
||||
|
||||
Returns:
|
||||
Description string for the latent
|
||||
"""
|
||||
sanitized_width, sanitized_height = sanitize_dimensions(width, height)
|
||||
latent_width = sanitized_width // 8
|
||||
latent_height = sanitized_height // 8
|
||||
|
||||
return (
|
||||
f"Empty latent batch: {batch_size} × 4 × {latent_height} × {latent_width} "
|
||||
f"(pixel dimensions: {sanitized_width}×{sanitized_height})"
|
||||
)
|
||||
|
||||
def get_memory_estimate(self, width: int, height: int, batch_size: int) -> str:
|
||||
"""
|
||||
Estimate memory usage for the latent batch.
|
||||
|
||||
Args:
|
||||
width: Width in pixels
|
||||
height: Height in pixels
|
||||
batch_size: Batch size
|
||||
|
||||
Returns:
|
||||
Memory estimate string
|
||||
"""
|
||||
sanitized_width, sanitized_height = sanitize_dimensions(width, height)
|
||||
latent_width = sanitized_width // 8
|
||||
latent_height = sanitized_height // 8
|
||||
|
||||
# Calculate tensor size in bytes (float32 = 4 bytes per element)
|
||||
elements = batch_size * 4 * latent_height * latent_width
|
||||
bytes_size = elements * 4 # 4 bytes per float32
|
||||
|
||||
# Convert to human-readable format
|
||||
if bytes_size < 1024:
|
||||
return f"{bytes_size} bytes"
|
||||
elif bytes_size < 1024 * 1024:
|
||||
return f"{bytes_size / 1024:.1f} KB"
|
||||
elif bytes_size < 1024 * 1024 * 1024:
|
||||
return f"{bytes_size / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
return f"{bytes_size / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of the node."""
|
||||
return "EmptyLatentBatchNode"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Detailed string representation of the node."""
|
||||
return (
|
||||
f"EmptyLatentBatchNode("
|
||||
f"category='{self.CATEGORY}', "
|
||||
f"function='{self.FUNCTION}'"
|
||||
f")"
|
||||
)
|
||||
|
||||
|
||||
# Node class mappings for ComfyUI registration
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmptyLatentBatch": EmptyLatentBatchNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmptyLatentBatch": "Empty Latent Batch",
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)})"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "kikotools"
|
||||
description = "Simple tools for ComfyUI"
|
||||
version = "1.0.2"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["# Development dependencies for ComfyUI-KikoTools", "# Testing framework", "pytest>=7.0.0", "pytest-cov>=4.0.0", "pytest-mock>=3.10.0", "# Code quality", "black>=23.0.0", "flake8>=6.0.0", "mypy>=1.0.0", "# Development utilities", "pre-commit>=3.0.0", "# ComfyUI testing (mock dependencies for unit tests)", "torch>=2.0.0", "numpy>=1.24.0", "pillow>=9.0.0"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/ComfyAssets/ComfyUI-KikoTools"
|
||||
# Used by Comfy Registry https://registry.comfy.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "kiko9"
|
||||
DisplayName = "ComfyUI-KikoTools"
|
||||
Icon = "https://avatars.githubusercontent.com/u/213204677?s=200"
|
||||
includes = []
|
||||
@@ -0,0 +1,19 @@
|
||||
# Development dependencies for ComfyUI-KikoTools
|
||||
|
||||
# Testing framework
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
pytest-mock>=3.10.0
|
||||
|
||||
# Code quality
|
||||
black>=23.0.0
|
||||
flake8>=6.0.0
|
||||
mypy>=1.0.0
|
||||
|
||||
# Development utilities
|
||||
pre-commit>=3.0.0
|
||||
|
||||
# ComfyUI testing (mock dependencies for unit tests)
|
||||
torch>=2.0.0
|
||||
numpy>=1.24.0
|
||||
pillow>=9.0.0
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Tests for Empty Latent Batch node and logic."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from kikotools.tools.empty_latent_batch.node import EmptyLatentBatchNode
|
||||
from kikotools.tools.empty_latent_batch.logic import (
|
||||
create_empty_latent_batch,
|
||||
validate_dimensions,
|
||||
sanitize_dimensions,
|
||||
)
|
||||
|
||||
|
||||
class TestEmptyLatentBatchLogic:
|
||||
"""Test the logic functions for empty latent batch creation."""
|
||||
|
||||
def test_create_empty_latent_batch_basic(self):
|
||||
"""Test basic empty latent creation."""
|
||||
result = create_empty_latent_batch(512, 512, 1)
|
||||
|
||||
assert "samples" in result
|
||||
samples = result["samples"]
|
||||
assert isinstance(samples, torch.Tensor)
|
||||
assert samples.shape == (1, 4, 64, 64) # 512/8 = 64
|
||||
assert torch.all(samples == 0) # Should be all zeros
|
||||
|
||||
def test_create_empty_latent_batch_with_batch_size(self):
|
||||
"""Test empty latent creation with larger batch size."""
|
||||
batch_size = 4
|
||||
result = create_empty_latent_batch(1024, 768, batch_size)
|
||||
|
||||
assert "samples" in result
|
||||
samples = result["samples"]
|
||||
assert isinstance(samples, torch.Tensor)
|
||||
assert samples.shape == (4, 4, 96, 128) # 768/8=96, 1024/8=128
|
||||
assert torch.all(samples == 0)
|
||||
|
||||
def test_create_empty_latent_batch_invalid_dimensions(self):
|
||||
"""Test error handling for invalid dimensions."""
|
||||
with pytest.raises(ValueError, match="Width and height must be positive"):
|
||||
create_empty_latent_batch(0, 512, 1)
|
||||
|
||||
with pytest.raises(ValueError, match="Width and height must be positive"):
|
||||
create_empty_latent_batch(512, -100, 1)
|
||||
|
||||
def test_create_empty_latent_batch_not_divisible_by_8(self):
|
||||
"""Test error handling for dimensions not divisible by 8."""
|
||||
with pytest.raises(ValueError, match="must be divisible by 8"):
|
||||
create_empty_latent_batch(513, 512, 1)
|
||||
|
||||
with pytest.raises(ValueError, match="must be divisible by 8"):
|
||||
create_empty_latent_batch(512, 515, 1)
|
||||
|
||||
def test_create_empty_latent_batch_invalid_batch_size(self):
|
||||
"""Test error handling for invalid batch size."""
|
||||
with pytest.raises(ValueError, match="Batch size must be positive"):
|
||||
create_empty_latent_batch(512, 512, 0)
|
||||
|
||||
with pytest.raises(ValueError, match="Batch size must be positive"):
|
||||
create_empty_latent_batch(512, 512, -1)
|
||||
|
||||
def test_validate_dimensions_valid(self):
|
||||
"""Test dimension validation with valid inputs."""
|
||||
assert validate_dimensions(512, 512) is True
|
||||
assert validate_dimensions(1024, 768) is True
|
||||
assert validate_dimensions(64, 64) is True # Minimum size
|
||||
assert validate_dimensions(8192, 8192) is True # Maximum size
|
||||
|
||||
def test_validate_dimensions_invalid(self):
|
||||
"""Test dimension validation with invalid inputs."""
|
||||
assert validate_dimensions(0, 512) is False # Zero dimension
|
||||
assert validate_dimensions(512, -100) is False # Negative dimension
|
||||
assert validate_dimensions(513, 512) is False # Not divisible by 8
|
||||
assert validate_dimensions(32, 32) is False # Too small
|
||||
assert validate_dimensions(8200, 8200) is False # Too large
|
||||
|
||||
def test_sanitize_dimensions_basic(self):
|
||||
"""Test basic dimension sanitization."""
|
||||
width, height = sanitize_dimensions(512, 512)
|
||||
assert width == 512
|
||||
assert height == 512
|
||||
|
||||
def test_sanitize_dimensions_not_divisible_by_8(self):
|
||||
"""Test sanitization of dimensions not divisible by 8."""
|
||||
width, height = sanitize_dimensions(513, 515)
|
||||
assert width == 512 # Rounds down to nearest multiple of 8
|
||||
assert height == 512
|
||||
|
||||
width, height = sanitize_dimensions(517, 519)
|
||||
assert width == 520 # Rounds up to nearest multiple of 8
|
||||
assert height == 520
|
||||
|
||||
def test_sanitize_dimensions_too_small(self):
|
||||
"""Test sanitization of dimensions that are too small."""
|
||||
width, height = sanitize_dimensions(32, 16)
|
||||
assert width == 64 # Minimum size
|
||||
assert height == 64
|
||||
|
||||
def test_sanitize_dimensions_too_large(self):
|
||||
"""Test sanitization of dimensions that are too large."""
|
||||
width, height = sanitize_dimensions(10000, 9000)
|
||||
assert width == 8192 # Maximum size
|
||||
assert height == 8192
|
||||
|
||||
|
||||
class TestEmptyLatentBatchNode:
|
||||
"""Test the EmptyLatentBatchNode ComfyUI node."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.node = EmptyLatentBatchNode()
|
||||
|
||||
def test_input_types_structure(self):
|
||||
"""Test that INPUT_TYPES returns proper structure."""
|
||||
input_types = EmptyLatentBatchNode.INPUT_TYPES()
|
||||
|
||||
assert "required" in input_types
|
||||
required = input_types["required"]
|
||||
|
||||
assert "width" in required
|
||||
assert "height" in required
|
||||
assert "batch_size" in required
|
||||
|
||||
# Check width parameter
|
||||
width_spec = required["width"]
|
||||
assert width_spec[0] == "INT"
|
||||
assert width_spec[1]["default"] == 1024
|
||||
assert width_spec[1]["min"] == 64
|
||||
assert width_spec[1]["max"] == 8192
|
||||
assert width_spec[1]["step"] == 8
|
||||
|
||||
def test_node_attributes(self):
|
||||
"""Test node class attributes."""
|
||||
assert EmptyLatentBatchNode.RETURN_TYPES == ("LATENT",)
|
||||
assert EmptyLatentBatchNode.RETURN_NAMES == ("latent",)
|
||||
assert EmptyLatentBatchNode.FUNCTION == "create_empty_latent"
|
||||
assert EmptyLatentBatchNode.CATEGORY == "ComfyAssets"
|
||||
|
||||
def test_create_empty_latent_basic(self):
|
||||
"""Test basic empty latent creation through node."""
|
||||
result = self.node.create_empty_latent(512, 512, 1)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
|
||||
latent_dict = result[0]
|
||||
assert isinstance(latent_dict, dict)
|
||||
assert "samples" in latent_dict
|
||||
|
||||
samples = latent_dict["samples"]
|
||||
assert isinstance(samples, torch.Tensor)
|
||||
assert samples.shape == (1, 4, 64, 64)
|
||||
|
||||
def test_create_empty_latent_with_batch(self):
|
||||
"""Test empty latent creation with batch size."""
|
||||
batch_size = 3
|
||||
result = self.node.create_empty_latent(1024, 768, batch_size)
|
||||
|
||||
latent_dict = result[0]
|
||||
samples = latent_dict["samples"]
|
||||
assert samples.shape == (3, 4, 96, 128) # batch=3, 768/8=96, 1024/8=128
|
||||
|
||||
def test_create_empty_latent_dimension_adjustment(self):
|
||||
"""Test that dimensions are adjusted when not divisible by 8."""
|
||||
# Input dimensions not divisible by 8
|
||||
result = self.node.create_empty_latent(513, 515, 1)
|
||||
|
||||
latent_dict = result[0]
|
||||
samples = latent_dict["samples"]
|
||||
# Should be adjusted to 512x512 -> 64x64 latent
|
||||
assert samples.shape == (1, 4, 64, 64)
|
||||
|
||||
def test_validate_inputs_valid(self):
|
||||
"""Test input validation with valid parameters."""
|
||||
assert self.node.validate_inputs(512, 512, 1) is True
|
||||
assert self.node.validate_inputs(1024, 768, 4) is True
|
||||
|
||||
def test_validate_inputs_invalid_batch_size(self):
|
||||
"""Test input validation with invalid batch size."""
|
||||
assert self.node.validate_inputs(512, 512, 0) is False
|
||||
assert self.node.validate_inputs(512, 512, 100) is False # Too large
|
||||
|
||||
def test_get_latent_info(self):
|
||||
"""Test latent info generation."""
|
||||
info = self.node.get_latent_info(512, 512, 2)
|
||||
assert "Empty latent batch" in info
|
||||
assert "2 × 4 × 64 × 64" in info
|
||||
assert "512×512" in info
|
||||
|
||||
def test_get_memory_estimate(self):
|
||||
"""Test memory estimation."""
|
||||
estimate = self.node.get_memory_estimate(512, 512, 1)
|
||||
assert "KB" in estimate or "MB" in estimate
|
||||
|
||||
# Larger batch should show larger estimate
|
||||
large_estimate = self.node.get_memory_estimate(1024, 1024, 8)
|
||||
assert "MB" in large_estimate
|
||||
|
||||
def test_node_registration_mappings(self):
|
||||
"""Test that node registration mappings are properly defined."""
|
||||
from kikotools.tools.empty_latent_batch.node import (
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
|
||||
assert "EmptyLatentBatch" in NODE_CLASS_MAPPINGS
|
||||
assert NODE_CLASS_MAPPINGS["EmptyLatentBatch"] == EmptyLatentBatchNode
|
||||
|
||||
assert "EmptyLatentBatch" in NODE_DISPLAY_NAME_MAPPINGS
|
||||
assert NODE_DISPLAY_NAME_MAPPINGS["EmptyLatentBatch"] == "Empty Latent Batch"
|
||||
|
||||
def test_node_inheritance(self):
|
||||
"""Test that node properly inherits from base class."""
|
||||
from kikotools.base.base_node import ComfyAssetsBaseNode
|
||||
|
||||
assert isinstance(self.node, ComfyAssetsBaseNode)
|
||||
assert hasattr(self.node, "handle_error")
|
||||
assert hasattr(self.node, "log_info")
|
||||
assert hasattr(self.node, "validate_inputs")
|
||||
@@ -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}"
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
// ComfyUI-KikoTools - Empty Latent Batch with Swap Button
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "comfyassets.EmptyLatentBatch",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
|
||||
if (nodeData.name === "EmptyLatentBatch") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (onNodeCreated) onNodeCreated.apply(this, []);
|
||||
|
||||
// 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) {
|
||||
const originalCallback = presetWidget.callback;
|
||||
presetWidget.callback = function (
|
||||
value,
|
||||
graphcanvas,
|
||||
node,
|
||||
pos,
|
||||
event,
|
||||
) {
|
||||
// Call original callback first
|
||||
if (originalCallback) {
|
||||
originalCallback.call(this, value, graphcanvas, node, pos, event);
|
||||
}
|
||||
|
||||
// Update width/height widgets based on preset
|
||||
const widthWidget = node.widgets.find((w) => w.name === "width");
|
||||
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
|
||||
"1024×1024": [1024, 1024],
|
||||
"896×1152": [896, 1152],
|
||||
"832×1216": [832, 1216],
|
||||
"768×1344": [768, 1344],
|
||||
"640×1536": [640, 1536],
|
||||
"1152×896": [1152, 896],
|
||||
"1216×832": [1216, 832],
|
||||
"1344×768": [1344, 768],
|
||||
"1536×640": [1536, 640],
|
||||
// FLUX Presets
|
||||
"1920×1080": [1920, 1080],
|
||||
"1536×1536": [1536, 1536],
|
||||
"1280×768": [1280, 768],
|
||||
"768×1280": [768, 1280],
|
||||
"1440×1080": [1440, 1080],
|
||||
"1080×1440": [1080, 1440],
|
||||
"1728×1152": [1728, 1152],
|
||||
"1152×1728": [1152, 1728],
|
||||
// Ultra-Wide Presets
|
||||
"2560×1080": [2560, 1080],
|
||||
"2048×768": [2048, 768],
|
||||
"1792×768": [1792, 768],
|
||||
"2304×768": [2304, 768],
|
||||
"1080×2560": [1080, 2560],
|
||||
"768×2048": [768, 2048],
|
||||
"768×1792": [768, 1792],
|
||||
"768×2304": [768, 2304],
|
||||
};
|
||||
|
||||
if (rawResolution && presetDimensions[rawResolution]) {
|
||||
const [w, h] = presetDimensions[rawResolution];
|
||||
widthWidget.value = w;
|
||||
heightWidget.value = h;
|
||||
|
||||
// Trigger widget callbacks to update the UI
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(w, graphcanvas, node, pos, event);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(h, graphcanvas, node, pos, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add swap functionality
|
||||
this.swapDimensions = function () {
|
||||
const widthWidget = this.widgets.find((w) => w.name === "width");
|
||||
const heightWidget = this.widgets.find((w) => w.name === "height");
|
||||
const presetWidget = this.widgets.find((w) => w.name === "preset");
|
||||
|
||||
if (widthWidget && heightWidget && presetWidget) {
|
||||
// Handle preset swapping first
|
||||
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 (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 swappedRawPreset = `${h}×${w}`;
|
||||
|
||||
// Find the formatted version of the swapped preset from available options
|
||||
const availablePresets =
|
||||
presetWidget.options.values || presetWidget.options;
|
||||
let swappedFormattedPreset = null;
|
||||
|
||||
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(
|
||||
swappedFormattedPreset,
|
||||
this,
|
||||
presetWidget,
|
||||
);
|
||||
}
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(h, this, widthWidget);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(w, this, heightWidget);
|
||||
}
|
||||
} else {
|
||||
// Swapped preset doesn't exist, switch to custom and swap manual values
|
||||
presetWidget.value = "custom";
|
||||
widthWidget.value = h;
|
||||
heightWidget.value = w;
|
||||
|
||||
if (presetWidget.callback) {
|
||||
presetWidget.callback("custom", this, presetWidget);
|
||||
}
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(h, this, widthWidget);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(w, this, heightWidget);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Custom preset - just swap the width and height values
|
||||
const tempWidth = widthWidget.value;
|
||||
widthWidget.value = heightWidget.value;
|
||||
heightWidget.value = tempWidth;
|
||||
|
||||
// Trigger widget change events
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(widthWidget.value, this, widthWidget);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(heightWidget.value, this, heightWidget);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the graph as changed
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
// Override onResize to refresh button position
|
||||
const originalOnResize = this.onResize;
|
||||
this.onResize = function (size) {
|
||||
if (originalOnResize) {
|
||||
originalOnResize.call(this, size);
|
||||
}
|
||||
// Force redraw to update button position
|
||||
this.setDirtyCanvas(true, true);
|
||||
// Also mark the graph as dirty
|
||||
if (this.graph) {
|
||||
this.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
// Override onBounding to ensure proper updates
|
||||
const originalOnBounding = this.onBounding;
|
||||
this.onBounding = function (out) {
|
||||
if (originalOnBounding) {
|
||||
originalOnBounding.call(this, out);
|
||||
}
|
||||
// Force redraw when bounds change
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
};
|
||||
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
if (onDrawForeground) {
|
||||
onDrawForeground.apply(this, arguments);
|
||||
}
|
||||
|
||||
if (this.flags.collapsed) return;
|
||||
|
||||
// Draw swap button with consistent spacing from widgets
|
||||
const swapButtonSize = 24;
|
||||
const margin = 6;
|
||||
const swapButtonX = this.size[0] - swapButtonSize - margin;
|
||||
|
||||
// Calculate button position based on widget spacing rather than bottom margin
|
||||
// Estimate widget area height and add consistent spacing
|
||||
const estimatedWidgetHeight = 90; // Approximate height for 3 widgets
|
||||
const topMargin = 35; // Space from top to first widget
|
||||
const buttonSpacing = 40; // Space between last widget and button (moved down 5)
|
||||
const swapButtonY = topMargin + estimatedWidgetHeight + buttonSpacing;
|
||||
|
||||
// Button background - change color based on pressed state
|
||||
if (this.swapButtonPressed) {
|
||||
// Darker when pressed
|
||||
ctx.fillStyle = "rgba(30, 120, 200, 0.9)"; // Darker blue when clicked
|
||||
} else {
|
||||
// Normal state
|
||||
ctx.fillStyle = "rgba(66, 165, 245, 0.8)"; // Material blue
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(
|
||||
swapButtonX,
|
||||
swapButtonY,
|
||||
swapButtonSize,
|
||||
swapButtonSize,
|
||||
4,
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
// Button border with subtle highlight
|
||||
ctx.strokeStyle = this.swapButtonPressed
|
||||
? "rgba(20, 100, 180, 1.0)"
|
||||
: "rgba(33, 150, 243, 0.9)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// Draw swap icon - modern double arrow design
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
|
||||
const centerX = swapButtonX + 12;
|
||||
const centerY = swapButtonY + 12;
|
||||
|
||||
// Top arrow (pointing right) - width to height
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX - 7, centerY - 3);
|
||||
ctx.lineTo(centerX + 5, centerY - 3);
|
||||
ctx.stroke();
|
||||
|
||||
// Top arrow head
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX + 5, centerY - 3);
|
||||
ctx.lineTo(centerX + 2, centerY - 5);
|
||||
ctx.moveTo(centerX + 5, centerY - 3);
|
||||
ctx.lineTo(centerX + 2, centerY - 1);
|
||||
ctx.stroke();
|
||||
|
||||
// Bottom arrow (pointing left) - height to width
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX + 5, centerY + 3);
|
||||
ctx.lineTo(centerX - 7, centerY + 3);
|
||||
ctx.stroke();
|
||||
|
||||
// Bottom arrow head
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX - 7, centerY + 3);
|
||||
ctx.lineTo(centerX - 4, centerY + 1);
|
||||
ctx.moveTo(centerX - 7, centerY + 3);
|
||||
ctx.lineTo(centerX - 4, centerY + 5);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const onMouseDown = nodeType.prototype.onMouseDown;
|
||||
nodeType.prototype.onMouseDown = function (e) {
|
||||
// Check if click is on swap button
|
||||
const swapButtonSize = 24;
|
||||
const margin = 6;
|
||||
const swapButtonX =
|
||||
this.pos[0] + this.size[0] - swapButtonSize - margin;
|
||||
|
||||
// Use same positioning logic as drawing
|
||||
const estimatedWidgetHeight = 90;
|
||||
const topMargin = 35;
|
||||
const buttonSpacing = 40;
|
||||
const swapButtonY =
|
||||
this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
|
||||
|
||||
if (
|
||||
e.canvasX >= swapButtonX &&
|
||||
e.canvasX <= swapButtonX + swapButtonSize &&
|
||||
e.canvasY >= swapButtonY &&
|
||||
e.canvasY <= swapButtonY + swapButtonSize
|
||||
) {
|
||||
// Visual feedback - set button as pressed
|
||||
this.swapButtonPressed = true;
|
||||
this.setDirtyCanvas(true, true);
|
||||
|
||||
// Execute swap
|
||||
this.swapDimensions();
|
||||
|
||||
// Reset button state after a short delay for visual feedback
|
||||
setTimeout(() => {
|
||||
this.swapButtonPressed = false;
|
||||
this.setDirtyCanvas(true, true);
|
||||
}, 150);
|
||||
|
||||
return true; // Consume the event
|
||||
}
|
||||
|
||||
// Call original onMouseDown if not clicking swap button
|
||||
if (onMouseDown) {
|
||||
return onMouseDown.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Optional: Add hover effect for better user feedback
|
||||
const onMouseMove = nodeType.prototype.onMouseMove;
|
||||
nodeType.prototype.onMouseMove = function (e) {
|
||||
// Check if hovering over swap button
|
||||
const swapButtonSize = 24;
|
||||
const margin = 6;
|
||||
const swapButtonX =
|
||||
this.pos[0] + this.size[0] - swapButtonSize - margin;
|
||||
|
||||
// Use same positioning logic as drawing
|
||||
const estimatedWidgetHeight = 90;
|
||||
const topMargin = 35;
|
||||
const buttonSpacing = 40;
|
||||
const swapButtonY =
|
||||
this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
|
||||
|
||||
const isHovering =
|
||||
e.canvasX >= swapButtonX &&
|
||||
e.canvasX <= swapButtonX + swapButtonSize &&
|
||||
e.canvasY >= swapButtonY &&
|
||||
e.canvasY <= swapButtonY + swapButtonSize;
|
||||
|
||||
// Update cursor style for better UX (safely)
|
||||
if (
|
||||
isHovering &&
|
||||
this.graph &&
|
||||
this.graph.canvas &&
|
||||
this.graph.canvas.canvas
|
||||
) {
|
||||
this.graph.canvas.canvas.style.cursor = "pointer";
|
||||
} else if (
|
||||
this.graph &&
|
||||
this.graph.canvas &&
|
||||
this.graph.canvas.canvas
|
||||
) {
|
||||
this.graph.canvas.canvas.style.cursor = "default";
|
||||
}
|
||||
|
||||
// Call original onMouseMove
|
||||
if (onMouseMove) {
|
||||
return onMouseMove.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
+44
-24
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user