diff --git a/README.md b/README.md
index 0f7c7b0..6e35dae 100644
--- a/README.md
+++ b/README.md
@@ -1,332 +1,173 @@
-
đ ComfyUI Bawk Nodes v2.0.4
+ComfyUI Bawk Nodes v2.0.5
-**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*
+
+[](https://github.com/comfyanonymous/ComfyUI)
+[](https://blackforestlabs.ai/)
+[]()
-
+---
+
+## **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)
-
----
-
-[](https://ko-fi.com/P5P57KRR9)
\ No newline at end of file
+This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details.
diff --git a/__init__.py b/__init__.py
index abdcb0e..e8a4512 100644
--- a/__init__.py
+++ b/__init__.py
@@ -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")
\ No newline at end of file
diff --git a/modules/__init__.py b/modules/__init__.py
index 6724b70..7cdd470 100644
--- a/modules/__init__.py
+++ b/modules/__init__.py
@@ -16,5 +16,5 @@ __all__ = [
]
# Version info
-__version__ = "2.0.4"
+__version__ = "2.0.5"
__author__ = "judd"
\ No newline at end of file
diff --git a/nodes.py b/nodes.py
index 6a5e690..432e3e0 100644
--- a/nodes.py
+++ b/nodes.py
@@ -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"
]
\ No newline at end of file
diff --git a/nodes/__init__.py b/nodes/__init__.py
index a456b7b..2902897 100644
--- a/nodes/__init__.py
+++ b/nodes/__init__.py
@@ -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"
]
\ No newline at end of file
diff --git a/nodes/bawk_batch_processor.py b/nodes/bawk_batch_processor.py
new file mode 100644
index 0000000..a574f42
--- /dev/null
+++ b/nodes/bawk_batch_processor.py
@@ -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)
\ No newline at end of file
diff --git a/nodes/bawk_controlnet.py b/nodes/bawk_controlnet.py
new file mode 100644
index 0000000..9aa7249
--- /dev/null
+++ b/nodes/bawk_controlnet.py
@@ -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
\ No newline at end of file
diff --git a/nodes/bawk_image_loader.py b/nodes/bawk_image_loader.py
new file mode 100644
index 0000000..53dacbc
--- /dev/null
+++ b/nodes/bawk_image_loader.py
@@ -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']
\ No newline at end of file
diff --git a/nodes/bawk_sampler.py b/nodes/bawk_sampler.py
index 3f1d7e6..ca59756 100644
--- a/nodes/bawk_sampler.py
+++ b/nodes/bawk_sampler.py
@@ -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")
\ No newline at end of file
+ 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)
\ No newline at end of file
diff --git a/nodes/flux_wildcard_encode.py b/nodes/flux_wildcard_encode.py
index 52e3546..5ab7f02 100644
--- a/nodes/flux_wildcard_encode.py
+++ b/nodes/flux_wildcard_encode.py
@@ -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")
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 3479f5e..c86c9c3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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" }