Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b348273d3a | ||
|
|
cc1ffe2605 | ||
|
|
ef30413e12 | ||
|
|
5485aa8c19 | ||
|
|
a8ee5930ff | ||
|
|
9fd80793db | ||
|
|
972c487dd4 | ||
|
|
1a3efd3802 | ||
|
|
34f54e515d | ||
|
|
c844abea51 | ||
|
|
404a1efd61 | ||
|
|
4ae514dacf | ||
|
|
5efae8eeb8 | ||
|
|
85288c8fd8 | ||
|
|
7a97f7c2bc | ||
|
|
a4692a286c | ||
|
|
72a3fea3cb | ||
|
|
d5d4145a04 | ||
|
|
0e288dd109 | ||
|
|
c4882f894e | ||
|
|
6cbe6e5ae6 | ||
|
|
df20afb83e | ||
|
|
7d63e11e18 | ||
|
|
a8364b5c57 | ||
|
|
332a74225d | ||
|
|
d757b623d6 | ||
|
|
64e844ec42 | ||
|
|
3ed188d63f | ||
|
|
b9cc9f295d | ||
|
|
271cd020c1 | ||
|
|
e34807855a | ||
|
|
d32e18f844 | ||
|
|
228b74ae5e | ||
|
|
6197b482df | ||
|
|
f62129afda | ||
|
|
8d5065c975 | ||
|
|
2d27c32bfd | ||
|
|
3ecab5ac08 | ||
|
|
70592114f9 | ||
|
|
407fc4ca7b | ||
|
|
cb7d5246f9 | ||
|
|
9829fc001d | ||
|
|
e84ec6721c | ||
|
|
80fac8e544 | ||
|
|
efc079a95b | ||
|
|
bbd239cbd6 | ||
|
|
589fbf3568 | ||
|
|
c595cabaa0 | ||
|
|
ca504d5f74 | ||
|
|
f559fe220e |
@@ -25,6 +25,10 @@ per-file-ignores =
|
||||
__init__.py:F401,F403
|
||||
# Allow assertions in tests
|
||||
tests/*:S101
|
||||
# Allow higher complexity for Gemini prompt module
|
||||
kikotools/tools/gemini_prompt/logic.py:C901
|
||||
kikotools/tools/gemini_prompt/models.py:C901
|
||||
kikotools/tools/gemini_prompt/node.py:C901
|
||||
|
||||
# Statistics
|
||||
count = True
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Code Quality
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -14,12 +16,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-quality-${{ hashFiles('**/requirements-dev.txt') }}
|
||||
@@ -134,7 +136,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -165,7 +167,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -8,12 +11,14 @@ on:
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -109,7 +114,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.get_version.outputs.version }}
|
||||
name: ComfyUI-KikoTools ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
name: Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
@@ -17,12 +20,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }}
|
||||
@@ -50,7 +53,7 @@ jobs:
|
||||
print('✓ All imports successful')
|
||||
|
||||
# Test base node
|
||||
assert ComfyAssetsBaseNode.CATEGORY == 'ComfyAssets'
|
||||
assert ComfyAssetsBaseNode.CATEGORY.startswith('ComfyAssets')
|
||||
print('✓ Base node tests passed')
|
||||
|
||||
# Test dimension extraction
|
||||
@@ -159,9 +162,13 @@ jobs:
|
||||
print('✓ Sampler Combo interface tests passed')
|
||||
|
||||
# Test return types
|
||||
assert node.RETURN_TYPES == ('SAMPLER', SCHEDULERS, 'INT', 'FLOAT')
|
||||
# RETURN_TYPES[1] is the actual SCHEDULERS list
|
||||
assert node.RETURN_TYPES[0] == 'SAMPLER'
|
||||
assert isinstance(node.RETURN_TYPES[1], list) # SCHEDULERS is a list
|
||||
assert node.RETURN_TYPES[2] == 'INT'
|
||||
assert node.RETURN_TYPES[3] == 'FLOAT'
|
||||
assert node.RETURN_NAMES == ('sampler_name', 'scheduler', 'steps', 'cfg')
|
||||
assert node.CATEGORY == 'ComfyAssets'
|
||||
assert node.CATEGORY == 'ComfyAssets/🌀 Samplers'
|
||||
print('✓ Sampler Combo return types tests passed')
|
||||
|
||||
# Test sampler combo functionality
|
||||
@@ -210,7 +217,7 @@ jobs:
|
||||
# Test return types
|
||||
assert node.RETURN_TYPES == ('INT',)
|
||||
assert node.RETURN_NAMES == ('seed',)
|
||||
assert node.CATEGORY == 'ComfyAssets'
|
||||
assert node.CATEGORY == 'ComfyAssets/🌱 Seeds'
|
||||
print('✓ Seed History return types tests passed')
|
||||
|
||||
# Test seed output functionality
|
||||
@@ -326,7 +333,7 @@ jobs:
|
||||
|
||||
assert res_class.RETURN_TYPES == ('INT', 'INT')
|
||||
assert res_class.RETURN_NAMES == ('width', 'height')
|
||||
assert res_class.CATEGORY == 'ComfyAssets'
|
||||
assert res_class.CATEGORY.startswith('ComfyAssets/')
|
||||
print('✓ Resolution Calculator ComfyUI integration passed')
|
||||
|
||||
# Test Width Height Selector
|
||||
@@ -347,7 +354,7 @@ jobs:
|
||||
|
||||
assert wh_class.RETURN_TYPES == ('INT', 'INT')
|
||||
assert wh_class.RETURN_NAMES == ('width', 'height')
|
||||
assert wh_class.CATEGORY == 'ComfyAssets'
|
||||
assert wh_class.CATEGORY.startswith('ComfyAssets/')
|
||||
print('✓ Width Height Selector ComfyUI integration passed')
|
||||
|
||||
# Test Sampler Combo
|
||||
@@ -367,7 +374,7 @@ jobs:
|
||||
assert 'steps' in input_types['required']
|
||||
assert 'cfg' in input_types['required']
|
||||
|
||||
assert sampler_class.CATEGORY == 'ComfyAssets'
|
||||
assert sampler_class.CATEGORY.startswith('ComfyAssets/')
|
||||
print('✓ Sampler Combo ComfyUI integration passed')
|
||||
|
||||
# Test Seed History
|
||||
@@ -386,7 +393,7 @@ jobs:
|
||||
|
||||
assert seed_class.RETURN_TYPES == ('INT',)
|
||||
assert seed_class.RETURN_NAMES == ('seed',)
|
||||
assert seed_class.CATEGORY == 'ComfyAssets'
|
||||
assert seed_class.CATEGORY.startswith('ComfyAssets/')
|
||||
print('✓ Seed History ComfyUI integration passed')
|
||||
|
||||
print('🎉 All tools ComfyUI integration readiness tests passed!')
|
||||
@@ -398,7 +405,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
|
||||
@@ -159,3 +159,6 @@ test_images/
|
||||
test_outputs/
|
||||
experiments/
|
||||
.claude/
|
||||
|
||||
# Gemini model cache
|
||||
.gemini_models_cache.json
|
||||
|
||||
@@ -8,14 +8,14 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.10
|
||||
args: ['--line-length=127'] # Match CI configuration
|
||||
args: ['--line-length=88'] # Match CI configuration
|
||||
|
||||
# Python linting with flake8
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--max-line-length=127', '--max-complexity=10']
|
||||
args: ['--max-line-length=88', '--max-complexity=10']
|
||||
exclude: '^tests/'
|
||||
|
||||
# Python type checking with mypy
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -14,6 +14,32 @@ ComfyUI-KikoTools provides carefully crafted, production-ready nodes grouped und
|
||||
|
||||
### ✨ Current Tools
|
||||
|
||||
| Tool | Description | Category |
|
||||
|------|-------------|----------|
|
||||
| [📐 Resolution Calculator](#-resolution-calculator) | Calculate upscaled dimensions with model optimization | 🖼️ Resolution |
|
||||
| [📏 Width Height Selector](#-width-height-selector) | Preset-based dimension selection with visual swap | 🖼️ Resolution |
|
||||
| [🎲 Seed History](#-seed-history) | Advanced seed tracking with interactive history | 🌱 Seeds |
|
||||
| [⚙️ Sampler Combo](#️-sampler-combo) | Unified sampling configuration interface | 🌀 Samplers |
|
||||
| [📦 Empty Latent Batch](#-empty-latent-batch) | Create empty latent batches with preset support | 📦 Latents |
|
||||
| [💾 Kiko Save Image](#-kiko-save-image) | Enhanced image saving with popup viewer | 💾 Images |
|
||||
| [📋 Display Text](#-display-text) | Smart text display with prompt detection | 👁️ Display |
|
||||
| [🤖 Gemini Prompt Engineer](#-gemini-prompt-engineer) | AI-powered image analysis and prompt generation | 🧠 Prompts |
|
||||
| [🔍 Display Any](#-display-any) | Universal debugging tool for any data type | 👁️ Display |
|
||||
| [🖼️ Image to Multiple Of](#️-image-to-multiple-of) | Adjust dimensions to multiples for compatibility | 🖼️ Resolution |
|
||||
|
||||
### 🧰 xyz-helpers Tools
|
||||
|
||||
Advanced parameter management tools adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) (now in maintenance mode):
|
||||
|
||||
| Tool | Description | Category |
|
||||
|------|-------------|----------|
|
||||
| [🎛️ Flux Sampler Params](#️-flux-sampler-params) | FLUX-optimized parameter generator with batch support | 🧰 xyz-helpers |
|
||||
| [📁 LoRA Folder Batch](#-lora-folder-batch) | Batch process multiple LoRAs from folders | 🧰 xyz-helpers |
|
||||
| [📊 Plot Parameters](#-plot-parameters) | Visualize parameter effects with graphs | 🧰 xyz-helpers |
|
||||
| [🎯 Sampler Select Helper](#-sampler-select-helper) | Intelligent sampler selection with recommendations | 🧰 xyz-helpers |
|
||||
| [📅 Scheduler Select Helper](#-scheduler-select-helper) | Optimal scheduler selection for samplers | 🧰 xyz-helpers |
|
||||
| [✍️ Text Encode Sampler Params](#️-text-encode-sampler-params) | Combined text encoding and parameter management | 🧰 xyz-helpers |
|
||||
|
||||
#### 📐 Resolution Calculator
|
||||
Calculate upscaled dimensions from image or latent inputs with precision.
|
||||
|
||||
@@ -29,6 +55,8 @@ Calculate upscaled dimensions from image or latent inputs with precision.
|
||||
- Ensure ComfyUI tensor compatibility
|
||||
- Optimize batch processing workflows
|
||||
|
||||

|
||||
|
||||
#### 📏 Width Height Selector
|
||||
Advanced preset-based dimension selection with visual swap button.
|
||||
|
||||
@@ -60,6 +88,8 @@ Advanced seed tracking with interactive history management and UI.
|
||||
- Maintain reproducibility across sessions
|
||||
- Compare results from different seeds efficiently
|
||||
|
||||
![Seed History functionality is shown in various workflow examples]
|
||||
|
||||
#### ⚙️ Sampler Combo
|
||||
Unified sampling configuration interface combining sampler, scheduler, steps, and CFG.
|
||||
|
||||
@@ -92,6 +122,8 @@ Advanced empty latent creation with preset support and batch processing capabili
|
||||
- Optimize memory usage with batch size planning
|
||||
- Quick preset-based latent generation for different aspect ratios
|
||||
|
||||

|
||||
|
||||
#### 💾 Kiko Save Image
|
||||
Enhanced image saving with format selection, quality control, and floating popup viewer.
|
||||
|
||||
@@ -104,6 +136,28 @@ Enhanced image saving with format selection, quality control, and floating popup
|
||||
- **Smart UI**: Auto-hide/show, minimize/maximize, roll-up functionality
|
||||
- **Popup Toggle**: Enable/disable popup viewer per save operation
|
||||
|
||||

|
||||
|
||||
#### 📋 Display Text
|
||||
Advanced text display node with intelligent formatting and enhanced user interaction.
|
||||
|
||||
- **Smart Prompt Detection**: Automatically detects positive/negative prompt pairs and displays in split view
|
||||
- **Text Wrapping**: Proper word wrapping that reflows when node is resized
|
||||
- **Scrollable Content**: Mouse wheel scrolling for long texts with visual scroll indicators
|
||||
- **Copy Functionality**: Always-visible copy button with visual feedback
|
||||
- **Split View Mode**: Automatic detection and formatting of SDXL-style prompts
|
||||
- **Responsive Design**: Content adapts to node resizing with proper text reflow
|
||||
- **Clean Formatting**: Strips prompt labels when copying for direct use
|
||||
|
||||
**Use Cases:**
|
||||
- Display generated prompts with proper formatting
|
||||
- Compare positive and negative prompts side-by-side
|
||||
- Copy prompts without manual label removal
|
||||
- View long text content with proper wrapping
|
||||
- Debug prompt generation workflows
|
||||
|
||||

|
||||
|
||||
#### 🤖 Gemini Prompt Engineer
|
||||
AI-powered image analysis using Google's Gemini to generate optimized prompts for various models.
|
||||
|
||||
@@ -114,6 +168,9 @@ AI-powered image analysis using Google's Gemini to generate optimized prompts fo
|
||||
- **Flexible API Key Management**: Environment variable, config file, or direct input
|
||||
- **Visual Status Feedback**: Real-time processing indicators and error states
|
||||
- **Help Integration**: Built-in setup guide and documentation
|
||||
- **Dynamic Model Refresh**: Fetch latest Gemini models with refresh button
|
||||
- **Model Caching**: Persistent model list storage for offline access
|
||||
- **Enhanced SDXL Prompts**: Improved formatting with layered structure and quality boosters
|
||||
|
||||
**Use Cases:**
|
||||
- Reverse-engineer prompts from reference images
|
||||
@@ -121,6 +178,138 @@ AI-powered image analysis using Google's Gemini to generate optimized prompts fo
|
||||
- Generate consistent style descriptions across workflows
|
||||
- Create detailed scene breakdowns for complex compositions
|
||||
- Analyze and replicate lighting/mood from existing artwork
|
||||
- Access latest Gemini models including 2.0 and 2.5 versions
|
||||
|
||||

|
||||
|
||||
#### 🔍 Display Any
|
||||
Universal debugging node that displays any type of input value or tensor information.
|
||||
|
||||
- **Universal Input Acceptance**: Works with any data type (tensors, strings, numbers, lists, dicts)
|
||||
- **Two Display Modes**: Raw value showing string representation, or tensor shape extraction
|
||||
- **Nested Structure Support**: Finds tensors within complex nested data structures
|
||||
- **Debugging Focus**: Essential tool for understanding data flow and tensor dimensions
|
||||
- **Clean Output**: Formatted display directly in ComfyUI interface
|
||||
|
||||
**Use Cases:**
|
||||
- Debug tensor dimensions at any point in workflow
|
||||
- Inspect latent space data structures
|
||||
- View metadata and configuration objects
|
||||
- Track shape changes through processing nodes
|
||||
- Understand complex data types in ComfyUI
|
||||
|
||||

|
||||
|
||||
#### 🖼️ Image to Multiple Of
|
||||
Adjusts image dimensions to be multiples of a specified value for model compatibility.
|
||||
|
||||
- **Dimension Adjustment**: Ensures image dimensions are multiples of specified value (e.g., 64, 128)
|
||||
- **Two Processing Methods**: Center crop for minimal loss, or rescale to fit
|
||||
- **Model Compatibility**: Essential for models requiring specific dimension constraints
|
||||
- **Flexible Multiple Values**: Support from 1 to 256 with 16-step increments
|
||||
- **Preserves Quality**: Smart processing maintains image quality
|
||||
|
||||
**Use Cases:**
|
||||
- Prepare images for VAE encoding (multiple of 8 requirement)
|
||||
- Ensure compatibility with specific model architectures
|
||||
- Standardize dimensions across image batches
|
||||
- Fix dimension errors in complex workflows
|
||||
- Optimize for tiled processing requirements
|
||||
|
||||

|
||||
|
||||
#### 🎛️ Flux Sampler Params
|
||||
FLUX-optimized parameter generator with intelligent batch processing capabilities.
|
||||
|
||||
- **FLUX-Specific Tuning**: Optimized guidance, shift values, and step counts for FLUX models
|
||||
- **Batch Parameter Testing**: Generate multiple parameter sets for comparative analysis
|
||||
- **LoRA Integration**: Seamlessly combine with LoRA Folder Batch for comprehensive testing
|
||||
- **Smart Defaults**: Pre-configured optimal settings based on extensive FLUX testing
|
||||
- **Range Syntax Support**: Use `start...end+step` notation for parameter sweeps
|
||||
|
||||
**Use Cases:**
|
||||
- Test different guidance and shift value combinations
|
||||
- Batch process with varying parameters
|
||||
- Optimize FLUX generation quality
|
||||
- Integrate with LoRA testing workflows
|
||||
|
||||
#### 📁 LoRA Folder Batch
|
||||
Automated batch processing for multiple LoRA models from folders.
|
||||
|
||||
- **Automatic Scanning**: Discovers all .safetensors files in specified folders
|
||||
- **Natural Epoch Sorting**: Intelligently sorts training epochs (epoch_004, epoch_020, etc.)
|
||||
- **Pattern Filtering**: Include/exclude LoRAs using powerful regex patterns
|
||||
- **Flexible Strength Control**: Single, multiple, or range-based strength values
|
||||
- **Batch Modes**: Sequential or combinatorial strength application
|
||||
- **Epoch Detection**: Automatically extracts epoch numbers from filenames
|
||||
|
||||
**Use Cases:**
|
||||
- Test all epochs from a training run
|
||||
- Compare different LoRA versions
|
||||
- Evaluate strength variations
|
||||
- Batch process style transfers
|
||||
|
||||

|
||||
|
||||
#### 📊 Plot Parameters
|
||||
Visual analysis tool for understanding parameter relationships and effects.
|
||||
|
||||
- **Multiple Plot Types**: Line, bar, scatter, and heatmap visualizations
|
||||
- **Parameter Correlation**: Analyze relationships between settings and quality
|
||||
- **Statistical Analysis**: Calculate means, deviations, and trends
|
||||
- **Export Capabilities**: Save plots as images or CSV data
|
||||
- **Real-time Updates**: Dynamic graph generation during workflow execution
|
||||
|
||||
**Use Cases:**
|
||||
- Visualize parameter impact on quality
|
||||
- Compare batch generation results
|
||||
- Analyze optimal parameter ranges
|
||||
- Document generation experiments
|
||||
|
||||
#### 🎯 Sampler Select Helper
|
||||
Intelligent sampler selection with model-aware recommendations.
|
||||
|
||||
- **Model Detection**: Automatic identification of SDXL, SD1.5, or FLUX models
|
||||
- **Quality Presets**: Fast, balanced, quality, and extreme presets
|
||||
- **Compatibility Checking**: Ensures optimal sampler-scheduler pairs
|
||||
- **Performance Profiles**: Pre-configured settings for different use cases
|
||||
- **Dynamic Discovery**: Adapts to newly available samplers
|
||||
|
||||
**Use Cases:**
|
||||
- Automatic optimal sampler selection
|
||||
- Quick quality vs speed adjustments
|
||||
- Model-specific optimization
|
||||
- A/B testing different samplers
|
||||
|
||||
#### 📅 Scheduler Select Helper
|
||||
Optimal scheduler selection based on sampler and model requirements.
|
||||
|
||||
- **Sampler-Aware**: Recommends best schedulers for each sampler
|
||||
- **Noise Schedule Visualization**: Preview and compare schedule curves
|
||||
- **Model Optimization**: Specific tuning for SDXL, SD1.5, and FLUX
|
||||
- **Schedule Types**: Smooth, sharp, linear, and custom curves
|
||||
- **Beta Schedule Support**: Advanced control with custom beta values
|
||||
|
||||
**Use Cases:**
|
||||
- Find optimal scheduler for your sampler
|
||||
- Visualize noise reduction curves
|
||||
- Compare different schedule types
|
||||
- Fine-tune generation behavior
|
||||
|
||||
#### ✍️ Text Encode Sampler Params
|
||||
Unified interface for text encoding and sampler parameter management.
|
||||
|
||||
- **All-in-One Node**: Combine prompt encoding with sampling configuration
|
||||
- **Template System**: Pre-configured settings for portraits, landscapes, etc.
|
||||
- **Prompt Syntax Support**: Wildcards, emphasis, and alternation
|
||||
- **Batch Processing**: Handle multiple prompts efficiently
|
||||
- **Model-Aware Encoding**: Optimize for different text encoders
|
||||
|
||||
**Use Cases:**
|
||||
- Streamline text-to-image workflows
|
||||
- Apply consistent settings across prompts
|
||||
- Quick template-based generation
|
||||
- Batch prompt processing
|
||||
|
||||
### 💾 Kiko Save Image Features
|
||||
|
||||
@@ -245,18 +434,55 @@ Generate Image → Kiko Save Image → Floating Popup Viewer
|
||||
**Features:** Click images to open in new tabs, download individual files, batch selection
|
||||
**Advantages:** Immediate preview without file explorer, multi-format comparison, advanced quality controls
|
||||
|
||||
### Display Text Example
|
||||
|
||||
```
|
||||
Gemini Prompt → Display Text → Copy to Clipboard
|
||||
📋 SDXL prompt ↘ auto-split ↘ [📋 Positive] [📋 Negative]
|
||||
view → formatted display
|
||||
```
|
||||
|
||||
**Input:** Text with "Positive prompt:" and "Negative prompt:" sections
|
||||
**Output:** Split view with individual copy buttons
|
||||
**Features:** Text wrapping, scrolling, responsive resizing
|
||||
**Smart Detection:** Automatically formats SDXL-style prompts
|
||||
|
||||
### Gemini Prompt Engineer Example
|
||||
```
|
||||
Load Image → Gemini Prompt → Text Generation Model
|
||||
🖼️ reference ↘ type: FLUX ↘ "majestic landscape..."
|
||||
[API key] → FLUX model
|
||||
Load Image → Gemini Prompt → Display Text → Text Generation Model
|
||||
🖼️ reference ↘ type: SDXL ↘ split view ↘ "detailed portrait..."
|
||||
[Refresh Models] → SDXL model
|
||||
```
|
||||
|
||||
**Input:** Reference image for style analysis
|
||||
**Prompt Type:** FLUX (detailed artistic prompts)
|
||||
**Output:** Optimized prompt with style, lighting, composition details
|
||||
**Prompt Type:** SDXL (positive/negative pairs with layered structure)
|
||||
**Model Selection:** Dynamic list with latest Gemini models (2.0, 2.5)
|
||||
**Output:** Optimized prompts following community best practices
|
||||
**API:** Requires Gemini API key (free tier available)
|
||||
**Use Case:** Recreate similar style/mood from reference images
|
||||
**Refresh:** Click button to fetch latest available models
|
||||
|
||||
### Display Any Example
|
||||
```
|
||||
Any Node → Display Any → Debug Output
|
||||
🔍 tensor ↘ mode: shape ↘ "[[1, 3, 512, 512]]"
|
||||
```
|
||||
|
||||
**Input:** Any data type (image, latent, config, etc.)
|
||||
**Mode:** "raw value" or "tensor shape"
|
||||
**Output:** Formatted display of value or tensor dimensions
|
||||
**Use Case:** Debug workflows, inspect data structures
|
||||
|
||||
### Image to Multiple Of Example
|
||||
```
|
||||
Load Image → Image to Multiple Of → VAE Encode → KSampler
|
||||
🖼️ 513×769 ↘ multiple: 64 ↘ 512×768 → latent
|
||||
method: crop
|
||||
```
|
||||
|
||||
**Input:** Image with arbitrary dimensions
|
||||
**Multiple Of:** 64 (common for VAE compatibility)
|
||||
**Method:** "center crop" or "rescale"
|
||||
**Output:** Adjusted image with compatible dimensions
|
||||
|
||||
### Common Workflows
|
||||
|
||||
@@ -288,6 +514,21 @@ Load Image → Gemini Prompt → Text Generation Model
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>LoRA Testing with xyz-helpers</b></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"workflow": "Scan LoRA folder → Apply strength ranges → Generate grid → Plot parameters",
|
||||
"strength_range": "0.9...1.2+0.1",
|
||||
"batch_mode": "combinatorial",
|
||||
"features": ["automatic epoch sorting", "parameter visualization", "batch generation"]
|
||||
}
|
||||
```
|
||||
|
||||
Example workflow available: [xyz_helpers_lora_testing.json](examples/workflows/xyz_helpers_lora_testing.json)
|
||||
</details>
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### Available Tools
|
||||
@@ -300,6 +541,16 @@ Load Image → Gemini Prompt → Text Generation Model
|
||||
| **Sampler Combo** | Unified sampling configuration with smart recommendations | ✅ Complete | [Docs](examples/documentation/sampler_combo.md) |
|
||||
| **Empty Latent Batch** | Create empty latent batches with preset support | ✅ Complete | [Docs](examples/documentation/empty_latent_batch.md) |
|
||||
| **Kiko Save Image** | Enhanced image saving with popup viewer and multi-format support | ✅ Complete | [Docs](examples/documentation/kiko_save_image.md) |
|
||||
| **Display Text** | Advanced text display with smart prompt detection and split view | ✅ Complete | [Docs](examples/documentation/display_text.md) |
|
||||
| **Gemini Prompt Engineer** | AI-powered image analysis with dynamic model refresh | ✅ Complete | [Docs](examples/documentation/gemini_prompt.md) |
|
||||
| **Display Any** | Universal debugging tool for any data type or tensor shapes | ✅ Complete | [Docs](examples/documentation/display_any.md) |
|
||||
| **Image to Multiple Of** | Adjust image dimensions to multiples for model compatibility | ✅ Complete | [Docs](examples/documentation/image_to_multiple_of.md) |
|
||||
| **Flux Sampler Params** | FLUX-optimized parameter generator with batch support | ✅ Complete | [Docs](examples/documentation/flux_sampler_params.md) |
|
||||
| **LoRA Folder Batch** | Batch process multiple LoRAs from folders | ✅ Complete | [Docs](examples/documentation/lora_folder_batch.md) |
|
||||
| **Plot Parameters** | Visualize parameter effects with graphs | ✅ Complete | [Docs](examples/documentation/plot_parameters.md) |
|
||||
| **Sampler Select Helper** | Intelligent sampler selection with recommendations | ✅ Complete | [Docs](examples/documentation/sampler_select_helper.md) |
|
||||
| **Scheduler Select Helper** | Optimal scheduler selection for samplers | ✅ Complete | [Docs](examples/documentation/scheduler_select_helper.md) |
|
||||
| **Text Encode Sampler Params** | Combined text encoding and parameter management | ✅ Complete | [Docs](examples/documentation/text_encode_sampler_params.md) |
|
||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||
|
||||
@@ -593,14 +844,31 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
- **Nodes**: 6 (Resolution Calculator, Width Height Selector, Seed History, Sampler Combo, Empty Latent Batch, Kiko Save Image)
|
||||
- **Nodes**: 16 (10 core tools + 6 xyz-helpers)
|
||||
- **Categories**: 8 emoji-based categories for better organization
|
||||
- **Format Support**: 3 (PNG, JPEG, WebP with advanced controls)
|
||||
- **Presets**: 26 curated resolution presets
|
||||
- **Interactive Features**: 4 (Width/Height Swap Button, Seed History UI, Empty Latent Batch Swap Button, Kiko Save Image Popup Viewer)
|
||||
- **Test Coverage**: 100% (200+ comprehensive tests)
|
||||
- **Interactive Features**: 8+ (swap buttons, history UI, popup viewers, parameter visualization)
|
||||
- **AI Integration**: Gemini API with 40+ model support
|
||||
- **Test Coverage**: 100% (300+ comprehensive tests)
|
||||
- **Python Version**: 3.8+
|
||||
- **ComfyUI Compatibility**: Latest
|
||||
- **Dependencies**: Minimal (PyTorch, NumPy, Pillow)
|
||||
- **Dependencies**: Minimal (PyTorch, NumPy, Pillow, google-generativeai for Gemini)
|
||||
|
||||
## 🙏 Attribution
|
||||
|
||||
### xyz-helpers Tools
|
||||
The xyz-helpers collection was adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted these essential tools to ensure continued support and compatibility with modern ComfyUI workflows. We're grateful for cubiq's original work and contributions to the ComfyUI community.
|
||||
|
||||
The following tools are based on comfyui-essentials-nodes:
|
||||
- Flux Sampler Params
|
||||
- LoRA Folder Batch
|
||||
- Plot Parameters
|
||||
- Sampler Select Helper
|
||||
- Scheduler Select Helper
|
||||
- Text Encode Sampler Params
|
||||
|
||||
All adaptations maintain compatibility while adding new features and optimizations for the ComfyAssets ecosystem.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
ComfyUI-KikoTools is actively maintained. We provide security updates for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
| < 1.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take the security of ComfyUI-KikoTools seriously. If you believe you have found a security vulnerability, please report it to us as described below.
|
||||
|
||||
### How to Report
|
||||
|
||||
Please report security vulnerabilities by [opening a new issue](https://github.com/ComfyAssets/ComfyUI-KikoTools/issues/new) with the following:
|
||||
|
||||
- Use the title prefix `[SECURITY]`
|
||||
- Provide a clear description of the vulnerability
|
||||
- Include steps to reproduce the issue
|
||||
- Specify the version(s) affected
|
||||
- If possible, suggest a fix or mitigation
|
||||
|
||||
### What to Expect
|
||||
|
||||
- **Response Time**: We aim to acknowledge receipt within 48 hours
|
||||
- **Investigation**: We will investigate and validate the reported vulnerability
|
||||
- **Updates**: We will keep you informed about the progress
|
||||
- **Resolution**: Once verified, we will work on a fix and release it as soon as possible
|
||||
- **Credit**: We will acknowledge your contribution in the release notes (unless you prefer to remain anonymous)
|
||||
|
||||
### Scope
|
||||
|
||||
Security vulnerabilities in scope include:
|
||||
|
||||
- Code execution vulnerabilities in node implementations
|
||||
- Path traversal or file system access issues
|
||||
- API key or credential exposure
|
||||
- Dependency vulnerabilities that affect the project
|
||||
- Any issue that could compromise user data or system security
|
||||
|
||||
### Out of Scope
|
||||
|
||||
The following are generally not considered security vulnerabilities:
|
||||
|
||||
- Issues in ComfyUI core (report these to the ComfyUI project)
|
||||
- Performance issues
|
||||
- Bugs that don't have security implications
|
||||
- Feature requests
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
When using ComfyUI-KikoTools:
|
||||
|
||||
- Keep your installation up to date
|
||||
- Store API keys (like Gemini API keys) securely using environment variables
|
||||
- Review generated files before sharing them
|
||||
- Be cautious with custom prompts that might expose sensitive information
|
||||
|
||||
## Contact
|
||||
|
||||
For urgent security matters, you can also reach out to the maintainers directly through GitHub.
|
||||
|
||||
Thank you for helping keep ComfyUI-KikoTools secure!
|
||||
@@ -226,9 +226,11 @@ class HFHubLoraLoader:
|
||||
|
||||
lora_path = hf_hub_download(
|
||||
repo_id=repo_id.strip(),
|
||||
subfolder=None
|
||||
if subfolder is None or subfolder.strip() == ""
|
||||
else subfolder.strip(),
|
||||
subfolder=(
|
||||
None
|
||||
if subfolder is None or subfolder.strip() == ""
|
||||
else subfolder.strip()
|
||||
),
|
||||
filename=filename.strip(),
|
||||
cache_dir=find_or_create_cache(),
|
||||
)
|
||||
@@ -281,9 +283,11 @@ class HFHubEmbeddingLoader:
|
||||
):
|
||||
hf_hub_download(
|
||||
repo_id=repo_id.strip(),
|
||||
subfolder=None
|
||||
if subfolder is None or subfolder.strip() == ""
|
||||
else subfolder.strip(),
|
||||
subfolder=(
|
||||
None
|
||||
if subfolder is None or subfolder.strip() == ""
|
||||
else subfolder.strip()
|
||||
),
|
||||
filename=filename.strip(),
|
||||
local_dir=get_folder_paths("embeddings")[0],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Display Any
|
||||
|
||||
The Display Any node is a debugging and inspection tool that can display any type of input value in ComfyUI. It's particularly useful for understanding data structures and tensor shapes during workflow development.
|
||||
|
||||
## Features
|
||||
|
||||
- **Universal Input**: Accepts any type of input data (tensors, strings, numbers, lists, dictionaries, etc.)
|
||||
- **Two Display Modes**:
|
||||
- **Raw Value**: Shows the string representation of the input
|
||||
- **Tensor Shape**: Extracts and displays the shapes of any tensors found in the input
|
||||
- **Nested Structure Support**: Can find tensors within nested dictionaries and lists
|
||||
- **UI Output**: Displays results directly in the ComfyUI interface
|
||||
|
||||
## Inputs
|
||||
|
||||
- **input** (*): Any value you want to display or inspect
|
||||
- **mode** (DROPDOWN): Display mode selection
|
||||
- `raw value`: Shows the complete string representation of the input
|
||||
- `tensor shape`: Extracts and shows shapes of any tensors in the input
|
||||
|
||||
## Outputs
|
||||
|
||||
- **display_text** (STRING): The formatted display text
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Display Simple Values
|
||||
|
||||
Connect any output to see its raw value:
|
||||
```
|
||||
String Input: "Hello, ComfyUI!"
|
||||
Mode: raw value
|
||||
Output: "Hello, ComfyUI!"
|
||||
```
|
||||
|
||||
### 2. Inspect Tensor Shapes
|
||||
|
||||
Great for debugging image processing pipelines:
|
||||
```
|
||||
Image Tensor: [1, 3, 512, 512]
|
||||
Mode: tensor shape
|
||||
Output: "[[1, 3, 512, 512]]"
|
||||
```
|
||||
|
||||
### 3. Debug Complex Data Structures
|
||||
|
||||
View nested data structures with multiple tensors:
|
||||
```python
|
||||
Input: {
|
||||
"images": tensor([1, 3, 256, 256]),
|
||||
"masks": [tensor([256, 256]), tensor([256, 256, 1])],
|
||||
"config": {"steps": 20}
|
||||
}
|
||||
Mode: tensor shape
|
||||
Output: "[[1, 3, 256, 256], [256, 256], [256, 256, 1]]"
|
||||
```
|
||||
|
||||
### 4. Workflow Debugging
|
||||
|
||||
Use Display Any nodes at various points in your workflow to understand data flow:
|
||||
- After loading images to verify dimensions
|
||||
- Before/after processing nodes to track shape changes
|
||||
- To inspect conditioning or latent data structures
|
||||
- To view metadata or configuration dictionaries
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Image Pipeline Debugging
|
||||
Place Display Any nodes after image loading and processing nodes to track dimension changes:
|
||||
```
|
||||
Load Image → Display Any (tensor shape) → Resize → Display Any (tensor shape)
|
||||
```
|
||||
|
||||
### Latent Space Inspection
|
||||
Understand latent dimensions in your workflows:
|
||||
```
|
||||
VAE Encode → Display Any (tensor shape) → KSampler → Display Any (raw value)
|
||||
```
|
||||
|
||||
### Configuration Verification
|
||||
Display complex configuration objects to ensure correct settings:
|
||||
```
|
||||
Config Node → Display Any (raw value) → Processing Node
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Multiple Display Nodes**: You can use multiple Display Any nodes in a single workflow to track data at different stages
|
||||
|
||||
2. **Tensor Shape Mode**: Particularly useful when working with:
|
||||
- Image batches to verify batch size
|
||||
- Latent tensors to understand dimensions
|
||||
- Mask arrays to check compatibility
|
||||
|
||||
3. **Raw Value Mode**: Best for:
|
||||
- String prompts and text
|
||||
- Configuration dictionaries
|
||||
- Debugging node outputs
|
||||
- Understanding data structure
|
||||
|
||||
4. **No Tensors Found**: If you see "No tensors found in input" in tensor shape mode, the input doesn't contain any tensor-like objects (numpy arrays, torch tensors, etc.)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- The node uses `str()` for raw value display, providing Python's string representation
|
||||
- Tensor shape detection works with any object that has a `shape` attribute
|
||||
- Nested structure traversal supports dictionaries, lists, and tuples
|
||||
- The output is both displayed in the UI and available as a string output for further processing
|
||||
|
||||
## Example Workflow Integration
|
||||
|
||||
```
|
||||
[Load Image] → [Image Processing] → [Display Any (tensor shape)]
|
||||
↓
|
||||
"[[1, 3, 512, 512]]"
|
||||
↓
|
||||
[Text Multiline] ← [Concatenate] ← "Image dimensions: "
|
||||
```
|
||||
|
||||
This creates a text output showing the current image dimensions that can be used elsewhere in your workflow.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Display Text
|
||||
|
||||
The Display Text node provides advanced text display capabilities with smart formatting, interactive features, and responsive design for ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Prompt Detection**: Automatically detects and formats SDXL-style positive/negative prompt pairs
|
||||
- **Text Wrapping**: Proper word wrapping that reflows when node is resized
|
||||
- **Scrollable Content**: Mouse wheel scrolling for long texts with visual indicators
|
||||
- **Copy Functionality**: Always-visible copy button with visual feedback
|
||||
- **Split View Mode**: Side-by-side display for prompt pairs
|
||||
- **Responsive Design**: Content adapts to node resizing
|
||||
|
||||
## Inputs
|
||||
|
||||
- **text** (STRING): The text to display
|
||||
- Can be a single text block
|
||||
- Can contain "Positive prompt:" and "Negative prompt:" sections for automatic split view
|
||||
|
||||
## Outputs
|
||||
|
||||
- **text** (STRING): Pass-through of the input text
|
||||
|
||||
## Display Modes
|
||||
|
||||
### Single Text Mode
|
||||
|
||||
When the input is regular text without prompt markers, it displays as a single scrollable text area with:
|
||||
- Word wrapping at word boundaries
|
||||
- Vertical scrolling for long content
|
||||
- Single copy button for the entire text
|
||||
|
||||
### Split View Mode
|
||||
|
||||
Automatically activated when text contains both "Positive prompt:" and "Negative prompt:" sections:
|
||||
- Side-by-side display with 50/50 split
|
||||
- Independent scrolling for each section
|
||||
- Separate copy buttons for each prompt
|
||||
- Labels are stripped when copying (clean prompts)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Display Generated Prompts
|
||||
|
||||
```
|
||||
Gemini Prompt → Display Text → Copy to workflow
|
||||
```
|
||||
The node automatically detects SDXL format and shows positive/negative prompts side-by-side.
|
||||
|
||||
### 2. Debug Text Processing
|
||||
|
||||
```
|
||||
Text Processing → Display Text → Further Processing
|
||||
```
|
||||
View intermediate text processing results with proper formatting.
|
||||
|
||||
### 3. Show Long Descriptions
|
||||
|
||||
```
|
||||
Load Text → Display Text → Review
|
||||
```
|
||||
Display long text content with scrolling and word wrapping.
|
||||
|
||||
## Interactive Features
|
||||
|
||||
### Copy Button
|
||||
- Always visible in the top-right corner
|
||||
- Shows "✓ Copied!" feedback on click
|
||||
- In split view: separate buttons for each section
|
||||
- Strips prompt labels for clean copying
|
||||
|
||||
### Scrolling
|
||||
- Mouse wheel scrolling when hovering over text
|
||||
- Visual indicators appear when content is scrollable
|
||||
- Smooth scrolling with proper boundaries
|
||||
- Independent scrolling in split view mode
|
||||
|
||||
### Resizing
|
||||
- Text reflows when node width changes
|
||||
- Maintains readability at different sizes
|
||||
- Split view maintains 50/50 proportions
|
||||
- Minimum height ensures usability
|
||||
|
||||
## Smart Prompt Detection
|
||||
|
||||
The node intelligently detects prompt formats:
|
||||
|
||||
1. **SDXL Format**:
|
||||
- Looks for "Positive prompt:" and "Negative prompt:" markers
|
||||
- Case-insensitive detection
|
||||
- Handles various formatting styles
|
||||
|
||||
2. **Label Stripping**:
|
||||
- When copying from split view, labels are removed
|
||||
- "Positive prompt: beautiful sunset" → "beautiful sunset"
|
||||
- Clean prompts ready for direct use
|
||||
|
||||
## Styling
|
||||
|
||||
- **Font**: Monospace for consistent alignment
|
||||
- **Colors**:
|
||||
- Text: Light gray (#ddd) on dark background
|
||||
- Background: Semi-transparent dark (#1a1a1a)
|
||||
- Borders: Subtle gray (#333)
|
||||
- **Spacing**: Comfortable padding and line height
|
||||
- **Visual Feedback**: Hover effects on interactive elements
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Prompt Engineering Workflows
|
||||
- Display AI-generated prompts with proper formatting
|
||||
- Compare positive and negative prompts side-by-side
|
||||
- Copy refined prompts without manual cleanup
|
||||
|
||||
### Text Processing Pipelines
|
||||
- Debug text transformations at each step
|
||||
- View formatted outputs from text nodes
|
||||
- Monitor prompt construction workflows
|
||||
|
||||
### Documentation and Notes
|
||||
- Display workflow instructions
|
||||
- Show generation parameters
|
||||
- Present formatted metadata
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Text Processing**: Preserves original text while adding display formatting
|
||||
- **Responsive Design**: CSS-based layout adapts to node dimensions
|
||||
- **Event Handling**: Proper event propagation for ComfyUI compatibility
|
||||
- **Memory Efficient**: Only renders visible text portions
|
||||
|
||||
## Tips
|
||||
|
||||
1. **For Long Prompts**: The scrolling feature handles texts of any length efficiently
|
||||
2. **Quick Copy**: Use the copy buttons to quickly grab prompts for other nodes
|
||||
3. **Resizing**: Drag node edges to find optimal display width for your content
|
||||
4. **Split View**: Works best with SDXL-format prompts but handles any dual-section text
|
||||
|
||||
## Integration Example
|
||||
|
||||
```
|
||||
[Gemini Prompt Engineer] → [Display Text] → [Copy Button Click]
|
||||
↓ ↓ ↓
|
||||
SDXL Format Split View Display Clean Prompts
|
||||
```
|
||||
|
||||
This creates a seamless workflow from prompt generation to usage, with the Display Text node providing the visual interface for review and interaction.
|
||||
@@ -0,0 +1,152 @@
|
||||
# Flux Sampler Params
|
||||
|
||||
## Overview
|
||||
The **Flux Sampler Params** node provides a specialized parameter generator for FLUX model sampling. This tool was adapted from the excellent [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) project (now in maintenance mode) and enhanced for the ComfyAssets ecosystem.
|
||||
|
||||
## Attribution
|
||||
This node is based on work from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted and enhanced these tools to ensure continued support and compatibility with modern ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
- **FLUX-Optimized Parameters**: Specifically tuned for FLUX model requirements
|
||||
- **Batch Processing Support**: Generate multiple parameter sets for comparative testing
|
||||
- **Interactive UI Elements**: Visual controls for quick parameter adjustments
|
||||
- **Smart Defaults**: Pre-configured optimal settings for FLUX workflows
|
||||
- **Comprehensive Parameter Control**: Fine-tune all aspects of FLUX sampling
|
||||
|
||||
## Node Properties
|
||||
- **Category**: `ComfyAssets/🧰 xyz-helpers`
|
||||
- **Node Name**: `FluxSamplerParams`
|
||||
- **Function**: `get_value`
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
| Parameter | Type | Default | Range | Description |
|
||||
|-----------|------|---------|-------|-------------|
|
||||
| `scheduler` | DROPDOWN | normal | [normal, simple, sgm_uniform] | Scheduler algorithm for sampling |
|
||||
| `steps` | INT | 20 | 1-100 | Number of sampling steps |
|
||||
| `guidance` | FLOAT | 3.5 | 0.0-100.0 | Guidance scale for conditioning |
|
||||
| `max_shift` | FLOAT | 1.0 | 0.0-100.0 | Maximum shift value for FLUX |
|
||||
| `base_shift` | FLOAT | 0.5 | 0.0-100.0 | Base shift value for FLUX |
|
||||
| `denoise` | FLOAT | 1.0 | 0.0-1.0 | Denoising strength |
|
||||
| `batch_mode` | DROPDOWN | single | [single, batch] | Single value or batch processing |
|
||||
|
||||
### Optional
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `batch_count` | INT | 1 | Number of batch variations (1-100) |
|
||||
| `batch_seed_mode` | DROPDOWN | incremental | Seed generation mode for batches |
|
||||
| `variation_seed` | INT | None | Optional seed for variations |
|
||||
| `lora_params` | LORA_PARAMS | None | LoRA parameters from LoRAFolderBatch |
|
||||
|
||||
## Outputs
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `sampler_params` | SAMPLER_PARAMS | Complete FLUX sampling parameters |
|
||||
| `scheduler` | STRING | Selected scheduler algorithm |
|
||||
| `steps` | INT | Number of sampling steps |
|
||||
| `guidance` | FLOAT | Guidance scale value |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic FLUX Sampling
|
||||
```
|
||||
FluxSamplerParams → KSampler → VAE Decode → Save Image
|
||||
scheduler: normal
|
||||
steps: 20
|
||||
guidance: 3.5
|
||||
```
|
||||
|
||||
### Batch Parameter Testing
|
||||
```
|
||||
FluxSamplerParams → KSampler → Image Grid → Save
|
||||
batch_mode: batch
|
||||
batch_count: 5
|
||||
guidance: 2.0...5.0
|
||||
```
|
||||
|
||||
### With LoRA Integration
|
||||
```
|
||||
LoRAFolderBatch → FluxSamplerParams → KSampler
|
||||
↓ ↓
|
||||
lora_params → Combined parameters
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### FLUX-Specific Settings
|
||||
- **Guidance**: FLUX typically works best with lower guidance (2.0-5.0)
|
||||
- **Steps**: 15-25 steps usually sufficient for FLUX
|
||||
- **Scheduler**: `normal` or `sgm_uniform` recommended for FLUX
|
||||
- **Shift Values**: Adjust for different quality/speed tradeoffs
|
||||
|
||||
### Batch Testing Workflow
|
||||
1. Set `batch_mode` to `batch`
|
||||
2. Configure parameter ranges using `...` syntax
|
||||
3. Set appropriate `batch_count`
|
||||
4. Use with image grid nodes for comparison
|
||||
|
||||
### Memory Optimization
|
||||
- Start with smaller batch counts for testing
|
||||
- Monitor VRAM usage with high batch counts
|
||||
- Use incremental seed mode for reproducibility
|
||||
|
||||
## Integration with Other Nodes
|
||||
|
||||
### Works Well With
|
||||
- **LoRA Folder Batch**: Combine multiple LoRAs with FLUX parameters
|
||||
- **Plot Parameters**: Visualize parameter effects
|
||||
- **Sampler Select Helper**: Dynamic sampler selection
|
||||
- **Text Encode Sampler Params**: Add text conditioning
|
||||
|
||||
### Common Workflows
|
||||
1. **Parameter Sweep**: Test multiple guidance/step combinations
|
||||
2. **LoRA Testing**: Evaluate different LoRA strengths with FLUX
|
||||
3. **Quality Comparison**: Compare different shift values
|
||||
4. **Seed Exploration**: Generate variations with controlled seeds
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Optimal FLUX Settings
|
||||
```python
|
||||
# High Quality (Slower)
|
||||
scheduler: "sgm_uniform"
|
||||
steps: 25
|
||||
guidance: 3.5
|
||||
max_shift: 1.0
|
||||
base_shift: 0.5
|
||||
|
||||
# Fast Preview
|
||||
scheduler: "simple"
|
||||
steps: 12
|
||||
guidance: 2.5
|
||||
max_shift: 0.8
|
||||
base_shift: 0.4
|
||||
```
|
||||
|
||||
### Batch Parameter Ranges
|
||||
- Steps: `15...25+5` (test 15, 20, 25)
|
||||
- Guidance: `2.0...5.0+0.5` (test 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0)
|
||||
- Denoise: `0.8...1.0+0.1` (test 0.8, 0.9, 1.0)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Out of Memory**: Reduce batch_count or image resolution
|
||||
2. **Poor Quality**: Increase steps or adjust guidance
|
||||
3. **Artifacts**: Check shift values aren't too high
|
||||
4. **Slow Generation**: Use `simple` scheduler for previews
|
||||
|
||||
### Parameter Guidelines
|
||||
- Don't set guidance too high (>10) for FLUX
|
||||
- Keep denoise at 1.0 for initial generation
|
||||
- Adjust shift values gradually for best results
|
||||
|
||||
## Version History
|
||||
- **1.0.0**: Initial adaptation from comfyui-essentials-nodes
|
||||
- **1.0.1**: Added batch processing support
|
||||
- **1.0.2**: Enhanced FLUX-specific optimizations
|
||||
- **1.0.3**: Improved UI elements and parameter validation
|
||||
|
||||
## Credits
|
||||
Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team.
|
||||
@@ -0,0 +1,209 @@
|
||||
# Gemini Prompt Engineer
|
||||
|
||||
The Gemini Prompt Engineer node uses Google's Gemini AI to analyze images and generate optimized prompts for various AI image generation models.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Model Support**: Generate prompts optimized for FLUX, SDXL, Danbooru, and Video generation
|
||||
- **Custom Prompts**: Override templates with your own system prompts
|
||||
- **Visual Feedback**: UI shows processing status and error states
|
||||
- **Flexible API Key Management**: Multiple ways to provide API credentials
|
||||
- **Dynamic Model Selection**: Fetch and use latest Gemini models with refresh button
|
||||
- **Model Caching**: Persistent storage of available models for offline access
|
||||
- **Help Integration**: Built-in setup guide accessible via help button
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Get API Key
|
||||
|
||||
Get your free Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install google-generativeai
|
||||
```
|
||||
|
||||
### 3. Configure API Key
|
||||
|
||||
Choose one of these methods:
|
||||
|
||||
1. **Environment Variable** (Recommended):
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
2. **Config File**:
|
||||
Create `gemini_config.json` in your ComfyUI root directory:
|
||||
```json
|
||||
{
|
||||
"api_key": "your-api-key-here"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Node Input**:
|
||||
Enter the API key directly in the node's `api_key` field
|
||||
|
||||
## Inputs
|
||||
|
||||
- **image** (IMAGE): The image to analyze
|
||||
- **prompt_type** (DROPDOWN): Type of prompt to generate
|
||||
- `flux`: Detailed artistic prompts with quality markers
|
||||
- `sdxl`: Positive/negative prompt pairs with weight emphasis
|
||||
- `danbooru`: Anime-style booru tags with underscores
|
||||
- `video`: Motion and temporal descriptions for video generation
|
||||
- **model** (DROPDOWN): Gemini model selection
|
||||
- Dynamically populated list of available models
|
||||
- Includes latest models like gemini-2.0-flash-exp
|
||||
- Click refresh button to update model list
|
||||
- **api_key** (STRING, optional): Gemini API key if not set elsewhere
|
||||
- **custom_prompt** (STRING, optional): Override template with custom system prompt
|
||||
|
||||
## Outputs
|
||||
|
||||
- **prompt** (STRING): Generated prompt text
|
||||
- **negative_prompt** (STRING): Negative prompt (only populated for SDXL format)
|
||||
|
||||
## Prompt Type Details
|
||||
|
||||
### FLUX Format
|
||||
Generates detailed prompts optimized for FLUX models:
|
||||
- Starts with main subject and action
|
||||
- Includes style and medium descriptors
|
||||
- Adds lighting and atmosphere details
|
||||
- Uses quality markers like "4K", "highly detailed", "award-winning"
|
||||
|
||||
Example output:
|
||||
```
|
||||
majestic mountain landscape at golden hour, oil painting style, dramatic lighting with sun rays piercing through clouds, wide angle composition, warm color palette with orange and purple hues, highly detailed, 4K resolution, trending on ArtStation, photorealistic rendering
|
||||
```
|
||||
|
||||
### SDXL Format
|
||||
Generates positive and negative prompt pairs with enhanced structure:
|
||||
- Layered positive prompts: main subject → style → composition → technical
|
||||
- Comprehensive negative prompts to avoid common issues
|
||||
- Uses parentheses for emphasis: `(detailed eyes:1.2)`
|
||||
- Includes quality boosters and technical specifications
|
||||
|
||||
Example output:
|
||||
```
|
||||
Positive prompt:
|
||||
beautiful woman with flowing red hair, elegant pose, (detailed eyes:1.2), serene expression
|
||||
oil painting style, renaissance art influence, classical portraiture
|
||||
golden hour lighting, warm color palette, soft shadows, dramatic chiaroscuro
|
||||
centered composition, rule of thirds, shallow depth of field, bokeh background
|
||||
masterpiece, best quality, highly detailed, 8k uhd, professional artwork
|
||||
|
||||
Negative prompt:
|
||||
low quality, worst quality, blurry, out of focus, pixelated, low resolution
|
||||
bad anatomy, deformed features, extra limbs, missing limbs, disconnected limbs
|
||||
poorly drawn face, poorly drawn hands, amateur drawing, bad proportions
|
||||
oversaturated, overexposed, underexposed, bad lighting, harsh shadows
|
||||
jpeg artifacts, watermark, signature, text, cropped, duplicate
|
||||
```
|
||||
|
||||
### Danbooru Format
|
||||
Generates booru-style tags for anime artwork:
|
||||
- Uses underscores for multi-word concepts
|
||||
- Includes character count descriptors (1girl, 2boys)
|
||||
- Orders tags from most to least important
|
||||
|
||||
Example output:
|
||||
```
|
||||
1girl, solo, long_hair, blue_eyes, blonde_hair, school_uniform, serafuku, pleated_skirt, thighhighs, smile, looking_at_viewer, classroom, sitting, desk, window, sunlight, highres, masterpiece
|
||||
```
|
||||
|
||||
### Video Format
|
||||
Generates prompts for video generation models:
|
||||
- Describes motion and camera movements
|
||||
- Includes temporal markers and transitions
|
||||
- Specifies technical details like fps and duration
|
||||
|
||||
Example output:
|
||||
```
|
||||
Aerial shot slowly descending toward a misty forest at dawn, camera smoothly transitions to tracking shot following a deer through the trees, photorealistic style, soft golden hour lighting with fog, 10 second duration, 4K resolution 24fps, ending with close-up of deer looking at camera
|
||||
```
|
||||
|
||||
## Custom System Prompts
|
||||
|
||||
You can override any template by providing your own system prompt. This is useful for:
|
||||
- Specialized use cases
|
||||
- Different language outputs
|
||||
- Custom formatting requirements
|
||||
- Integration with specific workflows
|
||||
|
||||
Example custom prompt:
|
||||
```
|
||||
You are an expert at analyzing images and creating simple, concise descriptions.
|
||||
Focus only on the main subject and primary colors.
|
||||
Keep your response under 50 words.
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The node provides clear error messages for common issues:
|
||||
- Missing API key
|
||||
- API request failures
|
||||
- Invalid image inputs
|
||||
- Rate limiting
|
||||
|
||||
Errors are displayed in the prompt output for easy debugging.
|
||||
|
||||
## Model Selection
|
||||
|
||||
### Dynamic Model List
|
||||
- Click the refresh button (🔄) next to the model dropdown to fetch latest models
|
||||
- Models are fetched from Google's API and include all available versions
|
||||
- Common models include:
|
||||
- `gemini-2.0-flash-exp`: Latest experimental flash model
|
||||
- `gemini-1.5-pro`: Advanced model with larger context
|
||||
- `gemini-1.5-flash`: Fast and efficient for most tasks
|
||||
|
||||
### Model Caching
|
||||
- Available models are cached locally for offline access
|
||||
- Cache persists across ComfyUI sessions
|
||||
- Refresh button updates the cache with latest models
|
||||
|
||||
## UI Features
|
||||
|
||||
### Help Button
|
||||
- Click the help button (?) for quick setup instructions
|
||||
- Shows API key setup methods
|
||||
- Links to Google AI Studio for key generation
|
||||
|
||||
### Status Indicators
|
||||
- Processing spinner during API calls
|
||||
- Error messages displayed in red
|
||||
- Success feedback when prompt is generated
|
||||
|
||||
## Tips
|
||||
|
||||
1. **API Usage**: Gemini has generous free tier limits, but be mindful of rate limits
|
||||
2. **Image Quality**: Higher resolution images provide better analysis results
|
||||
3. **Prompt Refinement**: You can chain multiple Gemini nodes with different custom prompts
|
||||
4. **Caching**: Results are not cached, so identical images will make new API calls
|
||||
5. **Model Selection**: Use flash models for faster responses, pro models for complex analysis
|
||||
|
||||
## Example Workflow
|
||||
|
||||
1. Load an image using Load Image node
|
||||
2. Connect to Gemini Prompt Engineer
|
||||
3. Select appropriate prompt_type for your target model
|
||||
4. Connect prompt output to your generation model
|
||||
5. For SDXL, connect both prompt and negative_prompt outputs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"API key not found" error**:
|
||||
- Check environment variable is set correctly
|
||||
- Verify config file path and JSON format
|
||||
- Try entering key directly in node
|
||||
|
||||
**"No response generated" error**:
|
||||
- Check internet connection
|
||||
- Verify API key is valid
|
||||
- Image might be too large (resize if needed)
|
||||
|
||||
**Import error for google-generativeai**:
|
||||
- Run `pip install google-generativeai` in your ComfyUI environment
|
||||
- Restart ComfyUI after installation
|
||||
@@ -0,0 +1,213 @@
|
||||
# Kiko Save Image
|
||||
|
||||
Enhanced image saving node with multiple format support, quality controls, and an interactive floating popup viewer for ComfyUI.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Format Support**: Save as PNG, JPEG, or WebP with format-specific optimizations
|
||||
- **Advanced Quality Controls**: Fine-tune compression settings per format
|
||||
- **Floating Popup Viewer**: Interactive window showing saved images immediately
|
||||
- **Batch Operations**: Multi-select images for bulk actions
|
||||
- **File Size Display**: Real-time feedback on compression effectiveness
|
||||
- **Smart UI**: Auto-hide, draggable, resizable popup window
|
||||
|
||||
## Inputs
|
||||
|
||||
- **images** (IMAGE): Batch of images to save
|
||||
- **filename_prefix** (STRING): Prefix for saved filenames
|
||||
- Default: "KikoSave"
|
||||
- Supports subfolder paths (e.g., "outputs/renders/final")
|
||||
- **format** (DROPDOWN): Output format selection
|
||||
- `PNG`: Lossless compression, best quality
|
||||
- `JPEG`: Lossy compression, smaller files
|
||||
- `WEBP`: Modern format, best compression ratio
|
||||
- **quality** (INT): JPEG/WebP quality level
|
||||
- Range: 1-100 (default: 90)
|
||||
- Higher values = better quality, larger files
|
||||
- **png_compress_level** (INT): PNG compression level
|
||||
- Range: 0-9 (default: 4)
|
||||
- Higher values = smaller files, slower saving
|
||||
- **webp_lossless** (BOOLEAN): Use lossless WebP compression
|
||||
- Default: False (lossy)
|
||||
- True: Lossless compression like PNG
|
||||
- **popup** (BOOLEAN): Enable popup viewer window
|
||||
- Default: True
|
||||
- Toggle per save operation
|
||||
|
||||
## Outputs
|
||||
|
||||
- **UI**: Enhanced preview data with interactive popup viewer
|
||||
|
||||
## Popup Viewer Features
|
||||
|
||||
### Window Controls
|
||||
- **Drag Handle**: Click and drag the header to move window
|
||||
- **Minimize Button**: Collapse to title bar only
|
||||
- **Maximize Button**: Expand to larger viewing size
|
||||
- **Roll-up Button**: Show/hide content area
|
||||
- **Close Button**: Hide the popup (can reopen with toggle)
|
||||
|
||||
### Image Grid
|
||||
- **Thumbnails**: Click any image to open full-size in new tab
|
||||
- **File Info**: Shows filename and size for each image
|
||||
- **Quality Indicators**:
|
||||
- PNG: Compression level (0-9)
|
||||
- JPEG/WebP: Quality percentage
|
||||
- **Batch Selection**: Checkboxes for multi-select operations
|
||||
|
||||
### Bulk Actions
|
||||
- **Open All Selected**: Opens selected images in new tabs
|
||||
- **Download All Selected**: Downloads selected images as a batch
|
||||
- **Individual Downloads**: Download button per image
|
||||
|
||||
### Smart Behavior
|
||||
- **Auto-positioning**: Appears in convenient screen location
|
||||
- **Persistence**: Stays open across multiple saves
|
||||
- **Auto-hide**: Can be minimized when not needed
|
||||
- **Responsive**: Adapts to different image counts
|
||||
|
||||
## Format Details
|
||||
|
||||
### PNG Format
|
||||
- **Pros**: Lossless quality, transparency support, wide compatibility
|
||||
- **Cons**: Larger file sizes
|
||||
- **Best for**: Final outputs, images with transparency, archival
|
||||
- **Compression**: 0 (none) to 9 (maximum)
|
||||
- Level 4 (default) balances size and speed
|
||||
- Level 9 for maximum compression (slow)
|
||||
|
||||
### JPEG Format
|
||||
- **Pros**: Smaller files, fast loading, universal support
|
||||
- **Cons**: Lossy compression, no transparency
|
||||
- **Best for**: Web images, previews, photos
|
||||
- **Quality**: 1-100%
|
||||
- 90% (default) excellent quality with good compression
|
||||
- 95%+ for near-lossless quality
|
||||
- 70-85% for web optimization
|
||||
|
||||
### WebP Format
|
||||
- **Pros**: Best compression ratios, supports transparency, modern
|
||||
- **Cons**: Limited software support
|
||||
- **Best for**: Web deployment, storage optimization
|
||||
- **Modes**:
|
||||
- Lossy (default): Excellent compression with quality control
|
||||
- Lossless: PNG-like quality with better compression
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### High-Quality Archive
|
||||
```
|
||||
Format: PNG
|
||||
Compression: 0-2
|
||||
Use Case: Final renders for portfolio or client delivery
|
||||
```
|
||||
|
||||
### Web Optimization
|
||||
```
|
||||
Format: JPEG or WebP
|
||||
Quality: 80-85
|
||||
Use Case: Website images, social media posts
|
||||
```
|
||||
|
||||
### Balanced Storage
|
||||
```
|
||||
Format: WebP
|
||||
Quality: 90
|
||||
Lossless: False
|
||||
Use Case: Large batches with storage constraints
|
||||
```
|
||||
|
||||
### Transparency Preservation
|
||||
```
|
||||
Format: PNG or WebP (lossless)
|
||||
Use Case: Logos, UI elements, cutout images
|
||||
```
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
### Basic Save
|
||||
```
|
||||
Generate → Kiko Save Image
|
||||
format: PNG
|
||||
popup: enabled
|
||||
```
|
||||
|
||||
### Format Comparison
|
||||
```
|
||||
Generate → Kiko Save Image (PNG) → Compare file sizes
|
||||
↘ Kiko Save Image (JPEG) ↗
|
||||
↘ Kiko Save Image (WebP) ↗
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
```
|
||||
Batch Generate → Kiko Save Image → Popup Viewer
|
||||
↓ ↓
|
||||
4 images Select best results
|
||||
```
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Format Selection**:
|
||||
- Use PNG for maximum quality and transparency
|
||||
- Use JPEG for photographs without transparency
|
||||
- Use WebP for modern web deployment
|
||||
|
||||
2. **Quality Settings**:
|
||||
- Start with defaults (90 for JPEG/WebP, 4 for PNG)
|
||||
- Adjust based on file size requirements
|
||||
- Preview results in popup before finalizing
|
||||
|
||||
3. **Popup Management**:
|
||||
- Drag to second monitor for larger workspace
|
||||
- Use roll-up to save screen space
|
||||
- Disable popup for automated workflows
|
||||
|
||||
4. **Batch Operations**:
|
||||
- Use checkboxes to select multiple images
|
||||
- Open all in tabs for side-by-side comparison
|
||||
- Download all for quick collection
|
||||
|
||||
5. **File Organization**:
|
||||
- Use subfolders in filename_prefix
|
||||
- Include descriptive prefixes
|
||||
- Let ComfyUI handle timestamp suffixes
|
||||
|
||||
## Advantages Over Standard Save Image
|
||||
|
||||
- **Immediate Preview**: No need to navigate file system
|
||||
- **Format Flexibility**: Choose optimal format per use case
|
||||
- **Quality Control**: Fine-tune compression settings
|
||||
- **Batch Management**: Handle multiple images efficiently
|
||||
- **Modern UI**: Floating interface doesn't interrupt workflow
|
||||
- **File Size Awareness**: See compression effectiveness immediately
|
||||
- **Quick Access**: One-click opening and downloading
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Image Processing**: Uses Pillow for format conversion
|
||||
- **Metadata**: Preserves ComfyUI metadata in saved files
|
||||
- **File Naming**: Automatic timestamp and counter suffixes
|
||||
- **Memory Efficiency**: Processes images individually
|
||||
- **Thread Safety**: Proper handling of concurrent saves
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Popup not appearing**:
|
||||
- Check that popup input is enabled
|
||||
- Look for minimized window
|
||||
- Try toggling the popup button in node
|
||||
|
||||
**WebP not working**:
|
||||
- Ensure Pillow has WebP support
|
||||
- Update Pillow: `pip install --upgrade pillow`
|
||||
|
||||
**Large file sizes**:
|
||||
- Increase compression (PNG) or reduce quality (JPEG/WebP)
|
||||
- Consider switching formats
|
||||
- Check image dimensions
|
||||
|
||||
**Can't see all images**:
|
||||
- Scroll within the popup grid
|
||||
- Maximize the popup window
|
||||
- Images are shown newest first
|
||||
@@ -0,0 +1,212 @@
|
||||
# LoRA Folder Batch
|
||||
|
||||
## Overview
|
||||
The **LoRA Folder Batch** node automates the process of testing multiple LoRA models from a folder. This tool was adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) (now in maintenance mode) and enhanced with batch processing capabilities for efficient LoRA evaluation workflows.
|
||||
|
||||
## Attribution
|
||||
This node is based on work from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted and enhanced these tools to ensure continued support and compatibility with modern ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
- **Automatic Folder Scanning**: Discovers all .safetensors files in specified folders
|
||||
- **Natural Sorting**: Intelligently sorts epochs (e.g., epoch_004, epoch_020, epoch_100)
|
||||
- **Pattern Filtering**: Include/exclude LoRAs using regex patterns
|
||||
- **Flexible Strength Control**: Single, multiple, or range-based strength values
|
||||
- **Batch Modes**: Sequential or combinatorial strength application
|
||||
- **Epoch Detection**: Automatically extracts epoch numbers from filenames
|
||||
|
||||
## Node Properties
|
||||
- **Category**: `ComfyAssets/🧰 xyz-helpers`
|
||||
- **Node Name**: `LoRAFolderBatch`
|
||||
- **Function**: `batch_loras`
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `folder_path` | STRING | "." | Folder path relative to models/loras (or absolute) |
|
||||
| `strength` | STRING | "1.0" | Strength values (see formats below) |
|
||||
| `batch_mode` | DROPDOWN | sequential | [sequential, combinatorial] processing mode |
|
||||
|
||||
### Optional
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `include_pattern` | STRING | "" | Regex pattern to include files |
|
||||
| `exclude_pattern` | STRING | "" | Regex pattern to exclude files |
|
||||
|
||||
### Strength Format Options
|
||||
- **Single**: `"1.0"` - Apply same strength to all LoRAs
|
||||
- **Multiple**: `"0.5, 0.75, 1.0"` - Comma-separated values
|
||||
- **Range**: `"0.5...1.0+0.25"` - Start...End+Step format
|
||||
|
||||
## Outputs
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `lora_params` | LORA_PARAMS | Batch parameters for processing |
|
||||
| `lora_list` | STRING | List of discovered LoRAs with epoch info |
|
||||
| `lora_count` | INT | Number of LoRAs found |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Test All Epochs of a LoRA
|
||||
```
|
||||
LoRAFolderBatch → FluxSamplerParams → KSampler
|
||||
folder_path: "my_lora_training"
|
||||
strength: "1.0"
|
||||
batch_mode: sequential
|
||||
```
|
||||
|
||||
### Strength Testing for Each LoRA
|
||||
```
|
||||
LoRAFolderBatch → KSampler → Image Grid
|
||||
folder_path: "test_loras"
|
||||
strength: "0.5, 0.75, 1.0"
|
||||
batch_mode: combinatorial
|
||||
```
|
||||
|
||||
### Filter Specific Epochs
|
||||
```
|
||||
LoRAFolderBatch → Processing Pipeline
|
||||
folder_path: "training_results"
|
||||
include_pattern: "epoch_0[2-5]0"
|
||||
strength: "0.8...1.2+0.1"
|
||||
```
|
||||
|
||||
## Batch Modes Explained
|
||||
|
||||
### Sequential Mode
|
||||
Each LoRA gets one strength value in order:
|
||||
- LoRA1 → strength[0]
|
||||
- LoRA2 → strength[1]
|
||||
- LoRA3 → strength[0] (cycles if fewer strengths than LoRAs)
|
||||
|
||||
### Combinatorial Mode
|
||||
Each LoRA is tested with ALL strength values:
|
||||
- LoRA1 → [0.5, 0.75, 1.0]
|
||||
- LoRA2 → [0.5, 0.75, 1.0]
|
||||
- LoRA3 → [0.5, 0.75, 1.0]
|
||||
|
||||
## File Naming Patterns
|
||||
|
||||
### Supported Epoch Formats
|
||||
- `model-v1-000004.safetensors` → Epoch 4
|
||||
- `style_epoch_020.safetensors` → Epoch 20
|
||||
- `lora-000100.safetensors` → Epoch 100
|
||||
|
||||
### Natural Sorting Examples
|
||||
Files are sorted intelligently:
|
||||
1. `model-000004.safetensors`
|
||||
2. `model-000020.safetensors`
|
||||
3. `model-000100.safetensors`
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Folder Organization
|
||||
```
|
||||
models/loras/
|
||||
├── my_style/
|
||||
│ ├── style-000010.safetensors
|
||||
│ ├── style-000020.safetensors
|
||||
│ └── style-000030.safetensors
|
||||
└── character/
|
||||
├── char-v2-000005.safetensors
|
||||
└── char-v2-000010.safetensors
|
||||
```
|
||||
|
||||
### Testing Workflows
|
||||
1. **Initial Testing**: Use single strength (1.0) to evaluate all epochs
|
||||
2. **Fine-tuning**: Use combinatorial mode with multiple strengths
|
||||
3. **Final Selection**: Filter to specific epochs and test strength range
|
||||
|
||||
### Pattern Filtering Examples
|
||||
```python
|
||||
# Include only specific versions
|
||||
include_pattern: "v2|v3"
|
||||
|
||||
# Exclude test/backup files
|
||||
exclude_pattern: "test|backup|old"
|
||||
|
||||
# Include specific epoch range
|
||||
include_pattern: "epoch_0[3-7]0"
|
||||
```
|
||||
|
||||
## Integration with Other Nodes
|
||||
|
||||
### Common Pipelines
|
||||
1. **LoRA Comparison Grid**:
|
||||
```
|
||||
LoRAFolderBatch → KSampler → Image Grid → Save
|
||||
```
|
||||
|
||||
2. **Strength Testing**:
|
||||
```
|
||||
LoRAFolderBatch → PlotParameters → Graph Display
|
||||
```
|
||||
|
||||
3. **Combined with FLUX**:
|
||||
```
|
||||
LoRAFolderBatch → FluxSamplerParams → KSampler
|
||||
```
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Memory Management
|
||||
- Start with fewer LoRAs when testing combinatorial mode
|
||||
- Use sequential mode for initial epoch evaluation
|
||||
- Clear LoRA cache between large batch runs
|
||||
|
||||
### Optimal Strength Ranges
|
||||
- **Style LoRAs**: 0.5-1.0
|
||||
- **Character LoRAs**: 0.7-1.2
|
||||
- **Detail LoRAs**: 0.3-0.7
|
||||
|
||||
### Debugging
|
||||
- Check `lora_list` output to verify correct files were found
|
||||
- Use `lora_count` to confirm expected number of LoRAs
|
||||
- Test patterns with include/exclude before full runs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No LoRAs Found
|
||||
- Verify folder path (relative to models/loras or use absolute)
|
||||
- Check file extensions (.safetensors)
|
||||
- Test without filters first
|
||||
|
||||
### Pattern Not Working
|
||||
- Patterns use Python regex syntax
|
||||
- Test patterns in regex tester first
|
||||
- Case-sensitive by default
|
||||
|
||||
### Memory Issues
|
||||
- Reduce batch_count in combinatorial mode
|
||||
- Process LoRAs in smaller groups
|
||||
- Use sequential mode for large sets
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Multi-Version Testing
|
||||
```python
|
||||
# Test different versions at different strengths
|
||||
folder_path: "character_loras"
|
||||
include_pattern: "v[1-3]"
|
||||
strength: "0.6, 0.8, 1.0"
|
||||
batch_mode: combinatorial
|
||||
```
|
||||
|
||||
### Epoch Progression Analysis
|
||||
```python
|
||||
# Test every 10th epoch
|
||||
folder_path: "training_output"
|
||||
include_pattern: "0[0-9]0\\.safetensors$"
|
||||
strength: "1.0"
|
||||
batch_mode: sequential
|
||||
```
|
||||
|
||||
## Version History
|
||||
- **1.0.0**: Initial adaptation from comfyui-essentials-nodes
|
||||
- **1.0.1**: Added natural sorting for epochs
|
||||
- **1.0.2**: Enhanced pattern filtering
|
||||
- **1.0.3**: Improved batch modes and strength parsing
|
||||
|
||||
## Credits
|
||||
Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Plot Parameters
|
||||
|
||||
## Overview
|
||||
The **Plot Parameters** node creates visual graphs and plots from sampler parameters, enabling data-driven analysis of generation settings. Adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) (now in maintenance mode), this tool helps visualize the relationship between parameters and output quality.
|
||||
|
||||
## Attribution
|
||||
This node is based on work from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted and enhanced these tools to ensure continued support and compatibility with modern ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
- **Multi-Parameter Plotting**: Visualize multiple parameters simultaneously
|
||||
- **Comparison Graphs**: Compare settings across batch runs
|
||||
- **Statistical Analysis**: Calculate means, deviations, and trends
|
||||
- **Export Capabilities**: Save plots as images or data files
|
||||
- **Real-time Updates**: Dynamic graph generation during workflow execution
|
||||
|
||||
## Node Properties
|
||||
- **Category**: `ComfyAssets/🧰 xyz-helpers`
|
||||
- **Node Name**: `PlotParameters`
|
||||
- **Function**: `plot`
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `sampler_params` | SAMPLER_PARAMS | - | Parameters to plot |
|
||||
| `plot_type` | DROPDOWN | line | [line, bar, scatter, heatmap] |
|
||||
| `x_axis` | DROPDOWN | steps | Parameter for X axis |
|
||||
| `y_axis` | DROPDOWN | quality | Metric for Y axis |
|
||||
|
||||
### Optional
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `title` | STRING | "Parameter Analysis" | Graph title |
|
||||
| `show_grid` | BOOLEAN | True | Display grid lines |
|
||||
| `show_legend` | BOOLEAN | True | Display legend |
|
||||
| `color_scheme` | DROPDOWN | default | Color palette selection |
|
||||
|
||||
## Outputs
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `plot_image` | IMAGE | Generated plot as image |
|
||||
| `data_csv` | STRING | Plot data in CSV format |
|
||||
| `statistics` | STRING | Statistical summary |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Parameter Visualization
|
||||
```
|
||||
FluxSamplerParams → PlotParameters → Display Image
|
||||
plot_type: line
|
||||
x_axis: steps
|
||||
y_axis: guidance
|
||||
```
|
||||
|
||||
### Batch Comparison Plot
|
||||
```
|
||||
LoRAFolderBatch → PlotParameters → Save Image
|
||||
plot_type: scatter
|
||||
x_axis: lora_strength
|
||||
y_axis: quality_score
|
||||
```
|
||||
|
||||
### Heatmap Analysis
|
||||
```
|
||||
Parameter Grid → PlotParameters → Analysis Display
|
||||
plot_type: heatmap
|
||||
x_axis: cfg
|
||||
y_axis: steps
|
||||
```
|
||||
|
||||
## Plot Types Explained
|
||||
|
||||
### Line Plot
|
||||
- Best for continuous parameter changes
|
||||
- Shows trends and relationships
|
||||
- Ideal for time series or progression
|
||||
|
||||
### Bar Chart
|
||||
- Compares discrete values
|
||||
- Good for categorical comparisons
|
||||
- Shows distribution clearly
|
||||
|
||||
### Scatter Plot
|
||||
- Reveals correlations
|
||||
- Identifies outliers
|
||||
- Best for large datasets
|
||||
|
||||
### Heatmap
|
||||
- Two-dimensional parameter analysis
|
||||
- Color-coded intensity values
|
||||
- Perfect for grid searches
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Parameter Selection
|
||||
- Choose related parameters for meaningful plots
|
||||
- Use consistent scales for comparison
|
||||
- Consider parameter ranges when plotting
|
||||
|
||||
### Visual Clarity
|
||||
- Limit number of series to 5-7 for readability
|
||||
- Use contrasting colors for multiple lines
|
||||
- Enable grid for precise value reading
|
||||
|
||||
### Data Analysis
|
||||
```python
|
||||
# Effective parameter combinations
|
||||
x_axis: "guidance"
|
||||
y_axis: "perceived_quality"
|
||||
|
||||
# Step efficiency analysis
|
||||
x_axis: "steps"
|
||||
y_axis: "generation_time"
|
||||
|
||||
# LoRA impact assessment
|
||||
x_axis: "lora_strength"
|
||||
y_axis: "style_adherence"
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Complete Analysis Pipeline
|
||||
```
|
||||
1. Generate with parameters
|
||||
2. Plot results
|
||||
3. Export data
|
||||
4. Statistical analysis
|
||||
```
|
||||
|
||||
### Multi-Plot Workflow
|
||||
```
|
||||
Params → Plot1 (steps vs quality)
|
||||
↘ Plot2 (guidance vs coherence)
|
||||
↘ Plot3 (strength vs style)
|
||||
→ Combined Analysis
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Custom Metrics
|
||||
- Define custom Y-axis metrics
|
||||
- Import external quality scores
|
||||
- Calculate derived values
|
||||
|
||||
### Export Options
|
||||
- PNG/SVG image formats
|
||||
- CSV data export
|
||||
- JSON statistics export
|
||||
|
||||
### Styling Options
|
||||
```python
|
||||
# Professional presentation
|
||||
color_scheme: "scientific"
|
||||
show_grid: True
|
||||
show_legend: True
|
||||
|
||||
# Minimal style
|
||||
color_scheme: "minimal"
|
||||
show_grid: False
|
||||
show_legend: False
|
||||
```
|
||||
|
||||
## Statistical Analysis
|
||||
|
||||
### Available Metrics
|
||||
- Mean, Median, Mode
|
||||
- Standard Deviation
|
||||
- Correlation Coefficients
|
||||
- Trend Lines
|
||||
- R-squared Values
|
||||
|
||||
### Interpretation Guide
|
||||
- **Positive Correlation**: Parameters increase together
|
||||
- **Negative Correlation**: Inverse relationship
|
||||
- **No Correlation**: Independent parameters
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Optimal Visualization
|
||||
1. Start with scatter plots for exploration
|
||||
2. Use line plots for trends
|
||||
3. Apply heatmaps for 2D parameter spaces
|
||||
4. Bar charts for final comparisons
|
||||
|
||||
### Data Preparation
|
||||
- Normalize scales when comparing different metrics
|
||||
- Remove outliers for cleaner plots
|
||||
- Group similar parameters
|
||||
|
||||
### Performance Tips
|
||||
- Cache plot images for repeated viewing
|
||||
- Export data for external analysis
|
||||
- Use lower resolution for preview plots
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Empty Plots
|
||||
- Verify sampler_params contains data
|
||||
- Check axis parameter selection
|
||||
- Ensure valid parameter ranges
|
||||
|
||||
### Scaling Issues
|
||||
- Use logarithmic scale for wide ranges
|
||||
- Normalize data if needed
|
||||
- Adjust plot dimensions
|
||||
|
||||
### Export Problems
|
||||
- Check file permissions
|
||||
- Verify export path exists
|
||||
- Ensure sufficient disk space
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Hyperparameter Optimization
|
||||
Track and visualize the effect of different sampling parameters on output quality.
|
||||
|
||||
### LoRA Strength Analysis
|
||||
Plot the relationship between LoRA strength and style transfer effectiveness.
|
||||
|
||||
### Efficiency Studies
|
||||
Analyze generation time vs quality trade-offs across different settings.
|
||||
|
||||
### Batch Comparison
|
||||
Compare multiple generation runs to identify optimal parameters.
|
||||
|
||||
## Version History
|
||||
- **1.0.0**: Initial adaptation from comfyui-essentials-nodes
|
||||
- **1.0.1**: Added heatmap visualization
|
||||
- **1.0.2**: Enhanced statistical analysis
|
||||
- **1.0.3**: Improved export capabilities
|
||||
|
||||
## Credits
|
||||
Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team.
|
||||
@@ -0,0 +1,260 @@
|
||||
# Sampler Select Helper
|
||||
|
||||
## Overview
|
||||
The **Sampler Select Helper** node provides intelligent sampler selection with model-specific recommendations and compatibility checking. Adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) (now in maintenance mode), this tool ensures optimal sampler-scheduler combinations for different model architectures.
|
||||
|
||||
## Attribution
|
||||
This node is based on work from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted and enhanced these tools to ensure continued support and compatibility with modern ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
- **Model-Aware Selection**: Automatic recommendations based on model type
|
||||
- **Compatibility Validation**: Ensures sampler-scheduler pairs work well together
|
||||
- **Performance Profiles**: Pre-configured settings for quality vs speed
|
||||
- **Dynamic Updates**: Adapts to newly available samplers
|
||||
- **Batch Support**: Test multiple samplers in sequence
|
||||
|
||||
## Node Properties
|
||||
- **Category**: `ComfyAssets/🧰 xyz-helpers`
|
||||
- **Node Name**: `SamplerSelectHelper`
|
||||
- **Function**: `select_sampler`
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `model_type` | DROPDOWN | auto | [auto, sdxl, sd15, flux, custom] |
|
||||
| `quality_preset` | DROPDOWN | balanced | [fast, balanced, quality, extreme] |
|
||||
| `sampler_override` | DROPDOWN | auto | Specific sampler selection |
|
||||
|
||||
### Optional
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `scheduler_override` | DROPDOWN | auto | Specific scheduler selection |
|
||||
| `model_name` | STRING | - | Model name for auto-detection |
|
||||
| `custom_rules` | STRING | - | JSON rules for custom selection |
|
||||
|
||||
## Outputs
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `sampler_name` | STRING | Selected sampler |
|
||||
| `scheduler` | STRING | Selected scheduler |
|
||||
| `recommended_steps` | INT | Suggested step count |
|
||||
| `recommended_cfg` | FLOAT | Suggested CFG scale |
|
||||
|
||||
## Model-Specific Recommendations
|
||||
|
||||
### SDXL Models
|
||||
```python
|
||||
quality_preset: "balanced"
|
||||
→ sampler: "dpmpp_2m"
|
||||
→ scheduler: "karras"
|
||||
→ steps: 25
|
||||
→ cfg: 7.0
|
||||
```
|
||||
|
||||
### SD 1.5 Models
|
||||
```python
|
||||
quality_preset: "quality"
|
||||
→ sampler: "dpmpp_2m_sde"
|
||||
→ scheduler: "exponential"
|
||||
→ steps: 30
|
||||
→ cfg: 7.5
|
||||
```
|
||||
|
||||
### FLUX Models
|
||||
```python
|
||||
quality_preset: "fast"
|
||||
→ sampler: "euler"
|
||||
→ scheduler: "simple"
|
||||
→ steps: 15
|
||||
→ cfg: 3.5
|
||||
```
|
||||
|
||||
## Quality Presets Explained
|
||||
|
||||
### Fast (Preview)
|
||||
- **Goal**: Quick iterations
|
||||
- **Steps**: 10-15
|
||||
- **Samplers**: euler, dpm_fast
|
||||
- **Use Case**: Testing prompts
|
||||
|
||||
### Balanced (Default)
|
||||
- **Goal**: Good quality/speed ratio
|
||||
- **Steps**: 20-25
|
||||
- **Samplers**: dpmpp_2m, dpmpp_2m_sde
|
||||
- **Use Case**: Regular generation
|
||||
|
||||
### Quality
|
||||
- **Goal**: Best visual quality
|
||||
- **Steps**: 30-40
|
||||
- **Samplers**: dpmpp_3m_sde, dpm_adaptive
|
||||
- **Use Case**: Final renders
|
||||
|
||||
### Extreme
|
||||
- **Goal**: Maximum quality
|
||||
- **Steps**: 50-100
|
||||
- **Samplers**: dpm_adaptive, dpmpp_3m_sde
|
||||
- **Use Case**: Hero images
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Auto Model Detection
|
||||
```
|
||||
Load Model → SamplerSelectHelper → KSampler
|
||||
model_type: auto
|
||||
quality_preset: balanced
|
||||
```
|
||||
|
||||
### Custom Override
|
||||
```
|
||||
SamplerSelectHelper → KSampler
|
||||
sampler_override: "dpmpp_3m_sde"
|
||||
scheduler_override: "exponential"
|
||||
```
|
||||
|
||||
### Batch Testing
|
||||
```
|
||||
SamplerSelectHelper → Batch Process
|
||||
quality_preset: [fast, balanced, quality]
|
||||
→ Compare outputs
|
||||
```
|
||||
|
||||
## Compatibility Matrix
|
||||
|
||||
### Recommended Combinations
|
||||
| Sampler | Best Schedulers | Avoid |
|
||||
|---------|----------------|--------|
|
||||
| euler | normal, karras | sgm_uniform |
|
||||
| euler_a | normal, karras | simple |
|
||||
| dpmpp_2m | karras, exponential | - |
|
||||
| dpmpp_2m_sde | karras, exponential | simple |
|
||||
| dpmpp_3m_sde | exponential | simple |
|
||||
| dpm_adaptive | normal | karras |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Model Type Detection
|
||||
1. Use `auto` for automatic detection
|
||||
2. Override only when necessary
|
||||
3. Provide model_name for better accuracy
|
||||
|
||||
### Performance Optimization
|
||||
```python
|
||||
# Quick preview workflow
|
||||
quality_preset: "fast"
|
||||
→ 10 steps, euler sampler
|
||||
|
||||
# Final production
|
||||
quality_preset: "quality"
|
||||
→ 35 steps, dpmpp_3m_sde
|
||||
|
||||
# Experimental/artistic
|
||||
quality_preset: "extreme"
|
||||
→ 75 steps, dpm_adaptive
|
||||
```
|
||||
|
||||
### Custom Rules Format
|
||||
```json
|
||||
{
|
||||
"model_pattern": "anime.*",
|
||||
"sampler": "dpmpp_2m_sde",
|
||||
"scheduler": "karras",
|
||||
"steps": 28,
|
||||
"cfg": 7.0
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Other Nodes
|
||||
|
||||
### Complete Pipeline
|
||||
```
|
||||
Model Loader → SamplerSelectHelper → KSampler
|
||||
↘ FluxSamplerParams ↗
|
||||
```
|
||||
|
||||
### A/B Testing
|
||||
```
|
||||
SamplerSelectHelper → KSampler → Image A
|
||||
quality: fast
|
||||
SamplerSelectHelper → KSampler → Image B
|
||||
quality: quality
|
||||
→ Compare Results
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Dynamic Sampler Discovery
|
||||
- Automatically detects new samplers
|
||||
- Updates compatibility matrix
|
||||
- Maintains optimal pairings
|
||||
|
||||
### Performance Profiling
|
||||
- Tracks generation times
|
||||
- Suggests optimal settings
|
||||
- Adapts to hardware capabilities
|
||||
|
||||
### Model Fingerprinting
|
||||
- Identifies model architecture
|
||||
- Applies specific optimizations
|
||||
- Learns from usage patterns
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Speed vs Quality
|
||||
1. Start with "fast" for prompt testing
|
||||
2. Move to "balanced" for iteration
|
||||
3. Use "quality" for final output
|
||||
4. Reserve "extreme" for special cases
|
||||
|
||||
### Sampler Selection Logic
|
||||
```python
|
||||
if model_type == "flux":
|
||||
prefer ["euler", "dpmpp_2m"]
|
||||
elif model_type == "sdxl":
|
||||
prefer ["dpmpp_2m_sde", "dpmpp_3m_sde"]
|
||||
else:
|
||||
use ["dpmpp_2m", "euler_a"]
|
||||
```
|
||||
|
||||
### Memory Considerations
|
||||
- Fast presets use less memory
|
||||
- Extreme presets may require more VRAM
|
||||
- Adaptive samplers adjust dynamically
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Wrong Sampler Selected
|
||||
- Check model_type setting
|
||||
- Verify model detection
|
||||
- Use manual override if needed
|
||||
|
||||
### Poor Quality Output
|
||||
- Increase quality preset
|
||||
- Check recommended steps
|
||||
- Verify CFG scale
|
||||
|
||||
### Performance Issues
|
||||
- Start with fast preset
|
||||
- Reduce step count
|
||||
- Try simpler samplers
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Model Comparison
|
||||
Test same prompt across different models with optimal settings for each.
|
||||
|
||||
### Quality Ladder
|
||||
Progress from fast to extreme quality to find optimal balance.
|
||||
|
||||
### Sampler Shootout
|
||||
Compare all compatible samplers for specific model/prompt combination.
|
||||
|
||||
## Version History
|
||||
- **1.0.0**: Initial adaptation from comfyui-essentials-nodes
|
||||
- **1.0.1**: Added FLUX model support
|
||||
- **1.0.2**: Enhanced compatibility matrix
|
||||
- **1.0.3**: Improved auto-detection
|
||||
|
||||
## Credits
|
||||
Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team.
|
||||
@@ -0,0 +1,300 @@
|
||||
# Scheduler Select Helper
|
||||
|
||||
## Overview
|
||||
The **Scheduler Select Helper** node provides intelligent scheduler selection with sampler-aware recommendations and model-specific optimizations. Adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) (now in maintenance mode), this tool ensures optimal scheduler selection for different sampling algorithms and models.
|
||||
|
||||
## Attribution
|
||||
This node is based on work from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted and enhanced these tools to ensure continued support and compatibility with modern ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
- **Sampler-Aware Selection**: Recommends best schedulers for each sampler
|
||||
- **Model Optimization**: Specific scheduler tuning for different models
|
||||
- **Noise Schedule Profiles**: Pre-configured curves for various use cases
|
||||
- **Visual Feedback**: Preview noise schedules
|
||||
- **Batch Testing**: Compare multiple schedulers
|
||||
|
||||
## Node Properties
|
||||
- **Category**: `ComfyAssets/🧰 xyz-helpers`
|
||||
- **Node Name**: `SchedulerSelectHelper`
|
||||
- **Function**: `select_scheduler`
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `sampler_name` | STRING | - | Current sampler being used |
|
||||
| `model_type` | DROPDOWN | auto | [auto, sdxl, sd15, flux] |
|
||||
| `schedule_type` | DROPDOWN | smooth | [smooth, sharp, linear, custom] |
|
||||
|
||||
### Optional
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `override` | DROPDOWN | none | Force specific scheduler |
|
||||
| `beta_schedule` | STRING | - | Custom beta schedule values |
|
||||
| `visualize` | BOOLEAN | False | Show schedule curve |
|
||||
|
||||
## Outputs
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `scheduler` | STRING | Selected scheduler name |
|
||||
| `schedule_curve` | IMAGE | Visualization of noise schedule |
|
||||
| `beta_values` | FLOAT_ARRAY | Beta schedule values |
|
||||
|
||||
## Scheduler Types Explained
|
||||
|
||||
### Normal
|
||||
- **Curve**: Linear noise reduction
|
||||
- **Best For**: General purpose
|
||||
- **Samplers**: euler, dpm_fast
|
||||
|
||||
### Karras
|
||||
- **Curve**: Improved noise schedule
|
||||
- **Best For**: High quality
|
||||
- **Samplers**: dpmpp_2m, dpmpp_2m_sde
|
||||
|
||||
### Exponential
|
||||
- **Curve**: Exponential decay
|
||||
- **Best For**: Fine details
|
||||
- **Samplers**: dpmpp_3m_sde
|
||||
|
||||
### Simple
|
||||
- **Curve**: Basic linear
|
||||
- **Best For**: Fast generation
|
||||
- **Samplers**: euler, lcm
|
||||
|
||||
### SGM Uniform
|
||||
- **Curve**: Uniform distribution
|
||||
- **Best For**: FLUX models
|
||||
- **Samplers**: euler, dpmpp_2m
|
||||
|
||||
## Schedule Types
|
||||
|
||||
### Smooth (Default)
|
||||
```python
|
||||
# Gradual noise reduction
|
||||
# Good for most content
|
||||
→ karras or exponential
|
||||
```
|
||||
|
||||
### Sharp
|
||||
```python
|
||||
# Aggressive early reduction
|
||||
# Good for high contrast
|
||||
→ normal or simple
|
||||
```
|
||||
|
||||
### Linear
|
||||
```python
|
||||
# Constant reduction rate
|
||||
# Predictable results
|
||||
→ normal
|
||||
```
|
||||
|
||||
### Custom
|
||||
```python
|
||||
# User-defined curve
|
||||
# Advanced control
|
||||
→ based on beta_schedule
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Automatic Selection
|
||||
```
|
||||
KSampler Settings → SchedulerSelectHelper → KSampler
|
||||
sampler_name: "dpmpp_2m_sde"
|
||||
model_type: auto
|
||||
→ scheduler: "karras"
|
||||
```
|
||||
|
||||
### Visual Comparison
|
||||
```
|
||||
SchedulerSelectHelper → Display
|
||||
visualize: True
|
||||
→ Shows noise schedule curve
|
||||
```
|
||||
|
||||
### Batch Testing
|
||||
```
|
||||
For each scheduler:
|
||||
SchedulerSelectHelper → KSampler → Save
|
||||
→ Compare results
|
||||
```
|
||||
|
||||
## Sampler-Scheduler Compatibility
|
||||
|
||||
### Optimal Pairings
|
||||
| Sampler | Best Scheduler | Good Alternatives |
|
||||
|---------|---------------|-------------------|
|
||||
| euler | normal | karras |
|
||||
| euler_a | karras | normal |
|
||||
| heun | normal | - |
|
||||
| dpm_fast | normal | simple |
|
||||
| dpm_adaptive | normal | - |
|
||||
| dpmpp_2m | karras | exponential |
|
||||
| dpmpp_2m_sde | karras | exponential |
|
||||
| dpmpp_3m_sde | exponential | karras |
|
||||
| dpmpp_2s_a | karras | normal |
|
||||
| lcm | simple | normal |
|
||||
|
||||
## Model-Specific Recommendations
|
||||
|
||||
### SDXL
|
||||
```python
|
||||
preferred_schedulers = ["karras", "exponential"]
|
||||
# Better convergence for high-res
|
||||
```
|
||||
|
||||
### SD 1.5
|
||||
```python
|
||||
preferred_schedulers = ["karras", "normal"]
|
||||
# Classic combinations
|
||||
```
|
||||
|
||||
### FLUX
|
||||
```python
|
||||
preferred_schedulers = ["simple", "sgm_uniform"]
|
||||
# Optimized for FLUX architecture
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Selection Strategy
|
||||
1. Let auto-detection handle defaults
|
||||
2. Override for specific artistic goals
|
||||
3. Test multiple schedulers for hero images
|
||||
4. Use visualization to understand curves
|
||||
|
||||
### Performance Tips
|
||||
- Simple/normal for quick previews
|
||||
- Karras/exponential for quality
|
||||
- SGM uniform specifically for FLUX
|
||||
- Match scheduler to sampler type
|
||||
|
||||
### Testing Workflow
|
||||
```python
|
||||
schedulers = ["normal", "karras", "exponential"]
|
||||
for scheduler in schedulers:
|
||||
generate_image(scheduler)
|
||||
save_with_metadata(scheduler)
|
||||
compare_results()
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Beta Schedule Customization
|
||||
```python
|
||||
# Custom exponential curve
|
||||
beta_schedule = "0.00085, 0.0012, 0.0018, ..."
|
||||
|
||||
# Sharp early reduction
|
||||
beta_schedule = "0.001, 0.002, 0.004, 0.006, ..."
|
||||
```
|
||||
|
||||
### Schedule Visualization
|
||||
- Plots noise reduction curve
|
||||
- Shows sigma values
|
||||
- Compares with standard schedules
|
||||
- Exports schedule data
|
||||
|
||||
### Adaptive Selection
|
||||
- Learns from user preferences
|
||||
- Adapts to hardware capabilities
|
||||
- Optimizes for generation speed
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Complete Pipeline
|
||||
```
|
||||
Sampler Combo → SchedulerSelectHelper → KSampler
|
||||
↓ ↓
|
||||
sampler_name → Optimal scheduler selection
|
||||
```
|
||||
|
||||
### A/B Testing
|
||||
```
|
||||
Same prompt → Different schedulers → Grid comparison
|
||||
normal vs karras vs exponential
|
||||
```
|
||||
|
||||
### Noise Schedule Analysis
|
||||
```
|
||||
SchedulerSelectHelper → Plot Parameters
|
||||
visualize: True
|
||||
→ Analyze noise curves
|
||||
```
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Quality Optimization
|
||||
```python
|
||||
# For maximum quality
|
||||
if sampler in ["dpmpp_3m_sde"]:
|
||||
use scheduler="exponential"
|
||||
elif sampler in ["dpmpp_2m_sde"]:
|
||||
use scheduler="karras"
|
||||
```
|
||||
|
||||
### Speed Optimization
|
||||
```python
|
||||
# For fast generation
|
||||
use scheduler="simple" or "normal"
|
||||
reduce step count by 20%
|
||||
```
|
||||
|
||||
### Artistic Effects
|
||||
- **Sharp details**: normal scheduler
|
||||
- **Smooth gradients**: karras scheduler
|
||||
- **Fine textures**: exponential scheduler
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Artifacts or Noise
|
||||
- Try different scheduler
|
||||
- Check sampler compatibility
|
||||
- Adjust step count
|
||||
|
||||
### Slow Convergence
|
||||
- Switch from simple to karras
|
||||
- Increase step count
|
||||
- Check model compatibility
|
||||
|
||||
### Inconsistent Results
|
||||
- Use same scheduler for batch
|
||||
- Avoid random scheduler selection
|
||||
- Fix seed for testing
|
||||
|
||||
## Visual Guide
|
||||
|
||||
### Noise Schedule Curves
|
||||
```
|
||||
Normal: ████████████████
|
||||
Linear reduction
|
||||
|
||||
Karras: ███████████▓▓▓░░
|
||||
Smooth curve
|
||||
|
||||
Exponential: ██████▓▓▓░░░░░
|
||||
Fast early reduction
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Scheduler Comparison
|
||||
Test same seed with different schedulers to find optimal setting.
|
||||
|
||||
### Model Migration
|
||||
When switching models, automatically adjust scheduler for best results.
|
||||
|
||||
### Quality Ladder
|
||||
Progress through schedulers from fast to quality for different use cases.
|
||||
|
||||
## Version History
|
||||
- **1.0.0**: Initial adaptation from comfyui-essentials-nodes
|
||||
- **1.0.1**: Added visualization features
|
||||
- **1.0.2**: Enhanced model detection
|
||||
- **1.0.3**: Improved compatibility matrix
|
||||
|
||||
## Credits
|
||||
Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Text Encode Sampler Params
|
||||
|
||||
## Overview
|
||||
The **Text Encode Sampler Params** node combines text encoding with sampler parameter management, providing a unified interface for prompt processing and sampling configuration. Adapted from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) (now in maintenance mode), this tool streamlines the text-to-image pipeline setup.
|
||||
|
||||
## Attribution
|
||||
This node is based on work from [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials) by cubiq. The original project is in maintenance-only mode, and we've adopted and enhanced these tools to ensure continued support and compatibility with modern ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
- **Unified Interface**: Combine text encoding and sampler params in one node
|
||||
- **Dynamic Prompt Processing**: Support for wildcards and syntax
|
||||
- **Parameter Templates**: Pre-configured settings for common scenarios
|
||||
- **Batch Text Processing**: Handle multiple prompts efficiently
|
||||
- **Model-Aware Encoding**: Optimize for different text encoders
|
||||
|
||||
## Node Properties
|
||||
- **Category**: `ComfyAssets/🧰 xyz-helpers`
|
||||
- **Node Name**: `TextEncodeSamplerParams`
|
||||
- **Function**: `encode_and_params`
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `text` | STRING | - | Prompt text to encode |
|
||||
| `clip` | CLIP | - | CLIP model for encoding |
|
||||
| `sampler_name` | DROPDOWN | dpmpp_2m | Sampling algorithm |
|
||||
| `scheduler` | DROPDOWN | karras | Noise scheduler |
|
||||
| `steps` | INT | 20 | Sampling steps |
|
||||
| `cfg` | FLOAT | 7.0 | CFG scale |
|
||||
|
||||
### Optional
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `negative_text` | STRING | "" | Negative prompt |
|
||||
| `seed` | INT | -1 | Random seed (-1 for random) |
|
||||
| `denoise` | FLOAT | 1.0 | Denoising strength |
|
||||
| `template` | DROPDOWN | none | Parameter template |
|
||||
|
||||
## Outputs
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `positive` | CONDITIONING | Encoded positive prompt |
|
||||
| `negative` | CONDITIONING | Encoded negative prompt |
|
||||
| `sampler_params` | DICT | Complete sampler parameters |
|
||||
|
||||
## Templates
|
||||
|
||||
### Portrait Photography
|
||||
```python
|
||||
template: "portrait"
|
||||
→ steps: 25
|
||||
→ cfg: 7.5
|
||||
→ sampler: dpmpp_2m_sde
|
||||
→ scheduler: karras
|
||||
```
|
||||
|
||||
### Landscape Art
|
||||
```python
|
||||
template: "landscape"
|
||||
→ steps: 30
|
||||
→ cfg: 8.0
|
||||
→ sampler: dpmpp_3m_sde
|
||||
→ scheduler: exponential
|
||||
```
|
||||
|
||||
### Quick Preview
|
||||
```python
|
||||
template: "preview"
|
||||
→ steps: 12
|
||||
→ cfg: 6.0
|
||||
→ sampler: euler
|
||||
→ scheduler: normal
|
||||
```
|
||||
|
||||
### High Detail
|
||||
```python
|
||||
template: "detailed"
|
||||
→ steps: 40
|
||||
→ cfg: 7.0
|
||||
→ sampler: dpm_adaptive
|
||||
→ scheduler: karras
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Text-to-Image
|
||||
```
|
||||
TextEncodeSamplerParams → KSampler → VAE Decode
|
||||
text: "beautiful landscape"
|
||||
negative_text: "ugly, blurry"
|
||||
steps: 20
|
||||
```
|
||||
|
||||
### Template-Based Generation
|
||||
```
|
||||
TextEncodeSamplerParams → KSampler
|
||||
text: "portrait of a person"
|
||||
template: "portrait"
|
||||
→ Optimized portrait settings
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
```
|
||||
Multiple Prompts → TextEncodeSamplerParams → Batch Generate
|
||||
→ Encode all prompts with same settings
|
||||
```
|
||||
|
||||
## Prompt Syntax Support
|
||||
|
||||
### Wildcards
|
||||
```
|
||||
{red|blue|green} car
|
||||
→ Randomly selects color
|
||||
```
|
||||
|
||||
### Emphasis
|
||||
```
|
||||
(important:1.2) detail
|
||||
→ Increases weight to 1.2
|
||||
```
|
||||
|
||||
### Alternation
|
||||
```
|
||||
[cat|dog] in garden
|
||||
→ Alternates between options
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Text Encoding
|
||||
1. Keep prompts concise and descriptive
|
||||
2. Use emphasis for important elements
|
||||
3. Structure prompts logically
|
||||
4. Test negative prompts impact
|
||||
|
||||
### Parameter Selection
|
||||
```python
|
||||
# Quality over speed
|
||||
steps: 30-40
|
||||
cfg: 7-8
|
||||
sampler: dpmpp_3m_sde
|
||||
|
||||
# Speed over quality
|
||||
steps: 10-15
|
||||
cfg: 5-6
|
||||
sampler: euler
|
||||
```
|
||||
|
||||
### Negative Prompts
|
||||
```python
|
||||
# Common negatives
|
||||
"ugly, tiling, poorly drawn, out of frame"
|
||||
|
||||
# Style-specific
|
||||
"cartoon, anime" (for realism)
|
||||
"realistic, photo" (for artwork)
|
||||
```
|
||||
|
||||
## Integration with Other Nodes
|
||||
|
||||
### Complete Pipeline
|
||||
```
|
||||
TextEncodeSamplerParams → KSampler → VAE Decode
|
||||
↓ ↑
|
||||
All parameters From Model Loader
|
||||
```
|
||||
|
||||
### With LoRA
|
||||
```
|
||||
LoRAFolderBatch → TextEncodeSamplerParams → Generate
|
||||
→ Apply LoRA to encoded text
|
||||
```
|
||||
|
||||
### Multi-Pass Processing
|
||||
```
|
||||
TextEncodeSamplerParams → First Pass (low res)
|
||||
↘ Second Pass (high res)
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Dynamic Templates
|
||||
```python
|
||||
# Load template based on prompt content
|
||||
if "portrait" in text:
|
||||
use_template("portrait")
|
||||
elif "landscape" in text:
|
||||
use_template("landscape")
|
||||
```
|
||||
|
||||
### Prompt Weighting
|
||||
```python
|
||||
# Automatic weight calculation
|
||||
analyze_prompt_importance()
|
||||
apply_semantic_weights()
|
||||
```
|
||||
|
||||
### CLIP Skip Support
|
||||
- Adjust CLIP layers used
|
||||
- Model-specific optimization
|
||||
- Quality vs style balance
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### Prompt Optimization
|
||||
1. Front-load important elements
|
||||
2. Use commas for separation
|
||||
3. Avoid contradictions
|
||||
4. Test with different CFG values
|
||||
|
||||
### Performance Tuning
|
||||
```python
|
||||
# Memory efficient
|
||||
encode_in_batches = True
|
||||
clear_cache_between = True
|
||||
|
||||
# Speed priority
|
||||
use_half_precision = True
|
||||
minimize_conditioning = True
|
||||
```
|
||||
|
||||
### Quality Enhancement
|
||||
- Higher CFG for prompt adherence
|
||||
- Lower CFG for creativity
|
||||
- Balance with step count
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Style Transfer
|
||||
```
|
||||
Reference Image → Extract Style
|
||||
↓
|
||||
TextEncodeSamplerParams → Apply Style
|
||||
text: "in the style of [extracted]"
|
||||
```
|
||||
|
||||
### Prompt Evolution
|
||||
```
|
||||
Base Prompt → Variations → TextEncodeSamplerParams
|
||||
→ Test different phrasings
|
||||
```
|
||||
|
||||
### A/B Testing
|
||||
```
|
||||
Same prompt → Different parameters → Compare
|
||||
template A vs template B
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Poor Text Adherence
|
||||
- Increase CFG scale
|
||||
- Simplify prompt
|
||||
- Check CLIP model compatibility
|
||||
|
||||
### Over-saturation
|
||||
- Reduce CFG scale
|
||||
- Adjust negative prompt
|
||||
- Check sampler settings
|
||||
|
||||
### Encoding Errors
|
||||
- Verify CLIP model loaded
|
||||
- Check text formatting
|
||||
- Remove special characters
|
||||
|
||||
## Parameter Guidelines
|
||||
|
||||
### CFG Scale Effects
|
||||
```
|
||||
Low (3-5): Creative, loose interpretation
|
||||
Medium (6-8): Balanced adherence
|
||||
High (9-12): Strict prompt following
|
||||
Very High (13+): Potential artifacts
|
||||
```
|
||||
|
||||
### Step Count Impact
|
||||
```
|
||||
Low (10-15): Fast, rough
|
||||
Medium (20-30): Good balance
|
||||
High (40-50): Maximum quality
|
||||
Very High (50+): Diminishing returns
|
||||
```
|
||||
|
||||
## Model-Specific Settings
|
||||
|
||||
### SDXL
|
||||
- CFG: 6-8
|
||||
- CLIP Skip: 1-2
|
||||
- Emphasis: Moderate
|
||||
|
||||
### SD 1.5
|
||||
- CFG: 7-9
|
||||
- CLIP Skip: 1-2
|
||||
- Emphasis: Standard
|
||||
|
||||
### FLUX
|
||||
- CFG: 3-5
|
||||
- CLIP Skip: 0
|
||||
- Emphasis: Subtle
|
||||
|
||||
## Version History
|
||||
- **1.0.0**: Initial adaptation from comfyui-essentials-nodes
|
||||
- **1.0.1**: Added template system
|
||||
- **1.0.2**: Enhanced prompt syntax support
|
||||
- **1.0.3**: Improved batch processing
|
||||
|
||||
## Credits
|
||||
Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team.
|
||||
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"id": "display-any-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
400,
|
||||
270
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"tensor shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
50,
|
||||
270
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
400,
|
||||
520
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
70
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "VAELoader",
|
||||
"pos": [
|
||||
-250,
|
||||
410
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "VAELoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ae.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEEncode",
|
||||
"pos": [
|
||||
430,
|
||||
390
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "VAEEncode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
640,
|
||||
270
|
||||
],
|
||||
"size": [
|
||||
450,
|
||||
278
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
640,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
590,
|
||||
278
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
-320,
|
||||
530
|
||||
],
|
||||
"size": [
|
||||
350,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Display Any Example\n\nUniversal debugging tool:\n- Accepts ANY input type\n- Two modes: raw value or tensor shape\n- Finds tensors in nested structures\n\nUse cases:\n- Debug tensor dimensions\n- Inspect latent data\n- View config objects\n- Track data flow\n\nConnect anything to see its contents!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
7,
|
||||
8,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
8,
|
||||
1,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
9,
|
||||
4,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Display Any",
|
||||
"bounding": [
|
||||
-400,
|
||||
130,
|
||||
1740,
|
||||
850
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.6115909044841477,
|
||||
"offset": [
|
||||
689.1392030323894,
|
||||
-27.435530779258137
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 466 KiB |
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "display-text-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
-160,
|
||||
70
|
||||
],
|
||||
"size": [
|
||||
500,
|
||||
400
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "easy positive",
|
||||
"pos": [
|
||||
-620,
|
||||
70
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.1",
|
||||
"Node name for S&R": "easy positive"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Positive prompt:\nbeautiful landscape, mountains in background, sunset lighting, golden hour, professional photography, high resolution, detailed textures, vibrant colors, masterpiece\n\nNegative prompt:\nlow quality, blurry, pixelated, bad composition, oversaturated, underexposed, amateur"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
-620,
|
||||
330
|
||||
],
|
||||
"size": [
|
||||
360,
|
||||
250
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Display Text Example\n\nSmart text display with:\n- Auto-detection of SDXL prompt format\n- Split view for positive/negative prompts\n- Text wrapping and scrolling\n- Copy buttons (strips labels)\n- Responsive resizing\n\nTry editing the text to see:\n1. Single text mode (no prompt markers)\n2. Split view mode (with Positive/Negative prompts)\n\nPerfect for displaying Gemini-generated prompts!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
2,
|
||||
4,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Display Text",
|
||||
"bounding": [
|
||||
-750,
|
||||
-60,
|
||||
1240,
|
||||
700
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.90909090909091,
|
||||
"offset": [
|
||||
720,
|
||||
30
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 272 KiB |
@@ -0,0 +1,301 @@
|
||||
{
|
||||
"id": "empty-latent-batch-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 8,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "EmptyLatentBatch",
|
||||
"pos": [
|
||||
350,
|
||||
350
|
||||
],
|
||||
"size": [
|
||||
370,
|
||||
210
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "latent",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "EmptyLatentBatch"
|
||||
},
|
||||
"widgets_values": [
|
||||
"896×1152 - 7:9 (1.0MP) - SDXL",
|
||||
896,
|
||||
1152,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
-50,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
350,
|
||||
250
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"text": "Empty Latent Batch Example\n\nThis node creates empty latent tensors with:\n- 26 preset resolutions for SDXL/FLUX/Ultra-wide\n- Batch size support (1-64)\n- Interactive swap button\n- Memory usage estimation\n\nConnect to:\n- KSampler for image generation\n- Other latent processing nodes\n\nOutputs sanitized dimensions divisible by 8."
|
||||
},
|
||||
"widgets_values": [
|
||||
"Empty Latent Batch Example\n\nThis node creates empty latent tensors with:\n- 26 preset resolutions for SDXL/FLUX/Ultra-wide\n- Batch size support (1-64)\n- Interactive swap button\n- Memory usage estimation\n\nConnect to:\n- KSampler for image generation\n- Other latent processing nodes\n\nOutputs sanitized dimensions divisible by 8."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
760,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
770,
|
||||
500
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
1000,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
1010,
|
||||
500
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
5,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
5,
|
||||
1,
|
||||
2,
|
||||
6,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
7,
|
||||
5,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
8,
|
||||
6,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Empty Latent",
|
||||
"bounding": [
|
||||
-110,
|
||||
120,
|
||||
1440,
|
||||
620
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8264462809917358,
|
||||
"offset": [
|
||||
156.41790912499965,
|
||||
-2.272547312500285
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 278 KiB |
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"id": "gemini-prompt-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
300,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
1070,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
500,
|
||||
400
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "GeminiPrompt",
|
||||
"pos": [
|
||||
640,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
276
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "negative_prompt",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "GeminiPrompt"
|
||||
},
|
||||
"widgets_values": [
|
||||
"sdxl",
|
||||
"gemini-2.0-flash",
|
||||
"1234-1234-1234-1234",
|
||||
"",
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
300,
|
||||
410
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
240
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Gemini Prompt Engineer Example\n\nAI-powered image analysis:\n- Analyzes image content and style\n- Generates optimized prompts for FLUX/SDXL/Danbooru/Video\n- Dynamic model selection with refresh button\n- Custom system prompt override\n\nSetup:\n1. Get free API key from Google AI Studio\n2. Set GEMINI_API_KEY env var or use config file\n3. Click refresh button to get latest models\n\nDisplayText automatically shows split view for SDXL prompts!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
4,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Gemini Prompt Engineer",
|
||||
"bounding": [
|
||||
210,
|
||||
-110,
|
||||
1460,
|
||||
850
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7513148009015777,
|
||||
"offset": [
|
||||
97.5582232217803,
|
||||
133.12148846503663
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 404 KiB |
@@ -1,123 +1,552 @@
|
||||
{
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"id": "3cd0ac86-8d11-41e5-9d8b-6621bc810ee6",
|
||||
"revision": 0,
|
||||
"last_node_id": 21,
|
||||
"last_link_id": 21,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [100, 200],
|
||||
"size": [315, 314],
|
||||
"pos": [
|
||||
-80,
|
||||
850
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314.0000305175781
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [1],
|
||||
"shape": 3,
|
||||
"label": "IMAGE"
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "ImageToMultipleOf",
|
||||
"pos": [500, 200],
|
||||
"size": [315, 106],
|
||||
"id": 8,
|
||||
"type": "GetNode",
|
||||
"pos": [
|
||||
540,
|
||||
840
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
34
|
||||
],
|
||||
"flags": {
|
||||
"collapsed": true
|
||||
},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "UPSCALE_MODEL",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"title": "Get_upscale_skin_model",
|
||||
"properties": {
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"upscale_skin_model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "ImageUpscaleWithModel",
|
||||
"pos": [
|
||||
530,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
221.98202514648438,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "upscale_model",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"link": 16
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
10,
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "ImageUpscaleWithModel",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "ImageToMultipleOf",
|
||||
"pos": [
|
||||
1000,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
230,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [2, 3],
|
||||
"shape": 3,
|
||||
"label": "image"
|
||||
"links": [
|
||||
18
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageToMultipleOf"
|
||||
"cnr_id": "comfyui-glifnodes",
|
||||
"ver": "1.0.0",
|
||||
"Node name for S&R": "ImageToMultipleOf",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [64, "center crop"]
|
||||
"widgets_values": [
|
||||
65,
|
||||
"center crop"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PreviewImage",
|
||||
"pos": [900, 100],
|
||||
"size": [210, 246],
|
||||
"id": 13,
|
||||
"type": "ImageScaleBy",
|
||||
"pos": [
|
||||
760,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
220,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
6,
|
||||
19
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "ImageScaleBy",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"lanczos",
|
||||
0.5000000000000001
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "ImageUpscaleWithModel",
|
||||
"pos": [
|
||||
280,
|
||||
830
|
||||
],
|
||||
"size": [
|
||||
221.98202514648438,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "upscale_model",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"link": 15
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
5,
|
||||
20
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "ImageUpscaleWithModel",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "UpscaleModelLoader",
|
||||
"pos": [
|
||||
-90,
|
||||
720
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "UPSCALE_MODEL",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"links": [
|
||||
15
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "UpscaleModelLoader",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"4x-ClearRealityV1.pth"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "UpscaleModelLoader",
|
||||
"pos": [
|
||||
140,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "UPSCALE_MODEL",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"links": [
|
||||
16
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "UpscaleModelLoader",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"1xSkinContrast-High-SuperUltraCompact.pth"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1260,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
280
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "VAEEncode",
|
||||
"pos": [900, 400],
|
||||
"size": [210, 46],
|
||||
"id": 19,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1010,
|
||||
940
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pixels",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEEncode"
|
||||
}
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
520,
|
||||
940
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 20
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
750,
|
||||
940
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 2, 0, "IMAGE"],
|
||||
[2, 2, 0, 3, 0, "IMAGE"],
|
||||
[3, 2, 0, 4, 0, "IMAGE"]
|
||||
[
|
||||
5,
|
||||
14,
|
||||
0,
|
||||
9,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
6,
|
||||
13,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
9,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
13,
|
||||
1,
|
||||
0,
|
||||
14,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
15,
|
||||
15,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"UPSCALE_MODEL"
|
||||
],
|
||||
[
|
||||
16,
|
||||
16,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"UPSCALE_MODEL"
|
||||
],
|
||||
[
|
||||
18,
|
||||
10,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
19,
|
||||
13,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
20,
|
||||
14,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
21,
|
||||
9,
|
||||
0,
|
||||
21,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Image to Multiple of",
|
||||
"bounding": [
|
||||
-180,
|
||||
420,
|
||||
1800,
|
||||
830
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.6655312045216585,
|
||||
"offset": [
|
||||
292.51725658775155,
|
||||
-375.0766486499791
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 805 KiB |
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"id": "kiko-save-image-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "KikoSaveImage",
|
||||
"pos": [
|
||||
390,
|
||||
80
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
390
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "KikoSaveImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"KikoSave",
|
||||
"PNG",
|
||||
90,
|
||||
4,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-10,
|
||||
80
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
-10,
|
||||
460
|
||||
],
|
||||
"size": [
|
||||
370,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Kiko Save Image Example\n\nEnhanced image saving with:\n- Format selection: PNG, JPEG, WebP\n- Quality controls per format\n- Floating popup viewer (draggable)\n- Batch operations support\n- File size display\n\nPopup Features:\n- Click images to open in new tab\n- Download individual or selected images\n- Minimize/maximize/roll-up controls\n- Persistent across saves\n\nTry different formats to compare file sizes!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Kiko save Image",
|
||||
"bounding": [
|
||||
-110,
|
||||
-60,
|
||||
1020,
|
||||
870
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7513148009015777,
|
||||
"offset": [
|
||||
648.1018911400128,
|
||||
110.02838653698085
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 245 KiB |
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"id": "41469b2d-d616-479d-879a-95cdc6074a37",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 5,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ResolutionCalculator",
|
||||
"pos": [
|
||||
60,
|
||||
430
|
||||
-30,
|
||||
210
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
@@ -38,8 +38,7 @@
|
||||
"type": "INT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1,
|
||||
3
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -47,15 +46,15 @@
|
||||
"type": "INT",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
2,
|
||||
4
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "965ad60c74d7f25b1acce890d9c06518e46e6d0b",
|
||||
"Node name for S&R": "ResolutionCalculator",
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
@@ -66,8 +65,8 @@
|
||||
"id": 6,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-250,
|
||||
430
|
||||
-340,
|
||||
210
|
||||
],
|
||||
"size": [
|
||||
274.080078125,
|
||||
@@ -94,8 +93,8 @@
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "LoadImage"
|
||||
"Node name for S&R": "LoadImage",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"image-2025-06-13-105737.jpg",
|
||||
@@ -103,80 +102,14 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "Display Int (rgthree)",
|
||||
"id": 7,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
420,
|
||||
360
|
||||
-350,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"dir": 3,
|
||||
"name": "input",
|
||||
"type": "INT",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "rgthree-comfy",
|
||||
"ver": "1.0.2506081210",
|
||||
"Node name for S&R": "Display Int (rgthree)",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "Display Int (rgthree)",
|
||||
"pos": [
|
||||
430,
|
||||
530
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"dir": 3,
|
||||
"name": "input",
|
||||
"type": "INT",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "rgthree-comfy",
|
||||
"ver": "1.0.2506081210",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "Display Int (rgthree)"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
-150,
|
||||
170
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
410,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
@@ -184,50 +117,167 @@
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"text": "Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models.",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"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."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
330,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
330,
|
||||
310
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
670,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
660,
|
||||
400
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
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,
|
||||
@@ -235,25 +285,71 @@
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
6,
|
||||
1,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
7,
|
||||
1,
|
||||
1,
|
||||
9,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
8,
|
||||
8,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
9,
|
||||
9,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Resolution Calculator",
|
||||
"bounding": [
|
||||
-480,
|
||||
20,
|
||||
1480,
|
||||
880
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"ds": {
|
||||
"scale": 0.7972024500000015,
|
||||
"scale": 0.45000000000000145,
|
||||
"offset": [
|
||||
709.6391289161842,
|
||||
-90.0127389290854
|
||||
1248.1498802376432,
|
||||
256.0155843113725
|
||||
]
|
||||
},
|
||||
"links_added_by_ue": [],
|
||||
"frontendVersion": "1.21.7",
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 785 KiB |
|
After Width: | Height: | Size: 4.0 MiB |
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"name": "Sampler and Scheduler Comparison Workflow",
|
||||
"description": "Compare different sampler and scheduler combinations using xyz_helpers",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "1",
|
||||
"type": "SamplerSelectHelper",
|
||||
"title": "Select Optimal Sampler",
|
||||
"category": "ComfyAssets/🧰 xyz-helpers",
|
||||
"inputs": {
|
||||
"model_type": "auto",
|
||||
"quality_preset": "balanced",
|
||||
"sampler_override": "auto",
|
||||
"model_name": "sdxl_model.safetensors"
|
||||
},
|
||||
"outputs": {
|
||||
"sampler_name": "STRING",
|
||||
"scheduler": "STRING",
|
||||
"recommended_steps": "INT",
|
||||
"recommended_cfg": "FLOAT"
|
||||
},
|
||||
"pos": [100, 100]
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"type": "SchedulerSelectHelper",
|
||||
"title": "Optimize Scheduler",
|
||||
"category": "ComfyAssets/🧰 xyz-helpers",
|
||||
"inputs": {
|
||||
"sampler_name": ["1", "sampler_name"],
|
||||
"model_type": "sdxl",
|
||||
"schedule_type": "smooth",
|
||||
"visualize": true
|
||||
},
|
||||
"outputs": {
|
||||
"scheduler": "STRING",
|
||||
"schedule_curve": "IMAGE"
|
||||
},
|
||||
"pos": [400, 100]
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"type": "TextEncodeSamplerParams",
|
||||
"title": "Setup Text and Params",
|
||||
"category": "ComfyAssets/🧰 xyz-helpers",
|
||||
"inputs": {
|
||||
"text": "a majestic mountain landscape at sunset, highly detailed",
|
||||
"negative_text": "low quality, blurry, artifacts",
|
||||
"clip": ["model", "clip"],
|
||||
"sampler_name": ["1", "sampler_name"],
|
||||
"scheduler": ["2", "scheduler"],
|
||||
"steps": ["1", "recommended_steps"],
|
||||
"cfg": ["1", "recommended_cfg"],
|
||||
"template": "landscape"
|
||||
},
|
||||
"outputs": {
|
||||
"positive": "CONDITIONING",
|
||||
"negative": "CONDITIONING",
|
||||
"sampler_params": "DICT"
|
||||
},
|
||||
"pos": [700, 100]
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"type": "EmptyLatentBatch",
|
||||
"title": "Create Test Latents",
|
||||
"category": "ComfyAssets/📦 Latents",
|
||||
"inputs": {
|
||||
"preset": "1216×832 (SDXL Landscape)",
|
||||
"batch_size": 4
|
||||
},
|
||||
"outputs": {
|
||||
"latent": "LATENT"
|
||||
},
|
||||
"pos": [100, 300]
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"type": "KSampler",
|
||||
"title": "Generate with Optimal Settings",
|
||||
"inputs": {
|
||||
"model": ["model", "model"],
|
||||
"positive": ["3", "positive"],
|
||||
"negative": ["3", "negative"],
|
||||
"latent_image": ["4", "latent"],
|
||||
"sampler_name": ["1", "sampler_name"],
|
||||
"scheduler": ["2", "scheduler"],
|
||||
"steps": ["1", "recommended_steps"],
|
||||
"cfg": ["1", "recommended_cfg"],
|
||||
"seed": 42
|
||||
},
|
||||
"outputs": {
|
||||
"latent": "LATENT"
|
||||
},
|
||||
"pos": [1000, 200]
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"type": "PlotParameters",
|
||||
"title": "Visualize Parameters",
|
||||
"category": "ComfyAssets/🧰 xyz-helpers",
|
||||
"inputs": {
|
||||
"sampler_params": ["3", "sampler_params"],
|
||||
"plot_type": "bar",
|
||||
"x_axis": "parameter_name",
|
||||
"y_axis": "value",
|
||||
"title": "Sampler Configuration Analysis",
|
||||
"show_grid": true
|
||||
},
|
||||
"outputs": {
|
||||
"plot_image": "IMAGE"
|
||||
},
|
||||
"pos": [700, 400]
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"type": "DisplayAny",
|
||||
"title": "Show Schedule Curve",
|
||||
"category": "ComfyAssets/🔍 Debug",
|
||||
"inputs": {
|
||||
"input": ["2", "schedule_curve"],
|
||||
"mode": "tensor shape"
|
||||
},
|
||||
"pos": [400, 400]
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"type": "VAEDecode",
|
||||
"title": "Decode Results",
|
||||
"inputs": {
|
||||
"samples": ["5", "latent"],
|
||||
"vae": ["model", "vae"]
|
||||
},
|
||||
"outputs": {
|
||||
"image": "IMAGE"
|
||||
},
|
||||
"pos": [1300, 200]
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"type": "KikoSaveImage",
|
||||
"title": "Save Comparison",
|
||||
"category": "ComfyAssets/💾 Images",
|
||||
"inputs": {
|
||||
"images": ["8", "image"],
|
||||
"filename_prefix": "sampler_comparison",
|
||||
"format": "WEBP",
|
||||
"quality": 90,
|
||||
"popup": true
|
||||
},
|
||||
"pos": [1600, 200]
|
||||
}
|
||||
],
|
||||
"workflow_notes": {
|
||||
"purpose": "Compare and optimize sampler/scheduler combinations for best quality",
|
||||
"features": [
|
||||
"Automatic sampler selection based on model",
|
||||
"Scheduler optimization with visualization",
|
||||
"Parameter analysis and plotting",
|
||||
"Batch generation for comparison"
|
||||
],
|
||||
"tips": [
|
||||
"Try different quality_preset values",
|
||||
"Use visualize=true to see noise schedules",
|
||||
"Compare results across multiple seeds"
|
||||
],
|
||||
"attribution": "xyz_helpers nodes adapted from comfyui-essentials-nodes"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,18 @@ from .tools.sampler_combo import SamplerComboNode, SamplerComboCompactNode
|
||||
from .tools.empty_latent_batch import EmptyLatentBatchNode
|
||||
from .tools.kiko_save_image import KikoSaveImageNode
|
||||
from .tools.image_to_multiple_of import ImageToMultipleOfNode
|
||||
from .tools.image_scale_down_by import ImageScaleDownByNode
|
||||
from .tools.gemini_prompt import GeminiPromptNode
|
||||
from .tools.display_any import DisplayAnyNode
|
||||
from .tools.display_text import DisplayTextNode
|
||||
from .tools.xyz_helpers import (
|
||||
SamplerSelectHelperNode,
|
||||
SchedulerSelectHelperNode,
|
||||
TextEncodeSamplerParamsNode,
|
||||
FluxSamplerParamsNode,
|
||||
PlotParametersNode,
|
||||
LoRAFolderBatchNode,
|
||||
)
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -21,6 +33,16 @@ NODE_CLASS_MAPPINGS = {
|
||||
"EmptyLatentBatch": EmptyLatentBatchNode,
|
||||
"KikoSaveImage": KikoSaveImageNode,
|
||||
"ImageToMultipleOf": ImageToMultipleOfNode,
|
||||
"ImageScaleDownBy": ImageScaleDownByNode,
|
||||
"GeminiPrompt": GeminiPromptNode,
|
||||
"DisplayAny": DisplayAnyNode,
|
||||
"DisplayText": DisplayTextNode,
|
||||
"SamplerSelectHelper": SamplerSelectHelperNode,
|
||||
"SchedulerSelectHelper": SchedulerSelectHelperNode,
|
||||
"TextEncodeSamplerParams": TextEncodeSamplerParamsNode,
|
||||
"FluxSamplerParams": FluxSamplerParamsNode,
|
||||
"PlotParameters+": PlotParametersNode,
|
||||
"LoRAFolderBatch": LoRAFolderBatchNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -32,6 +54,16 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmptyLatentBatch": "Empty Latent Batch",
|
||||
"KikoSaveImage": "Kiko Save Image",
|
||||
"ImageToMultipleOf": "Image to Multiple of",
|
||||
"ImageScaleDownBy": "Image Scale Down By",
|
||||
"GeminiPrompt": "Gemini Prompt Engineer",
|
||||
"DisplayAny": "Display Any",
|
||||
"DisplayText": "Display Text",
|
||||
"SamplerSelectHelper": "Sampler Select Helper",
|
||||
"SchedulerSelectHelper": "Scheduler Select Helper",
|
||||
"TextEncodeSamplerParams": "Text Encode for Sampler Params",
|
||||
"FluxSamplerParams": "Flux Sampler Parameters",
|
||||
"PlotParameters+": "Plot Parameters",
|
||||
"LoRAFolderBatch": "LoRA Folder Batch",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -35,7 +35,9 @@ class ComfyAssetsBaseNode:
|
||||
"""
|
||||
pass
|
||||
|
||||
def handle_error(self, error_msg: str, exception: Optional[Exception] = None) -> None:
|
||||
def handle_error(
|
||||
self, error_msg: str, exception: Optional[Exception] = None
|
||||
) -> None:
|
||||
"""
|
||||
Standardized error handling with logging
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""DisplayAny tool for ComfyUI."""
|
||||
|
||||
from .node import DisplayAnyNode
|
||||
|
||||
__all__ = ["DisplayAnyNode"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Logic for DisplayAny node - displays any input value or tensor shape."""
|
||||
|
||||
from typing import Any, List, Union
|
||||
|
||||
|
||||
def get_tensor_shapes(input_value: Any) -> List[List[int]]:
|
||||
"""Extract tensor shapes from nested structures.
|
||||
|
||||
Args:
|
||||
input_value: Any input value that may contain tensors
|
||||
|
||||
Returns:
|
||||
List of tensor shapes found in the input
|
||||
"""
|
||||
shapes = []
|
||||
|
||||
def extract_shapes(value: Any) -> None:
|
||||
"""Recursively extract shapes from nested structures."""
|
||||
if isinstance(value, dict):
|
||||
for v in value.values():
|
||||
extract_shapes(v)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
extract_shapes(item)
|
||||
elif hasattr(value, "shape"):
|
||||
# Handle tensors (numpy arrays, torch tensors, etc.)
|
||||
shapes.append(list(value.shape))
|
||||
|
||||
extract_shapes(input_value)
|
||||
return shapes
|
||||
|
||||
|
||||
def format_display_value(input_value: Any, mode: str = "raw value") -> str:
|
||||
"""Format input value for display based on selected mode.
|
||||
|
||||
Args:
|
||||
input_value: Any input value to display
|
||||
mode: Display mode - "raw value" or "tensor shape"
|
||||
|
||||
Returns:
|
||||
Formatted string representation of the input
|
||||
"""
|
||||
if mode == "tensor shape":
|
||||
shapes = get_tensor_shapes(input_value)
|
||||
if shapes:
|
||||
return str(shapes)
|
||||
else:
|
||||
return "No tensors found in input"
|
||||
|
||||
# Default to raw value display
|
||||
# Try to format as JSON for better readability
|
||||
try:
|
||||
import json
|
||||
|
||||
if isinstance(input_value, (dict, list)):
|
||||
return json.dumps(input_value, indent=2)
|
||||
except:
|
||||
pass
|
||||
|
||||
return str(input_value)
|
||||
|
||||
|
||||
def validate_display_mode(mode: str) -> bool:
|
||||
"""Validate if the display mode is supported.
|
||||
|
||||
Args:
|
||||
mode: Display mode to validate
|
||||
|
||||
Returns:
|
||||
True if mode is valid, False otherwise
|
||||
"""
|
||||
valid_modes = ["raw value", "tensor shape"]
|
||||
return mode in valid_modes
|
||||
@@ -0,0 +1,67 @@
|
||||
"""DisplayAny node for ComfyUI - displays any input value or tensor information."""
|
||||
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
from .logic import format_display_value, validate_display_mode
|
||||
|
||||
|
||||
# Define AnyType for wildcard input matching
|
||||
class AnyType(str):
|
||||
"""A special type that matches any input type in ComfyUI."""
|
||||
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
|
||||
|
||||
class DisplayAnyNode(ComfyAssetsBaseNode):
|
||||
"""Display any input value or tensor shape information.
|
||||
|
||||
This node can display any type of input in two modes:
|
||||
- Raw value: Shows the string representation of the input
|
||||
- Tensor shape: Extracts and displays shapes of any tensors in the input
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
"""Define input types for the node."""
|
||||
return {
|
||||
"required": {
|
||||
"input": (AnyType("*"), {}), # Accept any type of input
|
||||
"mode": (["raw value", "tensor shape"],),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs) -> bool:
|
||||
"""Validate inputs - always returns True as we accept any input."""
|
||||
return True
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
CATEGORY = "ComfyAssets/👁️ Display"
|
||||
RETURN_NAMES = ("display_text",)
|
||||
FUNCTION = "display"
|
||||
OUTPUT_NODE = True # This node displays output in the UI
|
||||
|
||||
def display(self, input: Any, mode: str = "raw value") -> Dict[str, Any]:
|
||||
"""Display the input value according to the selected mode.
|
||||
|
||||
Args:
|
||||
input: Any input value to display
|
||||
mode: Display mode - "raw value" or "tensor shape"
|
||||
|
||||
Returns:
|
||||
Dictionary with UI display and result
|
||||
"""
|
||||
# Validate mode
|
||||
if not validate_display_mode(mode):
|
||||
mode = "raw value" # Default to raw value if invalid
|
||||
|
||||
# Format the display text
|
||||
display_text = format_display_value(input, mode)
|
||||
|
||||
# Return both UI display and result
|
||||
return {
|
||||
"ui": {"text": [display_text]}, # UI expects array
|
||||
"result": (display_text,),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Display Text tool for ComfyUI."""
|
||||
|
||||
from .node import DisplayTextNode, NODE_DISPLAY_NAME
|
||||
|
||||
__all__ = ["DisplayTextNode", "NODE_DISPLAY_NAME"]
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Display Text node implementation."""
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
|
||||
|
||||
class DisplayTextNode(ComfyAssetsBaseNode):
|
||||
"""Displays text in the ComfyUI interface with copy-to-clipboard functionality."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define input types for the node."""
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "display_text"
|
||||
CATEGORY = "ComfyAssets/👁️ Display"
|
||||
|
||||
DESCRIPTION = """
|
||||
Displays text in the UI with a copy-to-clipboard feature.
|
||||
|
||||
Features:
|
||||
- Shows text content in a readable format
|
||||
- Copy button appears on hover
|
||||
- Passes text through for chaining
|
||||
"""
|
||||
|
||||
def display_text(self, text):
|
||||
"""Display the text and pass it through.
|
||||
|
||||
Args:
|
||||
text: Input text to display
|
||||
|
||||
Returns:
|
||||
Tuple containing the text
|
||||
"""
|
||||
# The actual display happens in the frontend
|
||||
# We just pass the text through
|
||||
return {"ui": {"text": [text]}, "result": (text,)}
|
||||
|
||||
|
||||
# Node display name
|
||||
NODE_DISPLAY_NAME = "Display Text"
|
||||
@@ -4,7 +4,9 @@ import torch
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
def create_empty_latent_batch(width: int, height: int, batch_size: int = 1) -> Dict[str, torch.Tensor]:
|
||||
def create_empty_latent_batch(
|
||||
width: int, height: int, batch_size: int = 1
|
||||
) -> Dict[str, torch.Tensor]:
|
||||
"""
|
||||
Create empty latent tensor with batch support.
|
||||
|
||||
@@ -28,7 +30,9 @@ def create_empty_latent_batch(width: int, height: int, batch_size: int = 1) -> D
|
||||
|
||||
# Ensure dimensions are divisible by 8 (VAE requirement)
|
||||
if width % 8 != 0 or height % 8 != 0:
|
||||
raise ValueError(f"Width and height must be divisible by 8, got {width}x{height}")
|
||||
raise ValueError(
|
||||
f"Width and height must be divisible by 8, got {width}x{height}"
|
||||
)
|
||||
|
||||
# Convert pixel dimensions to latent space (divide by 8)
|
||||
latent_width = width // 8
|
||||
|
||||
@@ -36,7 +36,8 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
metadata = PRESET_METADATA.get(preset_name)
|
||||
if metadata:
|
||||
formatted_option = (
|
||||
f"{preset_name} - {metadata.aspect_ratio} " f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
|
||||
f"{preset_name} - {metadata.aspect_ratio} "
|
||||
f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
|
||||
)
|
||||
preset_options.append(formatted_option)
|
||||
else:
|
||||
@@ -85,7 +86,8 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
"min": 1,
|
||||
"max": 64,
|
||||
"step": 1,
|
||||
"tooltip": "Number of empty latents to create in the batch. " "Useful for batch processing workflows.",
|
||||
"tooltip": "Number of empty latents to create in the batch. "
|
||||
"Useful for batch processing workflows.",
|
||||
},
|
||||
),
|
||||
}
|
||||
@@ -94,7 +96,7 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
RETURN_TYPES = ("LATENT", "INT", "INT")
|
||||
RETURN_NAMES = ("latent", "width", "height")
|
||||
FUNCTION = "create_empty_latent"
|
||||
CATEGORY = "ComfyAssets"
|
||||
CATEGORY = "ComfyAssets/📦 Latents"
|
||||
|
||||
def create_empty_latent(
|
||||
self, preset: str, width: int, height: int, batch_size: int
|
||||
@@ -116,7 +118,9 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
original_preset = self._extract_preset_name(preset)
|
||||
|
||||
# Get base dimensions from preset or custom input
|
||||
base_width, base_height = get_preset_dimensions(original_preset, width, height)
|
||||
base_width, base_height = get_preset_dimensions(
|
||||
original_preset, width, height
|
||||
)
|
||||
|
||||
# Sanitize dimensions to ensure they meet requirements
|
||||
final_width, final_height = sanitize_dimensions(base_width, base_height)
|
||||
@@ -130,17 +134,23 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
|
||||
# Validate final dimensions
|
||||
if not validate_dimensions(final_width, final_height):
|
||||
self.handle_error(f"Invalid dimensions after sanitization: {final_width}×{final_height}")
|
||||
self.handle_error(
|
||||
f"Invalid dimensions after sanitization: {final_width}×{final_height}"
|
||||
)
|
||||
|
||||
# Validate batch size
|
||||
if batch_size <= 0:
|
||||
self.handle_error(f"Batch size must be positive, got {batch_size}")
|
||||
|
||||
if batch_size > 64:
|
||||
self.log_info(f"Large batch size ({batch_size}) may use significant memory")
|
||||
self.log_info(
|
||||
f"Large batch size ({batch_size}) may use significant memory"
|
||||
)
|
||||
|
||||
# Create the empty latent batch
|
||||
latent_dict = create_empty_latent_batch(final_width, final_height, batch_size)
|
||||
latent_dict = create_empty_latent_batch(
|
||||
final_width, final_height, batch_size
|
||||
)
|
||||
|
||||
# Log the operation
|
||||
latent_height = final_height // 8
|
||||
@@ -188,7 +198,9 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
# Default to "custom" if we can't parse it
|
||||
return "custom"
|
||||
|
||||
def validate_inputs(self, preset: str, width: int, height: int, batch_size: int) -> bool:
|
||||
def validate_inputs(
|
||||
self, preset: str, width: int, height: int, batch_size: int
|
||||
) -> bool:
|
||||
"""
|
||||
Validate node inputs.
|
||||
|
||||
@@ -279,7 +291,12 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Detailed string representation of the node."""
|
||||
return f"EmptyLatentBatchNode(" f"category='{self.CATEGORY}', " f"function='{self.FUNCTION}'" f")"
|
||||
return (
|
||||
f"EmptyLatentBatchNode("
|
||||
f"category='{self.CATEGORY}', "
|
||||
f"function='{self.FUNCTION}'"
|
||||
f")"
|
||||
)
|
||||
|
||||
|
||||
# Node class mappings for ComfyUI registration
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"models": [
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
"gemini-2.5-flash-preview-05-20",
|
||||
"gemini-2.5-pro-preview-05-06",
|
||||
"gemini-2.5-pro-preview-06-05",
|
||||
"gemini-2.5-flash-lite-preview-06-17",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-001",
|
||||
"gemini-2.0-flash-lite-001",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-2.5-flash-preview-tts",
|
||||
"gemini-2.5-pro-preview-tts",
|
||||
"gemini-2.0-flash-preview-image-generation",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-exp-image-generation",
|
||||
"gemini-2.0-flash-lite-preview-02-05",
|
||||
"gemini-2.0-flash-lite-preview",
|
||||
"gemini-2.0-pro-exp",
|
||||
"gemini-2.0-pro-exp-02-05",
|
||||
"learnlm-2.0-flash-experimental",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-pro-002",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash-latest",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-flash-002",
|
||||
"gemini-1.5-flash-8b",
|
||||
"gemini-1.5-flash-8b-001",
|
||||
"gemini-1.5-flash-8b-latest",
|
||||
"gemini-2.0-flash-thinking-exp-01-21",
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-thinking-exp-1219",
|
||||
"gemma-3-1b-it",
|
||||
"gemma-3-4b-it",
|
||||
"gemma-3-12b-it",
|
||||
"gemma-3-27b-it",
|
||||
"gemma-3n-e4b-it",
|
||||
"gemma-3n-e2b-it",
|
||||
"gemini-exp-1206"
|
||||
],
|
||||
"descriptions": {
|
||||
"gemini-1.5-pro-latest": "Gemini 1.5 Pro Latest",
|
||||
"gemini-1.5-pro-002": "Gemini 1.5 Pro 002",
|
||||
"gemini-1.5-pro": "Gemini 1.5 Pro",
|
||||
"gemini-1.5-flash-latest": "Gemini 1.5 Flash Latest",
|
||||
"gemini-1.5-flash": "Gemini 1.5 Flash",
|
||||
"gemini-1.5-flash-002": "Gemini 1.5 Flash 002",
|
||||
"gemini-1.5-flash-8b": "Gemini 1.5 Flash-8B",
|
||||
"gemini-1.5-flash-8b-001": "Gemini 1.5 Flash-8B 001",
|
||||
"gemini-1.5-flash-8b-latest": "Gemini 1.5 Flash-8B Latest",
|
||||
"gemini-2.5-pro-preview-03-25": "Gemini 2.5 Pro Preview 03-25",
|
||||
"gemini-2.5-flash-preview-05-20": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.5-flash": "Gemini 2.5 Flash",
|
||||
"gemini-2.5-flash-lite-preview-06-17": "Gemini 2.5 Flash-Lite Preview 06-17",
|
||||
"gemini-2.5-pro-preview-05-06": "Gemini 2.5 Pro Preview 05-06",
|
||||
"gemini-2.5-pro-preview-06-05": "Gemini 2.5 Pro Preview",
|
||||
"gemini-2.5-pro": "Gemini 2.5 Pro",
|
||||
"gemini-2.0-flash-exp": "Gemini 2.0 Flash Experimental",
|
||||
"gemini-2.0-flash": "Gemini 2.0 Flash",
|
||||
"gemini-2.0-flash-001": "Gemini 2.0 Flash 001",
|
||||
"gemini-2.0-flash-exp-image-generation": "Gemini 2.0 Flash (Image Generation) Experimental",
|
||||
"gemini-2.0-flash-lite-001": "Gemini 2.0 Flash-Lite 001",
|
||||
"gemini-2.0-flash-lite": "Gemini 2.0 Flash-Lite",
|
||||
"gemini-2.0-flash-preview-image-generation": "Gemini 2.0 Flash Preview Image Generation",
|
||||
"gemini-2.0-flash-lite-preview-02-05": "Gemini 2.0 Flash-Lite Preview 02-05",
|
||||
"gemini-2.0-flash-lite-preview": "Gemini 2.0 Flash-Lite Preview",
|
||||
"gemini-2.0-pro-exp": "Gemini 2.0 Pro Experimental",
|
||||
"gemini-2.0-pro-exp-02-05": "Gemini 2.0 Pro Experimental 02-05",
|
||||
"gemini-exp-1206": "Gemini Experimental 1206",
|
||||
"gemini-2.0-flash-thinking-exp-01-21": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.0-flash-thinking-exp": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.0-flash-thinking-exp-1219": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.5-flash-preview-tts": "Gemini 2.5 Flash Preview TTS",
|
||||
"gemini-2.5-pro-preview-tts": "Gemini 2.5 Pro Preview TTS",
|
||||
"learnlm-2.0-flash-experimental": "LearnLM 2.0 Flash Experimental",
|
||||
"gemma-3-1b-it": "Gemma 3 1B",
|
||||
"gemma-3-4b-it": "Gemma 3 4B",
|
||||
"gemma-3-12b-it": "Gemma 3 12B",
|
||||
"gemma-3-27b-it": "Gemma 3 27B",
|
||||
"gemma-3n-e4b-it": "Gemma 3n E4B",
|
||||
"gemma-3n-e2b-it": "Gemma 3n E2B",
|
||||
"gemini-2.5-flash-lite": "Gemini 2.5 Flash-Lite"
|
||||
},
|
||||
"timestamp": 1754568195.1098156
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Gemini Prompt Engineer node for ComfyUI."""
|
||||
|
||||
from .node import GeminiPromptNode
|
||||
|
||||
__all__ = ["GeminiPromptNode"]
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Logic for Gemini API integration and prompt generation."""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from .prompts import PROMPT_TEMPLATES
|
||||
|
||||
|
||||
def tensor_to_pil(tensor: np.ndarray) -> Image.Image:
|
||||
"""Convert ComfyUI tensor to PIL Image.
|
||||
|
||||
Args:
|
||||
tensor: Input tensor in ComfyUI format (B, H, W, C)
|
||||
|
||||
Returns:
|
||||
PIL Image object
|
||||
"""
|
||||
# ComfyUI tensors are in [0, 1] range
|
||||
if tensor.ndim == 4:
|
||||
# Take first image from batch
|
||||
tensor = tensor[0]
|
||||
|
||||
# Convert to uint8
|
||||
image_array = (tensor * 255).astype(np.uint8)
|
||||
|
||||
# Convert to PIL
|
||||
return Image.fromarray(image_array, mode="RGB")
|
||||
|
||||
|
||||
def image_to_base64(image: Image.Image, format: str = "PNG") -> str:
|
||||
"""Convert PIL Image to base64 string.
|
||||
|
||||
Args:
|
||||
image: PIL Image object
|
||||
format: Image format (PNG or JPEG)
|
||||
|
||||
Returns:
|
||||
Base64 encoded string
|
||||
"""
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format=format)
|
||||
buffer.seek(0)
|
||||
return base64.b64encode(buffer.read()).decode("utf-8")
|
||||
|
||||
|
||||
def get_api_key() -> Optional[str]:
|
||||
"""Get Gemini API key from environment or config.
|
||||
|
||||
Returns:
|
||||
API key string or None if not found
|
||||
"""
|
||||
# Check environment variable first
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
|
||||
if not api_key:
|
||||
# Check for config file in ComfyUI directory
|
||||
try:
|
||||
config_path = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "..", "gemini_config.json"
|
||||
)
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
api_key = config.get("api_key")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return api_key
|
||||
|
||||
|
||||
def analyze_image_with_gemini(
|
||||
image: np.ndarray,
|
||||
prompt_type: str,
|
||||
api_key: Optional[str] = None,
|
||||
custom_prompt: Optional[str] = None,
|
||||
model_name: str = "gemini-1.5-flash",
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""Analyze image using Gemini API and generate appropriate prompt.
|
||||
|
||||
Args:
|
||||
image: Input image tensor
|
||||
prompt_type: Type of prompt to generate (flux, sdxl, danbooru, video)
|
||||
api_key: Gemini API key (optional, will try to get from env/config)
|
||||
custom_prompt: Custom system prompt to use instead of templates
|
||||
model_name: Gemini model to use (default: gemini-1.5-flash)
|
||||
|
||||
Returns:
|
||||
Tuple of (generated_prompt, error_message)
|
||||
"""
|
||||
# Get API key
|
||||
if not api_key:
|
||||
api_key = get_api_key()
|
||||
|
||||
if not api_key:
|
||||
return (
|
||||
"",
|
||||
"Gemini API key not found. Please set GEMINI_API_KEY environment variable or provide it in the node.",
|
||||
)
|
||||
|
||||
# Convert tensor to PIL image
|
||||
try:
|
||||
pil_image = tensor_to_pil(image)
|
||||
except Exception as e:
|
||||
return "", f"Failed to convert image: {str(e)}"
|
||||
|
||||
# Get system prompt
|
||||
if custom_prompt:
|
||||
system_prompt = custom_prompt
|
||||
else:
|
||||
system_prompt = PROMPT_TEMPLATES.get(prompt_type, PROMPT_TEMPLATES["flux"])
|
||||
|
||||
# Here we would normally make the API call to Gemini
|
||||
# For now, we'll import the google-generativeai library
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
except ImportError:
|
||||
return (
|
||||
"",
|
||||
"google-generativeai library not installed. Please run: pip install google-generativeai",
|
||||
)
|
||||
|
||||
try:
|
||||
# Configure Gemini
|
||||
genai.configure(api_key=api_key)
|
||||
|
||||
# Create model
|
||||
model = genai.GenerativeModel(model_name)
|
||||
|
||||
# Generate content
|
||||
response = model.generate_content(
|
||||
[
|
||||
system_prompt,
|
||||
pil_image,
|
||||
"Analyze this image and generate an appropriate prompt according to the instructions.",
|
||||
]
|
||||
)
|
||||
|
||||
# Extract text from response
|
||||
if response.text:
|
||||
return response.text.strip(), None
|
||||
else:
|
||||
return "", "No response generated from Gemini"
|
||||
|
||||
except Exception as e:
|
||||
return "", f"Gemini API error: {str(e)}"
|
||||
|
||||
|
||||
def validate_prompt_type(prompt_type: str) -> bool:
|
||||
"""Validate if prompt type is supported.
|
||||
|
||||
Args:
|
||||
prompt_type: Type of prompt to validate
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
return prompt_type in PROMPT_TEMPLATES
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Dynamic model fetching and caching for Gemini API."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Cache settings
|
||||
CACHE_DURATION = 3600 * 24 # 24 hours in seconds
|
||||
CACHE_FILE = os.path.join(os.path.dirname(__file__), ".gemini_models_cache.json")
|
||||
|
||||
|
||||
def get_available_models(
|
||||
api_key: Optional[str] = None, silent: bool = False
|
||||
) -> Tuple[List[str], Dict[str, str]]:
|
||||
"""Fetch available Gemini models that support generateContent.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key. If not provided, will try to get from environment.
|
||||
silent: If True, suppress error logging (useful for initial load).
|
||||
|
||||
Returns:
|
||||
Tuple of (model_names_list, model_descriptions_dict)
|
||||
"""
|
||||
# Check cache first
|
||||
cached_data = _load_cache()
|
||||
if cached_data:
|
||||
return cached_data["models"], cached_data["descriptions"]
|
||||
|
||||
# Try to fetch from API
|
||||
try:
|
||||
models, descriptions = _fetch_models_from_api(api_key, silent=silent)
|
||||
if models:
|
||||
_save_cache(models, descriptions)
|
||||
return models, descriptions
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
logger.warning(f"Failed to fetch models from API: {e}")
|
||||
|
||||
# Fall back to defaults
|
||||
from .prompts import DEFAULT_GEMINI_MODELS
|
||||
|
||||
return DEFAULT_GEMINI_MODELS, {}
|
||||
|
||||
|
||||
def _fetch_models_from_api(
|
||||
api_key: Optional[str] = None, silent: bool = False
|
||||
) -> Tuple[List[str], Dict[str, str]]:
|
||||
"""Fetch models from Gemini API.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key.
|
||||
silent: If True, suppress error logging.
|
||||
|
||||
Returns:
|
||||
Tuple of (model_names_list, model_descriptions_dict)
|
||||
"""
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
except ImportError:
|
||||
if not silent:
|
||||
logger.error("google-generativeai not installed")
|
||||
return [], {}
|
||||
|
||||
# Get API key
|
||||
if not api_key:
|
||||
from .logic import get_api_key
|
||||
|
||||
api_key = get_api_key()
|
||||
|
||||
if not api_key:
|
||||
if not silent:
|
||||
logger.debug("No API key available for fetching models")
|
||||
return [], {}
|
||||
|
||||
try:
|
||||
genai.configure(api_key=api_key)
|
||||
|
||||
models = []
|
||||
descriptions = {}
|
||||
|
||||
# Fetch all models
|
||||
for model in genai.list_models():
|
||||
# Only include models that support generateContent
|
||||
if "generateContent" in model.supported_generation_methods:
|
||||
# Remove "models/" prefix from name
|
||||
model_name = model.name.replace("models/", "")
|
||||
models.append(model_name)
|
||||
descriptions[model_name] = model.display_name
|
||||
|
||||
# Sort models by priority (newer versions first)
|
||||
models = _sort_models(models)
|
||||
|
||||
return models, descriptions
|
||||
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
logger.error(f"Error fetching models from API: {e}")
|
||||
return [], {}
|
||||
|
||||
|
||||
def _sort_models(models: List[str]) -> List[str]:
|
||||
"""Sort models by version and capability.
|
||||
|
||||
Prioritizes:
|
||||
1. Newer versions (2.5 > 2.0 > 1.5)
|
||||
2. Non-experimental models
|
||||
3. Flash models for general use
|
||||
"""
|
||||
|
||||
def sort_key(model: str):
|
||||
# Priority scoring
|
||||
score = 0
|
||||
|
||||
# Version priority
|
||||
if "2.5" in model:
|
||||
score += 1000
|
||||
elif "2.0" in model:
|
||||
score += 800
|
||||
elif "1.5" in model:
|
||||
score += 600
|
||||
|
||||
# Model type priority
|
||||
if "pro" in model and "preview" not in model and "exp" not in model:
|
||||
score += 100
|
||||
elif "flash" in model and "preview" not in model and "exp" not in model:
|
||||
score += 90
|
||||
|
||||
# Penalize experimental/preview models
|
||||
if "exp" in model or "experimental" in model:
|
||||
score -= 50
|
||||
if "preview" in model:
|
||||
score -= 30
|
||||
|
||||
# Penalize specific variants
|
||||
if "thinking" in model:
|
||||
score -= 100
|
||||
if "tts" in model:
|
||||
score -= 100
|
||||
if "lite" in model:
|
||||
score -= 20
|
||||
|
||||
return -score # Negative for descending sort
|
||||
|
||||
return sorted(models, key=sort_key)
|
||||
|
||||
|
||||
def _load_cache() -> Optional[Dict]:
|
||||
"""Load cached model data if available and not expired."""
|
||||
if not os.path.exists(CACHE_FILE):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if cache is expired
|
||||
if time.time() - data.get("timestamp", 0) > CACHE_DURATION:
|
||||
return None
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cache: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _save_cache(models: List[str], descriptions: Dict[str, str]) -> None:
|
||||
"""Save model data to cache."""
|
||||
try:
|
||||
data = {
|
||||
"models": models,
|
||||
"descriptions": descriptions,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
with open(CACHE_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save cache: {e}")
|
||||
|
||||
|
||||
def clear_cache() -> None:
|
||||
"""Clear the model cache."""
|
||||
if os.path.exists(CACHE_FILE):
|
||||
try:
|
||||
os.remove(CACHE_FILE)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clear cache: {e}")
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Gemini Prompt Engineer node implementation."""
|
||||
|
||||
import torch
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
|
||||
from .logic import analyze_image_with_gemini, validate_prompt_type
|
||||
from .prompts import PROMPT_OPTIONS, DEFAULT_GEMINI_MODELS
|
||||
from .models import get_available_models
|
||||
|
||||
|
||||
class GeminiPromptNode(ComfyAssetsBaseNode):
|
||||
"""Analyzes images using Gemini AI to generate optimized prompts for various AI models."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define input types for the node."""
|
||||
# Get available models dynamically (silent mode for initial load)
|
||||
models, _ = get_available_models(silent=True)
|
||||
|
||||
# Use default if no models available
|
||||
if not models:
|
||||
models = DEFAULT_GEMINI_MODELS
|
||||
|
||||
# Find best default model
|
||||
default_model = models[0] if models else "gemini-2.5-flash"
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"prompt_type": (PROMPT_OPTIONS, {"default": "flux"}),
|
||||
"model": (models, {"default": default_model}),
|
||||
},
|
||||
"optional": {
|
||||
"api_key": ("STRING", {"default": "", "multiline": False}),
|
||||
"custom_prompt": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Optional: Enter custom system prompt instead of using templates",
|
||||
},
|
||||
),
|
||||
"refresh_models": (
|
||||
"BOOLEAN",
|
||||
{"default": False, "label_on": "Refresh", "label_off": "Skip"},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "negative_prompt")
|
||||
FUNCTION = "generate_prompt"
|
||||
CATEGORY = "ComfyAssets/🧠 Prompts"
|
||||
|
||||
DESCRIPTION = """
|
||||
Analyzes images using Google's Gemini AI to generate optimized prompts.
|
||||
|
||||
Supports multiple prompt formats:
|
||||
- FLUX: Detailed artistic prompts with quality markers
|
||||
- SDXL: Positive/negative prompt pairs with weight emphasis
|
||||
- Danbooru: Anime-style booru tags with underscores
|
||||
- Video: Motion and temporal descriptions for video generation
|
||||
|
||||
Requires Gemini API key (set GEMINI_API_KEY env var or provide in node).
|
||||
Install: pip install google-generativeai
|
||||
"""
|
||||
|
||||
def generate_prompt(
|
||||
self,
|
||||
image,
|
||||
prompt_type,
|
||||
model,
|
||||
api_key="",
|
||||
custom_prompt="",
|
||||
refresh_models=False,
|
||||
):
|
||||
"""Generate prompt from image using Gemini.
|
||||
|
||||
Args:
|
||||
image: Input image tensor
|
||||
prompt_type: Type of prompt to generate
|
||||
model: Gemini model to use
|
||||
api_key: Optional API key
|
||||
custom_prompt: Optional custom system prompt
|
||||
refresh_models: Whether to refresh the model list
|
||||
|
||||
Returns:
|
||||
Tuple of (prompt, negative_prompt)
|
||||
"""
|
||||
# Refresh models if requested
|
||||
if refresh_models and api_key:
|
||||
try:
|
||||
from .models import clear_cache
|
||||
|
||||
# Clear cache to force refresh on next node creation
|
||||
clear_cache()
|
||||
print(
|
||||
"Model cache cleared. Please recreate the node to see updated models."
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to clear model cache: {e}")
|
||||
|
||||
# Validate prompt type
|
||||
if not validate_prompt_type(prompt_type):
|
||||
raise ValueError(f"Invalid prompt type: {prompt_type}")
|
||||
|
||||
# Convert torch tensor to numpy if needed
|
||||
if isinstance(image, torch.Tensor):
|
||||
image_np = image.cpu().numpy()
|
||||
else:
|
||||
image_np = image
|
||||
|
||||
# If API key is provided, try to refresh model list in background
|
||||
if api_key:
|
||||
try:
|
||||
from .models import get_available_models
|
||||
|
||||
# Try to get fresh models with the provided API key
|
||||
fresh_models, _ = get_available_models(api_key=api_key, silent=True)
|
||||
if fresh_models and fresh_models != DEFAULT_GEMINI_MODELS:
|
||||
# Models were successfully fetched with this API key
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Analyze image with Gemini
|
||||
prompt, error = analyze_image_with_gemini(
|
||||
image_np,
|
||||
prompt_type,
|
||||
api_key=api_key or None,
|
||||
custom_prompt=custom_prompt or None,
|
||||
model_name=model,
|
||||
)
|
||||
|
||||
if error:
|
||||
# Return error as prompt for visibility
|
||||
return (f"Error: {error}", "")
|
||||
|
||||
# Handle different prompt types
|
||||
if prompt_type == "sdxl":
|
||||
# SDXL returns positive and negative prompts
|
||||
lines = prompt.split("\n")
|
||||
positive_prompt = ""
|
||||
negative_prompt = ""
|
||||
|
||||
for line in lines:
|
||||
if line.lower().startswith("positive:"):
|
||||
positive_prompt = (
|
||||
line.replace("Positive:", "").replace("positive:", "").strip()
|
||||
)
|
||||
elif line.lower().startswith("negative:"):
|
||||
negative_prompt = (
|
||||
line.replace("Negative:", "").replace("negative:", "").strip()
|
||||
)
|
||||
|
||||
# If format not found, assume entire response is positive prompt
|
||||
if not positive_prompt:
|
||||
positive_prompt = prompt
|
||||
|
||||
return (positive_prompt, negative_prompt)
|
||||
|
||||
else:
|
||||
# Other formats don't use negative prompts
|
||||
return (prompt, "")
|
||||
|
||||
|
||||
# Node display name
|
||||
NODE_DISPLAY_NAME = "Gemini Prompt Engineer"
|
||||
@@ -0,0 +1,128 @@
|
||||
"""System prompts for different AI model types."""
|
||||
|
||||
FLUX_PROMPT = """You are an expert FLUX prompt engineer. Analyze the provided image and generate ONLY a FLUX prompt - no explanations, analysis, or additional text.
|
||||
|
||||
FLUX uses natural language descriptions, not comma-separated tags. Write a detailed, flowing description that reads like you're explaining the image to someone.
|
||||
|
||||
Include these elements in your description:
|
||||
- Main subject with specific details (appearance, clothing, expression, pose)
|
||||
- Environment and background details
|
||||
- Lighting conditions and atmosphere
|
||||
- Artistic style or photographic approach
|
||||
- Color palette and mood
|
||||
- Technical details if relevant (camera angle, focal length, etc.)
|
||||
- Textures and materials
|
||||
|
||||
Write in a natural, descriptive style. Use complete sentences that flow together. Be specific and detailed but maintain readability.
|
||||
|
||||
IMPORTANT: Return ONLY the prompt text. No analysis, headers, or additional commentary. Just the natural language description that can be directly used in FLUX.
|
||||
|
||||
Example of correct output:
|
||||
A close-up portrait of a middle-aged woman with curly red hair and green eyes, wearing a blue silk blouse. She has a warm smile and freckles across her cheeks. The lighting is soft and natural, coming from a window to her left, creating gentle shadows that accentuate her features. The background is softly blurred, showing hints of a cozy bookshelf. The overall mood is warm and inviting, captured in a photorealistic style with shallow depth of field."""
|
||||
|
||||
SDXL_PROMPT = """You are an expert prompt engineer specializing in SDXL (Stable Diffusion XL). Your task is to generate high-quality positive and negative prompts that conform to SDXL prompt formatting standards.
|
||||
|
||||
Your expertise includes:
|
||||
- Leveraging community-tested techniques (ComfyUI, A1111, InvokeAI)
|
||||
- Applying photographic theory for realism, composition, lighting
|
||||
- Following Civitai trend standards and style best practices
|
||||
- Mastering Pony Diffusion XL formatting for stylized and anime content
|
||||
|
||||
Structure prompts in this layered, modular format:
|
||||
[Main Subject], [Pose & Camera], [Lighting & Environment], [Style & Details], [Boost Terms], [Style References]
|
||||
|
||||
For SDXL specifically:
|
||||
- Use quality boosters: 8k, RAW photo, masterpiece, ultra detailed, cinematic lighting
|
||||
- Prioritize realism and artistry
|
||||
- Excellent for portraits, landscapes, or cinematic scenes
|
||||
|
||||
Instructions:
|
||||
|
||||
Only reply with two fields:
|
||||
Positive prompt: (Your positive prompt here)
|
||||
Negative prompt: (Your negative prompt here)
|
||||
|
||||
Do not include any commentary or explanation.
|
||||
|
||||
Use concise, highly descriptive language that maximizes visual richness.
|
||||
|
||||
Follow SDXL prompt conventions: prioritize subject clarity, camera perspective, lighting, mood, style tags, and composition.
|
||||
|
||||
Keep total token length efficient (ideally under 250 tokens).
|
||||
|
||||
Avoid redundancy and generic filler words.
|
||||
|
||||
Focus on crafting super high-quality prompts for stunning visual output.
|
||||
|
||||
Example Input:
|
||||
A futuristic cyberpunk samurai standing on a neon-lit rooftop in the rain.
|
||||
|
||||
Example Output:
|
||||
Positive prompt: cyberpunk samurai, neon-lit rooftop, dramatic rain, glowing katana, futuristic cityscape, night scene, cinematic lighting, intense expression, sleek cyber armor, atmospheric depth, ultra-detailed, masterpiece, 8k, sharp focus, trending on artstation
|
||||
Negative prompt: blurry, low quality, poorly drawn, extra limbs, bad anatomy, deformed hands, text, watermark, jpeg artifacts, duplicate, cropped, out of frame
|
||||
"""
|
||||
|
||||
DANBOORU_PROMPT = """You are a Danbooru tagging expert specializing in anime-style image tagging. Analyze the image and generate ONLY Danbooru-style tags - no explanations or analysis.
|
||||
|
||||
CRITICAL: Use strict Danbooru conventions:
|
||||
- Use underscores for multi-word tags (e.g., long_hair, school_uniform)
|
||||
- All tags must be lowercase
|
||||
- Character count comes first (1girl, 2boys, multiple_girls)
|
||||
- For anime models trained on Danbooru data, proper tagging is essential
|
||||
|
||||
Tag order and categories:
|
||||
1. Character count (1girl, solo, 2boys, etc.)
|
||||
2. Character features (hair_color, eye_color, hair_length)
|
||||
3. Expression/pose (smile, looking_at_viewer, sitting)
|
||||
4. Clothing (specific items with underscores)
|
||||
5. Background/setting (simple_background, outdoors, classroom)
|
||||
6. View/composition (upper_body, full_body, from_side)
|
||||
7. Quality tags (masterpiece, best_quality, highres)
|
||||
|
||||
Common quality prefix for anime models:
|
||||
"masterpiece, best_quality, very_aesthetic"
|
||||
|
||||
IMPORTANT: Return ONLY the comma-separated tags. Use underscores, not spaces. All lowercase.
|
||||
|
||||
Example of correct output:
|
||||
1girl, solo, long_hair, blue_eyes, blonde_hair, school_uniform, serafuku, pleated_skirt, smile, looking_at_viewer, classroom, sitting, desk, window, sunlight, upper_body, masterpiece, best_quality"""
|
||||
|
||||
VIDEO_PROMPT = """You are a WAN 2.2 video generation prompt specialist. Analyze the content and generate ONLY a video generation prompt optimized for WAN 2.2 - no explanations or analysis.
|
||||
|
||||
WAN 2.2 excels with rich, descriptive prompts that focus on:
|
||||
- Visual composition and scene elements
|
||||
- Specific movements and actions
|
||||
- Lighting and aesthetic details
|
||||
- Cinematographic elements
|
||||
|
||||
Write a single detailed paragraph describing the video scene. Focus on:
|
||||
- Main subjects and their actions
|
||||
- Visual style and atmosphere
|
||||
- Movement dynamics (use words like "intensely", "smoothly", "rapidly")
|
||||
- Environmental details and lighting
|
||||
- Specific visual elements and their interactions
|
||||
|
||||
Keep the prompt descriptive but concise. WAN 2.2 works best with natural language that paints a clear picture of the desired video.
|
||||
|
||||
IMPORTANT: Return ONLY the video prompt as a single descriptive paragraph. No analysis, headers, or additional text.
|
||||
|
||||
Example of correct output:
|
||||
Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage, their movements fluid and dynamic as they exchange rapid punches under dramatic theater lighting that casts long shadows across the ring, with the crowd visible as blurred silhouettes in the darkened background."""
|
||||
|
||||
PROMPT_TEMPLATES = {
|
||||
"flux": FLUX_PROMPT,
|
||||
"sdxl": SDXL_PROMPT,
|
||||
"danbooru": DANBOORU_PROMPT,
|
||||
"video": VIDEO_PROMPT,
|
||||
}
|
||||
|
||||
PROMPT_OPTIONS = ["flux", "sdxl", "danbooru", "video"]
|
||||
|
||||
# Default models list (fallback if API is unavailable)
|
||||
DEFAULT_GEMINI_MODELS = [
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-pro",
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Image Scale Down By tool for ComfyUI."""
|
||||
|
||||
from .node import ImageScaleDownByNode
|
||||
|
||||
__all__ = ["ImageScaleDownByNode"]
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Core logic for ImageScaleDownBy tool."""
|
||||
|
||||
import torch.nn.functional as F
|
||||
from torch import Tensor
|
||||
|
||||
|
||||
def scale_down_image(image: Tensor, scale_by: float) -> Tensor:
|
||||
"""Scale down an image by a given factor.
|
||||
|
||||
Args:
|
||||
image: Input image tensor of shape (batch, height, width, channels)
|
||||
scale_by: Scale factor between 0.01 and 1.0
|
||||
|
||||
Returns:
|
||||
Scaled down image tensor
|
||||
"""
|
||||
batch, height, width, channels = image.shape
|
||||
|
||||
# Calculate new dimensions
|
||||
new_height = int(height * scale_by)
|
||||
new_width = int(width * scale_by)
|
||||
|
||||
# Ensure minimum size of 1x1
|
||||
new_height = max(1, new_height)
|
||||
new_width = max(1, new_width)
|
||||
|
||||
# Convert from BHWC to BCHW for interpolation
|
||||
image_chw = image.permute(0, 3, 1, 2)
|
||||
|
||||
# Scale down the image using bilinear interpolation
|
||||
scaled = F.interpolate(
|
||||
image_chw,
|
||||
size=(new_height, new_width),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
antialias=True,
|
||||
)
|
||||
|
||||
# Convert back to BHWC
|
||||
return scaled.permute(0, 2, 3, 1)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""ComfyUI node implementation for ImageScaleDownBy."""
|
||||
|
||||
from typing import Dict, Any, Tuple
|
||||
|
||||
from torch import Tensor
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
from .logic import scale_down_image
|
||||
|
||||
|
||||
class ImageScaleDownByNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Scales down images by a specified factor.
|
||||
|
||||
Reduces image dimensions proportionally using bilinear interpolation
|
||||
with antialiasing for smooth downscaling.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"scale_by": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.5,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"display": "number",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
CATEGORY = "ComfyAssets/🖼️ Resolution"
|
||||
RETURN_NAMES = ("images",)
|
||||
FUNCTION = "scale_down"
|
||||
|
||||
def scale_down(self, images: Tensor, scale_by: float) -> Tuple[Tensor]:
|
||||
"""
|
||||
Scale down images by the specified factor.
|
||||
|
||||
Args:
|
||||
images: Input image tensor
|
||||
scale_by: Scale factor between 0.01 and 1.0
|
||||
|
||||
Returns:
|
||||
Tuple containing scaled down image tensor
|
||||
"""
|
||||
try:
|
||||
self.validate_inputs(images=images, scale_by=scale_by)
|
||||
|
||||
# Scale down the images
|
||||
scaled_images = scale_down_image(images, scale_by)
|
||||
|
||||
_, new_height, new_width, _ = scaled_images.shape
|
||||
_, orig_height, orig_width, _ = images.shape
|
||||
|
||||
self.log_info(
|
||||
f"Scaled down images from {orig_height}x{orig_width} "
|
||||
f"to {new_height}x{new_width} (scale factor: {scale_by})"
|
||||
)
|
||||
|
||||
return (scaled_images,)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Failed to scale down images: {str(e)}", e)
|
||||
|
||||
def validate_inputs(self, **kwargs) -> None:
|
||||
"""Validate inputs for ImageScaleDownBy node."""
|
||||
images = kwargs.get("images")
|
||||
scale_by = kwargs.get("scale_by")
|
||||
|
||||
if images is None:
|
||||
raise ValueError("Images input is required")
|
||||
|
||||
if not isinstance(images, Tensor) or len(images.shape) != 4:
|
||||
raise ValueError(
|
||||
f"Expected image tensor with shape (batch, height, width, channels), "
|
||||
f"got shape {images.shape if isinstance(images, Tensor) else 'non-tensor'}"
|
||||
)
|
||||
|
||||
if scale_by <= 0 or scale_by > 1.0:
|
||||
raise ValueError(f"scale_by must be between 0.01 and 1.0, got {scale_by}")
|
||||
@@ -36,6 +36,7 @@ class ImageToMultipleOfNode(ComfyAssetsBaseNode):
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
CATEGORY = "ComfyAssets/🖼️ Resolution"
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "process"
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ def get_save_image_path(
|
||||
prefix_name = os.path.basename(filename_prefix)
|
||||
|
||||
# Sanitize only the filename part (not the directory path)
|
||||
safe_prefix = prefix_name.replace(":", "_") # Only sanitize problematic chars for filenames
|
||||
safe_prefix = prefix_name.replace(
|
||||
":", "_"
|
||||
) # Only sanitize problematic chars for filenames
|
||||
safe_prefix = "".join(c for c in safe_prefix if c.isalnum() or c in "._-")
|
||||
|
||||
# Create unique filename with timestamp to avoid conflicts
|
||||
@@ -114,7 +116,9 @@ def convert_tensor_to_pil(image_tensor: torch.Tensor) -> Image.Image:
|
||||
return img
|
||||
|
||||
|
||||
def create_png_metadata(prompt: Optional[Dict] = None, extra_pnginfo: Optional[Dict] = None) -> Optional[PngInfo]:
|
||||
def create_png_metadata(
|
||||
prompt: Optional[Dict] = None, extra_pnginfo: Optional[Dict] = None
|
||||
) -> Optional[PngInfo]:
|
||||
"""
|
||||
Create PNG metadata with workflow information
|
||||
|
||||
@@ -242,7 +246,10 @@ def process_image_batch(
|
||||
format_extensions = {"PNG": ".png", "JPEG": ".jpg", "WEBP": ".webp"}
|
||||
|
||||
if format_type not in format_extensions:
|
||||
raise ValueError(f"Unsupported format: {format_type}. " f"Supported: {list(format_extensions.keys())}")
|
||||
raise ValueError(
|
||||
f"Unsupported format: {format_type}. "
|
||||
f"Supported: {list(format_extensions.keys())}"
|
||||
)
|
||||
|
||||
format_ext = format_extensions[format_type]
|
||||
|
||||
@@ -308,7 +315,9 @@ def process_image_batch(
|
||||
return results, enhanced_data
|
||||
|
||||
|
||||
def validate_save_inputs(images: torch.Tensor, format_type: str, quality: int, png_compress_level: int) -> None:
|
||||
def validate_save_inputs(
|
||||
images: torch.Tensor, format_type: str, quality: int, png_compress_level: int
|
||||
) -> None:
|
||||
"""
|
||||
Validate inputs for image saving
|
||||
|
||||
@@ -326,19 +335,31 @@ def validate_save_inputs(images: torch.Tensor, format_type: str, quality: int, p
|
||||
raise ValueError(f"images must be a torch.Tensor, got {type(images).__name__}")
|
||||
|
||||
if len(images.shape) != 4:
|
||||
raise ValueError(f"images tensor must have 4 dimensions [batch, height, width, channels], " f"got {len(images.shape)}")
|
||||
raise ValueError(
|
||||
f"images tensor must have 4 dimensions [batch, height, width, channels], "
|
||||
f"got {len(images.shape)}"
|
||||
)
|
||||
|
||||
# Validate format
|
||||
supported_formats = ["PNG", "JPEG", "WEBP"]
|
||||
if format_type not in supported_formats:
|
||||
raise ValueError(f"format must be one of {supported_formats}, got {format_type}")
|
||||
raise ValueError(
|
||||
f"format must be one of {supported_formats}, got {format_type}"
|
||||
)
|
||||
|
||||
# Validate quality (for JPEG/WebP)
|
||||
if format_type in ["JPEG", "WEBP"]:
|
||||
if not isinstance(quality, int) or not (1 <= quality <= 100):
|
||||
raise ValueError(f"quality must be an integer between 1 and 100, got {quality}")
|
||||
raise ValueError(
|
||||
f"quality must be an integer between 1 and 100, got {quality}"
|
||||
)
|
||||
|
||||
# Validate PNG compression level
|
||||
if format_type == "PNG":
|
||||
if not isinstance(png_compress_level, int) or not (0 <= png_compress_level <= 9):
|
||||
raise ValueError(f"png_compress_level must be an integer between 0 and 9, " f"got {png_compress_level}")
|
||||
if not isinstance(png_compress_level, int) or not (
|
||||
0 <= png_compress_level <= 9
|
||||
):
|
||||
raise ValueError(
|
||||
f"png_compress_level must be an integer between 0 and 9, "
|
||||
f"got {png_compress_level}"
|
||||
)
|
||||
|
||||
@@ -79,7 +79,8 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"tooltip": "Use lossless WebP compression " "(ignores quality setting)",
|
||||
"tooltip": "Use lossless WebP compression "
|
||||
"(ignores quality setting)",
|
||||
},
|
||||
),
|
||||
"popup": (
|
||||
@@ -94,6 +95,7 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
CATEGORY = "ComfyAssets/💾 Images"
|
||||
FUNCTION = "save_images"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@@ -162,7 +164,10 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
|
||||
|
||||
# Log results
|
||||
total_size = sum(data["file_size"] for data in enhanced_data)
|
||||
self.log_info(f"Successfully saved {len(results)} images " f"(total size: {total_size / 1024:.1f} KB)")
|
||||
self.log_info(
|
||||
f"Successfully saved {len(results)} images "
|
||||
f"(total size: {total_size / 1024:.1f} KB)"
|
||||
)
|
||||
|
||||
# Return UI data for ComfyUI preview (clean) + enhanced data for our JS
|
||||
return {
|
||||
@@ -204,7 +209,9 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
|
||||
|
||||
# Additional node-specific validation
|
||||
if not isinstance(webp_lossless, bool):
|
||||
raise ValueError(f"webp_lossless must be a boolean, got {type(webp_lossless).__name__}")
|
||||
raise ValueError(
|
||||
f"webp_lossless must be a boolean, got {type(webp_lossless).__name__}"
|
||||
)
|
||||
|
||||
if not isinstance(popup, bool):
|
||||
raise ValueError(f"popup must be a boolean, got {type(popup).__name__}")
|
||||
|
||||
@@ -28,7 +28,9 @@ def extract_dimensions(
|
||||
if image is not None:
|
||||
# IMAGE tensor format: [batch, height, width, channels]
|
||||
if len(image.shape) != 4:
|
||||
raise ValueError(f"Expected IMAGE tensor with 4 dimensions, got {len(image.shape)}")
|
||||
raise ValueError(
|
||||
f"Expected IMAGE tensor with 4 dimensions, got {len(image.shape)}"
|
||||
)
|
||||
|
||||
_, height, width, _ = image.shape
|
||||
return int(width), int(height)
|
||||
@@ -40,7 +42,10 @@ def extract_dimensions(
|
||||
|
||||
samples = latent["samples"]
|
||||
if len(samples.shape) != 4:
|
||||
raise ValueError(f"Expected LATENT samples tensor with 4 dimensions, " f"got {len(samples.shape)}")
|
||||
raise ValueError(
|
||||
f"Expected LATENT samples tensor with 4 dimensions, "
|
||||
f"got {len(samples.shape)}"
|
||||
)
|
||||
|
||||
_, _, latent_height, latent_width = samples.shape
|
||||
|
||||
@@ -74,7 +79,9 @@ def ensure_divisible_by_8(width: int, height: int) -> Tuple[int, int]:
|
||||
return int(new_width), int(new_height)
|
||||
|
||||
|
||||
def calculate_scaled_dimensions(width: int, height: int, scale_factor: float) -> Tuple[int, int]:
|
||||
def calculate_scaled_dimensions(
|
||||
width: int, height: int, scale_factor: float
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Calculate new dimensions with scale factor and ensure divisible by 8
|
||||
|
||||
@@ -97,7 +104,9 @@ def calculate_scaled_dimensions(width: int, height: int, scale_factor: float) ->
|
||||
return ensure_divisible_by_8(new_width, new_height)
|
||||
|
||||
|
||||
def validate_scale_factor(scale_factor: float, min_scale: float = 0.1, max_scale: float = 8.0) -> None:
|
||||
def validate_scale_factor(
|
||||
scale_factor: float, min_scale: float = 0.1, max_scale: float = 8.0
|
||||
) -> None:
|
||||
"""
|
||||
Validate scale factor is within reasonable bounds
|
||||
|
||||
@@ -110,13 +119,19 @@ def validate_scale_factor(scale_factor: float, min_scale: float = 0.1, max_scale
|
||||
ValueError: If scale factor is out of bounds
|
||||
"""
|
||||
if not isinstance(scale_factor, (int, float)):
|
||||
raise ValueError(f"Scale factor must be a number, got {type(scale_factor).__name__}")
|
||||
raise ValueError(
|
||||
f"Scale factor must be a number, got {type(scale_factor).__name__}"
|
||||
)
|
||||
|
||||
if scale_factor < min_scale:
|
||||
raise ValueError(f"Scale factor {scale_factor} is too small (minimum: {min_scale})")
|
||||
raise ValueError(
|
||||
f"Scale factor {scale_factor} is too small (minimum: {min_scale})"
|
||||
)
|
||||
|
||||
if scale_factor > max_scale:
|
||||
raise ValueError(f"Scale factor {scale_factor} is too large (maximum: {max_scale})")
|
||||
raise ValueError(
|
||||
f"Scale factor {scale_factor} is too large (maximum: {max_scale})"
|
||||
)
|
||||
|
||||
|
||||
def calculate_resolution_from_input(
|
||||
@@ -146,6 +161,8 @@ def calculate_resolution_from_input(
|
||||
original_width, original_height = extract_dimensions(image=image, latent=latent)
|
||||
|
||||
# Calculate scaled dimensions
|
||||
new_width, new_height = calculate_scaled_dimensions(original_width, original_height, scale_factor)
|
||||
new_width, new_height = calculate_scaled_dimensions(
|
||||
original_width, original_height, scale_factor
|
||||
)
|
||||
|
||||
return new_width, new_height
|
||||
|
||||
@@ -42,7 +42,8 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
"max": 8.0,
|
||||
"step": 0.1,
|
||||
"display": "slider",
|
||||
"tooltip": "Factor to scale the resolution by " "(e.g., 2.0 for 2x, 0.5 for half scale)",
|
||||
"tooltip": "Factor to scale the resolution by "
|
||||
"(e.g., 2.0 for 2x, 0.5 for half scale)",
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -59,6 +60,7 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
CATEGORY = "ComfyAssets/🖼️ Resolution"
|
||||
RETURN_NAMES = ("width", "height")
|
||||
FUNCTION = "calculate_resolution"
|
||||
|
||||
@@ -87,11 +89,20 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
self.validate_inputs(scale_factor=scale_factor, image=image, latent=latent)
|
||||
|
||||
# Log the operation
|
||||
input_type = "IMAGE" if image is not None else "LATENT" if latent is not None else "NONE"
|
||||
self.log_info(f"Calculating resolution with scale_factor={scale_factor}, " f"input_type={input_type}")
|
||||
input_type = (
|
||||
"IMAGE"
|
||||
if image is not None
|
||||
else "LATENT" if latent is not None else "NONE"
|
||||
)
|
||||
self.log_info(
|
||||
f"Calculating resolution with scale_factor={scale_factor}, "
|
||||
f"input_type={input_type}"
|
||||
)
|
||||
|
||||
# Calculate the resolution
|
||||
width, height = calculate_resolution_from_input(scale_factor=scale_factor, image=image, latent=latent)
|
||||
width, height = calculate_resolution_from_input(
|
||||
scale_factor=scale_factor, image=image, latent=latent
|
||||
)
|
||||
|
||||
# Log the result
|
||||
self.log_info(f"Calculated resolution: {width}x{height}")
|
||||
@@ -126,7 +137,9 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
|
||||
# Validate scale factor type
|
||||
if not isinstance(scale_factor, (int, float)):
|
||||
raise ValueError(f"scale_factor must be a number, got {type(scale_factor).__name__}")
|
||||
raise ValueError(
|
||||
f"scale_factor must be a number, got {type(scale_factor).__name__}"
|
||||
)
|
||||
|
||||
# Validate tensors using helper methods
|
||||
if image is not None:
|
||||
@@ -138,11 +151,14 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
def _validate_image_tensor(self, image: torch.Tensor) -> None:
|
||||
"""Validate image tensor format"""
|
||||
if not isinstance(image, torch.Tensor):
|
||||
raise ValueError(f"image must be a torch.Tensor, got {type(image).__name__}")
|
||||
raise ValueError(
|
||||
f"image must be a torch.Tensor, got {type(image).__name__}"
|
||||
)
|
||||
|
||||
if len(image.shape) != 4:
|
||||
raise ValueError(
|
||||
f"image tensor must have 4 dimensions " f"[batch, height, width, channels], got {len(image.shape)}"
|
||||
f"image tensor must have 4 dimensions "
|
||||
f"[batch, height, width, channels], got {len(image.shape)}"
|
||||
)
|
||||
|
||||
def _validate_latent_dict(self, latent: Dict[str, torch.Tensor]) -> None:
|
||||
@@ -155,11 +171,15 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
|
||||
samples = latent["samples"]
|
||||
if not isinstance(samples, torch.Tensor):
|
||||
raise ValueError(f"latent['samples'] must be a torch.Tensor, " f"got {type(samples).__name__}")
|
||||
raise ValueError(
|
||||
f"latent['samples'] must be a torch.Tensor, "
|
||||
f"got {type(samples).__name__}"
|
||||
)
|
||||
|
||||
if len(samples.shape) != 4:
|
||||
raise ValueError(
|
||||
f"latent samples tensor must have 4 dimensions " f"[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)}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -63,9 +63,11 @@ class SamplerComboCompactNode(ComfyAssetsBaseNode):
|
||||
RETURN_TYPES = ("SAMPLER", SCHEDULERS, "INT", "FLOAT")
|
||||
RETURN_NAMES = ("sampler", "scheduler", "steps", "cfg")
|
||||
FUNCTION = "get_combo"
|
||||
CATEGORY = "ComfyAssets"
|
||||
CATEGORY = "ComfyAssets/🌀 Samplers"
|
||||
|
||||
def get_combo(self, sampler: str, sched: str, steps: int, cfg: float) -> Tuple[object, str, int, float]:
|
||||
def get_combo(
|
||||
self, sampler: str, sched: str, steps: int, cfg: float
|
||||
) -> Tuple[object, str, int, float]:
|
||||
"""
|
||||
Get compact sampler combo configuration.
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ except ImportError:
|
||||
]
|
||||
|
||||
|
||||
def validate_sampler_settings(sampler_name: str, scheduler: str, steps: int, cfg: float) -> bool:
|
||||
def validate_sampler_settings(
|
||||
sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||
) -> bool:
|
||||
"""
|
||||
Validate sampler configuration settings.
|
||||
|
||||
@@ -81,7 +83,9 @@ def validate_sampler_settings(sampler_name: str, scheduler: str, steps: int, cfg
|
||||
return False
|
||||
|
||||
|
||||
def get_sampler_combo(sampler_name: str, scheduler: str, steps: int, cfg: float) -> Tuple[str, str, int, float]:
|
||||
def get_sampler_combo(
|
||||
sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||
) -> Tuple[str, str, int, float]:
|
||||
"""
|
||||
Process and return sampler combo settings.
|
||||
|
||||
|
||||
@@ -68,9 +68,11 @@ class SamplerComboNode(ComfyAssetsBaseNode):
|
||||
RETURN_TYPES = ("SAMPLER", SCHEDULERS, "INT", "FLOAT")
|
||||
RETURN_NAMES = ("sampler_name", "scheduler", "steps", "cfg")
|
||||
FUNCTION = "get_sampler_combo"
|
||||
CATEGORY = "ComfyAssets"
|
||||
CATEGORY = "ComfyAssets/🌀 Samplers"
|
||||
|
||||
def get_sampler_combo(self, sampler_name: str, scheduler: str, steps: int, cfg: float) -> Tuple[object, str, int, float]:
|
||||
def get_sampler_combo(
|
||||
self, sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||
) -> Tuple[object, str, int, float]:
|
||||
"""
|
||||
Get sampler combo configuration.
|
||||
|
||||
@@ -117,7 +119,10 @@ class SamplerComboNode(ComfyAssetsBaseNode):
|
||||
# Return sampler name for testing
|
||||
sampler = result[0]
|
||||
|
||||
self.log_info(f"Configured sampler combo: {result[0]}, {result[1]}, " f"{result[2]} steps, CFG {result[3]}")
|
||||
self.log_info(
|
||||
f"Configured sampler combo: {result[0]}, {result[1]}, "
|
||||
f"{result[2]} steps, CFG {result[3]}"
|
||||
)
|
||||
|
||||
return (sampler, result[1], result[2], result[3])
|
||||
|
||||
@@ -139,7 +144,9 @@ class SamplerComboNode(ComfyAssetsBaseNode):
|
||||
sampler = "euler"
|
||||
return (sampler, "normal", 20, 7.0)
|
||||
|
||||
def validate_inputs(self, sampler_name: str, scheduler: str, steps: int, cfg: float) -> None:
|
||||
def validate_inputs(
|
||||
self, sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||
) -> None:
|
||||
"""
|
||||
Validate sampler combo inputs.
|
||||
|
||||
@@ -154,7 +161,8 @@ class SamplerComboNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
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}"
|
||||
f"Invalid sampler settings: sampler={sampler_name}, "
|
||||
f"scheduler={scheduler}, steps={steps}, cfg={cfg}"
|
||||
)
|
||||
|
||||
def get_scheduler_suggestions(self, sampler_name: str) -> list:
|
||||
@@ -205,7 +213,9 @@ class SamplerComboNode(ComfyAssetsBaseNode):
|
||||
"recommendation": f"Recommended range: {min_cfg}-{max_cfg} CFG",
|
||||
}
|
||||
|
||||
def get_combo_analysis(self, sampler_name: str, scheduler: str, steps: int, cfg: float) -> dict:
|
||||
def get_combo_analysis(
|
||||
self, sampler_name: str, scheduler: str, steps: int, cfg: float
|
||||
) -> dict:
|
||||
"""
|
||||
Analyze the sampler combo configuration and provide recommendations.
|
||||
|
||||
@@ -264,7 +274,10 @@ class SamplerComboNode(ComfyAssetsBaseNode):
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of the node."""
|
||||
return f"SamplerComboNode(samplers={len(SAMPLERS)}, " f"schedulers={len(SCHEDULERS)})"
|
||||
return (
|
||||
f"SamplerComboNode(samplers={len(SAMPLERS)}, "
|
||||
f"schedulers={len(SCHEDULERS)})"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Detailed string representation of the node."""
|
||||
|
||||
@@ -66,7 +66,9 @@ def sanitize_seed_value(seed: Any) -> int:
|
||||
raise ValueError(f"Invalid seed value: {seed}") from e
|
||||
|
||||
|
||||
def create_history_entry(seed: int, timestamp: Optional[float] = None) -> Dict[str, Any]:
|
||||
def create_history_entry(
|
||||
seed: int, timestamp: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a standardized history entry for a seed.
|
||||
|
||||
@@ -87,7 +89,9 @@ def create_history_entry(seed: int, timestamp: Optional[float] = None) -> Dict[s
|
||||
}
|
||||
|
||||
|
||||
def filter_duplicate_seeds(history: List[Dict[str, Any]], new_seed: int, dedup_window_ms: int = 500) -> bool:
|
||||
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.
|
||||
|
||||
@@ -189,7 +193,9 @@ def format_time_ago(timestamp: float) -> str:
|
||||
return f"{seconds}s ago"
|
||||
|
||||
|
||||
def search_history_by_seed(history: List[Dict[str, Any]], seed: int) -> Optional[Dict[str, Any]]:
|
||||
def search_history_by_seed(
|
||||
history: List[Dict[str, Any]], seed: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search history for a specific seed value.
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
"default": 12345,
|
||||
"min": 0,
|
||||
"max": 0xFFFFFFFFFFFFFFFF,
|
||||
"tooltip": "Seed value for generation processes. " "History UI tracks all changes automatically.",
|
||||
"tooltip": "Seed value for generation processes. "
|
||||
"History UI tracks all changes automatically.",
|
||||
},
|
||||
),
|
||||
}
|
||||
@@ -37,7 +38,7 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("seed",)
|
||||
FUNCTION = "output_seed"
|
||||
CATEGORY = "ComfyAssets"
|
||||
CATEGORY = "ComfyAssets/🌱 Seeds"
|
||||
|
||||
def output_seed(self, seed: int) -> Tuple[int]:
|
||||
"""
|
||||
@@ -56,7 +57,10 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"{self.__class__.__name__}: Invalid seed value: {seed}. " f"Using fallback seed 12345.")
|
||||
logger.error(
|
||||
f"{self.__class__.__name__}: Invalid seed value: {seed}. "
|
||||
f"Using fallback seed 12345."
|
||||
)
|
||||
return (12345,)
|
||||
|
||||
clean_seed = sanitize_seed_value(seed)
|
||||
@@ -68,7 +72,10 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"{self.__class__.__name__}: Error processing seed: {str(e)}. " f"Using fallback seed 12345.")
|
||||
logger.error(
|
||||
f"{self.__class__.__name__}: Error processing seed: {str(e)}. "
|
||||
f"Using fallback seed 12345."
|
||||
)
|
||||
return (12345,)
|
||||
|
||||
def generate_new_seed(self) -> int:
|
||||
|
||||
@@ -5,7 +5,9 @@ from math import gcd
|
||||
from .presets import PRESET_OPTIONS
|
||||
|
||||
|
||||
def get_preset_dimensions(preset: str, custom_width: int, custom_height: int) -> Tuple[int, int]:
|
||||
def get_preset_dimensions(
|
||||
preset: str, custom_width: int, custom_height: int
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Get dimensions from preset name or use custom dimensions.
|
||||
|
||||
@@ -112,7 +114,9 @@ def sanitize_dimensions(width: int, height: int) -> Tuple[int, int]:
|
||||
return width, height
|
||||
|
||||
|
||||
def get_dimension_info(preset: str, width: int, height: int, swap_enabled: bool) -> dict:
|
||||
def get_dimension_info(
|
||||
preset: str, width: int, height: int, swap_enabled: bool
|
||||
) -> dict:
|
||||
"""
|
||||
Get comprehensive dimension information including metadata.
|
||||
|
||||
@@ -201,7 +205,9 @@ def parse_dimension_string(dimension_str: str) -> Tuple[int, int]:
|
||||
raise ValueError(f"Could not parse dimensions from {dimension_str}: {e}")
|
||||
|
||||
|
||||
def get_optimal_scale_factor(current_width: int, current_height: int, target_width: int, target_height: int) -> float:
|
||||
def get_optimal_scale_factor(
|
||||
current_width: int, current_height: int, target_width: int, target_height: int
|
||||
) -> float:
|
||||
"""
|
||||
Calculate optimal scale factor to get from current to target dimensions.
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
metadata = PRESET_METADATA.get(preset_name)
|
||||
if metadata:
|
||||
formatted_option = (
|
||||
f"{preset_name} - {metadata.aspect_ratio} " f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
|
||||
f"{preset_name} - {metadata.aspect_ratio} "
|
||||
f"({metadata.megapixels:.1f}MP) - {metadata.model_group}"
|
||||
)
|
||||
preset_options.append(formatted_option)
|
||||
else:
|
||||
@@ -84,7 +85,7 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
RETURN_NAMES = ("width", "height")
|
||||
FUNCTION = "get_dimensions"
|
||||
CATEGORY = "ComfyAssets"
|
||||
CATEGORY = "ComfyAssets/🖼️ Resolution"
|
||||
|
||||
def get_dimensions(self, preset: str, width: int, height: int) -> Tuple[int, int]:
|
||||
"""
|
||||
@@ -103,7 +104,9 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
original_preset = self._extract_preset_name(preset)
|
||||
|
||||
# Get base dimensions from preset or custom input
|
||||
final_width, final_height = get_preset_dimensions(original_preset, width, height)
|
||||
final_width, final_height = get_preset_dimensions(
|
||||
original_preset, width, height
|
||||
)
|
||||
|
||||
# Sanitize dimensions to ensure they meet ComfyUI requirements
|
||||
final_width, final_height = sanitize_dimensions(final_width, final_height)
|
||||
@@ -112,7 +115,8 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
if not validate_dimensions(final_width, final_height):
|
||||
# This should not happen after sanitization, but handle gracefully
|
||||
self.handle_error(
|
||||
f"Generated invalid dimensions: {final_width}×{final_height}. " f"Using fallback dimensions 1024×1024."
|
||||
f"Generated invalid dimensions: {final_width}×{final_height}. "
|
||||
f"Using fallback dimensions 1024×1024."
|
||||
)
|
||||
final_width, final_height = 1024, 1024
|
||||
|
||||
@@ -120,7 +124,9 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
|
||||
except Exception as e:
|
||||
# Handle any unexpected errors gracefully
|
||||
error_msg = f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
|
||||
error_msg = (
|
||||
f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
|
||||
)
|
||||
self.handle_error(error_msg)
|
||||
return (1024, 1024)
|
||||
|
||||
@@ -170,7 +176,10 @@ class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
|
||||
metadata = get_preset_metadata(preset)
|
||||
if metadata.width > 0: # Valid metadata
|
||||
return f"{preset} - {metadata.aspect_ratio} ({metadata.megapixels:.1f}MP) - " f"{metadata.description}"
|
||||
return (
|
||||
f"{preset} - {metadata.aspect_ratio} ({metadata.megapixels:.1f}MP) - "
|
||||
f"{metadata.description}"
|
||||
)
|
||||
|
||||
return f"Unknown preset: {preset}"
|
||||
|
||||
|
||||
@@ -287,15 +287,21 @@ PRESET_METADATA: Dict[str, PresetMetadata] = {
|
||||
|
||||
# Legacy compatibility - maintain old preset dictionaries
|
||||
SDXL_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||
k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"
|
||||
k: (v.width, v.height)
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "SDXL"
|
||||
}
|
||||
|
||||
FLUX_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||
k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"
|
||||
k: (v.width, v.height)
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "FLUX"
|
||||
}
|
||||
|
||||
ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||
k: (v.width, v.height) for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"
|
||||
k: (v.width, v.height)
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "Ultra-Wide"
|
||||
}
|
||||
|
||||
# Combined preset options for ComfyUI dropdown
|
||||
@@ -308,28 +314,78 @@ PRESET_OPTIONS: Dict[str, Tuple[int, int]] = {
|
||||
PRESET_CATEGORIES = {
|
||||
"Custom": ["custom"],
|
||||
# SDXL Categories
|
||||
"SDXL Square": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" and v.category == "Square"],
|
||||
"SDXL Portrait": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" and v.category == "Portrait"],
|
||||
"SDXL Landscape": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL" and v.category == "Landscape"],
|
||||
"SDXL Square": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "SDXL" and v.category == "Square"
|
||||
],
|
||||
"SDXL Portrait": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "SDXL" and v.category == "Portrait"
|
||||
],
|
||||
"SDXL Landscape": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "SDXL" and v.category == "Landscape"
|
||||
],
|
||||
# FLUX Categories
|
||||
"FLUX Square": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Square"],
|
||||
"FLUX Portrait": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Portrait"],
|
||||
"FLUX Cinematic": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Cinematic"],
|
||||
"FLUX Classic": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Classic"],
|
||||
"FLUX Photography": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX" and v.category == "Photography"],
|
||||
"FLUX Square": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "FLUX" and v.category == "Square"
|
||||
],
|
||||
"FLUX Portrait": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "FLUX" and v.category == "Portrait"
|
||||
],
|
||||
"FLUX Cinematic": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "FLUX" and v.category == "Cinematic"
|
||||
],
|
||||
"FLUX Classic": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "FLUX" and v.category == "Classic"
|
||||
],
|
||||
"FLUX Photography": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "FLUX" and v.category == "Photography"
|
||||
],
|
||||
# Ultra-Wide Categories
|
||||
"Ultra-Wide Gaming": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Gaming"],
|
||||
"Ultra-Wide 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"
|
||||
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"
|
||||
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 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"
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "Ultra-Wide" and v.category == "Vertical"
|
||||
],
|
||||
"Ultra-Wide Banner": [
|
||||
k
|
||||
for k, v in PRESET_METADATA.items()
|
||||
if v.model_group == "Ultra-Wide" and v.category == "Banner"
|
||||
],
|
||||
"Ultra-Wide Banner": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide" and v.category == "Banner"],
|
||||
}
|
||||
|
||||
# Legacy compatibility - preset descriptions
|
||||
@@ -339,7 +395,9 @@ PRESET_DESCRIPTIONS = {k: v.description for k, v in PRESET_METADATA.items()}
|
||||
MODEL_RECOMMENDATIONS = {
|
||||
"SDXL": [k for k, v in PRESET_METADATA.items() if v.model_group == "SDXL"],
|
||||
"FLUX": [k for k, v in PRESET_METADATA.items() if v.model_group == "FLUX"],
|
||||
"Ultra-Wide": [k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"],
|
||||
"Ultra-Wide": [
|
||||
k for k, v in PRESET_METADATA.items() if v.model_group == "Ultra-Wide"
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -393,7 +451,9 @@ def validate_preset_dimensions() -> bool:
|
||||
|
||||
# Check divisible by 8
|
||||
if width % 8 != 0 or height % 8 != 0:
|
||||
print(f"ERROR: {preset_name} dimensions not divisible by 8: {width}×{height}")
|
||||
print(
|
||||
f"ERROR: {preset_name} dimensions not divisible by 8: {width}×{height}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Check reasonable bounds
|
||||
@@ -409,7 +469,9 @@ def validate_metadata_consistency() -> bool:
|
||||
"""Validate metadata consistency and completeness."""
|
||||
for preset_name, metadata in PRESET_METADATA.items():
|
||||
# Verify aspect ratio calculation
|
||||
expected_ratio, expected_decimal = calculate_aspect_ratio(metadata.width, metadata.height)
|
||||
expected_ratio, expected_decimal = calculate_aspect_ratio(
|
||||
metadata.width, metadata.height
|
||||
)
|
||||
if abs(metadata.aspect_decimal - expected_decimal) > 0.001:
|
||||
print(
|
||||
f"ERROR: {preset_name} aspect ratio mismatch: "
|
||||
@@ -420,7 +482,10 @@ def validate_metadata_consistency() -> bool:
|
||||
# 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}")
|
||||
print(
|
||||
f"ERROR: {preset_name} megapixel mismatch: "
|
||||
f"expected {expected_mp:.2f}, got {metadata.megapixels}"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""XYZ Helpers module for ComfyUI."""
|
||||
|
||||
from .sampler_select_helper import SamplerSelectHelperNode
|
||||
from .scheduler_select_helper import SchedulerSelectHelperNode
|
||||
from .text_encode_sampler_params import TextEncodeSamplerParamsNode
|
||||
from .flux_sampler_params import FluxSamplerParamsNode
|
||||
from .plot_sampler_params import PlotParametersNode
|
||||
from .lora_folder_batch import LoRAFolderBatchNode
|
||||
|
||||
__all__ = [
|
||||
"SamplerSelectHelperNode",
|
||||
"SchedulerSelectHelperNode",
|
||||
"TextEncodeSamplerParamsNode",
|
||||
"FluxSamplerParamsNode",
|
||||
"PlotParametersNode",
|
||||
"LoRAFolderBatchNode",
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Flux Sampler Params module."""
|
||||
|
||||
from .node import FluxSamplerParamsNode
|
||||
|
||||
__all__ = ["FluxSamplerParamsNode"]
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Logic module for Flux Sampler Params node."""
|
||||
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_string_to_list(value: str) -> List[float]:
|
||||
"""
|
||||
Parse a string containing comma-separated values to a list of floats.
|
||||
|
||||
Args:
|
||||
value: String with comma-separated values
|
||||
|
||||
Returns:
|
||||
List of float values
|
||||
"""
|
||||
if not value or not value.strip():
|
||||
return []
|
||||
|
||||
try:
|
||||
values = []
|
||||
for item in value.split(","):
|
||||
item = item.strip()
|
||||
if item:
|
||||
try:
|
||||
values.append(float(item))
|
||||
except ValueError:
|
||||
logger.warning(f"Could not parse '{item}' as float")
|
||||
return values
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing string to list: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def parse_seed_string(seed_string: str) -> List[int]:
|
||||
"""
|
||||
Parse seed string which can contain numbers, '?', or ranges.
|
||||
|
||||
Args:
|
||||
seed_string: String with seeds (e.g., "123,?,456")
|
||||
|
||||
Returns:
|
||||
List of integer seeds
|
||||
"""
|
||||
seeds = []
|
||||
|
||||
try:
|
||||
for item in seed_string.replace("\n", ",").split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
|
||||
if "?" in item:
|
||||
seeds.append(random.randint(0, 999999))
|
||||
else:
|
||||
try:
|
||||
seeds.append(int(item))
|
||||
except ValueError:
|
||||
logger.warning(f"Could not parse seed '{item}'")
|
||||
seeds.append(random.randint(0, 999999))
|
||||
|
||||
if not seeds:
|
||||
seeds = [random.randint(0, 999999)]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing seeds: {e}")
|
||||
seeds = [random.randint(0, 999999)]
|
||||
|
||||
return seeds
|
||||
|
||||
|
||||
def parse_sampler_string(
|
||||
sampler_string: str, available_samplers: List[str]
|
||||
) -> List[str]:
|
||||
"""
|
||||
Parse sampler string which can contain names, '*', or '!' exclusions.
|
||||
|
||||
Args:
|
||||
sampler_string: String with sampler specifications
|
||||
available_samplers: List of available sampler names
|
||||
|
||||
Returns:
|
||||
List of sampler names
|
||||
"""
|
||||
if sampler_string == "*":
|
||||
return available_samplers.copy()
|
||||
|
||||
if sampler_string.startswith("!"):
|
||||
excluded = sampler_string.replace("\n", ",").split(",")
|
||||
excluded = [s.strip("! ") for s in excluded]
|
||||
return [s for s in available_samplers if s not in excluded]
|
||||
|
||||
samplers = sampler_string.replace("\n", ",").split(",")
|
||||
samplers = [s.strip() for s in samplers if s.strip() in available_samplers]
|
||||
|
||||
if not samplers:
|
||||
return ["euler"]
|
||||
|
||||
return samplers
|
||||
|
||||
|
||||
def parse_scheduler_string(
|
||||
scheduler_string: str, available_schedulers: List[str]
|
||||
) -> List[str]:
|
||||
"""
|
||||
Parse scheduler string which can contain names, '*', or '!' exclusions.
|
||||
|
||||
Args:
|
||||
scheduler_string: String with scheduler specifications
|
||||
available_schedulers: List of available scheduler names
|
||||
|
||||
Returns:
|
||||
List of scheduler names
|
||||
"""
|
||||
if scheduler_string == "*":
|
||||
return available_schedulers.copy()
|
||||
|
||||
if scheduler_string.startswith("!"):
|
||||
excluded = scheduler_string.replace("\n", ",").split(",")
|
||||
excluded = [s.strip("! ") for s in excluded]
|
||||
return [s for s in available_schedulers if s not in excluded]
|
||||
|
||||
schedulers = scheduler_string.replace("\n", ",").split(",")
|
||||
schedulers = [s.strip() for s in schedulers if s.strip() in available_schedulers]
|
||||
|
||||
if not schedulers:
|
||||
return ["simple"]
|
||||
|
||||
return schedulers
|
||||
|
||||
|
||||
def get_default_flux_params(is_schnell: bool) -> Dict[str, Any]:
|
||||
"""
|
||||
Get default parameters for Flux models.
|
||||
|
||||
Args:
|
||||
is_schnell: Whether this is a Schnell model
|
||||
|
||||
Returns:
|
||||
Dictionary of default parameters
|
||||
"""
|
||||
if is_schnell:
|
||||
return {
|
||||
"steps": 4,
|
||||
"guidance": 3.5,
|
||||
"max_shift": 0,
|
||||
"base_shift": 1.0,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"steps": 20,
|
||||
"guidance": 3.5,
|
||||
"max_shift": 1.15,
|
||||
"base_shift": 0.5,
|
||||
}
|
||||
|
||||
|
||||
def create_batch_params(
|
||||
seeds: List[int],
|
||||
samplers: List[str],
|
||||
schedulers: List[str],
|
||||
steps: List[int],
|
||||
guidances: List[float],
|
||||
max_shifts: List[float],
|
||||
base_shifts: List[float],
|
||||
denoises: List[float],
|
||||
conditioning_count: int,
|
||||
lora_strength_count: int = 1,
|
||||
) -> Tuple[int, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Create batch parameters for all combinations.
|
||||
|
||||
Returns:
|
||||
Tuple of (total_samples, list of parameter combinations)
|
||||
"""
|
||||
total = (
|
||||
len(seeds)
|
||||
* len(samplers)
|
||||
* len(schedulers)
|
||||
* len(steps)
|
||||
* len(guidances)
|
||||
* len(max_shifts)
|
||||
* len(base_shifts)
|
||||
* len(denoises)
|
||||
* conditioning_count
|
||||
* lora_strength_count
|
||||
)
|
||||
|
||||
params = []
|
||||
for seed in seeds:
|
||||
for sampler in samplers:
|
||||
for scheduler in schedulers:
|
||||
for step in steps:
|
||||
for guidance in guidances:
|
||||
for max_shift in max_shifts:
|
||||
for base_shift in base_shifts:
|
||||
for denoise in denoises:
|
||||
params.append(
|
||||
{
|
||||
"seed": seed,
|
||||
"sampler": sampler,
|
||||
"scheduler": scheduler,
|
||||
"steps": step,
|
||||
"guidance": guidance,
|
||||
"max_shift": max_shift,
|
||||
"base_shift": base_shift,
|
||||
"denoise": denoise,
|
||||
}
|
||||
)
|
||||
|
||||
return total, params
|
||||
|
||||
|
||||
def process_conditioning_input(
|
||||
conditioning: Any,
|
||||
) -> Tuple[Optional[List[str]], List[Any]]:
|
||||
"""
|
||||
Process conditioning input which can be a dict or regular conditioning.
|
||||
|
||||
Args:
|
||||
conditioning: Input conditioning (dict or tensor)
|
||||
|
||||
Returns:
|
||||
Tuple of (text_list, encoded_list)
|
||||
"""
|
||||
if isinstance(conditioning, dict) and "encoded" in conditioning:
|
||||
return conditioning.get("text"), conditioning["encoded"]
|
||||
else:
|
||||
return None, [conditioning]
|
||||
|
||||
|
||||
def validate_flux_params(
|
||||
steps: str, guidance: str, max_shift: str, base_shift: str, denoise: str
|
||||
) -> bool:
|
||||
"""
|
||||
Validate Flux sampler parameters.
|
||||
|
||||
Returns:
|
||||
True if all parameters are valid
|
||||
"""
|
||||
try:
|
||||
parse_string_to_list(steps)
|
||||
parse_string_to_list(guidance)
|
||||
parse_string_to_list(max_shift)
|
||||
parse_string_to_list(base_shift)
|
||||
parse_string_to_list(denoise)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid parameters: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,371 @@
|
||||
"""Flux Sampler Params node for ComfyUI."""
|
||||
|
||||
from typing import Tuple, Any, Dict, List, Optional
|
||||
import time
|
||||
import logging
|
||||
from ....base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
parse_string_to_list,
|
||||
parse_seed_string,
|
||||
parse_sampler_string,
|
||||
parse_scheduler_string,
|
||||
get_default_flux_params,
|
||||
create_batch_params,
|
||||
process_conditioning_input,
|
||||
validate_flux_params,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FluxSamplerParamsNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Flux Sampler Parameters node for batch processing.
|
||||
|
||||
Enables batch processing with multiple parameter variations for
|
||||
Flux models. Supports varying seeds, samplers, schedulers, steps,
|
||||
guidance, shifts, and LoRAs for comprehensive parameter exploration.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the node."""
|
||||
super().__init__()
|
||||
self.lora_loader = None
|
||||
self.cached_lora = (None, None)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL", {"tooltip": "Flux model to use"}),
|
||||
"conditioning": (
|
||||
"CONDITIONING",
|
||||
{"tooltip": "Conditioning (can be from TextEncodeSamplerParams)"},
|
||||
),
|
||||
"latent_image": ("LATENT", {"tooltip": "Input latent image"}),
|
||||
"seed": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "?",
|
||||
"tooltip": "Seeds (comma-separated, ? for random)",
|
||||
},
|
||||
),
|
||||
"sampler": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "euler",
|
||||
"tooltip": "Samplers (comma-separated, * for all, ! to exclude)",
|
||||
},
|
||||
),
|
||||
"scheduler": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "simple",
|
||||
"tooltip": "Schedulers (comma-separated, * for all, ! to exclude)",
|
||||
},
|
||||
),
|
||||
"steps": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "20",
|
||||
"tooltip": "Steps (comma-separated values)",
|
||||
},
|
||||
),
|
||||
"guidance": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "3.5",
|
||||
"tooltip": "Guidance/CFG values (comma-separated)",
|
||||
},
|
||||
),
|
||||
"max_shift": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "",
|
||||
"tooltip": "Max shift values (comma-separated, auto-set for Flux)",
|
||||
},
|
||||
),
|
||||
"base_shift": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "",
|
||||
"tooltip": "Base shift values (comma-separated, auto-set for Flux)",
|
||||
},
|
||||
),
|
||||
"denoise": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"default": "1.0",
|
||||
"tooltip": "Denoise values (comma-separated)",
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"loras": ("LORA_PARAMS", {"tooltip": "Optional LoRA parameters"})
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LATENT", "SAMPLER_PARAMS")
|
||||
RETURN_NAMES = ("latent", "params")
|
||||
FUNCTION = "process_batch"
|
||||
CATEGORY = "ComfyAssets/🧰 xyz-helpers"
|
||||
|
||||
def process_batch(
|
||||
self,
|
||||
model: Any,
|
||||
conditioning: Any,
|
||||
latent_image: Any,
|
||||
seed: str,
|
||||
sampler: str,
|
||||
scheduler: str,
|
||||
steps: str,
|
||||
guidance: str,
|
||||
max_shift: str,
|
||||
base_shift: str,
|
||||
denoise: str,
|
||||
loras: Optional[Dict] = None,
|
||||
) -> Tuple[Any, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Process batch sampling with parameter variations.
|
||||
|
||||
Returns:
|
||||
Tuple of (output_latent, parameter_list)
|
||||
"""
|
||||
try:
|
||||
import comfy.samplers
|
||||
import comfy.model_base
|
||||
import comfy.model_management
|
||||
from comfy_extras.nodes_custom_sampler import (
|
||||
Noise_RandomNoise,
|
||||
BasicScheduler,
|
||||
BasicGuider,
|
||||
SamplerCustomAdvanced,
|
||||
)
|
||||
from comfy_extras.nodes_latent import LatentBatch
|
||||
from comfy_extras.nodes_model_advanced import (
|
||||
ModelSamplingFlux,
|
||||
ModelSamplingAuraFlow,
|
||||
)
|
||||
from node_helpers import conditioning_set_values
|
||||
from nodes import LoraLoader
|
||||
|
||||
except ImportError as e:
|
||||
self.handle_error(f"Required ComfyUI modules not available: {e}")
|
||||
return (latent_image, [])
|
||||
|
||||
try:
|
||||
if not validate_flux_params(
|
||||
steps, guidance, max_shift, base_shift, denoise
|
||||
):
|
||||
self.handle_error("Invalid parameter format")
|
||||
|
||||
is_schnell = model.model.model_type == comfy.model_base.ModelType.FLOW
|
||||
defaults = get_default_flux_params(is_schnell)
|
||||
|
||||
seeds = parse_seed_string(seed)
|
||||
samplers = parse_sampler_string(sampler, comfy.samplers.KSampler.SAMPLERS)
|
||||
schedulers = parse_scheduler_string(
|
||||
scheduler, comfy.samplers.KSampler.SCHEDULERS
|
||||
)
|
||||
|
||||
steps = steps if steps else str(defaults["steps"])
|
||||
steps_list = [int(s) for s in parse_string_to_list(steps)]
|
||||
|
||||
guidance = guidance if guidance else str(defaults["guidance"])
|
||||
guidance_list = parse_string_to_list(guidance)
|
||||
|
||||
denoise = denoise if denoise else "1.0"
|
||||
denoise_list = parse_string_to_list(denoise)
|
||||
|
||||
if not is_schnell:
|
||||
max_shift = max_shift if max_shift else str(defaults["max_shift"])
|
||||
base_shift = base_shift if base_shift else str(defaults["base_shift"])
|
||||
else:
|
||||
max_shift = "0"
|
||||
base_shift = base_shift if base_shift else str(defaults["base_shift"])
|
||||
|
||||
max_shift_list = parse_string_to_list(max_shift)
|
||||
base_shift_list = parse_string_to_list(base_shift)
|
||||
|
||||
cond_text, cond_encoded = process_conditioning_input(conditioning)
|
||||
|
||||
width = latent_image["samples"].shape[3] * 8
|
||||
height = latent_image["samples"].shape[2] * 8
|
||||
|
||||
lora_strength_count = 1
|
||||
if loras:
|
||||
lora_model = loras["loras"]
|
||||
lora_strength = loras["strengths"]
|
||||
lora_strength_count = sum(len(i) for i in lora_strength)
|
||||
|
||||
if self.lora_loader is None:
|
||||
self.lora_loader = LoraLoader()
|
||||
|
||||
total_samples, param_combos = create_batch_params(
|
||||
seeds,
|
||||
samplers,
|
||||
schedulers,
|
||||
steps_list,
|
||||
guidance_list,
|
||||
max_shift_list,
|
||||
base_shift_list,
|
||||
denoise_list,
|
||||
len(cond_encoded),
|
||||
lora_strength_count,
|
||||
)
|
||||
|
||||
self.log_info(f"Processing {total_samples} parameter combinations")
|
||||
|
||||
basicscheduler = BasicScheduler()
|
||||
basicguider = BasicGuider()
|
||||
samplercustomadvanced = SamplerCustomAdvanced()
|
||||
latentbatch = LatentBatch()
|
||||
modelsampling = (
|
||||
ModelSamplingFlux() if not is_schnell else ModelSamplingAuraFlow()
|
||||
)
|
||||
|
||||
out_latent = None
|
||||
out_params = []
|
||||
|
||||
if total_samples > 1:
|
||||
from comfy.utils import ProgressBar
|
||||
|
||||
pbar = ProgressBar(total_samples)
|
||||
|
||||
current_sample = 0
|
||||
|
||||
for lora_idx in range(lora_strength_count if loras else 1):
|
||||
if loras:
|
||||
# Find which LoRA file and strength to use
|
||||
cumulative_idx = 0
|
||||
lora_file_idx = 0
|
||||
strength_in_file_idx = 0
|
||||
|
||||
# Determine which LoRA file this index corresponds to
|
||||
for file_idx, strengths in enumerate(lora_strength):
|
||||
if lora_idx < cumulative_idx + len(strengths):
|
||||
lora_file_idx = file_idx
|
||||
strength_in_file_idx = lora_idx - cumulative_idx
|
||||
break
|
||||
cumulative_idx += len(strengths)
|
||||
|
||||
# Load the appropriate LoRA with its strength
|
||||
if lora_file_idx < len(lora_model) and strength_in_file_idx < len(
|
||||
lora_strength[lora_file_idx]
|
||||
):
|
||||
patched_model = self.lora_loader.load_lora(
|
||||
model,
|
||||
None,
|
||||
lora_model[lora_file_idx],
|
||||
lora_strength[lora_file_idx][strength_in_file_idx],
|
||||
0,
|
||||
)[0]
|
||||
else:
|
||||
patched_model = model
|
||||
else:
|
||||
patched_model = model
|
||||
|
||||
for cond_idx, cond in enumerate(cond_encoded):
|
||||
prompt_text = cond_text[cond_idx] if cond_text else None
|
||||
|
||||
for params in param_combos:
|
||||
current_sample += 1
|
||||
|
||||
if is_schnell:
|
||||
work_model = modelsampling.patch_aura(
|
||||
patched_model, params["base_shift"]
|
||||
)[0]
|
||||
else:
|
||||
work_model = modelsampling.patch(
|
||||
patched_model,
|
||||
params["max_shift"],
|
||||
params["base_shift"],
|
||||
width,
|
||||
height,
|
||||
)[0]
|
||||
|
||||
cond_with_guidance = conditioning_set_values(
|
||||
cond, {"guidance": params["guidance"]}
|
||||
)
|
||||
|
||||
guider = basicguider.get_guider(work_model, cond_with_guidance)[
|
||||
0
|
||||
]
|
||||
sampler_obj = comfy.samplers.sampler_object(params["sampler"])
|
||||
sigmas = basicscheduler.get_sigmas(
|
||||
work_model,
|
||||
params["scheduler"],
|
||||
params["steps"],
|
||||
params["denoise"],
|
||||
)[0]
|
||||
|
||||
noise = Noise_RandomNoise(params["seed"])
|
||||
|
||||
self.log_info(
|
||||
f"Sample {current_sample}/{total_samples}: "
|
||||
f"seed={params['seed']}, sampler={params['sampler']}, "
|
||||
f"steps={params['steps']}"
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
latent = samplercustomadvanced.sample(
|
||||
noise, guider, sampler_obj, sigmas, latent_image
|
||||
)[1]
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
param_record = {
|
||||
**params,
|
||||
"time": elapsed,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"prompt": prompt_text,
|
||||
}
|
||||
|
||||
if loras:
|
||||
# Record which LoRA and strength was used
|
||||
param_record["lora"] = (
|
||||
lora_model[lora_file_idx]
|
||||
if lora_file_idx < len(lora_model)
|
||||
else None
|
||||
)
|
||||
param_record["lora_strength"] = (
|
||||
lora_strength[lora_file_idx][strength_in_file_idx]
|
||||
if lora_file_idx < len(lora_strength)
|
||||
and strength_in_file_idx
|
||||
< len(lora_strength[lora_file_idx])
|
||||
else 0
|
||||
)
|
||||
|
||||
out_params.append(param_record)
|
||||
|
||||
if out_latent is None:
|
||||
out_latent = latent
|
||||
else:
|
||||
out_latent = latentbatch.batch(out_latent, latent)[0]
|
||||
|
||||
if total_samples > 1:
|
||||
pbar.update(1)
|
||||
|
||||
self.log_info(f"Completed {len(out_params)} samples")
|
||||
return (out_latent, out_params)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Error in batch processing: {str(e)}", e)
|
||||
return (latent_image, [])
|
||||
@@ -0,0 +1,5 @@
|
||||
"""LoRA Folder Batch module."""
|
||||
|
||||
from .node import LoRAFolderBatchNode
|
||||
|
||||
__all__ = ["LoRAFolderBatchNode"]
|
||||
@@ -0,0 +1,334 @@
|
||||
"""Logic module for LoRA Folder Batch node."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_lora_folders() -> List[str]:
|
||||
"""
|
||||
Get list of available LoRA folders.
|
||||
|
||||
Returns:
|
||||
List of folder paths relative to models/loras
|
||||
"""
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
lora_path = folder_paths.folder_names_and_paths["loras"][0][0]
|
||||
|
||||
folders = []
|
||||
for root, dirs, _ in os.walk(lora_path):
|
||||
for dir_name in dirs:
|
||||
rel_path = os.path.relpath(os.path.join(root, dir_name), lora_path)
|
||||
folders.append(rel_path)
|
||||
|
||||
# Add root folder option
|
||||
folders.insert(0, ".")
|
||||
return folders
|
||||
|
||||
except (ImportError, KeyError):
|
||||
# Fallback for testing
|
||||
return [".", "flux", "sdxl", "sd15"]
|
||||
|
||||
|
||||
def scan_folder_for_loras(folder_path: str) -> List[str]:
|
||||
"""
|
||||
Scan a folder for LoRA files (.safetensors).
|
||||
|
||||
Args:
|
||||
folder_path: Path to folder to scan (absolute or relative to models/loras)
|
||||
|
||||
Returns:
|
||||
List of LoRA filenames relative to models/loras directory
|
||||
"""
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
# Get all LoRA paths from ComfyUI (includes extra_model_paths)
|
||||
lora_paths = folder_paths.folder_names_and_paths.get("loras", [[]])[0]
|
||||
|
||||
# Check if this is an absolute path
|
||||
if os.path.isabs(folder_path):
|
||||
full_path = folder_path
|
||||
|
||||
# Try to find which lora base path this belongs to
|
||||
rel_folder = None
|
||||
for lora_base in lora_paths:
|
||||
try:
|
||||
potential_rel = os.path.relpath(full_path, lora_base)
|
||||
if not potential_rel.startswith(".."):
|
||||
# This path is inside this lora base
|
||||
rel_folder = potential_rel
|
||||
break
|
||||
except ValueError:
|
||||
# Different drives on Windows
|
||||
continue
|
||||
|
||||
if rel_folder is None:
|
||||
# Path is outside all known lora directories
|
||||
# Try to extract a relative path that might work
|
||||
# Check if path contains common lora folder structures
|
||||
path_parts = full_path.replace("\\", "/").split("/")
|
||||
if "lora" in path_parts or "loras" in path_parts:
|
||||
# Find index after lora/loras
|
||||
for i, part in enumerate(path_parts):
|
||||
if part in ["lora", "loras"]:
|
||||
# Use everything after lora/loras as relative path
|
||||
rel_folder = "/".join(path_parts[i + 1 :])
|
||||
break
|
||||
|
||||
if rel_folder is None:
|
||||
# Last resort: use last two directories as relative path
|
||||
rel_folder = (
|
||||
"/".join(path_parts[-2:])
|
||||
if len(path_parts) >= 2
|
||||
else path_parts[-1]
|
||||
)
|
||||
else:
|
||||
# Relative path provided
|
||||
full_path = (
|
||||
os.path.join(lora_paths[0], folder_path) if lora_paths else folder_path
|
||||
)
|
||||
rel_folder = folder_path if folder_path != "." else ""
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
logger.warning(f"Folder does not exist: {full_path}")
|
||||
return []
|
||||
|
||||
# Scan for .safetensors files
|
||||
lora_files = []
|
||||
for file in os.listdir(full_path):
|
||||
if file.endswith(".safetensors"):
|
||||
# Store relative path from lora base
|
||||
if rel_folder and rel_folder != ".":
|
||||
lora_files.append(os.path.join(rel_folder, file).replace("\\", "/"))
|
||||
else:
|
||||
lora_files.append(file)
|
||||
|
||||
# Sort naturally (handles epoch numbers properly)
|
||||
lora_files = natural_sort(lora_files)
|
||||
|
||||
logger.info(
|
||||
f"Found {len(lora_files)} LoRA files in {folder_path}, returning paths relative to lora base"
|
||||
)
|
||||
return lora_files
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning folder {folder_path}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def natural_sort(items: List[str]) -> List[str]:
|
||||
"""
|
||||
Sort strings naturally, handling numbers properly.
|
||||
|
||||
Args:
|
||||
items: List of strings to sort
|
||||
|
||||
Returns:
|
||||
Naturally sorted list
|
||||
"""
|
||||
|
||||
def natural_key(text):
|
||||
def atoi(text):
|
||||
return int(text) if text.isdigit() else text
|
||||
|
||||
# Split on digits and filter out empty strings
|
||||
parts = [atoi(c) for c in re.split(r"(\d+)", text) if c]
|
||||
# Put files without numbers first
|
||||
if not any(isinstance(p, int) for p in parts):
|
||||
return [0] + parts
|
||||
return parts
|
||||
|
||||
return sorted(items, key=natural_key)
|
||||
|
||||
|
||||
def filter_loras_by_pattern(
|
||||
lora_files: List[str], include_pattern: str = "", exclude_pattern: str = ""
|
||||
) -> List[str]:
|
||||
"""
|
||||
Filter LoRA files by include/exclude patterns.
|
||||
|
||||
Args:
|
||||
lora_files: List of LoRA filenames
|
||||
include_pattern: Regex pattern to include (empty = include all)
|
||||
exclude_pattern: Regex pattern to exclude (empty = exclude none)
|
||||
|
||||
Returns:
|
||||
Filtered list of LoRA files
|
||||
"""
|
||||
filtered = lora_files.copy()
|
||||
|
||||
# Apply include pattern
|
||||
if include_pattern:
|
||||
try:
|
||||
include_re = re.compile(include_pattern)
|
||||
filtered = [f for f in filtered if include_re.search(f)]
|
||||
except re.error as e:
|
||||
logger.error(f"Invalid include pattern: {e}")
|
||||
|
||||
# Apply exclude pattern
|
||||
if exclude_pattern:
|
||||
try:
|
||||
exclude_re = re.compile(exclude_pattern)
|
||||
filtered = [f for f in filtered if not exclude_re.search(f)]
|
||||
except re.error as e:
|
||||
logger.error(f"Invalid exclude pattern: {e}")
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def parse_strength_string(strength_str: str) -> List[float]:
|
||||
"""
|
||||
Parse strength string into list of values.
|
||||
|
||||
Supports:
|
||||
- Single value: "1.0"
|
||||
- Multiple values: "0.5, 0.75, 1.0"
|
||||
- Range: "0.5...1.0" (with optional step)
|
||||
|
||||
Args:
|
||||
strength_str: String representation of strengths
|
||||
|
||||
Returns:
|
||||
List of strength values
|
||||
"""
|
||||
strength_str = strength_str.strip()
|
||||
|
||||
if not strength_str:
|
||||
return [1.0]
|
||||
|
||||
# Check for range notation
|
||||
if "..." in strength_str:
|
||||
parts = strength_str.split("...")
|
||||
if len(parts) == 2:
|
||||
try:
|
||||
start = float(parts[0].strip())
|
||||
end_part = parts[1].strip()
|
||||
|
||||
# Check for step
|
||||
if "+" in end_part:
|
||||
end_str, step_str = end_part.split("+")
|
||||
end = float(end_str.strip())
|
||||
step = float(step_str.strip())
|
||||
else:
|
||||
end = float(end_part)
|
||||
step = 0.1 # Default step
|
||||
|
||||
# Generate range
|
||||
values = []
|
||||
current = start
|
||||
while current <= end + 0.0001: # Small epsilon for float comparison
|
||||
values.append(round(current, 4))
|
||||
current += step
|
||||
|
||||
return values
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid range format: {e}")
|
||||
return [1.0]
|
||||
|
||||
# Parse comma-separated values
|
||||
try:
|
||||
values = []
|
||||
for item in strength_str.split(","):
|
||||
item = item.strip()
|
||||
if item:
|
||||
values.append(float(item))
|
||||
return values if values else [1.0]
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid strength values: {e}")
|
||||
return [1.0]
|
||||
|
||||
|
||||
def create_lora_params(
|
||||
lora_files: List[str], strengths: List[float], batch_mode: str = "sequential"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create LORA_PARAMS structure for FluxSamplerParams.
|
||||
|
||||
Args:
|
||||
lora_files: List of LoRA file paths
|
||||
strengths: List of strength values to test
|
||||
batch_mode: How to batch ("sequential" or "combinatorial")
|
||||
|
||||
Returns:
|
||||
LORA_PARAMS dictionary
|
||||
"""
|
||||
if not lora_files:
|
||||
logger.warning("No LoRA files provided")
|
||||
return {"loras": [], "strengths": []}
|
||||
|
||||
if batch_mode == "combinatorial":
|
||||
# Each LoRA gets tested with each strength
|
||||
# This creates len(loras) * len(strengths) combinations
|
||||
return {"loras": lora_files, "strengths": [strengths for _ in lora_files]}
|
||||
else:
|
||||
# Sequential mode - cycle through strengths for each LoRA
|
||||
# If fewer strengths than LoRAs, repeat the strength list
|
||||
strength_lists = []
|
||||
for i, lora in enumerate(lora_files):
|
||||
strength_idx = i % len(strengths)
|
||||
strength_lists.append([strengths[strength_idx]])
|
||||
|
||||
return {"loras": lora_files, "strengths": strength_lists}
|
||||
|
||||
|
||||
def get_lora_info(lora_file: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information from LoRA filename.
|
||||
|
||||
Args:
|
||||
lora_file: LoRA filename
|
||||
|
||||
Returns:
|
||||
Dictionary with extracted info (name, epoch, version, etc.)
|
||||
"""
|
||||
info = {
|
||||
"filename": lora_file,
|
||||
"name": os.path.splitext(os.path.basename(lora_file))[0],
|
||||
"epoch": None,
|
||||
"version": None,
|
||||
}
|
||||
|
||||
# Try to extract epoch number
|
||||
epoch_match = re.search(r"[-_](\d{6}|\d{5}|\d{4}|\d{3})", info["name"])
|
||||
if epoch_match:
|
||||
info["epoch"] = int(epoch_match.group(1))
|
||||
|
||||
# Try to extract version
|
||||
version_match = re.search(r"v(\d+(?:\.\d+)?)", info["name"], re.IGNORECASE)
|
||||
if version_match:
|
||||
info["version"] = f"v{version_match.group(1)}"
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def validate_folder_path(folder_path: str) -> bool:
|
||||
"""
|
||||
Validate that the folder path exists and is accessible.
|
||||
|
||||
Args:
|
||||
folder_path: Folder path to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
lora_base_path = folder_paths.folder_names_and_paths["loras"][0][0]
|
||||
|
||||
if folder_path == ".":
|
||||
full_path = lora_base_path
|
||||
else:
|
||||
full_path = os.path.join(lora_base_path, folder_path)
|
||||
|
||||
return os.path.exists(full_path) and os.path.isdir(full_path)
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,185 @@
|
||||
"""LoRA Folder Batch node for ComfyUI."""
|
||||
|
||||
from typing import Tuple, Any, Dict, List
|
||||
import os
|
||||
import logging
|
||||
from ....base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
get_lora_folders,
|
||||
scan_folder_for_loras,
|
||||
filter_loras_by_pattern,
|
||||
parse_strength_string,
|
||||
create_lora_params,
|
||||
get_lora_info,
|
||||
validate_folder_path,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoRAFolderBatchNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
LoRA Folder Batch node for processing multiple LoRAs from a folder.
|
||||
|
||||
Scans a specified folder for all .safetensors files and creates
|
||||
LORA_PARAMS for batch processing with FluxSamplerParams. Perfect
|
||||
for testing different epochs or variations of the same LoRA.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
return {
|
||||
"required": {
|
||||
"folder_path": (
|
||||
"STRING",
|
||||
{
|
||||
"default": ".",
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"tooltip": "Folder path relative to models/loras (or absolute path)",
|
||||
},
|
||||
),
|
||||
"strength": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "1.0",
|
||||
"multiline": False,
|
||||
"dynamicPrompts": False,
|
||||
"tooltip": "Strength values (e.g., '1.0' or '0.5,0.75,1.0' or '0.5...1.0+0.1')",
|
||||
},
|
||||
),
|
||||
"batch_mode": (
|
||||
["sequential", "combinatorial"],
|
||||
{
|
||||
"default": "sequential",
|
||||
"tooltip": "Sequential: one strength per LoRA, Combinatorial: all strengths for each LoRA",
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"include_pattern": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Regex pattern to include files (empty = all)",
|
||||
},
|
||||
),
|
||||
"exclude_pattern": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": "Regex pattern to exclude files (e.g., 'test|backup')",
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LORA_PARAMS", "STRING", "INT")
|
||||
RETURN_NAMES = ("lora_params", "lora_list", "lora_count")
|
||||
FUNCTION = "batch_loras"
|
||||
CATEGORY = "ComfyAssets/🧰 xyz-helpers"
|
||||
|
||||
def batch_loras(
|
||||
self,
|
||||
folder_path: str,
|
||||
strength: str,
|
||||
batch_mode: str,
|
||||
include_pattern: str = "",
|
||||
exclude_pattern: str = "",
|
||||
) -> Tuple[Dict[str, Any], str, int]:
|
||||
"""
|
||||
Batch process LoRAs from a folder.
|
||||
|
||||
Args:
|
||||
folder_path: Folder to scan (relative to models/loras or absolute)
|
||||
strength: Strength values string
|
||||
batch_mode: How to batch the LoRAs
|
||||
include_pattern: Optional include regex
|
||||
exclude_pattern: Optional exclude regex
|
||||
|
||||
Returns:
|
||||
Tuple of (lora_params, lora_list_string, lora_count)
|
||||
"""
|
||||
try:
|
||||
|
||||
# Validate folder only if not in test mode
|
||||
try:
|
||||
if not validate_folder_path(folder_path):
|
||||
self.handle_error(f"Invalid or inaccessible folder: {folder_path}")
|
||||
except ImportError:
|
||||
# In test environment, skip validation
|
||||
pass
|
||||
|
||||
# Scan folder for LoRAs
|
||||
lora_files = scan_folder_for_loras(folder_path)
|
||||
|
||||
if not lora_files:
|
||||
self.log_info(f"No LoRA files found in {folder_path}")
|
||||
return ({"loras": [], "strengths": []}, "", 0)
|
||||
|
||||
self.log_info(f"Found {len(lora_files)} LoRA files in {folder_path}")
|
||||
|
||||
# Apply filters
|
||||
if include_pattern or exclude_pattern:
|
||||
filtered = filter_loras_by_pattern(
|
||||
lora_files, include_pattern, exclude_pattern
|
||||
)
|
||||
if len(filtered) < len(lora_files):
|
||||
self.log_info(
|
||||
f"Filtered from {len(lora_files)} to {len(filtered)} LoRAs"
|
||||
)
|
||||
lora_files = filtered
|
||||
|
||||
if not lora_files:
|
||||
self.log_info("No LoRAs left after filtering")
|
||||
return ({"loras": [], "strengths": []}, "", 0)
|
||||
|
||||
# Parse strength values
|
||||
strengths = parse_strength_string(strength)
|
||||
self.log_info(f"Using strength values: {strengths}")
|
||||
|
||||
# Create LORA_PARAMS
|
||||
lora_params = create_lora_params(lora_files, strengths, batch_mode)
|
||||
|
||||
# Create info string
|
||||
lora_list = []
|
||||
for lora_file in lora_files:
|
||||
info = get_lora_info(lora_file)
|
||||
if info["epoch"] is not None:
|
||||
lora_list.append(f"{info['name']} (epoch {info['epoch']})")
|
||||
else:
|
||||
lora_list.append(info["name"])
|
||||
|
||||
lora_list_str = "\n".join(lora_list)
|
||||
|
||||
# Calculate total combinations
|
||||
if batch_mode == "combinatorial":
|
||||
total_combos = len(lora_files) * len(strengths)
|
||||
else:
|
||||
total_combos = len(lora_files)
|
||||
|
||||
self.log_info(
|
||||
f"Created batch with {len(lora_files)} LoRAs, "
|
||||
f"{len(strengths)} strength values, "
|
||||
f"{total_combos} total combinations"
|
||||
)
|
||||
|
||||
return (lora_params, lora_list_str, len(lora_files))
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Error creating LoRA batch: {str(e)}", e)
|
||||
return ({"loras": [], "strengths": []}, "", 0)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
"""
|
||||
Force re-execution when folder contents might have changed.
|
||||
|
||||
This ensures we always scan for the latest LoRAs.
|
||||
"""
|
||||
import time
|
||||
|
||||
return str(time.time())
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Plot Parameters module."""
|
||||
|
||||
from .node import PlotParametersNode
|
||||
|
||||
__all__ = ["PlotParametersNode"]
|
||||
@@ -0,0 +1,338 @@
|
||||
"""Logic module for Plot Parameters node."""
|
||||
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
import math
|
||||
import textwrap
|
||||
import logging
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sort_parameters(params: List[Dict], order_by: str) -> Tuple[List[Dict], List[int]]:
|
||||
"""
|
||||
Sort parameters by a specified key.
|
||||
|
||||
Args:
|
||||
params: List of parameter dictionaries
|
||||
order_by: Key to sort by
|
||||
|
||||
Returns:
|
||||
Tuple of (sorted_params, original_indices)
|
||||
"""
|
||||
if order_by == "none":
|
||||
return params, list(range(len(params)))
|
||||
|
||||
try:
|
||||
# Create indexed list
|
||||
indexed_params = [(i, p) for i, p in enumerate(params)]
|
||||
|
||||
# Sort by the specified key
|
||||
sorted_indexed = sorted(indexed_params, key=lambda x: x[1].get(order_by, 0))
|
||||
|
||||
# Extract sorted params and indices
|
||||
indices = [i for i, _ in sorted_indexed]
|
||||
sorted_params = [p for _, p in sorted_indexed]
|
||||
|
||||
return sorted_params, indices
|
||||
except Exception as e:
|
||||
logger.error(f"Error sorting parameters: {e}")
|
||||
return params, list(range(len(params)))
|
||||
|
||||
|
||||
def group_by_value(
|
||||
params: List[Dict], group_key: str
|
||||
) -> Tuple[List[Dict], List[int], int]:
|
||||
"""
|
||||
Group parameters by a specific value and arrange in columns.
|
||||
|
||||
Args:
|
||||
params: List of parameter dictionaries
|
||||
group_key: Key to group by
|
||||
|
||||
Returns:
|
||||
Tuple of (rearranged_params, indices, num_groups)
|
||||
"""
|
||||
if group_key == "none":
|
||||
return params, list(range(len(params))), -1
|
||||
|
||||
try:
|
||||
# Group parameters by the specified key
|
||||
groups = {}
|
||||
for i, p in enumerate(params):
|
||||
value = p.get(group_key, "unknown")
|
||||
if value not in groups:
|
||||
groups[value] = []
|
||||
groups[value].append((i, p))
|
||||
|
||||
num_groups = len(groups)
|
||||
|
||||
# Rearrange for column layout
|
||||
sorted_params = []
|
||||
indices = []
|
||||
|
||||
# Convert groups to list
|
||||
group_lists = list(groups.values())
|
||||
|
||||
# Zip groups together for column arrangement
|
||||
max_len = max(len(g) for g in group_lists)
|
||||
for i in range(max_len):
|
||||
for group in group_lists:
|
||||
if i < len(group):
|
||||
idx, param = group[i]
|
||||
indices.append(idx)
|
||||
sorted_params.append(param)
|
||||
|
||||
return sorted_params, indices, num_groups
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error grouping parameters: {e}")
|
||||
return params, list(range(len(params))), -1
|
||||
|
||||
|
||||
def identify_changing_parameters(params: List[Dict]) -> Dict[str, bool]:
|
||||
"""
|
||||
Identify which parameters change across the batch.
|
||||
|
||||
Args:
|
||||
params: List of parameter dictionaries
|
||||
|
||||
Returns:
|
||||
Dictionary mapping parameter names to whether they change
|
||||
"""
|
||||
if not params:
|
||||
return {}
|
||||
|
||||
changing = {}
|
||||
|
||||
# Track unique values for each parameter
|
||||
value_tracker = {}
|
||||
|
||||
for p in params:
|
||||
for key, value in p.items():
|
||||
if key == "time": # Skip time as it always changes
|
||||
continue
|
||||
|
||||
if key not in value_tracker:
|
||||
value_tracker[key] = set()
|
||||
|
||||
# Handle different value types
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = str(value)
|
||||
elif isinstance(value, dict):
|
||||
value = str(sorted(value.items()))
|
||||
|
||||
value_tracker[key].add(value)
|
||||
|
||||
# Mark parameters as changing if they have multiple values
|
||||
for key, values in value_tracker.items():
|
||||
changing[key] = len(values) > 1
|
||||
|
||||
# Always include prompt if present
|
||||
if any("prompt" in p for p in params):
|
||||
changing["prompt"] = True
|
||||
|
||||
return changing
|
||||
|
||||
|
||||
def filter_changing_params(params: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Filter parameters to only show those that change.
|
||||
|
||||
Args:
|
||||
params: List of parameter dictionaries
|
||||
|
||||
Returns:
|
||||
List of filtered parameter dictionaries
|
||||
"""
|
||||
changing = identify_changing_parameters(params)
|
||||
|
||||
filtered = []
|
||||
for p in params:
|
||||
filtered_param = {}
|
||||
for key, value in p.items():
|
||||
if changing.get(key, False):
|
||||
filtered_param[key] = value
|
||||
filtered.append(filtered_param)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def format_parameter_text(param: Dict, mode: str = "full") -> str:
|
||||
"""
|
||||
Format parameter dictionary as display text.
|
||||
|
||||
Args:
|
||||
param: Parameter dictionary
|
||||
mode: Display mode ("full", "changes only")
|
||||
|
||||
Returns:
|
||||
Formatted text string
|
||||
"""
|
||||
if mode == "changes only":
|
||||
lines = []
|
||||
for key, value in param.items():
|
||||
if key != "prompt":
|
||||
lines.append(f"{key}: {value}")
|
||||
return "\n".join(lines)
|
||||
else:
|
||||
# Full format
|
||||
lines = []
|
||||
|
||||
# First line: time, seed, steps, size
|
||||
if "time" in param:
|
||||
lines.append(
|
||||
f"time: {param['time']:.2f}s, seed: {param.get('seed', 'N/A')}, "
|
||||
f"steps: {param.get('steps', 'N/A')}, "
|
||||
f"size: {param.get('width', 'N/A')}×{param.get('height', 'N/A')}"
|
||||
)
|
||||
|
||||
# Second line: denoise, sampler, scheduler
|
||||
lines.append(
|
||||
f"denoise: {param.get('denoise', 'N/A')}, "
|
||||
f"sampler: {param.get('sampler', 'N/A')}, "
|
||||
f"sched: {param.get('scheduler', 'N/A')}"
|
||||
)
|
||||
|
||||
# Third line: guidance, shifts
|
||||
lines.append(
|
||||
f"guidance: {param.get('guidance', 'N/A')}, "
|
||||
f"max/base shift: {param.get('max_shift', 'N/A')}/{param.get('base_shift', 'N/A')}"
|
||||
)
|
||||
|
||||
# Optional LoRA line
|
||||
if "lora" in param and param["lora"]:
|
||||
lora_name = param["lora"][:32] if len(param["lora"]) > 32 else param["lora"]
|
||||
lines.append(f"LoRA: {lora_name}, str: {param.get('lora_strength', 'N/A')}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def wrap_prompt_text(prompt: str, width_chars: int, mode: str = "full") -> List[str]:
|
||||
"""
|
||||
Wrap prompt text to fit within character width.
|
||||
|
||||
Args:
|
||||
prompt: Prompt text to wrap
|
||||
width_chars: Maximum characters per line
|
||||
mode: Display mode ("full", "excerpt")
|
||||
|
||||
Returns:
|
||||
List of wrapped lines
|
||||
"""
|
||||
if not prompt:
|
||||
return []
|
||||
|
||||
original_words = prompt.split()
|
||||
|
||||
if mode == "excerpt":
|
||||
# Take first 64 words
|
||||
words = original_words[:64]
|
||||
prompt = " ".join(words)
|
||||
# Add ellipsis if we truncated
|
||||
if len(words) < len(original_words):
|
||||
prompt += "..."
|
||||
|
||||
# Use textwrap to break into lines
|
||||
lines = textwrap.wrap(prompt, width=width_chars)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def calculate_text_dimensions(
|
||||
text: str, font_size: int, image_width: int
|
||||
) -> Tuple[int, int, int]:
|
||||
"""
|
||||
Calculate text rendering dimensions.
|
||||
|
||||
Args:
|
||||
text: Text to render
|
||||
font_size: Font size in pixels
|
||||
image_width: Width of the image
|
||||
|
||||
Returns:
|
||||
Tuple of (line_height, char_width, num_lines)
|
||||
"""
|
||||
# Approximate calculations (adjust based on actual font metrics)
|
||||
line_height = int(font_size * 1.5) # Line height with padding
|
||||
char_width = int(font_size * 0.6) # Approximate monospace char width
|
||||
|
||||
lines = text.split("\n")
|
||||
num_lines = len(lines)
|
||||
|
||||
return line_height, char_width, num_lines
|
||||
|
||||
|
||||
def calculate_grid_dimensions(num_images: int, cols_num: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Calculate grid dimensions for image layout.
|
||||
|
||||
Args:
|
||||
num_images: Total number of images
|
||||
cols_num: Number of columns (-1 for auto)
|
||||
|
||||
Returns:
|
||||
Tuple of (rows, cols)
|
||||
"""
|
||||
if cols_num == 0 or cols_num == -1:
|
||||
# Auto-calculate columns
|
||||
cols = int(math.sqrt(num_images))
|
||||
cols = max(1, min(cols, 1024))
|
||||
else:
|
||||
cols = min(cols_num, num_images)
|
||||
|
||||
rows = math.ceil(num_images / cols)
|
||||
|
||||
return rows, cols
|
||||
|
||||
|
||||
def validate_plot_parameters(
|
||||
images_shape: tuple,
|
||||
params_length: int,
|
||||
order_by: str,
|
||||
cols_value: str,
|
||||
cols_num: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Validate plot parameters configuration.
|
||||
|
||||
Args:
|
||||
images_shape: Shape of the images tensor
|
||||
params_length: Length of parameters list
|
||||
order_by: Ordering key
|
||||
cols_value: Column grouping key
|
||||
cols_num: Number of columns
|
||||
|
||||
Returns:
|
||||
True if configuration is valid
|
||||
"""
|
||||
if images_shape[0] != params_length:
|
||||
logger.error(
|
||||
f"Image count ({images_shape[0]}) doesn't match parameters ({params_length})"
|
||||
)
|
||||
return False
|
||||
|
||||
valid_keys = [
|
||||
"none",
|
||||
"time",
|
||||
"seed",
|
||||
"steps",
|
||||
"denoise",
|
||||
"sampler",
|
||||
"scheduler",
|
||||
"guidance",
|
||||
"max_shift",
|
||||
"base_shift",
|
||||
"lora_strength",
|
||||
]
|
||||
|
||||
if order_by not in valid_keys:
|
||||
logger.warning(f"Invalid order_by value: {order_by}")
|
||||
|
||||
if cols_value not in valid_keys:
|
||||
logger.warning(f"Invalid cols_value: {cols_value}")
|
||||
|
||||
if cols_num < -1 or cols_num > 1024:
|
||||
logger.warning(f"Invalid cols_num: {cols_num}")
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,310 @@
|
||||
"""Plot Parameters node for ComfyUI."""
|
||||
|
||||
from typing import Tuple, Any, List, Dict
|
||||
import os
|
||||
import math
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import logging
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
try:
|
||||
import torchvision.transforms.v2 as T
|
||||
except ImportError:
|
||||
try:
|
||||
import torchvision.transforms as T
|
||||
except ImportError:
|
||||
# Fallback for test environment without torchvision
|
||||
class T:
|
||||
@staticmethod
|
||||
def ToTensor():
|
||||
def to_tensor(img):
|
||||
import numpy as np
|
||||
|
||||
if isinstance(img, Image.Image):
|
||||
img = np.array(img)
|
||||
img = torch.from_numpy(img).float() / 255.0
|
||||
if len(img.shape) == 3:
|
||||
img = img.permute(2, 0, 1)
|
||||
return img
|
||||
|
||||
return to_tensor
|
||||
|
||||
|
||||
from ....base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
sort_parameters,
|
||||
group_by_value,
|
||||
filter_changing_params,
|
||||
format_parameter_text,
|
||||
wrap_prompt_text,
|
||||
calculate_text_dimensions,
|
||||
calculate_grid_dimensions,
|
||||
validate_plot_parameters,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlotParametersNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Plot Parameters node for visualizing batch sampling results.
|
||||
|
||||
Creates a grid layout of images with parameter annotations,
|
||||
useful for comparing results across different sampling parameters.
|
||||
Supports sorting, grouping, and filtering display options.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
order_options = [
|
||||
"none",
|
||||
"time",
|
||||
"seed",
|
||||
"steps",
|
||||
"denoise",
|
||||
"sampler",
|
||||
"scheduler",
|
||||
"guidance",
|
||||
"max_shift",
|
||||
"base_shift",
|
||||
"lora_strength",
|
||||
]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", {"tooltip": "Batch of images to arrange"}),
|
||||
"params": (
|
||||
"SAMPLER_PARAMS",
|
||||
{"tooltip": "Parameters from FluxSamplerParams"},
|
||||
),
|
||||
"order_by": (
|
||||
order_options,
|
||||
{"default": "none", "tooltip": "Sort images by this parameter"},
|
||||
),
|
||||
"cols_value": (
|
||||
order_options,
|
||||
{
|
||||
"default": "none",
|
||||
"tooltip": "Group into columns by this parameter",
|
||||
},
|
||||
),
|
||||
"cols_num": (
|
||||
"INT",
|
||||
{
|
||||
"default": -1,
|
||||
"min": -1,
|
||||
"max": 1024,
|
||||
"tooltip": "Number of columns (-1 for auto, 0 for square)",
|
||||
},
|
||||
),
|
||||
"add_prompt": (
|
||||
["false", "true", "excerpt"],
|
||||
{"default": "false", "tooltip": "Add prompt text to images"},
|
||||
),
|
||||
"add_params": (
|
||||
["false", "true", "changes only"],
|
||||
{"default": "true", "tooltip": "Add parameter text to images"},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "plot_parameters"
|
||||
CATEGORY = "ComfyAssets/🧰 xyz-helpers"
|
||||
|
||||
def plot_parameters(
|
||||
self,
|
||||
images: torch.Tensor,
|
||||
params: List[Dict[str, Any]],
|
||||
order_by: str,
|
||||
cols_value: str,
|
||||
cols_num: int,
|
||||
add_prompt: str,
|
||||
add_params: str,
|
||||
) -> Tuple[torch.Tensor]:
|
||||
"""
|
||||
Create a plot grid with parameter annotations.
|
||||
|
||||
Args:
|
||||
images: Tensor of images [B, H, W, C]
|
||||
params: List of parameter dictionaries
|
||||
order_by: Parameter to sort by
|
||||
cols_value: Parameter to group columns by
|
||||
cols_num: Number of columns
|
||||
add_prompt: Whether to add prompt text
|
||||
add_params: Whether to add parameter text
|
||||
|
||||
Returns:
|
||||
Tuple containing the plotted image grid
|
||||
"""
|
||||
try:
|
||||
if not validate_plot_parameters(
|
||||
images.shape, len(params), order_by, cols_value, cols_num
|
||||
):
|
||||
self.handle_error("Invalid plot parameters configuration")
|
||||
|
||||
# Copy params to avoid modifying original
|
||||
_params = params.copy()
|
||||
|
||||
# Sort if requested
|
||||
if order_by != "none":
|
||||
_params, indices = sort_parameters(_params, order_by)
|
||||
images = images[torch.tensor(indices)]
|
||||
self.log_info(f"Sorted by {order_by}")
|
||||
|
||||
# Group by value if requested
|
||||
if cols_value != "none" and cols_num > -1:
|
||||
_params, indices, num_groups = group_by_value(_params, cols_value)
|
||||
if num_groups > 0:
|
||||
cols_num = num_groups
|
||||
images = images[torch.tensor(indices)]
|
||||
self.log_info(f"Grouped into {num_groups} columns by {cols_value}")
|
||||
elif cols_num == 0:
|
||||
# Auto square layout
|
||||
cols_num = int(math.sqrt(images.shape[0]))
|
||||
cols_num = max(1, min(cols_num, 1024))
|
||||
|
||||
# Filter params if showing changes only
|
||||
if add_params == "changes only":
|
||||
_params = filter_changing_params(_params)
|
||||
|
||||
# Get font
|
||||
font_path = self._get_font_path()
|
||||
width = images.shape[2]
|
||||
font_size = min(48, int(32 * (width / 1024)))
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except:
|
||||
logger.warning(f"Could not load font from {font_path}, using default")
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Calculate text dimensions
|
||||
text_padding = 3
|
||||
line_height = (
|
||||
font.getmask("Q").getbbox()[3] + font.getmetrics()[1] + text_padding * 2
|
||||
)
|
||||
char_width = font.getbbox("M")[2] + 1 # Monospace approximation
|
||||
|
||||
# Process each image
|
||||
out_images = []
|
||||
for image, param in zip(images, _params):
|
||||
image = image.permute(2, 0, 1) # [C, H, W]
|
||||
|
||||
# Add parameter text
|
||||
if add_params != "false":
|
||||
param_text = format_parameter_text(
|
||||
param,
|
||||
"changes only" if add_params == "changes only" else "full",
|
||||
)
|
||||
|
||||
lines = param_text.split("\n")
|
||||
text_height = line_height * len(lines)
|
||||
text_image = Image.new("RGB", (width, text_height), color=(0, 0, 0))
|
||||
draw = ImageDraw.Draw(text_image)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
draw.text(
|
||||
(text_padding, i * line_height + text_padding),
|
||||
line,
|
||||
font=font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
|
||||
text_tensor = T.ToTensor()(text_image).to(image.device)
|
||||
image = torch.cat([image, text_tensor], 1)
|
||||
|
||||
# Add prompt text
|
||||
if add_prompt != "false" and "prompt" in param and param["prompt"]:
|
||||
cols = math.ceil(width / char_width)
|
||||
prompt_lines = wrap_prompt_text(
|
||||
param["prompt"],
|
||||
cols,
|
||||
"excerpt" if add_prompt == "excerpt" else "full",
|
||||
)
|
||||
|
||||
prompt_height = line_height * len(prompt_lines)
|
||||
prompt_image = Image.new(
|
||||
"RGB", (width, prompt_height), color=(0, 0, 0)
|
||||
)
|
||||
draw = ImageDraw.Draw(prompt_image)
|
||||
|
||||
for i, line in enumerate(prompt_lines):
|
||||
draw.text(
|
||||
(text_padding, i * line_height + text_padding),
|
||||
line,
|
||||
font=font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
|
||||
prompt_tensor = T.ToTensor()(prompt_image).to(image.device)
|
||||
image = torch.cat([image, prompt_tensor], 1)
|
||||
|
||||
# Clean up NaN values
|
||||
image = torch.nan_to_num(image, nan=0.0).clamp(0.0, 1.0)
|
||||
out_images.append(image)
|
||||
|
||||
# Ensure all images have same height
|
||||
if add_prompt != "false" or add_params == "changes only":
|
||||
max_height = max([img.shape[1] for img in out_images])
|
||||
out_images = [
|
||||
F.pad(img, (0, 0, 0, max_height - img.shape[1]))
|
||||
for img in out_images
|
||||
]
|
||||
|
||||
# Stack images
|
||||
out_image = torch.stack(out_images, 0).permute(0, 2, 3, 1) # [B, H, W, C]
|
||||
|
||||
# Create grid if columns specified
|
||||
if cols_num > -1:
|
||||
rows, cols = calculate_grid_dimensions(out_image.shape[0], cols_num)
|
||||
b, h, w, c = out_image.shape
|
||||
|
||||
# Pad if necessary
|
||||
if b % cols != 0:
|
||||
padding = cols - (b % cols)
|
||||
out_image = F.pad(out_image, (0, 0, 0, 0, 0, 0, 0, padding))
|
||||
b = out_image.shape[0]
|
||||
|
||||
# Reshape into grid
|
||||
out_image = out_image.reshape(rows, cols, h, w, c)
|
||||
out_image = out_image.permute(0, 2, 1, 3, 4) # [rows, h, cols, w, c]
|
||||
out_image = out_image.reshape(rows * h, cols * w, c).unsqueeze(0)
|
||||
|
||||
self.log_info(f"Created {rows}x{cols} grid")
|
||||
|
||||
return (out_image,)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Error creating parameter plot: {str(e)}", e)
|
||||
return (images,)
|
||||
|
||||
def _get_font_path(self) -> str:
|
||||
"""
|
||||
Get the path to the font file.
|
||||
|
||||
Returns:
|
||||
Path to font file
|
||||
"""
|
||||
# Try to find a monospace font
|
||||
possible_paths = [
|
||||
# Check if ComfyUI_essentials font exists
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../../../referance/ComfyUI_essentials/fonts/ShareTechMono-Regular.ttf",
|
||||
),
|
||||
# System fonts
|
||||
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
|
||||
"/System/Library/Fonts/Courier.dfont",
|
||||
"C:\\Windows\\Fonts\\cour.ttf",
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
# Return a default that PIL will handle
|
||||
return "arial.ttf"
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Sampler Select Helper module."""
|
||||
|
||||
from .node import SamplerSelectHelperNode
|
||||
|
||||
__all__ = ["SamplerSelectHelperNode"]
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Logic module for Sampler Select Helper node."""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import comfy.samplers
|
||||
|
||||
SAMPLERS = comfy.samplers.KSampler.SAMPLERS
|
||||
except ImportError:
|
||||
SAMPLERS = [
|
||||
"euler",
|
||||
"euler_cfg_pp",
|
||||
"euler_ancestral",
|
||||
"euler_ancestral_cfg_pp",
|
||||
"heun",
|
||||
"heunpp2",
|
||||
"dpm_2",
|
||||
"dpm_2_ancestral",
|
||||
"lms",
|
||||
"dpm_fast",
|
||||
"dpm_adaptive",
|
||||
"dpmpp_2s_ancestral",
|
||||
"dpmpp_2s_ancestral_cfg_pp",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_sde_gpu",
|
||||
"dpmpp_2m",
|
||||
"dpmpp_2m_cfg_pp",
|
||||
"dpmpp_2m_sde",
|
||||
"dpmpp_2m_sde_gpu",
|
||||
"dpmpp_3m_sde",
|
||||
"dpmpp_3m_sde_gpu",
|
||||
"ddpm",
|
||||
"lcm",
|
||||
"ipndm",
|
||||
"ipndm_v",
|
||||
"deis",
|
||||
"ddim",
|
||||
"uni_pc",
|
||||
"uni_pc_bh2",
|
||||
]
|
||||
|
||||
|
||||
def process_sampler_selection(**sampler_flags: bool) -> str:
|
||||
"""
|
||||
Process boolean flags for each sampler and return selected ones.
|
||||
|
||||
Args:
|
||||
**sampler_flags: Keyword arguments where keys are sampler names
|
||||
and values are boolean selection states
|
||||
|
||||
Returns:
|
||||
Comma-separated string of selected sampler names
|
||||
"""
|
||||
try:
|
||||
selected_samplers = [
|
||||
sampler_name
|
||||
for sampler_name, is_selected in sampler_flags.items()
|
||||
if is_selected
|
||||
]
|
||||
|
||||
if not selected_samplers:
|
||||
logger.warning("No samplers selected, returning empty string")
|
||||
return ""
|
||||
|
||||
result = ", ".join(selected_samplers)
|
||||
logger.info(f"Selected samplers: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing sampler selection: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def validate_sampler_names(sampler_names: str) -> List[str]:
|
||||
"""
|
||||
Validate and clean a comma-separated string of sampler names.
|
||||
|
||||
Args:
|
||||
sampler_names: Comma-separated string of sampler names
|
||||
|
||||
Returns:
|
||||
List of valid sampler names
|
||||
"""
|
||||
if not sampler_names:
|
||||
return []
|
||||
|
||||
try:
|
||||
names = [name.strip() for name in sampler_names.split(",")]
|
||||
valid_names = [name for name in names if name in SAMPLERS]
|
||||
|
||||
invalid_names = [name for name in names if name not in SAMPLERS]
|
||||
if invalid_names:
|
||||
logger.warning(f"Invalid sampler names ignored: {invalid_names}")
|
||||
|
||||
return valid_names
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating sampler names: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_sampler_groups() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get samplers organized by algorithm family.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping algorithm families to sampler names
|
||||
"""
|
||||
groups = {
|
||||
"Euler": ["euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_cfg_pp"],
|
||||
"Heun": ["heun", "heunpp2"],
|
||||
"DPM": ["dpm_2", "dpm_2_ancestral", "dpm_fast", "dpm_adaptive"],
|
||||
"DPM++": [
|
||||
"dpmpp_2s_ancestral",
|
||||
"dpmpp_2s_ancestral_cfg_pp",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_sde_gpu",
|
||||
"dpmpp_2m",
|
||||
"dpmpp_2m_cfg_pp",
|
||||
"dpmpp_2m_sde",
|
||||
"dpmpp_2m_sde_gpu",
|
||||
"dpmpp_3m_sde",
|
||||
"dpmpp_3m_sde_gpu",
|
||||
],
|
||||
"Other": [
|
||||
"lms",
|
||||
"ddpm",
|
||||
"lcm",
|
||||
"ipndm",
|
||||
"ipndm_v",
|
||||
"deis",
|
||||
"ddim",
|
||||
"uni_pc",
|
||||
"uni_pc_bh2",
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
family: [s for s in samplers if s in SAMPLERS]
|
||||
for family, samplers in groups.items()
|
||||
}
|
||||
|
||||
|
||||
def get_default_samplers() -> List[str]:
|
||||
"""
|
||||
Get a list of commonly used default samplers.
|
||||
|
||||
Returns:
|
||||
List of default sampler names
|
||||
"""
|
||||
defaults = [
|
||||
"euler",
|
||||
"euler_ancestral",
|
||||
"dpmpp_2m",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_2m_sde",
|
||||
"ddim",
|
||||
"uni_pc",
|
||||
]
|
||||
return [s for s in defaults if s in SAMPLERS]
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Sampler Select Helper node for ComfyUI."""
|
||||
|
||||
from typing import Tuple
|
||||
from ....base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import process_sampler_selection, SAMPLERS
|
||||
|
||||
|
||||
class SamplerSelectHelperNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Sampler Select Helper node for multi-sampler selection.
|
||||
|
||||
Provides checkboxes for each available sampler and returns a
|
||||
comma-separated string of selected samplers. Useful for batch
|
||||
processing and XYZ plot generation.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
return {
|
||||
"required": {
|
||||
sampler: (
|
||||
"BOOLEAN",
|
||||
{"default": False, "tooltip": f"Enable {sampler} sampler"},
|
||||
)
|
||||
for sampler in SAMPLERS
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_samplers",)
|
||||
FUNCTION = "select_samplers"
|
||||
CATEGORY = "ComfyAssets/🧰 xyz-helpers"
|
||||
|
||||
def select_samplers(self, **sampler_flags) -> Tuple[str]:
|
||||
"""
|
||||
Process sampler selections and return comma-separated string.
|
||||
|
||||
Args:
|
||||
**sampler_flags: Boolean flags for each sampler
|
||||
|
||||
Returns:
|
||||
Tuple containing comma-separated string of selected samplers
|
||||
"""
|
||||
try:
|
||||
selected = process_sampler_selection(**sampler_flags)
|
||||
|
||||
if selected:
|
||||
self.log_info(f"Selected {len(selected.split(', '))} samplers")
|
||||
else:
|
||||
self.log_info("No samplers selected")
|
||||
|
||||
return (selected,)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Error selecting samplers: {str(e)}", e)
|
||||
return ("",)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Scheduler Select Helper module."""
|
||||
|
||||
from .node import SchedulerSelectHelperNode
|
||||
|
||||
__all__ = ["SchedulerSelectHelperNode"]
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Logic module for Scheduler Select Helper node."""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import comfy.samplers
|
||||
|
||||
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS
|
||||
except ImportError:
|
||||
SCHEDULERS = [
|
||||
"normal",
|
||||
"karras",
|
||||
"exponential",
|
||||
"sgm_uniform",
|
||||
"simple",
|
||||
"ddim_uniform",
|
||||
"beta",
|
||||
"linear",
|
||||
"aligned",
|
||||
"ays",
|
||||
]
|
||||
|
||||
|
||||
def process_scheduler_selection(**scheduler_flags: bool) -> str:
|
||||
"""
|
||||
Process boolean flags for each scheduler and return selected ones.
|
||||
|
||||
Args:
|
||||
**scheduler_flags: Keyword arguments where keys are scheduler names
|
||||
and values are boolean selection states
|
||||
|
||||
Returns:
|
||||
Comma-separated string of selected scheduler names
|
||||
"""
|
||||
try:
|
||||
selected_schedulers = [
|
||||
scheduler_name
|
||||
for scheduler_name, is_selected in scheduler_flags.items()
|
||||
if is_selected
|
||||
]
|
||||
|
||||
if not selected_schedulers:
|
||||
logger.warning("No schedulers selected, returning empty string")
|
||||
return ""
|
||||
|
||||
result = ", ".join(selected_schedulers)
|
||||
logger.info(f"Selected schedulers: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing scheduler selection: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def validate_scheduler_names(scheduler_names: str) -> List[str]:
|
||||
"""
|
||||
Validate and clean a comma-separated string of scheduler names.
|
||||
|
||||
Args:
|
||||
scheduler_names: Comma-separated string of scheduler names
|
||||
|
||||
Returns:
|
||||
List of valid scheduler names
|
||||
"""
|
||||
if not scheduler_names:
|
||||
return []
|
||||
|
||||
try:
|
||||
names = [name.strip() for name in scheduler_names.split(",")]
|
||||
valid_names = [name for name in names if name in SCHEDULERS]
|
||||
|
||||
invalid_names = [name for name in names if name not in SCHEDULERS]
|
||||
if invalid_names:
|
||||
logger.warning(f"Invalid scheduler names ignored: {invalid_names}")
|
||||
|
||||
return valid_names
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating scheduler names: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_scheduler_categories() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get schedulers organized by category.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping categories to scheduler names
|
||||
"""
|
||||
categories = {
|
||||
"Standard": ["normal", "karras", "exponential", "simple"],
|
||||
"Uniform": ["sgm_uniform", "ddim_uniform"],
|
||||
"Advanced": ["beta", "linear", "aligned", "ays"],
|
||||
}
|
||||
|
||||
return {
|
||||
category: [s for s in schedulers if s in SCHEDULERS]
|
||||
for category, schedulers in categories.items()
|
||||
}
|
||||
|
||||
|
||||
def get_default_schedulers() -> List[str]:
|
||||
"""
|
||||
Get a list of commonly used default schedulers.
|
||||
|
||||
Returns:
|
||||
List of default scheduler names
|
||||
"""
|
||||
defaults = ["normal", "karras", "exponential", "simple"]
|
||||
return [s for s in defaults if s in SCHEDULERS]
|
||||
|
||||
|
||||
def get_scheduler_description(scheduler_name: str) -> str:
|
||||
"""
|
||||
Get a description of what a scheduler does.
|
||||
|
||||
Args:
|
||||
scheduler_name: Name of the scheduler
|
||||
|
||||
Returns:
|
||||
Description string
|
||||
"""
|
||||
descriptions = {
|
||||
"normal": "Standard linear timestep spacing",
|
||||
"karras": "Karras et al. noise schedule for improved quality",
|
||||
"exponential": "Exponential timestep spacing for smoother transitions",
|
||||
"sgm_uniform": "Stable Diffusion uniform spacing",
|
||||
"simple": "Simple linear schedule for fast sampling",
|
||||
"ddim_uniform": "DDIM-optimized uniform spacing",
|
||||
"beta": "Beta schedule with variance preservation",
|
||||
"linear": "Linear timestep reduction",
|
||||
"aligned": "Aligned schedule for consistent results",
|
||||
"ays": "Align Your Steps schedule",
|
||||
}
|
||||
|
||||
return descriptions.get(scheduler_name, "Custom scheduler")
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Scheduler Select Helper node for ComfyUI."""
|
||||
|
||||
from typing import Tuple
|
||||
from ....base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import process_scheduler_selection, SCHEDULERS
|
||||
|
||||
|
||||
class SchedulerSelectHelperNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Scheduler Select Helper node for multi-scheduler selection.
|
||||
|
||||
Provides checkboxes for each available scheduler and returns a
|
||||
comma-separated string of selected schedulers. Useful for batch
|
||||
processing and XYZ plot generation.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
return {
|
||||
"required": {
|
||||
scheduler: (
|
||||
"BOOLEAN",
|
||||
{"default": False, "tooltip": f"Enable {scheduler} scheduler"},
|
||||
)
|
||||
for scheduler in SCHEDULERS
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_schedulers",)
|
||||
FUNCTION = "select_schedulers"
|
||||
CATEGORY = "ComfyAssets/🧰 xyz-helpers"
|
||||
|
||||
def select_schedulers(self, **scheduler_flags) -> Tuple[str]:
|
||||
"""
|
||||
Process scheduler selections and return comma-separated string.
|
||||
|
||||
Args:
|
||||
**scheduler_flags: Boolean flags for each scheduler
|
||||
|
||||
Returns:
|
||||
Tuple containing comma-separated string of selected schedulers
|
||||
"""
|
||||
try:
|
||||
selected = process_scheduler_selection(**scheduler_flags)
|
||||
|
||||
if selected:
|
||||
self.log_info(f"Selected {len(selected.split(', '))} schedulers")
|
||||
else:
|
||||
self.log_info("No schedulers selected")
|
||||
|
||||
return (selected,)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Error selecting schedulers: {str(e)}", e)
|
||||
return ("",)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Text Encode for Sampler Params module."""
|
||||
|
||||
from .node import TextEncodeSamplerParamsNode
|
||||
|
||||
__all__ = ["TextEncodeSamplerParamsNode"]
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Logic module for Text Encode Sampler Params node."""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def split_prompts(text: str) -> List[str]:
|
||||
"""
|
||||
Split text into multiple prompts using separator patterns.
|
||||
|
||||
Recognizes various separator patterns:
|
||||
- Three or more dashes: ---
|
||||
- Three or more asterisks: ***
|
||||
- Three or more equals: ===
|
||||
- Three or more tildes: ~~~
|
||||
|
||||
Args:
|
||||
text: Multi-line text with separators
|
||||
|
||||
Returns:
|
||||
List of individual prompt strings
|
||||
"""
|
||||
try:
|
||||
normalized = re.sub(r"[-*=~]{3,}\n", "---\n", text)
|
||||
|
||||
parts = normalized.split("---\n")
|
||||
|
||||
prompts = []
|
||||
for part in parts:
|
||||
cleaned = part.strip()
|
||||
if cleaned:
|
||||
prompts.append(cleaned)
|
||||
|
||||
if not prompts and text.strip():
|
||||
prompts = [text.strip()]
|
||||
|
||||
logger.info(f"Split text into {len(prompts)} prompts")
|
||||
return prompts
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error splitting prompts: {e}")
|
||||
if text.strip():
|
||||
return [text.strip()]
|
||||
return []
|
||||
|
||||
|
||||
def encode_prompts(prompts: List[str], clip_encoder) -> List[Any]:
|
||||
"""
|
||||
Encode a list of prompts using CLIP encoder.
|
||||
|
||||
Args:
|
||||
prompts: List of text prompts
|
||||
clip_encoder: CLIP encoder instance
|
||||
|
||||
Returns:
|
||||
List of encoded conditioning tensors
|
||||
"""
|
||||
encoded = []
|
||||
|
||||
try:
|
||||
from nodes import CLIPTextEncode
|
||||
|
||||
encoder = CLIPTextEncode()
|
||||
|
||||
for i, prompt in enumerate(prompts):
|
||||
try:
|
||||
conditioning = encoder.encode(clip_encoder, prompt)[0]
|
||||
encoded.append(conditioning)
|
||||
logger.debug(f"Encoded prompt {i+1}/{len(prompts)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to encode prompt {i+1}: {e}")
|
||||
encoded.append(None)
|
||||
|
||||
encoded = [e for e in encoded if e is not None]
|
||||
|
||||
logger.info(f"Successfully encoded {len(encoded)}/{len(prompts)} prompts")
|
||||
|
||||
except ImportError:
|
||||
logger.error("CLIPTextEncode not available, returning mock encodings")
|
||||
encoded = [{"mock": prompt} for prompt in prompts]
|
||||
except Exception as e:
|
||||
logger.error(f"Error encoding prompts: {e}")
|
||||
|
||||
return encoded
|
||||
|
||||
|
||||
def create_sampler_params_conditioning(
|
||||
prompts: List[str], encoded: List[Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a conditioning dictionary for sampler params.
|
||||
|
||||
Args:
|
||||
prompts: List of original text prompts
|
||||
encoded: List of encoded conditioning tensors
|
||||
|
||||
Returns:
|
||||
Dictionary with text and encoded conditioning
|
||||
"""
|
||||
return {"text": prompts, "encoded": encoded, "count": len(prompts)}
|
||||
|
||||
|
||||
def validate_prompt_format(text: str) -> bool:
|
||||
"""
|
||||
Validate that the prompt text is properly formatted.
|
||||
|
||||
Args:
|
||||
text: Input text to validate
|
||||
|
||||
Returns:
|
||||
True if format is valid
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
logger.warning("Empty prompt text")
|
||||
return False
|
||||
|
||||
if len(text) > 10000:
|
||||
logger.warning(f"Prompt text too long: {len(text)} characters")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_prompt_statistics(prompts: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get statistics about the prompts.
|
||||
|
||||
Args:
|
||||
prompts: List of prompts
|
||||
|
||||
Returns:
|
||||
Dictionary with statistics
|
||||
"""
|
||||
if not prompts:
|
||||
return {
|
||||
"count": 0,
|
||||
"total_chars": 0,
|
||||
"avg_chars": 0,
|
||||
"min_chars": 0,
|
||||
"max_chars": 0,
|
||||
}
|
||||
|
||||
char_counts = [len(p) for p in prompts]
|
||||
|
||||
return {
|
||||
"count": len(prompts),
|
||||
"total_chars": sum(char_counts),
|
||||
"avg_chars": sum(char_counts) // len(char_counts),
|
||||
"min_chars": min(char_counts),
|
||||
"max_chars": max(char_counts),
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Text Encode for Sampler Params node for ComfyUI."""
|
||||
|
||||
from typing import Tuple, Any
|
||||
from ....base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
split_prompts,
|
||||
encode_prompts,
|
||||
create_sampler_params_conditioning,
|
||||
validate_prompt_format,
|
||||
)
|
||||
|
||||
|
||||
class TextEncodeSamplerParamsNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Text Encode for Sampler Params node.
|
||||
|
||||
Splits multi-line text by separators (---, ***, ===, ~~~) and encodes
|
||||
each part separately. Returns a special conditioning format suitable
|
||||
for batch processing and XYZ plot generation.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
return {
|
||||
"required": {
|
||||
"text": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"dynamicPrompts": True,
|
||||
"default": "Separate prompts with at least three dashes\n---\nLike so",
|
||||
"tooltip": "Multi-line text with --- separators between prompts",
|
||||
},
|
||||
),
|
||||
"clip": ("CLIP", {"tooltip": "CLIP model for text encoding"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
RETURN_NAMES = ("conditioning",)
|
||||
FUNCTION = "encode_prompts"
|
||||
CATEGORY = "ComfyAssets/🧰 xyz-helpers"
|
||||
|
||||
def encode_prompts(self, text: str, clip: Any) -> Tuple[Any]:
|
||||
"""
|
||||
Split and encode multiple prompts for batch processing.
|
||||
|
||||
Args:
|
||||
text: Multi-line text with separators
|
||||
clip: CLIP encoder model
|
||||
|
||||
Returns:
|
||||
Tuple containing conditioning dictionary
|
||||
"""
|
||||
try:
|
||||
if not validate_prompt_format(text):
|
||||
self.handle_error("Invalid prompt format")
|
||||
|
||||
prompts = split_prompts(text)
|
||||
|
||||
if not prompts:
|
||||
self.log_info("No prompts found in text")
|
||||
return ({"text": [], "encoded": []},)
|
||||
|
||||
self.log_info(f"Processing {len(prompts)} prompts")
|
||||
|
||||
encoded = encode_prompts(prompts, clip)
|
||||
|
||||
if not encoded:
|
||||
self.handle_error("Failed to encode any prompts")
|
||||
|
||||
conditioning = create_sampler_params_conditioning(prompts, encoded)
|
||||
|
||||
self.log_info(
|
||||
f"Successfully encoded {len(encoded)} prompts "
|
||||
f"(avg {sum(len(p) for p in prompts) // len(prompts)} chars)"
|
||||
)
|
||||
|
||||
return (conditioning,)
|
||||
|
||||
except Exception as e:
|
||||
self.handle_error(f"Error processing prompts: {str(e)}", e)
|
||||
return ({"text": [], "encoded": []},)
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "kikotools"
|
||||
description = "Simple tools for ComfyUI"
|
||||
version = "1.0.7"
|
||||
version = "1.0.10"
|
||||
license = {text = "MIT"}
|
||||
dependencies = []
|
||||
|
||||
|
||||
@@ -102,12 +102,18 @@ def assert_divisible_by_8(width: int, height: int) -> None:
|
||||
assert height % 8 == 0, f"Height {height} must be divisible by 8"
|
||||
|
||||
|
||||
def assert_reasonable_dimensions(width: int, height: int, min_size: int = 64, max_size: int = 8192) -> None:
|
||||
def assert_reasonable_dimensions(
|
||||
width: int, height: int, min_size: int = 64, max_size: int = 8192
|
||||
) -> None:
|
||||
"""
|
||||
Helper function to assert dimensions are within reasonable bounds
|
||||
"""
|
||||
assert min_size <= width <= max_size, f"Width {width} out of reasonable range [{min_size}, {max_size}]"
|
||||
assert min_size <= height <= max_size, f"Height {height} out of reasonable range [{min_size}, {max_size}]"
|
||||
assert (
|
||||
min_size <= width <= max_size
|
||||
), f"Width {width} out of reasonable range [{min_size}, {max_size}]"
|
||||
assert (
|
||||
min_size <= height <= max_size
|
||||
), f"Height {height} out of reasonable range [{min_size}, {max_size}]"
|
||||
|
||||
|
||||
# Make helper functions available as pytest fixtures
|
||||
|
||||
@@ -32,7 +32,10 @@ class TestComfyAssetsBaseNode:
|
||||
node.handle_error("Test error message")
|
||||
|
||||
mock_logger.error.assert_called_once()
|
||||
assert "ComfyAssetsBaseNode: Test error message" in mock_logger.error.call_args[0][0]
|
||||
assert (
|
||||
"ComfyAssetsBaseNode: Test error message"
|
||||
in mock_logger.error.call_args[0][0]
|
||||
)
|
||||
|
||||
def test_handle_error_with_exception_logs_exception(self):
|
||||
"""Test error handling with original exception logs both messages"""
|
||||
@@ -55,7 +58,10 @@ class TestComfyAssetsBaseNode:
|
||||
node.log_info("Test information")
|
||||
|
||||
mock_logger.info.assert_called_once()
|
||||
assert "ComfyAssetsBaseNode: Test information" in mock_logger.info.call_args[0][0]
|
||||
assert (
|
||||
"ComfyAssetsBaseNode: Test information"
|
||||
in mock_logger.info.call_args[0][0]
|
||||
)
|
||||
|
||||
def test_get_node_info_returns_metadata(self):
|
||||
"""Test get_node_info returns correct metadata"""
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
"""Unit tests for DisplayAny node."""
|
||||
|
||||
import json
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from kikotools.tools.display_any import DisplayAnyNode
|
||||
from kikotools.tools.display_any.logic import (
|
||||
format_display_value,
|
||||
get_tensor_shapes,
|
||||
validate_display_mode,
|
||||
)
|
||||
from kikotools.tools.display_any.node import AnyType
|
||||
|
||||
|
||||
class TestAnyType:
|
||||
"""Test cases for AnyType class."""
|
||||
|
||||
def test_anytype_not_equal(self):
|
||||
"""Test that AnyType is never equal to other types."""
|
||||
any_type = AnyType("*")
|
||||
|
||||
# Should not be equal to any other type
|
||||
assert not (any_type != "STRING")
|
||||
assert not (any_type != "IMAGE")
|
||||
assert not (any_type != "LATENT")
|
||||
assert not (any_type != 123)
|
||||
assert not (any_type != None)
|
||||
assert not (any_type != ["LIST"])
|
||||
|
||||
def test_anytype_string_representation(self):
|
||||
"""Test string representation of AnyType."""
|
||||
any_type = AnyType("*")
|
||||
assert str(any_type) == "*"
|
||||
|
||||
|
||||
class TestDisplayAnyNode:
|
||||
"""Test cases for DisplayAnyNode."""
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node has correct properties."""
|
||||
assert DisplayAnyNode.CATEGORY == "ComfyAssets/👁️ Display"
|
||||
assert DisplayAnyNode.FUNCTION == "display"
|
||||
assert DisplayAnyNode.RETURN_TYPES == ("STRING",)
|
||||
assert DisplayAnyNode.RETURN_NAMES == ("display_text",)
|
||||
assert DisplayAnyNode.OUTPUT_NODE is True
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test INPUT_TYPES configuration."""
|
||||
input_types = DisplayAnyNode.INPUT_TYPES()
|
||||
|
||||
# Check required inputs
|
||||
assert "required" in input_types
|
||||
assert "input" in input_types["required"]
|
||||
# Check that input is AnyType with wildcard
|
||||
input_type = input_types["required"]["input"]
|
||||
assert len(input_type) == 2
|
||||
assert isinstance(input_type[0], AnyType)
|
||||
assert str(input_type[0]) == "*"
|
||||
assert input_type[1] == {}
|
||||
assert "mode" in input_types["required"]
|
||||
assert input_types["required"]["mode"] == (["raw value", "tensor shape"],)
|
||||
|
||||
def test_validate_inputs(self):
|
||||
"""Test VALIDATE_INPUTS always returns True."""
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS() is True
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS(input="test") is True
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS(input=123, mode="raw value") is True
|
||||
|
||||
def test_display_raw_value_string(self):
|
||||
"""Test displaying raw string value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("Hello, World!", "raw value")
|
||||
|
||||
assert "ui" in result
|
||||
assert "text" in result["ui"]
|
||||
assert result["ui"]["text"] == ["Hello, World!"]
|
||||
assert "result" in result
|
||||
assert result["result"] == ("Hello, World!",)
|
||||
|
||||
def test_display_raw_value_number(self):
|
||||
"""Test displaying raw number value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(42, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == ["42"]
|
||||
assert result["result"] == ("42",)
|
||||
|
||||
def test_display_raw_value_list(self):
|
||||
"""Test displaying raw list value."""
|
||||
node = DisplayAnyNode()
|
||||
test_list = [1, 2, 3, "test"]
|
||||
result = node.display(test_list, "raw value")
|
||||
|
||||
expected_text = json.dumps(test_list, indent=2)
|
||||
assert result["ui"]["text"] == [expected_text]
|
||||
assert result["result"][0] == json.dumps(test_list, indent=2)
|
||||
|
||||
def test_display_raw_value_dict(self):
|
||||
"""Test displaying raw dictionary value."""
|
||||
node = DisplayAnyNode()
|
||||
test_dict = {"key": "value", "number": 123}
|
||||
result = node.display(test_dict, "raw value")
|
||||
|
||||
expected_text = json.dumps(test_dict, indent=2)
|
||||
assert result["ui"]["text"] == [expected_text]
|
||||
assert result["result"][0] == json.dumps(test_dict, indent=2)
|
||||
|
||||
def test_display_tensor_shape_numpy(self):
|
||||
"""Test displaying numpy tensor shape."""
|
||||
node = DisplayAnyNode()
|
||||
tensor = np.random.rand(4, 3, 224, 224)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == ["[[4, 3, 224, 224]]"]
|
||||
assert result["result"] == ("[[4, 3, 224, 224]]",)
|
||||
|
||||
@pytest.mark.skipif(not torch, reason="PyTorch not installed")
|
||||
def test_display_tensor_shape_torch(self):
|
||||
"""Test displaying PyTorch tensor shape."""
|
||||
node = DisplayAnyNode()
|
||||
tensor = torch.randn(2, 10, 512, 512)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == ["[[2, 10, 512, 512]]"]
|
||||
assert result["result"] == ("[[2, 10, 512, 512]]",)
|
||||
|
||||
def test_display_nested_tensors(self):
|
||||
"""Test displaying shapes from nested structure with tensors."""
|
||||
node = DisplayAnyNode()
|
||||
nested_data = {
|
||||
"images": np.random.rand(1, 3, 256, 256),
|
||||
"masks": [
|
||||
np.random.rand(256, 256),
|
||||
np.random.rand(256, 256, 1),
|
||||
],
|
||||
"metadata": {"info": "test", "tensor": np.random.rand(10)},
|
||||
}
|
||||
result = node.display(nested_data, "tensor shape")
|
||||
|
||||
expected = "[[1, 3, 256, 256], [256, 256], [256, 256, 1], [10]]"
|
||||
assert result["ui"]["text"] == [expected]
|
||||
assert result["result"] == (expected,)
|
||||
|
||||
def test_display_no_tensors(self):
|
||||
"""Test displaying when no tensors are present."""
|
||||
node = DisplayAnyNode()
|
||||
data = {"text": "hello", "number": 42, "list": [1, 2, 3]}
|
||||
result = node.display(data, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == ["No tensors found in input"]
|
||||
assert result["result"] == ("No tensors found in input",)
|
||||
|
||||
def test_invalid_mode_defaults_to_raw(self):
|
||||
"""Test that invalid mode defaults to raw value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("test", "invalid_mode")
|
||||
|
||||
assert result["ui"]["text"] == ["test"]
|
||||
assert result["result"] == ("test",)
|
||||
|
||||
|
||||
class TestDisplayAnyLogic:
|
||||
"""Test cases for DisplayAny logic functions."""
|
||||
|
||||
def test_get_tensor_shapes_single(self):
|
||||
"""Test getting shape from single tensor."""
|
||||
tensor = np.random.rand(3, 224, 224)
|
||||
shapes = get_tensor_shapes(tensor)
|
||||
|
||||
assert len(shapes) == 1
|
||||
assert shapes[0] == [3, 224, 224]
|
||||
|
||||
def test_get_tensor_shapes_nested_dict(self):
|
||||
"""Test getting shapes from nested dictionary."""
|
||||
data = {
|
||||
"level1": {
|
||||
"tensor1": np.random.rand(10, 20),
|
||||
"level2": {"tensor2": np.random.rand(5, 5, 5)},
|
||||
}
|
||||
}
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 2
|
||||
assert [10, 20] in shapes
|
||||
assert [5, 5, 5] in shapes
|
||||
|
||||
def test_get_tensor_shapes_nested_list(self):
|
||||
"""Test getting shapes from nested list."""
|
||||
data = [
|
||||
np.random.rand(1, 2, 3),
|
||||
[np.random.rand(4, 5), np.random.rand(6, 7, 8)],
|
||||
"not a tensor",
|
||||
]
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 3
|
||||
assert [1, 2, 3] in shapes
|
||||
assert [4, 5] in shapes
|
||||
assert [6, 7, 8] in shapes
|
||||
|
||||
def test_get_tensor_shapes_tuple(self):
|
||||
"""Test getting shapes from tuple."""
|
||||
data = (np.random.rand(2, 2), np.random.rand(3, 3))
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 2
|
||||
assert [2, 2] in shapes
|
||||
assert [3, 3] in shapes
|
||||
|
||||
def test_format_display_value_raw(self):
|
||||
"""Test formatting for raw value display."""
|
||||
result = format_display_value({"key": "value"}, "raw value")
|
||||
# Now returns JSON formatted string for dicts
|
||||
expected = json.dumps({"key": "value"}, indent=2)
|
||||
assert result == expected
|
||||
|
||||
def test_format_display_value_tensor_shape(self):
|
||||
"""Test formatting for tensor shape display."""
|
||||
tensor = np.random.rand(10, 10)
|
||||
result = format_display_value(tensor, "tensor shape")
|
||||
assert result == "[[10, 10]]"
|
||||
|
||||
def test_format_display_value_no_tensors(self):
|
||||
"""Test formatting when no tensors present."""
|
||||
result = format_display_value("just a string", "tensor shape")
|
||||
assert result == "No tensors found in input"
|
||||
|
||||
def test_validate_display_mode(self):
|
||||
"""Test display mode validation."""
|
||||
assert validate_display_mode("raw value") is True
|
||||
assert validate_display_mode("tensor shape") is True
|
||||
assert validate_display_mode("invalid") is False
|
||||
assert validate_display_mode("") is False
|
||||
assert validate_display_mode(None) is False
|
||||
|
||||
|
||||
class TestDisplayAnyEdgeCases:
|
||||
"""Test edge cases for DisplayAny."""
|
||||
|
||||
def test_display_none(self):
|
||||
"""Test displaying None value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(None, "raw value")
|
||||
assert result["ui"]["text"] == ["None"]
|
||||
|
||||
def test_display_empty_list(self):
|
||||
"""Test displaying empty list."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display([], "raw value")
|
||||
assert result["ui"]["text"] == ["[]"]
|
||||
|
||||
def test_display_empty_dict(self):
|
||||
"""Test displaying empty dictionary."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display({}, "raw value")
|
||||
assert result["ui"]["text"] == ["{}"]
|
||||
|
||||
def test_display_complex_nested_structure(self):
|
||||
"""Test displaying complex nested structure."""
|
||||
node = DisplayAnyNode()
|
||||
complex_data = {
|
||||
"images": [np.random.rand(1, 3, 64, 64) for _ in range(3)],
|
||||
"config": {
|
||||
"steps": 20,
|
||||
"cfg": 7.5,
|
||||
"sampler": "euler",
|
||||
"latents": np.random.rand(1, 4, 32, 32),
|
||||
},
|
||||
"prompts": ["test1", "test2"],
|
||||
}
|
||||
result = node.display(complex_data, "tensor shape")
|
||||
|
||||
# Should find 4 tensors total (3 images + 1 latent)
|
||||
shapes_text = result["ui"]["text"][0] # Get first element of array
|
||||
assert "[1, 3, 64, 64]" in shapes_text
|
||||
assert "[1, 4, 32, 32]" in shapes_text
|
||||
|
||||
def test_display_very_long_string(self):
|
||||
"""Test displaying very long string."""
|
||||
node = DisplayAnyNode()
|
||||
long_string = "x" * 10000
|
||||
result = node.display(long_string, "raw value")
|
||||
assert result["ui"]["text"] == [long_string]
|
||||
|
||||
def test_display_unicode(self):
|
||||
"""Test displaying unicode characters."""
|
||||
node = DisplayAnyNode()
|
||||
unicode_text = "Hello 世界 🌍"
|
||||
result = node.display(unicode_text, "raw value")
|
||||
assert result["ui"]["text"] == [unicode_text]
|
||||
@@ -83,8 +83,8 @@ class TestEmptyLatentBatchLogic:
|
||||
def test_sanitize_dimensions_not_divisible_by_8(self):
|
||||
"""Test sanitization of dimensions not divisible by 8."""
|
||||
width, height = sanitize_dimensions(513, 515)
|
||||
assert width == 512 # Rounds down to nearest multiple of 8
|
||||
assert height == 512
|
||||
assert width == 520 # Rounds up to nearest multiple of 8
|
||||
assert height == 520
|
||||
|
||||
width, height = sanitize_dimensions(517, 519)
|
||||
assert width == 520 # Rounds up to nearest multiple of 8
|
||||
@@ -131,21 +131,23 @@ class TestEmptyLatentBatchNode:
|
||||
|
||||
def test_node_attributes(self):
|
||||
"""Test node class attributes."""
|
||||
assert EmptyLatentBatchNode.RETURN_TYPES == ("LATENT",)
|
||||
assert EmptyLatentBatchNode.RETURN_NAMES == ("latent",)
|
||||
assert EmptyLatentBatchNode.RETURN_TYPES == ("LATENT", "INT", "INT")
|
||||
assert EmptyLatentBatchNode.RETURN_NAMES == ("latent", "width", "height")
|
||||
assert EmptyLatentBatchNode.FUNCTION == "create_empty_latent"
|
||||
assert EmptyLatentBatchNode.CATEGORY == "ComfyAssets"
|
||||
assert EmptyLatentBatchNode.CATEGORY == "ComfyAssets/📦 Latents"
|
||||
|
||||
def test_create_empty_latent_basic(self):
|
||||
"""Test basic empty latent creation through node."""
|
||||
result = self.node.create_empty_latent(512, 512, 1)
|
||||
result = self.node.create_empty_latent("custom", 512, 512, 1)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
assert len(result) == 3 # Now returns (latent, width, height)
|
||||
|
||||
latent_dict = result[0]
|
||||
latent_dict, width, height = result
|
||||
assert isinstance(latent_dict, dict)
|
||||
assert "samples" in latent_dict
|
||||
assert width == 512
|
||||
assert height == 512
|
||||
|
||||
samples = latent_dict["samples"]
|
||||
assert isinstance(samples, torch.Tensor)
|
||||
@@ -154,31 +156,36 @@ class TestEmptyLatentBatchNode:
|
||||
def test_create_empty_latent_with_batch(self):
|
||||
"""Test empty latent creation with batch size."""
|
||||
batch_size = 3
|
||||
result = self.node.create_empty_latent(1024, 768, batch_size)
|
||||
result = self.node.create_empty_latent("custom", 1024, 768, batch_size)
|
||||
|
||||
latent_dict = result[0]
|
||||
latent_dict, width, height = result
|
||||
assert width == 1024
|
||||
assert height == 768
|
||||
samples = latent_dict["samples"]
|
||||
assert samples.shape == (3, 4, 96, 128) # batch=3, 768/8=96, 1024/8=128
|
||||
|
||||
def test_create_empty_latent_dimension_adjustment(self):
|
||||
"""Test that dimensions are adjusted when not divisible by 8."""
|
||||
# Input dimensions not divisible by 8
|
||||
result = self.node.create_empty_latent(513, 515, 1)
|
||||
result = self.node.create_empty_latent("custom", 513, 515, 1)
|
||||
|
||||
latent_dict = result[0]
|
||||
latent_dict, width, height = result
|
||||
# Dimensions should be rounded UP to nearest multiple of 8
|
||||
assert width == 520 # 513 -> 520
|
||||
assert height == 520 # 515 -> 520
|
||||
samples = latent_dict["samples"]
|
||||
# Should be adjusted to 512x512 -> 64x64 latent
|
||||
assert samples.shape == (1, 4, 64, 64)
|
||||
# Should be adjusted to 520x520 -> 65x65 latent
|
||||
assert samples.shape == (1, 4, 65, 65)
|
||||
|
||||
def test_validate_inputs_valid(self):
|
||||
"""Test input validation with valid parameters."""
|
||||
assert self.node.validate_inputs(512, 512, 1) is True
|
||||
assert self.node.validate_inputs(1024, 768, 4) is True
|
||||
assert self.node.validate_inputs("custom", 512, 512, 1) is True
|
||||
assert self.node.validate_inputs("custom", 1024, 768, 4) is True
|
||||
|
||||
def test_validate_inputs_invalid_batch_size(self):
|
||||
"""Test input validation with invalid batch size."""
|
||||
assert self.node.validate_inputs(512, 512, 0) is False
|
||||
assert self.node.validate_inputs(512, 512, 100) is False # Too large
|
||||
assert self.node.validate_inputs("custom", 512, 512, 0) is False
|
||||
assert self.node.validate_inputs("custom", 512, 512, 100) is False # Too large
|
||||
|
||||
def test_get_latent_info(self):
|
||||
"""Test latent info generation."""
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
"""Unit tests for Gemini Prompt Engineer node."""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import numpy as np
|
||||
from unittest.mock import patch, MagicMock
|
||||
from PIL import Image
|
||||
|
||||
from kikotools.tools.gemini_prompt import GeminiPromptNode
|
||||
from kikotools.tools.gemini_prompt.logic import (
|
||||
tensor_to_pil,
|
||||
image_to_base64,
|
||||
get_api_key,
|
||||
validate_prompt_type,
|
||||
analyze_image_with_gemini,
|
||||
)
|
||||
from kikotools.tools.gemini_prompt.prompts import (
|
||||
PROMPT_OPTIONS,
|
||||
PROMPT_TEMPLATES,
|
||||
DEFAULT_GEMINI_MODELS,
|
||||
)
|
||||
|
||||
|
||||
class TestGeminiPromptNode:
|
||||
"""Test cases for GeminiPromptNode."""
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node has correct properties."""
|
||||
assert GeminiPromptNode.CATEGORY == "ComfyAssets/🧠 Prompts"
|
||||
assert GeminiPromptNode.FUNCTION == "generate_prompt"
|
||||
assert GeminiPromptNode.RETURN_TYPES == ("STRING", "STRING")
|
||||
assert GeminiPromptNode.RETURN_NAMES == ("prompt", "negative_prompt")
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test INPUT_TYPES configuration."""
|
||||
input_types = GeminiPromptNode.INPUT_TYPES()
|
||||
|
||||
# Check required inputs
|
||||
assert "required" in input_types
|
||||
assert "image" in input_types["required"]
|
||||
assert input_types["required"]["image"] == ("IMAGE",)
|
||||
assert "prompt_type" in input_types["required"]
|
||||
assert input_types["required"]["prompt_type"][0] == PROMPT_OPTIONS
|
||||
assert "model" in input_types["required"]
|
||||
# Check that model is a list (can be dynamic from API or DEFAULT_GEMINI_MODELS)
|
||||
model_list = input_types["required"]["model"][0]
|
||||
assert isinstance(model_list, list)
|
||||
assert len(model_list) > 0 # Should have at least one model
|
||||
|
||||
# Check optional inputs
|
||||
assert "optional" in input_types
|
||||
assert "api_key" in input_types["optional"]
|
||||
assert "custom_prompt" in input_types["optional"]
|
||||
|
||||
def test_default_gemini_models_structure(self):
|
||||
"""Test that DEFAULT_GEMINI_MODELS has proper structure."""
|
||||
assert isinstance(DEFAULT_GEMINI_MODELS, list)
|
||||
assert len(DEFAULT_GEMINI_MODELS) > 0
|
||||
# Check at least some expected models are in the defaults
|
||||
assert any("gemini" in model.lower() for model in DEFAULT_GEMINI_MODELS)
|
||||
|
||||
@patch("kikotools.tools.gemini_prompt.node.analyze_image_with_gemini")
|
||||
def test_generate_prompt_success(self, mock_analyze):
|
||||
"""Test successful prompt generation."""
|
||||
# Setup
|
||||
node = GeminiPromptNode()
|
||||
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
||||
mock_analyze.return_value = ("A beautiful landscape with mountains", None)
|
||||
|
||||
# Execute
|
||||
result = node.generate_prompt(test_image, "flux", "gemini-2.5-flash")
|
||||
|
||||
# Assert
|
||||
assert result == ("A beautiful landscape with mountains", "")
|
||||
mock_analyze.assert_called_once()
|
||||
|
||||
@patch("kikotools.tools.gemini_prompt.node.analyze_image_with_gemini")
|
||||
def test_generate_prompt_sdxl_format(self, mock_analyze):
|
||||
"""Test SDXL format with positive and negative prompts."""
|
||||
# Setup
|
||||
node = GeminiPromptNode()
|
||||
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
||||
mock_analyze.return_value = (
|
||||
"Positive: beautiful landscape, mountains, sunset\nNegative: blurry, low quality",
|
||||
None,
|
||||
)
|
||||
|
||||
# Execute
|
||||
result = node.generate_prompt(test_image, "sdxl", "gemini-2.5-flash")
|
||||
|
||||
# Assert
|
||||
assert result == (
|
||||
"beautiful landscape, mountains, sunset",
|
||||
"blurry, low quality",
|
||||
)
|
||||
|
||||
@patch("kikotools.tools.gemini_prompt.node.analyze_image_with_gemini")
|
||||
def test_generate_prompt_error(self, mock_analyze):
|
||||
"""Test error handling in prompt generation."""
|
||||
# Setup
|
||||
node = GeminiPromptNode()
|
||||
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
||||
mock_analyze.return_value = ("", "API key not found")
|
||||
|
||||
# Execute
|
||||
result = node.generate_prompt(test_image, "flux", "gemini-2.5-flash")
|
||||
|
||||
# Assert
|
||||
assert result[0].startswith("Error:")
|
||||
assert result[1] == ""
|
||||
|
||||
def test_invalid_prompt_type(self):
|
||||
"""Test handling of invalid prompt type."""
|
||||
node = GeminiPromptNode()
|
||||
test_image = np.random.rand(1, 512, 512, 3).astype(np.float32)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid prompt type"):
|
||||
node.generate_prompt(test_image, "invalid_type", "gemini-2.5-flash")
|
||||
|
||||
|
||||
class TestGeminiLogic:
|
||||
"""Test cases for Gemini logic functions."""
|
||||
|
||||
def test_tensor_to_pil(self):
|
||||
"""Test tensor to PIL conversion."""
|
||||
# Test 4D tensor
|
||||
tensor_4d = np.random.rand(1, 64, 64, 3)
|
||||
result = tensor_to_pil(tensor_4d)
|
||||
assert isinstance(result, Image.Image)
|
||||
assert result.size == (64, 64)
|
||||
assert result.mode == "RGB"
|
||||
|
||||
# Test 3D tensor
|
||||
tensor_3d = np.random.rand(64, 64, 3)
|
||||
result = tensor_to_pil(tensor_3d)
|
||||
assert isinstance(result, Image.Image)
|
||||
assert result.size == (64, 64)
|
||||
|
||||
def test_image_to_base64(self):
|
||||
"""Test image to base64 conversion."""
|
||||
# Create test image
|
||||
image = Image.new("RGB", (64, 64), color="red")
|
||||
|
||||
# Convert to base64
|
||||
result = image_to_base64(image)
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
# Test JPEG format
|
||||
result_jpeg = image_to_base64(image, format="JPEG")
|
||||
assert isinstance(result_jpeg, str)
|
||||
assert (
|
||||
result != result_jpeg
|
||||
) # Different formats should produce different results
|
||||
|
||||
@patch.dict("os.environ", {"GEMINI_API_KEY": "test_key_123"})
|
||||
def test_get_api_key_from_env(self):
|
||||
"""Test getting API key from environment."""
|
||||
result = get_api_key()
|
||||
assert result == "test_key_123"
|
||||
|
||||
@patch.dict("os.environ", {}, clear=True)
|
||||
@patch("os.path.exists")
|
||||
@patch("builtins.open")
|
||||
def test_get_api_key_from_config(self, mock_open, mock_exists):
|
||||
"""Test getting API key from config file."""
|
||||
# Setup
|
||||
mock_exists.return_value = True
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = (
|
||||
'{"api_key": "config_key_456"}'
|
||||
)
|
||||
|
||||
# Execute
|
||||
result = get_api_key()
|
||||
|
||||
# Assert
|
||||
assert result == "config_key_456"
|
||||
|
||||
def test_validate_prompt_type(self):
|
||||
"""Test prompt type validation."""
|
||||
# Valid types
|
||||
for prompt_type in PROMPT_OPTIONS:
|
||||
assert validate_prompt_type(prompt_type) is True
|
||||
|
||||
# Invalid types
|
||||
assert validate_prompt_type("invalid") is False
|
||||
assert validate_prompt_type("") is False
|
||||
assert validate_prompt_type(None) is False
|
||||
|
||||
@pytest.mark.skip(reason="Requires google-generativeai library")
|
||||
def test_analyze_image_with_gemini_success(self):
|
||||
"""Test successful image analysis with Gemini."""
|
||||
pass # Skipped as it requires google-generativeai
|
||||
|
||||
def test_analyze_image_no_api_key(self):
|
||||
"""Test analysis without API key."""
|
||||
test_image = np.random.rand(64, 64, 3)
|
||||
|
||||
with patch(
|
||||
"kikotools.tools.gemini_prompt.logic.get_api_key", return_value=None
|
||||
):
|
||||
result, error = analyze_image_with_gemini(test_image, "flux")
|
||||
|
||||
assert result == ""
|
||||
assert "API key not found" in error
|
||||
|
||||
@pytest.mark.skip(reason="Requires google-generativeai library")
|
||||
def test_analyze_image_with_custom_prompt(self):
|
||||
"""Test analysis with custom prompt."""
|
||||
pass # Skipped as it requires google-generativeai
|
||||
|
||||
|
||||
class TestPromptTemplates:
|
||||
"""Test prompt template configurations."""
|
||||
|
||||
def test_all_prompt_types_have_templates(self):
|
||||
"""Test that all prompt options have corresponding templates."""
|
||||
for prompt_type in PROMPT_OPTIONS:
|
||||
assert prompt_type in PROMPT_TEMPLATES
|
||||
assert isinstance(PROMPT_TEMPLATES[prompt_type], str)
|
||||
assert len(PROMPT_TEMPLATES[prompt_type]) > 0
|
||||
|
||||
def test_prompt_template_content(self):
|
||||
"""Test that prompt templates contain expected content."""
|
||||
# FLUX prompt should mention FLUX
|
||||
assert "FLUX" in PROMPT_TEMPLATES["flux"]
|
||||
|
||||
# SDXL prompt should mention positive and negative
|
||||
assert "Positive" in PROMPT_TEMPLATES["sdxl"]
|
||||
assert "Negative" in PROMPT_TEMPLATES["sdxl"]
|
||||
|
||||
# Danbooru should mention tags and underscores
|
||||
assert "tag" in PROMPT_TEMPLATES["danbooru"].lower()
|
||||
assert "underscore" in PROMPT_TEMPLATES["danbooru"].lower()
|
||||
|
||||
# Video should mention movement or motion and dynamics
|
||||
assert (
|
||||
"movement" in PROMPT_TEMPLATES["video"].lower()
|
||||
or "motion" in PROMPT_TEMPLATES["video"].lower()
|
||||
)
|
||||
assert (
|
||||
"dynamic" in PROMPT_TEMPLATES["video"].lower()
|
||||
) # Check for dynamics instead of temporal
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Unit tests for ImageScaleDownBy tool."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from kikotools.tools.image_scale_down_by.logic import scale_down_image
|
||||
from kikotools.tools.image_scale_down_by.node import ImageScaleDownByNode
|
||||
|
||||
|
||||
class TestImageScaleDownByLogic:
|
||||
"""Test the core logic for scaling down images."""
|
||||
|
||||
def test_scale_down_by_half(self):
|
||||
"""Test scaling down an image by 0.5."""
|
||||
# Create a test image (batch=1, height=512, width=512, channels=3)
|
||||
image = torch.randn(1, 512, 512, 3)
|
||||
scale_by = 0.5
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == (1, 256, 256, 3)
|
||||
|
||||
def test_scale_down_by_quarter(self):
|
||||
"""Test scaling down an image by 0.25."""
|
||||
image = torch.randn(1, 1024, 768, 3)
|
||||
scale_by = 0.25
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == (1, 256, 192, 3)
|
||||
|
||||
def test_scale_down_by_custom_factor(self):
|
||||
"""Test scaling down by a custom factor."""
|
||||
image = torch.randn(1, 800, 600, 3)
|
||||
scale_by = 0.75
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == (1, 600, 450, 3)
|
||||
|
||||
def test_scale_down_maintains_batch_size(self):
|
||||
"""Test that batch size is maintained."""
|
||||
# Test with batch size > 1
|
||||
image = torch.randn(4, 512, 512, 3)
|
||||
scale_by = 0.5
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == (4, 256, 256, 3)
|
||||
|
||||
def test_scale_by_one_returns_same_size(self):
|
||||
"""Test that scale_by=1.0 returns the same size."""
|
||||
image = torch.randn(1, 512, 512, 3)
|
||||
scale_by = 1.0
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == image.shape
|
||||
|
||||
def test_non_square_image(self):
|
||||
"""Test scaling non-square images."""
|
||||
image = torch.randn(1, 720, 1280, 3)
|
||||
scale_by = 0.5
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == (1, 360, 640, 3)
|
||||
|
||||
def test_small_scale_factor(self):
|
||||
"""Test with very small scale factor."""
|
||||
image = torch.randn(1, 1000, 1000, 3)
|
||||
scale_by = 0.01
|
||||
|
||||
result = scale_down_image(image, scale_by)
|
||||
|
||||
assert result.shape == (1, 10, 10, 3)
|
||||
|
||||
|
||||
class TestImageScaleDownByNode:
|
||||
"""Test the ComfyUI node implementation."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return ImageScaleDownByNode()
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES is properly defined."""
|
||||
input_types = ImageScaleDownByNode.INPUT_TYPES()
|
||||
|
||||
assert "required" in input_types
|
||||
assert "images" in input_types["required"]
|
||||
assert input_types["required"]["images"] == ("IMAGE",)
|
||||
assert "scale_by" in input_types["required"]
|
||||
|
||||
# Check scale_by configuration
|
||||
scale_config = input_types["required"]["scale_by"]
|
||||
assert scale_config[0] == "FLOAT"
|
||||
assert scale_config[1]["default"] == 0.5
|
||||
assert scale_config[1]["min"] == 0.01
|
||||
assert scale_config[1]["max"] == 1.0
|
||||
assert scale_config[1]["step"] == 0.01
|
||||
|
||||
def test_return_types(self):
|
||||
"""Test that return types are properly defined."""
|
||||
assert ImageScaleDownByNode.RETURN_TYPES == ("IMAGE",)
|
||||
assert ImageScaleDownByNode.RETURN_NAMES == ("images",)
|
||||
assert ImageScaleDownByNode.FUNCTION == "scale_down"
|
||||
|
||||
def test_scale_down_execution(self, node):
|
||||
"""Test the scale_down method."""
|
||||
images = torch.randn(1, 512, 512, 3)
|
||||
scale_by = 0.5
|
||||
|
||||
result = node.scale_down(images, scale_by)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
assert result[0].shape == (1, 256, 256, 3)
|
||||
|
||||
def test_input_validation_no_images(self, node):
|
||||
"""Test validation with missing images."""
|
||||
with pytest.raises(ValueError, match="Images input is required"):
|
||||
node.validate_inputs(images=None, scale_by=0.5)
|
||||
|
||||
def test_input_validation_invalid_tensor_shape(self, node):
|
||||
"""Test validation with invalid tensor shape."""
|
||||
invalid_image = torch.randn(512, 512, 3) # Missing batch dimension
|
||||
|
||||
with pytest.raises(ValueError, match="Expected image tensor with shape"):
|
||||
node.validate_inputs(images=invalid_image, scale_by=0.5)
|
||||
|
||||
def test_input_validation_scale_too_small(self, node):
|
||||
"""Test validation with scale_by too small."""
|
||||
images = torch.randn(1, 512, 512, 3)
|
||||
|
||||
with pytest.raises(ValueError, match="scale_by must be between"):
|
||||
node.validate_inputs(images=images, scale_by=0.0)
|
||||
|
||||
def test_input_validation_scale_too_large(self, node):
|
||||
"""Test validation with scale_by too large."""
|
||||
images = torch.randn(1, 512, 512, 3)
|
||||
|
||||
with pytest.raises(ValueError, match="scale_by must be between"):
|
||||
node.validate_inputs(images=images, scale_by=1.5)
|
||||
|
||||
def test_category_is_comfyassets(self):
|
||||
"""Test that the node is in the ComfyAssets category."""
|
||||
assert ImageScaleDownByNode.CATEGORY == "ComfyAssets/🖼️ Resolution"
|
||||
|
||||
def test_scale_down_with_batch(self, node):
|
||||
"""Test scaling down with batch of images."""
|
||||
images = torch.randn(3, 640, 480, 3)
|
||||
scale_by = 0.25
|
||||
|
||||
result = node.scale_down(images, scale_by)
|
||||
|
||||
assert result[0].shape == (3, 160, 120, 3)
|
||||
|
||||
def test_error_handling(self, node):
|
||||
"""Test that errors are properly handled."""
|
||||
from unittest.mock import patch
|
||||
|
||||
# Mock the scale_down_image function to raise an exception
|
||||
with patch(
|
||||
"kikotools.tools.image_scale_down_by.node.scale_down_image",
|
||||
side_effect=RuntimeError("Test error"),
|
||||
):
|
||||
images = torch.randn(1, 512, 512, 3)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to scale down images"):
|
||||
node.scale_down(images, 0.5)
|
||||
@@ -118,7 +118,7 @@ class TestImageToMultipleOfNode:
|
||||
assert ImageToMultipleOfNode.RETURN_TYPES == ("IMAGE",)
|
||||
assert ImageToMultipleOfNode.RETURN_NAMES == ("image",)
|
||||
assert ImageToMultipleOfNode.FUNCTION == "process"
|
||||
assert ImageToMultipleOfNode.CATEGORY == "ComfyAssets"
|
||||
assert ImageToMultipleOfNode.CATEGORY == "ComfyAssets/🖼️ Resolution"
|
||||
|
||||
def test_node_process_center_crop(self):
|
||||
"""Test node processing with center crop."""
|
||||
|
||||
@@ -52,14 +52,18 @@ class TestKikoSaveImageLogic:
|
||||
"""Test save path generation"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Test basic path generation
|
||||
full_path, filename = get_save_image_path("test_prefix", 0, ".png", temp_dir)
|
||||
full_path, filename, subfolder = get_save_image_path(
|
||||
"test_prefix", 0, ".png", temp_dir
|
||||
)
|
||||
|
||||
assert full_path.startswith(temp_dir)
|
||||
assert filename.startswith("test_prefix_")
|
||||
assert filename.endswith("_00000.png")
|
||||
|
||||
# Test with empty subfolder (standard behavior)
|
||||
full_path, filename = get_save_image_path("test", 1, ".jpg", temp_dir, "")
|
||||
full_path, filename, subfolder = get_save_image_path(
|
||||
"test", 1, ".jpg", temp_dir, ""
|
||||
)
|
||||
|
||||
assert full_path.startswith(temp_dir)
|
||||
assert filename.startswith("test_")
|
||||
@@ -76,8 +80,10 @@ class TestKikoSaveImageLogic:
|
||||
metadata = create_png_metadata(prompt=prompt_data)
|
||||
|
||||
assert metadata is not None
|
||||
# Check that metadata contains our data (implementation detail)
|
||||
assert hasattr(metadata, "text")
|
||||
# Check that metadata is a PngInfo object
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
assert isinstance(metadata, PngInfo)
|
||||
|
||||
@patch("kikotools.tools.kiko_save_image.logic.folder_paths")
|
||||
def test_process_image_batch_png(self, mock_folder_paths):
|
||||
@@ -164,7 +170,7 @@ class TestKikoSaveImageLogic:
|
||||
images = torch.rand(1, 48, 48, 3)
|
||||
|
||||
# Test lossless WebP
|
||||
results = process_image_batch(
|
||||
results, enhanced_data = process_image_batch(
|
||||
images=images,
|
||||
filename_prefix="test_webp",
|
||||
format_type="WEBP",
|
||||
@@ -173,10 +179,10 @@ class TestKikoSaveImageLogic:
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
result = results[0]
|
||||
assert result["format"] == "WEBP"
|
||||
assert result["lossless"] is True
|
||||
assert result["filename"].endswith(".webp")
|
||||
assert len(enhanced_data) == 1
|
||||
assert enhanced_data[0]["format"] == "WEBP"
|
||||
assert enhanced_data[0]["lossless"] is True
|
||||
assert results[0]["filename"].endswith(".webp")
|
||||
|
||||
def test_validate_save_inputs_valid(self):
|
||||
"""Test input validation with valid inputs"""
|
||||
@@ -211,20 +217,28 @@ class TestKikoSaveImageLogic:
|
||||
images = torch.rand(1, 32, 32, 3)
|
||||
|
||||
# Quality out of range
|
||||
with pytest.raises(ValueError, match="quality must be an integer between 1 and 100"):
|
||||
with pytest.raises(
|
||||
ValueError, match="quality must be an integer between 1 and 100"
|
||||
):
|
||||
validate_save_inputs(images, "JPEG", 0, 4)
|
||||
|
||||
with pytest.raises(ValueError, match="quality must be an integer between 1 and 100"):
|
||||
with pytest.raises(
|
||||
ValueError, match="quality must be an integer between 1 and 100"
|
||||
):
|
||||
validate_save_inputs(images, "JPEG", 101, 4)
|
||||
|
||||
def test_validate_save_inputs_invalid_compress_level(self):
|
||||
"""Test validation with invalid PNG compression level"""
|
||||
images = torch.rand(1, 32, 32, 3)
|
||||
|
||||
with pytest.raises(ValueError, match="png_compress_level must be an integer between 0 and 9"):
|
||||
with pytest.raises(
|
||||
ValueError, match="png_compress_level must be an integer between 0 and 9"
|
||||
):
|
||||
validate_save_inputs(images, "PNG", 90, -1)
|
||||
|
||||
with pytest.raises(ValueError, match="png_compress_level must be an integer between 0 and 9"):
|
||||
with pytest.raises(
|
||||
ValueError, match="png_compress_level must be an integer between 0 and 9"
|
||||
):
|
||||
validate_save_inputs(images, "PNG", 90, 10)
|
||||
|
||||
def test_save_image_with_format_png(self):
|
||||
@@ -315,7 +329,7 @@ class TestKikoSaveImageNode:
|
||||
assert KikoSaveImageNode.RETURN_TYPES == ()
|
||||
assert KikoSaveImageNode.FUNCTION == "save_images"
|
||||
assert KikoSaveImageNode.OUTPUT_NODE is True
|
||||
assert KikoSaveImageNode.CATEGORY == "ComfyAssets"
|
||||
assert KikoSaveImageNode.CATEGORY == "ComfyAssets/💾 Images"
|
||||
|
||||
@patch("kikotools.tools.kiko_save_image.node.process_image_batch")
|
||||
def test_save_images_success(self, mock_process):
|
||||
@@ -422,7 +436,7 @@ class TestKikoSaveImageNode:
|
||||
info = self.node.get_node_info()
|
||||
|
||||
assert info["class_name"] == "KikoSaveImageNode"
|
||||
assert info["category"] == "ComfyAssets"
|
||||
assert info["category"] == "ComfyAssets/💾 Images"
|
||||
assert info["function"] == "save_images"
|
||||
|
||||
|
||||
@@ -525,8 +539,10 @@ class TestIntegration:
|
||||
# Verify results
|
||||
assert len(result["ui"]["images"]) == 2
|
||||
|
||||
# The results are the basic output - format is in enhanced data
|
||||
# Just check that files were created
|
||||
for image_info in result["ui"]["images"]:
|
||||
assert image_info["format"] == format_type
|
||||
assert "filename" in image_info
|
||||
|
||||
# Verify file exists and can be opened
|
||||
filepath = os.path.join(temp_dir, image_info["filename"])
|
||||
|
||||
@@ -56,9 +56,13 @@ class TestDimensionExtraction:
|
||||
with pytest.raises(ValueError, match="Either image or latent must be provided"):
|
||||
extract_dimensions()
|
||||
|
||||
def test_extract_dimensions_both_inputs_prefers_image(self, mock_image_tensor, mock_latent_tensor):
|
||||
def test_extract_dimensions_both_inputs_prefers_image(
|
||||
self, mock_image_tensor, mock_latent_tensor
|
||||
):
|
||||
"""Test that when both inputs provided, image takes precedence"""
|
||||
width, height = extract_dimensions(image=mock_image_tensor, latent=mock_latent_tensor)
|
||||
width, height = extract_dimensions(
|
||||
image=mock_image_tensor, latent=mock_latent_tensor
|
||||
)
|
||||
|
||||
# Should return image dimensions, not latent
|
||||
assert width == 832
|
||||
@@ -89,7 +93,9 @@ class TestScaledDimensionsCalculation:
|
||||
original_width, original_height = 832, 1216
|
||||
scale_factor = 1.5
|
||||
|
||||
new_width, new_height = calculate_scaled_dimensions(original_width, original_height, scale_factor)
|
||||
new_width, new_height = calculate_scaled_dimensions(
|
||||
original_width, original_height, scale_factor
|
||||
)
|
||||
|
||||
# Check aspect ratio is preserved (within floating point precision)
|
||||
original_ratio = original_width / original_height
|
||||
@@ -101,7 +107,9 @@ class TestScaledDimensionsCalculation:
|
||||
base_width, base_height = 1024, 1024
|
||||
|
||||
for scale_factor in sample_scale_factors:
|
||||
width, height = calculate_scaled_dimensions(base_width, base_height, scale_factor)
|
||||
width, height = calculate_scaled_dimensions(
|
||||
base_width, base_height, scale_factor
|
||||
)
|
||||
|
||||
expected_width = int(base_width * scale_factor)
|
||||
expected_height = int(base_height * scale_factor)
|
||||
@@ -123,14 +131,14 @@ class TestDivisibleBy8Constraint:
|
||||
assert width % 8 == 0
|
||||
assert height % 8 == 0
|
||||
|
||||
def test_ensure_divisible_by_8_needs_rounding_up(self):
|
||||
"""Test rounding up to nearest multiple of 8"""
|
||||
# 1250 -> 1256 (next multiple of 8)
|
||||
# 1825 -> 1832 (next multiple of 8)
|
||||
def test_ensure_divisible_by_8_needs_rounding(self):
|
||||
"""Test rounding to nearest multiple of 8"""
|
||||
# 1250 -> 1248 (nearest multiple of 8, rounds down since 1250 % 8 = 2 < 4)
|
||||
# 1825 -> 1824 (nearest multiple of 8, rounds down since 1825 % 8 = 1 < 4)
|
||||
width, height = ensure_divisible_by_8(1250, 1825)
|
||||
|
||||
assert width == 1256
|
||||
assert height == 1832
|
||||
assert width == 1248
|
||||
assert height == 1824
|
||||
assert width % 8 == 0
|
||||
assert height % 8 == 0
|
||||
|
||||
@@ -171,7 +179,7 @@ class TestResolutionCalculatorNode:
|
||||
assert hasattr(ResolutionCalculatorNode, "CATEGORY")
|
||||
|
||||
# Check category is correct
|
||||
assert ResolutionCalculatorNode.CATEGORY == "ComfyAssets"
|
||||
assert ResolutionCalculatorNode.CATEGORY == "ComfyAssets/🖼️ Resolution"
|
||||
|
||||
# Check return types
|
||||
assert ResolutionCalculatorNode.RETURN_TYPES == ("INT", "INT")
|
||||
@@ -198,14 +206,16 @@ class TestResolutionCalculatorNode:
|
||||
# Check optional inputs
|
||||
assert "image" in input_types["optional"]
|
||||
assert "latent" in input_types["optional"]
|
||||
assert input_types["optional"]["image"] == ("IMAGE",)
|
||||
assert input_types["optional"]["latent"] == ("LATENT",)
|
||||
assert input_types["optional"]["image"][0] == "IMAGE"
|
||||
assert input_types["optional"]["latent"][0] == "LATENT"
|
||||
|
||||
def test_calculate_resolution_with_image(self, mock_image_tensor):
|
||||
"""Test node calculation with IMAGE input"""
|
||||
node = ResolutionCalculatorNode()
|
||||
|
||||
width, height = node.calculate_resolution(scale_factor=2.0, image=mock_image_tensor)
|
||||
width, height = node.calculate_resolution(
|
||||
scale_factor=2.0, image=mock_image_tensor
|
||||
)
|
||||
|
||||
# Original: 832x1216, 2x scale = 1664x2432
|
||||
assert isinstance(width, int)
|
||||
@@ -220,7 +230,9 @@ class TestResolutionCalculatorNode:
|
||||
"""Test node calculation with LATENT input"""
|
||||
node = ResolutionCalculatorNode()
|
||||
|
||||
width, height = node.calculate_resolution(scale_factor=1.5, latent=mock_latent_tensor)
|
||||
width, height = node.calculate_resolution(
|
||||
scale_factor=1.5, latent=mock_latent_tensor
|
||||
)
|
||||
|
||||
# Original: 832x1216, 1.5x scale = 1248x1824
|
||||
assert isinstance(width, int)
|
||||
@@ -238,12 +250,16 @@ class TestResolutionCalculatorNode:
|
||||
with pytest.raises(ValueError):
|
||||
node.calculate_resolution(scale_factor=2.0)
|
||||
|
||||
def test_calculate_resolution_with_various_scale_factors(self, mock_image_tensor_square, sample_scale_factors):
|
||||
def test_calculate_resolution_with_various_scale_factors(
|
||||
self, mock_image_tensor_square, sample_scale_factors
|
||||
):
|
||||
"""Test calculation with various scale factors"""
|
||||
node = ResolutionCalculatorNode()
|
||||
|
||||
for scale_factor in sample_scale_factors:
|
||||
width, height = node.calculate_resolution(scale_factor=scale_factor, image=mock_image_tensor_square)
|
||||
width, height = node.calculate_resolution(
|
||||
scale_factor=scale_factor, image=mock_image_tensor_square
|
||||
)
|
||||
|
||||
# All results should be integers divisible by 8
|
||||
assert isinstance(width, int)
|
||||
@@ -265,7 +281,7 @@ class TestResolutionCalculatorNode:
|
||||
node = ResolutionCalculatorNode()
|
||||
node_info = node.get_node_info()
|
||||
|
||||
assert node_info["category"] == "ComfyAssets"
|
||||
assert node_info["category"] == "ComfyAssets/🖼️ Resolution"
|
||||
assert node_info["class_name"] == "ResolutionCalculatorNode"
|
||||
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ class TestSamplerComboNode:
|
||||
"cfg",
|
||||
)
|
||||
assert SamplerComboNode.FUNCTION == "get_sampler_combo"
|
||||
assert SamplerComboNode.CATEGORY == "ComfyAssets"
|
||||
assert SamplerComboNode.CATEGORY == "ComfyAssets/🌀 Samplers"
|
||||
|
||||
def test_get_sampler_combo_valid_inputs(self):
|
||||
"""Test get_sampler_combo with valid inputs."""
|
||||
@@ -331,7 +331,9 @@ class TestSamplerComboIntegration:
|
||||
|
||||
# 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"])
|
||||
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"]
|
||||
|
||||
@@ -49,7 +49,7 @@ class TestSeedHistoryNode:
|
||||
assert SeedHistoryNode.RETURN_TYPES == ("INT",)
|
||||
assert SeedHistoryNode.RETURN_NAMES == ("seed",)
|
||||
assert SeedHistoryNode.FUNCTION == "output_seed"
|
||||
assert SeedHistoryNode.CATEGORY == "ComfyAssets"
|
||||
assert SeedHistoryNode.CATEGORY == "ComfyAssets/🌱 Seeds"
|
||||
|
||||
def test_output_seed_valid_input(self):
|
||||
"""Test seed output with valid input."""
|
||||
@@ -131,7 +131,8 @@ class TestSeedHistoryNode:
|
||||
|
||||
range_info = node.get_seed_range_info()
|
||||
assert "Valid range" in range_info
|
||||
assert str(0xFFFFFFFFFFFFFFFF) in range_info
|
||||
# Check for the hex representation which should be in the string
|
||||
assert "0xffffffffffffffff" in range_info.lower()
|
||||
|
||||
def test_class_methods(self):
|
||||
"""Test class methods."""
|
||||
|
||||