From 1bd0b8db45a5e021455d6581d0bbd0ad06f06c3d Mon Sep 17 00:00:00 2001 From: Chris Judd <85649796+juddisjudd@users.noreply.github.com> Date: Sun, 5 Oct 2025 22:53:41 -0700 Subject: [PATCH] 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 --- README.md | 431 +++++++++++----------------------- __init__.py | 36 ++- modules/__init__.py | 2 +- nodes.py | 8 +- nodes/__init__.py | 10 +- nodes/bawk_batch_processor.py | 298 +++++++++++++++++++++++ nodes/bawk_controlnet.py | 330 ++++++++++++++++++++++++++ nodes/bawk_image_loader.py | 280 ++++++++++++++++++++++ nodes/bawk_sampler.py | 85 +++++-- nodes/flux_wildcard_encode.py | 62 ++++- pyproject.toml | 2 +- 11 files changed, 1208 insertions(+), 336 deletions(-) create mode 100644 nodes/bawk_batch_processor.py create mode 100644 nodes/bawk_controlnet.py create mode 100644 nodes/bawk_image_loader.py 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* + +[![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)]()
-![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) \ 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" }