refactor/feat: modular rewrite w/ more nodes
This commit is contained in:
@@ -1,165 +1,331 @@
|
||||
# 🐓 ComfyUI Bawk Nodes
|
||||
<div align="center">
|
||||
<h1>🐓 ComfyUI Bawk Nodes v2.0.0</h1>
|
||||
|
||||
A collection of ComfyUI nodes **focused primarily on FLUX model workflows**. While some nodes may work with other diffusion models, all development and testing is done specifically with FLUX architectures to ensure optimal performance and compatibility.
|
||||
**A collection of FLUX-optimized ComfyUI nodes for efficient AI image generation.**
|
||||
|
||||
## ⚡ FLUX-First Design Philosophy
|
||||
</div>
|
||||
|
||||
Our nodes are built with FLUX models as the primary target:
|
||||
- **Optimized for FLUX**: All nodes designed around FLUX model architecture
|
||||
- **FLUX-Tested**: Extensive testing with FLUX Dev, FLUX Schnell, and FLUX variants
|
||||
- **Other Models**: May work with SDXL/SD1.5 but **not officially supported or tested**
|
||||

|
||||
|
||||
> **Note**: While these nodes might function with other diffusion models, we only guarantee compatibility and provide support for FLUX models. Use with other models at your own discretion.
|
||||
---
|
||||
|
||||
## Current Nodes
|
||||
## 🎯 **What's New in v2.0.0**
|
||||
|
||||
### 🚀 Diffusion Model Loader (Advanced)
|
||||
A powerful diffusion model loader specifically optimized for FLUX models with separate component loading.
|
||||
**Major Rewrite**: Complete FLUX-first redesign with modular architecture and workflow consolidation.
|
||||
|
||||
**FLUX-Optimized Features:**
|
||||
- **FLUX Model Support**: Loads models from `diffusion_models` directory (FLUX format)
|
||||
- **Separate Component Loading**: Independent VAE and dual CLIP text encoder support
|
||||
- **FLUX Weight Types**: Support for FP8, FP16, BF16, and FP32 optimized for FLUX
|
||||
- **FLUX CLIP Types**: Proper `CLIPType.FLUX` handling for T5 + CLIP-L encoders
|
||||
- **Model String Output**: Returns clean model name for workflow identification
|
||||
- **FLUX Validation**: Input validation designed around FLUX model requirements
|
||||
- 🎲 **Enhanced Wildcard Encoder** with 6 LoRA slots
|
||||
- 🐓 **All-in-One BawkSampler** with integrated VAE decoding
|
||||
- 📁 **Modular Architecture** for better maintainability
|
||||
- ⚡ **Streamlined Workflows** - fewer nodes, more power
|
||||
- 💾 **Enhanced Image Saver** with prompt saving
|
||||
|
||||
## 📦 Installation
|
||||
---
|
||||
|
||||
## 🚀 **Node Collection Overview**
|
||||
|
||||
| Node | Description | Category |
|
||||
|------|-------------|----------|
|
||||
| 🚀 **Diffusion Model Loader** | Advanced FLUX-optimized model loading | Loaders |
|
||||
| 🎲 **FLUX Wildcard Encoder** | Text encoding + 6 LoRA slots + wildcards | Conditioning |
|
||||
| 🐓 **Bawk Sampler** | All-in-one latent generation, sampling & VAE decode | Sampling |
|
||||
| 💾 **FLUX Image Saver** | Organized saving with metadata & prompt files | Image |
|
||||
| 📝 **FLUX Prompt Saver** | Standalone prompt archiving | Text |
|
||||
|
||||
---
|
||||
|
||||
## 🔥 **Complete FLUX Workflow**
|
||||
|
||||
**Before BawkNodes (5+ nodes):**
|
||||
```
|
||||
CheckpointLoader → LoraLoader → CLIPTextEncode → EmptyLatent → KSampler → VAEDecode → SaveImage
|
||||
```
|
||||
|
||||
**After BawkNodes (3 nodes):**
|
||||
```
|
||||
🚀 DiffusionModelLoader → 🎲 FluxWildcardEncode → 🐓 BawkSampler → 💾 FluxImageSaver
|
||||
```
|
||||
|
||||
**60% fewer nodes, 100% of the power!**
|
||||
|
||||
---
|
||||
|
||||
## 📦 **Installation**
|
||||
|
||||
### Method 1: ComfyUI Manager (Recommended)
|
||||
1. Open ComfyUI Manager
|
||||
2. Search for "Bawk Nodes" or "ComfyUI-BawkNodes"
|
||||
2. Search for "Bawk Nodes"
|
||||
3. Click Install
|
||||
4. Restart ComfyUI
|
||||
|
||||
### Method 2: Manual Installation
|
||||
1. Navigate to your ComfyUI custom nodes directory:
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/
|
||||
```
|
||||
|
||||
2. Clone this repository:
|
||||
```bash
|
||||
git clone https://github.com/juddisjudd/ComfyUI-BawkNodes.git
|
||||
```
|
||||
|
||||
3. Restart ComfyUI
|
||||
|
||||
## 🎯 Usage
|
||||
|
||||
### Diffusion Model Loader (Advanced)
|
||||
1. Add the "🚀 Diffusion Model Loader (Advanced)" node to your workflow
|
||||
2. Select your diffusion model from the dropdown
|
||||
3. Choose your VAE (or use "baked VAE" for none)
|
||||
4. Select your text encoders (CLIP models)
|
||||
5. Choose your preferred weight data type
|
||||
6. Connect the outputs to your workflow
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
#### Weight Data Types
|
||||
- **default**: Automatic selection based on hardware
|
||||
- **fp8_e4m3fn**: 8-bit floating point (requires modern GPUs)
|
||||
- **fp8_e4m3fn_fast**: Optimized 8-bit variant
|
||||
- **fp8_e5m2**: Alternative 8-bit format
|
||||
- **fp16**: 16-bit floating point (most common)
|
||||
- **bf16**: Brain floating point 16-bit
|
||||
- **fp32**: Full precision 32-bit
|
||||
|
||||
#### Separate VAE Loading
|
||||
- Select "baked VAE" to use the VAE included in your model
|
||||
- Choose a specific VAE file to override the model's VAE
|
||||
|
||||
#### Dual Text Encoders (FLUX Models)
|
||||
- **clip_name1**: First text encoder (typically T5 for FLUX)
|
||||
- **clip_name2**: Second text encoder (typically CLIP-L for FLUX)
|
||||
|
||||
## 🔧 Node Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `model_name` | STRING | - | Checkpoint file name (required) |
|
||||
| `weight_dtype` | COMBO | "default" | Weight data type |
|
||||
| `vae_name` | COMBO | "baked VAE" | VAE model (optional) |
|
||||
| `clip_name1` | COMBO | "none" | First text encoder (optional) |
|
||||
| `clip_name2` | COMBO | "none" | Second text encoder (optional) |
|
||||
|
||||
## 📤 Node Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `MODEL` | MODEL | Loaded diffusion model |
|
||||
| `VAE` | VAE | Variational autoencoder |
|
||||
| `CLIP` | CLIP | Text encoder(s) |
|
||||
| `MODEL_STRING` | STRING | Model information summary |
|
||||
|
||||
|
||||
## 🔮 Planned Nodes (FLUX-Focused)
|
||||
|
||||
***TBD***
|
||||
*All future nodes will maintain our FLUX-first design philosophy*
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### Basic FLUX Workflow
|
||||
```
|
||||
Diffusion Model Loader (Advanced)
|
||||
├── model_name: "flux1-dev.safetensors"
|
||||
├── weight_dtype: "fp8_e4m3fn"
|
||||
├── vae_name: "ae.safetensors"
|
||||
├── clip_name1: "t5xxl_fp16.safetensors"
|
||||
└── clip_name2: "clip_l.safetensors"
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/juddisjudd/ComfyUI-BawkNodes.git
|
||||
# Restart ComfyUI
|
||||
```
|
||||
|
||||
## ⚡ FLUX Performance Tips
|
||||
---
|
||||
|
||||
1. **Hardware Optimization**:
|
||||
- Use FP8 data types on RTX 4000+ series GPUs for maximum VRAM efficiency
|
||||
- FLUX models benefit significantly from modern GPU architectures
|
||||
- Ensure adequate VRAM (12GB+ recommended for FLUX Dev)
|
||||
## 🎲 **Node Details**
|
||||
|
||||
2. **FLUX-Specific Settings**:
|
||||
- Use `fp8_e4m3fn` for best quality/memory balance
|
||||
- Use `fp8_e4m3fn_fast` for maximum speed
|
||||
- T5 + CLIP-L combination provides optimal text understanding
|
||||
### 🚀 **Diffusion Model Loader (Advanced)**
|
||||
|
||||
3. **Model Organization**:
|
||||
- Keep FLUX models in `ComfyUI/models/diffusion_models/`
|
||||
- Use `ae.safetensors` VAE for all FLUX variants
|
||||
- Separate text encoders allow better memory management
|
||||
**FLUX-optimized model loading with advanced features.**
|
||||
|
||||
## Troubleshooting
|
||||
**Features:**
|
||||
- Multiple model formats (FLUX, SDXL, SD1.5)
|
||||
- Flexible weight data types (fp8, fp16, bf16, fp32)
|
||||
- Separate VAE and CLIP loading
|
||||
- Multiple directory support
|
||||
|
||||
### FLUX-Specific Issues
|
||||
**Inputs:**
|
||||
- `model_name` - Model from diffusion_models folder
|
||||
- `vae_name` - VAE or "baked VAE"
|
||||
- `clip_name1/2` - CLIP models for FLUX
|
||||
- `weight_dtype` - Precision optimization
|
||||
|
||||
**"Model not found"**
|
||||
- Ensure FLUX models are in `ComfyUI/models/diffusion_models/` (NOT checkpoints!)
|
||||
- VAE files go in `ComfyUI/models/vae/`
|
||||
- Text encoders go in `ComfyUI/models/text_encoders/` or `ComfyUI/models/clip/`
|
||||
**Outputs:** `MODEL`, `VAE`, `CLIP`, `MODEL_STRING`
|
||||
|
||||
**Matrix multiplication errors with FLUX samplers**
|
||||
- This usually means a checkpoint was loaded instead of a diffusion model
|
||||
- Ensure your FLUX model is in `diffusion_models` directory
|
||||
- Our loader is specifically designed to prevent this issue
|
||||
---
|
||||
|
||||
**"Insufficient VRAM" with FLUX**
|
||||
- Try `fp8_e4m3fn` or `fp8_e4m3fn_fast` weight types
|
||||
- FLUX models are large - consider using smaller variants for lower VRAM
|
||||
- Ensure no other models are loaded in memory
|
||||
### 🎲 **FLUX Wildcard Encoder**
|
||||
|
||||
**Empty dropdowns**
|
||||
- The node only shows files that actually exist in the correct directories
|
||||
- Check that your FLUX files are in the proper locations
|
||||
- This loader ONLY shows diffusion models (FLUX format)
|
||||
**Enhanced text encoder with 6 LoRA slots and wildcard support.**
|
||||
|
||||
**Not compatible with other models**
|
||||
- Remember: These nodes are designed specifically for FLUX
|
||||
- Other diffusion models may not work correctly
|
||||
- Use standard ComfyUI loaders for non-FLUX models
|
||||
**Features:**
|
||||
- **Wildcard Processing**: `{option1|option2|option3}` syntax
|
||||
- **6 LoRA Slots**: Individual enable/disable toggles
|
||||
- **Fuzzy LoRA Matching**: Flexible file resolution
|
||||
- **FLUX Optimization**: 16-channel conditioning
|
||||
|
||||
**Inputs:**
|
||||
- `model`, `clip` - From model loader
|
||||
- `prompt` - Text with wildcard support
|
||||
- `wildcard_seed` - Seed for consistent wildcard selection
|
||||
- `lora_X_on` - Enable/disable each LoRA (X = 1-6)
|
||||
- `lora_X_name` - LoRA selection dropdown
|
||||
- `lora_X_strength` - Strength adjustment (-10.0 to +10.0)
|
||||
|
||||
## Support
|
||||
**Outputs:** `MODEL`, `CLIP`, `CONDITIONING`, `PROMPT_OUT`
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/juddisjudd/ComfyUI-BawkNodes/issues)
|
||||
**Example Prompt with Wildcards:**
|
||||
```
|
||||
A {beautiful|stunning|gorgeous} {cat|dog|bird} in a {forest|garden|meadow},
|
||||
{photorealistic|artistic|stylized} style
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🐓 **Bawk Sampler (All-in-One)**
|
||||
|
||||
**Complete latent generation, sampling, and VAE decoding in one node.**
|
||||
|
||||
**Features:**
|
||||
- **Smart Resolution Presets**: Pre-configured FLUX-optimized resolutions
|
||||
- **Custom Resolution Support**: Manual width/height with 64px alignment
|
||||
- **Advanced FLUX Sampling**: All FLUX-specific parameters
|
||||
- **Integrated VAE Decoding**: Direct image output
|
||||
- **Batch Generation**: Up to 64 images at once
|
||||
|
||||
**Key Inputs:**
|
||||
- `model`, `conditioning`, `vae` - From previous nodes
|
||||
- `resolution` - Smart presets or custom
|
||||
- `batch_size` - Number of images (default: 4)
|
||||
- `sampler` - Sampling method (default: euler)
|
||||
- `scheduler` - Noise schedule (default: beta)
|
||||
- `steps` - Sampling steps (default: 30)
|
||||
- `guidance` - FLUX guidance scale (default: 3.5)
|
||||
- `max_shift` - FLUX max shift (default: 0.5)
|
||||
- `base_shift` - FLUX base shift (default: 0.3)
|
||||
|
||||
**Resolution Presets:**
|
||||
- `FHD 16:9 - 1920x1080` (default)
|
||||
- `Medium Square - 1024x1024`
|
||||
- `Portrait 9:16 - 1080x1920`
|
||||
- `Ultra-wide - 1792x768`
|
||||
- And many more...
|
||||
|
||||
**Outputs:** `IMAGE`, `LATENT`
|
||||
|
||||
---
|
||||
|
||||
### 💾 **FLUX Image Saver**
|
||||
|
||||
**Organized image saving with metadata and prompt archiving.**
|
||||
|
||||
**Features:**
|
||||
- **Smart Folder Organization**: `[MODEL]-DD-MM-YYYY` structure
|
||||
- **Multiple Formats**: PNG, JPG, WebP support
|
||||
- **Metadata Embedding**: PNG metadata support
|
||||
- **Prompt File Saving**: Separate `.txt` files with processed prompts
|
||||
- **JSON Metadata**: Complete generation parameters
|
||||
|
||||
**Inputs:**
|
||||
- `images` - From BawkSampler
|
||||
- `model_string` - From model loader
|
||||
- `processed_prompt` - From wildcard encoder
|
||||
- `save_prompt` - Enable prompt file saving (default: True)
|
||||
- `format` - Image format (PNG/JPG/WebP)
|
||||
- `quality` - Compression quality (1-100)
|
||||
|
||||
**File Output Example:**
|
||||
```
|
||||
ComfyUI/output/[FLUX_Model]-01-08-2025/
|
||||
├── flux_image_01-08-2025_14-30-15_001.png
|
||||
├── flux_image_01-08-2025_14-30-15_002.png
|
||||
├── flux_image_01-08-2025_14-30-15_prompt.txt
|
||||
├── flux_image_01-08-2025_14-30-15_001_metadata.json
|
||||
└── flux_image_01-08-2025_14-30-15_002_metadata.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📝 **FLUX Prompt Saver**
|
||||
|
||||
**Standalone prompt and parameter archiving.**
|
||||
|
||||
**Features:**
|
||||
- **JSON Format**: Structured data storage
|
||||
- **Complete Parameters**: All generation settings
|
||||
- **Organized Storage**: Matches image saver folder structure
|
||||
- **Workflow Integration**: Links with other BawkNodes
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Advanced Usage**
|
||||
|
||||
### **Wildcard Examples**
|
||||
|
||||
**Basic Wildcards:**
|
||||
```
|
||||
A {red|blue|green} car in the {city|countryside}
|
||||
```
|
||||
|
||||
**Nested Concepts:**
|
||||
```
|
||||
{A majestic|An elegant|A powerful} {dragon|phoenix|griffin}
|
||||
{soaring through|perched upon|emerging from} {clouds|mountains|flames}
|
||||
```
|
||||
|
||||
**Style Variations:**
|
||||
```
|
||||
Portrait of a woman, {photorealistic|oil painting|digital art|watercolor} style,
|
||||
{studio lighting|natural lighting|dramatic lighting}
|
||||
```
|
||||
|
||||
### **LoRA Management**
|
||||
|
||||
**Best Practices:**
|
||||
1. **Enable LoRAs individually** for precise control
|
||||
2. **Use strength between 0.5-1.5** for most LoRAs
|
||||
3. **Combine complementary LoRAs** (style + subject)
|
||||
4. **Test different combinations** for unique results
|
||||
|
||||
**Example LoRA Setup:**
|
||||
- LoRA 1: `realistic_skin_v2.safetensors` (0.8)
|
||||
- LoRA 2: `dramatic_lighting.safetensors` (0.6)
|
||||
- LoRA 3: `detail_enhancer.safetensors` (0.4)
|
||||
|
||||
### **Resolution Guidelines**
|
||||
|
||||
**Recommended Presets:**
|
||||
- **Square**: `Medium Square - 1024x1024`
|
||||
- **Landscape**: `FHD 16:9 - 1920x1080`
|
||||
- **Portrait**: `Portrait 9:16 - 1080x1920`
|
||||
- **Widescreen**: `Ultra-wide - 1792x768`
|
||||
|
||||
**Custom Resolution Rules:**
|
||||
- Must be multiples of 64 pixels
|
||||
- Keep total pixel count reasonable (<4MP for speed)
|
||||
- Consider VRAM limitations for large batches
|
||||
|
||||
---
|
||||
|
||||
## 🔧 **Configuration**
|
||||
|
||||
### **Model Setup**
|
||||
|
||||
1. **FLUX Models**: Place in `models/diffusion_models/`
|
||||
2. **VAE Files**: Place in `models/vae/`
|
||||
3. **CLIP Models**: Place in `models/text_encoders/`
|
||||
4. **LoRA Files**: Place in `models/loras/`
|
||||
|
||||
### **Recommended Settings**
|
||||
|
||||
**For Speed:**
|
||||
- Resolution: `Medium Square - 1024x1024`
|
||||
- Batch Size: `4`
|
||||
- Steps: `20-25`
|
||||
- Sampler: `euler`
|
||||
|
||||
**For Quality:**
|
||||
- Resolution: `FHD 16:9 - 1920x1080`
|
||||
- Batch Size: `1-2`
|
||||
- Steps: `30-40`
|
||||
- Sampler: `dpmpp_2m`
|
||||
|
||||
**For Experimentation:**
|
||||
- Use wildcards with high variation
|
||||
- Enable multiple LoRAs
|
||||
- Try different guidance scales (2.0-5.0)
|
||||
|
||||
---
|
||||
|
||||
## **Troubleshooting**
|
||||
|
||||
### **Common Issues**
|
||||
|
||||
**Node Not Appearing:**
|
||||
```bash
|
||||
# Check ComfyUI console for errors
|
||||
# Ensure all files are in correct directories
|
||||
# Restart ComfyUI completely
|
||||
```
|
||||
|
||||
**LoRA Not Loading:**
|
||||
- Check file is in `models/loras/`
|
||||
- Verify file isn't corrupted
|
||||
- Check console for specific error messages
|
||||
|
||||
**Memory Issues:**
|
||||
- Reduce batch size
|
||||
- Use lower resolution
|
||||
- Enable `fp8` weight dtype in loader
|
||||
|
||||
**Generation Errors:**
|
||||
- Verify all connections are correct
|
||||
- Check that VAE is connected to BawkSampler
|
||||
- Ensure CLIP and MODEL are from same loader
|
||||
|
||||
### **Performance Optimization**
|
||||
|
||||
**VRAM Usage:**
|
||||
- Use `fp8_e4m3fn_fast` for weight dtype
|
||||
- Reduce batch size for large images
|
||||
- Close other GPU applications
|
||||
|
||||
**Speed Improvements:**
|
||||
- Use `euler` sampler with `beta` scheduler
|
||||
- Reduce step count (20-30 is often sufficient)
|
||||
- Use medium resolution presets
|
||||
|
||||
---
|
||||
|
||||
## 📄 **License**
|
||||
|
||||
GPL-3.0 license - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 **Acknowledgments**
|
||||
- **[rgthree](https://github.com/rgthree/rgthree-comfy)** - Inspiration for dynamic UI patterns
|
||||
|
||||
---
|
||||
|
||||
## 🔗 **Links**
|
||||
|
||||
- **Comfy Registry**: [Comfy-Registry](https://registry.comfy.org/publishers/judd/nodes/comfyui-bawknodes)
|
||||
- **Issues**: [Report Bugs](https://github.com/juddisjudd/ComfyUI-BawkNodes/issues)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+23
-9
@@ -3,29 +3,43 @@ ComfyUI Bawk Nodes - Node Registration
|
||||
File: __init__.py
|
||||
"""
|
||||
|
||||
from .nodes import DiffusionModelLoader
|
||||
from .nodes import (
|
||||
DiffusionModelLoader,
|
||||
FluxImageSaver,
|
||||
FluxPromptSaver,
|
||||
FluxWildcardEncode,
|
||||
BawkSampler
|
||||
)
|
||||
|
||||
# Node class mappings for ComfyUI discovery
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DiffusionModelLoader": DiffusionModelLoader,
|
||||
"FluxImageSaver": FluxImageSaver,
|
||||
"FluxPromptSaver": FluxPromptSaver,
|
||||
"FluxWildcardEncode": FluxWildcardEncode,
|
||||
"BawkSampler": BawkSampler,
|
||||
}
|
||||
|
||||
# Display names in the ComfyUI interface
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DiffusionModelLoader": "🚀 Diffusion Model Loader (Advanced)",
|
||||
"FluxImageSaver": "💾 FLUX Image Saver",
|
||||
"FluxPromptSaver": "📝 FLUX Prompt Saver",
|
||||
"FluxWildcardEncode": "🎲 FLUX Wildcard Encoder",
|
||||
"BawkSampler": "🐓 Bawk Sampler (All-in-One)",
|
||||
}
|
||||
|
||||
# Export for ComfyUI
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
# Package metadata
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "2.0.0"
|
||||
__author__ = "Bawk Nodes"
|
||||
__description__ = "A collection of useful ComfyUI nodes for enhanced workflows"
|
||||
__description__ = "A complete collection of FLUX-optimized ComfyUI nodes for enhanced workflows"
|
||||
|
||||
# Optional: Print loading message
|
||||
print(f"🐓 ComfyUI Bawk Nodes v{__version__} loaded successfully!")
|
||||
print(" 🎉 Major Update - Complete FLUX Workflow Suite!")
|
||||
print(" Current nodes:")
|
||||
print(" • 🚀 Diffusion Model Loader (Advanced) - FLUX-optimized model loading")
|
||||
print(" • More nodes coming soon...")
|
||||
print(" • 🎲 FLUX Wildcard Encoder - Text encoding with wildcard support and 6 LoRA slots")
|
||||
print(" • 💾 FLUX Image Saver - Organized image saving with metadata")
|
||||
print(" • 📝 FLUX Prompt Saver - Save prompts and generation parameters")
|
||||
print(" • 🐓 Bawk Sampler (All-in-One) - Combined latent optimizer and sampler")
|
||||
print(" 📦 Modular architecture for easy maintenance and debugging")
|
||||
print(" Visit: https://github.com/juddisjudd/ComfyUI-BawkNodes")
|
||||
@@ -1,339 +1,18 @@
|
||||
"""
|
||||
ComfyUI Bawk Nodes - Diffusion Model Loader
|
||||
ComfyUI Bawk Nodes - Main nodes file (clean imports only)
|
||||
File: nodes.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import torch
|
||||
import comfy.sd
|
||||
import comfy.model_management as mm
|
||||
import comfy.utils
|
||||
import folder_paths
|
||||
from typing import Optional, Tuple, Dict, Any, Union
|
||||
from .modules.validation import ValidationMixin
|
||||
from .nodes.diffusion_model_loader import DiffusionModelLoader
|
||||
from .nodes.flux_wildcard_encode import FluxWildcardEncode
|
||||
from .nodes.flux_image_saver import FluxImageSaver
|
||||
from .nodes.flux_prompt_saver import FluxPromptSaver
|
||||
from .nodes.bawk_sampler import BawkSampler
|
||||
|
||||
|
||||
class DiffusionModelLoader(ValidationMixin):
|
||||
"""
|
||||
Advanced Diffusion Model Loader with support for:
|
||||
- Multiple model formats (FLUX, SDXL, SD1.5)
|
||||
- Flexible weight data types (fp8, fp16, bf16, fp32)
|
||||
- Separate VAE and CLIP loading
|
||||
- Multiple directory support
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# Match the exact format of the working DoomFluxLoader
|
||||
return {
|
||||
"required": {
|
||||
"model_name": (folder_paths.get_filename_list("diffusion_models"),),
|
||||
"vae_name": (folder_paths.get_filename_list("vae") + ["baked VAE"],),
|
||||
"clip_name1": (folder_paths.get_filename_list("text_encoders") + ["none"],),
|
||||
"clip_name2": (folder_paths.get_filename_list("text_encoders") + ["none"],),
|
||||
"weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "VAE", "CLIP", "STRING")
|
||||
RETURN_NAMES = ("MODEL", "VAE", "CLIP", "MODEL_STRING")
|
||||
FUNCTION = "load_diffusion_model"
|
||||
CATEGORY = "BawkNodes/loaders"
|
||||
DESCRIPTION = "Advanced diffusion model loader for FLUX and other modern architectures"
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
"""Simple validation matching the working loader pattern"""
|
||||
errors = []
|
||||
|
||||
# Validate model exists in diffusion_models directory
|
||||
model_name = kwargs.get("model_name")
|
||||
if model_name:
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
||||
except:
|
||||
errors.append(f"Model '{model_name}' not found in diffusion_models directory")
|
||||
|
||||
# Validate VAE if specified
|
||||
vae_name = kwargs.get("vae_name", "baked VAE")
|
||||
if vae_name and vae_name != "baked VAE":
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("vae", vae_name)
|
||||
except:
|
||||
errors.append(f"VAE '{vae_name}' not found in vae directory")
|
||||
|
||||
# Validate CLIP models if specified
|
||||
clip1 = kwargs.get("clip_name1", "none")
|
||||
if clip1 and clip1 != "none":
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("text_encoders", clip1)
|
||||
except:
|
||||
errors.append(f"CLIP model '{clip1}' not found in text_encoders directory")
|
||||
|
||||
clip2 = kwargs.get("clip_name2", "none")
|
||||
if clip2 and clip2 != "none":
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("text_encoders", clip2)
|
||||
except:
|
||||
errors.append(f"CLIP model '{clip2}' not found in text_encoders directory")
|
||||
|
||||
return "; ".join(errors) if errors else True
|
||||
|
||||
def load_diffusion_model(
|
||||
self,
|
||||
model_name: str,
|
||||
weight_dtype: str = "default",
|
||||
vae_name: str = "baked VAE",
|
||||
clip_name1: str = "none",
|
||||
clip_name2: str = "none"
|
||||
) -> Tuple[Any, Any, Any, str]:
|
||||
"""
|
||||
Main loading function for diffusion models (based on working DoomFluxLoader)
|
||||
"""
|
||||
try:
|
||||
# Handle "none" and "baked VAE" values like the working loader
|
||||
vae_name = vae_name if vae_name and vae_name != "baked VAE" else None
|
||||
clip_name1 = clip_name1 if clip_name1 and clip_name1 != "none" else None
|
||||
clip_name2 = clip_name2 if clip_name2 and clip_name2 != "none" else None
|
||||
weight_dtype = weight_dtype if weight_dtype else "default"
|
||||
|
||||
# Set up model options based on weight_dtype (like DoomFluxLoader)
|
||||
model_options = {}
|
||||
if weight_dtype == "fp8_e4m3fn":
|
||||
model_options["dtype"] = torch.float8_e4m3fn
|
||||
elif weight_dtype == "fp8_e4m3fn_fast":
|
||||
model_options["dtype"] = torch.float8_e4m3fn
|
||||
model_options["fp8_optimizations"] = True
|
||||
elif weight_dtype == "fp8_e5m2":
|
||||
model_options["dtype"] = torch.float8_e5m2
|
||||
|
||||
# Load diffusion model using the exact same method as DoomFluxLoader
|
||||
print(f"[DiffusionModelLoader] Loading diffusion model: {model_name}")
|
||||
model_path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
||||
model = comfy.sd.load_diffusion_model(model_path, model_options=model_options)
|
||||
|
||||
if model is None:
|
||||
raise RuntimeError("Failed to load diffusion model - output was None")
|
||||
|
||||
# Load VAE (exactly like DoomFluxLoader)
|
||||
vae = None
|
||||
if vae_name:
|
||||
print(f"[DiffusionModelLoader] Loading VAE: {vae_name}")
|
||||
vae_path = folder_paths.get_full_path_or_raise("vae", vae_name)
|
||||
vae_sd = comfy.utils.load_torch_file(vae_path)
|
||||
vae = comfy.sd.VAE(sd=vae_sd)
|
||||
|
||||
# Load CLIP models (exactly like DoomFluxLoader)
|
||||
clip_paths = []
|
||||
if clip_name1:
|
||||
clip_paths.append(folder_paths.get_full_path_or_raise("text_encoders", clip_name1))
|
||||
if clip_name2:
|
||||
clip_paths.append(folder_paths.get_full_path_or_raise("text_encoders", clip_name2))
|
||||
|
||||
clip = None
|
||||
if clip_paths:
|
||||
print(f"[DiffusionModelLoader] Loading CLIP models")
|
||||
clip = comfy.sd.load_clip(
|
||||
ckpt_paths=clip_paths,
|
||||
embedding_directory=folder_paths.get_folder_paths("embeddings"),
|
||||
clip_type=comfy.sd.CLIPType.FLUX
|
||||
)
|
||||
|
||||
# Generate simple model name string
|
||||
model_string = os.path.splitext(model_name)[0]
|
||||
|
||||
print(f"[DiffusionModelLoader] Successfully loaded diffusion model: {model_string}")
|
||||
return (model, vae, clip, model_string)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to load diffusion model '{model_name}': {str(e)}"
|
||||
print(f"[DiffusionModelLoader] Error: {error_msg}")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
def _create_cache_key(self, model_name: str, weight_dtype: str, vae_name: str,
|
||||
clip_name1: str, clip_name2: str) -> str:
|
||||
"""Create unique cache key for this configuration"""
|
||||
return f"{model_name}|{weight_dtype}|{vae_name}|{clip_name1}|{clip_name2}"
|
||||
|
||||
def _validate_model_path(self, model_name: str) -> str:
|
||||
"""Validate model exists and is accessible, checking multiple directories"""
|
||||
model_path = None
|
||||
|
||||
# Try checkpoints directory first
|
||||
model_path = folder_paths.get_full_path("checkpoints", model_name)
|
||||
|
||||
# If not found, try diffusion_models directory
|
||||
if model_path is None:
|
||||
model_path = folder_paths.get_full_path("diffusion_models", model_name)
|
||||
|
||||
if model_path is None:
|
||||
raise FileNotFoundError(f"Model not found in checkpoints or diffusion_models directories: {model_name}")
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
raise FileNotFoundError(f"Model file does not exist: {model_path}")
|
||||
|
||||
# Basic file size validation
|
||||
file_size = os.path.getsize(model_path)
|
||||
if file_size < 1024 * 1024: # Less than 1MB
|
||||
raise ValueError(f"Model file appears corrupted (too small): {model_path}")
|
||||
|
||||
return model_path
|
||||
|
||||
def _select_dtype(self, weight_dtype: str, device: torch.device) -> torch.dtype:
|
||||
"""Select appropriate data type based on user preference and hardware"""
|
||||
if weight_dtype == "default":
|
||||
return self._auto_select_dtype(device)
|
||||
|
||||
dtype_map = {
|
||||
"fp32": torch.float32,
|
||||
"fp16": torch.float16,
|
||||
"bf16": torch.bfloat16,
|
||||
}
|
||||
|
||||
# Handle FP8 types if supported
|
||||
if weight_dtype.startswith("fp8_"):
|
||||
if self._supports_fp8(device):
|
||||
if weight_dtype == "fp8_e4m3fn":
|
||||
return torch.float8_e4m3fn
|
||||
elif weight_dtype == "fp8_e5m2":
|
||||
return torch.float8_e5m2
|
||||
else:
|
||||
print(f"FP8 not supported on device {device}, falling back to fp16")
|
||||
return torch.float16
|
||||
|
||||
selected_dtype = dtype_map.get(weight_dtype, torch.float32)
|
||||
|
||||
# Validate dtype compatibility
|
||||
if not self.validate_dtype_compatibility(weight_dtype, device):
|
||||
print(f"Dtype {weight_dtype} not compatible with {device}, using auto selection")
|
||||
return self._auto_select_dtype(device)
|
||||
|
||||
return selected_dtype
|
||||
|
||||
def _auto_select_dtype(self, device: torch.device) -> torch.dtype:
|
||||
"""Automatically select best dtype for device"""
|
||||
if self._supports_fp8(device):
|
||||
free_memory = mm.get_free_memory(device)
|
||||
if free_memory < 6 * 1024**3: # Less than 6GB VRAM
|
||||
return torch.float8_e4m3fn
|
||||
|
||||
if mm.should_use_fp16(device):
|
||||
return torch.float16
|
||||
|
||||
if device.type == "cuda" and torch.cuda.is_bf16_supported():
|
||||
return torch.bfloat16
|
||||
|
||||
return torch.float32
|
||||
|
||||
def _supports_fp8(self, device: torch.device) -> bool:
|
||||
"""Check if device supports FP8 computation"""
|
||||
if device.type != "cuda":
|
||||
return False
|
||||
|
||||
try:
|
||||
compute_capability = torch.cuda.get_device_capability(device)
|
||||
return compute_capability >= (8, 9) # Ada Lovelace or newer
|
||||
except:
|
||||
return False
|
||||
|
||||
def _load_checkpoint_components(
|
||||
self, model_path: str, dtype: torch.dtype, device: torch.device
|
||||
) -> Tuple[Any, Any, Any]:
|
||||
"""Load main checkpoint components using ComfyUI's standard API"""
|
||||
try:
|
||||
# Use ComfyUI's memory management
|
||||
mm.soft_empty_cache() # Clear cache before loading
|
||||
|
||||
# Use the same API as CheckpointLoaderSimple
|
||||
out = comfy.sd.load_checkpoint_guess_config(
|
||||
model_path,
|
||||
output_vae=True,
|
||||
output_clip=True,
|
||||
embedding_directory=folder_paths.get_folder_paths("embeddings")
|
||||
)
|
||||
|
||||
if out is None or len(out) < 3:
|
||||
raise RuntimeError("Failed to load checkpoint components")
|
||||
|
||||
return out[:3] # model, clip, vae
|
||||
|
||||
except Exception as e:
|
||||
if "Could not detect model type" in str(e):
|
||||
raise RuntimeError(
|
||||
f"Unsupported model format. This may be a FLUX model that requires "
|
||||
f"separate component loading. Try using individual loaders for "
|
||||
f"UNet/CLIP/VAE components."
|
||||
)
|
||||
raise RuntimeError(f"Error loading checkpoint: {str(e)}")
|
||||
|
||||
def _load_separate_vae(self, vae_name: str):
|
||||
"""Load separate VAE component"""
|
||||
vae_path = folder_paths.get_full_path("vae", vae_name)
|
||||
if vae_path is None:
|
||||
raise FileNotFoundError(f"VAE not found: {vae_name}")
|
||||
|
||||
try:
|
||||
# Use ComfyUI's standard VAE loading
|
||||
vae_sd = comfy.utils.load_torch_file(vae_path, safe_load=True)
|
||||
vae = comfy.sd.VAE(sd=vae_sd)
|
||||
return vae
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to load VAE '{vae_name}': {str(e)}")
|
||||
|
||||
def _load_custom_clip(self, clip_name1: str, clip_name2: str):
|
||||
"""Load custom CLIP models (for FLUX dual text encoders)"""
|
||||
clip_paths = []
|
||||
|
||||
if clip_name1 != "none":
|
||||
# Try clip directory first, then text_encoders
|
||||
clip_path1 = folder_paths.get_full_path("clip", clip_name1)
|
||||
if clip_path1 is None:
|
||||
clip_path1 = folder_paths.get_full_path("text_encoders", clip_name1)
|
||||
|
||||
if clip_path1 is None:
|
||||
raise FileNotFoundError(f"CLIP model not found in clip or text_encoders directories: {clip_name1}")
|
||||
clip_paths.append(clip_path1)
|
||||
|
||||
if clip_name2 != "none":
|
||||
# Try clip directory first, then text_encoders
|
||||
clip_path2 = folder_paths.get_full_path("clip", clip_name2)
|
||||
if clip_path2 is None:
|
||||
clip_path2 = folder_paths.get_full_path("text_encoders", clip_name2)
|
||||
|
||||
if clip_path2 is None:
|
||||
raise FileNotFoundError(f"CLIP model not found in clip or text_encoders directories: {clip_name2}")
|
||||
clip_paths.append(clip_path2)
|
||||
|
||||
if not clip_paths:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Use ComfyUI's standard CLIP loading
|
||||
clip = comfy.sd.load_clip(
|
||||
ckpt_paths=clip_paths,
|
||||
embedding_directory=folder_paths.get_folder_paths("embeddings")
|
||||
)
|
||||
return clip
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to load CLIP models: {str(e)}")
|
||||
|
||||
def _generate_model_info(
|
||||
self, model_name: str, dtype: torch.dtype, device: torch.device,
|
||||
vae_name: str, clip_name1: str, clip_name2: str
|
||||
) -> str:
|
||||
"""Generate model name string (just the base model name without extension)"""
|
||||
# Extract base name without extension
|
||||
base_name = os.path.splitext(model_name)[0]
|
||||
return base_name
|
||||
|
||||
def get_model_string_preview(self, model_name: str, weight_dtype: str = "default",
|
||||
vae_name: str = "baked VAE", clip_name1: str = "none",
|
||||
clip_name2: str = "none") -> str:
|
||||
"""
|
||||
Preview what the model string output will look like.
|
||||
Returns just the base model name without extension.
|
||||
"""
|
||||
try:
|
||||
return os.path.splitext(model_name)[0]
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
__all__ = [
|
||||
"DiffusionModelLoader",
|
||||
"FluxWildcardEncode",
|
||||
"FluxImageSaver",
|
||||
"FluxPromptSaver",
|
||||
"BawkSampler"
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Nodes package initialization
|
||||
File: nodes/__init__.py
|
||||
"""
|
||||
|
||||
from .diffusion_model_loader import DiffusionModelLoader
|
||||
from .flux_wildcard_encode import FluxWildcardEncode
|
||||
from .flux_image_saver import FluxImageSaver
|
||||
from .flux_prompt_saver import FluxPromptSaver
|
||||
from .bawk_sampler import BawkSampler
|
||||
|
||||
__all__ = [
|
||||
"DiffusionModelLoader",
|
||||
"FluxWildcardEncode",
|
||||
"FluxImageSaver",
|
||||
"FluxPromptSaver",
|
||||
"BawkSampler"
|
||||
]
|
||||
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
BawkSampler - Combined Latent Generator and Advanced Sampler for FLUX
|
||||
File: nodes/bawk_sampler.py
|
||||
"""
|
||||
|
||||
import torch
|
||||
import math
|
||||
import time
|
||||
import logging
|
||||
import comfy.samplers
|
||||
import comfy.model_management as mm
|
||||
from comfy.utils import ProgressBar
|
||||
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 typing import Tuple, Dict, Any, List
|
||||
|
||||
|
||||
def round_to_nearest_multiple(value, multiple):
|
||||
"""Rounds a value to the nearest multiple of 'multiple'."""
|
||||
if multiple <= 0:
|
||||
return value
|
||||
return int(round(value / multiple) * multiple)
|
||||
|
||||
|
||||
def parse_string_to_list(input_string):
|
||||
"""Parse comma/newline separated string to list of numbers"""
|
||||
try:
|
||||
if not input_string:
|
||||
return []
|
||||
items = input_string.replace('\n', ',').split(',')
|
||||
result = []
|
||||
for item in items:
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
try:
|
||||
num = float(item)
|
||||
if num.is_integer():
|
||||
num = int(num)
|
||||
result.append(num)
|
||||
except ValueError:
|
||||
continue
|
||||
return result
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def conditioning_set_values(conditioning, values):
|
||||
"""Set conditioning values like guidance scale"""
|
||||
c = []
|
||||
for t in conditioning:
|
||||
n = [t[0], t[1].copy()]
|
||||
for k, v in values.items():
|
||||
if k == "guidance":
|
||||
n[1]['guidance_scale'] = v
|
||||
c.append(tuple(n))
|
||||
return c
|
||||
|
||||
|
||||
class BawkSampler:
|
||||
"""
|
||||
Combined latent generator and advanced sampler for FLUX models.
|
||||
Generates optimized latent dimensions, then performs advanced sampling.
|
||||
Designed to work with FluxWildcardEncode for prompt handling.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# Combined resolution presets with aspect ratios built-in
|
||||
# Format: "Name - WxH (aspect ratio)"
|
||||
resolution_presets = [
|
||||
# 16:9 aspect ratio options
|
||||
"HD 16:9 - 1024x576",
|
||||
"FHD 16:9 - 1920x1080",
|
||||
"2K 16:9 - 2048x1152",
|
||||
|
||||
# 1:1 square options
|
||||
"Small Square - 768x768",
|
||||
"Medium Square - 1024x1024",
|
||||
"Large Square - 1536x1536",
|
||||
"XL Square - 2048x2048",
|
||||
|
||||
# 3:2 photo aspect ratio
|
||||
"Photo 3:2 - 1152x768",
|
||||
"Photo 3:2 HD - 1728x1152",
|
||||
|
||||
# 4:3 classic aspect ratio
|
||||
"Classic 4:3 - 1024x768",
|
||||
"Classic 4:3 HD - 1536x1152",
|
||||
|
||||
# 9:16 portrait/mobile
|
||||
"Portrait 9:16 - 576x1024",
|
||||
"Mobile 9:16 - 1080x1920",
|
||||
|
||||
# 2:3 portrait photo
|
||||
"Portrait Photo - 768x1152",
|
||||
|
||||
# 21:9 ultra-wide
|
||||
"Ultra-wide - 1792x768",
|
||||
|
||||
# Custom option
|
||||
"Custom Resolution",
|
||||
]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
# Core inputs
|
||||
"model": ("MODEL",),
|
||||
"conditioning": ("CONDITIONING",),
|
||||
"vae": ("VAE",), # Added VAE input for decoding
|
||||
|
||||
# Latent generation
|
||||
"resolution": (resolution_presets, {
|
||||
"default": "FHD 16:9 - 1920x1080",
|
||||
"tooltip": "Select resolution preset or custom option"
|
||||
}),
|
||||
"batch_size": ("INT", {
|
||||
"default": 4, "min": 1, "max": 64, "step": 1,
|
||||
"tooltip": "Number of images to generate"
|
||||
}),
|
||||
|
||||
# Sampling parameters
|
||||
"seed": ("INT", {
|
||||
"default": 0, "min": 0, "max": 0xffffffffffffffff,
|
||||
"tooltip": "Random seed for generation"
|
||||
}),
|
||||
"sampler": (comfy.samplers.KSampler.SAMPLERS, {
|
||||
"default": "euler",
|
||||
"tooltip": "Sampling method for generation"
|
||||
}),
|
||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {
|
||||
"default": "beta",
|
||||
"tooltip": "Noise scheduling method"
|
||||
}),
|
||||
"steps": ("INT", {
|
||||
"default": 30, "min": 1, "max": 100, "step": 1,
|
||||
"tooltip": "Number of sampling steps"
|
||||
}),
|
||||
"guidance": ("FLOAT", {
|
||||
"default": 3.5, "min": 0.0, "max": 20.0, "step": 0.1,
|
||||
"tooltip": "Guidance scale for FLUX"
|
||||
}),
|
||||
"max_shift": ("FLOAT", {
|
||||
"default": 0.5, "min": 0.0, "max": 2.0, "step": 0.01,
|
||||
"tooltip": "Max shift parameter for FLUX"
|
||||
}),
|
||||
"base_shift": ("FLOAT", {
|
||||
"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.01,
|
||||
"tooltip": "Base shift parameter for FLUX"
|
||||
}),
|
||||
"denoise": ("FLOAT", {
|
||||
"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"tooltip": "Denoise strength"
|
||||
}),
|
||||
|
||||
# Custom resolution toggle
|
||||
"use_custom_resolution": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Enable custom width/height instead of presets"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# Custom resolution (only used when use_custom_resolution=True)
|
||||
"custom_width": ("INT", {
|
||||
"default": 1920, "min": 64, "max": 4096, "step": 64,
|
||||
"tooltip": "Custom width (must be multiple of 64). Only used when custom resolution is enabled."
|
||||
}),
|
||||
"custom_height": ("INT", {
|
||||
"default": 1080, "min": 64, "max": 4096, "step": 64,
|
||||
"tooltip": "Custom height (must be multiple of 64). Only used when custom resolution is enabled."
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "LATENT")
|
||||
RETURN_NAMES = ("images", "latent")
|
||||
FUNCTION = "generate_sample_and_decode"
|
||||
CATEGORY = "BawkNodes/sampling"
|
||||
DESCRIPTION = "Combined latent generator, sampler, and VAE decoder optimized for FLUX models"
|
||||
|
||||
def generate_sample_and_decode(
|
||||
self,
|
||||
model, conditioning, vae,
|
||||
resolution="FHD 16:9 - 1920x1080", batch_size=4,
|
||||
seed=0, sampler="euler", scheduler="beta", steps=30,
|
||||
guidance=3.5, max_shift=0.5, base_shift=0.3, denoise=1.0,
|
||||
use_custom_resolution=False, custom_width=1920, custom_height=1080
|
||||
):
|
||||
"""
|
||||
Generate optimized latent, perform FLUX sampling, and decode to images
|
||||
"""
|
||||
try:
|
||||
# Step 1: Generate optimized latent
|
||||
latent = self._generate_optimized_latent(
|
||||
resolution, batch_size, use_custom_resolution, custom_width, custom_height
|
||||
)
|
||||
|
||||
# Step 2: Perform sampling
|
||||
sampled_latent = self._perform_flux_sampling(
|
||||
model, conditioning, latent, seed, sampler, scheduler,
|
||||
steps, guidance, max_shift, base_shift, denoise
|
||||
)
|
||||
|
||||
# Step 3: Decode latent to images using VAE
|
||||
decoded_images = self._decode_latent_to_images(vae, sampled_latent)
|
||||
|
||||
print(f"[BawkSampler] Successfully generated, sampled, and decoded {batch_size} images")
|
||||
|
||||
return (decoded_images, sampled_latent)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"BawkSampler failed: {str(e)}"
|
||||
print(f"[BawkSampler] Error: {error_msg}")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
def _generate_optimized_latent(
|
||||
self, resolution, batch_size, use_custom_resolution, custom_width, custom_height
|
||||
) -> Dict:
|
||||
"""Generate optimized empty latent for FLUX"""
|
||||
|
||||
# Determine target dimensions
|
||||
if use_custom_resolution:
|
||||
# Use custom dimensions (ensure they're multiples of 64)
|
||||
target_width = round_to_nearest_multiple(custom_width, 64)
|
||||
target_height = round_to_nearest_multiple(custom_height, 64)
|
||||
print(f"[BawkSampler] Using custom resolution: {target_width}x{target_height}")
|
||||
else:
|
||||
# Parse dimensions from preset string
|
||||
target_width, target_height = self._parse_resolution_preset(resolution)
|
||||
print(f"[BawkSampler] Using preset resolution: {resolution}")
|
||||
|
||||
# FLUX-specific parameters
|
||||
vae_scale_factor = 8
|
||||
latent_channels = 16 # FLUX uses 16 channels
|
||||
|
||||
# Calculate latent dimensions
|
||||
latent_width = target_width // vae_scale_factor
|
||||
latent_height = target_height // vae_scale_factor
|
||||
|
||||
# Generate latent tensor
|
||||
try:
|
||||
latent_tensor = torch.zeros([batch_size, latent_channels, latent_height, latent_width])
|
||||
latent = {"samples": latent_tensor}
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error creating FLUX latent tensor [{batch_size}, {latent_channels}, {latent_height}, {latent_width}]: {e}")
|
||||
|
||||
print(f"[BawkSampler] Generated FLUX latent: {latent_width}x{latent_height} "
|
||||
f"(pixel: {target_width}x{target_height}, batch: {batch_size})")
|
||||
|
||||
return latent
|
||||
|
||||
def _parse_resolution_preset(self, resolution_preset):
|
||||
"""Parse width and height from resolution preset string"""
|
||||
try:
|
||||
# Extract dimensions from string like "FHD 16:9 - 1920x1080"
|
||||
# Split by " - " and take the part after it
|
||||
parts = resolution_preset.split(" - ")
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f"Invalid resolution format: {resolution_preset}")
|
||||
|
||||
dimensions = parts[1] # e.g., "1920x1080"
|
||||
width_str, height_str = dimensions.split("x")
|
||||
|
||||
target_width = int(width_str)
|
||||
target_height = int(height_str)
|
||||
|
||||
# Ensure dimensions are multiples of 64 for FLUX compatibility
|
||||
target_width = round_to_nearest_multiple(target_width, 64)
|
||||
target_height = round_to_nearest_multiple(target_height, 64)
|
||||
|
||||
return target_width, target_height
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BawkSampler] Warning: Could not parse resolution '{resolution_preset}': {e}")
|
||||
# Fallback to default FHD resolution
|
||||
return 1920, 1080
|
||||
|
||||
def _perform_flux_sampling(
|
||||
self, model, conditioning, latent_image, seed, sampler, scheduler,
|
||||
steps, guidance, max_shift, base_shift, denoise
|
||||
):
|
||||
"""Perform FLUX-optimized sampling"""
|
||||
|
||||
# Single parameter sampling (no multi-parameter sweeps for cleaner UX)
|
||||
noise_seed = seed
|
||||
|
||||
# Initialize FLUX sampling components
|
||||
basicscheduler = BasicScheduler()
|
||||
basicguider = BasicGuider()
|
||||
samplercustomadvanced = SamplerCustomAdvanced()
|
||||
modelsamplingflux = ModelSamplingFlux()
|
||||
randnoise = Noise_RandomNoise(noise_seed)
|
||||
|
||||
# Get dimensions for model sampling
|
||||
width = latent_image["samples"].shape[3] * 8
|
||||
height = latent_image["samples"].shape[2] * 8
|
||||
|
||||
# Apply FLUX model sampling
|
||||
work_model = modelsamplingflux.patch(model, max_shift, base_shift, width, height)[0]
|
||||
|
||||
# Set guidance in conditioning
|
||||
cond = conditioning_set_values(conditioning, {"guidance": guidance})
|
||||
guider = basicguider.get_guider(work_model, cond)[0]
|
||||
|
||||
# Create sampler object
|
||||
samplerobj = comfy.samplers.sampler_object(sampler)
|
||||
|
||||
# Generate sigmas
|
||||
sigmas = basicscheduler.get_sigmas(work_model, scheduler, steps, denoise)[0]
|
||||
|
||||
logging.info(f"FLUX Sampling: seed={noise_seed}, {sampler}_{scheduler}, "
|
||||
f"steps={steps}, guidance={guidance}, max_shift={max_shift}, base_shift={base_shift}")
|
||||
|
||||
# Perform FLUX sampling
|
||||
latent = samplercustomadvanced.sample(
|
||||
randnoise, guider, samplerobj, sigmas, latent_image
|
||||
)[1]
|
||||
|
||||
print(f"[BawkSampler] Completed FLUX sampling with {sampler}_{scheduler}")
|
||||
return latent
|
||||
|
||||
def _decode_latent_to_images(self, vae, latent):
|
||||
"""Decode latent to images using the provided VAE"""
|
||||
try:
|
||||
print(f"[BawkSampler] Decoding latent to images...")
|
||||
|
||||
# Decode latent using VAE
|
||||
decoded_images = vae.decode(latent["samples"])
|
||||
|
||||
print(f"[BawkSampler] Successfully decoded latent to images, shape: {decoded_images.shape}")
|
||||
return decoded_images
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to decode latent: {str(e)}"
|
||||
print(f"[BawkSampler] Error: {error_msg}")
|
||||
raise RuntimeError(error_msg)
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
DiffusionModelLoader Node
|
||||
File: nodes/diffusion_model_loader.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import torch
|
||||
import comfy.sd
|
||||
import comfy.utils
|
||||
import folder_paths
|
||||
from typing import Tuple, Any
|
||||
|
||||
# Try importing validation from parent modules
|
||||
try:
|
||||
from ..modules.validation import ValidationMixin
|
||||
except ImportError:
|
||||
# Fallback if modules structure is different
|
||||
class ValidationMixin:
|
||||
pass
|
||||
|
||||
|
||||
class DiffusionModelLoader(ValidationMixin):
|
||||
"""
|
||||
Advanced Diffusion Model Loader with support for:
|
||||
- Multiple model formats (FLUX, SDXL, SD1.5)
|
||||
- Flexible weight data types (fp8, fp16, bf16, fp32)
|
||||
- Separate VAE and CLIP loading
|
||||
- Multiple directory support
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model_name": (folder_paths.get_filename_list("diffusion_models"),),
|
||||
"vae_name": (folder_paths.get_filename_list("vae") + ["baked VAE"],),
|
||||
"clip_name1": (folder_paths.get_filename_list("text_encoders") + ["none"],),
|
||||
"clip_name2": (folder_paths.get_filename_list("text_encoders") + ["none"],),
|
||||
"weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "VAE", "CLIP", "STRING")
|
||||
RETURN_NAMES = ("MODEL", "VAE", "CLIP", "MODEL_STRING")
|
||||
FUNCTION = "load_diffusion_model"
|
||||
CATEGORY = "BawkNodes/loaders"
|
||||
DESCRIPTION = "Advanced diffusion model loader for FLUX and other modern architectures"
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
"""Simple validation matching the working loader pattern"""
|
||||
errors = []
|
||||
|
||||
# Validate model exists in diffusion_models directory
|
||||
model_name = kwargs.get("model_name")
|
||||
if model_name:
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
||||
except:
|
||||
errors.append(f"Model '{model_name}' not found in diffusion_models directory")
|
||||
|
||||
# Validate VAE if specified
|
||||
vae_name = kwargs.get("vae_name", "baked VAE")
|
||||
if vae_name and vae_name != "baked VAE":
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("vae", vae_name)
|
||||
except:
|
||||
errors.append(f"VAE '{vae_name}' not found in vae directory")
|
||||
|
||||
# Validate CLIP models if specified
|
||||
clip1 = kwargs.get("clip_name1", "none")
|
||||
if clip1 and clip1 != "none":
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("text_encoders", clip1)
|
||||
except:
|
||||
errors.append(f"CLIP model '{clip1}' not found in text_encoders directory")
|
||||
|
||||
clip2 = kwargs.get("clip_name2", "none")
|
||||
if clip2 and clip2 != "none":
|
||||
try:
|
||||
folder_paths.get_full_path_or_raise("text_encoders", clip2)
|
||||
except:
|
||||
errors.append(f"CLIP model '{clip2}' not found in text_encoders directory")
|
||||
|
||||
return "; ".join(errors) if errors else True
|
||||
|
||||
def load_diffusion_model(
|
||||
self,
|
||||
model_name: str,
|
||||
weight_dtype: str = "default",
|
||||
vae_name: str = "baked VAE",
|
||||
clip_name1: str = "none",
|
||||
clip_name2: str = "none"
|
||||
) -> Tuple[Any, Any, Any, str]:
|
||||
"""
|
||||
Main loading function for diffusion models
|
||||
"""
|
||||
try:
|
||||
# Handle "none" and "baked VAE" values
|
||||
vae_name = vae_name if vae_name and vae_name != "baked VAE" else None
|
||||
clip_name1 = clip_name1 if clip_name1 and clip_name1 != "none" else None
|
||||
clip_name2 = clip_name2 if clip_name2 and clip_name2 != "none" else None
|
||||
weight_dtype = weight_dtype if weight_dtype else "default"
|
||||
|
||||
# Set up model options based on weight_dtype
|
||||
model_options = {}
|
||||
if weight_dtype == "fp8_e4m3fn":
|
||||
model_options["dtype"] = torch.float8_e4m3fn
|
||||
elif weight_dtype == "fp8_e4m3fn_fast":
|
||||
model_options["dtype"] = torch.float8_e4m3fn
|
||||
model_options["fp8_optimizations"] = True
|
||||
elif weight_dtype == "fp8_e5m2":
|
||||
model_options["dtype"] = torch.float8_e5m2
|
||||
|
||||
# Load diffusion model
|
||||
print(f"[DiffusionModelLoader] Loading diffusion model: {model_name}")
|
||||
model_path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
||||
model = comfy.sd.load_diffusion_model(model_path, model_options=model_options)
|
||||
|
||||
if model is None:
|
||||
raise RuntimeError("Failed to load diffusion model - output was None")
|
||||
|
||||
# Load VAE
|
||||
vae = None
|
||||
if vae_name:
|
||||
print(f"[DiffusionModelLoader] Loading VAE: {vae_name}")
|
||||
vae_path = folder_paths.get_full_path_or_raise("vae", vae_name)
|
||||
vae_sd = comfy.utils.load_torch_file(vae_path)
|
||||
vae = comfy.sd.VAE(sd=vae_sd)
|
||||
|
||||
# Load CLIP models
|
||||
clip_paths = []
|
||||
if clip_name1:
|
||||
clip_paths.append(folder_paths.get_full_path_or_raise("text_encoders", clip_name1))
|
||||
if clip_name2:
|
||||
clip_paths.append(folder_paths.get_full_path_or_raise("text_encoders", clip_name2))
|
||||
|
||||
clip = None
|
||||
if clip_paths:
|
||||
print(f"[DiffusionModelLoader] Loading CLIP models")
|
||||
clip = comfy.sd.load_clip(
|
||||
ckpt_paths=clip_paths,
|
||||
embedding_directory=folder_paths.get_folder_paths("embeddings"),
|
||||
clip_type=comfy.sd.CLIPType.FLUX
|
||||
)
|
||||
|
||||
# Generate simple model name string
|
||||
model_string = os.path.splitext(model_name)[0]
|
||||
|
||||
print(f"[DiffusionModelLoader] Successfully loaded diffusion model: {model_string}")
|
||||
return (model, vae, clip, model_string)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to load diffusion model '{model_name}': {str(e)}"
|
||||
print(f"[DiffusionModelLoader] Error: {error_msg}")
|
||||
raise RuntimeError(error_msg)
|
||||
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
FluxImageSaver Node
|
||||
File: nodes/flux_image_saver.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import numpy as np
|
||||
import folder_paths
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class FluxImageSaver:
|
||||
"""
|
||||
FLUX-optimized image saver with organized folder structure and metadata support
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"model_string": ("STRING", {"forceInput": True}),
|
||||
"processed_prompt": ("STRING", {"forceInput": True}), # Connect from wildcard encoder
|
||||
"prefix": ("STRING", {"default": "flux_image"}),
|
||||
"format": (["png", "jpg", "webp"], {"default": "png"}),
|
||||
"quality": ("INT", {"default": 95, "min": 1, "max": 100}),
|
||||
"save_metadata": ("BOOLEAN", {"default": True}),
|
||||
"save_prompt": ("BOOLEAN", {"default": True, "tooltip": "Save the processed prompt as a separate text file"}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt_hidden": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_NAMES = ()
|
||||
FUNCTION = "save_images"
|
||||
CATEGORY = "BawkNodes/image"
|
||||
OUTPUT_NODE = True
|
||||
DESCRIPTION = "Save FLUX-generated images with organized folder structure"
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
|
||||
def save_images(self, images, model_string, processed_prompt, prefix="flux_image",
|
||||
format="png", quality=95, save_metadata=True, save_prompt=True,
|
||||
prompt_hidden=None, extra_pnginfo=None):
|
||||
"""
|
||||
Save images with FLUX-optimized organization and metadata
|
||||
"""
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
print(f"[FluxImageSaver] Processing {len(images)} images")
|
||||
|
||||
# Get current date for folder and filename
|
||||
now = datetime.now()
|
||||
date_str = now.strftime("%d-%m-%Y")
|
||||
datetime_str = now.strftime("%d-%m-%Y_%H-%M-%S")
|
||||
|
||||
# Clean model string for folder name
|
||||
clean_model = self._clean_filename(model_string)
|
||||
|
||||
# Create folder structure: [MODEL]-DD-MM-YYYY
|
||||
folder_name = f"[{clean_model}]-{date_str}"
|
||||
output_dir = os.path.join(folder_paths.get_output_directory(), folder_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
saved_paths = []
|
||||
results = [] # For UI image preview
|
||||
|
||||
# Save the processed prompt as a text file if enabled
|
||||
if save_prompt and processed_prompt:
|
||||
self._save_prompt_file(output_dir, processed_prompt, datetime_str, prefix)
|
||||
|
||||
for i, image_tensor in enumerate(images):
|
||||
print(f"[FluxImageSaver] Processing image {i+1}/{len(images)}, shape: {image_tensor.shape}")
|
||||
|
||||
# Convert tensor to PIL Image
|
||||
image_np = (255.0 * image_tensor.cpu().numpy()).astype(np.uint8)
|
||||
pil_image = Image.fromarray(image_np)
|
||||
|
||||
# Generate filename: prefix_DD-MM-YYYY_HH-MM-SS_001.ext
|
||||
batch_suffix = f"_{i+1:03d}" if len(images) > 1 else ""
|
||||
filename = f"{prefix}_{datetime_str}{batch_suffix}.{format}"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
# Check if file exists and add counter if necessary
|
||||
counter = 1
|
||||
original_filepath = filepath
|
||||
while os.path.exists(filepath):
|
||||
name_part = f"{prefix}_{datetime_str}{batch_suffix}_{counter:03d}"
|
||||
filename = f"{name_part}.{format}"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
counter += 1
|
||||
|
||||
# Save image with format-specific options
|
||||
save_kwargs = {}
|
||||
if format.lower() == "jpg" or format.lower() == "jpeg":
|
||||
save_kwargs = {"quality": quality, "optimize": True}
|
||||
elif format.lower() == "webp":
|
||||
save_kwargs = {"quality": quality, "method": 6}
|
||||
elif format.lower() == "png":
|
||||
save_kwargs = {"optimize": True}
|
||||
# Add metadata to PNG
|
||||
if save_metadata and extra_pnginfo:
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
metadata = PngInfo()
|
||||
for key, value in extra_pnginfo.items():
|
||||
metadata.add_text(key, str(value))
|
||||
save_kwargs["pnginfo"] = metadata
|
||||
|
||||
pil_image.save(filepath, format.upper(), **save_kwargs)
|
||||
saved_paths.append(filepath)
|
||||
|
||||
# Add to results for UI preview (relative to output directory)
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"subfolder": folder_name,
|
||||
"type": self.type
|
||||
})
|
||||
|
||||
print(f"[FluxImageSaver] Saved: {filepath}")
|
||||
|
||||
# Save metadata/prompt file if requested
|
||||
if save_metadata and prompt_hidden:
|
||||
self._save_metadata(filepath, prompt_hidden, extra_pnginfo,
|
||||
model_string, format, quality, processed_prompt)
|
||||
|
||||
print(f"[FluxImageSaver] Returning {len(results)} images for preview")
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to save images: {str(e)}"
|
||||
print(f"[FluxImageSaver] Error: {error_msg}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
def _clean_filename(self, filename):
|
||||
"""Clean filename for use in folder/file names"""
|
||||
# Remove file extensions and clean up
|
||||
clean = re.sub(r'\.[^.]*$', '', filename) # Remove extension
|
||||
clean = re.sub(r'[<>:"/\\|?*]', '_', clean) # Replace invalid chars
|
||||
clean = re.sub(r'_{2,}', '_', clean) # Collapse multiple underscores
|
||||
return clean.strip('_')
|
||||
|
||||
def _save_prompt_file(self, output_dir, processed_prompt, datetime_str, prefix):
|
||||
"""Save the processed prompt as a text file"""
|
||||
try:
|
||||
# Create prompt filename
|
||||
prompt_filename = f"{prefix}_{datetime_str}_prompt.txt"
|
||||
prompt_filepath = os.path.join(output_dir, prompt_filename)
|
||||
|
||||
# Check if file exists and add counter if necessary
|
||||
counter = 1
|
||||
while os.path.exists(prompt_filepath):
|
||||
prompt_filename = f"{prefix}_{datetime_str}_prompt_{counter:03d}.txt"
|
||||
prompt_filepath = os.path.join(output_dir, prompt_filename)
|
||||
counter += 1
|
||||
|
||||
# Save the prompt
|
||||
with open(prompt_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(processed_prompt)
|
||||
|
||||
print(f"[FluxImageSaver] Saved prompt: {prompt_filepath}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FluxImageSaver] Failed to save prompt file: {str(e)}")
|
||||
|
||||
def _save_metadata(self, image_path, prompt_hidden, extra_pnginfo,
|
||||
model_string, format, quality, processed_prompt=None):
|
||||
"""Save metadata as JSON file"""
|
||||
try:
|
||||
# Create metadata filename
|
||||
base_path = os.path.splitext(image_path)[0]
|
||||
metadata_path = f"{base_path}_metadata.json"
|
||||
|
||||
# Collect metadata
|
||||
metadata = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"model": model_string,
|
||||
"processed_prompt": processed_prompt, # Add the processed prompt
|
||||
"format": format,
|
||||
"quality": quality if format != "png" else None,
|
||||
"workflow_prompt": prompt_hidden,
|
||||
"extra_info": extra_pnginfo
|
||||
}
|
||||
|
||||
# Save as JSON
|
||||
with open(metadata_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"[FluxImageSaver] Metadata saved: {metadata_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FluxImageSaver] Failed to save metadata: {str(e)}")
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
FluxPromptSaver Node
|
||||
File: nodes/flux_prompt_saver.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import folder_paths
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class FluxPromptSaver:
|
||||
"""
|
||||
Standalone prompt saver for FLUX workflows
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model_string": ("STRING", {"forceInput": True}),
|
||||
"prompt": ("STRING", {"multiline": True}),
|
||||
"filename_prefix": ("STRING", {"default": "flux_prompt"}),
|
||||
},
|
||||
"optional": {
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"generation_params": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"hidden": {
|
||||
"extra_pnginfo": "EXTRA_PNGINFO"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("saved_path",)
|
||||
FUNCTION = "save_prompt"
|
||||
CATEGORY = "BawkNodes/text"
|
||||
OUTPUT_NODE = True
|
||||
DESCRIPTION = "Save FLUX prompts and generation parameters"
|
||||
|
||||
def save_prompt(self, model_string, prompt, filename_prefix="flux_prompt",
|
||||
negative_prompt="", generation_params="", extra_pnginfo=None):
|
||||
"""Save prompt and parameters to organized text files"""
|
||||
try:
|
||||
# Get current date for folder structure
|
||||
now = datetime.now()
|
||||
date_str = now.strftime("%d-%m-%Y")
|
||||
datetime_str = now.strftime("%d-%m-%Y_%H-%M-%S")
|
||||
|
||||
# Clean model string for folder name
|
||||
clean_model = self._clean_filename(model_string)
|
||||
|
||||
# Create folder structure matching image saver
|
||||
folder_name = f"[{clean_model}]-{date_str}"
|
||||
output_dir = os.path.join(folder_paths.get_output_directory(), folder_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Create filename
|
||||
filename = f"{filename_prefix}_{datetime_str}.json"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
# Prepare data
|
||||
prompt_data = {
|
||||
"timestamp": now.isoformat(),
|
||||
"model": model_string,
|
||||
"positive_prompt": prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"generation_parameters": generation_params,
|
||||
"workflow_info": extra_pnginfo
|
||||
}
|
||||
|
||||
# Save as JSON
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(prompt_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"[FluxPromptSaver] Saved: {filepath}")
|
||||
return (f"Prompt saved to {filename}",)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to save prompt: {str(e)}"
|
||||
print(f"[FluxPromptSaver] Error: {error_msg}")
|
||||
return (f"Error: {error_msg}",)
|
||||
|
||||
def _clean_filename(self, filename):
|
||||
"""Clean filename for use in folder/file names"""
|
||||
clean = re.sub(r'\.[^.]*$', '', filename)
|
||||
clean = re.sub(r'[<>:"/\\|?*]', '_', clean)
|
||||
clean = re.sub(r'_{2,}', '_', clean)
|
||||
return clean.strip('_')
|
||||
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
FluxWildcardEncode Node with LoRA Support (using rgthree-style dynamic UI)
|
||||
File: nodes/flux_wildcard_encode.py
|
||||
"""
|
||||
|
||||
import re
|
||||
import random
|
||||
import torch
|
||||
import folder_paths
|
||||
from typing import Tuple, Any
|
||||
|
||||
|
||||
# Fallback implementation for flexible inputs
|
||||
class FlexibleOptionalInputType(dict):
|
||||
def __init__(self, type_func, data=None):
|
||||
super().__init__()
|
||||
self.data = data or {}
|
||||
self.type = type_func
|
||||
for key, value in self.data.items():
|
||||
self[key] = value
|
||||
|
||||
def __contains__(self, key):
|
||||
return True
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self.data:
|
||||
return self.data[key]
|
||||
return self.type
|
||||
|
||||
|
||||
def any_type(s=None):
|
||||
return ("*",)
|
||||
|
||||
|
||||
class FluxWildcardEncode:
|
||||
"""
|
||||
Enhanced FLUX wildcard text encoder with integrated LoRA loading.
|
||||
Uses rgthree-style dynamic UI for clean LoRA management.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# Get available LoRA files
|
||||
lora_list = folder_paths.get_filename_list("loras")
|
||||
lora_options = ["None"] + lora_list
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
"prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"wildcard_seed": ("INT", {
|
||||
"default": -1, "min": -1, "max": 0xffffffffffffffff,
|
||||
"tooltip": "Seed for wildcard selection. Use -1 to disable wildcards"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# LoRA 1
|
||||
"lora_1_on": ("BOOLEAN", {"default": False}),
|
||||
"lora_1_name": (lora_options, {"default": "None"}),
|
||||
"lora_1_strength": ("FLOAT", {"default": 1.00, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
|
||||
# LoRA 2
|
||||
"lora_2_on": ("BOOLEAN", {"default": False}),
|
||||
"lora_2_name": (lora_options, {"default": "None"}),
|
||||
"lora_2_strength": ("FLOAT", {"default": 1.00, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
|
||||
# LoRA 3
|
||||
"lora_3_on": ("BOOLEAN", {"default": False}),
|
||||
"lora_3_name": (lora_options, {"default": "None"}),
|
||||
"lora_3_strength": ("FLOAT", {"default": 1.00, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
|
||||
# LoRA 4
|
||||
"lora_4_on": ("BOOLEAN", {"default": False}),
|
||||
"lora_4_name": (lora_options, {"default": "None"}),
|
||||
"lora_4_strength": ("FLOAT", {"default": 1.00, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
|
||||
# LoRA 5
|
||||
"lora_5_on": ("BOOLEAN", {"default": False}),
|
||||
"lora_5_name": (lora_options, {"default": "None"}),
|
||||
"lora_5_strength": ("FLOAT", {"default": 1.00, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
|
||||
# LoRA 6
|
||||
"lora_6_on": ("BOOLEAN", {"default": False}),
|
||||
"lora_6_name": (lora_options, {"default": "None"}),
|
||||
"lora_6_strength": ("FLOAT", {"default": 1.00, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING", "STRING")
|
||||
RETURN_NAMES = ("MODEL", "CLIP", "CONDITIONING", "PROMPT_OUT")
|
||||
FUNCTION = "encode_with_loras"
|
||||
CATEGORY = "BawkNodes/conditioning"
|
||||
DESCRIPTION = "🎲 FLUX Wildcard Encoder with Dynamic LoRA Support"
|
||||
|
||||
def encode_with_loras(self, model, clip, prompt, wildcard_seed=-1, **kwargs):
|
||||
"""
|
||||
Encode text prompt with wildcard processing and dynamic LoRA loading
|
||||
"""
|
||||
try:
|
||||
print(f"[FluxWildcardEncode] Starting encode with dynamic LoRAs")
|
||||
print(f"[FluxWildcardEncode] Received kwargs: {list(kwargs.keys())}")
|
||||
|
||||
# Step 1: Process wildcards if seed is provided
|
||||
processed_prompt = prompt
|
||||
if wildcard_seed != -1:
|
||||
processed_prompt = self._process_wildcards(prompt, wildcard_seed)
|
||||
if processed_prompt != prompt:
|
||||
print(f"[FluxWildcardEncode] Processed wildcards in prompt")
|
||||
|
||||
# Step 2: Apply LoRAs from fixed slots
|
||||
working_model = model
|
||||
working_clip = clip
|
||||
ui_lora_count = 0
|
||||
|
||||
# Process 6 fixed LoRA slots
|
||||
for i in range(1, 7): # LoRA 1-6
|
||||
on_key = f"lora_{i}_on"
|
||||
name_key = f"lora_{i}_name"
|
||||
strength_key = f"lora_{i}_strength"
|
||||
|
||||
# Check if this LoRA slot is enabled and has a valid selection
|
||||
lora_enabled = kwargs.get(on_key, False)
|
||||
lora_name = kwargs.get(name_key, "None")
|
||||
|
||||
if lora_enabled and lora_name and lora_name != "None":
|
||||
strength = kwargs.get(strength_key, 1.00)
|
||||
|
||||
# Skip if strength is zero
|
||||
if strength == 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
from nodes import LoraLoader
|
||||
working_model, working_clip = LoraLoader().load_lora(
|
||||
working_model, working_clip, lora_name, strength, strength
|
||||
)
|
||||
ui_lora_count += 1
|
||||
print(f"[FluxWildcardEncode] Applied LoRA {i}: {lora_name} (strength: {strength})")
|
||||
except Exception as e:
|
||||
print(f"[FluxWildcardEncode] Failed to load LoRA {i} ({lora_name}): {e}")
|
||||
|
||||
if ui_lora_count > 0:
|
||||
print(f"[FluxWildcardEncode] Applied {ui_lora_count} LoRAs")
|
||||
else:
|
||||
print(f"[FluxWildcardEncode] No LoRAs applied")
|
||||
|
||||
# Step 3: Encode the processed prompt
|
||||
if not processed_prompt.strip():
|
||||
print("[FluxWildcardEncode] Warning: Empty prompt")
|
||||
empty_conditioning = [[torch.zeros((1, 77, 768)), {"pooled_output": torch.zeros((1, 768))}]]
|
||||
return (working_model, working_clip, empty_conditioning, processed_prompt)
|
||||
|
||||
# Encode using the LoRA-modified CLIP
|
||||
tokens = working_clip.tokenize(processed_prompt)
|
||||
conditioning, pooled = working_clip.encode_from_tokens(tokens, return_pooled=True)
|
||||
conditioning = [[conditioning, {"pooled_output": pooled}]]
|
||||
|
||||
print(f"[FluxWildcardEncode] Success: {ui_lora_count} LoRAs, conditioning shape: {conditioning[0][0].shape}")
|
||||
return (working_model, working_clip, conditioning, processed_prompt)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to encode with LoRAs: {str(e)}"
|
||||
print(f"[FluxWildcardEncode] Error: {error_msg}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
empty_conditioning = [[torch.zeros((1, 77, 768)), {"pooled_output": torch.zeros((1, 768))}]]
|
||||
return (model, clip, empty_conditioning, prompt)
|
||||
|
||||
def _process_wildcards(self, prompt, seed):
|
||||
"""
|
||||
Process wildcard syntax in prompts
|
||||
Wildcard format: {option1|option2|option3}
|
||||
"""
|
||||
# Set seed for consistent wildcard selection
|
||||
random.seed(seed)
|
||||
|
||||
processed = prompt
|
||||
|
||||
# Find and process wildcards in {option1|option2|option3} format
|
||||
wildcard_pattern = r'\{([^}]+)\}'
|
||||
|
||||
def replace_wildcard(match):
|
||||
wildcard_content = match.group(1)
|
||||
if '|' in wildcard_content:
|
||||
options = [option.strip() for option in wildcard_content.split('|')]
|
||||
if options: # Make sure we have options
|
||||
return random.choice(options)
|
||||
return match.group(0) # Return original if no valid options
|
||||
|
||||
# Replace all wildcards
|
||||
processed = re.sub(wildcard_pattern, replace_wildcard, processed)
|
||||
|
||||
return processed
|
||||
|
||||
def _get_lora_by_filename(self, file_path):
|
||||
"""Find LoRA file with fuzzy matching (rgthree style)"""
|
||||
if not file_path:
|
||||
return None
|
||||
|
||||
lora_paths = folder_paths.get_filename_list('loras')
|
||||
|
||||
# Direct match
|
||||
if file_path in lora_paths:
|
||||
return file_path
|
||||
|
||||
# Match without extension
|
||||
lora_paths_no_ext = [os.path.splitext(x)[0] for x in lora_paths]
|
||||
if file_path in lora_paths_no_ext:
|
||||
return lora_paths[lora_paths_no_ext.index(file_path)]
|
||||
|
||||
# Force input without extension
|
||||
file_path_no_ext = os.path.splitext(file_path)[0]
|
||||
if file_path_no_ext in lora_paths_no_ext:
|
||||
return lora_paths[lora_paths_no_ext.index(file_path_no_ext)]
|
||||
|
||||
# Basename matching
|
||||
lora_basenames = [os.path.basename(x) for x in lora_paths]
|
||||
if file_path in lora_basenames:
|
||||
return lora_paths[lora_basenames.index(file_path)]
|
||||
|
||||
# Basename without extension
|
||||
file_basename = os.path.basename(file_path)
|
||||
if file_basename in lora_basenames:
|
||||
return lora_paths[lora_basenames.index(file_basename)]
|
||||
|
||||
# Basename no extension matching
|
||||
lora_basenames_no_ext = [os.path.splitext(os.path.basename(x))[0] for x in lora_paths]
|
||||
file_basename_no_ext = os.path.splitext(os.path.basename(file_path))[0]
|
||||
if file_basename_no_ext in lora_basenames_no_ext:
|
||||
return lora_paths[lora_basenames_no_ext.index(file_basename_no_ext)]
|
||||
|
||||
# Fuzzy partial match
|
||||
for lora_path in lora_paths:
|
||||
if file_path in lora_path:
|
||||
print(f"[FluxWildcardEncode] Fuzzy-matched '{file_path}' to '{lora_path}'")
|
||||
return lora_path
|
||||
|
||||
print(f"[FluxWildcardEncode] LoRA '{file_path}' not found")
|
||||
return None
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 621 KiB |
+3
-3
@@ -1,12 +1,12 @@
|
||||
[project]
|
||||
name = "comfyui-bawknodes"
|
||||
version = "1.0.0"
|
||||
description = "A collection of useful ComfyUI nodes including advanced diffusion model loader and more"
|
||||
version = "2.0.0"
|
||||
description = "A complete collection of FLUX-optimized ComfyUI nodes for enhanced AI image generation workflows."
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "Your Name", email = "your.email@example.com" }
|
||||
{ name = "judd", email = "" }
|
||||
]
|
||||
keywords = ["comfyui", "diffusion", "model-loading", "ai", "flux", "nodes", "bawk"]
|
||||
classifiers = [
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Flexible Input Utilities for Dynamic UI (based on rgthree)
|
||||
File: utils/flexible_inputs.py
|
||||
"""
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
|
||||
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class FlexibleOptionalInputType(dict):
|
||||
"""A special class to make flexible nodes that pass data to our python handlers.
|
||||
|
||||
Enables both flexible/dynamic input types (like for Any Switch) or a dynamic number of inputs
|
||||
(like for Any Switch, Context Switch, Context Merge, Power Lora Loader, etc).
|
||||
|
||||
Initially, ComfyUI only needed to return True for `__contains__` below, which told ComfyUI that
|
||||
our node will handle the input, regardless of what it is.
|
||||
|
||||
However, after https://github.com/comfyanonymous/ComfyUI/pull/2666 ComfyUI's execution changed
|
||||
also checking the data for the key; specifically, the type which is the first tuple entry. This
|
||||
type is supplied to our FlexibleOptionalInputType and returned for any non-data key. This can be a
|
||||
real type, or use the AnyType for additional flexibility.
|
||||
"""
|
||||
|
||||
def __init__(self, type_func, data: Union[dict, None] = None):
|
||||
"""Initializes the FlexibleOptionalInputType.
|
||||
|
||||
Args:
|
||||
type_func: The flexible type to use when ComfyUI retrieves an unknown key (via `__getitem__`).
|
||||
data: An optional dict to use as the basis. This is stored both in a `data` attribute, so we
|
||||
can look it up without hitting our overrides, as well as iterated over and adding its key
|
||||
and values to our `self` keys. This way, when looked at, we will appear to represent this
|
||||
data. When used in an "optional" INPUT_TYPES, these are the starting optional node types.
|
||||
"""
|
||||
super().__init__()
|
||||
self.data = data or {}
|
||||
self.type = type_func
|
||||
for key, value in self.data.items():
|
||||
self[key] = value
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Return True for any key, allowing ComfyUI to pass any input to our node."""
|
||||
return True
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return the type for any unknown key, or the stored value for known keys."""
|
||||
if key in self.data:
|
||||
return self.data[key]
|
||||
return self.type
|
||||
|
||||
|
||||
def any_type(s=None):
|
||||
"""Helper function for any type inputs"""
|
||||
return AnyType("*")
|
||||
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* FLUX Wildcard Encoder Extension - rgthree-style Power LoRA Loader UI
|
||||
* File: web/extensions/flux_wildcard_encoder.js
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
|
||||
const NODE_TYPE = "FluxWildcardEncode";
|
||||
const MAX_LORAS = 10; // Maximum number of LoRAs
|
||||
|
||||
class FluxWildcardEncoderNode {
|
||||
constructor() {
|
||||
this.loras = [];
|
||||
this.loraCounter = 0;
|
||||
}
|
||||
|
||||
setup(node) {
|
||||
this.node = node;
|
||||
this.setupLoraUI();
|
||||
}
|
||||
|
||||
setupLoraUI() {
|
||||
// Remove default widgets that we'll replace
|
||||
this.node.widgets = this.node.widgets?.filter(w =>
|
||||
!w.name.startsWith("lora_") &&
|
||||
w.name !== "toggle_all_loras"
|
||||
) || [];
|
||||
|
||||
// Add "Toggle All" button at the top
|
||||
const toggleAllWidget = ComfyWidgets.BOOLEAN(this.node, "toggle_all", ["BOOLEAN", { default: false }], app);
|
||||
toggleAllWidget.name = "toggle_all";
|
||||
toggleAllWidget.callback = () => {
|
||||
const enabled = toggleAllWidget.value;
|
||||
this.loras.forEach(lora => {
|
||||
if (lora.toggle) {
|
||||
lora.toggle.value = enabled;
|
||||
}
|
||||
});
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
// Add initial LoRA row
|
||||
this.addLoraRow();
|
||||
|
||||
// Add "Add LoRA" button
|
||||
this.addLoraButton();
|
||||
|
||||
// Update node size
|
||||
this.updateNodeSize();
|
||||
}
|
||||
|
||||
addLoraRow() {
|
||||
if (this.loras.length >= MAX_LORAS) {
|
||||
console.warn(`Maximum ${MAX_LORAS} LoRAs reached`);
|
||||
return;
|
||||
}
|
||||
|
||||
const loraIndex = this.loraCounter++;
|
||||
const lora = {
|
||||
index: loraIndex,
|
||||
enabled: false,
|
||||
name: "None",
|
||||
strength: 1.0,
|
||||
strengthTwo: 1.0
|
||||
};
|
||||
|
||||
// Create toggle widget
|
||||
const toggleWidget = ComfyWidgets.BOOLEAN(
|
||||
this.node,
|
||||
`lora_${loraIndex}_toggle`,
|
||||
["BOOLEAN", { default: false }],
|
||||
app
|
||||
);
|
||||
toggleWidget.name = `lora_${loraIndex}_on`;
|
||||
toggleWidget.callback = () => {
|
||||
lora.enabled = toggleWidget.value;
|
||||
this.updateNodeInputs();
|
||||
};
|
||||
|
||||
// Create LoRA selection widget
|
||||
const loraOptions = this.getLoraOptions();
|
||||
const loraWidget = ComfyWidgets.COMBO(
|
||||
this.node,
|
||||
`lora_${loraIndex}_name`,
|
||||
[loraOptions, { default: "None" }],
|
||||
app
|
||||
);
|
||||
loraWidget.name = `lora_${loraIndex}_lora`;
|
||||
loraWidget.callback = () => {
|
||||
lora.name = loraWidget.value;
|
||||
this.updateNodeInputs();
|
||||
};
|
||||
|
||||
// Create strength widget
|
||||
const strengthWidget = ComfyWidgets.NUMBER(
|
||||
this.node,
|
||||
`lora_${loraIndex}_strength`,
|
||||
["FLOAT", { default: 1.0, min: -10.0, max: 10.0, step: 0.01 }],
|
||||
app
|
||||
);
|
||||
strengthWidget.name = `lora_${loraIndex}_strength`;
|
||||
strengthWidget.callback = () => {
|
||||
lora.strength = strengthWidget.value;
|
||||
lora.strengthTwo = strengthWidget.value; // Use same for both by default
|
||||
this.updateNodeInputs();
|
||||
};
|
||||
|
||||
// Create remove button for this LoRA
|
||||
const removeWidget = ComfyWidgets.BUTTON(
|
||||
this.node,
|
||||
`remove_lora_${loraIndex}`,
|
||||
"🗑️",
|
||||
() => this.removeLoraRow(loraIndex)
|
||||
);
|
||||
removeWidget.name = `remove_lora_${loraIndex}`;
|
||||
|
||||
// Store references
|
||||
lora.toggle = toggleWidget;
|
||||
lora.loraWidget = loraWidget;
|
||||
lora.strengthWidget = strengthWidget;
|
||||
lora.removeWidget = removeWidget;
|
||||
|
||||
this.loras.push(lora);
|
||||
this.updateNodeSize();
|
||||
this.updateNodeInputs();
|
||||
}
|
||||
|
||||
removeLoraRow(index) {
|
||||
// Find and remove the LoRA
|
||||
const loraIndex = this.loras.findIndex(l => l.index === index);
|
||||
if (loraIndex === -1) return;
|
||||
|
||||
const lora = this.loras[loraIndex];
|
||||
|
||||
// Remove widgets
|
||||
this.node.widgets = this.node.widgets.filter(w =>
|
||||
w !== lora.toggle &&
|
||||
w !== lora.loraWidget &&
|
||||
w !== lora.strengthWidget &&
|
||||
w !== lora.removeWidget
|
||||
);
|
||||
|
||||
// Remove from array
|
||||
this.loras.splice(loraIndex, 1);
|
||||
|
||||
this.updateNodeSize();
|
||||
this.updateNodeInputs();
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
addLoraButton() {
|
||||
const addButton = ComfyWidgets.BUTTON(
|
||||
this.node,
|
||||
"add_lora",
|
||||
"+ Add LoRA",
|
||||
() => this.addLoraRow()
|
||||
);
|
||||
addButton.name = "add_lora_button";
|
||||
}
|
||||
|
||||
getLoraOptions() {
|
||||
// Get LoRA files from ComfyUI
|
||||
try {
|
||||
const loraList = app.ui.settings.getSettingValue("Comfy.LoraList") || [];
|
||||
return ["None", ...loraList];
|
||||
} catch (e) {
|
||||
// Fallback - we'll populate this from the backend
|
||||
return ["None"];
|
||||
}
|
||||
}
|
||||
|
||||
updateNodeInputs() {
|
||||
// Clear existing LoRA inputs
|
||||
if (this.node.inputs) {
|
||||
this.node.inputs = this.node.inputs.filter(input =>
|
||||
!input.name.startsWith("lora_")
|
||||
);
|
||||
}
|
||||
|
||||
// Add current LoRA inputs to the node's input data
|
||||
this.loras.forEach(lora => {
|
||||
if (lora.enabled && lora.name !== "None") {
|
||||
const loraData = {
|
||||
on: lora.enabled,
|
||||
lora: lora.name,
|
||||
strength: lora.strength,
|
||||
strengthTwo: lora.strengthTwo
|
||||
};
|
||||
|
||||
// Store in node's widget values for serialization
|
||||
if (!this.node.widgets_values) {
|
||||
this.node.widgets_values = {};
|
||||
}
|
||||
this.node.widgets_values[`lora_${lora.index}`] = loraData;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateNodeSize() {
|
||||
// Calculate required height based on number of widgets
|
||||
const baseHeight = 100; // Base height for prompt and wildcard_seed
|
||||
const loraRowHeight = 25; // Height per LoRA row
|
||||
const buttonHeight = 25; // Height for buttons
|
||||
|
||||
const totalHeight = baseHeight +
|
||||
(this.loras.length * loraRowHeight) +
|
||||
buttonHeight +
|
||||
25; // Toggle all button
|
||||
|
||||
this.node.size = [
|
||||
Math.max(300, this.node.size[0]), // Minimum width
|
||||
Math.max(totalHeight, this.node.size[1])
|
||||
];
|
||||
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
onDrawForeground(ctx) {
|
||||
// Custom drawing for LoRA UI (similar to rgthree style)
|
||||
if (!this.loras.length) return;
|
||||
|
||||
const margin = 10;
|
||||
let y = 80; // Start below the main inputs
|
||||
|
||||
// Draw LoRA section header
|
||||
ctx.fillStyle = "#555";
|
||||
ctx.fillRect(margin, y, this.node.size[0] - margin * 2, 1);
|
||||
y += 10;
|
||||
|
||||
// Draw each LoRA row
|
||||
this.loras.forEach((lora, index) => {
|
||||
this.drawLoraRow(ctx, lora, margin, y + index * 25);
|
||||
});
|
||||
}
|
||||
|
||||
drawLoraRow(ctx, lora, x, y) {
|
||||
const rowHeight = 20;
|
||||
const toggleWidth = 20;
|
||||
const nameWidth = 150;
|
||||
const strengthWidth = 60;
|
||||
|
||||
// Draw toggle
|
||||
this.drawToggle(ctx, x, y, toggleWidth, rowHeight, lora.enabled);
|
||||
|
||||
// Draw LoRA name
|
||||
ctx.fillStyle = lora.name !== "None" ? "#fff" : "#888";
|
||||
ctx.font = "12px Arial";
|
||||
ctx.fillText(
|
||||
lora.name.length > 20 ? lora.name.substring(0, 17) + "..." : lora.name,
|
||||
x + toggleWidth + 5,
|
||||
y + 14
|
||||
);
|
||||
|
||||
// Draw strength value
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillText(
|
||||
lora.strength.toFixed(2),
|
||||
x + toggleWidth + nameWidth + 10,
|
||||
y + 14
|
||||
);
|
||||
}
|
||||
|
||||
drawToggle(ctx, x, y, width, height, enabled) {
|
||||
// Draw toggle button background
|
||||
ctx.fillStyle = enabled ? "#4a9eff" : "#333";
|
||||
ctx.fillRect(x + 2, y + 2, width - 4, height - 4);
|
||||
|
||||
// Draw toggle border
|
||||
ctx.strokeStyle = "#666";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x + 2, y + 2, width - 4, height - 4);
|
||||
|
||||
// Draw checkmark if enabled
|
||||
if (enabled) {
|
||||
ctx.strokeStyle = "#fff";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 6, y + height / 2);
|
||||
ctx.lineTo(x + width / 2, y + height - 6);
|
||||
ctx.lineTo(x + width - 4, y + 6);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const data = {};
|
||||
this.loras.forEach(lora => {
|
||||
if (lora.enabled && lora.name !== "None") {
|
||||
data[`lora_${lora.index}`] = {
|
||||
on: lora.enabled,
|
||||
lora: lora.name,
|
||||
strength: lora.strength,
|
||||
strengthTwo: lora.strengthTwo
|
||||
};
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
deserialize(data) {
|
||||
// Clear existing LoRAs
|
||||
this.loras = [];
|
||||
this.loraCounter = 0;
|
||||
|
||||
// Recreate LoRAs from data
|
||||
Object.keys(data).forEach(key => {
|
||||
if (key.startsWith("lora_")) {
|
||||
const loraData = data[key];
|
||||
this.addLoraRow();
|
||||
const lora = this.loras[this.loras.length - 1];
|
||||
|
||||
lora.enabled = loraData.on || false;
|
||||
lora.name = loraData.lora || "None";
|
||||
lora.strength = loraData.strength || 1.0;
|
||||
lora.strengthTwo = loraData.strengthTwo || lora.strength;
|
||||
|
||||
// Update widgets
|
||||
if (lora.toggle) lora.toggle.value = lora.enabled;
|
||||
if (lora.loraWidget) lora.loraWidget.value = lora.name;
|
||||
if (lora.strengthWidget) lora.strengthWidget.value = lora.strength;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateNodeSize();
|
||||
this.updateNodeInputs();
|
||||
}
|
||||
}
|
||||
|
||||
// Extension registration
|
||||
app.registerExtension({
|
||||
name: "BawkNodes.FluxWildcardEncoder",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === NODE_TYPE) {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Initialize our custom UI
|
||||
this.fluxWildcardEncoder = new FluxWildcardEncoderNode();
|
||||
this.fluxWildcardEncoder.setup(this);
|
||||
|
||||
// Override drawing
|
||||
const originalOnDrawForeground = this.onDrawForeground;
|
||||
this.onDrawForeground = function(ctx) {
|
||||
originalOnDrawForeground?.apply(this, arguments);
|
||||
this.fluxWildcardEncoder?.onDrawForeground(ctx);
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Override serialization
|
||||
const originalSerialize = nodeType.prototype.serialize;
|
||||
nodeType.prototype.serialize = function() {
|
||||
const data = originalSerialize?.apply(this, arguments) || {};
|
||||
if (this.fluxWildcardEncoder) {
|
||||
data.fluxWildcardEncoder = this.fluxWildcardEncoder.serialize();
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// Override deserialization
|
||||
const originalConfigure = nodeType.prototype.configure;
|
||||
nodeType.prototype.configure = function(data) {
|
||||
originalConfigure?.apply(this, arguments);
|
||||
if (this.fluxWildcardEncoder && data.fluxWildcardEncoder) {
|
||||
this.fluxWildcardEncoder.deserialize(data.fluxWildcardEncoder);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🎲 FLUX Wildcard Encoder extension loaded");
|
||||
@@ -0,0 +1,343 @@
|
||||
{
|
||||
"id": "5acb6057-af87-40fa-90de-4b9e3cdcd5e3",
|
||||
"revision": 0,
|
||||
"last_node_id": 35,
|
||||
"last_link_id": 78,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 30,
|
||||
"type": "DiffusionModelLoader",
|
||||
"pos": [
|
||||
-259.1785888671875,
|
||||
87.82862091064453
|
||||
],
|
||||
"size": [
|
||||
342.2134704589844,
|
||||
214
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
74
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
77
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
75
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MODEL_STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
66
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-bawknodes",
|
||||
"ver": "97bbbef153df94caebb97f054b8fd53de1861cf1",
|
||||
"Node name for S&R": "DiffusionModelLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"flux1-dev.safetensors",
|
||||
"ae.safetensors",
|
||||
"t5xxl_fp16.safetensors",
|
||||
"clip_l.safetensors",
|
||||
"fp8_e4m3fn"
|
||||
],
|
||||
"shape": 1
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"type": "BawkSampler",
|
||||
"pos": [
|
||||
788.8836059570312,
|
||||
87.903076171875
|
||||
],
|
||||
"size": [
|
||||
288.7222595214844,
|
||||
622
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 72
|
||||
},
|
||||
{
|
||||
"name": "conditioning",
|
||||
"type": "CONDITIONING",
|
||||
"link": 73
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 77
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
76
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "latent",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-bawknodes",
|
||||
"ver": "97bbbef153df94caebb97f054b8fd53de1861cf1",
|
||||
"Node name for S&R": "BawkSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
"FHD 16:9 - 1920x1080",
|
||||
4,
|
||||
417938486318098,
|
||||
"randomize",
|
||||
"euler",
|
||||
"beta",
|
||||
30,
|
||||
3.5,
|
||||
0.5,
|
||||
0.3,
|
||||
1,
|
||||
false,
|
||||
1920,
|
||||
1080
|
||||
],
|
||||
"shape": 1
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"type": "FluxImageSaver",
|
||||
"pos": [
|
||||
1109.04248046875,
|
||||
87.9061508178711
|
||||
],
|
||||
"size": [
|
||||
536.477294921875,
|
||||
470.7662353515625
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 76
|
||||
},
|
||||
{
|
||||
"name": "model_string",
|
||||
"type": "STRING",
|
||||
"link": 66
|
||||
},
|
||||
{
|
||||
"name": "processed_prompt",
|
||||
"type": "STRING",
|
||||
"link": 78
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-bawknodes",
|
||||
"ver": "97bbbef153df94caebb97f054b8fd53de1861cf1",
|
||||
"Node name for S&R": "FluxImageSaver"
|
||||
},
|
||||
"widgets_values": [
|
||||
"NEW-CKH",
|
||||
"png",
|
||||
100,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"shape": 1
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"type": "FluxWildcardEncode",
|
||||
"pos": [
|
||||
118.85038757324219,
|
||||
87.09760284423828
|
||||
],
|
||||
"size": [
|
||||
633.321533203125,
|
||||
695.784912109375
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 74
|
||||
},
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 75
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
72
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
73
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PROMPT_OUT",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
78
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-bawknodes",
|
||||
"ver": "97bbbef153df94caebb97f054b8fd53de1861cf1",
|
||||
"Node name for S&R": "FluxWildcardEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
-1,
|
||||
false,
|
||||
"None",
|
||||
1,
|
||||
false,
|
||||
"None",
|
||||
1,
|
||||
false,
|
||||
"None",
|
||||
1,
|
||||
false,
|
||||
"None",
|
||||
1,
|
||||
false,
|
||||
"None",
|
||||
1,
|
||||
false,
|
||||
"None",
|
||||
1
|
||||
],
|
||||
"shape": 1
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
66,
|
||||
30,
|
||||
3,
|
||||
31,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
72,
|
||||
35,
|
||||
0,
|
||||
34,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
73,
|
||||
35,
|
||||
2,
|
||||
34,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
74,
|
||||
30,
|
||||
0,
|
||||
35,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
75,
|
||||
30,
|
||||
2,
|
||||
35,
|
||||
1,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
76,
|
||||
34,
|
||||
0,
|
||||
31,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
77,
|
||||
30,
|
||||
1,
|
||||
34,
|
||||
2,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
78,
|
||||
35,
|
||||
3,
|
||||
31,
|
||||
2,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8358208385450411,
|
||||
"offset": [
|
||||
299.5806636329205,
|
||||
171.9927929925298
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 15,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DiffusionModelLoader",
|
||||
"pos": [50, 50],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [1],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [2],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [3],
|
||||
"slot_index": 2
|
||||
},
|
||||
{
|
||||
"name": "MODEL_STRING",
|
||||
"type": "STRING",
|
||||
"links": [4],
|
||||
"slot_index": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DiffusionModelLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"flux1-dev.safetensors",
|
||||
"fp8_e4m3fn",
|
||||
"ae.safetensors",
|
||||
"t5xxl_fp16.safetensors",
|
||||
"clip_l.safetensors"
|
||||
],
|
||||
"title": "🚀 FLUX Model Loader"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [500, 50],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [5],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"A beautiful landscape with mountains and a lake, highly detailed, photorealistic"
|
||||
],
|
||||
"title": "Positive Prompt"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [500, 300],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [6],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"blurry, low quality, distorted"
|
||||
],
|
||||
"title": "Negative Prompt"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [50, 300],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [7],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
1024,
|
||||
1024,
|
||||
1
|
||||
],
|
||||
"title": "Empty Latent Image"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [950, 50],
|
||||
"size": [400, 300],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [8],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
12345,
|
||||
"randomize",
|
||||
20,
|
||||
1.0,
|
||||
"euler",
|
||||
"normal",
|
||||
1.0
|
||||
],
|
||||
"title": "KSampler"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1400, 50],
|
||||
"size": [200, 100],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 8
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [9],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"title": "VAE Decode"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "SaveImage",
|
||||
"pos": [1650, 50],
|
||||
"size": [300, 300],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUI_JLoader"
|
||||
],
|
||||
"title": "Save Image"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "ShowText",
|
||||
"pos": [50, 450],
|
||||
"size": [400, 100],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShowText"
|
||||
},
|
||||
"title": "Model Info Display"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 5, 0, "MODEL"],
|
||||
[2, 1, 1, 6, 1, "VAE"],
|
||||
[3, 1, 2, 2, 0, "CLIP"],
|
||||
[4, 1, 3, 8, 0, "STRING"],
|
||||
[5, 2, 0, 5, 1, "CONDITIONING"],
|
||||
[6, 3, 0, 5, 2, "CONDITIONING"],
|
||||
[7, 4, 0, 5, 3, "LATENT"],
|
||||
[8, 5, 0, 6, 0, "LATENT"],
|
||||
[9, 6, 0, 7, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Model Loading",
|
||||
"bounding": [25, 25, 450, 400],
|
||||
"color": "#3f789e"
|
||||
},
|
||||
{
|
||||
"title": "Text Encoding",
|
||||
"bounding": [475, 25, 450, 500],
|
||||
"color": "#a1309b"
|
||||
},
|
||||
{
|
||||
"title": "Generation",
|
||||
"bounding": [925, 25, 450, 350],
|
||||
"color": "#b58b2a"
|
||||
},
|
||||
{
|
||||
"title": "Output",
|
||||
"bounding": [1375, 25, 600, 350],
|
||||
"color": "#8a8a8a"
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4,
|
||||
"workflow_info": {
|
||||
"name": "FLUX Model Loading Example - Bawk Nodes",
|
||||
"description": "Example workflow showing how to use the Bawk Nodes Diffusion Model Loader with FLUX models, including separate VAE and dual CLIP text encoders.",
|
||||
"author": "ComfyUI Bawk Nodes",
|
||||
"version": "1.0.0",
|
||||
"tags": ["flux", "model-loading", "advanced", "fp8", "bawk-nodes"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user