Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
218a208bf0 | ||
|
|
a7b612d799 | ||
|
|
df3d4c19cb | ||
|
|
938d0d93de | ||
|
|
e65bf123b5 | ||
|
|
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 |
@@ -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"
|
||||
|
||||
|
||||
@@ -162,3 +162,4 @@ experiments/
|
||||
|
||||
# Gemini model cache
|
||||
.gemini_models_cache.json
|
||||
referance/
|
||||
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
@@ -8,6 +8,9 @@ The Gemini Prompt Engineer node uses Google's Gemini AI to analyze images and ge
|
||||
- **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
|
||||
|
||||
@@ -49,6 +52,10 @@ Choose one of these methods:
|
||||
- `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
|
||||
|
||||
@@ -72,15 +79,27 @@ majestic mountain landscape at golden hour, oil painting style, dramatic lightin
|
||||
```
|
||||
|
||||
### SDXL Format
|
||||
Generates positive and negative prompt pairs:
|
||||
- Detailed positive prompts with weight emphasis
|
||||
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: beautiful woman, (detailed eyes:1.2), flowing red dress, golden hour lighting, professional photography, 85mm lens, shallow depth of field, bokeh, high resolution, masterpiece
|
||||
Negative: low quality, blurry, distorted features, bad anatomy, poorly drawn, amateur, oversaturated, jpeg artifacts
|
||||
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
|
||||
@@ -130,12 +149,40 @@ The node provides clear error messages for common issues:
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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,8 +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 = {
|
||||
@@ -23,8 +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 = {
|
||||
@@ -36,8 +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"]
|
||||
|
||||
@@ -48,6 +48,15 @@ def format_display_value(input_value: Any, mode: str = "raw value") -> str:
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class DisplayAnyNode(ComfyAssetsBaseNode):
|
||||
return True
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
CATEGORY = "ComfyAssets/👁️ Display"
|
||||
RETURN_NAMES = ("display_text",)
|
||||
FUNCTION = "display"
|
||||
OUTPUT_NODE = True # This node displays output in the UI
|
||||
@@ -61,6 +62,6 @@ class DisplayAnyNode(ComfyAssetsBaseNode):
|
||||
|
||||
# Return both UI display and result
|
||||
return {
|
||||
"ui": {"text": display_text},
|
||||
"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"
|
||||
@@ -96,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -41,13 +41,17 @@ class GeminiPromptNode(ComfyAssetsBaseNode):
|
||||
"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"
|
||||
CATEGORY = "ComfyAssets/🧠 Prompts"
|
||||
|
||||
DESCRIPTION = """
|
||||
Analyzes images using Google's Gemini AI to generate optimized prompts.
|
||||
@@ -62,7 +66,15 @@ 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=""):
|
||||
def generate_prompt(
|
||||
self,
|
||||
image,
|
||||
prompt_type,
|
||||
model,
|
||||
api_key="",
|
||||
custom_prompt="",
|
||||
refresh_models=False,
|
||||
):
|
||||
"""Generate prompt from image using Gemini.
|
||||
|
||||
Args:
|
||||
@@ -71,10 +83,24 @@ Install: pip install google-generativeai
|
||||
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}")
|
||||
@@ -119,10 +145,14 @@ Install: pip install google-generativeai
|
||||
negative_prompt = ""
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("Positive:"):
|
||||
positive_prompt = line.replace("Positive:", "").strip()
|
||||
elif line.startswith("Negative:"):
|
||||
negative_prompt = line.replace("Negative:", "").strip()
|
||||
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:
|
||||
|
||||
@@ -20,29 +20,47 @@ IMPORTANT: Return ONLY the prompt text. No analysis, headers, or additional comm
|
||||
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 SDXL prompt engineer. Analyze the image and generate ONLY the positive and negative prompts for SDXL - no explanations or analysis.
|
||||
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.
|
||||
|
||||
SDXL works best with natural language descriptions but also supports comma-separated keywords. Keep prompts concise but descriptive.
|
||||
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
|
||||
|
||||
Return your response in EXACTLY this format:
|
||||
Positive: [your positive prompt here]
|
||||
Negative: [your negative prompt here]
|
||||
Structure prompts in this layered, modular format:
|
||||
[Main Subject], [Pose & Camera], [Lighting & Environment], [Style & Details], [Boost Terms], [Style References]
|
||||
|
||||
Guidelines for Positive prompt:
|
||||
- Start with the main subject and medium (e.g., "photograph of", "digital art of")
|
||||
- Use natural language or keywords separated by commas
|
||||
- Include style descriptors (photographic, cinematic, fantasy art, etc.)
|
||||
- Add quality markers like "8K", "highly detailed", "professional"
|
||||
- Use (parentheses:1.1) sparingly for slight emphasis (max 1.4)
|
||||
- Keep it clear and specific but not overly long
|
||||
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
|
||||
|
||||
Guidelines for Negative prompt:
|
||||
- Keep it simple and minimal
|
||||
- Common negatives: ugly, blurry, low quality, distorted, deformed
|
||||
- Only add specifics you want to avoid (e.g., "cartoon" for photorealistic)
|
||||
- Don't overload with negative prompts - SDXL needs fewer than SD1.5
|
||||
Instructions:
|
||||
|
||||
IMPORTANT: Return ONLY the two lines starting with "Positive:" and "Negative:". No other text."""
|
||||
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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ class KikoSaveImageNode(ComfyAssetsBaseNode):
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
CATEGORY = "ComfyAssets/💾 Images"
|
||||
FUNCTION = "save_images"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class ResolutionCalculatorNode(ComfyAssetsBaseNode):
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
CATEGORY = "ComfyAssets/🖼️ Resolution"
|
||||
RETURN_NAMES = ("width", "height")
|
||||
FUNCTION = "calculate_resolution"
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ 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
|
||||
|
||||
@@ -68,7 +68,7 @@ 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
|
||||
|
||||
@@ -38,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]:
|
||||
"""
|
||||
|
||||
@@ -85,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]:
|
||||
"""
|
||||
|
||||
@@ -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.8"
|
||||
version = "1.0.11"
|
||||
license = {text = "MIT"}
|
||||
dependencies = []
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Unit tests for DisplayAny node."""
|
||||
|
||||
import json
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
@@ -39,7 +40,7 @@ class TestDisplayAnyNode:
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node has correct properties."""
|
||||
assert DisplayAnyNode.CATEGORY == "ComfyAssets"
|
||||
assert DisplayAnyNode.CATEGORY == "ComfyAssets/👁️ Display"
|
||||
assert DisplayAnyNode.FUNCTION == "display"
|
||||
assert DisplayAnyNode.RETURN_TYPES == ("STRING",)
|
||||
assert DisplayAnyNode.RETURN_NAMES == ("display_text",)
|
||||
@@ -74,7 +75,7 @@ class TestDisplayAnyNode:
|
||||
|
||||
assert "ui" in result
|
||||
assert "text" in result["ui"]
|
||||
assert result["ui"]["text"] == "Hello, World!"
|
||||
assert result["ui"]["text"] == ["Hello, World!"]
|
||||
assert "result" in result
|
||||
assert result["result"] == ("Hello, World!",)
|
||||
|
||||
@@ -83,7 +84,7 @@ class TestDisplayAnyNode:
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(42, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == "42"
|
||||
assert result["ui"]["text"] == ["42"]
|
||||
assert result["result"] == ("42",)
|
||||
|
||||
def test_display_raw_value_list(self):
|
||||
@@ -92,8 +93,9 @@ class TestDisplayAnyNode:
|
||||
test_list = [1, 2, 3, "test"]
|
||||
result = node.display(test_list, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == str(test_list)
|
||||
assert result["result"] == (str(test_list),)
|
||||
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."""
|
||||
@@ -101,8 +103,9 @@ class TestDisplayAnyNode:
|
||||
test_dict = {"key": "value", "number": 123}
|
||||
result = node.display(test_dict, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == str(test_dict)
|
||||
assert result["result"] == (str(test_dict),)
|
||||
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."""
|
||||
@@ -110,7 +113,7 @@ class TestDisplayAnyNode:
|
||||
tensor = np.random.rand(4, 3, 224, 224)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "[[4, 3, 224, 224]]"
|
||||
assert result["ui"]["text"] == ["[[4, 3, 224, 224]]"]
|
||||
assert result["result"] == ("[[4, 3, 224, 224]]",)
|
||||
|
||||
@pytest.mark.skipif(not torch, reason="PyTorch not installed")
|
||||
@@ -120,7 +123,7 @@ class TestDisplayAnyNode:
|
||||
tensor = torch.randn(2, 10, 512, 512)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "[[2, 10, 512, 512]]"
|
||||
assert result["ui"]["text"] == ["[[2, 10, 512, 512]]"]
|
||||
assert result["result"] == ("[[2, 10, 512, 512]]",)
|
||||
|
||||
def test_display_nested_tensors(self):
|
||||
@@ -137,7 +140,7 @@ class TestDisplayAnyNode:
|
||||
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["ui"]["text"] == [expected]
|
||||
assert result["result"] == (expected,)
|
||||
|
||||
def test_display_no_tensors(self):
|
||||
@@ -146,7 +149,7 @@ class TestDisplayAnyNode:
|
||||
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["ui"]["text"] == ["No tensors found in input"]
|
||||
assert result["result"] == ("No tensors found in input",)
|
||||
|
||||
def test_invalid_mode_defaults_to_raw(self):
|
||||
@@ -154,7 +157,7 @@ class TestDisplayAnyNode:
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("test", "invalid_mode")
|
||||
|
||||
assert result["ui"]["text"] == "test"
|
||||
assert result["ui"]["text"] == ["test"]
|
||||
assert result["result"] == ("test",)
|
||||
|
||||
|
||||
@@ -209,7 +212,9 @@ class TestDisplayAnyLogic:
|
||||
def test_format_display_value_raw(self):
|
||||
"""Test formatting for raw value display."""
|
||||
result = format_display_value({"key": "value"}, "raw value")
|
||||
assert result == "{'key': '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."""
|
||||
@@ -238,19 +243,19 @@ class TestDisplayAnyEdgeCases:
|
||||
"""Test displaying None value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(None, "raw value")
|
||||
assert result["ui"]["text"] == "None"
|
||||
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"] == "[]"
|
||||
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"] == "{}"
|
||||
assert result["ui"]["text"] == ["{}"]
|
||||
|
||||
def test_display_complex_nested_structure(self):
|
||||
"""Test displaying complex nested structure."""
|
||||
@@ -268,7 +273,7 @@ class TestDisplayAnyEdgeCases:
|
||||
result = node.display(complex_data, "tensor shape")
|
||||
|
||||
# Should find 4 tensors total (3 images + 1 latent)
|
||||
shapes_text = result["ui"]["text"]
|
||||
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
|
||||
|
||||
@@ -277,11 +282,11 @@ class TestDisplayAnyEdgeCases:
|
||||
node = DisplayAnyNode()
|
||||
long_string = "x" * 10000
|
||||
result = node.display(long_string, "raw value")
|
||||
assert result["ui"]["text"] == long_string
|
||||
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
|
||||
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."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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
|
||||
@@ -16,7 +17,7 @@ from kikotools.tools.gemini_prompt.logic import (
|
||||
from kikotools.tools.gemini_prompt.prompts import (
|
||||
PROMPT_OPTIONS,
|
||||
PROMPT_TEMPLATES,
|
||||
GEMINI_MODELS,
|
||||
DEFAULT_GEMINI_MODELS,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,7 +26,7 @@ class TestGeminiPromptNode:
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node has correct properties."""
|
||||
assert GeminiPromptNode.CATEGORY == "ComfyAssets"
|
||||
assert GeminiPromptNode.CATEGORY == "ComfyAssets/🧠 Prompts"
|
||||
assert GeminiPromptNode.FUNCTION == "generate_prompt"
|
||||
assert GeminiPromptNode.RETURN_TYPES == ("STRING", "STRING")
|
||||
assert GeminiPromptNode.RETURN_NAMES == ("prompt", "negative_prompt")
|
||||
@@ -41,24 +42,22 @@ class TestGeminiPromptNode:
|
||||
assert "prompt_type" in input_types["required"]
|
||||
assert input_types["required"]["prompt_type"][0] == PROMPT_OPTIONS
|
||||
assert "model" in input_types["required"]
|
||||
assert input_types["required"]["model"][0] == GEMINI_MODELS
|
||||
# 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_gemini_models_available(self):
|
||||
"""Test that all expected Gemini models are available."""
|
||||
expected_models = [
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-flash-8b",
|
||||
"gemini-pro-vision",
|
||||
"gemini-1.0-pro",
|
||||
]
|
||||
for model in expected_models:
|
||||
assert model in GEMINI_MODELS
|
||||
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):
|
||||
@@ -69,7 +68,7 @@ class TestGeminiPromptNode:
|
||||
mock_analyze.return_value = ("A beautiful landscape with mountains", None)
|
||||
|
||||
# Execute
|
||||
result = node.generate_prompt(test_image, "flux")
|
||||
result = node.generate_prompt(test_image, "flux", "gemini-2.5-flash")
|
||||
|
||||
# Assert
|
||||
assert result == ("A beautiful landscape with mountains", "")
|
||||
@@ -87,7 +86,7 @@ class TestGeminiPromptNode:
|
||||
)
|
||||
|
||||
# Execute
|
||||
result = node.generate_prompt(test_image, "sdxl")
|
||||
result = node.generate_prompt(test_image, "sdxl", "gemini-2.5-flash")
|
||||
|
||||
# Assert
|
||||
assert result == (
|
||||
@@ -104,7 +103,7 @@ class TestGeminiPromptNode:
|
||||
mock_analyze.return_value = ("", "API key not found")
|
||||
|
||||
# Execute
|
||||
result = node.generate_prompt(test_image, "flux")
|
||||
result = node.generate_prompt(test_image, "flux", "gemini-2.5-flash")
|
||||
|
||||
# Assert
|
||||
assert result[0].startswith("Error:")
|
||||
@@ -116,7 +115,7 @@ class TestGeminiPromptNode:
|
||||
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")
|
||||
node.generate_prompt(test_image, "invalid_type", "gemini-2.5-flash")
|
||||
|
||||
|
||||
class TestGeminiLogic:
|
||||
@@ -188,29 +187,10 @@ class TestGeminiLogic:
|
||||
assert validate_prompt_type("") is False
|
||||
assert validate_prompt_type(None) is False
|
||||
|
||||
@patch("google.generativeai.configure")
|
||||
@patch("google.generativeai.GenerativeModel")
|
||||
def test_analyze_image_with_gemini_success(self, mock_model_class, mock_configure):
|
||||
@pytest.mark.skip(reason="Requires google-generativeai library")
|
||||
def test_analyze_image_with_gemini_success(self):
|
||||
"""Test successful image analysis with Gemini."""
|
||||
# Setup
|
||||
mock_model = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = "A beautiful sunset over mountains"
|
||||
mock_model.generate_content.return_value = mock_response
|
||||
mock_model_class.return_value = mock_model
|
||||
|
||||
test_image = np.random.rand(64, 64, 3)
|
||||
|
||||
# Execute
|
||||
result, error = analyze_image_with_gemini(
|
||||
test_image, "flux", api_key="test_key"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == "A beautiful sunset over mountains"
|
||||
assert error is None
|
||||
mock_configure.assert_called_once_with(api_key="test_key")
|
||||
mock_model.generate_content.assert_called_once()
|
||||
pass # Skipped as it requires google-generativeai
|
||||
|
||||
def test_analyze_image_no_api_key(self):
|
||||
"""Test analysis without API key."""
|
||||
@@ -224,32 +204,10 @@ class TestGeminiLogic:
|
||||
assert result == ""
|
||||
assert "API key not found" in error
|
||||
|
||||
@patch("google.generativeai.configure")
|
||||
@patch("google.generativeai.GenerativeModel")
|
||||
def test_analyze_image_with_custom_prompt(self, mock_model_class, mock_configure):
|
||||
@pytest.mark.skip(reason="Requires google-generativeai library")
|
||||
def test_analyze_image_with_custom_prompt(self):
|
||||
"""Test analysis with custom prompt."""
|
||||
# Setup
|
||||
mock_model = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = "Custom analysis result"
|
||||
mock_model.generate_content.return_value = mock_response
|
||||
mock_model_class.return_value = mock_model
|
||||
|
||||
test_image = np.random.rand(64, 64, 3)
|
||||
custom_prompt = "Analyze this image and describe the colors"
|
||||
|
||||
# Execute
|
||||
result, error = analyze_image_with_gemini(
|
||||
test_image, "flux", api_key="test_key", custom_prompt=custom_prompt
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == "Custom analysis result"
|
||||
assert error is None
|
||||
|
||||
# Check that custom prompt was used
|
||||
call_args = mock_model.generate_content.call_args[0][0]
|
||||
assert custom_prompt in call_args
|
||||
pass # Skipped as it requires google-generativeai
|
||||
|
||||
|
||||
class TestPromptTemplates:
|
||||
@@ -275,6 +233,11 @@ class TestPromptTemplates:
|
||||
assert "tag" in PROMPT_TEMPLATES["danbooru"].lower()
|
||||
assert "underscore" in PROMPT_TEMPLATES["danbooru"].lower()
|
||||
|
||||
# Video should mention motion and temporal
|
||||
assert "motion" in PROMPT_TEMPLATES["video"].lower()
|
||||
assert "temporal" in PROMPT_TEMPLATES["video"].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,7 +52,7 @@ class TestKikoSaveImageLogic:
|
||||
"""Test save path generation"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Test basic path generation
|
||||
full_path, filename = get_save_image_path(
|
||||
full_path, filename, subfolder = get_save_image_path(
|
||||
"test_prefix", 0, ".png", temp_dir
|
||||
)
|
||||
|
||||
@@ -61,7 +61,9 @@ class TestKikoSaveImageLogic:
|
||||
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_")
|
||||
@@ -78,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):
|
||||
@@ -166,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",
|
||||
@@ -175,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"""
|
||||
@@ -325,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):
|
||||
@@ -432,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"
|
||||
|
||||
|
||||
@@ -535,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"])
|
||||
|
||||
@@ -131,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
|
||||
|
||||
@@ -179,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")
|
||||
@@ -206,8 +206,8 @@ 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"""
|
||||
@@ -281,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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestWidthHeightSelectorNode:
|
||||
assert self.node.RETURN_TYPES == ("INT", "INT")
|
||||
assert self.node.RETURN_NAMES == ("width", "height")
|
||||
assert self.node.FUNCTION == "get_dimensions"
|
||||
assert self.node.CATEGORY == "ComfyAssets"
|
||||
assert self.node.CATEGORY == "ComfyAssets/🖼️ Resolution"
|
||||
|
||||
def test_custom_dimensions(self):
|
||||
"""Test custom dimensions."""
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Test suite for xyz_helpers module."""
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Tests for Flux Sampler Params node."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from kikotools.tools.xyz_helpers.flux_sampler_params import FluxSamplerParamsNode
|
||||
from kikotools.tools.xyz_helpers.flux_sampler_params.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,
|
||||
)
|
||||
|
||||
|
||||
class TestFluxSamplerParamsLogic:
|
||||
"""Test the logic functions for Flux Sampler Params."""
|
||||
|
||||
def test_parse_string_to_list(self):
|
||||
"""Test parsing comma-separated strings to lists."""
|
||||
assert parse_string_to_list("1.0, 2.5, 3.7") == [1.0, 2.5, 3.7]
|
||||
assert parse_string_to_list("5") == [5.0]
|
||||
assert parse_string_to_list("") == []
|
||||
assert parse_string_to_list("1.0, invalid, 3.0") == [1.0, 3.0]
|
||||
|
||||
def test_parse_seed_string(self):
|
||||
"""Test parsing seed strings."""
|
||||
seeds = parse_seed_string("123, 456, 789")
|
||||
assert seeds == [123, 456, 789]
|
||||
|
||||
# Test with ? for random
|
||||
seeds = parse_seed_string("123, ?")
|
||||
assert len(seeds) == 2
|
||||
assert seeds[0] == 123
|
||||
assert 0 <= seeds[1] <= 999999
|
||||
|
||||
# Test with newlines
|
||||
seeds = parse_seed_string("123\n456\n789")
|
||||
assert seeds == [123, 456, 789]
|
||||
|
||||
def test_parse_sampler_string(self):
|
||||
"""Test parsing sampler strings."""
|
||||
available = ["euler", "dpmpp_2m", "ddim", "uni_pc"]
|
||||
|
||||
# Test normal selection
|
||||
result = parse_sampler_string("euler, ddim", available)
|
||||
assert result == ["euler", "ddim"]
|
||||
|
||||
# Test wildcard
|
||||
result = parse_sampler_string("*", available)
|
||||
assert result == available
|
||||
|
||||
# Test exclusion
|
||||
result = parse_sampler_string("!euler, ddim", available)
|
||||
assert "euler" not in result
|
||||
assert "ddim" not in result
|
||||
assert "dpmpp_2m" in result
|
||||
assert "uni_pc" in result
|
||||
|
||||
def test_parse_scheduler_string(self):
|
||||
"""Test parsing scheduler strings."""
|
||||
available = ["normal", "karras", "simple", "exponential"]
|
||||
|
||||
# Test normal selection
|
||||
result = parse_scheduler_string("normal, simple", available)
|
||||
assert result == ["normal", "simple"]
|
||||
|
||||
# Test wildcard
|
||||
result = parse_scheduler_string("*", available)
|
||||
assert result == available
|
||||
|
||||
# Test exclusion
|
||||
result = parse_scheduler_string("!normal", available)
|
||||
assert "normal" not in result
|
||||
assert "karras" in result
|
||||
|
||||
def test_get_default_flux_params(self):
|
||||
"""Test getting default Flux parameters."""
|
||||
# Test Schnell defaults
|
||||
params = get_default_flux_params(is_schnell=True)
|
||||
assert params["steps"] == 4
|
||||
assert params["max_shift"] == 0
|
||||
assert params["base_shift"] == 1.0
|
||||
|
||||
# Test regular Flux defaults
|
||||
params = get_default_flux_params(is_schnell=False)
|
||||
assert params["steps"] == 20
|
||||
assert params["max_shift"] == 1.15
|
||||
assert params["base_shift"] == 0.5
|
||||
|
||||
def test_create_batch_params(self):
|
||||
"""Test creating batch parameters."""
|
||||
total, params = create_batch_params(
|
||||
seeds=[1, 2],
|
||||
samplers=["euler"],
|
||||
schedulers=["normal"],
|
||||
steps=[20],
|
||||
guidances=[7.0],
|
||||
max_shifts=[1.0],
|
||||
base_shifts=[0.5],
|
||||
denoises=[1.0],
|
||||
conditioning_count=1,
|
||||
lora_strength_count=1,
|
||||
)
|
||||
|
||||
assert total == 2 # 2 seeds * 1 of everything else
|
||||
assert len(params) == 2
|
||||
assert params[0]["seed"] == 1
|
||||
assert params[1]["seed"] == 2
|
||||
|
||||
def test_process_conditioning_input(self):
|
||||
"""Test processing conditioning input."""
|
||||
# Test dict input
|
||||
cond_dict = {
|
||||
"text": ["prompt1", "prompt2"],
|
||||
"encoded": ["encoded1", "encoded2"],
|
||||
}
|
||||
text, encoded = process_conditioning_input(cond_dict)
|
||||
assert text == ["prompt1", "prompt2"]
|
||||
assert encoded == ["encoded1", "encoded2"]
|
||||
|
||||
# Test regular conditioning
|
||||
regular_cond = "regular_conditioning"
|
||||
text, encoded = process_conditioning_input(regular_cond)
|
||||
assert text is None
|
||||
assert encoded == ["regular_conditioning"]
|
||||
|
||||
def test_validate_flux_params(self):
|
||||
"""Test validating Flux parameters."""
|
||||
assert validate_flux_params("20", "7.0", "1.15", "0.5", "1.0") == True
|
||||
assert validate_flux_params("20, 30", "7.0, 8.0", "1.15", "0.5", "1.0") == True
|
||||
|
||||
|
||||
class TestFluxSamplerParamsNode:
|
||||
"""Test the Flux Sampler Params node."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return FluxSamplerParamsNode()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_model(self):
|
||||
"""Create a mock model."""
|
||||
model = Mock()
|
||||
model.model = Mock()
|
||||
model.model.model_type = Mock()
|
||||
return model
|
||||
|
||||
@pytest.fixture
|
||||
def mock_conditioning(self):
|
||||
"""Create mock conditioning."""
|
||||
return {"text": ["test prompt"], "encoded": [Mock()]}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_latent(self):
|
||||
"""Create mock latent."""
|
||||
latent = {"samples": Mock()}
|
||||
latent["samples"].shape = [1, 4, 64, 64] # batch, channels, height, width
|
||||
return latent
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES returns correct structure."""
|
||||
input_types = FluxSamplerParamsNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
assert "optional" in input_types
|
||||
|
||||
required = input_types["required"]
|
||||
assert "model" in required
|
||||
assert "conditioning" in required
|
||||
assert "latent_image" in required
|
||||
assert "seed" in required
|
||||
assert "sampler" in required
|
||||
assert "scheduler" in required
|
||||
assert "steps" in required
|
||||
assert "guidance" in required
|
||||
|
||||
optional = input_types["optional"]
|
||||
assert "loras" in optional
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node properties."""
|
||||
assert FluxSamplerParamsNode.CATEGORY == "ComfyAssets/🧰 xyz-helpers"
|
||||
assert FluxSamplerParamsNode.FUNCTION == "process_batch"
|
||||
assert FluxSamplerParamsNode.RETURN_TYPES == ("LATENT", "SAMPLER_PARAMS")
|
||||
assert FluxSamplerParamsNode.RETURN_NAMES == ("latent", "params")
|
||||
|
||||
def test_init(self):
|
||||
"""Test node initialization."""
|
||||
node = FluxSamplerParamsNode()
|
||||
assert node.lora_loader is None
|
||||
assert node.cached_lora == (None, None)
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Tests for LoRA Folder Batch node."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import os
|
||||
from kikotools.tools.xyz_helpers.lora_folder_batch import LoRAFolderBatchNode
|
||||
from kikotools.tools.xyz_helpers.lora_folder_batch.logic import (
|
||||
scan_folder_for_loras,
|
||||
natural_sort,
|
||||
filter_loras_by_pattern,
|
||||
parse_strength_string,
|
||||
create_lora_params,
|
||||
get_lora_info,
|
||||
validate_folder_path,
|
||||
)
|
||||
|
||||
|
||||
class TestLoRAFolderBatchLogic:
|
||||
"""Test the logic functions for LoRA Folder Batch."""
|
||||
|
||||
def test_natural_sort(self):
|
||||
"""Test natural sorting of filenames."""
|
||||
files = [
|
||||
"model-v1-000100.safetensors",
|
||||
"model-v1-000020.safetensors",
|
||||
"model-v1-000004.safetensors",
|
||||
"model-v1.safetensors",
|
||||
]
|
||||
sorted_files = natural_sort(files)
|
||||
|
||||
# Natural sort should put numbered epochs in order
|
||||
assert "000004" in sorted_files[0]
|
||||
assert "000020" in sorted_files[1]
|
||||
assert "000100" in sorted_files[2]
|
||||
# Base file could be first or last depending on implementation
|
||||
assert "model-v1.safetensors" in sorted_files
|
||||
|
||||
def test_filter_loras_by_pattern(self):
|
||||
"""Test filtering LoRAs by patterns."""
|
||||
files = [
|
||||
"model-v1.safetensors",
|
||||
"model-v2.safetensors",
|
||||
"test-model.safetensors",
|
||||
"backup-model.safetensors",
|
||||
]
|
||||
|
||||
# Test include pattern
|
||||
filtered = filter_loras_by_pattern(files, include_pattern="model-v")
|
||||
assert len(filtered) == 2
|
||||
assert "model-v1.safetensors" in filtered
|
||||
assert "model-v2.safetensors" in filtered
|
||||
|
||||
# Test exclude pattern
|
||||
filtered = filter_loras_by_pattern(files, exclude_pattern="test|backup")
|
||||
assert len(filtered) == 2
|
||||
assert "test-model.safetensors" not in filtered
|
||||
assert "backup-model.safetensors" not in filtered
|
||||
|
||||
def test_parse_strength_string_single(self):
|
||||
"""Test parsing single strength value."""
|
||||
strengths = parse_strength_string("0.75")
|
||||
assert strengths == [0.75]
|
||||
|
||||
def test_parse_strength_string_multiple(self):
|
||||
"""Test parsing multiple strength values."""
|
||||
strengths = parse_strength_string("0.5, 0.75, 1.0")
|
||||
assert strengths == [0.5, 0.75, 1.0]
|
||||
|
||||
def test_parse_strength_string_range(self):
|
||||
"""Test parsing strength range."""
|
||||
strengths = parse_strength_string("0.5...1.0+0.25")
|
||||
assert strengths == [0.5, 0.75, 1.0]
|
||||
|
||||
# Test default step
|
||||
strengths = parse_strength_string("0.8...1.0")
|
||||
assert len(strengths) == 3 # 0.8, 0.9, 1.0
|
||||
|
||||
def test_parse_strength_string_empty(self):
|
||||
"""Test parsing empty strength string."""
|
||||
strengths = parse_strength_string("")
|
||||
assert strengths == [1.0]
|
||||
|
||||
def test_create_lora_params_sequential(self):
|
||||
"""Test creating LORA_PARAMS in sequential mode."""
|
||||
loras = ["lora1.safetensors", "lora2.safetensors"]
|
||||
strengths = [0.5, 1.0]
|
||||
|
||||
params = create_lora_params(loras, strengths, "sequential")
|
||||
|
||||
assert params["loras"] == loras
|
||||
assert len(params["strengths"]) == 2
|
||||
assert params["strengths"][0] == [0.5]
|
||||
assert params["strengths"][1] == [1.0]
|
||||
|
||||
def test_create_lora_params_combinatorial(self):
|
||||
"""Test creating LORA_PARAMS in combinatorial mode."""
|
||||
loras = ["lora1.safetensors", "lora2.safetensors"]
|
||||
strengths = [0.5, 1.0]
|
||||
|
||||
params = create_lora_params(loras, strengths, "combinatorial")
|
||||
|
||||
assert params["loras"] == loras
|
||||
assert len(params["strengths"]) == 2
|
||||
assert params["strengths"][0] == [0.5, 1.0]
|
||||
assert params["strengths"][1] == [0.5, 1.0]
|
||||
|
||||
def test_get_lora_info(self):
|
||||
"""Test extracting info from LoRA filename."""
|
||||
info = get_lora_info("model-v8-000012.safetensors")
|
||||
assert info["epoch"] == 12
|
||||
assert "v8" in info["version"]
|
||||
|
||||
info = get_lora_info("simple-model.safetensors")
|
||||
assert info["epoch"] is None
|
||||
assert info["version"] is None
|
||||
|
||||
|
||||
class TestLoRAFolderBatchNode:
|
||||
"""Test the LoRA Folder Batch node."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return LoRAFolderBatchNode()
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES returns correct structure."""
|
||||
input_types = LoRAFolderBatchNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
assert "optional" in input_types
|
||||
|
||||
required = input_types["required"]
|
||||
assert "folder_path" in required
|
||||
assert "strength" in required
|
||||
assert "batch_mode" in required
|
||||
|
||||
optional = input_types["optional"]
|
||||
assert "include_pattern" in optional
|
||||
assert "exclude_pattern" in optional
|
||||
|
||||
def test_batch_loras_empty_folder(self, node):
|
||||
"""Test with empty folder."""
|
||||
with patch(
|
||||
"kikotools.tools.xyz_helpers.lora_folder_batch.node.validate_folder_path"
|
||||
) as mock_validate:
|
||||
with patch(
|
||||
"kikotools.tools.xyz_helpers.lora_folder_batch.logic.scan_folder_for_loras"
|
||||
) as mock_scan:
|
||||
mock_validate.return_value = True
|
||||
mock_scan.return_value = []
|
||||
|
||||
result = node.batch_loras(
|
||||
folder_path="test", strength="1.0", batch_mode="sequential"
|
||||
)
|
||||
|
||||
assert result[0] == {"loras": [], "strengths": []}
|
||||
assert result[1] == ""
|
||||
assert result[2] == 0
|
||||
|
||||
def test_batch_loras_with_files(self, node):
|
||||
"""Test with LoRA files found."""
|
||||
with patch(
|
||||
"kikotools.tools.xyz_helpers.lora_folder_batch.node.validate_folder_path"
|
||||
) as mock_validate:
|
||||
with patch(
|
||||
"kikotools.tools.xyz_helpers.lora_folder_batch.node.scan_folder_for_loras"
|
||||
) as mock_scan:
|
||||
mock_validate.return_value = True
|
||||
mock_scan.return_value = [
|
||||
"model-000004.safetensors",
|
||||
"model-000008.safetensors",
|
||||
]
|
||||
|
||||
result = node.batch_loras(
|
||||
folder_path="test", strength="1.0", batch_mode="sequential"
|
||||
)
|
||||
|
||||
params, lora_list, count = result
|
||||
assert count == 2
|
||||
assert len(params["loras"]) == 2
|
||||
assert "model-000004" in lora_list
|
||||
assert "epoch 4" in lora_list
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node properties."""
|
||||
assert LoRAFolderBatchNode.CATEGORY == "ComfyAssets/🧰 xyz-helpers"
|
||||
assert LoRAFolderBatchNode.FUNCTION == "batch_loras"
|
||||
assert LoRAFolderBatchNode.RETURN_TYPES == ("LORA_PARAMS", "STRING", "INT")
|
||||
assert LoRAFolderBatchNode.RETURN_NAMES == (
|
||||
"lora_params",
|
||||
"lora_list",
|
||||
"lora_count",
|
||||
)
|
||||
|
||||
def test_is_changed(self):
|
||||
"""Test IS_CHANGED method returns unique value."""
|
||||
result1 = LoRAFolderBatchNode.IS_CHANGED()
|
||||
import time
|
||||
|
||||
time.sleep(0.01)
|
||||
result2 = LoRAFolderBatchNode.IS_CHANGED()
|
||||
assert result1 != result2
|
||||
@@ -0,0 +1,233 @@
|
||||
"""Tests for Plot Parameters node."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from unittest.mock import Mock, patch
|
||||
from kikotools.tools.xyz_helpers.plot_sampler_params import PlotParametersNode
|
||||
from kikotools.tools.xyz_helpers.plot_sampler_params.logic import (
|
||||
sort_parameters,
|
||||
group_by_value,
|
||||
identify_changing_parameters,
|
||||
filter_changing_params,
|
||||
format_parameter_text,
|
||||
wrap_prompt_text,
|
||||
calculate_grid_dimensions,
|
||||
validate_plot_parameters,
|
||||
)
|
||||
|
||||
|
||||
class TestPlotParametersLogic:
|
||||
"""Test the logic functions for Plot Parameters."""
|
||||
|
||||
def test_sort_parameters(self):
|
||||
"""Test sorting parameters."""
|
||||
params = [
|
||||
{"seed": 3, "steps": 20},
|
||||
{"seed": 1, "steps": 30},
|
||||
{"seed": 2, "steps": 10},
|
||||
]
|
||||
|
||||
# Sort by seed
|
||||
sorted_params, indices = sort_parameters(params, "seed")
|
||||
assert sorted_params[0]["seed"] == 1
|
||||
assert sorted_params[1]["seed"] == 2
|
||||
assert sorted_params[2]["seed"] == 3
|
||||
assert indices == [1, 2, 0]
|
||||
|
||||
# Sort by steps
|
||||
sorted_params, indices = sort_parameters(params, "steps")
|
||||
assert sorted_params[0]["steps"] == 10
|
||||
assert sorted_params[1]["steps"] == 20
|
||||
assert sorted_params[2]["steps"] == 30
|
||||
|
||||
# No sorting
|
||||
sorted_params, indices = sort_parameters(params, "none")
|
||||
assert sorted_params == params
|
||||
assert indices == [0, 1, 2]
|
||||
|
||||
def test_group_by_value(self):
|
||||
"""Test grouping by value."""
|
||||
params = [
|
||||
{"sampler": "euler", "seed": 1},
|
||||
{"sampler": "ddim", "seed": 2},
|
||||
{"sampler": "euler", "seed": 3},
|
||||
{"sampler": "ddim", "seed": 4},
|
||||
]
|
||||
|
||||
grouped, indices, num_groups = group_by_value(params, "sampler")
|
||||
assert num_groups == 2
|
||||
# Check that same samplers are grouped
|
||||
assert grouped[0]["sampler"] == grouped[2]["sampler"]
|
||||
assert grouped[1]["sampler"] == grouped[3]["sampler"]
|
||||
|
||||
def test_identify_changing_parameters(self):
|
||||
"""Test identifying changing parameters."""
|
||||
params = [
|
||||
{"seed": 1, "steps": 20, "sampler": "euler"},
|
||||
{"seed": 2, "steps": 20, "sampler": "ddim"},
|
||||
{"seed": 3, "steps": 20, "sampler": "euler"},
|
||||
]
|
||||
|
||||
changing = identify_changing_parameters(params)
|
||||
assert changing["seed"] == True # Seed changes
|
||||
assert changing["steps"] == False # Steps don't change
|
||||
assert changing["sampler"] == True # Sampler changes
|
||||
|
||||
def test_filter_changing_params(self):
|
||||
"""Test filtering to only changing parameters."""
|
||||
params = [
|
||||
{"seed": 1, "steps": 20, "sampler": "euler"},
|
||||
{"seed": 2, "steps": 20, "sampler": "ddim"},
|
||||
]
|
||||
|
||||
filtered = filter_changing_params(params)
|
||||
assert "seed" in filtered[0]
|
||||
assert "sampler" in filtered[0]
|
||||
assert "steps" not in filtered[0] # Steps don't change
|
||||
|
||||
def test_format_parameter_text_full(self):
|
||||
"""Test formatting parameter text in full mode."""
|
||||
param = {
|
||||
"time": 2.5,
|
||||
"seed": 12345,
|
||||
"steps": 20,
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"denoise": 1.0,
|
||||
"sampler": "euler",
|
||||
"scheduler": "normal",
|
||||
"guidance": 7.0,
|
||||
"max_shift": 1.15,
|
||||
"base_shift": 0.5,
|
||||
}
|
||||
|
||||
text = format_parameter_text(param, "full")
|
||||
assert "time: 2.50s" in text
|
||||
assert "seed: 12345" in text
|
||||
assert "steps: 20" in text
|
||||
assert "512×512" in text
|
||||
|
||||
def test_format_parameter_text_changes_only(self):
|
||||
"""Test formatting parameter text in changes only mode."""
|
||||
param = {"seed": 12345, "sampler": "euler", "prompt": "test prompt"}
|
||||
|
||||
text = format_parameter_text(param, "changes only")
|
||||
assert "seed: 12345" in text
|
||||
assert "sampler: euler" in text
|
||||
assert "prompt" not in text # Prompt handled separately
|
||||
|
||||
def test_wrap_prompt_text(self):
|
||||
"""Test wrapping prompt text."""
|
||||
prompt = "This is a very long prompt that needs to be wrapped"
|
||||
|
||||
# Full mode
|
||||
lines = wrap_prompt_text(prompt, 20, "full")
|
||||
assert len(lines) > 1
|
||||
assert all(len(line) <= 20 for line in lines)
|
||||
|
||||
# Excerpt mode
|
||||
long_prompt = " ".join(["word"] * 100)
|
||||
lines = wrap_prompt_text(long_prompt, 50, "excerpt")
|
||||
full_text = " ".join(lines)
|
||||
assert "..." in full_text
|
||||
|
||||
def test_calculate_grid_dimensions(self):
|
||||
"""Test calculating grid dimensions."""
|
||||
# Auto mode
|
||||
rows, cols = calculate_grid_dimensions(9, -1)
|
||||
assert rows == 3
|
||||
assert cols == 3
|
||||
|
||||
# Fixed columns
|
||||
rows, cols = calculate_grid_dimensions(10, 3)
|
||||
assert rows == 4
|
||||
assert cols == 3
|
||||
|
||||
# Auto square
|
||||
rows, cols = calculate_grid_dimensions(16, 0)
|
||||
assert rows == 4
|
||||
assert cols == 4
|
||||
|
||||
def test_validate_plot_parameters(self):
|
||||
"""Test validating plot parameters."""
|
||||
# Valid
|
||||
assert validate_plot_parameters((5, 256, 256, 3), 5, "none", "none", -1) == True
|
||||
|
||||
# Mismatch
|
||||
assert (
|
||||
validate_plot_parameters((5, 256, 256, 3), 3, "none", "none", -1) == False
|
||||
)
|
||||
|
||||
|
||||
class TestPlotParametersNode:
|
||||
"""Test the Plot Parameters node."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return PlotParametersNode()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_images(self):
|
||||
"""Create mock images tensor."""
|
||||
return torch.rand(4, 256, 256, 3)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_params(self):
|
||||
"""Create mock parameters."""
|
||||
return [
|
||||
{
|
||||
"time": 2.0,
|
||||
"seed": 1,
|
||||
"steps": 20,
|
||||
"width": 256,
|
||||
"height": 256,
|
||||
"sampler": "euler",
|
||||
"scheduler": "normal",
|
||||
"guidance": 7.0,
|
||||
"denoise": 1.0,
|
||||
"max_shift": 1.0,
|
||||
"base_shift": 0.5,
|
||||
}
|
||||
for i in range(4)
|
||||
]
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES returns correct structure."""
|
||||
input_types = PlotParametersNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
|
||||
required = input_types["required"]
|
||||
assert "images" in required
|
||||
assert "params" in required
|
||||
assert "order_by" in required
|
||||
assert "cols_value" in required
|
||||
assert "cols_num" in required
|
||||
assert "add_prompt" in required
|
||||
assert "add_params" in required
|
||||
|
||||
def test_plot_parameters_basic(self, node, mock_images, mock_params):
|
||||
"""Test basic plot creation."""
|
||||
with patch(
|
||||
"kikotools.tools.xyz_helpers.plot_sampler_params.node.ImageFont.truetype"
|
||||
):
|
||||
result = node.plot_parameters(
|
||||
mock_images,
|
||||
mock_params,
|
||||
order_by="none",
|
||||
cols_value="none",
|
||||
cols_num=-1,
|
||||
add_prompt="false",
|
||||
add_params="false",
|
||||
)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], torch.Tensor)
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node properties."""
|
||||
assert PlotParametersNode.CATEGORY == "ComfyAssets/🧰 xyz-helpers"
|
||||
assert PlotParametersNode.FUNCTION == "plot_parameters"
|
||||
assert PlotParametersNode.RETURN_TYPES == ("IMAGE",)
|
||||
assert PlotParametersNode.RETURN_NAMES == ("image",)
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Tests for Sampler Select Helper node."""
|
||||
|
||||
import pytest
|
||||
from kikotools.tools.xyz_helpers.sampler_select_helper import SamplerSelectHelperNode
|
||||
from kikotools.tools.xyz_helpers.sampler_select_helper.logic import (
|
||||
process_sampler_selection,
|
||||
validate_sampler_names,
|
||||
get_sampler_groups,
|
||||
get_default_samplers,
|
||||
)
|
||||
|
||||
|
||||
class TestSamplerSelectHelperLogic:
|
||||
"""Test the logic functions for Sampler Select Helper."""
|
||||
|
||||
def test_process_sampler_selection_with_selections(self):
|
||||
"""Test processing sampler selections."""
|
||||
result = process_sampler_selection(
|
||||
euler=True, dpmpp_2m=True, ddim=False, uni_pc=True
|
||||
)
|
||||
assert result == "euler, dpmpp_2m, uni_pc"
|
||||
|
||||
def test_process_sampler_selection_no_selections(self):
|
||||
"""Test with no selections."""
|
||||
result = process_sampler_selection(euler=False, dpmpp_2m=False)
|
||||
assert result == ""
|
||||
|
||||
def test_validate_sampler_names(self):
|
||||
"""Test validating sampler names."""
|
||||
valid = validate_sampler_names("euler, dpmpp_2m, invalid_sampler")
|
||||
assert "euler" in valid
|
||||
assert "dpmpp_2m" in valid
|
||||
assert "invalid_sampler" not in valid
|
||||
|
||||
def test_get_sampler_groups(self):
|
||||
"""Test getting sampler groups."""
|
||||
groups = get_sampler_groups()
|
||||
assert "Euler" in groups
|
||||
assert "DPM" in groups
|
||||
assert "DPM++" in groups
|
||||
assert "Other" in groups
|
||||
|
||||
def test_get_default_samplers(self):
|
||||
"""Test getting default samplers."""
|
||||
defaults = get_default_samplers()
|
||||
assert len(defaults) > 0
|
||||
assert "euler" in defaults
|
||||
|
||||
|
||||
class TestSamplerSelectHelperNode:
|
||||
"""Test the Sampler Select Helper node."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return SamplerSelectHelperNode()
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES returns correct structure."""
|
||||
input_types = SamplerSelectHelperNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
|
||||
# Check that samplers are in required inputs
|
||||
required = input_types["required"]
|
||||
assert "euler" in required
|
||||
assert required["euler"][0] == "BOOLEAN"
|
||||
|
||||
def test_select_samplers_with_selections(self, node):
|
||||
"""Test selecting samplers."""
|
||||
result = node.select_samplers(
|
||||
euler=True, dpmpp_2m=True, ddim=False, uni_pc=True, lms=False
|
||||
)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
selected = result[0]
|
||||
assert "euler" in selected
|
||||
assert "dpmpp_2m" in selected
|
||||
assert "uni_pc" in selected
|
||||
assert "ddim" not in selected
|
||||
|
||||
def test_select_samplers_no_selection(self, node):
|
||||
"""Test with no samplers selected."""
|
||||
result = node.select_samplers(euler=False, dpmpp_2m=False)
|
||||
assert result == ("",)
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node properties."""
|
||||
assert SamplerSelectHelperNode.CATEGORY == "ComfyAssets/🧰 xyz-helpers"
|
||||
assert SamplerSelectHelperNode.FUNCTION == "select_samplers"
|
||||
assert SamplerSelectHelperNode.RETURN_TYPES == ("STRING",)
|
||||
assert SamplerSelectHelperNode.RETURN_NAMES == ("selected_samplers",)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Tests for Scheduler Select Helper node."""
|
||||
|
||||
import pytest
|
||||
from kikotools.tools.xyz_helpers.scheduler_select_helper import (
|
||||
SchedulerSelectHelperNode,
|
||||
)
|
||||
from kikotools.tools.xyz_helpers.scheduler_select_helper.logic import (
|
||||
process_scheduler_selection,
|
||||
validate_scheduler_names,
|
||||
get_scheduler_categories,
|
||||
get_default_schedulers,
|
||||
get_scheduler_description,
|
||||
)
|
||||
|
||||
|
||||
class TestSchedulerSelectHelperLogic:
|
||||
"""Test the logic functions for Scheduler Select Helper."""
|
||||
|
||||
def test_process_scheduler_selection_with_selections(self):
|
||||
"""Test processing scheduler selections."""
|
||||
result = process_scheduler_selection(
|
||||
normal=True, karras=True, exponential=False, simple=True
|
||||
)
|
||||
assert result == "normal, karras, simple"
|
||||
|
||||
def test_process_scheduler_selection_no_selections(self):
|
||||
"""Test with no selections."""
|
||||
result = process_scheduler_selection(normal=False, karras=False)
|
||||
assert result == ""
|
||||
|
||||
def test_validate_scheduler_names(self):
|
||||
"""Test validating scheduler names."""
|
||||
valid = validate_scheduler_names("normal, karras, invalid_scheduler")
|
||||
assert "normal" in valid
|
||||
assert "karras" in valid
|
||||
assert "invalid_scheduler" not in valid
|
||||
|
||||
def test_get_scheduler_categories(self):
|
||||
"""Test getting scheduler categories."""
|
||||
categories = get_scheduler_categories()
|
||||
assert "Standard" in categories
|
||||
assert "Uniform" in categories
|
||||
assert "Advanced" in categories
|
||||
|
||||
def test_get_default_schedulers(self):
|
||||
"""Test getting default schedulers."""
|
||||
defaults = get_default_schedulers()
|
||||
assert len(defaults) > 0
|
||||
assert "normal" in defaults
|
||||
assert "karras" in defaults
|
||||
|
||||
def test_get_scheduler_description(self):
|
||||
"""Test getting scheduler descriptions."""
|
||||
desc = get_scheduler_description("karras")
|
||||
assert "Karras" in desc
|
||||
|
||||
desc = get_scheduler_description("normal")
|
||||
assert "linear" in desc.lower()
|
||||
|
||||
desc = get_scheduler_description("unknown")
|
||||
assert desc == "Custom scheduler"
|
||||
|
||||
|
||||
class TestSchedulerSelectHelperNode:
|
||||
"""Test the Scheduler Select Helper node."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return SchedulerSelectHelperNode()
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES returns correct structure."""
|
||||
input_types = SchedulerSelectHelperNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
|
||||
# Check that schedulers are in required inputs
|
||||
required = input_types["required"]
|
||||
assert "normal" in required
|
||||
assert required["normal"][0] == "BOOLEAN"
|
||||
|
||||
def test_select_schedulers_with_selections(self, node):
|
||||
"""Test selecting schedulers."""
|
||||
result = node.select_schedulers(
|
||||
normal=True, karras=True, exponential=False, simple=True, beta=False
|
||||
)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
selected = result[0]
|
||||
assert "normal" in selected
|
||||
assert "karras" in selected
|
||||
assert "simple" in selected
|
||||
assert "exponential" not in selected
|
||||
|
||||
def test_select_schedulers_no_selection(self, node):
|
||||
"""Test with no schedulers selected."""
|
||||
result = node.select_schedulers(normal=False, karras=False)
|
||||
assert result == ("",)
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node properties."""
|
||||
assert SchedulerSelectHelperNode.CATEGORY == "ComfyAssets/🧰 xyz-helpers"
|
||||
assert SchedulerSelectHelperNode.FUNCTION == "select_schedulers"
|
||||
assert SchedulerSelectHelperNode.RETURN_TYPES == ("STRING",)
|
||||
assert SchedulerSelectHelperNode.RETURN_NAMES == ("selected_schedulers",)
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Tests for Text Encode Sampler Params node."""
|
||||
|
||||
import pytest
|
||||
from kikotools.tools.xyz_helpers.text_encode_sampler_params import (
|
||||
TextEncodeSamplerParamsNode,
|
||||
)
|
||||
from kikotools.tools.xyz_helpers.text_encode_sampler_params.logic import (
|
||||
split_prompts,
|
||||
create_sampler_params_conditioning,
|
||||
validate_prompt_format,
|
||||
get_prompt_statistics,
|
||||
)
|
||||
|
||||
|
||||
class TestTextEncodeSamplerParamsLogic:
|
||||
"""Test the logic functions for Text Encode Sampler Params."""
|
||||
|
||||
def test_split_prompts_with_dashes(self):
|
||||
"""Test splitting prompts with dash separators."""
|
||||
text = "First prompt\n---\nSecond prompt\n---\nThird prompt"
|
||||
prompts = split_prompts(text)
|
||||
assert len(prompts) == 3
|
||||
assert prompts[0] == "First prompt"
|
||||
assert prompts[1] == "Second prompt"
|
||||
assert prompts[2] == "Third prompt"
|
||||
|
||||
def test_split_prompts_with_various_separators(self):
|
||||
"""Test with different separator types."""
|
||||
text = "First\n***\nSecond\n===\nThird\n~~~\nFourth"
|
||||
prompts = split_prompts(text)
|
||||
assert len(prompts) == 4
|
||||
|
||||
def test_split_prompts_with_extra_separators(self):
|
||||
"""Test with longer separators."""
|
||||
text = "First\n--------\nSecond\n*********\nThird"
|
||||
prompts = split_prompts(text)
|
||||
assert len(prompts) == 3
|
||||
|
||||
def test_split_prompts_no_separator(self):
|
||||
"""Test with no separator."""
|
||||
text = "Single prompt without separator"
|
||||
prompts = split_prompts(text)
|
||||
assert len(prompts) == 1
|
||||
assert prompts[0] == "Single prompt without separator"
|
||||
|
||||
def test_split_prompts_empty_sections(self):
|
||||
"""Test with empty sections between separators."""
|
||||
text = "First\n---\n\n---\nThird"
|
||||
prompts = split_prompts(text)
|
||||
assert len(prompts) == 2
|
||||
assert prompts[0] == "First"
|
||||
assert prompts[1] == "Third"
|
||||
|
||||
def test_create_sampler_params_conditioning(self):
|
||||
"""Test creating conditioning dictionary."""
|
||||
prompts = ["prompt1", "prompt2"]
|
||||
encoded = [{"mock": "encoded1"}, {"mock": "encoded2"}]
|
||||
|
||||
result = create_sampler_params_conditioning(prompts, encoded)
|
||||
assert result["text"] == prompts
|
||||
assert result["encoded"] == encoded
|
||||
assert result["count"] == 2
|
||||
|
||||
def test_validate_prompt_format(self):
|
||||
"""Test prompt format validation."""
|
||||
assert validate_prompt_format("Valid prompt") == True
|
||||
assert validate_prompt_format("") == False
|
||||
assert validate_prompt_format(" ") == False
|
||||
|
||||
# Test very long prompt
|
||||
long_prompt = "a" * 10001
|
||||
assert validate_prompt_format(long_prompt) == False
|
||||
|
||||
def test_get_prompt_statistics(self):
|
||||
"""Test getting prompt statistics."""
|
||||
prompts = ["short", "medium prompt", "this is a longer prompt"]
|
||||
stats = get_prompt_statistics(prompts)
|
||||
|
||||
assert stats["count"] == 3
|
||||
assert stats["min_chars"] == 5
|
||||
assert stats["max_chars"] == 23
|
||||
assert stats["total_chars"] == 41
|
||||
|
||||
def test_get_prompt_statistics_empty(self):
|
||||
"""Test statistics with empty prompts."""
|
||||
stats = get_prompt_statistics([])
|
||||
assert stats["count"] == 0
|
||||
assert stats["total_chars"] == 0
|
||||
|
||||
|
||||
class TestTextEncodeSamplerParamsNode:
|
||||
"""Test the Text Encode Sampler Params node."""
|
||||
|
||||
@pytest.fixture
|
||||
def node(self):
|
||||
"""Create a node instance."""
|
||||
return TextEncodeSamplerParamsNode()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_clip(self):
|
||||
"""Create a mock CLIP encoder."""
|
||||
|
||||
class MockCLIP:
|
||||
pass
|
||||
|
||||
return MockCLIP()
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test that INPUT_TYPES returns correct structure."""
|
||||
input_types = TextEncodeSamplerParamsNode.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
|
||||
required = input_types["required"]
|
||||
assert "text" in required
|
||||
assert "clip" in required
|
||||
assert required["text"][0] == "STRING"
|
||||
assert required["clip"][0] == "CLIP"
|
||||
|
||||
def test_encode_prompts_single(self, node, mock_clip):
|
||||
"""Test encoding a single prompt."""
|
||||
text = "Single prompt without separator"
|
||||
result = node.encode_prompts(text, mock_clip)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 1
|
||||
conditioning = result[0]
|
||||
assert "text" in conditioning
|
||||
assert "encoded" in conditioning
|
||||
|
||||
def test_encode_prompts_multiple(self, node, mock_clip):
|
||||
"""Test encoding multiple prompts."""
|
||||
text = "First prompt\n---\nSecond prompt\n---\nThird prompt"
|
||||
result = node.encode_prompts(text, mock_clip)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
conditioning = result[0]
|
||||
assert len(conditioning["text"]) == 3
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node properties."""
|
||||
assert TextEncodeSamplerParamsNode.CATEGORY == "ComfyAssets/🧰 xyz-helpers"
|
||||
assert TextEncodeSamplerParamsNode.FUNCTION == "encode_prompts"
|
||||
assert TextEncodeSamplerParamsNode.RETURN_TYPES == ("CONDITIONING",)
|
||||
assert TextEncodeSamplerParamsNode.RETURN_NAMES == ("conditioning",)
|
||||
@@ -0,0 +1,133 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyAssets.DisplayAny",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "DisplayAny") {
|
||||
function populate(text) {
|
||||
// Remove existing display widgets
|
||||
if (this.widgets) {
|
||||
const toRemove = [];
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
if (this.widgets[i].name?.startsWith("display_") || this.widgets[i].name === "copy_value") {
|
||||
this.widgets[i].onRemove?.();
|
||||
toRemove.push(i);
|
||||
}
|
||||
}
|
||||
// Remove in reverse order to maintain indices
|
||||
for (let i = toRemove.length - 1; i >= 0; i--) {
|
||||
this.widgets.splice(toRemove[i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Split text into multiple widgets if it's too long or has multiple lines
|
||||
const lines = text ? text.split('\n') : [''];
|
||||
const maxLinesPerWidget = 20;
|
||||
const chunks = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i += maxLinesPerWidget) {
|
||||
chunks.push(lines.slice(i, i + maxLinesPerWidget).join('\n'));
|
||||
}
|
||||
|
||||
// Create a widget for each chunk
|
||||
chunks.forEach((chunk, index) => {
|
||||
const w = ComfyWidgets["STRING"](this, `display_${index}`, ["STRING", { multiline: true }], app).widget;
|
||||
w.inputEl.readOnly = true;
|
||||
w.inputEl.style.opacity = 0.8;
|
||||
w.inputEl.style.fontFamily = "monospace";
|
||||
w.value = chunk;
|
||||
});
|
||||
|
||||
// Add copy button widget
|
||||
const copyWidget = {
|
||||
type: "button",
|
||||
name: "copy_value",
|
||||
text: "📋 Copy Value",
|
||||
callback: () => {
|
||||
navigator.clipboard.writeText(text || '').then(() => {
|
||||
copyWidget.text = "✓ Copied!";
|
||||
this.setDirtyCanvas(true);
|
||||
setTimeout(() => {
|
||||
copyWidget.text = "📋 Copy Value";
|
||||
this.setDirtyCanvas(true);
|
||||
}, 1500);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.addCustomWidget(copyWidget);
|
||||
|
||||
// Update node title with condensed info
|
||||
const firstLine = text ? text.split('\n')[0] : '';
|
||||
const condensed = firstLine.length > 50
|
||||
? firstLine.substring(0, 50) + "..."
|
||||
: firstLine;
|
||||
this.title = `DisplayAny: ${condensed}`;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const sz = this.computeSize();
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0];
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1];
|
||||
}
|
||||
this.onResize?.(sz);
|
||||
app.graph.setDirtyCanvas(true, false);
|
||||
});
|
||||
}
|
||||
|
||||
// When the node is executed we will be sent the display text
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
console.log("DisplayAny onExecuted message:", message);
|
||||
|
||||
if (message?.text && message.text.length > 0) {
|
||||
const displayText = message.text[0];
|
||||
console.log("DisplayAny displayText:", displayText);
|
||||
populate.call(this, displayText);
|
||||
} else {
|
||||
console.log("DisplayAny no text in message");
|
||||
}
|
||||
};
|
||||
|
||||
const VALUES = Symbol();
|
||||
const configure = nodeType.prototype.configure;
|
||||
nodeType.prototype.configure = function () {
|
||||
this[VALUES] = arguments[0]?.widgets_values;
|
||||
return configure?.apply(this, arguments);
|
||||
};
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
onConfigure?.apply(this, arguments);
|
||||
const widgets_values = this[VALUES];
|
||||
if (widgets_values?.length) {
|
||||
requestAnimationFrame(() => {
|
||||
const startIdx = widgets_values.length > 1 && this.inputs?.[0]?.widget ? 1 : 0;
|
||||
if (widgets_values[startIdx]) {
|
||||
populate.call(this, widgets_values[startIdx]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on node creation
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Set minimum size - make it wider for better JSON display
|
||||
this.size[0] = Math.max(this.size[0], 450);
|
||||
this.size[1] = Math.max(this.size[1], 250);
|
||||
|
||||
// Add placeholder text
|
||||
populate.call(this, "Value will appear here...");
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyAssets.DisplayText",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "DisplayText") {
|
||||
function populate(text) {
|
||||
// Remove existing widgets
|
||||
if (this.widgets) {
|
||||
const toRemove = [];
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
if (this.widgets[i].name?.startsWith("text_") || this.widgets[i].name?.startsWith("copy_")) {
|
||||
this.widgets[i].onRemove?.();
|
||||
toRemove.push(i);
|
||||
}
|
||||
}
|
||||
// Remove in reverse order to maintain indices
|
||||
for (let i = toRemove.length - 1; i >= 0; i--) {
|
||||
this.widgets.splice(toRemove[i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the text to detect positive/negative prompt format
|
||||
const posMatch = text.match(/Positive prompt:\s*([\s\S]*?)(?=Negative prompt:|$)/i);
|
||||
const negMatch = text.match(/Negative prompt:\s*([\s\S]*?)(?=\*\*|$)/i);
|
||||
|
||||
if (posMatch && negMatch) {
|
||||
// Extract prompt content
|
||||
let positiveText = posMatch[1].trim();
|
||||
let negativeText = negMatch[1].trim();
|
||||
|
||||
// Remove any trailing ** markers
|
||||
const posEndIndex = positiveText.indexOf('**');
|
||||
if (posEndIndex > 0) {
|
||||
positiveText = positiveText.substring(0, posEndIndex).trim();
|
||||
}
|
||||
|
||||
const negEndIndex = negativeText.indexOf('**');
|
||||
if (negEndIndex > 0) {
|
||||
negativeText = negativeText.substring(0, negEndIndex).trim();
|
||||
}
|
||||
|
||||
// Create header widget for positive prompt
|
||||
const posHeader = ComfyWidgets["STRING"](this, "text_pos_header", ["STRING", { multiline: false }], app).widget;
|
||||
posHeader.inputEl.readOnly = true;
|
||||
posHeader.inputEl.style.opacity = 0.7;
|
||||
posHeader.inputEl.style.backgroundColor = "#2a4a2a";
|
||||
posHeader.inputEl.style.color = "#8f8";
|
||||
posHeader.inputEl.style.fontWeight = "bold";
|
||||
posHeader.value = "✓ Positive Prompt";
|
||||
|
||||
// Create positive prompt widget
|
||||
const posWidget = ComfyWidgets["STRING"](this, "text_positive", ["STRING", { multiline: true }], app).widget;
|
||||
posWidget.inputEl.readOnly = true;
|
||||
posWidget.inputEl.style.opacity = 0.9;
|
||||
posWidget.value = positiveText;
|
||||
|
||||
// Create copy button widget for positive prompt
|
||||
const posCopyWidget = {
|
||||
type: "button",
|
||||
name: "copy_positive",
|
||||
text: "📋 Copy Positive",
|
||||
callback: () => {
|
||||
navigator.clipboard.writeText(positiveText).then(() => {
|
||||
posCopyWidget.text = "✓ Copied!";
|
||||
this.setDirtyCanvas(true);
|
||||
setTimeout(() => {
|
||||
posCopyWidget.text = "📋 Copy Positive";
|
||||
this.setDirtyCanvas(true);
|
||||
}, 1500);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.addCustomWidget(posCopyWidget);
|
||||
|
||||
// Create header widget for negative prompt
|
||||
const negHeader = ComfyWidgets["STRING"](this, "text_neg_header", ["STRING", { multiline: false }], app).widget;
|
||||
negHeader.inputEl.readOnly = true;
|
||||
negHeader.inputEl.style.opacity = 0.7;
|
||||
negHeader.inputEl.style.backgroundColor = "#4a2a2a";
|
||||
negHeader.inputEl.style.color = "#f88";
|
||||
negHeader.inputEl.style.fontWeight = "bold";
|
||||
negHeader.value = "✗ Negative Prompt";
|
||||
|
||||
// Create negative prompt widget
|
||||
const negWidget = ComfyWidgets["STRING"](this, "text_negative", ["STRING", { multiline: true }], app).widget;
|
||||
negWidget.inputEl.readOnly = true;
|
||||
negWidget.inputEl.style.opacity = 0.9;
|
||||
negWidget.value = negativeText;
|
||||
|
||||
// Create copy button widget for negative prompt
|
||||
const negCopyWidget = {
|
||||
type: "button",
|
||||
name: "copy_negative",
|
||||
text: "📋 Copy Negative",
|
||||
callback: () => {
|
||||
navigator.clipboard.writeText(negativeText).then(() => {
|
||||
negCopyWidget.text = "✓ Copied!";
|
||||
this.setDirtyCanvas(true);
|
||||
setTimeout(() => {
|
||||
negCopyWidget.text = "📋 Copy Negative";
|
||||
this.setDirtyCanvas(true);
|
||||
}, 1500);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.addCustomWidget(negCopyWidget);
|
||||
|
||||
} else {
|
||||
// Single text display with ComfyUI's standard STRING widget
|
||||
const w = ComfyWidgets["STRING"](this, "text_display", ["STRING", { multiline: true }], app).widget;
|
||||
w.inputEl.readOnly = true;
|
||||
w.inputEl.style.opacity = 0.9;
|
||||
w.value = text;
|
||||
|
||||
// Create copy button widget
|
||||
const copyWidget = {
|
||||
type: "button",
|
||||
name: "copy_text",
|
||||
text: "📋 Copy",
|
||||
callback: () => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copyWidget.text = "✓ Copied!";
|
||||
this.setDirtyCanvas(true);
|
||||
setTimeout(() => {
|
||||
copyWidget.text = "📋 Copy";
|
||||
this.setDirtyCanvas(true);
|
||||
}, 1500);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.addCustomWidget(copyWidget);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const sz = this.computeSize();
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0];
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1];
|
||||
}
|
||||
this.onResize?.(sz);
|
||||
app.graph.setDirtyCanvas(true, false);
|
||||
});
|
||||
}
|
||||
|
||||
// When the node is executed we will be sent the input text
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
if (message?.text) {
|
||||
populate.call(this, message.text[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const VALUES = Symbol();
|
||||
const configure = nodeType.prototype.configure;
|
||||
nodeType.prototype.configure = function () {
|
||||
this[VALUES] = arguments[0]?.widgets_values;
|
||||
return configure?.apply(this, arguments);
|
||||
};
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
onConfigure?.apply(this, arguments);
|
||||
const widgets_values = this[VALUES];
|
||||
if (widgets_values?.length) {
|
||||
requestAnimationFrame(() => {
|
||||
const startIdx = widgets_values.length > 1 && this.inputs?.[0]?.widget ? 1 : 0;
|
||||
if (widgets_values[startIdx]) {
|
||||
populate.call(this, widgets_values[startIdx]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on node creation
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Set default size
|
||||
this.size[0] = Math.max(this.size[0], 400);
|
||||
this.size[1] = Math.max(this.size[1], 300);
|
||||
|
||||
// Add placeholder text
|
||||
populate.call(this, "Text will appear here after execution...");
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -26,6 +26,12 @@ app.registerExtension({
|
||||
// Style the button
|
||||
helpButton.serialize = false;
|
||||
|
||||
// Add refresh models button
|
||||
const refreshButton = this.addWidget("button", "Refresh Model List", null, () => {
|
||||
this.refreshModelList();
|
||||
});
|
||||
refreshButton.serialize = false;
|
||||
|
||||
// Add status indicator
|
||||
this.status = this.addWidget("text", "status", "Ready", () => {}, {
|
||||
serialize: false
|
||||
@@ -94,6 +100,28 @@ app.registerExtension({
|
||||
// For now, it's always visible but this method provides extensibility
|
||||
};
|
||||
|
||||
// Add method to refresh model list
|
||||
nodeType.prototype.refreshModelList = function() {
|
||||
if (this.status) {
|
||||
this.status.value = "Refreshing models...";
|
||||
}
|
||||
|
||||
// Set the refresh_models flag
|
||||
const refreshWidget = this.widgets.find(w => w.name === "refresh_models");
|
||||
if (refreshWidget) {
|
||||
refreshWidget.value = true;
|
||||
}
|
||||
|
||||
// Show message
|
||||
alert("Model list will refresh on next execution. Make sure API key is set and run the node.");
|
||||
|
||||
if (this.status) {
|
||||
setTimeout(() => {
|
||||
this.status.value = "Ready - Run node to refresh";
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Override execute to show status
|
||||
const onExecute = nodeType.prototype.onExecute;
|
||||
nodeType.prototype.onExecute = function() {
|
||||
|
||||