feat: Enhance BawkSampler with Img2Img support and improved validation

- Added input_image parameter for image-to-image generation.
- Updated resolution default to include MP information.
- Improved denoise tooltip for better user guidance.
- Implemented smart validation for Img2Img mode with feedback on denoise strength.
- Introduced _encode_image_to_latent method for encoding input images.
- Enhanced error handling and logging for better debugging.

feat: Introduce BawkBatchProcessor for batch processing of prompts

- Added support for loading prompts from CSV/JSON files.
- Implemented validation and extraction of settings from batch items.
- Included preview functionality for batch files before processing.

feat: Add BawkControlNet for ControlNet integration

- Implemented preprocessing for various ControlNet types (Canny, Depth, etc.).
- Added parameter validation and feedback for ControlNet processing.
- Created placeholder methods for ControlNet conditioning.

feat: Enhance BawkImageLoader with advanced image loading features

- Added options for auto-orienting images based on EXIF data.
- Implemented resizing and padding options for images.
- Included mask generation functionality for images with alpha channels.

chore: Update version to 2.0.5 in pyproject.toml
This commit is contained in:
Chris Judd
2025-10-05 22:53:41 -07:00
parent e5a8759f01
commit 1bd0b8db45
11 changed files with 1208 additions and 336 deletions
+136 -295
View File
@@ -1,332 +1,173 @@
<div align="center">
<h1>🐓 ComfyUI Bawk Nodes v2.0.4</h1>
<h1>ComfyUI Bawk Nodes v2.0.5</h1>
**A collection of FLUX-optimized ComfyUI nodes for efficient AI image generation.**
**The Ultimate FLUX Workflow Suite for ComfyUI**
*Transform your AI image generation with powerful, easy-to-use nodes designed specifically for FLUX models*
[![ComfyUI](https://img.shields.io/badge/ComfyUI-Compatible-brightgreen)](https://github.com/comfyanonymous/ComfyUI)
[![FLUX](https://img.shields.io/badge/FLUX-Optimized-blue)](https://blackforestlabs.ai/)
[![Version](https://img.shields.io/badge/Version-2.0.5-orange)]()
</div>
![Image Description](previews/efficient-workflow-preview.png)
---
## **Why Choose Bawk Nodes?**
### **All-in-One Workflow Solutions**
- **No more node spaghetti!** Each Bawk Node combines multiple functions into clean, powerful tools
- **FLUX-first design** - Every node is optimized specifically for FLUX models
- **Professional results** with minimal setup time
### **Perfect For:**
- **Content Creators** - Instagram, TikTok, and social media workflows
- **Artists & Designers** - Professional image generation and experimentation
- **Hobbyists** - Easy-to-use tools without complicated setups
- **Power Users** - Advanced features like batch processing and Discord integration
---
## 🎯 **What's New in v2.0.4**
## **What You Get**
**Major Rewrite**: Complete FLUX-first redesign with modular architecture and workflow consolidation.
### **Bawk Image Loader**
*Your gateway to img2img workflows*
- **Click to browse** - No more typing file paths!
- **Auto-rotation** - Handles phone photos perfectly
- **Smart resizing** - Perfect dimensions every time
- **Multiple formats** - JPG, PNG, WEBP, and more
- 🎲 **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
### **Bawk Wildcard Encoder**
*Text prompts + LoRAs made simple*
- **6 LoRA slots** with smart recommendations
- **Wildcard support** - Randomize your prompts
- **Helpful tooltips** - Know exactly what each setting does
- **FLUX-optimized** text encoding
### **Bawk Sampler**
*The heart of your workflow*
- **Text-to-Image & Image-to-Image** in one node
- **Resolution presets** - Instagram, TikTok, 4K, and more
- **Smart validation** - Helpful tips and warnings
- **All-in-one** - Generates, samples, and decodes in one step
### **Bawk Image Saver**
*Save and share like a pro*
- **Organized folders** by model and date
- **Discord integration** - Auto-post your creations
- **Batch support** - Upload multiple images at once
- **Metadata saving** - Never lose your settings
### **Bawk Batch Processor**
*Automate your workflow*
- **CSV/JSON support** - Process hundreds of prompts
- **A/B testing** - Compare different settings easily
- **Preview mode** - Check your files before processing
- **Perfect for** content creation at scale
### **Bawk ControlNet**
*Guided generation made easy*
- **Built-in preprocessing** - Canny, depth, pose, and more
- **No external tools needed** - Everything works out of the box
- **FLUX-optimized** control strength recommendations
- **Multiple control types** in one node
### **Bawk Model Loader**
*Advanced model management*
- **Smart caching** - Faster loading times
- **Memory optimization** - Handle large models efficiently
- **Validation** - Helpful warnings for compatibility issues
- **FLUX-specific** optimizations
---
## 🚀 **Node Collection Overview**
## **Quick Start Guide**
| 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 |
### **1. Basic Text-to-Image Workflow**
```
Bawk Model Loader → Bawk Wildcard Encoder → Bawk Sampler → Bawk Image Saver
```
### **2. Image-to-Image Workflow**
```
Bawk Image Loader → Bawk Sampler → Bawk Image Saver
↗ (set denoise 0.6-0.8)
```
### **3. Batch Generation Workflow**
```
Bawk Batch Processor → Bawk Wildcard Encoder → Bawk Sampler → Bawk Image Saver
```
---
## 🔥 **Complete FLUX Workflow**
## **Popular Use Cases**
**Before BawkNodes (5+ nodes):**
```
CheckpointLoader → LoraLoader → CLIPTextEncode → EmptyLatent → KSampler → VAEDecode → SaveImage
```
### **Social Media Content**
- Use **Instagram presets** in Bawk Sampler (1080x1080, 1080x1920)
- Set up **Discord webhooks** to auto-post to your content channels
- **Batch process** multiple variations for A/B testing
**After BawkNodes (3 nodes):**
```
🚀 DiffusionModelLoader → 🎲 FluxWildcardEncode → 🐓 BawkSampler → 💾 FluxImageSaver
```
### **Art & Design**
- Load reference images with **Bawk Image Loader**
- Use **LoRA slots** for consistent character/style
- Try **different denoise levels** for style transfer effects
**60% fewer nodes, 100% of the power!**
### **Workflow Automation**
- Create **CSV files** with prompts and settings
- Use **Bawk Batch Processor** for unattended generation
- **Discord integration** notifies you when batches complete
---
## 📦 **Installation**
## **Pro Tips**
### Method 1: ComfyUI Manager (Recommended)
### **LoRA Management**
- **Slot 1**: Main character/style (strength 0.8-1.2)
- **Slot 2**: Secondary effects (strength 0.6-1.0)
- **Slot 3**: Clothing/objects (strength 0.4-0.8)
- **Slots 4-6**: Fine details and adjustments (strength 0.2-0.6)
### **Denoise Settings for Img2Img**
- **0.3-0.5**: Subtle improvements, keep original structure
- **0.6-0.7**: Style changes, good balance
- **0.8-0.9**: Major transformations
- **1.0**: Complete replacement (text2img mode)
### **Batch Processing**
Create a CSV file like this:
```csv
prompt,seed,steps,guidance,resolution
"beautiful sunset",12345,30,3.5,"Instagram Square - 1080x1080 - 1.2MP"
"city at night",67890,25,4.0,"Instagram Story - 1080x1920 - 2.1MP"
```
---
## **Installation**
### **Method 1: ComfyUI Manager (Recommended)**
1. Open ComfyUI Manager
2. Search for "Bawk Nodes"
3. Click Install
4. Restart ComfyUI
### Method 2: Manual Installation
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/juddisjudd/ComfyUI-BawkNodes.git
# Restart ComfyUI
```
### **Method 2: Manual Installation**
1. Navigate to `ComfyUI/custom_nodes/`
2. Clone this repository:
```bash
git clone https://github.com/juddisjudd/ComfyUI-BawkNodes.git
```
3. Restart ComfyUI
---
## 🎲 **Node Details**
## **Community & Support**
### 🚀 **Diffusion Model Loader (Advanced)**
**FLUX-optimized model loading with advanced features.**
**Features:**
- Multiple model formats (FLUX, SDXL, SD1.5)
- Flexible weight data types (fp8, fp16, bf16, fp32)
- Separate VAE and CLIP loading
- Multiple directory support
**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
**Outputs:** `MODEL`, `VAE`, `CLIP`, `MODEL_STRING`
- **Issues & Feature Requests**: [GitHub Issues](https://github.com/juddisjudd/ComfyUI-BawkNodes/issues)
---
### 🎲 **FLUX Wildcard Encoder**
## **License**
**Enhanced text encoder with 6 LoRA slots and wildcard support.**
**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)
**Outputs:** `MODEL`, `CLIP`, `CONDITIONING`, `PROMPT_OUT`
**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)
---
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/P5P57KRR9)
This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details.
+24 -12
View File
@@ -4,10 +4,13 @@ File: __init__.py
"""
from .nodes import (
DiffusionModelLoader,
FluxImageSaver,
DiffusionModelLoader,
FluxImageSaver,
FluxWildcardEncode,
BawkSampler
BawkSampler,
BawkBatchProcessor,
BawkControlNet,
BawkImageLoader
)
NODE_CLASS_MAPPINGS = {
@@ -15,18 +18,24 @@ NODE_CLASS_MAPPINGS = {
"FluxImageSaver": FluxImageSaver,
"FluxWildcardEncode": FluxWildcardEncode,
"BawkSampler": BawkSampler,
"BawkBatchProcessor": BawkBatchProcessor,
"BawkControlNet": BawkControlNet,
"BawkImageLoader": BawkImageLoader,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DiffusionModelLoader": "🚀 Diffusion Model Loader (Advanced)",
"FluxImageSaver": "💾 FLUX Image Saver",
"FluxWildcardEncode": "🎲 FLUX Wildcard Encoder",
"BawkSampler": "🐓 Bawk Sampler (All-in-One)",
"DiffusionModelLoader": "🚀 Bawk Model Loader",
"FluxImageSaver": "💾 Bawk Image Saver",
"FluxWildcardEncode": "🎲 Bawk Wildcard Encoder",
"BawkSampler": "🐓 Bawk Sampler",
"BawkBatchProcessor": "📁 Bawk Batch Processor",
"BawkControlNet": "🎛️ Bawk ControlNet",
"BawkImageLoader": "📸 Bawk Image Loader",
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
__version__ = "2.0.4"
__version__ = "2.0.5"
__author__ = "Bawk Nodes"
__description__ = "A complete collection of FLUX-optimized ComfyUI nodes for enhanced workflows"
@@ -34,8 +43,11 @@ __description__ = "A complete collection of FLUX-optimized ComfyUI nodes for enh
print(f"\033[1m\033[92m🐓 ComfyUI Bawk Nodes v{__version__} loaded successfully!\033[0m")
print(f"\033[1m\033[93m 🎉 Major Update - Complete FLUX Workflow Suite!\033[0m")
print(f"\033[1m\033[96m Current nodes:\033[0m")
print(f"\033[94m • 🚀 Diffusion Model Loader (Advanced) - FLUX-optimized model loading\033[0m")
print(f"\033[95m • 🎲 FLUX Wildcard Encoder - Text encoding with wildcard support and 6 LoRA slots\033[0m")
print(f"\033[92m • 💾 FLUX Image Saver - Organized image saving with metadata and prompt files\033[0m")
print(f"\033[91m • 🐓 Bawk Sampler (All-in-One) - Combined latent optimizer and sampler\033[0m")
print(f"\033[94m • 🚀 Bawk Model Loader - FLUX-optimized model loading with advanced caching\033[0m")
print(f"\033[95m • 🎲 Bawk Wildcard Encoder - Text encoding, wildcards & 6 LoRA slots\033[0m")
print(f"\033[92m • 💾 Bawk Image Saver - Organized saving with metadata, prompts & Discord webhooks\033[0m")
print(f"\033[91m • 🐓 Bawk Sampler - All-in-one text2img and img2img sampler with VAE decoding\033[0m")
print(f"\033[93m • 📁 Bawk Batch Processor - Process multiple prompts from CSV/JSON files\033[0m")
print(f"\033[96m • 🎛️ Bawk ControlNet - FLUX-optimized ControlNet preprocessing\033[0m")
print(f"\033[97m • 📸 Bawk Image Loader - Enhanced image loading with preprocessing\033[0m")
print(f"\033[3m\033[96m • 📦 Modular architecture for easy maintenance and debugging\033[0m")
+1 -1
View File
@@ -16,5 +16,5 @@ __all__ = [
]
# Version info
__version__ = "2.0.4"
__version__ = "2.0.5"
__author__ = "judd"
+7 -1
View File
@@ -7,10 +7,16 @@ from .nodes.diffusion_model_loader import DiffusionModelLoader
from .nodes.flux_wildcard_encode import FluxWildcardEncode
from .nodes.flux_image_saver import FluxImageSaver
from .nodes.bawk_sampler import BawkSampler
from .nodes.bawk_batch_processor import BawkBatchProcessor
from .nodes.bawk_controlnet import BawkControlNet
from .nodes.bawk_image_loader import BawkImageLoader
__all__ = [
"DiffusionModelLoader",
"FluxWildcardEncode",
"FluxImageSaver",
"BawkSampler"
"BawkSampler",
"BawkBatchProcessor",
"BawkControlNet",
"BawkImageLoader"
]
+8 -2
View File
@@ -7,10 +7,16 @@ from .diffusion_model_loader import DiffusionModelLoader
from .flux_wildcard_encode import FluxWildcardEncode
from .flux_image_saver import FluxImageSaver
from .bawk_sampler import BawkSampler
from .bawk_batch_processor import BawkBatchProcessor
from .bawk_controlnet import BawkControlNet
from .bawk_image_loader import BawkImageLoader
__all__ = [
"DiffusionModelLoader",
"FluxWildcardEncode",
"FluxWildcardEncode",
"FluxImageSaver",
"BawkSampler"
"BawkSampler",
"BawkBatchProcessor",
"BawkControlNet",
"BawkImageLoader"
]
+298
View File
@@ -0,0 +1,298 @@
"""
BawkBatchProcessor - Process multiple prompts from CSV/JSON files
File: nodes/bawk_batch_processor.py
"""
import os
import csv
import json
from typing import List, Dict, Any, Tuple
# ComfyUI imports with fallback
try:
import folder_paths
except ImportError:
folder_paths = None
# Optional pandas import for advanced CSV handling
try:
import pandas as pd
HAS_PANDAS = True
except ImportError:
HAS_PANDAS = False
class BawkBatchProcessor:
"""
Process multiple prompts and settings from CSV or JSON files.
Supports batch generation with different parameters per image.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"batch_file": ("STRING", {
"default": "",
"tooltip": "Path to CSV or JSON file with batch settings. CSV columns: prompt, seed, steps, guidance, etc."
}),
"file_format": (["Auto-detect", "CSV", "JSON"], {
"default": "Auto-detect",
"tooltip": "File format - Auto-detect will guess from extension"
}),
"batch_index": ("INT", {
"default": 0, "min": 0, "max": 999999,
"tooltip": "Which row/item to process from the batch file (0-based index)"
}),
"preview_only": ("BOOLEAN", {
"default": False,
"tooltip": "Show file contents without processing - useful for checking batch files"
}),
},
"optional": {
# Override defaults
"default_resolution": ("STRING", {
"default": "FHD 16:9 - 1920x1080 - 2.1MP",
"tooltip": "Default resolution for items that don't specify one"
}),
"default_steps": ("INT", {
"default": 30, "min": 1, "max": 100,
"tooltip": "Default steps for items that don't specify them"
}),
"default_guidance": ("FLOAT", {
"default": 3.5, "min": 0.0, "max": 20.0, "step": 0.1,
"tooltip": "Default guidance for items that don't specify it"
}),
}
}
RETURN_TYPES = ("STRING", "INT", "INT", "FLOAT", "STRING", "STRING")
RETURN_NAMES = ("prompt", "seed", "steps", "guidance", "resolution", "batch_info")
FUNCTION = "process_batch"
CATEGORY = "BawkNodes/batch"
DESCRIPTION = "Process prompts and settings from CSV/JSON files for batch generation"
def process_batch(
self,
batch_file,
file_format="Auto-detect",
batch_index=0,
preview_only=False,
default_resolution="FHD 16:9 - 1920x1080 - 2.1MP",
default_steps=30,
default_guidance=3.5
):
"""
Process a batch file and return settings for the specified index
"""
try:
print(f"[BawkBatchProcessor] Processing batch file: {batch_file}")
if not batch_file or not os.path.exists(batch_file):
raise ValueError(f"Batch file not found: {batch_file}")
# Load batch data
batch_data = self._load_batch_file(batch_file, file_format)
if preview_only:
return self._preview_batch_file(batch_data)
# Validate index
if batch_index >= len(batch_data):
raise ValueError(f"Batch index {batch_index} out of range. File contains {len(batch_data)} items.")
# Get the specific batch item
batch_item = batch_data[batch_index]
# Extract settings with defaults
settings = self._extract_settings(
batch_item, default_resolution, default_steps, default_guidance
)
batch_info = f"Processing item {batch_index + 1}/{len(batch_data)} from {os.path.basename(batch_file)}"
print(f"[BawkBatchProcessor] {batch_info}")
return (
settings["prompt"],
settings["seed"],
settings["steps"],
settings["guidance"],
settings["resolution"],
batch_info
)
except Exception as e:
error_msg = f"Batch processing failed: {str(e)}"
print(f"[BawkBatchProcessor] Error: {error_msg}")
# Return safe defaults on error
return (
f"Error: {error_msg}",
0,
default_steps,
default_guidance,
default_resolution,
f"Error processing batch file"
)
def _load_batch_file(self, file_path: str, file_format: str) -> List[Dict]:
"""Load batch data from CSV or JSON file"""
# Auto-detect format
if file_format == "Auto-detect":
ext = os.path.splitext(file_path)[1].lower()
if ext == '.csv':
file_format = "CSV"
elif ext == '.json':
file_format = "JSON"
else:
raise ValueError(f"Cannot auto-detect format for extension: {ext}")
if file_format == "CSV":
return self._load_csv_file(file_path)
elif file_format == "JSON":
return self._load_json_file(file_path)
else:
raise ValueError(f"Unsupported file format: {file_format}")
def _load_csv_file(self, file_path: str) -> List[Dict]:
"""Load batch data from CSV file"""
try:
batch_data = []
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
# Clean up the row data
clean_row = {k.strip(): v.strip() if isinstance(v, str) else v
for k, v in row.items() if k and k.strip()}
if clean_row: # Only add non-empty rows
batch_data.append(clean_row)
print(f"[BawkBatchProcessor] Loaded {len(batch_data)} items from CSV")
return batch_data
except Exception as e:
raise ValueError(f"Failed to load CSV file: {str(e)}")
def _load_json_file(self, file_path: str) -> List[Dict]:
"""Load batch data from JSON file"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Handle different JSON structures
if isinstance(data, list):
batch_data = data
elif isinstance(data, dict):
# Try common keys for batch data
for key in ['items', 'prompts', 'batch', 'data']:
if key in data and isinstance(data[key], list):
batch_data = data[key]
break
else:
# Single item wrapped in dict
batch_data = [data]
else:
raise ValueError("JSON must contain a list or dict with batch data")
print(f"[BawkBatchProcessor] Loaded {len(batch_data)} items from JSON")
return batch_data
except Exception as e:
raise ValueError(f"Failed to load JSON file: {str(e)}")
def _extract_settings(
self, batch_item: Dict, default_resolution: str, default_steps: int, default_guidance: float
) -> Dict[str, Any]:
"""Extract and validate settings from a batch item"""
settings = {}
# Extract prompt (required)
prompt_keys = ['prompt', 'text', 'description', 'input']
settings['prompt'] = ""
for key in prompt_keys:
if key in batch_item and batch_item[key]:
settings['prompt'] = str(batch_item[key])
break
if not settings['prompt']:
raise ValueError(f"No prompt found in batch item. Looked for keys: {prompt_keys}")
# Extract seed
seed_keys = ['seed', 'random_seed', 'noise_seed']
settings['seed'] = 0
for key in seed_keys:
if key in batch_item:
try:
settings['seed'] = int(float(batch_item[key]))
break
except (ValueError, TypeError):
continue
# Extract steps
steps_keys = ['steps', 'sampling_steps', 'iterations']
settings['steps'] = default_steps
for key in steps_keys:
if key in batch_item:
try:
steps = int(float(batch_item[key]))
if 1 <= steps <= 100:
settings['steps'] = steps
break
except (ValueError, TypeError):
continue
# Extract guidance
guidance_keys = ['guidance', 'guidance_scale', 'cfg', 'cfg_scale']
settings['guidance'] = default_guidance
for key in guidance_keys:
if key in batch_item:
try:
guidance = float(batch_item[key])
if 0.0 <= guidance <= 20.0:
settings['guidance'] = guidance
break
except (ValueError, TypeError):
continue
# Extract resolution
resolution_keys = ['resolution', 'size', 'dimensions', 'preset']
settings['resolution'] = default_resolution
for key in resolution_keys:
if key in batch_item and batch_item[key]:
settings['resolution'] = str(batch_item[key])
break
return settings
def _preview_batch_file(self, batch_data: List[Dict]) -> Tuple[str, int, int, float, str, str]:
"""Preview the contents of a batch file"""
preview_info = []
preview_info.append(f"📁 Batch File Preview - {len(batch_data)} items total")
preview_info.append("")
# Show first few items
preview_count = min(5, len(batch_data))
for i, item in enumerate(batch_data[:preview_count]):
preview_info.append(f"🔹 Item {i + 1}:")
# Show key fields
if 'prompt' in item:
prompt_preview = item['prompt'][:100] + "..." if len(item['prompt']) > 100 else item['prompt']
preview_info.append(f" Prompt: {prompt_preview}")
for key in ['seed', 'steps', 'guidance', 'resolution']:
if key in item:
preview_info.append(f" {key.title()}: {item[key]}")
preview_info.append("")
if len(batch_data) > preview_count:
preview_info.append(f"... and {len(batch_data) - preview_count} more items")
preview_text = "\\n".join(preview_info)
batch_info = f"Preview mode: {len(batch_data)} items loaded"
return (preview_text, 0, 30, 3.5, "FHD 16:9 - 1920x1080 - 2.1MP", batch_info)
+330
View File
@@ -0,0 +1,330 @@
"""
BawkControlNet - FLUX ControlNet Integration
File: nodes/bawk_controlnet.py
"""
import torch
import numpy as np
from typing import Tuple, Any
class BawkControlNet:
"""
FLUX-optimized ControlNet preprocessing and integration.
Handles common ControlNet types with smart preprocessing.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {
"tooltip": "Input image for ControlNet processing"
}),
"control_type": ([
"Canny Edge",
"Depth Map",
"Normal Map",
"Pose/OpenPose",
"Segmentation",
"Scribble",
"Lineart",
"QR Code",
"Custom/Raw"
], {
"default": "Canny Edge",
"tooltip": "Type of ControlNet preprocessing to apply"
}),
"strength": ("FLOAT", {
"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01,
"tooltip": "ControlNet influence strength"
}),
"start_percent": ("FLOAT", {
"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "When to start applying ControlNet (0.0 = from beginning)"
}),
"end_percent": ("FLOAT", {
"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "When to stop applying ControlNet (1.0 = until end)"
}),
},
"optional": {
# Canny specific
"canny_low_threshold": ("INT", {
"default": 100, "min": 1, "max": 255,
"tooltip": "Canny edge detection low threshold"
}),
"canny_high_threshold": ("INT", {
"default": 200, "min": 1, "max": 255,
"tooltip": "Canny edge detection high threshold"
}),
# Depth specific
"depth_near": ("FLOAT", {
"default": 0.1, "min": 0.01, "max": 10.0, "step": 0.01,
"tooltip": "Near plane for depth normalization"
}),
"depth_far": ("FLOAT", {
"default": 100.0, "min": 1.0, "max": 1000.0, "step": 1.0,
"tooltip": "Far plane for depth normalization"
}),
# Advanced options
"preprocessor_resolution": ("INT", {
"default": 512, "min": 256, "max": 2048, "step": 64,
"tooltip": "Resolution for preprocessing (will be resized back to original)"
}),
"auto_resize": ("BOOLEAN", {
"default": True,
"tooltip": "Automatically resize control image to match generation resolution"
}),
}
}
RETURN_TYPES = ("IMAGE", "CONDITIONING", "STRING")
RETURN_NAMES = ("control_image", "control_conditioning", "control_info")
FUNCTION = "process_controlnet"
CATEGORY = "BawkNodes/control"
DESCRIPTION = "FLUX-optimized ControlNet preprocessing and integration"
def process_controlnet(
self,
image,
control_type="Canny Edge",
strength=1.0,
start_percent=0.0,
end_percent=1.0,
canny_low_threshold=100,
canny_high_threshold=200,
depth_near=0.1,
depth_far=100.0,
preprocessor_resolution=512,
auto_resize=True
):
"""
Process image with ControlNet preprocessing
"""
try:
print(f"[BawkControlNet] Processing {control_type} with strength {strength}")
# Validate parameters
self._validate_parameters(strength, start_percent, end_percent)
# Preprocess the control image
control_image = self._preprocess_image(
image, control_type,
canny_low_threshold, canny_high_threshold,
depth_near, depth_far, preprocessor_resolution
)
# Create control conditioning (placeholder for now - would integrate with actual ControlNet)
control_conditioning = self._create_control_conditioning(
control_image, strength, start_percent, end_percent
)
# Generate info string
control_info = self._generate_control_info(
control_type, strength, start_percent, end_percent, image.shape
)
print(f"[BawkControlNet] ✅ {control_info}")
return (control_image, control_conditioning, control_info)
except Exception as e:
error_msg = f"ControlNet processing failed: {str(e)}"
print(f"[BawkControlNet] ❌ {error_msg}")
# Return safe defaults
return (image, [], error_msg)
def _validate_parameters(self, strength: float, start_percent: float, end_percent: float):
"""Validate ControlNet parameters and provide feedback"""
if strength > 1.5:
print(f"[BawkControlNet] ⚠️ High strength ({strength}) may cause over-conditioning")
elif strength < 0.3:
print(f"[BawkControlNet] ℹ️ Low strength ({strength}) may have minimal effect")
if start_percent >= end_percent:
print(f"[BawkControlNet] ⚠️ Start percent ({start_percent}) should be < end percent ({end_percent})")
if end_percent - start_percent < 0.2:
print(f"[BawkControlNet] ℹ️ Short control duration ({end_percent - start_percent:.1%}) may have limited effect")
def _preprocess_image(
self,
image,
control_type: str,
canny_low: int,
canny_high: int,
depth_near: float,
depth_far: float,
resolution: int
):
"""Preprocess image based on control type"""
# Convert to numpy for processing
if len(image.shape) == 4:
img_np = image[0].cpu().numpy() # Take first image from batch
else:
img_np = image.cpu().numpy()
# Convert from 0-1 to 0-255
img_np = (img_np * 255).astype(np.uint8)
if control_type == "Canny Edge":
processed = self._apply_canny_edge(img_np, canny_low, canny_high)
elif control_type == "Depth Map":
processed = self._apply_depth_processing(img_np, depth_near, depth_far)
elif control_type == "Normal Map":
processed = self._apply_normal_map(img_np)
elif control_type == "Pose/OpenPose":
processed = self._apply_pose_detection(img_np)
elif control_type == "Segmentation":
processed = self._apply_segmentation(img_np)
elif control_type == "Scribble":
processed = self._apply_scribble_effect(img_np)
elif control_type == "Lineart":
processed = self._apply_lineart(img_np)
elif control_type == "QR Code":
processed = self._apply_qr_processing(img_np)
else: # Custom/Raw
processed = img_np
print(f"[BawkControlNet] Using raw image without preprocessing")
# Convert back to tensor
processed_tensor = torch.from_numpy(processed.astype(np.float32) / 255.0)
# Ensure correct shape [batch, height, width, channels]
if len(processed_tensor.shape) == 3:
processed_tensor = processed_tensor.unsqueeze(0)
return processed_tensor
def _apply_canny_edge(self, img_np: np.ndarray, low_thresh: int, high_thresh: int):
"""Apply Canny edge detection"""
try:
import cv2
# Convert to grayscale if needed
if len(img_np.shape) == 3:
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
else:
gray = img_np
# Apply Canny edge detection
edges = cv2.Canny(gray, low_thresh, high_thresh)
# Convert back to 3-channel
edges_rgb = np.stack([edges, edges, edges], axis=-1)
print(f"[BawkControlNet] Applied Canny edge detection (thresholds: {low_thresh}, {high_thresh})")
return edges_rgb
except ImportError:
print(f"[BawkControlNet] ⚠️ OpenCV not available, using simple edge detection")
return self._simple_edge_detection(img_np)
def _simple_edge_detection(self, img_np: np.ndarray):
"""Simple edge detection fallback without OpenCV"""
# Convert to grayscale
if len(img_np.shape) == 3:
gray = np.mean(img_np, axis=-1)
else:
gray = img_np
# Simple gradient-based edge detection
grad_x = np.abs(np.diff(gray, axis=1, prepend=gray[:, :1]))
grad_y = np.abs(np.diff(gray, axis=0, prepend=gray[:1, :]))
edges = np.sqrt(grad_x**2 + grad_y**2)
# Threshold and normalize
edges = np.clip(edges * 3, 0, 255).astype(np.uint8)
# Convert to 3-channel
edges_rgb = np.stack([edges, edges, edges], axis=-1)
return edges_rgb
def _apply_depth_processing(self, img_np: np.ndarray, near: float, far: float):
"""Apply depth map processing (placeholder)"""
# Simple luminance-based depth approximation
if len(img_np.shape) == 3:
depth = np.mean(img_np, axis=-1)
else:
depth = img_np
# Normalize depth
depth = np.clip((depth - near * 255) / ((far - near) * 255), 0, 1) * 255
depth_rgb = np.stack([depth, depth, depth], axis=-1).astype(np.uint8)
print(f"[BawkControlNet] Applied depth processing (range: {near}-{far})")
return depth_rgb
def _apply_normal_map(self, img_np: np.ndarray):
"""Apply normal map processing (placeholder)"""
print(f"[BawkControlNet] Applied normal map processing (simplified)")
return img_np # Placeholder - would need actual normal map generation
def _apply_pose_detection(self, img_np: np.ndarray):
"""Apply pose detection (placeholder)"""
print(f"[BawkControlNet] Applied pose detection (placeholder)")
return np.zeros_like(img_np) # Placeholder - would need pose detection model
def _apply_segmentation(self, img_np: np.ndarray):
"""Apply segmentation (placeholder)"""
print(f"[BawkControlNet] Applied segmentation (placeholder)")
return img_np # Placeholder - would need segmentation model
def _apply_scribble_effect(self, img_np: np.ndarray):
"""Apply scribble effect"""
# Simple edge-based scribble effect
edges = self._simple_edge_detection(img_np)
# Thin the edges for scribble effect
scribble = np.where(edges > 128, 255, 0).astype(np.uint8)
print(f"[BawkControlNet] Applied scribble effect")
return scribble
def _apply_lineart(self, img_np: np.ndarray):
"""Apply lineart processing"""
# Similar to scribble but with cleaner lines
edges = self._simple_edge_detection(img_np)
lineart = np.where(edges > 100, 255, 0).astype(np.uint8)
print(f"[BawkControlNet] Applied lineart processing")
return lineart
def _apply_qr_processing(self, img_np: np.ndarray):
"""Apply QR code processing"""
# High contrast black and white
if len(img_np.shape) == 3:
gray = np.mean(img_np, axis=-1)
else:
gray = img_np
qr = np.where(gray > 128, 255, 0).astype(np.uint8)
qr_rgb = np.stack([qr, qr, qr], axis=-1)
print(f"[BawkControlNet] Applied QR code processing")
return qr_rgb
def _create_control_conditioning(self, control_image, strength: float, start: float, end: float):
"""Create control conditioning (placeholder for actual ControlNet integration)"""
# This would integrate with actual ControlNet models in a real implementation
# For now, return empty conditioning as placeholder
conditioning_info = {
"control_image": control_image,
"strength": strength,
"start_percent": start,
"end_percent": end
}
return [] # Placeholder - would return actual conditioning
def _generate_control_info(self, control_type: str, strength: float, start: float, end: float, image_shape):
"""Generate informational string about the control setup"""
height, width = image_shape[-2:]
duration = end - start
info = f"{control_type} | Strength: {strength:.2f} | Duration: {start:.0%}-{end:.0%} ({duration:.0%}) | Size: {width}x{height}"
return info
+280
View File
@@ -0,0 +1,280 @@
"""
BawkImageLoader - Enhanced Image Loading with Preprocessing
File: nodes/bawk_image_loader.py
"""
import os
import numpy as np
import torch
from PIL import Image, ImageOps, ExifTags
from typing import Tuple, Any
# ComfyUI imports with fallback
try:
import folder_paths
from comfy.utils import common_upsampling_factor
except ImportError:
folder_paths = None
common_upsampling_factor = None
class BawkImageLoader:
"""
Enhanced image loader with file browser and preprocessing options.
Compatible with ComfyUI's Load Image node but with additional features.
"""
@classmethod
def INPUT_TYPES(cls):
# Get available images from ComfyUI input directory
input_dir = folder_paths.get_input_directory() if folder_paths else "input"
files = []
if os.path.exists(input_dir):
files = [f for f in os.listdir(input_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.gif'))]
return {
"required": {
"image": (sorted(files), {
"image_upload": True,
"tooltip": "Select image file or upload new one"
}),
},
"optional": {
"auto_orient": ("BOOLEAN", {
"default": True,
"tooltip": "Automatically rotate image based on EXIF orientation data"
}),
"target_size": (["Original", "512", "768", "1024", "1536", "2048"], {
"default": "Original",
"tooltip": "Resize image to target size (maintains aspect ratio)"
}),
"resize_method": (["Lanczos", "Bilinear", "Bicubic", "Nearest"], {
"default": "Lanczos",
"tooltip": "Resampling method for resizing"
}),
"pad_to_square": ("BOOLEAN", {
"default": False,
"tooltip": "Pad image to square aspect ratio with black borders"
}),
"normalize_colors": ("BOOLEAN", {
"default": True,
"tooltip": "Normalize color values to 0-1 range for better FLUX compatibility"
}),
"create_mask": ("BOOLEAN", {
"default": False,
"tooltip": "Generate mask output (white = opaque, black = transparent)"
}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT", "INT")
RETURN_NAMES = ("image", "mask", "filename", "width", "height")
FUNCTION = "load_image"
CATEGORY = "BawkNodes/image"
DESCRIPTION = "Enhanced image loader with file browser, preprocessing, and mask support"
@classmethod
def IS_CHANGED(cls, image, **kwargs):
"""Check if image file has changed"""
if folder_paths:
image_path = folder_paths.get_annotated_filepath(image)
if os.path.exists(image_path):
return os.path.getmtime(image_path)
return float("inf")
@classmethod
def VALIDATE_INPUTS(cls, image, **kwargs):
"""Validate that the image file exists"""
if not folder_paths:
return True
if not image:
return "No image selected"
image_path = folder_paths.get_annotated_filepath(image)
if not os.path.exists(image_path):
return f"Image file does not exist: {image}"
return True
def load_image(
self,
image,
auto_orient=True,
target_size="Original",
resize_method="Lanczos",
pad_to_square=False,
normalize_colors=True,
create_mask=False
):
"""
Load and preprocess image with various options
"""
try:
print(f"[BawkImageLoader] Loading image: {image}")
# Get full path using ComfyUI's folder_paths
if folder_paths:
image_path = folder_paths.get_annotated_filepath(image)
else:
image_path = image
if not os.path.exists(image_path):
raise ValueError(f"Image file not found: {image_path}")
# Load image with PIL
pil_image = Image.open(image_path)
filename = os.path.basename(image_path)
# Store original dimensions
original_width, original_height = pil_image.size
print(f"[BawkImageLoader] Original size: {original_width}x{original_height}")
# Handle EXIF orientation
if auto_orient:
pil_image = self._auto_orient_image(pil_image)
if pil_image.size != (original_width, original_height):
print(f"[BawkImageLoader] Auto-rotated image to: {pil_image.size[0]}x{pil_image.size[1]}")
# Convert to RGB if needed
if pil_image.mode not in ('RGB', 'RGBA'):
print(f"[BawkImageLoader] Converting from {pil_image.mode} to RGB")
pil_image = pil_image.convert('RGB')
# Resize if requested
if target_size != "Original":
pil_image = self._resize_image(pil_image, int(target_size), resize_method)
print(f"[BawkImageLoader] Resized to: {pil_image.size[0]}x{pil_image.size[1]}")
# Pad to square if requested
if pad_to_square:
pil_image = self._pad_to_square(pil_image)
print(f"[BawkImageLoader] Padded to square: {pil_image.size[0]}x{pil_image.size[1]}")
# Convert to tensor
image_tensor = self._pil_to_tensor(pil_image, normalize_colors)
# Create mask if requested
mask_tensor = self._create_mask_tensor(pil_image) if create_mask else torch.zeros((1, pil_image.size[1], pil_image.size[0]), dtype=torch.float32)
final_width, final_height = pil_image.size
print(f"[BawkImageLoader] ✅ Successfully loaded {filename} ({final_width}x{final_height})")
return (image_tensor, mask_tensor, filename, final_width, final_height)
except Exception as e:
error_msg = f"Failed to load image: {str(e)}"
print(f"[BawkImageLoader] ❌ {error_msg}")
# Return a small black image as fallback
fallback_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
fallback_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
return (fallback_image, fallback_mask, f"Error: {error_msg}", 64, 64)
def _auto_orient_image(self, image: Image.Image) -> Image.Image:
"""Auto-rotate image based on EXIF orientation"""
try:
# Get EXIF data
exif = image._getexif()
if exif is not None:
# Find orientation tag
for tag, value in exif.items():
if tag in ExifTags.TAGS and ExifTags.TAGS[tag] == 'Orientation':
# Apply rotation based on orientation value
if value == 3:
image = image.rotate(180, expand=True)
elif value == 6:
image = image.rotate(270, expand=True)
elif value == 8:
image = image.rotate(90, expand=True)
break
except (AttributeError, KeyError, TypeError):
# No EXIF data or orientation tag, use ImageOps fallback
try:
image = ImageOps.exif_transpose(image)
except Exception:
pass # Keep original orientation
return image
def _resize_image(self, image: Image.Image, target_size: int, method: str) -> Image.Image:
"""Resize image maintaining aspect ratio"""
# Map method names to PIL constants
method_map = {
"Lanczos": Image.Resampling.LANCZOS,
"Bilinear": Image.Resampling.BILINEAR,
"Bicubic": Image.Resampling.BICUBIC,
"Nearest": Image.Resampling.NEAREST
}
resample_method = method_map.get(method, Image.Resampling.LANCZOS)
# Calculate new size maintaining aspect ratio
width, height = image.size
aspect_ratio = width / height
if width > height:
new_width = target_size
new_height = int(target_size / aspect_ratio)
else:
new_height = target_size
new_width = int(target_size * aspect_ratio)
# Ensure dimensions are even numbers (better for some models)
new_width = (new_width // 2) * 2
new_height = (new_height // 2) * 2
return image.resize((new_width, new_height), resample_method)
def _pad_to_square(self, image: Image.Image) -> Image.Image:
"""Pad image to square with black borders"""
width, height = image.size
max_size = max(width, height)
# Create new square image with black background
square_image = Image.new('RGB', (max_size, max_size), (0, 0, 0))
# Calculate position to center the original image
x_offset = (max_size - width) // 2
y_offset = (max_size - height) // 2
# Paste original image onto square background
square_image.paste(image, (x_offset, y_offset))
return square_image
def _pil_to_tensor(self, image: Image.Image, normalize: bool = True) -> torch.Tensor:
"""Convert PIL image to tensor format expected by ComfyUI"""
# Convert to numpy array
image_np = np.array(image)
# Normalize to 0-1 range if requested
if normalize:
image_np = image_np.astype(np.float32) / 255.0
else:
image_np = image_np.astype(np.float32)
# Convert to tensor and add batch dimension [batch, height, width, channels]
image_tensor = torch.from_numpy(image_np).unsqueeze(0)
return image_tensor
def _create_mask_tensor(self, image: Image.Image) -> torch.Tensor:
"""Create mask tensor from image alpha channel or full white mask"""
if image.mode == 'RGBA':
# Use alpha channel as mask
alpha = image.split()[-1] # Get alpha channel
mask_np = np.array(alpha).astype(np.float32) / 255.0
else:
# Create full white mask (opaque)
mask_np = np.ones((image.size[1], image.size[0]), dtype=np.float32)
# Convert to tensor [batch, height, width]
mask_tensor = torch.from_numpy(mask_np).unsqueeze(0)
return mask_tensor
def _get_supported_formats(self):
"""Get list of supported image formats"""
return ['.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.tif']
+71 -14
View File
@@ -128,7 +128,7 @@ class BawkSampler:
# Latent generation
"resolution": (resolution_presets, {
"default": "FHD 16:9 - 1920x1080",
"default": "FHD 16:9 - 1920x1080 - 2.1MP",
"tooltip": "Select resolution preset or custom option"
}),
"batch_size": ("INT", {
@@ -167,9 +167,9 @@ class BawkSampler:
}),
"denoise": ("FLOAT", {
"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01,
"tooltip": "Denoise strength"
"tooltip": "Denoise strength - Use 1.0 for text-to-image, 0.6-0.9 for image-to-image"
}),
# Custom resolution toggle
"use_custom_resolution": ("BOOLEAN", {
"default": False,
@@ -177,6 +177,11 @@ class BawkSampler:
}),
},
"optional": {
# Img2Img support
"input_image": ("IMAGE", {
"tooltip": "Input image for img2img generation. Leave empty for text-to-image mode."
}),
# Custom resolution (only used when use_custom_resolution=True)
"custom_width": ("INT", {
"default": 1920, "min": 64, "max": 4096, "step": 64,
@@ -193,30 +198,36 @@ class BawkSampler:
RETURN_NAMES = ("images", "latent")
FUNCTION = "generate_sample_and_decode"
CATEGORY = "BawkNodes/sampling"
DESCRIPTION = "Combined latent generator, sampler, and VAE decoder optimized for FLUX models"
DESCRIPTION = "All-in-one FLUX sampler with text-to-image and image-to-image support, including VAE decoding"
def generate_sample_and_decode(
self,
model, conditioning, vae,
resolution="FHD 16:9 - 1920x1080", batch_size=4,
resolution="FHD 16:9 - 1920x1080 - 2.1MP", 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
use_custom_resolution=False, input_image=None, custom_width=1920, custom_height=1080
):
"""
Generate optimized latent, perform FLUX sampling, and decode to images
"""
try:
# Step 0: Smart validation with user feedback
is_img2img = input_image is not None
self._validate_parameters_with_feedback(
model, batch_size, steps, guidance, max_shift, base_shift,
resolution, use_custom_resolution, custom_width, custom_height
resolution, use_custom_resolution, custom_width, custom_height, is_img2img, denoise
)
# Step 1: Generate optimized latent
latent = self._generate_optimized_latent(
resolution, batch_size, use_custom_resolution, custom_width, custom_height
)
# Step 1: Generate or encode latent (img2img vs txt2img)
if is_img2img:
print(f"[BawkSampler] Using img2img mode with denoise strength: {denoise}")
latent = self._encode_image_to_latent(vae, input_image, batch_size)
else:
print(f"[BawkSampler] Using text-to-image mode")
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(
@@ -368,7 +379,7 @@ class BawkSampler:
def _validate_parameters_with_feedback(
self, model, batch_size, steps, guidance, max_shift, base_shift,
resolution, use_custom_resolution, custom_width, custom_height
resolution, use_custom_resolution, custom_width, custom_height, is_img2img=False, denoise=1.0
):
"""Smart validation with user-friendly feedback and recommendations"""
@@ -404,10 +415,22 @@ class BawkSampler:
elif total_pixels > 2048 * 2048:
print(f"[BawkSampler] ℹ️ INFO: High resolution ({custom_width}x{custom_height}) detected. Ensure sufficient VRAM.")
# Img2Img specific validation
if is_img2img:
if denoise == 1.0:
print(f"[BawkSampler] ℹ️ IMG2IMG: Denoise at 1.0 will completely replace input image. Consider 0.6-0.9 for image modification.")
elif denoise < 0.3:
print(f"[BawkSampler] ℹ️ IMG2IMG: Very low denoise ({denoise}) may result in minimal changes to input image.")
elif denoise > 0.9:
print(f"[BawkSampler] ℹ️ IMG2IMG: High denoise ({denoise}) will heavily modify the input image.")
else:
print(f"[BawkSampler] ✅ IMG2IMG: Good denoise strength ({denoise}) for image modification.")
# Memory estimation and recommendations
self._estimate_memory_usage(batch_size, resolution, use_custom_resolution, custom_width, custom_height)
print(f"[BawkSampler] ✅ Validation complete. Proceeding with generation...")
mode_str = "img2img" if is_img2img else "text-to-image"
print(f"[BawkSampler] ✅ Validation complete. Proceeding with {mode_str} generation...")
def _estimate_memory_usage(self, batch_size, resolution, use_custom_resolution, custom_width, custom_height):
"""Estimate and report memory usage"""
@@ -509,4 +532,38 @@ class BawkSampler:
)
# Generic fallback
return (error_msg, "Check the console for detailed error information and ensure all inputs are valid")
return (error_msg, "Check the console for detailed error information and ensure all inputs are valid")
def _encode_image_to_latent(self, vae, input_image, batch_size):
"""Encode input image to latent for img2img processing"""
try:
print(f"[BawkSampler] Encoding input image to latent space...")
# Handle batch size adjustment
if len(input_image.shape) == 4: # Batch dimension exists
image_batch_size = input_image.shape[0]
if image_batch_size == 1 and batch_size > 1:
# Repeat single image for batch
input_image = input_image.repeat(batch_size, 1, 1, 1)
print(f"[BawkSampler] Expanded single input image to batch size {batch_size}")
elif image_batch_size != batch_size:
# Use first image and repeat if needed
input_image = input_image[0:1].repeat(batch_size, 1, 1, 1)
print(f"[BawkSampler] Using first image from batch, expanded to batch size {batch_size}")
# Encode image to latent using VAE
latent_samples = vae.encode(input_image)
# Create latent dictionary
latent = {"samples": latent_samples}
image_h, image_w = input_image.shape[-2:]
latent_h, latent_w = latent_samples.shape[-2:]
print(f"[BawkSampler] Encoded image {image_w}x{image_h} to latent {latent_w}x{latent_h}")
return latent
except Exception as e:
error_msg = f"Failed to encode input image to latent: {str(e)}"
print(f"[BawkSampler] Error: {error_msg}")
raise RuntimeError(error_msg)
+52 -10
View File
@@ -44,7 +44,7 @@ class FluxWildcardEncode:
# Get available LoRA files
lora_list = folder_paths.get_filename_list("loras")
lora_options = ["None"] + lora_list
return {
"required": {
"model": ("MODEL",),
@@ -92,7 +92,7 @@ class FluxWildcardEncode:
RETURN_NAMES = ("MODEL", "CLIP", "CONDITIONING", "PROMPT_OUT")
FUNCTION = "encode_with_loras"
CATEGORY = "BawkNodes/conditioning"
DESCRIPTION = "🎲 FLUX Wildcard Encoder with Dynamic LoRA Support"
DESCRIPTION = "FLUX text encoder with wildcard support and 6 LoRA slots"
def encode_with_loras(self, model, clip, prompt, wildcard_seed=-1, **kwargs):
"""
@@ -101,7 +101,10 @@ class FluxWildcardEncode:
try:
print(f"[FluxWildcardEncode] Starting encode with dynamic LoRAs")
print(f"[FluxWildcardEncode] Received kwargs: {list(kwargs.keys())}")
# Clean up kwargs - handle ComfyUI's string conversion issues
cleaned_kwargs = self._clean_kwargs(kwargs)
# Step 1: Process wildcards if seed is provided
processed_prompt = prompt
if wildcard_seed != -1:
@@ -121,17 +124,17 @@ class FluxWildcardEncode:
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")
lora_strength = kwargs.get(strength_key, 1.0)
# Get cleaned values for this LoRA slot
lora_enabled = cleaned_kwargs.get(on_key, False)
lora_name = cleaned_kwargs.get(name_key, "None")
lora_strength = cleaned_kwargs.get(strength_key, 1.0)
# Validate LoRA configuration and provide feedback
if lora_enabled and lora_name != "None":
self._validate_lora_config(i, lora_name, lora_strength, lora_warnings)
if lora_enabled and lora_name and lora_name != "None":
strength = kwargs.get(strength_key, 1.00)
strength = lora_strength
# Skip if strength is zero
if strength == 0:
@@ -283,4 +286,43 @@ class FluxWildcardEncode:
elif "pose" in lora_lower and strength > 1.0:
print(f"[FluxWildcardEncode] 💡 Pose LoRAs often work better at 0.6-0.9 strength")
elif "lighting" in lora_lower and strength > 0.8:
print(f"[FluxWildcardEncode] 💡 Lighting LoRAs typically work best at 0.4-0.7 strength")
print(f"[FluxWildcardEncode] 💡 Lighting LoRAs typically work best at 0.4-0.7 strength")
def _clean_kwargs(self, kwargs):
"""Clean kwargs to handle ComfyUI's string conversion issues"""
cleaned = {}
for key, value in kwargs.items():
if key.endswith('_on'):
# Boolean parameters
if isinstance(value, str):
cleaned[key] = value.lower() in ('true', '1', 'yes', 'on')
else:
cleaned[key] = bool(value)
elif key.endswith('_strength'):
# Float parameters
if isinstance(value, str):
if value.lower() in ('none', '', 'null'):
cleaned[key] = 1.0 # Default strength
else:
try:
cleaned[key] = float(value)
except ValueError:
cleaned[key] = 1.0 # Default on error
else:
cleaned[key] = float(value) if value is not None else 1.0
elif key.endswith('_name'):
# String parameters (LoRA names)
if isinstance(value, str):
if value.lower() in ('false', 'none', '', 'null'):
cleaned[key] = "None"
else:
cleaned[key] = value
else:
cleaned[key] = str(value) if value is not None else "None"
else:
# Other parameters - pass through
cleaned[key] = value
return cleaned
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "comfyui-bawknodes"
version = "2.0.4"
version = "2.0.5"
description = "A complete collection of FLUX-optimized ComfyUI nodes for enhanced AI image generation workflows."
readme = "README.md"
license = { file = "LICENSE" }