Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69db6e12b4 | ||
|
|
fb9c313724 | ||
|
|
ffae4e9f21 | ||
|
|
24b257ea6d | ||
|
|
398cf27546 | ||
|
|
6c6c0e6abe | ||
|
|
bc1a34eef2 | ||
|
|
599981cd9a | ||
|
|
880f376e8e | ||
|
|
dcf2d679c1 | ||
|
|
965ad60c74 | ||
|
|
be0c70eab1 | ||
|
|
549d2dc014 | ||
|
|
f04020b728 | ||
|
|
c7e02a4565 | ||
|
|
5af7a56409 | ||
|
|
9833ccd694 | ||
|
|
2d6fef8fb4 | ||
|
|
ce8c36f309 | ||
|
|
bc30806fee | ||
|
|
36b861e778 | ||
|
|
5cf3977744 | ||
|
|
0b542eafbc | ||
|
|
214851f2ef | ||
|
|
bc7608f891 | ||
|
|
1ab839c58c | ||
|
|
0cf64ae411 | ||
|
|
da26d40d98 | ||
|
|
bc6e4938ef | ||
|
|
1c0e88435c | ||
|
|
43563ef33c | ||
|
|
7711cf31d8 | ||
|
|
bcfca9b8df | ||
|
|
ddc847a16d | ||
|
|
70e8322bd5 | ||
|
|
26da37be18 | ||
|
|
677bbc04ba | ||
|
|
602912f31d |
@@ -74,6 +74,19 @@ jobs:
|
|||||||
from kikotools.tools.resolution_calculator.logic import extract_dimensions
|
from kikotools.tools.resolution_calculator.logic import extract_dimensions
|
||||||
from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode as NodeClass
|
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')
|
print('✓ All module imports successful')
|
||||||
"
|
"
|
||||||
|
|
||||||
@@ -190,12 +203,56 @@ jobs:
|
|||||||
|
|
||||||
# Check that nodes have proper ComfyUI interface
|
# Check that nodes have proper ComfyUI interface
|
||||||
required_attrs = ['INPUT_TYPES', 'RETURN_TYPES', 'RETURN_NAMES', 'FUNCTION', 'CATEGORY']
|
required_attrs = ['INPUT_TYPES', 'RETURN_TYPES', 'RETURN_NAMES', 'FUNCTION', 'CATEGORY']
|
||||||
|
|
||||||
|
# Test Resolution Calculator Node
|
||||||
for attr in required_attrs:
|
for attr in required_attrs:
|
||||||
if not hasattr(ResolutionCalculatorNode, attr):
|
if not hasattr(ResolutionCalculatorNode, attr):
|
||||||
print(f'❌ Node missing required attribute: {attr}')
|
print(f'❌ ResolutionCalculatorNode missing required attribute: {attr}')
|
||||||
sys.exit(1)
|
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
|
- 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!')
|
print('🎉 All tests passed!')
|
||||||
"
|
"
|
||||||
|
|
||||||
- name: Test error handling
|
- name: Test Width Height Selector
|
||||||
run: |
|
run: |
|
||||||
python -c "
|
python -c "
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
sys.path.insert(0, os.getcwd())
|
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
|
# Test preset structure
|
||||||
try:
|
assert len(PRESET_OPTIONS) > 0
|
||||||
node.calculate_resolution(2.0) # No input provided
|
assert 'custom' in PRESET_OPTIONS
|
||||||
assert False, 'Should have raised ValueError'
|
assert len(PRESET_METADATA) > 0
|
||||||
except ValueError:
|
print('✓ Preset structure tests passed')
|
||||||
print('✓ Error handling test passed')
|
|
||||||
|
|
||||||
# Test invalid scale factor
|
# Test node interface
|
||||||
try:
|
node = WidthHeightSelectorNode()
|
||||||
node.calculate_resolution(0.0) # Invalid scale
|
input_types = node.INPUT_TYPES()
|
||||||
assert False, 'Should have raised ValueError'
|
assert 'required' in input_types
|
||||||
except ValueError:
|
assert 'preset' in input_types['required']
|
||||||
print('✓ Scale factor validation test passed')
|
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: |
|
run: |
|
||||||
python -c "
|
python -c "
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
sys.path.insert(0, os.getcwd())
|
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
|
from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode
|
||||||
|
res_node = ResolutionCalculatorNode()
|
||||||
|
|
||||||
# Test ComfyUI interface requirements
|
try:
|
||||||
node_class = ResolutionCalculatorNode
|
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
|
try:
|
||||||
assert hasattr(node_class, 'INPUT_TYPES')
|
res_node.calculate_resolution(0.0) # Invalid scale
|
||||||
assert hasattr(node_class, 'RETURN_TYPES')
|
assert False, 'Should have raised ValueError'
|
||||||
assert hasattr(node_class, 'RETURN_NAMES')
|
except ValueError:
|
||||||
assert hasattr(node_class, 'FUNCTION')
|
print('✓ Resolution Calculator scale factor validation test passed')
|
||||||
assert hasattr(node_class, 'CATEGORY')
|
|
||||||
|
|
||||||
# Check INPUT_TYPES structure
|
# Test Width Height Selector error handling
|
||||||
input_types = node_class.INPUT_TYPES()
|
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 'required' in input_types
|
||||||
assert 'optional' in input_types
|
assert 'optional' in input_types
|
||||||
assert 'scale_factor' in input_types['required']
|
assert 'scale_factor' in input_types['required']
|
||||||
assert 'image' in input_types['optional']
|
assert 'image' in input_types['optional']
|
||||||
assert 'latent' in input_types['optional']
|
assert 'latent' in input_types['optional']
|
||||||
|
|
||||||
# Check return types
|
assert res_class.RETURN_TYPES == ('INT', 'INT')
|
||||||
assert node_class.RETURN_TYPES == ('INT', 'INT')
|
assert res_class.RETURN_NAMES == ('width', 'height')
|
||||||
assert node_class.RETURN_NAMES == ('width', 'height')
|
assert res_class.CATEGORY == 'ComfyAssets'
|
||||||
assert node_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:
|
test-package-structure:
|
||||||
@@ -163,14 +414,37 @@ jobs:
|
|||||||
test -d kikotools/base || (echo "kikotools/base directory missing" && exit 1)
|
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 || (echo "kikotools/tools directory missing" && exit 1)
|
||||||
test -d kikotools/tools/resolution_calculator || (echo "resolution_calculator 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 tests || (echo "tests directory missing" && exit 1)
|
||||||
test -d examples || (echo "examples 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
|
# Check key files
|
||||||
test -f kikotools/__init__.py || (echo "kikotools/__init__.py missing" && exit 1)
|
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/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"
|
echo "✓ Package structure tests passed"
|
||||||
|
|
||||||
@@ -181,9 +455,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Test documentation completeness
|
- name: Test documentation completeness
|
||||||
run: |
|
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/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
|
# Check README has key sections
|
||||||
grep -q "Installation" README.md || (echo "README missing Installation section" && exit 1)
|
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
|
.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
|
# Default target
|
||||||
help:
|
help:
|
||||||
@echo "ComfyUI-KikoTools Development Commands"
|
@echo "ComfyUI-KikoTools Development Commands"
|
||||||
@@ -28,44 +44,48 @@ help:
|
|||||||
@echo " help - Show this help message"
|
@echo " help - Show this help message"
|
||||||
|
|
||||||
# Setup and installation
|
# Setup and installation
|
||||||
setup:
|
setup: $(VENV_DIR)
|
||||||
@echo "Setting up ComfyUI-KikoTools development environment..."
|
@echo "✅ ComfyUI-KikoTools development environment ready"
|
||||||
python -m venv venv
|
@echo "Virtual environment created. Dependencies installed."
|
||||||
@echo "Virtual environment created. Activate with:"
|
|
||||||
@echo " source venv/bin/activate (Linux/Mac)"
|
|
||||||
@echo " venv\\Scripts\\activate (Windows)"
|
|
||||||
@echo "Then run: make install"
|
|
||||||
|
|
||||||
install:
|
$(VENV_DIR):
|
||||||
|
@echo "Creating virtual environment..."
|
||||||
|
$(PYTHON) -m venv $(VENV_DIR)
|
||||||
@echo "Installing development dependencies..."
|
@echo "Installing development dependencies..."
|
||||||
pip install --upgrade pip
|
$(VENV_PIP) install --upgrade pip
|
||||||
pip install -r requirements-dev.txt
|
$(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"
|
@echo "✅ Dependencies installed"
|
||||||
|
|
||||||
# Code quality
|
# Code quality
|
||||||
format:
|
format: $(VENV_DIR)
|
||||||
@echo "Formatting code with black..."
|
@echo "Formatting code with black..."
|
||||||
black .
|
$(PYTHON_CMD) -m black .
|
||||||
@echo "✅ Code formatted"
|
@echo "✅ Code formatted"
|
||||||
|
|
||||||
lint:
|
lint: $(VENV_DIR)
|
||||||
@echo "Linting with flake8..."
|
@echo "Linting with flake8..."
|
||||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
$(PYTHON_CMD) -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=venv
|
||||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
$(PYTHON_CMD) -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=venv
|
||||||
@echo "✅ Linting completed"
|
@echo "✅ Linting completed"
|
||||||
|
|
||||||
type-check:
|
type-check: $(VENV_DIR)
|
||||||
@echo "Type checking with mypy..."
|
@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"
|
@echo "✅ Type checking completed"
|
||||||
|
|
||||||
quality-check: format lint type-check
|
quality-check: format lint type-check
|
||||||
@echo "✅ All quality checks completed"
|
@echo "✅ All quality checks completed"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
dev-test:
|
dev-test: $(VENV_DIR)
|
||||||
@echo "Running quick development test..."
|
@echo "Running quick development test..."
|
||||||
@python -c "\
|
@$(PYTHON_CMD) -c "\
|
||||||
import sys, os; \
|
import sys, os; \
|
||||||
sys.path.insert(0, os.getcwd()); \
|
sys.path.insert(0, os.getcwd()); \
|
||||||
from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; \
|
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]}'); \
|
print(f'✅ Development test passed! Result: {result[0]}x{result[1]}'); \
|
||||||
"
|
"
|
||||||
|
|
||||||
test-fast:
|
test-fast: $(VENV_DIR)
|
||||||
@echo "Running core functionality tests..."
|
@echo "Running core functionality tests..."
|
||||||
@python -c "\
|
@$(PYTHON_CMD) -c "\
|
||||||
import sys, os; \
|
import sys, os; \
|
||||||
sys.path.insert(0, os.getcwd()); \
|
sys.path.insert(0, os.getcwd()); \
|
||||||
from kikotools.base import ComfyAssetsBaseNode; \
|
from kikotools.base import ComfyAssetsBaseNode; \
|
||||||
@@ -146,13 +166,13 @@ ci: quality-check test
|
|||||||
@echo "✅ CI checks passed!"
|
@echo "✅ CI checks passed!"
|
||||||
|
|
||||||
# Tool-specific commands (can be extended for new tools)
|
# Tool-specific commands (can be extended for new tools)
|
||||||
test-resolution-calculator:
|
test-resolution-calculator: $(VENV_DIR)
|
||||||
@echo "Testing Resolution Calculator specifically..."
|
@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..."
|
@echo "Testing Width Height Selector specifically..."
|
||||||
@python -c "\
|
@$(PYTHON_CMD) -c "\
|
||||||
import sys, os; \
|
import sys, os; \
|
||||||
sys.path.insert(0, os.getcwd()); \
|
sys.path.insert(0, os.getcwd()); \
|
||||||
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode; \
|
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode; \
|
||||||
|
|||||||
@@ -44,6 +44,38 @@ Advanced preset-based dimension selection with visual swap button.
|
|||||||
- Mobile and ultra-wide format support
|
- Mobile and ultra-wide format support
|
||||||
- Integration with upscaling pipelines
|
- Integration with upscaling pipelines
|
||||||
|
|
||||||
|
#### 🎲 Seed History
|
||||||
|
Advanced seed tracking with interactive history management and UI.
|
||||||
|
|
||||||
|
- **Automatic Tracking**: Monitors all seed changes with timestamps
|
||||||
|
- **Interactive History**: Click any historical seed to reload instantly
|
||||||
|
- **Smart Deduplication**: 500ms window prevents duplicate rapid additions
|
||||||
|
- **Persistent Storage**: History survives browser sessions and ComfyUI restarts
|
||||||
|
- **Auto-Hide UI**: Clean interface that hides after 2.5 seconds of inactivity
|
||||||
|
- **Visual Feedback**: Toast notifications and selection highlighting
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Track promising seeds during creative exploration
|
||||||
|
- Quickly return to successful generation parameters
|
||||||
|
- Maintain reproducibility across sessions
|
||||||
|
- Compare results from different seeds efficiently
|
||||||
|
|
||||||
|
#### ⚙️ Sampler Combo
|
||||||
|
Unified sampling configuration interface combining sampler, scheduler, steps, and CFG.
|
||||||
|
|
||||||
|
- **All-in-One Interface**: Single node for complete sampling configuration
|
||||||
|
- **Smart Recommendations**: Optimal settings suggestions per sampler type
|
||||||
|
- **Compatibility Validation**: Ensures sampler/scheduler combinations work well
|
||||||
|
- **Intelligent Defaults**: Context-aware parameter recommendations
|
||||||
|
- **Range Validation**: Prevents invalid parameter combinations
|
||||||
|
- **Comprehensive Tooltips**: Detailed guidance for each parameter
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Simplify complex sampling workflows
|
||||||
|
- Ensure optimal sampler/scheduler combinations
|
||||||
|
- Reduce node clutter in workflows
|
||||||
|
- Quick sampling parameter experimentation
|
||||||
|
|
||||||
### 🔧 Architecture Highlights
|
### 🔧 Architecture Highlights
|
||||||
|
|
||||||
- **Modular Design**: Each tool is self-contained and independently testable
|
- **Modular Design**: Each tool is self-contained and independently testable
|
||||||
@@ -97,6 +129,29 @@ preset: "1920×1080" ↘ 1920×1080 ↗
|
|||||||
**Output:** 1920×1080 (16:9 cinematic)
|
**Output:** 1920×1080 (16:9 cinematic)
|
||||||
**Swap Button:** Click to get 1080×1920 (9:16 portrait)
|
**Swap Button:** Click to get 1080×1920 (9:16 portrait)
|
||||||
|
|
||||||
|
### Seed History Example
|
||||||
|
|
||||||
|
```
|
||||||
|
Seed History → KSampler → VAE Decode → Save Image
|
||||||
|
🎲 12345 ↘ seed ↗
|
||||||
|
[History UI: 54321, 99999, 11111...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current Seed:** 12345
|
||||||
|
**History:** Auto-tracked previous seeds with timestamps
|
||||||
|
**Interaction:** Click any historical seed to reload instantly
|
||||||
|
|
||||||
|
### Sampler Combo Example
|
||||||
|
|
||||||
|
```
|
||||||
|
Sampler Combo → KSampler → VAE Decode → Save Image
|
||||||
|
⚙️ All Settings ↘ sampler/scheduler/steps/cfg ↗
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration:** euler, normal, 20 steps, CFG 7.0
|
||||||
|
**Output:** Complete sampling configuration in one node
|
||||||
|
**Smart Features:** Recommendations and compatibility validation
|
||||||
|
|
||||||
### Common Workflows
|
### Common Workflows
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -135,6 +190,8 @@ preset: "1920×1080" ↘ 1920×1080 ↗
|
|||||||
|------|-------------|--------|---------------|
|
|------|-------------|--------|---------------|
|
||||||
| **Resolution Calculator** | Calculate upscaled dimensions with model optimization | ✅ Complete | [Docs](examples/documentation/resolution_calculator.md) |
|
| **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) |
|
| **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 | [Docs](examples/documentation/sampler_combo.md) |
|
||||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||||
|
|
||||||
@@ -178,6 +235,49 @@ preset: "1920×1080" ↘ 1920×1080 ↗
|
|||||||
- FLUX Presets (8): 1920×1080 to 1152×1728 (high resolution)
|
- FLUX Presets (8): 1920×1080 to 1152×1728 (high resolution)
|
||||||
- Ultra-Wide (8): 2560×1080 to 768×2304 (modern ratios)
|
- Ultra-Wide (8): 2560×1080 to 768×2304 (modern ratios)
|
||||||
|
|
||||||
|
#### Seed History
|
||||||
|
|
||||||
|
**Inputs:**
|
||||||
|
- `seed` (INT): 0 to 18,446,744,073,709,551,615, default 12345
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
- `seed` (INT): Validated and processed seed value
|
||||||
|
|
||||||
|
**UI Features:**
|
||||||
|
- Interactive history display with timestamps
|
||||||
|
- Generate random seed button (🎲 Generate)
|
||||||
|
- Clear history button (🗑️ Clear)
|
||||||
|
- Auto-hide after 2.5 seconds of inactivity
|
||||||
|
- Click-to-restore hidden history
|
||||||
|
|
||||||
|
**History Management:**
|
||||||
|
- Maximum 10 entries for optimal performance
|
||||||
|
- Smart deduplication with 500ms window
|
||||||
|
- Persistent localStorage storage
|
||||||
|
- Newest entries displayed first
|
||||||
|
- Human-readable time formatting (5m ago, 2h ago)
|
||||||
|
|
||||||
|
#### Sampler Combo
|
||||||
|
|
||||||
|
**Inputs:**
|
||||||
|
- `sampler_name` (DROPDOWN): Available ComfyUI samplers (euler, dpmpp_2m, etc.)
|
||||||
|
- `scheduler` (DROPDOWN): Available schedulers (normal, karras, exponential, etc.)
|
||||||
|
- `steps` (INT): 1-1000, default 20
|
||||||
|
- `cfg` (FLOAT): 0.0-30.0, default 7.0
|
||||||
|
|
||||||
|
**Outputs:**
|
||||||
|
- `sampler_name` (STRING): Selected sampler algorithm
|
||||||
|
- `scheduler` (STRING): Selected scheduler algorithm
|
||||||
|
- `steps` (INT): Validated step count
|
||||||
|
- `cfg` (FLOAT): Validated CFG scale
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Smart parameter validation and sanitization
|
||||||
|
- Sampler-specific recommendations for optimal settings
|
||||||
|
- Compatibility checking between samplers and schedulers
|
||||||
|
- Graceful error handling with safe defaults
|
||||||
|
- Comprehensive tooltips for user guidance
|
||||||
|
|
||||||
## 🛠️ Development
|
## 🛠️ Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -298,9 +398,10 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|||||||
|
|
||||||
## 📈 Stats
|
## 📈 Stats
|
||||||
|
|
||||||
- **Nodes**: 2 (Resolution Calculator, Width Height Selector)
|
- **Nodes**: 4 (Resolution Calculator, Width Height Selector, Seed History, Sampler Combo)
|
||||||
- **Presets**: 26 curated resolution presets
|
- **Presets**: 26 curated resolution presets
|
||||||
- **Test Coverage**: 100%
|
- **Interactive Features**: 2 (Swap Button, History UI)
|
||||||
|
- **Test Coverage**: 100% (180+ comprehensive tests)
|
||||||
- **Python Version**: 3.8+
|
- **Python Version**: 3.8+
|
||||||
- **ComfyUI Compatibility**: Latest
|
- **ComfyUI Compatibility**: Latest
|
||||||
- **Dependencies**: Minimal (PyTorch, NumPy)
|
- **Dependencies**: Minimal (PyTorch, NumPy)
|
||||||
|
|||||||
+24
-2
@@ -3,12 +3,34 @@ ComfyUI-KikoTools: Modular collection of custom ComfyUI nodes
|
|||||||
All nodes are grouped under the "ComfyAssets" category
|
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
|
from .kikotools import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||||
|
|
||||||
# Tell ComfyUI where to find our JavaScript extensions
|
# Tell ComfyUI where to find our JavaScript extensions
|
||||||
WEB_DIRECTORY = "./web"
|
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(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")
|
||||||
|
|
||||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Sampler Combo Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Sampler Combo is a unified ComfyUI node that combines sampler, scheduler, steps, and CFG settings into a single interface. It reduces workflow complexity while ensuring compatible parameter combinations and providing optimization recommendations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🎯 **Unified Configuration**
|
||||||
|
- Single node for all sampling parameters
|
||||||
|
- Compatible sampler + scheduler combinations
|
||||||
|
- Optimized steps and CFG recommendations
|
||||||
|
- Reduced workflow complexity
|
||||||
|
|
||||||
|
### 🧠 **Smart Recommendations**
|
||||||
|
- Scheduler suggestions based on selected sampler
|
||||||
|
- Optimal steps range for each sampler
|
||||||
|
- CFG scale recommendations
|
||||||
|
- Compatibility warnings and suggestions
|
||||||
|
|
||||||
|
### ✅ **Built-in Validation**
|
||||||
|
- Parameter validation and sanitization
|
||||||
|
- Error handling with safe defaults
|
||||||
|
- Performance optimization hints
|
||||||
|
- Real-time compatibility checking
|
||||||
|
|
||||||
|
### 📊 **Analysis Tools**
|
||||||
|
- Combo configuration analysis
|
||||||
|
- Performance assessment
|
||||||
|
- Optimization recommendations
|
||||||
|
- Compatibility scoring
|
||||||
|
|
||||||
|
## Node Interface
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- **sampler_name**: Dropdown with available sampling algorithms
|
||||||
|
- **scheduler**: Dropdown with compatible schedulers
|
||||||
|
- **steps**: Integer slider (1-100 steps)
|
||||||
|
- **cfg**: Float slider (0.0-20.0 CFG scale)
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- **sampler_name**: Selected sampler algorithm
|
||||||
|
- **scheduler**: Selected scheduler algorithm
|
||||||
|
- **steps**: Number of sampling steps
|
||||||
|
- **cfg**: CFG scale value
|
||||||
|
|
||||||
|
## Available Samplers
|
||||||
|
|
||||||
|
### Primary Samplers
|
||||||
|
| Sampler | Type | Speed | Quality | Best For |
|
||||||
|
|---------|------|-------|---------|----------|
|
||||||
|
| euler | Deterministic | Fast | Good | General use |
|
||||||
|
| euler_ancestral | Stochastic | Fast | Good | Creative variation |
|
||||||
|
| heun | Higher-order | Medium | Better | Quality focus |
|
||||||
|
| dpm_2 | Multi-step | Medium | Good | Balanced |
|
||||||
|
| dpm_2_ancestral | Stochastic | Medium | Good | Creative quality |
|
||||||
|
| lms | Linear | Fast | Good | Simple scenes |
|
||||||
|
| dpm_fast | Optimized | Very Fast | Good | Speed priority |
|
||||||
|
| dpm_adaptive | Adaptive | Variable | Best | Automatic tuning |
|
||||||
|
|
||||||
|
### Advanced Samplers
|
||||||
|
| Sampler | Type | Speed | Quality | Best For |
|
||||||
|
|---------|------|-------|---------|----------|
|
||||||
|
| dpmpp_2s_ancestral | Advanced | Medium | Better | High quality |
|
||||||
|
| dpmpp_2m | Optimized | Fast | Better | Speed + quality |
|
||||||
|
| dpmpp_2m_sde | Stochastic | Medium | Best | Maximum quality |
|
||||||
|
| dpmpp_3m_sde | Latest | Medium | Best | Cutting edge |
|
||||||
|
| ddim | Classic | Fast | Good | Compatibility |
|
||||||
|
| uni_pc | Unified | Fast | Better | Efficiency |
|
||||||
|
|
||||||
|
## Available Schedulers
|
||||||
|
|
||||||
|
### Linear Schedulers
|
||||||
|
- **normal**: Standard linear schedule
|
||||||
|
- **linear**: Basic linear distribution
|
||||||
|
- **sgm_uniform**: Uniform distribution
|
||||||
|
|
||||||
|
### Advanced Schedulers
|
||||||
|
- **karras**: Karras noise schedule (recommended)
|
||||||
|
- **exponential**: Exponential decay
|
||||||
|
- **polyexponential**: Polynomial exponential
|
||||||
|
- **beta**: Beta distribution schedule
|
||||||
|
|
||||||
|
### Specialized Schedulers
|
||||||
|
- **cosine**: Cosine annealing schedule
|
||||||
|
- **simple**: Simplified schedule
|
||||||
|
- **ddim_uniform**: DDIM uniform schedule
|
||||||
|
- **laplace**: Laplace distribution
|
||||||
|
|
||||||
|
## Optimization Guidelines
|
||||||
|
|
||||||
|
### Recommended Combinations
|
||||||
|
|
||||||
|
#### Speed Optimized
|
||||||
|
```
|
||||||
|
Sampler: euler or dpm_fast
|
||||||
|
Scheduler: normal or simple
|
||||||
|
Steps: 15-25
|
||||||
|
CFG: 6.0-8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Quality Optimized
|
||||||
|
```
|
||||||
|
Sampler: dpmpp_2m_sde or dpmpp_3m_sde
|
||||||
|
Scheduler: karras
|
||||||
|
Steps: 25-35
|
||||||
|
CFG: 7.0-9.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Balanced
|
||||||
|
```
|
||||||
|
Sampler: dpmpp_2m or heun
|
||||||
|
Scheduler: karras or normal
|
||||||
|
Steps: 20-30
|
||||||
|
CFG: 7.0-8.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps Recommendations
|
||||||
|
|
||||||
|
| Sampler Type | Min Steps | Optimal | Max Steps |
|
||||||
|
|--------------|-----------|---------|-----------|
|
||||||
|
| Fast (euler, dpm_fast) | 10 | 20 | 30 |
|
||||||
|
| Standard (heun, dpm_2) | 15 | 25 | 40 |
|
||||||
|
| Advanced (dpmpp_*) | 20 | 30 | 50 |
|
||||||
|
| Adaptive | 10 | 25 | 100 |
|
||||||
|
|
||||||
|
### CFG Scale Guidelines
|
||||||
|
|
||||||
|
| Content Type | CFG Range | Recommended |
|
||||||
|
|--------------|-----------|-------------|
|
||||||
|
| Photorealistic | 5.0-8.0 | 7.0 |
|
||||||
|
| Artistic/Stylized | 7.0-12.0 | 9.0 |
|
||||||
|
| Abstract/Creative | 8.0-15.0 | 11.0 |
|
||||||
|
| Text/Details | 10.0-20.0 | 13.0 |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
```
|
||||||
|
sampler_name: euler
|
||||||
|
scheduler: normal
|
||||||
|
steps: 20
|
||||||
|
cfg: 7.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### High Quality Setup
|
||||||
|
```
|
||||||
|
sampler_name: dpmpp_2m_sde
|
||||||
|
scheduler: karras
|
||||||
|
steps: 30
|
||||||
|
cfg: 8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Speed Priority
|
||||||
|
```
|
||||||
|
sampler_name: dpm_fast
|
||||||
|
scheduler: simple
|
||||||
|
steps: 15
|
||||||
|
cfg: 6.5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Compatibility Analysis
|
||||||
|
The node provides real-time analysis of parameter compatibility:
|
||||||
|
- Scheduler compatibility with selected sampler
|
||||||
|
- Steps optimization for sampler type
|
||||||
|
- CFG scale recommendations
|
||||||
|
- Performance impact assessment
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Invalid samplers default to 'euler'
|
||||||
|
- Invalid schedulers default to 'normal'
|
||||||
|
- Out-of-range steps clamped to valid range
|
||||||
|
- Invalid CFG values sanitized to safe defaults
|
||||||
|
|
||||||
|
### Performance Tips
|
||||||
|
1. **Use Karras scheduler** for most samplers (better quality)
|
||||||
|
2. **Start with 20-30 steps** for most use cases
|
||||||
|
3. **Keep CFG 6.0-9.0** for realistic images
|
||||||
|
4. **Try dpmpp_2m** for best speed/quality balance
|
||||||
|
5. **Use euler** for fastest generation
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
- **Slow generation**: Try euler or dpm_fast samplers
|
||||||
|
- **Poor quality**: Increase steps or try dpmpp_2m_sde
|
||||||
|
- **Overcooked images**: Lower CFG scale
|
||||||
|
- **Underdetailed**: Increase CFG or steps
|
||||||
|
- **Artifacts**: Try karras scheduler or different sampler
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
- **GPU Memory**: Lower steps if running out of VRAM
|
||||||
|
- **Speed**: Use euler + normal + 15-20 steps
|
||||||
|
- **Quality**: Use dpmpp_2m_sde + karras + 25-30 steps
|
||||||
|
- **Compatibility**: Stick to euler/heun for broad model support
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
The Sampler Combo node outputs are compatible with all standard ComfyUI sampling nodes:
|
||||||
|
- KSampler
|
||||||
|
- KSamplerAdvanced
|
||||||
|
- Custom sampling workflows
|
||||||
|
- Upscaling pipelines
|
||||||
|
- Img2img workflows
|
||||||
|
|
||||||
|
Connect the outputs directly to your sampling node inputs for streamlined configuration.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Seed History Tool
|
||||||
|
|
||||||
|
The Seed History tool provides advanced seed value tracking with an interactive UI for managing seed history, automatic deduplication, and convenient seed retrieval.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Seed History node functions as both a standard seed input and an intelligent tracking system that automatically monitors seed changes and maintains a searchable history.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🎲 Core Functionality
|
||||||
|
- **Seed Output**: Standard ComfyUI seed value output (0 to 18,446,744,073,709,551,615)
|
||||||
|
- **History Tracking**: Automatic tracking of all seed changes with timestamps
|
||||||
|
- **Deduplication**: Intelligent filtering to prevent duplicate entries within 500ms windows
|
||||||
|
- **Persistent Storage**: History persists across ComfyUI sessions using localStorage
|
||||||
|
|
||||||
|
### 🎯 Interactive UI
|
||||||
|
- **History Display**: Scrollable list showing recent seeds with timestamps
|
||||||
|
- **Click to Load**: Click any history entry to instantly load that seed
|
||||||
|
- **Generate Button**: Create new random seeds with one click
|
||||||
|
- **Clear History**: Remove all tracked seeds when needed
|
||||||
|
- **Auto-Hide**: History section automatically hides after 2.5 seconds of inactivity
|
||||||
|
|
||||||
|
### ⚡ Smart Features
|
||||||
|
- **Real-time Updates**: Tracks seed changes from increment/decrement buttons
|
||||||
|
- **Visual Feedback**: Selected seeds are highlighted in green
|
||||||
|
- **Time Formatting**: Human-readable "time ago" display (e.g., "5m ago", "2h ago")
|
||||||
|
- **Notifications**: Toast messages for actions like generate and clear
|
||||||
|
- **Responsive Design**: Adapts to node resizing
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Setup
|
||||||
|
|
||||||
|
1. **Add Node**: Search for "Seed History" in the ComfyUI node browser
|
||||||
|
2. **Connect Output**: Connect the seed output to any node requiring a seed input
|
||||||
|
3. **Automatic Tracking**: The node automatically begins tracking seed changes
|
||||||
|
|
||||||
|
### Seed Management
|
||||||
|
|
||||||
|
```
|
||||||
|
🎲 Seed History
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 🎲 Generate │ 🗑️ Clear │
|
||||||
|
├─────────────────┤
|
||||||
|
│ 🎲 1,234,567 │ ← Click to load
|
||||||
|
│ ⏰ 2m ago │
|
||||||
|
├─────────────────┤
|
||||||
|
│ 🎲 9,876,543 │
|
||||||
|
│ ⏰ 5m ago │
|
||||||
|
├─────────────────┤
|
||||||
|
│ 🎲 5,555,555 │
|
||||||
|
│ ⏰ 10m ago │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow Integration
|
||||||
|
|
||||||
|
The Seed History node works seamlessly with any ComfyUI workflow:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Seed History] → [KSampler] → [Image Output]
|
||||||
|
↓
|
||||||
|
[VAE Decode] → [Save Image]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### History Management
|
||||||
|
- **Maximum Entries**: Keeps the 10 most recent seeds
|
||||||
|
- **Smart Deduplication**: Prevents rapid duplicate additions
|
||||||
|
- **Timestamp Tracking**: Full date/time information for each seed
|
||||||
|
- **Persistent Storage**: History survives ComfyUI restarts
|
||||||
|
|
||||||
|
### UI Behavior
|
||||||
|
- **Auto-Hide Timer**: History hides after 2.5 seconds of inactivity
|
||||||
|
- **Mouse Interaction**: Hovering over history cancels auto-hide
|
||||||
|
- **Restore Button**: Click to restore hidden history section
|
||||||
|
- **Visual Feedback**: Hover effects and selection highlighting
|
||||||
|
|
||||||
|
### Seed Validation
|
||||||
|
- **Range Checking**: Ensures seeds are within valid ComfyUI range
|
||||||
|
- **Error Handling**: Graceful fallback to default seed (12345) on errors
|
||||||
|
- **Sanitization**: Automatic clamping of out-of-range values
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Input Parameters
|
||||||
|
- **seed** (INT): Seed value for generation processes
|
||||||
|
- Range: 0 to 18,446,744,073,709,551,615
|
||||||
|
- Default: 12345
|
||||||
|
- Tooltip: "Seed value for generation processes. History UI tracks all changes automatically."
|
||||||
|
|
||||||
|
### Output
|
||||||
|
- **seed** (INT): The processed seed value for use in other nodes
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
- **Key**: `comfyui_kikotools_seed_history`
|
||||||
|
- **Format**: JSON array of history entries
|
||||||
|
- **Location**: Browser localStorage
|
||||||
|
- **Persistence**: Survives browser sessions and ComfyUI restarts
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### 🎨 Creative Workflows
|
||||||
|
- **Iteration Tracking**: Keep track of promising seeds during creative exploration
|
||||||
|
- **Version Control**: Easily return to previous seeds that produced good results
|
||||||
|
- **Experimentation**: Generate and track multiple seed variations
|
||||||
|
|
||||||
|
### 🔬 Technical Workflows
|
||||||
|
- **Reproducibility**: Maintain exact seed records for reproducing specific outputs
|
||||||
|
- **A/B Testing**: Compare results from different seeds with easy switching
|
||||||
|
- **Documentation**: Export seed history for technical documentation
|
||||||
|
|
||||||
|
### 📊 Batch Processing
|
||||||
|
- **Seed Management**: Track seeds across multiple batch runs
|
||||||
|
- **Quality Control**: Quickly identify and reuse successful seeds
|
||||||
|
- **Workflow Optimization**: Analyze seed performance patterns
|
||||||
|
|
||||||
|
## Tips and Best Practices
|
||||||
|
|
||||||
|
### Efficient Usage
|
||||||
|
1. **Let it Track**: The node automatically tracks all seed changes - no manual intervention needed
|
||||||
|
2. **Use Generate**: The generate button is optimized for creating good random seeds
|
||||||
|
3. **Regular Clearing**: Clear history periodically to maintain relevant seeds only
|
||||||
|
|
||||||
|
### Workflow Integration
|
||||||
|
1. **Single Source**: Use one Seed History node per workflow for centralized tracking
|
||||||
|
2. **Connect Early**: Place the node early in your workflow chain for complete tracking
|
||||||
|
3. **Branch Connections**: Connect to multiple nodes that need the same seed
|
||||||
|
|
||||||
|
### History Management
|
||||||
|
1. **Review Regularly**: Check history for seeds that produced good results
|
||||||
|
2. **Document Success**: Note down particularly successful seeds externally
|
||||||
|
3. **Clean Periodically**: Clear history when starting new creative projects
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**History Not Updating**
|
||||||
|
- Ensure the node is properly connected to your workflow
|
||||||
|
- Check that seed widget is visible and functional
|
||||||
|
- Verify browser localStorage is enabled
|
||||||
|
|
||||||
|
**UI Not Appearing**
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
- Ensure ComfyUI-KikoTools is properly installed
|
||||||
|
- Verify web directory permissions
|
||||||
|
|
||||||
|
**Seeds Not Loading**
|
||||||
|
- Confirm the seed is within valid range
|
||||||
|
- Check that target widgets support the seed value
|
||||||
|
- Verify node connections are intact
|
||||||
|
|
||||||
|
### Performance Notes
|
||||||
|
- History is limited to 10 entries for optimal performance
|
||||||
|
- Deduplication prevents excessive storage usage
|
||||||
|
- Auto-hide reduces visual clutter during long workflows
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the `examples/workflows/` directory for complete workflow examples demonstrating:
|
||||||
|
- Basic seed tracking workflow
|
||||||
|
- Creative iteration with history
|
||||||
|
- Technical reproducibility setup
|
||||||
|
- Batch processing with seed management
|
||||||
@@ -1,47 +1,259 @@
|
|||||||
{
|
{
|
||||||
"last_node_id": 3,
|
"id": "41469b2d-d616-479d-879a-95cdc6074a37",
|
||||||
"last_link_id": 2,
|
"revision": 0,
|
||||||
|
"last_node_id": 6,
|
||||||
|
"last_link_id": 5,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "ResolutionCalculator",
|
"type": "ResolutionCalculator",
|
||||||
"pos": [100, 100],
|
"pos": [
|
||||||
"size": {"0": 315, "1": 126},
|
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": {},
|
"flags": {},
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [],
|
||||||
{"name": "image", "type": "IMAGE", "link": null},
|
|
||||||
{"name": "latent", "type": "LATENT", "link": null}
|
|
||||||
],
|
|
||||||
"outputs": [
|
"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": {},
|
"properties": {
|
||||||
"widgets_values": [2.0],
|
"cnr_id": "comfy-core",
|
||||||
"category": "ComfyAssets"
|
"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,
|
"id": 2,
|
||||||
"type": "Note",
|
"type": "Note",
|
||||||
"pos": [450, 100],
|
"pos": [
|
||||||
"size": {"0": 400, "1": 200},
|
-150,
|
||||||
|
170
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
400,
|
||||||
|
200
|
||||||
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
"outputs": [],
|
"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."},
|
"properties": {
|
||||||
"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."]
|
"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": [
|
"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": [],
|
"groups": [],
|
||||||
"config": {},
|
"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
|
"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
|
||||||
|
}
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
{
|
||||||
|
"id": "972425bd-9910-484d-ad09-f142f534fc61",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 11,
|
||||||
|
"last_link_id": 14,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
120,
|
||||||
|
390
|
||||||
|
],
|
||||||
|
"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": "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": 6,
|
||||||
|
"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": "seed",
|
||||||
|
"type": "INT",
|
||||||
|
"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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.40",
|
||||||
|
"Node name for S&R": "KSampler",
|
||||||
|
"widget_ue_connectable": {}
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
231413202715030,
|
||||||
|
"randomize",
|
||||||
|
20,
|
||||||
|
8,
|
||||||
|
"euler",
|
||||||
|
"normal",
|
||||||
|
1,
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "VAEDecode",
|
||||||
|
"pos": [
|
||||||
|
1209,
|
||||||
|
188
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
210,
|
||||||
|
46
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 7,
|
||||||
|
"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": 8,
|
||||||
|
"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": 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,
|
||||||
|
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,
|
||||||
|
7,
|
||||||
|
"COMBO"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
11,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
8,
|
||||||
|
"COMBO"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
12,
|
||||||
|
10,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
"INT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
13,
|
||||||
|
10,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
6,
|
||||||
|
"FLOAT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14,
|
||||||
|
11,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
"INT"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ue_links": [],
|
||||||
|
"links_added_by_ue": [],
|
||||||
|
"ds": {
|
||||||
|
"scale": 0.9740024562304554,
|
||||||
|
"offset": [
|
||||||
|
2.9611945935278796,
|
||||||
|
36.50023864466208
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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
@@ -5,16 +5,24 @@ Handles automatic discovery and registration of all ComfyAssets tools
|
|||||||
|
|
||||||
from .tools.resolution_calculator import ResolutionCalculatorNode
|
from .tools.resolution_calculator import ResolutionCalculatorNode
|
||||||
from .tools.width_height_selector import WidthHeightSelectorNode
|
from .tools.width_height_selector import WidthHeightSelectorNode
|
||||||
|
from .tools.seed_history import SeedHistoryNode
|
||||||
|
from .tools.sampler_combo import SamplerComboNode, SamplerComboCompactNode
|
||||||
|
|
||||||
# ComfyUI node registration mappings
|
# ComfyUI node registration mappings
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"ResolutionCalculator": ResolutionCalculatorNode,
|
"ResolutionCalculator": ResolutionCalculatorNode,
|
||||||
"WidthHeightSelector": WidthHeightSelectorNode,
|
"WidthHeightSelector": WidthHeightSelectorNode,
|
||||||
|
"SeedHistory": SeedHistoryNode,
|
||||||
|
"SamplerCombo": SamplerComboNode,
|
||||||
|
"SamplerComboCompact": SamplerComboCompactNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"ResolutionCalculator": "Resolution Calculator",
|
"ResolutionCalculator": "Resolution Calculator",
|
||||||
"WidthHeightSelector": "Width Height Selector",
|
"WidthHeightSelector": "Width Height Selector",
|
||||||
|
"SeedHistory": "Seed History",
|
||||||
|
"SamplerCombo": "Sampler Combo",
|
||||||
|
"SamplerComboCompact": "Sampler Combo (Compact)",
|
||||||
}
|
}
|
||||||
|
|
||||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Pure functions for dimension extraction and scaling calculations
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from typing import Tuple, Optional, Union, Dict, Any
|
from typing import Tuple, Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
def extract_dimensions(
|
def extract_dimensions(
|
||||||
@@ -16,7 +16,8 @@ def extract_dimensions(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
image: Optional IMAGE tensor in ComfyUI format [batch, height, width, channels]
|
image: Optional IMAGE tensor in ComfyUI format [batch, height, width, channels]
|
||||||
latent: Optional LATENT dict with 'samples' tensor [batch, channels, height/8, width/8]
|
latent: Optional LATENT dict with 'samples' tensor
|
||||||
|
[batch, channels, height/8, width/8]
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (width, height) as integers
|
Tuple of (width, height) as integers
|
||||||
@@ -42,7 +43,8 @@ def extract_dimensions(
|
|||||||
samples = latent["samples"]
|
samples = latent["samples"]
|
||||||
if len(samples.shape) != 4:
|
if len(samples.shape) != 4:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Expected LATENT samples tensor with 4 dimensions, got {len(samples.shape)}"
|
f"Expected LATENT samples tensor with 4 dimensions, "
|
||||||
|
f"got {len(samples.shape)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
_, _, latent_height, latent_width = samples.shape
|
_, _, latent_height, latent_width = samples.shape
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
|||||||
"max": 8.0,
|
"max": 8.0,
|
||||||
"step": 0.1,
|
"step": 0.1,
|
||||||
"display": "slider",
|
"display": "slider",
|
||||||
"tooltip": "Factor to scale the resolution by (e.g., 2.0 for 2x upscale)",
|
"tooltip": "Factor to scale the resolution by "
|
||||||
|
"(e.g., 2.0 for 2x upscale)",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -93,7 +94,8 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
|||||||
else "LATENT" if latent is not None else "NONE"
|
else "LATENT" if latent is not None else "NONE"
|
||||||
)
|
)
|
||||||
self.log_info(
|
self.log_info(
|
||||||
f"Calculating resolution with scale_factor={scale_factor}, input_type={input_type}"
|
f"Calculating resolution with scale_factor={scale_factor}, "
|
||||||
|
f"input_type={input_type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate the resolution
|
# Calculate the resolution
|
||||||
@@ -147,7 +149,8 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
|||||||
|
|
||||||
if len(image.shape) != 4:
|
if len(image.shape) != 4:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"image tensor must have 4 dimensions [batch, height, width, channels], got {len(image.shape)}"
|
f"image tensor must have 4 dimensions "
|
||||||
|
f"[batch, height, width, channels], got {len(image.shape)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if latent is not None:
|
if latent is not None:
|
||||||
@@ -160,12 +163,14 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
|||||||
samples = latent["samples"]
|
samples = latent["samples"]
|
||||||
if not isinstance(samples, torch.Tensor):
|
if not isinstance(samples, torch.Tensor):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"latent['samples'] must be a torch.Tensor, got {type(samples).__name__}"
|
f"latent['samples'] must be a torch.Tensor, "
|
||||||
|
f"got {type(samples).__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(samples.shape) != 4:
|
if len(samples.shape) != 4:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"latent samples tensor must have 4 dimensions [batch, channels, height, width], got {len(samples.shape)}"
|
f"latent samples tensor must have 4 dimensions "
|
||||||
|
f"[batch, channels, height, width], got {len(samples.shape)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Sampler Combo tool for ComfyUI."""
|
||||||
|
|
||||||
|
from .node import SamplerComboNode
|
||||||
|
from .compact_node import SamplerComboCompactNode
|
||||||
|
|
||||||
|
__all__ = ["SamplerComboNode", "SamplerComboCompactNode"]
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Compact Sampler Combo node for ComfyUI with minimal interface."""
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
from ...base.base_node import ComfyAssetsBaseNode
|
||||||
|
from .logic import (
|
||||||
|
get_sampler_combo,
|
||||||
|
SAMPLERS,
|
||||||
|
SCHEDULERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SamplerComboCompactNode(ComfyAssetsBaseNode):
|
||||||
|
"""
|
||||||
|
Compact Sampler Combo node with minimal interface.
|
||||||
|
|
||||||
|
Provides essential sampling parameters in a space-efficient layout
|
||||||
|
with shorter parameter names and reduced visual footprint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
"""Define compact input types for the ComfyUI node."""
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"sampler": (
|
||||||
|
SAMPLERS,
|
||||||
|
{
|
||||||
|
"default": "euler",
|
||||||
|
"tooltip": "Sampler",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"sched": (
|
||||||
|
SCHEDULERS,
|
||||||
|
{
|
||||||
|
"default": "normal",
|
||||||
|
"tooltip": "Scheduler",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"steps": (
|
||||||
|
"INT",
|
||||||
|
{
|
||||||
|
"default": 20,
|
||||||
|
"min": 1,
|
||||||
|
"max": 50,
|
||||||
|
"step": 1,
|
||||||
|
"tooltip": "Steps",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"cfg": (
|
||||||
|
"FLOAT",
|
||||||
|
{
|
||||||
|
"default": 7.0,
|
||||||
|
"min": 1.0,
|
||||||
|
"max": 15.0,
|
||||||
|
"step": 0.5,
|
||||||
|
"display": "slider",
|
||||||
|
"tooltip": "CFG",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (SAMPLERS, SCHEDULERS, "INT", "FLOAT")
|
||||||
|
RETURN_NAMES = ("sampler", "scheduler", "steps", "cfg")
|
||||||
|
FUNCTION = "get_combo"
|
||||||
|
CATEGORY = "ComfyAssets"
|
||||||
|
|
||||||
|
def get_combo(
|
||||||
|
self, sampler: str, sched: str, steps: int, cfg: float
|
||||||
|
) -> Tuple[str, str, int, float]:
|
||||||
|
"""
|
||||||
|
Get compact sampler combo configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler: The sampler algorithm name
|
||||||
|
sched: The scheduler algorithm name
|
||||||
|
steps: Number of sampling steps
|
||||||
|
cfg: CFG scale value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (sampler, scheduler, steps, cfg)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Use the same validation logic but with compact interface
|
||||||
|
result = get_sampler_combo(sampler, sched, steps, cfg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Graceful fallback
|
||||||
|
self.handle_error(f"Error in compact combo: {str(e)}")
|
||||||
|
return ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""String representation of the compact node."""
|
||||||
|
return "SamplerComboCompactNode"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Detailed string representation of the compact node."""
|
||||||
|
return f"SamplerComboCompactNode(category='{self.CATEGORY}')"
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
"""Logic module for Sampler Combo node."""
|
||||||
|
|
||||||
|
from typing import Tuple, Dict, Any, List
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Import ComfyUI samplers - will be available when running in ComfyUI
|
||||||
|
try:
|
||||||
|
import comfy.samplers
|
||||||
|
|
||||||
|
SAMPLERS = comfy.samplers.KSampler.SAMPLERS
|
||||||
|
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS
|
||||||
|
except ImportError:
|
||||||
|
# Fallback for testing/development environment
|
||||||
|
SAMPLERS = [
|
||||||
|
"euler",
|
||||||
|
"euler_ancestral",
|
||||||
|
"heun",
|
||||||
|
"dpm_2",
|
||||||
|
"dpm_2_ancestral",
|
||||||
|
"lms",
|
||||||
|
"dpm_fast",
|
||||||
|
"dpm_adaptive",
|
||||||
|
"dpmpp_2s_ancestral",
|
||||||
|
"dpmpp_sde",
|
||||||
|
"dpmpp_2m",
|
||||||
|
"ddim",
|
||||||
|
"uni_pc",
|
||||||
|
"uni_pc_bh2",
|
||||||
|
]
|
||||||
|
SCHEDULERS = [
|
||||||
|
"normal",
|
||||||
|
"karras",
|
||||||
|
"exponential",
|
||||||
|
"sgm_uniform",
|
||||||
|
"simple",
|
||||||
|
"ddim_uniform",
|
||||||
|
"beta",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sampler_settings(
|
||||||
|
sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Validate sampler configuration settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
scheduler: The scheduler algorithm name
|
||||||
|
steps: Number of sampling steps
|
||||||
|
cfg: CFG (classifier-free guidance) scale value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all settings are valid
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate sampler
|
||||||
|
if sampler_name not in SAMPLERS:
|
||||||
|
logger.error(f"Invalid sampler: {sampler_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate scheduler
|
||||||
|
if scheduler not in SCHEDULERS:
|
||||||
|
logger.error(f"Invalid scheduler: {scheduler}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate steps
|
||||||
|
if not isinstance(steps, int) or steps < 1 or steps > 1000:
|
||||||
|
logger.error(f"Invalid steps: {steps} (must be 1-1000)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate CFG
|
||||||
|
if not isinstance(cfg, (int, float)) or cfg < 0 or cfg > 30:
|
||||||
|
logger.error(f"Invalid CFG: {cfg} (must be 0-30)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error validating sampler settings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_sampler_combo(
|
||||||
|
sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||||
|
) -> Tuple[str, str, int, float]:
|
||||||
|
"""
|
||||||
|
Process and return sampler combo settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
scheduler: The scheduler algorithm name
|
||||||
|
steps: Number of sampling steps
|
||||||
|
cfg: CFG scale value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (sampler_name, scheduler, steps, cfg)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate inputs
|
||||||
|
if not validate_sampler_settings(sampler_name, scheduler, steps, cfg):
|
||||||
|
# Return safe defaults if validation fails
|
||||||
|
logger.warning("Invalid settings provided, using safe defaults")
|
||||||
|
return ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
# Sanitize values
|
||||||
|
steps = max(1, min(1000, int(steps)))
|
||||||
|
cfg = max(0.0, min(30.0, float(cfg)))
|
||||||
|
|
||||||
|
return (sampler_name, scheduler, steps, cfg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing sampler combo: {e}")
|
||||||
|
# Return safe defaults on any error
|
||||||
|
return ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_compatible_scheduler_suggestions(sampler_name: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get scheduler suggestions that work well with specific samplers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of recommended scheduler names
|
||||||
|
"""
|
||||||
|
# Scheduler compatibility recommendations
|
||||||
|
compatibility_map = {
|
||||||
|
"euler": ["normal", "simple", "sgm_uniform"],
|
||||||
|
"euler_ancestral": ["normal", "karras", "exponential"],
|
||||||
|
"heun": ["normal", "karras"],
|
||||||
|
"dpm_2": ["normal", "karras"],
|
||||||
|
"dpm_2_ancestral": ["normal", "karras", "exponential"],
|
||||||
|
"dpmpp_2s_ancestral": ["normal", "karras", "exponential"],
|
||||||
|
"dpmpp_sde": ["normal", "karras", "exponential"],
|
||||||
|
"dpmpp_2m": ["normal", "karras", "sgm_uniform"],
|
||||||
|
"ddim": ["ddim_uniform", "normal"],
|
||||||
|
"uni_pc": ["normal", "sgm_uniform"],
|
||||||
|
"uni_pc_bh2": ["normal", "sgm_uniform"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return compatibility_map.get(sampler_name, ["normal", "karras"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_recommended_steps_range(sampler_name: str) -> Tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Get recommended steps range for specific samplers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (min_steps, max_steps, default_steps)
|
||||||
|
"""
|
||||||
|
# Steps recommendations by sampler
|
||||||
|
steps_map = {
|
||||||
|
"euler": (10, 30, 20),
|
||||||
|
"euler_ancestral": (15, 40, 25),
|
||||||
|
"heun": (10, 25, 15),
|
||||||
|
"dpm_2": (10, 30, 22),
|
||||||
|
"dpm_2_ancestral": (15, 35, 25),
|
||||||
|
"dpmpp_2s_ancestral": (15, 40, 28),
|
||||||
|
"dpmpp_sde": (15, 35, 25),
|
||||||
|
"dpmpp_2m": (15, 30, 20),
|
||||||
|
"ddim": (20, 50, 30),
|
||||||
|
"uni_pc": (10, 25, 15),
|
||||||
|
"uni_pc_bh2": (10, 25, 15),
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps_map.get(sampler_name, (10, 50, 20))
|
||||||
|
|
||||||
|
|
||||||
|
def get_recommended_cfg_range(sampler_name: str) -> Tuple[float, float, float]:
|
||||||
|
"""
|
||||||
|
Get recommended CFG range for specific samplers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (min_cfg, max_cfg, default_cfg)
|
||||||
|
"""
|
||||||
|
# CFG recommendations by sampler
|
||||||
|
cfg_map = {
|
||||||
|
"euler": (3.0, 15.0, 7.0),
|
||||||
|
"euler_ancestral": (5.0, 20.0, 8.0),
|
||||||
|
"heun": (3.0, 12.0, 6.0),
|
||||||
|
"dpm_2": (4.0, 15.0, 7.5),
|
||||||
|
"dpm_2_ancestral": (5.0, 18.0, 8.5),
|
||||||
|
"dpmpp_2s_ancestral": (6.0, 20.0, 9.0),
|
||||||
|
"dpmpp_sde": (5.0, 18.0, 8.0),
|
||||||
|
"dpmpp_2m": (4.0, 15.0, 7.0),
|
||||||
|
"ddim": (3.0, 12.0, 6.0),
|
||||||
|
"uni_pc": (3.0, 12.0, 6.5),
|
||||||
|
"uni_pc_bh2": (3.0, 12.0, 6.5),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg_map.get(sampler_name, (1.0, 20.0, 7.0))
|
||||||
|
|
||||||
|
|
||||||
|
def get_sampler_info() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get information about available samplers and schedulers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing sampler/scheduler information
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"samplers": SAMPLERS,
|
||||||
|
"schedulers": SCHEDULERS,
|
||||||
|
"sampler_count": len(SAMPLERS),
|
||||||
|
"scheduler_count": len(SCHEDULERS),
|
||||||
|
"default_sampler": "euler",
|
||||||
|
"default_scheduler": "normal",
|
||||||
|
"default_steps": 20,
|
||||||
|
"default_cfg": 7.0,
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
"""Sampler Combo node for ComfyUI."""
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
from ...base.base_node import ComfyAssetsBaseNode
|
||||||
|
from .logic import (
|
||||||
|
get_sampler_combo,
|
||||||
|
validate_sampler_settings,
|
||||||
|
get_compatible_scheduler_suggestions,
|
||||||
|
get_recommended_steps_range,
|
||||||
|
get_recommended_cfg_range,
|
||||||
|
SAMPLERS,
|
||||||
|
SCHEDULERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SamplerComboNode(ComfyAssetsBaseNode):
|
||||||
|
"""
|
||||||
|
Sampler Combo node for selecting sampling configuration.
|
||||||
|
|
||||||
|
Provides a unified interface for selecting sampler, scheduler, steps,
|
||||||
|
and CFG settings in a single node, reducing workflow complexity and
|
||||||
|
ensuring compatible parameter combinations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
"""Define the input types for the ComfyUI node."""
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"sampler_name": (
|
||||||
|
SAMPLERS,
|
||||||
|
{
|
||||||
|
"default": "euler",
|
||||||
|
"tooltip": "Sampling algorithm",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"scheduler": (
|
||||||
|
SCHEDULERS,
|
||||||
|
{
|
||||||
|
"default": "normal",
|
||||||
|
"tooltip": "Step distribution schedule",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"steps": (
|
||||||
|
"INT",
|
||||||
|
{
|
||||||
|
"default": 20,
|
||||||
|
"min": 1,
|
||||||
|
"max": 100,
|
||||||
|
"step": 1,
|
||||||
|
"tooltip": "Sampling steps (1-100)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"cfg": (
|
||||||
|
"FLOAT",
|
||||||
|
{
|
||||||
|
"default": 7.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 20.0,
|
||||||
|
"step": 0.5,
|
||||||
|
"display": "slider",
|
||||||
|
"tooltip": "CFG scale (0-20)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (SAMPLERS, SCHEDULERS, "INT", "FLOAT")
|
||||||
|
RETURN_NAMES = ("sampler_name", "scheduler", "steps", "cfg")
|
||||||
|
FUNCTION = "get_sampler_combo"
|
||||||
|
CATEGORY = "ComfyAssets"
|
||||||
|
|
||||||
|
def get_sampler_combo(
|
||||||
|
self, sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||||
|
) -> Tuple[str, str, int, float]:
|
||||||
|
"""
|
||||||
|
Get sampler combo configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
scheduler: The scheduler algorithm name
|
||||||
|
steps: Number of sampling steps
|
||||||
|
cfg: CFG scale value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (sampler_name, scheduler, steps, cfg)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate inputs
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.log_info(
|
||||||
|
f"Configured sampler combo: {result[0]}, {result[1]}, "
|
||||||
|
f"{result[2]} steps, CFG {result[3]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Handle any unexpected errors gracefully
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(
|
||||||
|
f"{self.__class__.__name__}: Error processing sampler combo: {str(e)}. "
|
||||||
|
f"Using safe defaults: euler, normal, 20 steps, CFG 7.0"
|
||||||
|
)
|
||||||
|
return ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
def validate_inputs(
|
||||||
|
self, sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Validate sampler combo inputs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
scheduler: The scheduler algorithm name
|
||||||
|
steps: Number of sampling steps
|
||||||
|
cfg: CFG scale value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If validation fails
|
||||||
|
"""
|
||||||
|
if not validate_sampler_settings(sampler_name, scheduler, steps, cfg):
|
||||||
|
self.handle_error(
|
||||||
|
f"Invalid sampler settings: sampler={sampler_name}, "
|
||||||
|
f"scheduler={scheduler}, steps={steps}, cfg={cfg}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_scheduler_suggestions(self, sampler_name: str) -> list:
|
||||||
|
"""
|
||||||
|
Get scheduler suggestions compatible with the selected sampler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of recommended scheduler names
|
||||||
|
"""
|
||||||
|
return get_compatible_scheduler_suggestions(sampler_name)
|
||||||
|
|
||||||
|
def get_steps_recommendation(self, sampler_name: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get steps recommendation for the selected sampler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with min, max, and default steps
|
||||||
|
"""
|
||||||
|
min_steps, max_steps, default_steps = get_recommended_steps_range(sampler_name)
|
||||||
|
return {
|
||||||
|
"min": min_steps,
|
||||||
|
"max": max_steps,
|
||||||
|
"default": default_steps,
|
||||||
|
"recommendation": f"Range: {min_steps}-{max_steps} steps",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_cfg_recommendation(self, sampler_name: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get CFG recommendation for the selected sampler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with min, max, and default CFG values
|
||||||
|
"""
|
||||||
|
min_cfg, max_cfg, default_cfg = get_recommended_cfg_range(sampler_name)
|
||||||
|
return {
|
||||||
|
"min": min_cfg,
|
||||||
|
"max": max_cfg,
|
||||||
|
"default": default_cfg,
|
||||||
|
"recommendation": f"Recommended range: {min_cfg}-{max_cfg} CFG",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_combo_analysis(
|
||||||
|
self, sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Analyze the sampler combo configuration and provide recommendations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sampler_name: The sampler algorithm name
|
||||||
|
scheduler: The scheduler algorithm name
|
||||||
|
steps: Number of sampling steps
|
||||||
|
cfg: CFG scale value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing analysis and recommendations
|
||||||
|
"""
|
||||||
|
analysis = {
|
||||||
|
"sampler": sampler_name,
|
||||||
|
"scheduler": scheduler,
|
||||||
|
"steps": steps,
|
||||||
|
"cfg": cfg,
|
||||||
|
"valid": validate_sampler_settings(sampler_name, scheduler, steps, cfg),
|
||||||
|
"scheduler_suggestions": self.get_scheduler_suggestions(sampler_name),
|
||||||
|
"steps_rec": self.get_steps_recommendation(sampler_name),
|
||||||
|
"cfg_rec": self.get_cfg_recommendation(sampler_name),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add compatibility assessment
|
||||||
|
suggested_schedulers = self.get_scheduler_suggestions(sampler_name)
|
||||||
|
analysis["scheduler_compatible"] = scheduler in suggested_schedulers
|
||||||
|
|
||||||
|
# Add performance assessment
|
||||||
|
steps_rec = self.get_steps_recommendation(sampler_name)
|
||||||
|
analysis["steps_optimal"] = steps_rec["min"] <= steps <= steps_rec["max"]
|
||||||
|
|
||||||
|
cfg_rec = self.get_cfg_recommendation(sampler_name)
|
||||||
|
analysis["cfg_optimal"] = cfg_rec["min"] <= cfg <= cfg_rec["max"]
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_available_samplers(cls) -> list:
|
||||||
|
"""
|
||||||
|
Get list of available samplers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of sampler names
|
||||||
|
"""
|
||||||
|
return list(SAMPLERS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_available_schedulers(cls) -> list:
|
||||||
|
"""
|
||||||
|
Get list of available schedulers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of scheduler names
|
||||||
|
"""
|
||||||
|
return list(SCHEDULERS)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""String representation of the node."""
|
||||||
|
return (
|
||||||
|
f"SamplerComboNode(samplers={len(SAMPLERS)}, "
|
||||||
|
f"schedulers={len(SCHEDULERS)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Detailed string representation of the node."""
|
||||||
|
return (
|
||||||
|
f"SamplerComboNode("
|
||||||
|
f"samplers={len(SAMPLERS)}, "
|
||||||
|
f"schedulers={len(SCHEDULERS)}, "
|
||||||
|
f"category='{self.CATEGORY}', "
|
||||||
|
f"function='{self.FUNCTION}'"
|
||||||
|
f")"
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Seed History tool for ComfyUI-KikoTools.
|
||||||
|
|
||||||
|
Provides seed value tracking with history management,
|
||||||
|
automatic deduplication, and interactive UI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .node import SeedHistoryNode
|
||||||
|
|
||||||
|
__all__ = ["SeedHistoryNode"]
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
"""Core logic for Seed History tool."""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from typing import List, Dict, Any, Tuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_seed() -> int:
|
||||||
|
"""
|
||||||
|
Generate a cryptographically strong random seed value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Random integer in the valid ComfyUI seed range
|
||||||
|
"""
|
||||||
|
return random.randint(0, 0xFFFFFFFFFFFFFFFF)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_seed_value(seed: Any) -> bool:
|
||||||
|
"""
|
||||||
|
Validate that a seed value is within acceptable range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Seed value to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if seed is valid, False otherwise
|
||||||
|
"""
|
||||||
|
if seed is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
seed_int = int(seed)
|
||||||
|
return 0 <= seed_int <= 0xFFFFFFFFFFFFFFFF
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_seed_value(seed: Any) -> int:
|
||||||
|
"""
|
||||||
|
Sanitize and convert seed value to valid integer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Raw seed value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Valid seed integer
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If seed cannot be converted to valid range
|
||||||
|
"""
|
||||||
|
if seed is None:
|
||||||
|
raise ValueError("Seed cannot be None")
|
||||||
|
|
||||||
|
try:
|
||||||
|
seed_int = int(seed)
|
||||||
|
|
||||||
|
# Clamp to valid range
|
||||||
|
if seed_int < 0:
|
||||||
|
seed_int = 0
|
||||||
|
elif seed_int > 0xFFFFFFFFFFFFFFFF:
|
||||||
|
seed_int = 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
return seed_int
|
||||||
|
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
raise ValueError(f"Invalid seed value: {seed}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def create_history_entry(
|
||||||
|
seed: int, timestamp: Optional[float] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create a standardized history entry for a seed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Seed value
|
||||||
|
timestamp: Optional timestamp (uses current time if None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing seed history entry
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = time.time()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"seed": seed,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"dateString": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_duplicate_seeds(
|
||||||
|
history: List[Dict[str, Any]], new_seed: int, dedup_window_ms: int = 500
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a seed should be filtered as a duplicate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history: Current seed history
|
||||||
|
new_seed: New seed to check
|
||||||
|
dedup_window_ms: Deduplication window in milliseconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if seed should be filtered (is duplicate), False otherwise
|
||||||
|
"""
|
||||||
|
if not history:
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_time = time.time() * 1000 # Convert to milliseconds
|
||||||
|
|
||||||
|
# Check most recent entry for duplicates within window
|
||||||
|
latest_entry = history[0]
|
||||||
|
latest_timestamp_ms = latest_entry["timestamp"] * 1000
|
||||||
|
|
||||||
|
time_diff = current_time - latest_timestamp_ms
|
||||||
|
is_same_seed = latest_entry["seed"] == new_seed
|
||||||
|
is_within_window = time_diff < dedup_window_ms
|
||||||
|
|
||||||
|
return is_same_seed and is_within_window
|
||||||
|
|
||||||
|
|
||||||
|
def add_seed_to_history(
|
||||||
|
history: List[Dict[str, Any]],
|
||||||
|
seed: int,
|
||||||
|
max_history: int = 10,
|
||||||
|
dedup_window_ms: int = 500,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], bool]:
|
||||||
|
"""
|
||||||
|
Add a seed to the history with deduplication and size management.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history: Current seed history
|
||||||
|
seed: Seed to add
|
||||||
|
max_history: Maximum number of entries to keep
|
||||||
|
dedup_window_ms: Deduplication window in milliseconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (updated_history, was_added)
|
||||||
|
"""
|
||||||
|
# Validate seed
|
||||||
|
if not validate_seed_value(seed):
|
||||||
|
return history, False
|
||||||
|
|
||||||
|
# Sanitize seed
|
||||||
|
try:
|
||||||
|
clean_seed = sanitize_seed_value(seed)
|
||||||
|
except ValueError:
|
||||||
|
return history, False
|
||||||
|
|
||||||
|
# Check for duplicates
|
||||||
|
if filter_duplicate_seeds(history, clean_seed, dedup_window_ms):
|
||||||
|
return history, False
|
||||||
|
|
||||||
|
# Create new history list (don't modify original)
|
||||||
|
new_history = [entry for entry in history if entry["seed"] != clean_seed]
|
||||||
|
|
||||||
|
# Add new entry at the beginning
|
||||||
|
new_entry = create_history_entry(clean_seed)
|
||||||
|
new_history.insert(0, new_entry)
|
||||||
|
|
||||||
|
# Trim to max size
|
||||||
|
if len(new_history) > max_history:
|
||||||
|
new_history = new_history[:max_history]
|
||||||
|
|
||||||
|
return new_history, True
|
||||||
|
|
||||||
|
|
||||||
|
def format_time_ago(timestamp: float) -> str:
|
||||||
|
"""
|
||||||
|
Format a timestamp as a human-readable time ago string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timestamp: Unix timestamp
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted time ago string
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
diff = now - timestamp
|
||||||
|
|
||||||
|
days = int(diff // 86400)
|
||||||
|
hours = int((diff % 86400) // 3600)
|
||||||
|
minutes = int((diff % 3600) // 60)
|
||||||
|
seconds = int(diff % 60)
|
||||||
|
|
||||||
|
if days > 0:
|
||||||
|
return f"{days}d ago"
|
||||||
|
elif hours > 0:
|
||||||
|
return f"{hours}h ago"
|
||||||
|
elif minutes > 0:
|
||||||
|
return f"{minutes}m ago"
|
||||||
|
else:
|
||||||
|
return f"{seconds}s ago"
|
||||||
|
|
||||||
|
|
||||||
|
def search_history_by_seed(
|
||||||
|
history: List[Dict[str, Any]], seed: int
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Search history for a specific seed value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history: Seed history to search
|
||||||
|
seed: Seed value to find
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
History entry if found, None otherwise
|
||||||
|
"""
|
||||||
|
for entry in history:
|
||||||
|
if entry["seed"] == seed:
|
||||||
|
return entry
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_history_statistics(history: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Calculate statistics about the seed history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history: Seed history
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing history statistics
|
||||||
|
"""
|
||||||
|
if not history:
|
||||||
|
return {
|
||||||
|
"total_seeds": 0,
|
||||||
|
"oldest_timestamp": None,
|
||||||
|
"newest_timestamp": None,
|
||||||
|
"time_span_hours": 0,
|
||||||
|
"unique_seeds": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamps = [entry["timestamp"] for entry in history]
|
||||||
|
oldest = min(timestamps)
|
||||||
|
newest = max(timestamps)
|
||||||
|
time_span = (newest - oldest) / 3600 # Convert to hours
|
||||||
|
|
||||||
|
unique_seeds = len(set(entry["seed"] for entry in history))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_seeds": len(history),
|
||||||
|
"oldest_timestamp": oldest,
|
||||||
|
"newest_timestamp": newest,
|
||||||
|
"time_span_hours": time_span,
|
||||||
|
"unique_seeds": unique_seeds,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def export_history_to_text(history: List[Dict[str, Any]]) -> str:
|
||||||
|
"""
|
||||||
|
Export seed history to a formatted text string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history: Seed history to export
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted text representation
|
||||||
|
"""
|
||||||
|
if not history:
|
||||||
|
return "# Seed History (Empty)\n\nNo seeds tracked yet."
|
||||||
|
|
||||||
|
lines = ["# ComfyUI Seed History", ""]
|
||||||
|
lines.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
lines.append(f"Total seeds: {len(history)}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for i, entry in enumerate(history, 1):
|
||||||
|
time_ago = format_time_ago(entry["timestamp"])
|
||||||
|
lines.append(f"{i:2d}. {entry['seed']} ({time_ago})")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def import_seeds_from_list(seed_list: List[int]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Import a list of seeds as history entries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed_list: List of seed integers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of history entries
|
||||||
|
"""
|
||||||
|
history = []
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
for i, seed in enumerate(seed_list):
|
||||||
|
if validate_seed_value(seed):
|
||||||
|
# Spread timestamps by 1 minute intervals (newest first)
|
||||||
|
timestamp = current_time - (i * 60)
|
||||||
|
entry = create_history_entry(seed, timestamp)
|
||||||
|
history.append(entry)
|
||||||
|
|
||||||
|
return history
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
"""Seed History node for ComfyUI."""
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
from ...base.base_node import ComfyAssetsBaseNode
|
||||||
|
from .logic import (
|
||||||
|
generate_random_seed,
|
||||||
|
validate_seed_value,
|
||||||
|
sanitize_seed_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||||
|
"""
|
||||||
|
Seed History node for tracking and managing seed values.
|
||||||
|
|
||||||
|
Provides seed value output with integrated history tracking,
|
||||||
|
deduplication, and interactive UI for seed management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
"""Define the input types for the ComfyUI node."""
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"seed": (
|
||||||
|
"INT",
|
||||||
|
{
|
||||||
|
"default": 12345,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0xFFFFFFFFFFFFFFFF,
|
||||||
|
"tooltip": "Seed value for generation processes. "
|
||||||
|
"History UI tracks all changes automatically.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("INT",)
|
||||||
|
RETURN_NAMES = ("seed",)
|
||||||
|
FUNCTION = "output_seed"
|
||||||
|
CATEGORY = "ComfyAssets"
|
||||||
|
|
||||||
|
def output_seed(self, seed: int) -> Tuple[int]:
|
||||||
|
"""
|
||||||
|
Output the seed value for use in other nodes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Input seed value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing the seed value
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate and sanitize the seed
|
||||||
|
if not validate_seed_value(seed):
|
||||||
|
# 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,)
|
||||||
|
|
||||||
|
clean_seed = sanitize_seed_value(seed)
|
||||||
|
|
||||||
|
return (clean_seed,)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Handle any unexpected errors gracefully
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Generate a new random seed value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New random seed integer
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
new_seed = generate_random_seed()
|
||||||
|
self.log_info(f"Generated new seed: {new_seed}")
|
||||||
|
return new_seed
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error generating seed: {str(e)}. Using fallback."
|
||||||
|
self.handle_error(error_msg)
|
||||||
|
return 12345
|
||||||
|
|
||||||
|
def validate_seed_input(self, seed: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate seed input value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Seed value to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if seed is valid
|
||||||
|
"""
|
||||||
|
return validate_seed_value(seed)
|
||||||
|
|
||||||
|
def get_seed_info(self, seed: int) -> str:
|
||||||
|
"""
|
||||||
|
Get descriptive information about a seed value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Seed value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Information string about the seed
|
||||||
|
"""
|
||||||
|
if not validate_seed_value(seed):
|
||||||
|
return f"Invalid seed: {seed} (outside valid range)"
|
||||||
|
|
||||||
|
# Convert to hex for additional info
|
||||||
|
hex_value = hex(seed)
|
||||||
|
|
||||||
|
# Check if it's a "nice" number (power of 2, round number, etc.)
|
||||||
|
seed_type = "standard"
|
||||||
|
if seed == 0:
|
||||||
|
seed_type = "zero"
|
||||||
|
elif seed & (seed - 1) == 0: # Power of 2
|
||||||
|
seed_type = "power of 2"
|
||||||
|
elif str(seed).count("0") > len(str(seed)) // 2:
|
||||||
|
seed_type = "round number"
|
||||||
|
elif seed == 12345:
|
||||||
|
seed_type = "default"
|
||||||
|
|
||||||
|
return f"Seed {seed} ({hex_value}) - {seed_type}"
|
||||||
|
|
||||||
|
def get_seed_range_info(self) -> str:
|
||||||
|
"""
|
||||||
|
Get information about the valid seed range.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Range information string
|
||||||
|
"""
|
||||||
|
max_seed = 0xFFFFFFFFFFFFFFFF
|
||||||
|
return f"Valid range: 0 to {max_seed:,} ({hex(max_seed)})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_seed(cls) -> int:
|
||||||
|
"""
|
||||||
|
Get the default seed value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Default seed integer
|
||||||
|
"""
|
||||||
|
return 12345
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_seed_in_range(cls, seed: int) -> bool:
|
||||||
|
"""
|
||||||
|
Check if seed is within valid ComfyUI range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed: Seed value to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if seed is in valid range
|
||||||
|
"""
|
||||||
|
return 0 <= seed <= 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""String representation of the node."""
|
||||||
|
return "SeedHistoryNode(with_ui_tracking)"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Detailed string representation of the node."""
|
||||||
|
return (
|
||||||
|
f"SeedHistoryNode("
|
||||||
|
f"category='{self.CATEGORY}', "
|
||||||
|
f"function='{self.FUNCTION}', "
|
||||||
|
f"max_seed={hex(0xFFFFFFFFFFFFFFFF)}"
|
||||||
|
f")"
|
||||||
|
)
|
||||||
@@ -4,14 +4,15 @@ from typing import Tuple
|
|||||||
from ...base.base_node import ComfyAssetsBaseNode
|
from ...base.base_node import ComfyAssetsBaseNode
|
||||||
from .logic import (
|
from .logic import (
|
||||||
get_preset_dimensions,
|
get_preset_dimensions,
|
||||||
calculate_aspect_ratio,
|
|
||||||
validate_dimensions,
|
validate_dimensions,
|
||||||
sanitize_dimensions,
|
sanitize_dimensions,
|
||||||
)
|
)
|
||||||
from .presets import (
|
from .presets import (
|
||||||
PRESET_OPTIONS,
|
PRESET_OPTIONS,
|
||||||
PRESET_DESCRIPTIONS,
|
PRESET_METADATA,
|
||||||
get_model_recommendation,
|
get_model_recommendation,
|
||||||
|
get_preset_metadata,
|
||||||
|
get_presets_by_model_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,18 +27,32 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
"""Define the input types for the ComfyUI node."""
|
"""Define the input types for the ComfyUI node."""
|
||||||
# Get all preset options excluding the custom tuple
|
# Create formatted preset options with metadata
|
||||||
preset_keys = [key for key in PRESET_OPTIONS.keys()]
|
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 {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"preset": (
|
"preset": (
|
||||||
preset_keys,
|
preset_options,
|
||||||
{
|
{
|
||||||
"default": "custom",
|
"default": "custom",
|
||||||
"tooltip": "Select from optimized resolution presets or use custom dimensions. "
|
"tooltip": "Select from optimized resolution presets or use "
|
||||||
"SDXL presets are ~1MP, FLUX presets are higher resolution, "
|
"custom dimensions. SDXL presets are ~1MP, FLUX presets are "
|
||||||
"Ultra-wide presets support modern aspect ratios.",
|
"higher resolution, Ultra-wide presets support modern "
|
||||||
|
"aspect ratios.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"width": (
|
"width": (
|
||||||
@@ -48,7 +63,8 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
"max": 8192,
|
"max": 8192,
|
||||||
"step": 8,
|
"step": 8,
|
||||||
"tooltip": "Custom width in pixels (must be multiple of 8). "
|
"tooltip": "Custom width in pixels (must be multiple of 8). "
|
||||||
"Used when preset is 'custom' or as fallback for invalid presets.",
|
"Used when preset is 'custom' or as fallback for invalid "
|
||||||
|
"presets.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"height": (
|
"height": (
|
||||||
@@ -59,7 +75,8 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
"max": 8192,
|
"max": 8192,
|
||||||
"step": 8,
|
"step": 8,
|
||||||
"tooltip": "Custom height in pixels (must be multiple of 8). "
|
"tooltip": "Custom height in pixels (must be multiple of 8). "
|
||||||
"Used when preset is 'custom' or as fallback for invalid presets.",
|
"Used when preset is 'custom' or as fallback for invalid "
|
||||||
|
"presets.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -75,7 +92,7 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
Get width and height dimensions with preset and swap support.
|
Get width and height dimensions with preset and swap support.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
preset: Selected preset name or "custom"
|
preset: Selected preset name or formatted preset string
|
||||||
width: Custom width value
|
width: Custom width value
|
||||||
height: Custom height value
|
height: Custom height value
|
||||||
|
|
||||||
@@ -83,8 +100,13 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
Tuple of (width, height)
|
Tuple of (width, height)
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
# 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
|
# Sanitize dimensions to ensure they meet ComfyUI requirements
|
||||||
final_width, final_height = sanitize_dimensions(final_width, final_height)
|
final_width, final_height = sanitize_dimensions(final_width, final_height)
|
||||||
@@ -108,6 +130,37 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
self.handle_error(error_msg)
|
self.handle_error(error_msg)
|
||||||
return (1024, 1024)
|
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:
|
def get_preset_info(self, preset: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get descriptive information about a preset.
|
Get descriptive information about a preset.
|
||||||
@@ -121,14 +174,12 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
if preset == "custom":
|
if preset == "custom":
|
||||||
return "Custom dimensions - use the width and height inputs below"
|
return "Custom dimensions - use the width and height inputs below"
|
||||||
|
|
||||||
if preset in PRESET_DESCRIPTIONS:
|
metadata = get_preset_metadata(preset)
|
||||||
return PRESET_DESCRIPTIONS[preset]
|
if metadata.width > 0: # Valid metadata
|
||||||
|
return (
|
||||||
# Fallback for unknown presets
|
f"{preset} - {metadata.aspect_ratio} ({metadata.megapixels:.1f}MP) - "
|
||||||
if preset in PRESET_OPTIONS:
|
f"{metadata.description}"
|
||||||
width, height = PRESET_OPTIONS[preset]
|
)
|
||||||
aspect_ratio = calculate_aspect_ratio(width, height)
|
|
||||||
return f"{preset} - {aspect_ratio} aspect ratio"
|
|
||||||
|
|
||||||
return f"Unknown preset: {preset}"
|
return f"Unknown preset: {preset}"
|
||||||
|
|
||||||
@@ -149,19 +200,22 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
Validate node inputs.
|
Validate node inputs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
preset: Preset name
|
preset: Preset name or formatted preset string
|
||||||
width: Width value
|
width: Width value
|
||||||
height: Height value
|
height: Height value
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if inputs are valid
|
True if inputs are valid
|
||||||
"""
|
"""
|
||||||
|
# Extract original preset name
|
||||||
|
original_preset = self._extract_preset_name(preset)
|
||||||
|
|
||||||
# Check if preset exists or is custom
|
# 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
|
return False
|
||||||
|
|
||||||
# For custom preset, validate dimensions
|
# For custom preset, validate dimensions
|
||||||
if preset == "custom":
|
if original_preset == "custom":
|
||||||
if not validate_dimensions(width, height):
|
if not validate_dimensions(width, height):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -192,6 +246,52 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
|||||||
return PRESET_OPTIONS[preset]
|
return PRESET_OPTIONS[preset]
|
||||||
return (0, 0)
|
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:
|
def __str__(self) -> str:
|
||||||
"""String representation of the node."""
|
"""String representation of the node."""
|
||||||
return f"WidthHeightSelectorNode(presets={len(PRESET_OPTIONS)})"
|
return f"WidthHeightSelectorNode(presets={len(PRESET_OPTIONS)})"
|
||||||
|
|||||||
@@ -1,114 +1,435 @@
|
|||||||
"""Preset definitions for Width Height Selector."""
|
"""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]] = {
|
SDXL_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||||
# Square
|
k: (v.width, v.height)
|
||||||
"1024×1024": (1024, 1024), # 1:1 - Base SDXL resolution
|
for k, v in PRESET_METADATA.items()
|
||||||
# Portrait ratios
|
if v.model_group == "SDXL"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# FLUX optimized presets (higher resolution, flexible ratios)
|
|
||||||
FLUX_PRESETS: Dict[str, Tuple[int, int]] = {
|
FLUX_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||||
# Recommended high-quality resolutions
|
k: (v.width, v.height)
|
||||||
"1920×1080": (1920, 1080), # 16:9 - Full HD landscape
|
for k, v in PRESET_METADATA.items()
|
||||||
"1536×1536": (1536, 1536), # 1:1 - High-res square
|
if v.model_group == "FLUX"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ultra-wide and modern aspect ratios
|
|
||||||
ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = {
|
ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||||
# Ultra-wide landscape (21:9 and variants)
|
k: (v.width, v.height)
|
||||||
"2560×1080": (2560, 1080), # 64:27 - Ultra-wide gaming
|
for k, v in PRESET_METADATA.items()
|
||||||
"2048×768": (2048, 768), # 8:3 - Wide cinematic
|
if v.model_group == "Ultra-Wide"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Combined preset options for ComfyUI dropdown
|
# Combined preset options for ComfyUI dropdown
|
||||||
PRESET_OPTIONS: Dict[str, Tuple[int, int]] = {
|
PRESET_OPTIONS: Dict[str, Tuple[int, int]] = {
|
||||||
"custom": (0, 0), # Special case for custom dimensions
|
"custom": (0, 0), # Special case for custom dimensions
|
||||||
**SDXL_PRESETS,
|
**{k: (v.width, v.height) for k, v in PRESET_METADATA.items()},
|
||||||
**FLUX_PRESETS,
|
|
||||||
**ULTRA_WIDE_PRESETS,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Organized preset categories for better UX
|
# Enhanced preset categories organized by model groups and aspect ratios
|
||||||
PRESET_CATEGORIES = {
|
PRESET_CATEGORIES = {
|
||||||
"Custom": ["custom"],
|
"Custom": ["custom"],
|
||||||
"SDXL Square": ["1024×1024"],
|
# SDXL Categories
|
||||||
"SDXL Portrait": ["896×1152", "832×1216", "768×1344", "640×1536"],
|
"SDXL Square": [
|
||||||
"SDXL Landscape": ["1152×896", "1216×832", "1344×768", "1536×640"],
|
k
|
||||||
"FLUX Recommended": ["1920×1080", "1536×1536", "1280×768", "768×1280"],
|
for k, v in PRESET_METADATA.items()
|
||||||
"FLUX Alternative": ["1440×1080", "1080×1440", "1728×1152", "1152×1728"],
|
if v.model_group == "SDXL" and v.category == "Square"
|
||||||
"Ultra-Wide Landscape": ["2560×1080", "2048×768", "1792×768", "2304×768"],
|
],
|
||||||
"Ultra-Wide Portrait": ["1080×2560", "768×2048", "768×1792", "768×2304"],
|
"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
|
# Legacy compatibility - preset descriptions
|
||||||
PRESET_DESCRIPTIONS = {
|
PRESET_DESCRIPTIONS = {k: v.description for k, v in PRESET_METADATA.items()}
|
||||||
# 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",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Model-specific recommendations
|
# Model-specific recommendations with metadata
|
||||||
MODEL_RECOMMENDATIONS = {
|
MODEL_RECOMMENDATIONS = {
|
||||||
"SDXL": list(SDXL_PRESETS.keys()),
|
"SDXL": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"],
|
||||||
"FLUX": list(FLUX_PRESETS.keys()),
|
"FLUX": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"],
|
||||||
"Ultra-Wide": list(ULTRA_WIDE_PRESETS.keys()),
|
"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:
|
def get_preset_category(preset_name: str) -> str:
|
||||||
"""Get the category for a given preset name."""
|
"""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():
|
for category, presets in PRESET_CATEGORIES.items():
|
||||||
if preset_name in presets:
|
if preset_name in presets:
|
||||||
return category
|
return category
|
||||||
@@ -117,21 +438,17 @@ def get_preset_category(preset_name: str) -> str:
|
|||||||
|
|
||||||
def get_model_recommendation(preset_name: str) -> str:
|
def get_model_recommendation(preset_name: str) -> str:
|
||||||
"""Get model recommendation for a given preset."""
|
"""Get model recommendation for a given preset."""
|
||||||
if preset_name in SDXL_PRESETS:
|
metadata = PRESET_METADATA.get(preset_name)
|
||||||
return "Optimized for SDXL"
|
if metadata:
|
||||||
elif preset_name in FLUX_PRESETS:
|
return f"Optimized for {metadata.model_group}"
|
||||||
return "Optimized for FLUX"
|
return "Custom dimensions"
|
||||||
elif preset_name in ULTRA_WIDE_PRESETS:
|
|
||||||
return "Modern ultra-wide ratios"
|
|
||||||
else:
|
|
||||||
return "Custom dimensions"
|
|
||||||
|
|
||||||
|
|
||||||
def validate_preset_dimensions() -> bool:
|
def validate_preset_dimensions() -> bool:
|
||||||
"""Validate that all presets meet ComfyUI requirements."""
|
"""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
|
# Check divisible by 8
|
||||||
if width % 8 != 0 or height % 8 != 0:
|
if width % 8 != 0 or height % 8 != 0:
|
||||||
print(
|
print(
|
||||||
@@ -147,6 +464,36 @@ def validate_preset_dimensions() -> bool:
|
|||||||
return True
|
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
|
# Validate presets on import
|
||||||
if not validate_preset_dimensions():
|
if not validate_preset_dimensions():
|
||||||
raise ValueError("Preset validation failed - check console for details")
|
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.1"
|
||||||
|
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
|
||||||
@@ -5,8 +5,6 @@ Provides mock ComfyUI environments and test data
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
import numpy as np
|
|
||||||
from typing import Dict, Any
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ Tests the shared functionality for all ComfyAssets tools
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import logging
|
from unittest.mock import patch
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from kikotools.base import ComfyAssetsBaseNode
|
from kikotools.base import ComfyAssetsBaseNode
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Following TDD principles - these tests define the expected behavior
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
# Import the modules we're going to test (they don't exist yet - TDD!)
|
# Import the modules we're going to test (they don't exist yet - TDD!)
|
||||||
from kikotools.tools.resolution_calculator.logic import (
|
from kikotools.tools.resolution_calculator.logic import (
|
||||||
@@ -172,8 +171,6 @@ class TestResolutionCalculatorNode:
|
|||||||
|
|
||||||
def test_node_has_correct_comfyui_attributes(self):
|
def test_node_has_correct_comfyui_attributes(self):
|
||||||
"""Test node has all required ComfyUI attributes"""
|
"""Test node has all required ComfyUI attributes"""
|
||||||
node = ResolutionCalculatorNode()
|
|
||||||
|
|
||||||
# Check class attributes exist
|
# Check class attributes exist
|
||||||
assert hasattr(ResolutionCalculatorNode, "INPUT_TYPES")
|
assert hasattr(ResolutionCalculatorNode, "INPUT_TYPES")
|
||||||
assert hasattr(ResolutionCalculatorNode, "RETURN_TYPES")
|
assert hasattr(ResolutionCalculatorNode, "RETURN_TYPES")
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
"""Tests for Sampler Combo node."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from kikotools.tools.sampler_combo.node import SamplerComboNode
|
||||||
|
from kikotools.tools.sampler_combo.logic import (
|
||||||
|
validate_sampler_settings,
|
||||||
|
get_sampler_combo,
|
||||||
|
get_compatible_scheduler_suggestions,
|
||||||
|
get_recommended_steps_range,
|
||||||
|
get_recommended_cfg_range,
|
||||||
|
get_sampler_info,
|
||||||
|
SAMPLERS,
|
||||||
|
SCHEDULERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSamplerComboLogic:
|
||||||
|
"""Test cases for sampler combo logic functions."""
|
||||||
|
|
||||||
|
def test_validate_sampler_settings_valid(self):
|
||||||
|
"""Test validation with valid settings."""
|
||||||
|
assert validate_sampler_settings("euler", "normal", 20, 7.0) is True
|
||||||
|
assert validate_sampler_settings("dpmpp_2m", "karras", 15, 8.5) is True
|
||||||
|
assert validate_sampler_settings("ddim", "ddim_uniform", 30, 6.0) is True
|
||||||
|
|
||||||
|
def test_validate_sampler_settings_invalid_sampler(self):
|
||||||
|
"""Test validation with invalid sampler."""
|
||||||
|
assert validate_sampler_settings("invalid_sampler", "normal", 20, 7.0) is False
|
||||||
|
|
||||||
|
def test_validate_sampler_settings_invalid_scheduler(self):
|
||||||
|
"""Test validation with invalid scheduler."""
|
||||||
|
assert validate_sampler_settings("euler", "invalid_scheduler", 20, 7.0) is False
|
||||||
|
|
||||||
|
def test_validate_sampler_settings_invalid_steps(self):
|
||||||
|
"""Test validation with invalid steps."""
|
||||||
|
assert validate_sampler_settings("euler", "normal", 0, 7.0) is False
|
||||||
|
assert validate_sampler_settings("euler", "normal", 1001, 7.0) is False
|
||||||
|
assert validate_sampler_settings("euler", "normal", -5, 7.0) is False
|
||||||
|
|
||||||
|
def test_validate_sampler_settings_invalid_cfg(self):
|
||||||
|
"""Test validation with invalid CFG."""
|
||||||
|
assert validate_sampler_settings("euler", "normal", 20, -1.0) is False
|
||||||
|
assert validate_sampler_settings("euler", "normal", 20, 31.0) is False
|
||||||
|
|
||||||
|
def test_get_sampler_combo_valid(self):
|
||||||
|
"""Test getting sampler combo with valid inputs."""
|
||||||
|
result = get_sampler_combo("euler", "normal", 20, 7.0)
|
||||||
|
assert result == ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
result = get_sampler_combo("dpmpp_2m", "karras", 25, 8.5)
|
||||||
|
assert result == ("dpmpp_2m", "karras", 25, 8.5)
|
||||||
|
|
||||||
|
def test_get_sampler_combo_invalid_returns_defaults(self):
|
||||||
|
"""Test that invalid inputs return safe defaults."""
|
||||||
|
result = get_sampler_combo("invalid", "normal", 20, 7.0)
|
||||||
|
assert result == ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
result = get_sampler_combo("euler", "invalid", 20, 7.0)
|
||||||
|
assert result == ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
def test_get_sampler_combo_sanitizes_values(self):
|
||||||
|
"""Test that values are sanitized to valid ranges."""
|
||||||
|
# Test steps clamping
|
||||||
|
result = get_sampler_combo("euler", "normal", 0, 7.0)
|
||||||
|
assert result[2] >= 1 # steps should be at least 1
|
||||||
|
|
||||||
|
result = get_sampler_combo("euler", "normal", 1500, 7.0)
|
||||||
|
assert result[2] <= 1000 # steps should be at most 1000
|
||||||
|
|
||||||
|
# Test CFG clamping
|
||||||
|
result = get_sampler_combo("euler", "normal", 20, -5.0)
|
||||||
|
assert result[3] >= 0.0 # CFG should be at least 0
|
||||||
|
|
||||||
|
result = get_sampler_combo("euler", "normal", 20, 50.0)
|
||||||
|
assert result[3] <= 30.0 # CFG should be at most 30
|
||||||
|
|
||||||
|
def test_get_compatible_scheduler_suggestions(self):
|
||||||
|
"""Test getting scheduler suggestions for different samplers."""
|
||||||
|
suggestions = get_compatible_scheduler_suggestions("euler")
|
||||||
|
assert isinstance(suggestions, list)
|
||||||
|
assert len(suggestions) > 0
|
||||||
|
assert "normal" in suggestions
|
||||||
|
|
||||||
|
suggestions = get_compatible_scheduler_suggestions("ddim")
|
||||||
|
assert "ddim_uniform" in suggestions
|
||||||
|
|
||||||
|
# Test unknown sampler returns defaults
|
||||||
|
suggestions = get_compatible_scheduler_suggestions("unknown_sampler")
|
||||||
|
assert "normal" in suggestions
|
||||||
|
assert "karras" in suggestions
|
||||||
|
|
||||||
|
def test_get_recommended_steps_range(self):
|
||||||
|
"""Test getting recommended steps range for samplers."""
|
||||||
|
min_steps, max_steps, default_steps = get_recommended_steps_range("euler")
|
||||||
|
assert isinstance(min_steps, int)
|
||||||
|
assert isinstance(max_steps, int)
|
||||||
|
assert isinstance(default_steps, int)
|
||||||
|
assert min_steps <= default_steps <= max_steps
|
||||||
|
assert min_steps > 0
|
||||||
|
|
||||||
|
# Test unknown sampler returns defaults
|
||||||
|
min_steps, max_steps, default_steps = get_recommended_steps_range("unknown")
|
||||||
|
assert min_steps == 10
|
||||||
|
assert max_steps == 50
|
||||||
|
assert default_steps == 20
|
||||||
|
|
||||||
|
def test_get_recommended_cfg_range(self):
|
||||||
|
"""Test getting recommended CFG range for samplers."""
|
||||||
|
min_cfg, max_cfg, default_cfg = get_recommended_cfg_range("euler")
|
||||||
|
assert isinstance(min_cfg, float)
|
||||||
|
assert isinstance(max_cfg, float)
|
||||||
|
assert isinstance(default_cfg, float)
|
||||||
|
assert min_cfg <= default_cfg <= max_cfg
|
||||||
|
assert min_cfg >= 0.0
|
||||||
|
|
||||||
|
# Test unknown sampler returns defaults
|
||||||
|
min_cfg, max_cfg, default_cfg = get_recommended_cfg_range("unknown")
|
||||||
|
assert min_cfg == 1.0
|
||||||
|
assert max_cfg == 20.0
|
||||||
|
assert default_cfg == 7.0
|
||||||
|
|
||||||
|
def test_get_sampler_info(self):
|
||||||
|
"""Test getting sampler information."""
|
||||||
|
info = get_sampler_info()
|
||||||
|
assert isinstance(info, dict)
|
||||||
|
assert "samplers" in info
|
||||||
|
assert "schedulers" in info
|
||||||
|
assert "sampler_count" in info
|
||||||
|
assert "scheduler_count" in info
|
||||||
|
assert info["sampler_count"] == len(SAMPLERS)
|
||||||
|
assert info["scheduler_count"] == len(SCHEDULERS)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSamplerComboNode:
|
||||||
|
"""Test cases for SamplerComboNode."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.node = SamplerComboNode()
|
||||||
|
|
||||||
|
def test_input_types_structure(self):
|
||||||
|
"""Test that INPUT_TYPES returns correct structure."""
|
||||||
|
input_types = SamplerComboNode.INPUT_TYPES()
|
||||||
|
|
||||||
|
assert "required" in input_types
|
||||||
|
required = input_types["required"]
|
||||||
|
|
||||||
|
# Check all required inputs are present
|
||||||
|
assert "sampler_name" in required
|
||||||
|
assert "scheduler" in required
|
||||||
|
assert "steps" in required
|
||||||
|
assert "cfg" in required
|
||||||
|
|
||||||
|
# Check sampler input structure
|
||||||
|
sampler_input = required["sampler_name"]
|
||||||
|
assert sampler_input[0] == SAMPLERS
|
||||||
|
assert isinstance(sampler_input[1], dict)
|
||||||
|
assert "default" in sampler_input[1]
|
||||||
|
assert "tooltip" in sampler_input[1]
|
||||||
|
|
||||||
|
# Check scheduler input structure
|
||||||
|
scheduler_input = required["scheduler"]
|
||||||
|
assert scheduler_input[0] == SCHEDULERS
|
||||||
|
assert isinstance(scheduler_input[1], dict)
|
||||||
|
|
||||||
|
# Check steps input structure
|
||||||
|
steps_input = required["steps"]
|
||||||
|
assert steps_input[0] == "INT"
|
||||||
|
assert steps_input[1]["min"] == 1
|
||||||
|
assert steps_input[1]["max"] == 1000
|
||||||
|
|
||||||
|
# Check CFG input structure
|
||||||
|
cfg_input = required["cfg"]
|
||||||
|
assert cfg_input[0] == "FLOAT"
|
||||||
|
assert cfg_input[1]["min"] == 0.0
|
||||||
|
assert cfg_input[1]["max"] == 30.0
|
||||||
|
|
||||||
|
def test_return_types_structure(self):
|
||||||
|
"""Test that return types are correctly defined."""
|
||||||
|
assert SamplerComboNode.RETURN_TYPES == (SAMPLERS, SCHEDULERS, "INT", "FLOAT")
|
||||||
|
assert SamplerComboNode.RETURN_NAMES == (
|
||||||
|
"sampler_name",
|
||||||
|
"scheduler",
|
||||||
|
"steps",
|
||||||
|
"cfg",
|
||||||
|
)
|
||||||
|
assert SamplerComboNode.FUNCTION == "get_sampler_combo"
|
||||||
|
assert SamplerComboNode.CATEGORY == "ComfyAssets"
|
||||||
|
|
||||||
|
def test_get_sampler_combo_valid_inputs(self):
|
||||||
|
"""Test get_sampler_combo with valid inputs."""
|
||||||
|
result = self.node.get_sampler_combo("euler", "normal", 20, 7.0)
|
||||||
|
assert result == ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
result = self.node.get_sampler_combo("dpmpp_2m", "karras", 15, 8.5)
|
||||||
|
assert result == ("dpmpp_2m", "karras", 15, 8.5)
|
||||||
|
|
||||||
|
def test_get_sampler_combo_invalid_inputs_returns_defaults(self):
|
||||||
|
"""Test that invalid inputs return safe defaults."""
|
||||||
|
with patch.object(self.node, "handle_error") as mock_error:
|
||||||
|
mock_error.side_effect = ValueError("Invalid settings")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.node.get_sampler_combo("invalid", "normal", 20, 7.0)
|
||||||
|
except ValueError:
|
||||||
|
pass # Expected when handle_error raises
|
||||||
|
|
||||||
|
# Test with exception handling bypassed
|
||||||
|
with patch(
|
||||||
|
"kikotools.tools.sampler_combo.node.validate_sampler_settings",
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
result = self.node.get_sampler_combo("invalid", "normal", 20, 7.0)
|
||||||
|
assert result == ("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
def test_validate_inputs_valid(self):
|
||||||
|
"""Test input validation with valid inputs."""
|
||||||
|
# Should not raise any exception
|
||||||
|
self.node.validate_inputs("euler", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
def test_validate_inputs_invalid(self):
|
||||||
|
"""Test input validation with invalid inputs."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
self.node.validate_inputs("invalid", "normal", 20, 7.0)
|
||||||
|
|
||||||
|
def test_get_scheduler_suggestions(self):
|
||||||
|
"""Test getting scheduler suggestions."""
|
||||||
|
suggestions = self.node.get_scheduler_suggestions("euler")
|
||||||
|
assert isinstance(suggestions, list)
|
||||||
|
assert len(suggestions) > 0
|
||||||
|
|
||||||
|
suggestions = self.node.get_scheduler_suggestions("ddim")
|
||||||
|
assert "ddim_uniform" in suggestions
|
||||||
|
|
||||||
|
def test_get_steps_recommendation(self):
|
||||||
|
"""Test getting steps recommendations."""
|
||||||
|
rec = self.node.get_steps_recommendation("euler")
|
||||||
|
assert isinstance(rec, dict)
|
||||||
|
assert "min" in rec
|
||||||
|
assert "max" in rec
|
||||||
|
assert "default" in rec
|
||||||
|
assert "recommendation" in rec
|
||||||
|
|
||||||
|
def test_get_cfg_recommendation(self):
|
||||||
|
"""Test getting CFG recommendations."""
|
||||||
|
rec = self.node.get_cfg_recommendation("euler")
|
||||||
|
assert isinstance(rec, dict)
|
||||||
|
assert "min" in rec
|
||||||
|
assert "max" in rec
|
||||||
|
assert "default" in rec
|
||||||
|
assert "recommendation" in rec
|
||||||
|
|
||||||
|
def test_get_combo_analysis(self):
|
||||||
|
"""Test getting combo analysis."""
|
||||||
|
analysis = self.node.get_combo_analysis("euler", "normal", 20, 7.0)
|
||||||
|
assert isinstance(analysis, dict)
|
||||||
|
assert "sampler" in analysis
|
||||||
|
assert "scheduler" in analysis
|
||||||
|
assert "steps" in analysis
|
||||||
|
assert "cfg" in analysis
|
||||||
|
assert "valid" in analysis
|
||||||
|
assert "scheduler_suggestions" in analysis
|
||||||
|
assert "scheduler_compatible" in analysis
|
||||||
|
assert "steps_optimal" in analysis
|
||||||
|
assert "cfg_optimal" in analysis
|
||||||
|
|
||||||
|
def test_get_available_samplers(self):
|
||||||
|
"""Test getting available samplers."""
|
||||||
|
samplers = SamplerComboNode.get_available_samplers()
|
||||||
|
assert isinstance(samplers, list)
|
||||||
|
assert len(samplers) > 0
|
||||||
|
assert "euler" in samplers
|
||||||
|
|
||||||
|
def test_get_available_schedulers(self):
|
||||||
|
"""Test getting available schedulers."""
|
||||||
|
schedulers = SamplerComboNode.get_available_schedulers()
|
||||||
|
assert isinstance(schedulers, list)
|
||||||
|
assert len(schedulers) > 0
|
||||||
|
assert "normal" in schedulers
|
||||||
|
|
||||||
|
def test_string_representations(self):
|
||||||
|
"""Test string representations of the node."""
|
||||||
|
str_repr = str(self.node)
|
||||||
|
assert "SamplerComboNode" in str_repr
|
||||||
|
assert "samplers=" in str_repr
|
||||||
|
assert "schedulers=" in str_repr
|
||||||
|
|
||||||
|
repr_str = repr(self.node)
|
||||||
|
assert "SamplerComboNode" in repr_str
|
||||||
|
assert "category=" in repr_str
|
||||||
|
assert "function=" in repr_str
|
||||||
|
|
||||||
|
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, "validate_inputs")
|
||||||
|
assert hasattr(self.node, "handle_error")
|
||||||
|
assert hasattr(self.node, "log_info")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSamplerComboIntegration:
|
||||||
|
"""Integration tests for Sampler Combo functionality."""
|
||||||
|
|
||||||
|
def test_full_workflow_valid_settings(self):
|
||||||
|
"""Test complete workflow with valid settings."""
|
||||||
|
node = SamplerComboNode()
|
||||||
|
|
||||||
|
# Test with different sampler/scheduler combinations
|
||||||
|
test_cases = [
|
||||||
|
("euler", "normal", 20, 7.0),
|
||||||
|
("dpmpp_2m", "karras", 15, 8.0),
|
||||||
|
("euler_ancestral", "exponential", 25, 9.0),
|
||||||
|
("ddim", "ddim_uniform", 30, 6.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
for sampler, scheduler, steps, cfg in test_cases:
|
||||||
|
result = node.get_sampler_combo(sampler, scheduler, steps, cfg)
|
||||||
|
assert result == (sampler, scheduler, steps, cfg)
|
||||||
|
|
||||||
|
def test_recommendation_compatibility(self):
|
||||||
|
"""Test that recommendations are compatible with actual functionality."""
|
||||||
|
node = SamplerComboNode()
|
||||||
|
|
||||||
|
for sampler in SAMPLERS[:5]: # Test first 5 samplers
|
||||||
|
suggestions = node.get_scheduler_suggestions(sampler)
|
||||||
|
steps_rec = node.get_steps_recommendation(sampler)
|
||||||
|
cfg_rec = node.get_cfg_recommendation(sampler)
|
||||||
|
|
||||||
|
# Test that recommendations work with the node
|
||||||
|
for scheduler in suggestions[:2]: # Test first 2 suggestions
|
||||||
|
result = node.get_sampler_combo(
|
||||||
|
sampler, scheduler, steps_rec["default"], cfg_rec["default"]
|
||||||
|
)
|
||||||
|
assert result[0] == sampler
|
||||||
|
assert result[1] == scheduler
|
||||||
|
assert result[2] == steps_rec["default"]
|
||||||
|
assert result[3] == cfg_rec["default"]
|
||||||
|
|
||||||
|
def test_error_recovery(self):
|
||||||
|
"""Test error recovery with malformed inputs."""
|
||||||
|
node = SamplerComboNode()
|
||||||
|
|
||||||
|
# These should all return safe defaults due to error handling
|
||||||
|
with patch(
|
||||||
|
"kikotools.tools.sampler_combo.logic.validate_sampler_settings",
|
||||||
|
side_effect=Exception("Simulated error"),
|
||||||
|
):
|
||||||
|
result = node.get_sampler_combo("euler", "normal", 20, 7.0)
|
||||||
|
assert result == ("euler", "normal", 20, 7.0) # Safe defaults
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
"""Tests for Seed History tool."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from kikotools.tools.seed_history.node import SeedHistoryNode
|
||||||
|
from kikotools.tools.seed_history.logic import (
|
||||||
|
generate_random_seed,
|
||||||
|
validate_seed_value,
|
||||||
|
sanitize_seed_value,
|
||||||
|
create_history_entry,
|
||||||
|
filter_duplicate_seeds,
|
||||||
|
add_seed_to_history,
|
||||||
|
format_time_ago,
|
||||||
|
search_history_by_seed,
|
||||||
|
get_history_statistics,
|
||||||
|
export_history_to_text,
|
||||||
|
import_seeds_from_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSeedHistoryNode:
|
||||||
|
"""Test SeedHistoryNode functionality."""
|
||||||
|
|
||||||
|
def test_node_structure(self):
|
||||||
|
"""Test that node has correct ComfyUI structure."""
|
||||||
|
# Test class attributes
|
||||||
|
assert hasattr(SeedHistoryNode, "INPUT_TYPES")
|
||||||
|
assert hasattr(SeedHistoryNode, "RETURN_TYPES")
|
||||||
|
assert hasattr(SeedHistoryNode, "RETURN_NAMES")
|
||||||
|
assert hasattr(SeedHistoryNode, "FUNCTION")
|
||||||
|
assert hasattr(SeedHistoryNode, "CATEGORY")
|
||||||
|
|
||||||
|
# Test input types structure
|
||||||
|
input_types = SeedHistoryNode.INPUT_TYPES()
|
||||||
|
assert "required" in input_types
|
||||||
|
assert "seed" in input_types["required"]
|
||||||
|
|
||||||
|
# Test seed input configuration
|
||||||
|
seed_config = input_types["required"]["seed"]
|
||||||
|
assert seed_config[0] == "INT"
|
||||||
|
assert isinstance(seed_config[1], dict)
|
||||||
|
assert "default" in seed_config[1]
|
||||||
|
assert "min" in seed_config[1]
|
||||||
|
assert "max" in seed_config[1]
|
||||||
|
assert seed_config[1]["min"] == 0
|
||||||
|
assert seed_config[1]["max"] == 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
# Test return types
|
||||||
|
assert SeedHistoryNode.RETURN_TYPES == ("INT",)
|
||||||
|
assert SeedHistoryNode.RETURN_NAMES == ("seed",)
|
||||||
|
assert SeedHistoryNode.FUNCTION == "output_seed"
|
||||||
|
assert SeedHistoryNode.CATEGORY == "ComfyAssets"
|
||||||
|
|
||||||
|
def test_output_seed_valid_input(self):
|
||||||
|
"""Test seed output with valid input."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Test various valid seeds
|
||||||
|
test_seeds = [0, 12345, 999999, 0xFFFFFFFFFFFFFFFF]
|
||||||
|
|
||||||
|
for seed in test_seeds:
|
||||||
|
result = node.output_seed(seed)
|
||||||
|
assert isinstance(result, tuple)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == seed
|
||||||
|
|
||||||
|
def test_output_seed_invalid_input(self):
|
||||||
|
"""Test seed output with invalid input."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Test invalid seeds (negative values)
|
||||||
|
result = node.output_seed(-1)
|
||||||
|
assert result == (12345,) # Fallback
|
||||||
|
|
||||||
|
# Test seeds too large
|
||||||
|
result = node.output_seed(0xFFFFFFFFFFFFFFFF + 1)
|
||||||
|
assert result == (12345,) # Fallback
|
||||||
|
|
||||||
|
def test_generate_new_seed(self):
|
||||||
|
"""Test random seed generation."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Generate multiple seeds
|
||||||
|
seeds = []
|
||||||
|
for _ in range(10):
|
||||||
|
seed = node.generate_new_seed()
|
||||||
|
seeds.append(seed)
|
||||||
|
|
||||||
|
# Test all seeds are valid
|
||||||
|
for seed in seeds:
|
||||||
|
assert validate_seed_value(seed)
|
||||||
|
|
||||||
|
# Test seeds are different (probabilistically)
|
||||||
|
assert len(set(seeds)) > 5 # Should have some variety
|
||||||
|
|
||||||
|
def test_validate_seed_input(self):
|
||||||
|
"""Test seed validation."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Valid seeds
|
||||||
|
assert node.validate_seed_input(0)
|
||||||
|
assert node.validate_seed_input(12345)
|
||||||
|
assert node.validate_seed_input(0xFFFFFFFFFFFFFFFF)
|
||||||
|
|
||||||
|
# Invalid seeds
|
||||||
|
assert not node.validate_seed_input(-1)
|
||||||
|
assert not node.validate_seed_input(0xFFFFFFFFFFFFFFFF + 1)
|
||||||
|
assert not node.validate_seed_input(None)
|
||||||
|
|
||||||
|
def test_get_seed_info(self):
|
||||||
|
"""Test seed information generation."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Test various seed types
|
||||||
|
info_zero = node.get_seed_info(0)
|
||||||
|
assert "zero" in info_zero.lower()
|
||||||
|
|
||||||
|
info_default = node.get_seed_info(12345)
|
||||||
|
assert "default" in info_default.lower()
|
||||||
|
|
||||||
|
info_power_of_2 = node.get_seed_info(1024)
|
||||||
|
assert "power of 2" in info_power_of_2.lower()
|
||||||
|
|
||||||
|
# Test invalid seed
|
||||||
|
info_invalid = node.get_seed_info(-1)
|
||||||
|
assert "invalid" in info_invalid.lower()
|
||||||
|
|
||||||
|
def test_seed_range_info(self):
|
||||||
|
"""Test seed range information."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
range_info = node.get_seed_range_info()
|
||||||
|
assert "Valid range" in range_info
|
||||||
|
assert str(0xFFFFFFFFFFFFFFFF) in range_info
|
||||||
|
|
||||||
|
def test_class_methods(self):
|
||||||
|
"""Test class methods."""
|
||||||
|
# Test default seed
|
||||||
|
default_seed = SeedHistoryNode.get_default_seed()
|
||||||
|
assert default_seed == 12345
|
||||||
|
|
||||||
|
# Test range checking
|
||||||
|
assert SeedHistoryNode.is_seed_in_range(0)
|
||||||
|
assert SeedHistoryNode.is_seed_in_range(12345)
|
||||||
|
assert SeedHistoryNode.is_seed_in_range(0xFFFFFFFFFFFFFFFF)
|
||||||
|
assert not SeedHistoryNode.is_seed_in_range(-1)
|
||||||
|
assert not SeedHistoryNode.is_seed_in_range(0xFFFFFFFFFFFFFFFF + 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSeedHistoryLogic:
|
||||||
|
"""Test seed history logic functions."""
|
||||||
|
|
||||||
|
def test_generate_random_seed(self):
|
||||||
|
"""Test random seed generation."""
|
||||||
|
# Generate multiple seeds
|
||||||
|
seeds = [generate_random_seed() for _ in range(100)]
|
||||||
|
|
||||||
|
# Test all seeds are valid
|
||||||
|
for seed in seeds:
|
||||||
|
assert validate_seed_value(seed)
|
||||||
|
|
||||||
|
# Test seeds have variety
|
||||||
|
assert len(set(seeds)) > 50 # Should have good variety
|
||||||
|
|
||||||
|
def test_validate_seed_value(self):
|
||||||
|
"""Test seed validation logic."""
|
||||||
|
# Valid seeds
|
||||||
|
assert validate_seed_value(0)
|
||||||
|
assert validate_seed_value(12345)
|
||||||
|
assert validate_seed_value(0xFFFFFFFFFFFFFFFF)
|
||||||
|
|
||||||
|
# Invalid seeds
|
||||||
|
assert not validate_seed_value(-1)
|
||||||
|
assert not validate_seed_value(0xFFFFFFFFFFFFFFFF + 1)
|
||||||
|
assert not validate_seed_value(None)
|
||||||
|
assert not validate_seed_value("invalid")
|
||||||
|
assert not validate_seed_value([])
|
||||||
|
|
||||||
|
def test_sanitize_seed_value(self):
|
||||||
|
"""Test seed sanitization."""
|
||||||
|
# Valid seeds should pass through
|
||||||
|
assert sanitize_seed_value(12345) == 12345
|
||||||
|
assert sanitize_seed_value(0) == 0
|
||||||
|
assert sanitize_seed_value(0xFFFFFFFFFFFFFFFF) == 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
# String numbers should convert
|
||||||
|
assert sanitize_seed_value("12345") == 12345
|
||||||
|
assert sanitize_seed_value("0") == 0
|
||||||
|
|
||||||
|
# Out of range should clamp
|
||||||
|
assert sanitize_seed_value(-100) == 0
|
||||||
|
assert sanitize_seed_value(0xFFFFFFFFFFFFFFFF + 100) == 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
# Invalid should raise
|
||||||
|
try:
|
||||||
|
sanitize_seed_value(None)
|
||||||
|
assert False, "Should have raised ValueError"
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
sanitize_seed_value("invalid")
|
||||||
|
assert False, "Should have raised ValueError"
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_create_history_entry(self):
|
||||||
|
"""Test history entry creation."""
|
||||||
|
seed = 12345
|
||||||
|
timestamp = time.time()
|
||||||
|
|
||||||
|
# With explicit timestamp
|
||||||
|
entry = create_history_entry(seed, timestamp)
|
||||||
|
assert entry["seed"] == seed
|
||||||
|
assert entry["timestamp"] == timestamp
|
||||||
|
assert "dateString" in entry
|
||||||
|
|
||||||
|
# With auto timestamp
|
||||||
|
entry_auto = create_history_entry(seed)
|
||||||
|
assert entry_auto["seed"] == seed
|
||||||
|
assert "timestamp" in entry_auto
|
||||||
|
assert "dateString" in entry_auto
|
||||||
|
|
||||||
|
def test_filter_duplicate_seeds(self):
|
||||||
|
"""Test duplicate seed filtering."""
|
||||||
|
seed = 12345
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Empty history should not filter
|
||||||
|
assert not filter_duplicate_seeds([], seed, 500)
|
||||||
|
|
||||||
|
# Recent duplicate should filter
|
||||||
|
recent_entry = create_history_entry(seed, current_time - 0.1)
|
||||||
|
history = [recent_entry]
|
||||||
|
assert filter_duplicate_seeds(history, seed, 500)
|
||||||
|
|
||||||
|
# Old duplicate should not filter
|
||||||
|
old_entry = create_history_entry(seed, current_time - 1.0)
|
||||||
|
history = [old_entry]
|
||||||
|
assert not filter_duplicate_seeds(history, seed, 500)
|
||||||
|
|
||||||
|
# Different seed should not filter
|
||||||
|
different_entry = create_history_entry(54321, current_time - 0.1)
|
||||||
|
history = [different_entry]
|
||||||
|
assert not filter_duplicate_seeds(history, seed, 500)
|
||||||
|
|
||||||
|
def test_add_seed_to_history(self):
|
||||||
|
"""Test adding seeds to history."""
|
||||||
|
history = []
|
||||||
|
|
||||||
|
# Add first seed
|
||||||
|
new_history, was_added = add_seed_to_history(history, 12345)
|
||||||
|
assert was_added
|
||||||
|
assert len(new_history) == 1
|
||||||
|
assert new_history[0]["seed"] == 12345
|
||||||
|
|
||||||
|
# Add different seed
|
||||||
|
new_history2, was_added2 = add_seed_to_history(new_history, 54321)
|
||||||
|
assert was_added2
|
||||||
|
assert len(new_history2) == 2
|
||||||
|
assert new_history2[0]["seed"] == 54321 # Most recent first
|
||||||
|
|
||||||
|
# Add duplicate (should remove old and add new)
|
||||||
|
time.sleep(0.6) # Wait past dedup window
|
||||||
|
new_history3, was_added3 = add_seed_to_history(new_history2, 12345)
|
||||||
|
assert was_added3
|
||||||
|
assert len(new_history3) == 2
|
||||||
|
assert new_history3[0]["seed"] == 12345 # Most recent first
|
||||||
|
|
||||||
|
# Test max history limit
|
||||||
|
history_long = []
|
||||||
|
for i in range(15):
|
||||||
|
history_long, _ = add_seed_to_history(history_long, i, max_history=10)
|
||||||
|
time.sleep(0.001) # Small delay to avoid dedup
|
||||||
|
|
||||||
|
assert len(history_long) == 10
|
||||||
|
|
||||||
|
def test_format_time_ago(self):
|
||||||
|
"""Test time ago formatting."""
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
# Recent times
|
||||||
|
assert "s ago" in format_time_ago(now - 30)
|
||||||
|
assert "m ago" in format_time_ago(now - 300)
|
||||||
|
assert "h ago" in format_time_ago(now - 7200)
|
||||||
|
assert "d ago" in format_time_ago(now - 86400)
|
||||||
|
|
||||||
|
def test_search_history_by_seed(self):
|
||||||
|
"""Test history search."""
|
||||||
|
history = [
|
||||||
|
create_history_entry(12345),
|
||||||
|
create_history_entry(54321),
|
||||||
|
create_history_entry(99999),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Found seed
|
||||||
|
result = search_history_by_seed(history, 54321)
|
||||||
|
assert result is not None
|
||||||
|
assert result["seed"] == 54321
|
||||||
|
|
||||||
|
# Not found seed
|
||||||
|
result = search_history_by_seed(history, 11111)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_get_history_statistics(self):
|
||||||
|
"""Test history statistics."""
|
||||||
|
# Empty history
|
||||||
|
stats = get_history_statistics([])
|
||||||
|
assert stats["total_seeds"] == 0
|
||||||
|
assert stats["unique_seeds"] == 0
|
||||||
|
|
||||||
|
# History with data
|
||||||
|
now = time.time()
|
||||||
|
history = [
|
||||||
|
create_history_entry(12345, now - 3600),
|
||||||
|
create_history_entry(54321, now - 1800),
|
||||||
|
create_history_entry(12345, now), # Duplicate
|
||||||
|
]
|
||||||
|
|
||||||
|
stats = get_history_statistics(history)
|
||||||
|
assert stats["total_seeds"] == 3
|
||||||
|
assert stats["unique_seeds"] == 2
|
||||||
|
assert stats["time_span_hours"] == 1.0
|
||||||
|
|
||||||
|
def test_export_history_to_text(self):
|
||||||
|
"""Test history export."""
|
||||||
|
# Empty history
|
||||||
|
text = export_history_to_text([])
|
||||||
|
assert "Empty" in text
|
||||||
|
|
||||||
|
# History with data
|
||||||
|
history = [create_history_entry(12345), create_history_entry(54321)]
|
||||||
|
|
||||||
|
text = export_history_to_text(history)
|
||||||
|
assert "12345" in text
|
||||||
|
assert "54321" in text
|
||||||
|
assert "Total seeds: 2" in text
|
||||||
|
|
||||||
|
def test_import_seeds_from_list(self):
|
||||||
|
"""Test importing seeds from list."""
|
||||||
|
seed_list = [12345, 54321, 99999]
|
||||||
|
|
||||||
|
history = import_seeds_from_list(seed_list)
|
||||||
|
assert len(history) == 3
|
||||||
|
|
||||||
|
# Check seeds are in correct order (newest first)
|
||||||
|
assert history[0]["seed"] == 12345
|
||||||
|
assert history[1]["seed"] == 54321
|
||||||
|
assert history[2]["seed"] == 99999
|
||||||
|
|
||||||
|
# Check timestamps are spaced
|
||||||
|
assert history[0]["timestamp"] > history[1]["timestamp"]
|
||||||
|
assert history[1]["timestamp"] > history[2]["timestamp"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestSeedHistoryIntegration:
|
||||||
|
"""Test integration scenarios."""
|
||||||
|
|
||||||
|
def test_complete_workflow(self):
|
||||||
|
"""Test complete seed history workflow."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Test basic seed output
|
||||||
|
result = node.output_seed(12345)
|
||||||
|
assert result == (12345,)
|
||||||
|
|
||||||
|
# Test seed generation
|
||||||
|
new_seed = node.generate_new_seed()
|
||||||
|
assert validate_seed_value(new_seed)
|
||||||
|
|
||||||
|
# Test seed info
|
||||||
|
info = node.get_seed_info(new_seed)
|
||||||
|
assert str(new_seed) in info
|
||||||
|
|
||||||
|
def test_history_management(self):
|
||||||
|
"""Test history management operations."""
|
||||||
|
history = []
|
||||||
|
|
||||||
|
# Add seeds over time
|
||||||
|
seeds = [12345, 54321, 99999, 11111, 22222]
|
||||||
|
for seed in seeds:
|
||||||
|
history, was_added = add_seed_to_history(history, seed)
|
||||||
|
assert was_added
|
||||||
|
time.sleep(0.001) # Avoid dedup
|
||||||
|
|
||||||
|
# Check history order (newest first)
|
||||||
|
assert history[0]["seed"] == 22222
|
||||||
|
assert history[-1]["seed"] == 12345
|
||||||
|
|
||||||
|
# Test search
|
||||||
|
found = search_history_by_seed(history, 99999)
|
||||||
|
assert found is not None
|
||||||
|
|
||||||
|
# Test statistics
|
||||||
|
stats = get_history_statistics(history)
|
||||||
|
assert stats["total_seeds"] == 5
|
||||||
|
assert stats["unique_seeds"] == 5
|
||||||
|
|
||||||
|
def test_error_handling(self):
|
||||||
|
"""Test error handling scenarios."""
|
||||||
|
node = SeedHistoryNode()
|
||||||
|
|
||||||
|
# Test with invalid seeds
|
||||||
|
result = node.output_seed(-1)
|
||||||
|
assert result == (12345,) # Fallback
|
||||||
|
|
||||||
|
# Test validation
|
||||||
|
assert not node.validate_seed_input(None)
|
||||||
|
assert not node.validate_seed_input("invalid")
|
||||||
|
|
||||||
|
# Test history with invalid seeds
|
||||||
|
history = []
|
||||||
|
history, was_added = add_seed_to_history(history, -1)
|
||||||
|
assert not was_added
|
||||||
|
assert len(history) == 0
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
"""Tests for Width Height Selector tool."""
|
"""Tests for Width Height Selector tool."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import Mock
|
|
||||||
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode
|
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode
|
||||||
from kikotools.tools.width_height_selector.logic import (
|
from kikotools.tools.width_height_selector.logic import (
|
||||||
get_preset_dimensions,
|
get_preset_dimensions,
|
||||||
@@ -10,9 +8,12 @@ from kikotools.tools.width_height_selector.logic import (
|
|||||||
)
|
)
|
||||||
from kikotools.tools.width_height_selector.presets import (
|
from kikotools.tools.width_height_selector.presets import (
|
||||||
PRESET_OPTIONS,
|
PRESET_OPTIONS,
|
||||||
|
PRESET_METADATA,
|
||||||
SDXL_PRESETS,
|
SDXL_PRESETS,
|
||||||
FLUX_PRESETS,
|
FLUX_PRESETS,
|
||||||
ULTRA_WIDE_PRESETS,
|
ULTRA_WIDE_PRESETS,
|
||||||
|
get_preset_metadata,
|
||||||
|
get_presets_by_model_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +45,8 @@ class TestWidthHeightSelectorNode:
|
|||||||
assert result == (1920, 1080)
|
assert result == (1920, 1080)
|
||||||
|
|
||||||
def test_sdxl_square_preset(self):
|
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(
|
result = self.node.get_dimensions(
|
||||||
preset="1024×1024",
|
preset="1024×1024",
|
||||||
width=512, # Should be ignored
|
width=512, # Should be ignored
|
||||||
@@ -52,38 +54,91 @@ class TestWidthHeightSelectorNode:
|
|||||||
)
|
)
|
||||||
assert result == (1024, 1024)
|
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):
|
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)
|
result = self.node.get_dimensions(preset="832×1216", width=512, height=512)
|
||||||
assert result == (832, 1216)
|
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):
|
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)
|
result = self.node.get_dimensions(preset="1216×832", width=512, height=512)
|
||||||
assert result == (1216, 832)
|
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):
|
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)
|
result = self.node.get_dimensions(preset="1920×1080", width=512, height=512)
|
||||||
assert result == (1920, 1080)
|
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):
|
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)
|
result = self.node.get_dimensions(preset="2560×1080", width=512, height=512)
|
||||||
assert result == (2560, 1080)
|
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):
|
def test_all_presets_available(self):
|
||||||
"""Test that all presets are available in INPUT_TYPES."""
|
"""Test that all presets are available in INPUT_TYPES."""
|
||||||
input_types = self.node.INPUT_TYPES()
|
input_types = self.node.INPUT_TYPES()
|
||||||
available_presets = input_types["required"]["preset"][0]
|
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 "custom" in available_presets
|
||||||
assert "1024×1024" in available_presets # SDXL square
|
|
||||||
assert "832×1216" in available_presets # SDXL portrait
|
# Check that formatted presets are available (with metadata)
|
||||||
assert "1216×832" in available_presets # SDXL landscape
|
# Extract raw preset names from formatted options
|
||||||
assert "1920×1080" in available_presets # FLUX
|
raw_presets = []
|
||||||
assert "2560×1080" in available_presets # Ultra-wide
|
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):
|
def test_invalid_preset_fallback(self):
|
||||||
"""Test handling of invalid preset."""
|
"""Test handling of invalid preset."""
|
||||||
@@ -261,3 +316,243 @@ class TestEdgeCases:
|
|||||||
# Prime number dimensions
|
# Prime number dimensions
|
||||||
ratio = calculate_aspect_ratio(1920, 1080)
|
ratio = calculate_aspect_ratio(1920, 1080)
|
||||||
assert ratio == "16:9"
|
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,524 @@
|
|||||||
|
// ComfyUI-KikoTools - Seed History with Tracking UI
|
||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "comfyassets.SeedHistory",
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
// Store reference to all SeedHistory nodes
|
||||||
|
window.seedHistoryNodes = window.seedHistoryNodes || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
|
if (nodeData.name === "SeedHistory") {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
if (onNodeCreated) {
|
||||||
|
onNodeCreated.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize seed history
|
||||||
|
this.seedHistory = this.loadSeedHistory();
|
||||||
|
this.hideTimer = null;
|
||||||
|
this.mouseOverHistory = false;
|
||||||
|
|
||||||
|
// Register this node in global registry
|
||||||
|
window.seedHistoryNodes = window.seedHistoryNodes || [];
|
||||||
|
window.seedHistoryNodes.push(this);
|
||||||
|
|
||||||
|
// Create UI container
|
||||||
|
const uiContainer = document.createElement("div");
|
||||||
|
uiContainer.style.padding = "8px";
|
||||||
|
uiContainer.style.backgroundColor = "#1e1e1e";
|
||||||
|
uiContainer.style.borderRadius = "6px";
|
||||||
|
uiContainer.style.marginTop = "6px";
|
||||||
|
uiContainer.style.border = "1px solid #404040";
|
||||||
|
|
||||||
|
this.buildSeedInterface(uiContainer);
|
||||||
|
|
||||||
|
// Add as widget
|
||||||
|
this.seedWidget = this.addDOMWidget(
|
||||||
|
"seed_history_ui",
|
||||||
|
"div",
|
||||||
|
uiContainer,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set node size
|
||||||
|
if (!this.hasBeenResized) {
|
||||||
|
this.size = [280, 320];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track manual resizing
|
||||||
|
const originalResize = this.onResize;
|
||||||
|
this.onResize = function (size) {
|
||||||
|
this.hasBeenResized = true;
|
||||||
|
if (originalResize) {
|
||||||
|
originalResize.call(this, size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook into seed widget callbacks to track all changes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setupSeedWidgetCallbacks();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Hook directly into widget value changes
|
||||||
|
const originalOnWidgetChange = this.onWidgetChange;
|
||||||
|
this.onWidgetChange = function(name, value, oldValue, widget) {
|
||||||
|
if (name === "seed" && value !== oldValue) {
|
||||||
|
this.addSeedToHistory(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalOnWidgetChange) {
|
||||||
|
return originalOnWidgetChange.call(this, name, value, oldValue, widget);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save/load data
|
||||||
|
const originalSerialize = this.serialize;
|
||||||
|
this.serialize = function () {
|
||||||
|
const data = originalSerialize ? originalSerialize.call(this) : {};
|
||||||
|
data.hasBeenResized = this.hasBeenResized;
|
||||||
|
data.seedHistory = this.seedHistory;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalConfigure = this.configure;
|
||||||
|
this.configure = function (data) {
|
||||||
|
if (originalConfigure) {
|
||||||
|
originalConfigure.call(this, data);
|
||||||
|
}
|
||||||
|
if (data.hasBeenResized) {
|
||||||
|
this.hasBeenResized = data.hasBeenResized;
|
||||||
|
}
|
||||||
|
if (data.seedHistory) {
|
||||||
|
this.seedHistory = data.seedHistory;
|
||||||
|
this.refreshHistoryDisplay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup interval on node removal
|
||||||
|
const originalOnRemoved = this.onRemoved;
|
||||||
|
this.onRemoved = function () {
|
||||||
|
if (this.seedValueWatcher) {
|
||||||
|
clearInterval(this.seedValueWatcher);
|
||||||
|
this.seedValueWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up deduplication tracking
|
||||||
|
if (this.lastAddedSeed) {
|
||||||
|
this.lastAddedSeed = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from global registry
|
||||||
|
if (window.seedHistoryNodes) {
|
||||||
|
const index = window.seedHistoryNodes.indexOf(this);
|
||||||
|
if (index !== -1) {
|
||||||
|
window.seedHistoryNodes.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalOnRemoved) {
|
||||||
|
originalOnRemoved.call(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setDirtyCanvas(true, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup seed widget callbacks to track increment/decrement/randomize
|
||||||
|
nodeType.prototype.setupSeedWidgetCallbacks = function () {
|
||||||
|
// Find the seed widget
|
||||||
|
const seedWidget = this.widgets?.find(w => w.name === "seed");
|
||||||
|
if (!seedWidget) {
|
||||||
|
setTimeout(() => this.setupSeedWidgetCallbacks(), 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the last known seed value to detect changes
|
||||||
|
this.lastSeedValue = seedWidget.value;
|
||||||
|
|
||||||
|
// Monitor for value changes that might not trigger callback
|
||||||
|
this.seedValueWatcher = setInterval(() => {
|
||||||
|
if (seedWidget.value !== this.lastSeedValue) {
|
||||||
|
this.lastSeedValue = seedWidget.value;
|
||||||
|
this.addSeedToHistory(seedWidget.value);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the seed interface
|
||||||
|
nodeType.prototype.buildSeedInterface = function (container) {
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const headerDiv = document.createElement("div");
|
||||||
|
headerDiv.style.marginBottom = "8px";
|
||||||
|
|
||||||
|
const titleDiv = document.createElement("div");
|
||||||
|
titleDiv.style.fontWeight = "bold";
|
||||||
|
titleDiv.style.color = "#00d4ff";
|
||||||
|
titleDiv.style.textAlign = "center";
|
||||||
|
titleDiv.style.padding = "4px";
|
||||||
|
titleDiv.style.backgroundColor = "rgba(0, 212, 255, 0.1)";
|
||||||
|
titleDiv.style.borderRadius = "4px";
|
||||||
|
titleDiv.style.fontSize = "11px";
|
||||||
|
titleDiv.innerHTML = "🎲 Seed History";
|
||||||
|
headerDiv.appendChild(titleDiv);
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
const buttonDiv = document.createElement("div");
|
||||||
|
buttonDiv.style.display = "flex";
|
||||||
|
buttonDiv.style.gap = "4px";
|
||||||
|
buttonDiv.style.marginTop = "6px";
|
||||||
|
|
||||||
|
// Generate button
|
||||||
|
const generateBtn = document.createElement("button");
|
||||||
|
generateBtn.textContent = "🎲 Generate";
|
||||||
|
generateBtn.style.flex = "1";
|
||||||
|
generateBtn.style.padding = "3px 6px";
|
||||||
|
generateBtn.style.backgroundColor = "#0088cc";
|
||||||
|
generateBtn.style.color = "white";
|
||||||
|
generateBtn.style.border = "none";
|
||||||
|
generateBtn.style.borderRadius = "3px";
|
||||||
|
generateBtn.style.cursor = "pointer";
|
||||||
|
generateBtn.style.fontSize = "10px";
|
||||||
|
generateBtn.addEventListener("click", () => this.generateRandomSeed());
|
||||||
|
buttonDiv.appendChild(generateBtn);
|
||||||
|
|
||||||
|
// Clear button
|
||||||
|
const clearBtn = document.createElement("button");
|
||||||
|
clearBtn.textContent = "🗑️ Clear";
|
||||||
|
clearBtn.style.flex = "1";
|
||||||
|
clearBtn.style.padding = "3px 6px";
|
||||||
|
clearBtn.style.backgroundColor = "#cc4444";
|
||||||
|
clearBtn.style.color = "white";
|
||||||
|
clearBtn.style.border = "none";
|
||||||
|
clearBtn.style.borderRadius = "3px";
|
||||||
|
clearBtn.style.cursor = "pointer";
|
||||||
|
clearBtn.style.fontSize = "10px";
|
||||||
|
clearBtn.addEventListener("click", () => this.clearSeedHistory());
|
||||||
|
buttonDiv.appendChild(clearBtn);
|
||||||
|
|
||||||
|
headerDiv.appendChild(buttonDiv);
|
||||||
|
container.appendChild(headerDiv);
|
||||||
|
|
||||||
|
// History display
|
||||||
|
const historyDiv = document.createElement("div");
|
||||||
|
historyDiv.style.maxHeight = "180px";
|
||||||
|
historyDiv.style.overflowY = "auto";
|
||||||
|
historyDiv.style.border = "1px solid #333";
|
||||||
|
historyDiv.style.borderRadius = "4px";
|
||||||
|
historyDiv.style.backgroundColor = "#2a2a2a";
|
||||||
|
historyDiv.style.padding = "6px";
|
||||||
|
historyDiv.style.fontSize = "10px";
|
||||||
|
historyDiv.style.fontFamily = "monospace";
|
||||||
|
|
||||||
|
// Mouse events for auto-hide
|
||||||
|
historyDiv.addEventListener("mouseenter", () => {
|
||||||
|
this.mouseOverHistory = true;
|
||||||
|
this.cancelAutoHide();
|
||||||
|
});
|
||||||
|
|
||||||
|
historyDiv.addEventListener("mouseleave", () => {
|
||||||
|
this.mouseOverHistory = false;
|
||||||
|
this.startAutoHide();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.historyDisplay = historyDiv;
|
||||||
|
container.appendChild(historyDiv);
|
||||||
|
|
||||||
|
this.refreshHistoryDisplay();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load history from storage
|
||||||
|
nodeType.prototype.loadSeedHistory = function () {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('comfyui_kikotools_seed_history');
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save history to storage
|
||||||
|
nodeType.prototype.saveSeedHistory = function () {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('comfyui_kikotools_seed_history', JSON.stringify(this.seedHistory));
|
||||||
|
} catch (error) {
|
||||||
|
// Silently handle storage errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add seed to history
|
||||||
|
nodeType.prototype.addSeedToHistory = function (seed) {
|
||||||
|
if (!seed || seed === 0) return;
|
||||||
|
|
||||||
|
const numSeed = typeof seed === 'string' ? parseInt(seed) : seed;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Deduplication: prevent adding the same seed within 500ms window
|
||||||
|
if (!this.lastAddedSeed) {
|
||||||
|
this.lastAddedSeed = { seed: null, timestamp: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSinceLastAdd = now - this.lastAddedSeed.timestamp;
|
||||||
|
const isSameSeed = this.lastAddedSeed.seed === numSeed;
|
||||||
|
const isWithinDupeWindow = timeSinceLastAdd < 500; // 500ms window
|
||||||
|
|
||||||
|
if (isSameSeed && isWithinDupeWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update deduplication tracking
|
||||||
|
this.lastAddedSeed = { seed: numSeed, timestamp: now };
|
||||||
|
|
||||||
|
// Remove if already exists in history
|
||||||
|
this.seedHistory = this.seedHistory.filter(item => item.seed !== numSeed);
|
||||||
|
|
||||||
|
// Add to front
|
||||||
|
this.seedHistory.unshift({
|
||||||
|
seed: numSeed,
|
||||||
|
timestamp: now,
|
||||||
|
dateString: new Date().toLocaleString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 10
|
||||||
|
if (this.seedHistory.length > 10) {
|
||||||
|
this.seedHistory = this.seedHistory.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveSeedHistory();
|
||||||
|
this.refreshHistoryDisplay();
|
||||||
|
this.startAutoHide();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate new random seed
|
||||||
|
nodeType.prototype.generateRandomSeed = function () {
|
||||||
|
const newSeed = Math.floor(Math.random() * 0xFFFFFFFFFFFFFFFF);
|
||||||
|
|
||||||
|
const seedWidget = this.widgets?.find(w => w.name === "seed");
|
||||||
|
if (seedWidget) {
|
||||||
|
seedWidget.value = newSeed;
|
||||||
|
if (seedWidget.callback) {
|
||||||
|
seedWidget.callback(newSeed, this, seedWidget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addSeedToHistory(newSeed);
|
||||||
|
this.setDirtyCanvas(true, true);
|
||||||
|
this.showMessage(`Generated: ${newSeed}`, "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use seed from history
|
||||||
|
nodeType.prototype.useSeedFromHistory = function (historyItem, index) {
|
||||||
|
const seedWidget = this.widgets?.find(w => w.name === "seed");
|
||||||
|
if (seedWidget) {
|
||||||
|
seedWidget.value = historyItem.seed;
|
||||||
|
if (seedWidget.callback) {
|
||||||
|
seedWidget.callback(historyItem.seed, this, seedWidget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.highlightHistoryEntry(index);
|
||||||
|
this.setDirtyCanvas(true, true);
|
||||||
|
this.startAutoHide();
|
||||||
|
this.showMessage(`Loaded: ${historyItem.seed}`, "info");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear history
|
||||||
|
nodeType.prototype.clearSeedHistory = function () {
|
||||||
|
this.seedHistory = [];
|
||||||
|
this.saveSeedHistory();
|
||||||
|
this.refreshHistoryDisplay();
|
||||||
|
this.showMessage("History cleared", "info");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Refresh history display
|
||||||
|
nodeType.prototype.refreshHistoryDisplay = function () {
|
||||||
|
if (!this.historyDisplay) return;
|
||||||
|
|
||||||
|
if (!this.seedHistory || this.seedHistory.length === 0) {
|
||||||
|
this.historyDisplay.innerHTML =
|
||||||
|
'<div style="color: #888; text-align: center; padding: 15px;">No seeds tracked<br><small>Generate seeds to build history</small></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.historyDisplay.innerHTML = "";
|
||||||
|
|
||||||
|
this.seedHistory.forEach((item, index) => {
|
||||||
|
const entryDiv = document.createElement("div");
|
||||||
|
entryDiv.style.padding = "4px";
|
||||||
|
entryDiv.style.marginBottom = "3px";
|
||||||
|
entryDiv.style.backgroundColor = "#333";
|
||||||
|
entryDiv.style.borderRadius = "2px";
|
||||||
|
entryDiv.style.cursor = "pointer";
|
||||||
|
entryDiv.style.border = "1px solid transparent";
|
||||||
|
entryDiv.style.lineHeight = "1.2";
|
||||||
|
|
||||||
|
entryDiv.addEventListener("mouseenter", () => {
|
||||||
|
entryDiv.style.backgroundColor = "#444";
|
||||||
|
entryDiv.style.border = "1px solid #555";
|
||||||
|
});
|
||||||
|
entryDiv.addEventListener("mouseleave", () => {
|
||||||
|
entryDiv.style.backgroundColor = "#333";
|
||||||
|
entryDiv.style.border = "1px solid transparent";
|
||||||
|
});
|
||||||
|
|
||||||
|
entryDiv.addEventListener("click", () => {
|
||||||
|
this.useSeedFromHistory(item, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeAgo = this.formatTimeAgo(item.timestamp);
|
||||||
|
entryDiv.innerHTML = `
|
||||||
|
<div style="color: #fff; font-weight: bold; margin-bottom: 1px;">
|
||||||
|
🎲 ${item.seed}
|
||||||
|
</div>
|
||||||
|
<div style="color: #999; font-size: 8px;">
|
||||||
|
⏰ ${timeAgo}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.historyDisplay.appendChild(entryDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startAutoHide();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Highlight selected entry
|
||||||
|
nodeType.prototype.highlightHistoryEntry = function (index) {
|
||||||
|
const entries = this.historyDisplay.querySelectorAll('div[style*="cursor: pointer"]');
|
||||||
|
entries.forEach((entry, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
entry.style.backgroundColor = "#006600";
|
||||||
|
entry.style.border = "1px solid #00aa00";
|
||||||
|
} else {
|
||||||
|
entry.style.backgroundColor = "#333";
|
||||||
|
entry.style.border = "1px solid transparent";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-hide functionality
|
||||||
|
nodeType.prototype.startAutoHide = function () {
|
||||||
|
this.cancelAutoHide();
|
||||||
|
if (!this.mouseOverHistory) {
|
||||||
|
this.hideTimer = setTimeout(() => {
|
||||||
|
this.hideHistorySection();
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeType.prototype.cancelAutoHide = function () {
|
||||||
|
if (this.hideTimer) {
|
||||||
|
clearTimeout(this.hideTimer);
|
||||||
|
this.hideTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeType.prototype.hideHistorySection = function () {
|
||||||
|
if (this.historyDisplay && !this.mouseOverHistory) {
|
||||||
|
this.historyDisplay.style.display = "none";
|
||||||
|
|
||||||
|
if (!this.restoreButton) {
|
||||||
|
const restoreDiv = document.createElement("div");
|
||||||
|
restoreDiv.style.padding = "10px";
|
||||||
|
restoreDiv.style.backgroundColor = "#2a2a2a";
|
||||||
|
restoreDiv.style.border = "1px solid #333";
|
||||||
|
restoreDiv.style.borderRadius = "4px";
|
||||||
|
restoreDiv.style.textAlign = "center";
|
||||||
|
restoreDiv.style.cursor = "pointer";
|
||||||
|
restoreDiv.style.color = "#888";
|
||||||
|
restoreDiv.style.fontSize = "10px";
|
||||||
|
restoreDiv.innerHTML = "🎲 History auto-hidden<br><small>Click to show</small>";
|
||||||
|
|
||||||
|
restoreDiv.addEventListener("mouseenter", () => {
|
||||||
|
restoreDiv.style.backgroundColor = "#333";
|
||||||
|
restoreDiv.style.color = "#bbb";
|
||||||
|
});
|
||||||
|
restoreDiv.addEventListener("mouseleave", () => {
|
||||||
|
restoreDiv.style.backgroundColor = "#2a2a2a";
|
||||||
|
restoreDiv.style.color = "#888";
|
||||||
|
});
|
||||||
|
|
||||||
|
restoreDiv.addEventListener("click", () => {
|
||||||
|
this.showHistorySection();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.restoreButton = restoreDiv;
|
||||||
|
this.historyDisplay.parentNode.insertBefore(
|
||||||
|
restoreDiv,
|
||||||
|
this.historyDisplay.nextSibling
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeType.prototype.showHistorySection = function () {
|
||||||
|
if (this.historyDisplay) {
|
||||||
|
this.historyDisplay.style.display = "block";
|
||||||
|
|
||||||
|
if (this.restoreButton && this.restoreButton.parentNode) {
|
||||||
|
this.restoreButton.parentNode.removeChild(this.restoreButton);
|
||||||
|
this.restoreButton = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startAutoHide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format time ago
|
||||||
|
nodeType.prototype.formatTimeAgo = function (timestamp) {
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - timestamp;
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) return `${days}d ago`;
|
||||||
|
if (hours > 0) return `${hours}h ago`;
|
||||||
|
if (minutes > 0) return `${minutes}m ago`;
|
||||||
|
return `${seconds}s ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show messages
|
||||||
|
nodeType.prototype.showMessage = function (message, type = "info") {
|
||||||
|
const notification = document.createElement("div");
|
||||||
|
notification.style.position = "fixed";
|
||||||
|
notification.style.top = "15px";
|
||||||
|
notification.style.right = "15px";
|
||||||
|
notification.style.padding = "6px 10px";
|
||||||
|
notification.style.borderRadius = "3px";
|
||||||
|
notification.style.color = "white";
|
||||||
|
notification.style.fontSize = "10px";
|
||||||
|
notification.style.zIndex = "10000";
|
||||||
|
notification.style.maxWidth = "200px";
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "success":
|
||||||
|
notification.style.backgroundColor = "#28a745";
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
notification.style.backgroundColor = "#dc3545";
|
||||||
|
break;
|
||||||
|
case "warning":
|
||||||
|
notification.style.backgroundColor = "#ffc107";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
notification.style.backgroundColor = "#17a2b8";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 1800);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
+44
-24
@@ -12,6 +12,20 @@ app.registerExtension({
|
|||||||
// Track button click state for visual feedback
|
// Track button click state for visual feedback
|
||||||
this.swapButtonPressed = false;
|
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
|
// Override preset callback to update width/height widgets when preset changes
|
||||||
const presetWidget = this.widgets.find(w => w.name === "preset");
|
const presetWidget = this.widgets.find(w => w.name === "preset");
|
||||||
if (presetWidget) {
|
if (presetWidget) {
|
||||||
@@ -27,6 +41,9 @@ app.registerExtension({
|
|||||||
const heightWidget = node.widgets.find(w => w.name === "height");
|
const heightWidget = node.widgets.find(w => w.name === "height");
|
||||||
|
|
||||||
if (widthWidget && heightWidget && value !== "custom") {
|
if (widthWidget && heightWidget && value !== "custom") {
|
||||||
|
// Extract raw resolution from formatted preset
|
||||||
|
const rawResolution = node.extractResolutionFromPreset(value);
|
||||||
|
|
||||||
// Define all available presets from our preset system
|
// Define all available presets from our preset system
|
||||||
const presetDimensions = {
|
const presetDimensions = {
|
||||||
// SDXL Presets
|
// SDXL Presets
|
||||||
@@ -43,8 +60,8 @@ app.registerExtension({
|
|||||||
"768×1792": [768, 1792], "768×2304": [768, 2304]
|
"768×1792": [768, 1792], "768×2304": [768, 2304]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (presetDimensions[value]) {
|
if (rawResolution && presetDimensions[rawResolution]) {
|
||||||
const [w, h] = presetDimensions[value];
|
const [w, h] = presetDimensions[rawResolution];
|
||||||
widthWidget.value = w;
|
widthWidget.value = w;
|
||||||
heightWidget.value = h;
|
heightWidget.value = h;
|
||||||
|
|
||||||
@@ -71,39 +88,42 @@ app.registerExtension({
|
|||||||
if (presetWidget.value !== "custom") {
|
if (presetWidget.value !== "custom") {
|
||||||
const currentPreset = presetWidget.value;
|
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)
|
// Parse current preset dimensions (handle both × and x separators)
|
||||||
let w, h;
|
let w, h;
|
||||||
if (currentPreset.includes('×')) {
|
if (rawResolution.includes('×')) {
|
||||||
[w, h] = currentPreset.split('×').map(v => parseInt(v));
|
[w, h] = rawResolution.split('×').map(v => parseInt(v));
|
||||||
} else if (currentPreset.includes('x')) {
|
} else if (rawResolution.includes('x')) {
|
||||||
[w, h] = currentPreset.split('x').map(v => parseInt(v));
|
[w, h] = rawResolution.split('x').map(v => parseInt(v));
|
||||||
} else {
|
} else {
|
||||||
return; // Invalid preset format
|
return; // Invalid preset format
|
||||||
}
|
}
|
||||||
|
|
||||||
const swappedPreset = `${h}×${w}`;
|
const swappedRawPreset = `${h}×${w}`;
|
||||||
|
|
||||||
// Define all available presets from our preset system
|
// Find the formatted version of the swapped preset from available options
|
||||||
const availablePresets = [
|
const availablePresets = presetWidget.options.values || presetWidget.options;
|
||||||
"custom",
|
let swappedFormattedPreset = null;
|
||||||
// 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"
|
|
||||||
];
|
|
||||||
|
|
||||||
if (availablePresets.includes(swappedPreset)) {
|
for (const option of availablePresets) {
|
||||||
// Swapped preset exists, use it
|
if (option === "custom") continue;
|
||||||
presetWidget.value = swappedPreset;
|
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;
|
widthWidget.value = h;
|
||||||
heightWidget.value = w;
|
heightWidget.value = w;
|
||||||
if (presetWidget.callback) {
|
if (presetWidget.callback) {
|
||||||
presetWidget.callback(swappedPreset, this, presetWidget);
|
presetWidget.callback(swappedFormattedPreset, this, presetWidget);
|
||||||
}
|
}
|
||||||
if (widthWidget.callback) {
|
if (widthWidget.callback) {
|
||||||
widthWidget.callback(h, this, widthWidget);
|
widthWidget.callback(h, this, widthWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user