diff --git a/.gitignore b/.gitignore index 8b8387a..3a6b15f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,41 @@ __pycache__/ *$py.class *.so .Python +*.egg-info/ +dist/ +build/ -# IDE +# IDEs .vscode/ .idea/ *.swp *.swo +*~ # OS .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db +desktop.ini + +# Personal folders +old/ +backup/ +test/ +tmp/ +temp/ +archive/ +_old/ +_backup/ +_test/ + +# Development files +DEVELOPMENT_SUMMARY.md +note_article.md +release_notes_*.md +*_backup.md +*_fixed.toml +__init___no_advanced.py + +# User data +*.log +*.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index d76222b..c49d23c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,62 @@ All notable changes to this project will be documented in this file. +## [1.2.0] - 2026-01-13 + +### Added + +#### New Node: Filtered Random LoRA Loader (LBW) +- **LoRA Block Weight (LBW) support** for precise effect control +- **Automatic SD1.5/SDXL detection** - no manual model type selection needed +- **4 preset modes**: Style Focused, Character Focused, Structure/Composition Only, Balanced/Soft +- **Preset: Random mode** - randomly selects one of the 4 presets +- **Direct Input mode** - custom weight specification with auto-adjustment +- **Automatic weight adjustment** - handles mismatched element counts + - Too few elements: pads with 1.0 at the end + - Too many elements: truncates from the end +- U-Net block structure support: + - SDXL: 20 elements (BASE + IN:9 + MID:1 + OUT:9) + - SD1.5: 17 elements (BASE + IN:6 + MID:1 + OUT:9) +- LBW syntax output in `positive_text` for Wildcard Encode compatibility +- Includes all features from Filtered Random LoRA Loader + +#### Video Preview Support (All Nodes) +- Video file preview support (.mp4, .webm, .avi, .mov) +- Extracts first frame as static image +- Requires optional `opencv-python` dependency +- Graceful fallback to black screen with warning if opencv-python not installed +- Priority order: static images → animated images → video files + +### Changed + +#### Model Compatibility +- ⚠️ **Explicitly SD1.5/SDXL only** - other architectures (Flux, SD3, etc.) not supported +- LBW feature specifically designed for SD1.5/SDXL U-Net structure + +#### Dependencies +- Added optional dependency: `opencv-python` (for video preview only) +- Core functionality works without opencv-python + +### Technical Details + +#### LBW Implementation +- Direct tensor weight manipulation for precise block control +- Automatic LoRA type detection via output_blocks analysis +- Compatible with serial node chaining (different LBW per node) +- Zero weight (all 0) = complete LoRA disable + +#### Preview Image Processing +- Pillow-only support: .png, .jpg, .jpeg, .gif, .webp (always works) +- opencv-python support: .mp4, .webm, .avi, .mov (optional) +- First-frame extraction for animated/video formats +- Consistent 1240px preprocessing + +### Performance +- No performance impact when LBW not used (Normal mode) +- Metadata caching unchanged from v1.1.0 + +--- + ## [1.1.0] - 2026-01-04 ### Added @@ -63,7 +119,7 @@ All notable changes to this project will be documented in this file. --- -## [1.0.0] - 2025-12-XX +## [1.0.0] - 2025-12-30 ### Initial Release diff --git a/README.md b/README.md index 86e2495..d17d697 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,26 @@ **English | [日本語版 README](./README_ja.md)** -A ComfyUI custom node package for randomly selecting and applying LoRAs. Includes two nodes: +A ComfyUI custom node package for randomly selecting and applying LoRAs. Includes three nodes: 1. **Random LoRA Loader** - Select from 3 folders simultaneously -2. **Filtered Random LoRA Loader** - Single folder with keyword filtering (NEW v1.1.0) +2. **Filtered Random LoRA Loader** - Single folder with keyword filtering +3. **Filtered Random LoRA Loader (LBW)** - 🆕 LBW support with automatic SD1.5/SDXL detection (NEW v1.2.0) ![Random LoRA Loader Workflow Example](./images/RLL_final_single.webp) ![Filtered Random LoRA Loader Workflow Example](./images/filterd_final_single.webp) +![FilteredRandomLoRALoader(LBW) Workflow Example](./images/lbw_final_single.webp) + +--- + +## ⚠️ Important: Supported Models + +**These nodes are designed for SD1.5 and SDXL models ONLY.** + +- ✅ **Supported:** Stable Diffusion 1.5, Stable Diffusion XL (SDXL) +- ❌ **NOT Supported:** Flux, SD3, SDXL Turbo, Pony, or other architectures + +The LBW node's block weight feature specifically targets SD1.5/SDXL U-Net architecture. Other model types will not work correctly with LBW. --- @@ -23,7 +36,7 @@ Select LoRAs from up to 3 different folders. Ideal for folder-based organization - Fixed LoRA categories - Simple folder-based workflow -### Filtered Random LoRA Loader (NEW) +### Filtered Random LoRA Loader Select LoRAs from a single folder using keyword filtering. Ideal for dynamic selection and large collections. @@ -33,45 +46,101 @@ Select LoRAs from a single folder using keyword filtering. Ideal for dynamic sel - Metadata search capability - Recommended for chaining multiple instances +### Filtered Random LoRA Loader (LBW) 🆕 NEW v1.2.0 + +Advanced node with LoRA Block Weight (LBW) support for precise effect control. + +**Use case:** +- Separate control of style vs. structure +- Fine-tuned LoRA application +- Professional workflows requiring precision +- 4 preset modes + custom input + +**See [README_LBW.md](README_LBW.md) for detailed LBW documentation.** + --- -## Key Features (Both Nodes) +## Key Features + +### Common Features (All Nodes) -- **3-Group Support**: Select LoRAs from up to 3 different folders - **Multi-Source Metadata Reading**: Automatically retrieve trigger words and sample prompts with priority order: 1. `.metadata.json` format (ComfyUI Lora Manager) - Priority 1 2. `.info` format (Civitai Helper) - Priority 2 - 3. **Embedded metadata in LoRA file** - Priority 3 (NEW) -- **Strength Randomization**: Randomize strength with range specification (e.g., `0.4-0.8`) + 3. **Embedded metadata in LoRA file** - Priority 3 +- **Strength Randomization**: + - **Range specification with negative support**: `0.4-0.8`, `-0.8--0.3` (v1.2.0) + - **0.1 increments for ranges**: Easier to observe effect differences + - **Fixed values rounded to 2 decimals**: `0.847` → `0.85` - **Flexible Trigger Word Retrieval**: - `json_combined`: Combine all trigger word patterns (with deduplication) - `json_random`: Randomly select one pattern - `json_sample_prompt`: Randomly retrieve sample prompts - - **`metadata`**: Read directly from embedded metadata (NEW) + - `metadata`: Read directly from embedded metadata - **Sample Prompt Optimization**: Automatically remove LoRA syntax from samples and apply node-configured strength - **Dual Text Outputs**: Separate positive_text and negative_text outputs - **ComfyUI Standard Seed Control**: Supports fixed/randomize/increment/decrement - **Wildcard Encode Integration**: Works seamlessly with Wildcard Encode (Inspire) - **LoRA Syntax Auto-Removal**: Cleans up LoRA syntax in additional_prompt to prevent noise +- **Preview Image Support**: Display preview images/videos for selected LoRAs (v1.1.0) +- **Duplicate Filename Handling**: Exclude duplicate filenames across subfolders (v1.1.0) + +### Filtered Nodes (Filtered & LBW) + +- **Keyword Filtering**: Space-separated keywords with phrase support +- **AND/OR Modes**: Flexible filtering logic +- **Metadata Search**: Search in filenames or embedded metadata +- **Fast Caching**: Instant metadata access after first load + +### LBW Node Only 🆕 + +- **LoRA Block Weight Support**: Control which U-Net blocks are affected +- **Automatic SD1.5/SDXL Detection**: No manual model type selection needed +- **4 Preset Modes**: + - Style Focused - OUTPUT blocks only + - Character Focused - Balanced IN+MID+OUT + - Structure/Composition Only - INPUT+MID blocks only + - Balanced / Soft - Gentle application +- **Preset: Random Mode**: Randomly select one of the 4 presets +- **Direct Input Mode**: Custom weight specification +- **Automatic Weight Adjustment**: Handles mismatched element counts + +--- ## Installation +### ⚠️ Optional: Video Preview Support + +**For video file preview (.mp4, .webm, .avi, .mov), opencv-python is REQUIRED:** + +```bash +pip install opencv-python +``` + +**Without opencv-python:** +- ✅ Static images (.png, .jpg, .jpeg) work +- ✅ Animated images (.gif, .webp) work +- ❌ **Video files will show BLACK SCREEN** (with warning message in console) + +LoRA functionality works normally regardless of opencv-python installation. Only preview display is affected. + ### Requirements - ComfyUI (works with standard installation) -- **No additional libraries required** ✅ +- Python 3.9+ +- **Core dependencies included with ComfyUI** ✅ ### Steps ```bash cd ComfyUI/custom_nodes -git clone https://github.com/shin131002/RandomLoRALoader.git +git clone https://github.com/YOUR_USERNAME/RandomLoRALoader.git # Or manually create RandomLoRALoader folder and copy files ``` Restart ComfyUI. -**Note:** Uses only Python standard library and ComfyUI bundled libraries, so no `pip install` needed. +**Note:** Uses only Python standard library and ComfyUI bundled libraries for core functionality. opencv-python is optional for video preview only. ### Uninstallation @@ -88,9 +157,11 @@ rmdir /s RandomLoRALoader Or manually delete the RandomLoRALoader folder and restart ComfyUI. +--- + ## Usage -### Basic Usage +### Basic Usage (Random LoRA Loader) 1. **Add Node**: Search for "Random LoRA Loader" in the node browser 2. **Connect MODEL/CLIP**: Connect base model and CLIP to inputs @@ -119,7 +190,7 @@ This node works seamlessly with Wildcard Encode (Inspire) for dynamic prompt gen text: "__style__, {red|blue|green}, " ↓ ├─ populated_text ──→ [Random LoRA Loader] - │ additional_prompt + │ additional_prompt_positive └─ MODEL/CLIP ──────→ model/clip input [Random LoRA Loader] @@ -155,8 +226,8 @@ This node works seamlessly with Wildcard Encode (Inspire) for dynamic prompt gen ``` Group 1: style LoRAs - num_loras_1: 2 - - model_strength_1: "0.6-0.9" ← Random - - clip_strength_1: "0.6-0.9" ← Random + - model_strength_1: "0.6-0.9" ← Random (0.1 increments) + - clip_strength_1: "0.6-0.9" ← Random (0.1 increments) Group 2: character LoRAs - num_loras_2: 1 @@ -170,6 +241,8 @@ Group 3: Unused → Result: 2 style LoRAs (variable strength) + 1 character LoRA (fixed strength) = 3 LoRAs total ``` +--- + ## Settings ### Common Settings @@ -178,7 +251,8 @@ Group 3: Unused |---------|-------------|---------| | `token_normalization` | Token normalization method | `none` | | `weight_interpretation` | Prompt emphasis notation interpretation | `A1111` | -| `additional_prompt` | Additional prompt (combined with trigger words) | (empty) | +| `additional_prompt_positive` | Additional positive prompt (combined with trigger words) | (empty) | +| `additional_prompt_negative` | Additional negative prompt | (empty) | | `trigger_word_source` | Trigger word source | `json_combined` | | `seed` | Random selection seed | `0` | @@ -209,7 +283,7 @@ CLIP tokenization: [1girl, lora, anime, style, 0, 8, beautiful] **Correct usage:** ``` -additional_prompt: "1girl, beautiful" ← No LoRA syntax ✅ +additional_prompt_positive: "1girl, beautiful" ← No LoRA syntax ✅ LoRA application: Use folder specification feature ✅ ``` @@ -217,7 +291,7 @@ Or, when connecting from Wildcard Encode, LoRA syntax is automatically removed: ``` [Wildcard Encode] populated_text: "1girl, , beautiful" ↓ -[This Node] additional_prompt received → LoRA syntax auto-removed +[This Node] additional_prompt_positive received → LoRA syntax auto-removed ↓ Final prompt: "1girl, beautiful" ✅ ``` @@ -235,9 +309,11 @@ Each group can be configured individually: | `clip_strength_X` | CLIP application strength | `"1.0"` | | `num_loras_X` | Number of LoRAs to select | Group 1: `1`, Groups 2/3: `0` | +--- + ## Strength Specification (Important) -Strength fields accept **fixed values** or **random ranges**. +Strength fields accept **fixed values** or **random ranges**, including **negative values**. ### Fixed Value @@ -246,740 +322,828 @@ Input: "1.0" → Always applied at 1.0 Input: "0.55" -→ Always applied at 0.55 -``` +→ Rounded to 0.55 (2 decimals) -**Features:** -- Input value used as-is -- Up to 2 decimal places (e.g., `0.55`) +Input: "-0.5" +→ Negative LoRA at -0.5 ✅ (v1.2.0) +``` ### Random Range -``` -Input: "0.4-0.8" -→ Randomly selects from 0.4, 0.5, 0.6, 0.7, 0.8 (0.1 increments) - -Input: "0.5-1.0" -→ Randomly selects from 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 - -Input: "0.44-0.82" -→ Randomly selects from 0.4, 0.5, 0.6, 0.7, 0.8 - (Range automatically rounded to 1 decimal place) -``` - -**Features:** -- Specify range with hyphen (`-`) -- Values generated in 0.1 increments -- Range bounds with 2+ decimal places are rounded to 1 decimal place -- Different value selected each execution - -### Error Handling +**Format:** `min-max` ``` -Input: "abc" or invalid format -→ Error logged, processed with strength 1.0 +Input: "0.6-0.9" +→ Random from [0.6, 0.7, 0.8, 0.9] (0.1 increments) + +Input: "-0.8--0.3" +→ Random from [-0.8, -0.7, -0.6, -0.5, -0.4, -0.3] ✅ (v1.2.0) + +Input: "-0.5-0.5" +→ Random from [-0.5, -0.4, ..., 0.4, 0.5] ✅ ``` -**Error message example:** -``` -[RandomLoRALoader] ❌ Cannot parse strength 'abc', using 1.0 -[RandomLoRALoader] 💡 Usage: '1.0' or '0.4-0.8' -``` +**v1.2.0 Improvements:** +- ✅ **Negative range support**: `-0.8--0.3` now works correctly +- ✅ **0.1 increments for all ranges**: Easier to observe effect differences +- ✅ **Fixed values rounded to 2 decimals**: `0.847` → `0.85` -### Verifying Actual Strength +### Behavior -Actual strength used can be verified in `positive_text` output: - -``` -, anime style, vibrant, - ↑ ↑ - MODEL CLIP - strength strength -``` - -**Recommended:** Connect Show Text node to `positive_text` to verify actual strength. - -## MODEL Strength vs CLIP Strength - -### Basic Concept - -- **MODEL strength**: Affects image itself (art style, composition, colors) -- **CLIP strength**: Affects prompt understanding (trigger word effectiveness) - -### Recommended Settings - -**Beginners / General Use:** -``` -model_strength: "1.0" -clip_strength: "1.0" -``` -Using same value for both is the standard approach. - -**Advanced / Fine-tuning:** -``` -model_strength: "0.7" -clip_strength: "1.0" -``` -For cases like wanting subtle art style but strong trigger word effect. - -**Randomization for Experimentation:** -``` -model_strength: "0.6-0.9" -clip_strength: "0.6-0.9" -``` -For testing various strength combinations. - -## Trigger Word Retrieval Methods - -### json_combined (All Combined) - -Combines all trigger word patterns and removes duplicates. - -**Example:** -```json -"trainedWords": [ - "character, red hair, blue eyes, uniform", - "character, red hair, blue eyes, casual clothes" -] -``` -↓ -``` -Output: character, red hair, blue eyes, uniform, casual clothes -``` - -### json_random (Random Selection) - -Randomly selects one pattern from trigger words. - -**Example:** -```json -"trainedWords": [ - "character, red hair, blue eyes, uniform", - "character, red hair, blue eyes, casual clothes" -] -``` -↓ -``` -Output: character, red hair, blue eyes, casual clothes (random) -``` - -### json_sample_prompt (Sample Retrieval) - -Randomly retrieves one sample prompt from JSON. - -- Outputs both positive_text and negative_text -- Automatically removes `` syntax from samples -- Generates new LoRA syntax with node-configured strength - -**Example:** -```json -"images": [{ - "meta": { - "prompt": "1girl, , beautiful", - "negativePrompt": "bad quality, worst quality" - } -}] -``` -↓ -``` -positive_text: 1girl, beautiful (LoRA syntax removed) -negative_text: bad quality, worst quality -``` - -### metadata (Embedded Metadata Only - NEW) - -Reads trigger words **directly from LoRA file's embedded metadata**, ignoring external `.metadata.json` and `.info` files. - -**Supported metadata fields:** -- `ss_tag_frequency`: kohya_ss format tag frequency (extracts top 20 tags) -- `modelspec.trigger_word`: Trigger word -- `ss_output_name`: Model name - -**Use case:** -- When you want to use only the metadata embedded by the LoRA trainer -- When external JSON files contain incorrect information -- For LoRAs without external metadata files +**All Nodes:** +- Range: 0.1 increments list → random selection +- Fixed: Rounded to 2 decimals **Example:** ``` -LoRA file contains embedded metadata: - ss_tag_frequency: {"dataset1": {"alice": 150, "blonde": 120, ...}} - ↓ -Output: alice, blonde, blue_eyes, ... +model_strength_1: "0.6-0.9" +→ Each execution randomly selects from [0.6, 0.7, 0.8, 0.9] +→ Console: "[RandomLoRALoader] Selected strength: 0.7" ``` -**Note:** Other modes (`json_combined`, `json_random`, `json_sample_prompt`) use external files with priority, falling back to embedded metadata if external files are not found. - -## Outputs - -| Output | Type | Description | -|--------|------|-------------| -| `MODEL` | MODEL | Model with LoRA applied | -| `CLIP` | CLIP | CLIP with LoRA applied | -| `positive_text` | STRING | LoRA info and positive prompt | -| `negative_text` | STRING | Negative prompt (json_sample_prompt only) | -| `positive` | CONDITIONING | Positive conditioning | -| `negative` | CONDITIONING | Negative conditioning | - -### positive_text Output Example - -``` -1girl, beautiful, -, anime style, vibrant colors, -, alice, blonde hair, blue eyes, -``` - -## Required File Structure - -**Metadata files or embedded data** required for trigger word retrieval. - -### Priority Order - -The node reads metadata in the following priority order: - -1. **`.metadata.json`** (ComfyUI Lora Manager format) -2. **`.info`** (Civitai Helper format) -3. **Embedded metadata in LoRA file** (Always available if LoRA was trained with metadata) - -### 1. ComfyUI Lora Manager format (Recommended) - -``` -/path/to/lora/ -├── style_anime_v2.safetensors -├── style_anime_v2.metadata.json ← ComfyUI Lora Manager -├── character_alice.safetensors -└── character_alice.metadata.json -``` - -### 2. Civitai Helper format - -``` -/path/to/lora/ -├── style_anime_v2.safetensors -├── style_anime_v2.info ← Civitai Helper -├── character_alice.safetensors -└── character_alice.info -``` - -### 3. Embedded metadata (No external file needed) - -``` -/path/to/lora/ -├── style_anime_v2.safetensors ← Contains embedded metadata -└── character_alice.safetensors ← Contains embedded metadata -``` - -**Note:** -- If external files (`.metadata.json` or `.info`) exist, they take priority over embedded metadata (except when using `trigger_word_source: metadata`) -- When `trigger_word_source` is set to `metadata`, only embedded metadata is read, ignoring external files - -### Generating metadata files - -**ComfyUI Lora Manager (Recommended):** -- [ComfyUI Lora Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) -- Generates `.metadata.json` files - -**Civitai Helper:** -- [Civitai Helper](https://github.com/butaixianran/Stable-Diffusion-Webui-Civitai-Helper) -- Generates `.info` files (also supported) - -**Embedded metadata:** -- Automatically created during LoRA training with kohya_ss or other trainers -- No additional tools required - -## Troubleshooting - -### Cannot retrieve trigger words - -**For external metadata files:** -- Check if metadata file exists (`.metadata.json` or `.info`) -- Verify JSON contains `civitai.trainedWords` or `civitai.images` -- Confirm filename matches `{LoRAfilename}.metadata.json` or `{LoRAfilename}.info` - -**For embedded metadata:** -- Set `trigger_word_source` to `metadata` to read directly from LoRA file -- Check if LoRA was trained with metadata (most modern LoRAs include this) -- Supported fields: `ss_tag_frequency`, `modelspec.trigger_word`, `ss_output_name` - - -### additional_prompt with LoRA syntax doesn't work - -**Symptom:** -``` -additional_prompt: ", 1girl" -→ anime_style not applied -``` - -**Cause:** -This node doesn't interpret `` syntax. LoRA syntax is automatically removed. - -**Solution:** -1. Use folder specification feature for LoRA application -2. When integrating with Wildcard Encode (Inspire), let it handle LoRA syntax - -### Unintended words in prompt - -**Symptom:** -``` -additional_prompt: "" -→ "anime", "style" words automatically added -``` - -**Cause:** -v1.0.0 and later automatically remove LoRA syntax, so this issue doesn't occur. -If using older version, LoRA syntax filename part was interpreted as tokens. - -**Solution:** -- Update node to latest version -- Don't write LoRA syntax in additional_prompt - -### LoRA not applied - -- Verify folder path is correct -- Check `num_loras` isn't 0 -- Check ComfyUI console for error logs - -### Strength not as intended - -- Check `positive_text` output with Show Text -- Displays actual strength like `` -- With random range specification, different value selected each time -- Check console for strength selection log: `[RandomLoRALoader] Selected 0.6 from range 0.4-0.8` - -### Strength input error - -**Symptom:** -``` -[RandomLoRALoader] ❌ Cannot parse strength 'abc', using 1.0 -``` - -**Solution:** -- Use correct format: `"1.0"` or `"0.4-0.8"` -- Range specification must use hyphen (`-`) -- Node won't crash with invalid input, operates with 1.0 - -### Default values not displayed - -- Completely restart ComfyUI -- Clear browser cache (Ctrl+Shift+R) -- Delete and re-add node - -### Console shows many "lora key not loaded" messages - -**Symptom:** -``` -lora key not loaded: lora_unet_input_blocks_4_1_proj_in.alpha -lora key not loaded: lora_unet_input_blocks_4_1_proj_in.lora_down.weight -... -``` - -**Cause and Solution:** - -These messages are compatibility warnings output by ComfyUI core and **don't affect functionality**. Displayed when some keys in LoRA file aren't compatible with current model, but compatible keys apply normally. - -v1.0.0 and later automatically suppress these warning messages. If still displayed in large quantities: - -1. Verify node file is latest version -2. Restart ComfyUI -3. If still appears and functionality is fine, safe to ignore - -## Tips & Tricks - -### Random LoRA Loader: Using as a LoRA Syntax Filter Only - -You can use this node as a simple LoRA syntax remover without applying any LoRAs. - -**Setup:** -- Set all `num_loras` to `0` (or leave all folder paths empty) -- Connect Wildcard Encode's `populated_text` to `additional_prompt` -- Connect `positive_text` output to next node - -**Use case:** -- Remove LoRA syntax from Wildcard Encode output -- Clean up prompts before passing to other nodes -- Use Wildcard Encode's LoRA processing while removing the syntax for compatibility - -**Example workflow:** -``` -[Wildcard Encode (Inspire)] - text: "__style__, {red|blue|green}, " - ↓ - populated_text: "anime style, blue, " - (LoRA already applied to MODEL by Wildcard Encode) - ↓ -[Random LoRA Loader] - num_loras: 0, 0, 0 ← No LoRA application - additional_prompt ← Connected from populated_text - ↓ - positive_text: "anime style, blue," ← LoRA syntax removed ✅ - ↓ -[Next Node] -``` - -**Benefits:** -- ✅ Removes `` syntax that can become noise tokens -- ✅ No need for separate text processing nodes -- ✅ Works with any text containing LoRA syntax - --- -## Filtered Random LoRA Loader (NEW v1.1.0) +## MODEL Strength vs CLIP Strength -![Filtered Random LoRA Loader x1 Workflow](./images/filterd_final_single.webp) +### MODEL Strength +- **Affects**: Image itself (art style, composition, colors, shapes) +- **Effect**: Visual characteristics of generated image +- **Example**: + - High value: Strong art style transfer + - Low value: Subtle style hints -### Overview +### CLIP Strength +- **Affects**: Prompt interpretation (concept understanding, language-image association) +- **Effect**: How strongly the model follows prompt instructions +- **Example**: + - High value: Strong semantic guidance from trigger words + - Low value: Weak semantic influence -A single-group LoRA loader with keyword filtering capabilities. Designed for chaining multiple instances together. +### Recommended Settings -**Key differences from Random LoRA Loader:** -- Single folder path (not 3) -- Keyword filtering with AND/OR modes -- Metadata search capability with caching -- Dual prompt inputs (positive and negative) -- Recommended for serial connection +**For most use cases:** +``` +model_strength: 0.8 +clip_strength: 0.8 +→ Balanced application +``` + +**For subtle style application:** +``` +model_strength: 0.4-0.6 +clip_strength: 0.8-1.0 +→ Light visual effect, strong semantic guidance +``` + +**For strong style override:** +``` +model_strength: 1.0-1.2 +clip_strength: 0.6-0.8 +→ Strong visual effect, moderate semantic guidance +``` + +--- + +## Trigger Word Retrieval Methods + +### json_combined (Default) + +Combines all trigger word patterns from JSON metadata. + +**Source:** +1. `activation text` field +2. `ss_tag_frequency` keys (first 3 highest frequency) +3. `modelspec.description` keywords +4. `ss_output_name` keywords + +**Example:** +```json +{ + "activation text": "anime style, detailed eyes", + "ss_tag_frequency": { + "1girl": 500, + "blue eyes": 300, + "long hair": 250 + } +} +``` +**Result:** `"anime style, detailed eyes, 1girl, blue eyes, long hair"` + +--- + +### json_random + +Randomly selects one trigger word pattern. + +**Example:** +``` +Candidates: +- "anime style, detailed eyes" +- "1girl, blue eyes, long hair" +- "anime, girl, portrait" + +Random selection: "1girl, blue eyes, long hair" +``` + +--- + +### json_sample_prompt + +Retrieves sample prompts from JSON metadata. + +**Source fields (priority order):** +1. `sample_prompts` +2. `civitai.images[0].meta.prompt` + +**Processing:** +- Automatically removes LoRA syntax (``) +- Replaces with node-configured strength +- Extracts negative prompt if available + +**Example:** +```json +{ + "sample_prompts": "1girl, , beautiful, detailed eyes" +} +``` +**Result:** `"1girl, beautiful, detailed eyes"` (LoRA syntax removed) + +--- + +### metadata + +Reads trigger words directly from embedded LoRA file metadata. + +**Source:** +- Safetensors metadata keys +- Similar to json_combined but reads from LoRA file directly + +**Use when:** +- No external JSON files available +- Want to use original training metadata + +--- + +## Outputs + +All nodes provide the following outputs: + +| Output | Type | Description | +|--------|------|-------------| +| `MODEL` | MODEL | Model with LoRAs applied | +| `CLIP` | CLIP | CLIP with LoRAs applied | +| `positive` | CONDITIONING | Positive conditioning (LoRA syntax removed) | +| `negative` | CONDITIONING | Negative conditioning | +| `positive_text` | STRING | Full positive prompt text with LoRA syntax | +| `negative_text` | STRING | Full negative prompt text | +| `preview` | IMAGE | Batch of preview images for selected LoRAs (v1.1.0) | + +### Text Output Format + +**positive_text example:** +``` +, , anime style, alice, blonde hair, 1girl, beautiful +``` + +**LBW node positive_text example:** +``` +, anime style, 1girl, beautiful +``` + +**positive (CONDITIONING) content:** +``` +anime style, alice, blonde hair, 1girl, beautiful +(LoRA syntax removed for clean prompt) +``` + +### Preview Images (v1.1.0) + +**Supported formats (priority order):** +1. Static images (.png, .jpg, .jpeg) - Always works +2. Animated images (.gif, .webp) - First frame, always works +3. Video files (.mp4, .webm, .avi, .mov) - First frame, requires opencv-python + +**File matching:** +- Files starting with the LoRA filename (case-insensitive) +- Example: `style_anime.safetensors` matches: + - `style_anime.png` ✅ + - `style_anime_preview.jpg` ✅ + - `STYLE_ANIME.PNG` ✅ + +--- + +## Filtered Random LoRA Loader + +**Single folder with keyword filtering.** + +![Filtered Random LoRA Loader Workflow](./images/filterd_final_single.webp) + +### Key Features + +- **Keyword Filtering**: Filter LoRAs by space-separated keywords +- **AND/OR Modes**: Flexible filtering logic +- **Phrase Matching**: Use quotes for exact phrases +- **Metadata Search**: Search in filenames or embedded metadata +- **Fast Caching**: Instant metadata access after first load +- **Serial Connection Ready**: Chain multiple instances for complex workflows ### Parameters -#### Common Settings -- `token_normalization` - Same as Random LoRA Loader -- `weight_interpretation` - Same as Random LoRA Loader +| Parameter | Description | Default | +|-----------|-------------|---------| +| `lora_folder_path` | LoRA folder path | (empty) | +| `keyword_filter` | Space-separated keywords or quoted phrases | (empty) | +| `filter_mode` | `AND` / `OR` | `AND` | +| `search_in_metadata` | Search in JSON/embedded metadata | `false` | +| `num_loras` | Number of LoRAs to select | `1` | +| `model_strength` | MODEL strength (fixed or range) | `"1.0"` | +| `clip_strength` | CLIP strength (fixed or range) | `"1.0"` | +| `include_subfolders` | Include subfolders | `true` | +| `unique_by_filename` | Exclude duplicate filenames | `true` | -#### Folder Settings -- `lora_folder_path` - Single folder path -- `include_subfolders` - Include subfolders (default: True) -- `unique_by_filename` - Exclude duplicate filenames across subfolders (default: True) +### Keyword Filter Syntax -#### Keyword Filter -- `keyword_filter` - Keywords (comma-separated, e.g., "style, anime") -- `filter_mode` - AND (all keywords) or OR (any keyword, default: AND) -- `search_in_metadata` - Search in metadata files (default: False - filename only) - -#### LoRA Settings -- `model_strength` - MODEL strength (e.g., "1.0" or "0.6-0.9") -- `clip_strength` - CLIP strength (e.g., "1.0" or "0.6-0.9") -- `num_loras` - Number of LoRAs to select (default: 1, range: 0-20) - -#### Trigger Word Settings -- `trigger_word_source` - Same as Random LoRA Loader - -#### Additional Prompts -- `additional_prompt_positive` - Additional positive prompt (input or direct entry) -- `additional_prompt_negative` - Additional negative prompt (input or direct entry) - -#### Seed -- `seed` - Random seed -- `control_after_generate` - Seed control mode - -### Keyword Filtering - -#### Filename Search (Default - Fast) - -Searches only in LoRA filename. - -**Example:** +**Basic keywords (AND mode):** ``` -Folder contents: - - style_anime_v1.safetensors - - style_realistic.safetensors - - character_alice.safetensors - -keyword_filter: "anime" -→ Matches: style_anime_v1.safetensors ✅ +keyword_filter: "anime girl" +→ Matches files containing BOTH "anime" AND "girl" ``` -#### Metadata Search (Optional - Slower) - -Searches in both filename and metadata fields: -- `model_name` -- `civitai.name` -- `civitai.trainedWords` -- `civitai.model.name` -- `civitai.model.tags` -- `tags` -- Embedded metadata - -**Performance Notes:** - -Initial loading time (SSD environment, with progress display): -- 100 files: Instant -- 1,000 files: ~2 seconds -- 5,000 files: ~10 seconds -- 10,000 files: ~20 seconds -- 20,000 files: ~40 seconds -- 50,000 files: ~100 seconds (1 min 40 sec) - -Memory usage: -- 10,000 files: ~150MB -- 20,000 files: ~300MB -- 50,000 files: ~750MB - -**Second time onward:** Instant (cached) - -**Recommendations:** -- Default filename search is always fast -- Enable metadata search only when needed -- For large collections (5,000+ files), initial loading will take time -- HDD environments take approximately 3x longer than SSD for initial loading - -**Example:** +**OR mode:** ``` -File: abc123.safetensors -Metadata: {"trainedWords": ["anime style", "vibrant colors"]} - -search_in_metadata: True -keyword_filter: "anime" -→ Matches ✅ +filter_mode: OR +keyword_filter: "anime realistic" +→ Matches files containing "anime" OR "realistic" ``` -#### AND vs OR Modes - -**AND Mode (default):** +**Phrase matching:** ``` -keyword_filter: "style, anime" -filter_mode: "AND" -→ Matches files containing BOTH "style" AND "anime" +keyword_filter: '"anime style" detailed' +→ Must contain exact phrase "anime style" AND word "detailed" ``` -**OR Mode:** +**Multiple phrases:** ``` -keyword_filter: "style, anime" -filter_mode: "OR" -→ Matches files containing EITHER "style" OR "anime" +keyword_filter: '"anime style" "detailed eyes" red' +→ Must contain "anime style" AND "detailed eyes" AND "red" ``` -### Handling Duplicate Filenames +### Metadata Search -If the same LoRA filename exists in multiple subfolders (e.g., originals and backups), the `unique_by_filename` option prevents selecting the same LoRA multiple times. - -**Example:** +**Filename search (default):** ``` -Folder structure: - /LoRA/ - ├── style/anime_v1.safetensors - └── backup/anime_v1.safetensors - -unique_by_filename: True (default) - → Only one anime_v1.safetensors will be selected - → First occurrence is kept (/LoRA/style/anime_v1.safetensors) - -unique_by_filename: False - → Both files can be selected - → May result in same LoRA applied twice with different strengths +search_in_metadata: false +→ Fast, searches only filenames ``` -**Log output when duplicates detected:** -``` -[FilteredRandomLoRALoader] Duplicate filename detected: anime_v1.safetensors - Keeping: /LoRA/style/anime_v1.safetensors - Skipping: /LoRA/backup/anime_v1.safetensors -``` - -**Recommendation:** Keep default `True` to avoid unintentional duplicate applications. - -### Metadata Search Performance - -**Filename search (Default):** -- `search_in_metadata: False` (default) -- Very fast even with 10,000+ LoRAs -- Processing time: <100ms - **Metadata search:** -- `search_in_metadata: True` -- Searches in both filename and LoRA metadata -- **First execution:** Builds cache - - 1,000 LoRAs: ~2 seconds - - 10,000 LoRAs: ~7-75 seconds (depends on metadata files) - - Progress displayed in console -- **Subsequent executions:** Very fast (<100ms) -- Cache persists until ComfyUI restart - -**Performance tip:** -- Use `include_subfolders: True` on first run to cache all LoRAs at once -- After cache is built, `include_subfolders: False` will still use cached metadata -- Cache is shared across all node instances - -### Usage Examples - -#### Basic Usage - ``` -[Filtered Random LoRA Loader] - lora_folder_path: /loras/all/ - keyword_filter: "anime" - num_loras: 1 - ↓ -Randomly selects 1 LoRA containing "anime" in filename +search_in_metadata: true +→ Slower, searches JSON/embedded metadata +→ Cached after first search (instant on 2nd+) ``` -#### Chaining Multiple Instances +**Performance:** +- Initial load (SSD): + - 1,000 files: ~2 seconds + - 5,000 files: ~10 seconds + - 10,000 files: ~20 seconds +- 2nd+ time: Instant (<100ms) +- Memory: ~150MB per 10,000 files + +### Use Cases + +#### Example 1: Character Selection + +``` +lora_folder_path: "/path/to/all_loras" +keyword_filter: "character girl" +filter_mode: AND +num_loras: 1 +``` + +**Result:** Select 1 character LoRA containing both "character" and "girl" + +--- + +#### Example 2: Multiple Instances (Recommended) ![Filtered Random LoRA Loader x2 Workflow](./images/filterd_final_double.webp) ``` -[Filtered Random LoRA Loader A] - folder: /loras/all/ - keyword_filter: "style, anime" - filter_mode: "AND" +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader #1] + keyword_filter: "style anime" num_loras: 1 ↓ -[Filtered Random LoRA Loader B] - folder: /loras/all/ +[Filtered Random LoRA Loader #2] keyword_filter: "character" num_loras: 1 - additional_prompt_positive ← A's positive_text - additional_prompt_negative ← A's negative_text ↓ -[Filtered Random LoRA Loader C] - folder: /loras/all/ - keyword_filter: "concept" - num_loras: 1 - additional_prompt_positive ← B's positive_text - additional_prompt_negative ← B's negative_text +[KSampler] ``` -#### With Wildcard Encode - -![Wildcard Encode + Filtered Random LoRA Loader Workflow](./images/filterd_final_wce.webp) - -``` -[Wildcard Encode] - populated_text: "1girl, beautiful" - negative_text: "bad quality" - ↓ -[Filtered Random LoRA Loader] - additional_prompt_positive ← populated_text - additional_prompt_negative ← negative_text - keyword_filter: "style" - ↓ -Combined output ✅ -``` - -### Best Practices - -1. **Use descriptive filenames:** `style_anime_v1.safetensors` -2. **Default mode (fast):** Keep `search_in_metadata: False` for daily use -3. **Enable metadata search when:** - - Filenames don't contain useful keywords - - Need to search in trigger words or tags -4. **First run optimization:** - - Use `include_subfolders: True` once to build complete cache - - Switch to `False` after for faster folder scanning -5. **Chaining:** - - Connect `positive_text` → next node's `additional_prompt_positive` - - Connect `negative_text` → next node's `additional_prompt_negative` - -### Troubleshooting - -**No LoRAs found matching filter:** -- Check if keyword exists in filenames -- Enable `search_in_metadata: True` to search in metadata -- Try OR mode instead of AND mode -- Verify folder path is correct - -**First execution is slow:** -- This is normal when `search_in_metadata: True` -- Cache is being built, subsequent executions will be fast -- Progress is shown in console - -**Cache not working:** -- Cache clears on ComfyUI restart -- Run once after startup to rebuild cache +**Benefits:** +- Different filtering per category +- Independent strength settings +- More flexibility --- -## Tips & Tricks (Original Node) +#### Example 3: Metadata Search -### Using as a LoRA Syntax Filter Only +``` +search_in_metadata: true +keyword_filter: "detailed eyes" +``` -You can use this node as a simple LoRA syntax remover without applying any LoRAs. +**Searches in:** +- Filenames +- JSON `activation text` +- JSON `ss_tag_frequency` +- JSON `modelspec.description` +- Embedded LoRA metadata -**Setup:** -- Set all `num_loras` to `0` (or leave all folder paths empty) -- Connect Wildcard Encode's `populated_text` to `additional_prompt` -- Connect `positive_text` output to next node +--- -**Use case:** -- Remove LoRA syntax from Wildcard Encode output -- Clean up prompts before passing to other nodes -- Use Wildcard Encode's LoRA processing while removing the syntax for compatibility +## Filtered Random LoRA Loader (LBW) 🆕 NEW v1.2.0 + +**Advanced node with LoRA Block Weight support.** + +![FilteredRandomLoRALoader(LBW) Workflow Example](./images/lbw_final_single.webp) + +### What is LBW? + +LoRA Block Weight allows you to control which parts of the U-Net are affected by the LoRA: + +``` +INPUT blocks → Structure, composition, layout + ↓ +MIDDLE block → Overall features + ↓ +OUTPUT blocks → Style, details, fine-tuning +``` + +### Key Features + +- **4 Preset Modes**: + - **Style Focused**: OUTPUT blocks only (style without changing structure) + - **Character Focused**: Balanced IN+MID+OUT (character features) + - **Structure/Composition Only**: INPUT+MID blocks (layout without style) + - **Balanced / Soft**: Gentle application (natural results) +- **Preset: Random**: Randomly select one of the 4 presets +- **Direct Input**: Custom weight specification +- **Automatic SD1.5/SDXL Detection**: No manual model type selection +- **Automatic Weight Adjustment**: Handles mismatched element counts + +### Parameters + +Same as Filtered Random LoRA Loader, plus: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `weight_mode` | LBW preset or Direct Input | `Normal (All 1.0)` | +| `lbw_input` | Custom weights (for Direct Input) | (empty) | + +### Weight Modes + +#### Normal (All 1.0) +Standard LoRA application without block weighting. + +#### Style Focused +``` +SDXL: 1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1 +SD1.5: 1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1 +``` +- OUTPUT blocks only +- Style without changing structure +- Use for: Art style LoRAs, effect LoRAs + +#### Character Focused +``` +SDXL: 1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1 +SD1.5: 1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1 +``` +- Balanced IN+MID+OUT +- Character features preserved +- Use for: Character LoRAs, person LoRAs + +#### Structure/Composition Only +``` +SDXL: 1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0 +SD1.5: 1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0 +``` +- INPUT+MID blocks only +- Structure without changing style +- Use for: Pose LoRAs, composition LoRAs + +#### Balanced / Soft +``` +SDXL: 1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0 +SD1.5: 1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0 +``` +- Gentle application +- Natural results +- Use for: General LoRAs, multi-LoRA workflows + +#### Preset: Random +Randomly selects one of the 4 presets above each execution. + +#### Direct Input +``` +weight_mode: Direct Input +lbw_input: "1,0.5,0.5,0,0,0,0,0,0,0,1,0.8,0.8,0.8,0.8,0.8,0.5,0.5,0.5,0.5" +``` +- Custom weight specification +- SDXL: 20 elements, SD1.5: 17 elements +- Auto-adjusted if count doesn't match + +### Use Cases + +#### Example 1: Style Only + +``` +[Filtered Random LoRA Loader (LBW)] + keyword_filter: "anime watercolor" + weight_mode: Style Focused + ↓ +[KSampler] +``` + +**Result:** Applies anime watercolor style without changing composition + +--- + +#### Example 2: Structure then Style + +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader (LBW)] + keyword_filter: "pose" + weight_mode: Structure/Composition Only + ↓ +[Filtered Random LoRA Loader (LBW)] + keyword_filter: "oil painting" + weight_mode: Style Focused + ↓ +[KSampler] +``` + +**Result:** Pose adjustment → Oil painting style applied + +--- + +#### Example 3: Custom Weights + +``` +weight_mode: Direct Input +lbw_input: "1,0.5,0.5,0.5,0,0,0,0,0,0,1,1,1,1,1,1,0.5,0.5,0.5,0.5" +``` + +**Result:** Custom block weight distribution + +--- + +### Advanced: Custom Presets + +You can edit presets directly in the source file: + +**File:** `filtered_random_lora_loader_lbw.py` (lines 29-41) + +See [README_LBW.md](README_LBW.md) for detailed customization guide. + +--- + +## Tips & Tricks + +### Serial Connection for Multiple Effects + +Connect nodes in series to apply different LoRA categories: + +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader (LBW)] (Structure/Composition Only) + keyword_filter: "pose" + ↓ +[Filtered Random LoRA Loader (LBW)] (Style Focused) + keyword_filter: "watercolor" + ↓ +[KSampler] +``` + +**Benefits:** +- Different settings per category +- Cumulative LoRA effects +- More control over final result + +--- + +### Strength Randomization for Variety + +Use ranges instead of fixed values: + +``` +model_strength_1: "0.6-0.9" +clip_strength_1: "0.6-0.9" +``` + +**Effect:** +- Each generation uses different strength +- More variety in results +- Easier to find optimal strength + +--- + +### Keyword Filtering Best Practices + +**Use specific keywords:** +``` +✅ Good: "anime girl" +❌ Too broad: "anime" +``` + +**Combine with metadata search:** +``` +search_in_metadata: true +keyword_filter: "detailed eyes" +``` + +**Use phrases for precision:** +``` +keyword_filter: '"anime style" detailed' +→ Must contain exact phrase "anime style" AND word "detailed" +``` + +--- + +### Wildcard Encode + Random LoRA Loader + +**Ultimate dynamic workflow:** + +``` +[Wildcard Encode (Inspire)] + text: "__style__, __pose__, {red|blue|green}" + ↓ + populated_text ──→ [Random LoRA Loader] + additional_prompt_positive + ↓ +[KSampler] +``` + +**Result:** +- Wildcard expansion for prompts +- Random LoRA selection for variety +- Clean prompt without LoRA syntax +- Maximum flexibility + +--- + +### Preview Images for Organization + +Enable preview display to: +- See which LoRAs were selected +- Verify LoRA appearance before generation +- Organize LoRA collections visually + +**Recommended workflow:** +``` +[Random LoRA Loader] + preview ──→ [Preview Image] (for verification) + MODEL ──→ [KSampler] +``` + +--- + +## Troubleshooting + +### No LoRAs Found + +**Check:** +1. LoRA folder path is correct (absolute path) +2. `.safetensors` files exist in the folder +3. `include_subfolders` setting if LoRAs are in subfolders +4. Console for error messages + +**Example valid paths:** +``` +Windows: C:/ComfyUI/models/loras/style +Linux/Mac: /home/user/ComfyUI/models/loras/style +``` + +--- + +### No Trigger Words Found + +**Check:** +1. Metadata files (`.metadata.json` or `.info`) exist +2. JSON files contain required fields: + - `activation text` + - `ss_tag_frequency` + - `sample_prompts` + - `civitai.trainedWords` or `civitai.images` +3. Try different `trigger_word_source` settings + +**Verify JSON format:** +```json +{ + "activation text": "anime style, detailed", + "ss_tag_frequency": {...}, + "modelspec.description": "...", + "sample_prompts": "..." +} +``` + +--- + +### LoRA Not Applied + +**Check:** +1. `num_loras` is not 0 +2. Strength values are not all 0 +3. MODEL/CLIP outputs are connected properly +4. Console for LoRA application messages +5. For LBW node: weight_mode is not "Normal (All 1.0)" with all weights set to 0 + +--- + +### Preview Images Not Showing + +**Check:** +1. Preview files match LoRA filename +2. For video files: opencv-python is installed (`pip install opencv-python`) +3. Supported formats: .png, .jpg, .jpeg, .gif, .webp, .mp4, .webm, .avi, .mov +4. File permissions are correct + +**Without opencv-python:** +- Static images work ✅ +- Animated images work ✅ +- Video files show black screen ⚠️ + +--- + +### Strength Not Working with Negative Values (Fixed in v1.2.0) + +**Previous issue (v1.1.0):** +``` +Input: "-0.5" +Result: 1.0 ❌ (converted to default) +``` + +**Fixed in v1.2.0:** +``` +Input: "-0.5" +Result: -0.5 ✅ (negative LoRA applied correctly) + +Input: "-0.8--0.3" +Result: Random from [-0.8, -0.7, -0.6, -0.5, -0.4, -0.3] ✅ +``` + +--- + +### Wildcard Encode Issues + +**If wildcard text appears in output:** + +1. **Check connection**: Connect Wildcard Encode's `populated_text` to `additional_prompt_positive` +2. **Wrong output**: Don't connect `text` (input) to this node +3. **LoRA syntax appearing**: This node automatically removes it **Example workflow:** ``` [Wildcard Encode (Inspire)] - text: "__style__, {red|blue|green}, " - ↓ - populated_text: "anime style, blue, " - (LoRA already applied to MODEL by Wildcard Encode) - ↓ -[Random LoRA Loader] - num_loras: 0, 0, 0 ← No LoRA application - additional_prompt ← Connected from populated_text - ↓ - positive_text: "anime style, blue," ← LoRA syntax removed ✅ - ↓ -[Next Node] + populated_text ──→ [Random LoRA Loader] + (not "text") additional_prompt_positive ``` -**Benefits:** -- ✅ Removes `` syntax that can become noise tokens -- ✅ No need for separate text processing nodes -- ✅ Works with any text containing LoRA syntax +--- + +### Duplicate LoRAs Selected + +**Solution:** Enable `unique_by_filename` + +``` +unique_by_filename_1: true +``` + +This prevents selecting the same LoRA from different subfolders: +``` +/style/lora.safetensors +/backup/lora.safetensors +→ Only one selected ✅ +``` + +--- + +## Detailed Documentation + +- **[README_LBW.md](README_LBW.md)** - Complete LBW documentation (English) +- **[README_LBW_ja.md](README_LBW_ja.md)** - LBW詳細ガイド(日本語) +- **[README_ja.md](README_ja.md)** - 日本語メインドキュメント + +--- ## Disclaimer and Support Policy -### About This Node +### Disclaimer -This custom node was developed for personal use and is being shared publicly. +- This node is provided as-is with **no technical support** +- No warranty or guarantee of functionality +- No guaranteed compatibility with future ComfyUI updates +- Bug reports and feature requests may not be addressed +- Use at your own risk -### Support +### Support Status -- ❌ **No technical support provided** - - Usage questions - - Environment setup assistance - - Individual troubleshooting support +- ❌ No individual support via issues or email +- ❌ No guaranteed bug fixes or feature additions +- ✅ Code is open source - feel free to fork and modify +- ✅ Community discussions welcome (no promises of response) -- ❌ **No warranty** - - Operation in specific environments - - Compatibility with future ComfyUI updates - - Liability for data loss, etc. +### Reporting Issues -### Community Contributions +While support is not guaranteed, you can: +1. Check existing issues in the repository +2. Review this README and troubleshooting section +3. Open an issue (may or may not be addressed) +4. Fork and fix it yourself -The following are welcome but not guaranteed to be addressed: - -- ✅ Bug reports (Issues) -- ✅ Pull requests -- ✅ Feature suggestions - -### Terms of Use - -- Use entirely at your own risk -- Test thoroughly before production use -- Author assumes no liability for any issues +--- ## License MIT License -Copyright (c) 2025 - -This software is provided "as is" without any express or implied warranties. +--- ## Changelog -### v1.0.0 (2024-12-30) -- Initial public release -- 3-group support (select from different folders) -- Strength randomization (range specification: `0.4-0.8`) -- External JSON reading (Civitai Helper compatible) -- Auto-remove LoRA syntax in sample prompts -- **Auto-remove LoRA syntax in additional_prompt** (noise prevention) -- Separate positive_text/negative_text outputs -- Wildcard Encode (Inspire) integration -- Fully local operation (no API required) +### v1.2.0 (2026-01-13) + +#### Added +- ✅ **NEW NODE**: Filtered Random LoRA Loader (LBW) +- ✅ LoRA Block Weight (LBW) support with 4 presets +- ✅ Automatic SD1.5/SDXL detection for LBW +- ✅ Preset: Random mode for LBW +- ✅ Direct Input mode for custom LBW weights +- ✅ Automatic weight adjustment for LBW +- ✅ **Negative value support for strength**: `-0.5`, `-0.8--0.3` now work correctly +- ✅ **Video preview support**: .mp4, .webm, .avi, .mov (requires opencv-python) +- ✅ **Strength precision improvements**: + - Range specification: 0.1 increments for all nodes + - Fixed values: Rounded to 2 decimals + +#### Fixed +- ✅ **Critical bug**: Negative single values (`"-0.5"`) now work correctly (previously converted to 1.0) +- ✅ **Critical bug**: Negative ranges (`"-0.8--0.3"`) now work correctly (previously caused errors) +- ✅ Improved strength parsing with regex pattern matching + +#### Changed +- ✅ Unified strength behavior across all 3 nodes +- ✅ Renamed node: "Filtered Random LoRA Loader (Advanced)" → "Filtered Random LoRA Loader (LBW)" +- ✅ Enhanced documentation with model compatibility warnings + +### v1.1.0 (2026-01-04) + +#### Added +- ✅ **NEW NODE**: Filtered Random LoRA Loader +- ✅ Keyword filtering with AND/OR modes +- ✅ Metadata search with caching +- ✅ Preview image output (IMAGE type) +- ✅ Duplicate filename handling (`unique_by_filename`) +- ✅ Animated image support (.gif, .webp) + +#### Changed +- ✅ **Breaking**: Split `additional_prompt` into `additional_prompt_positive` and `additional_prompt_negative` +- ✅ Unified strength precision to 1 decimal place +- ✅ Improved LoRA syntax removal + +#### Fixed +- ✅ Fixed negative_text output in `json_sample_prompt` mode +- ✅ Fixed LoRA syntax appearing in metadata trigger words +- ✅ Fixed duplicate parameter in INPUT_TYPES + +### v1.0.0 (2025-12-30) + +#### Added +- ✅ Initial release +- ✅ Random LoRA Loader with 3-group support +- ✅ Multi-source metadata reading +- ✅ Strength randomization with range specification +- ✅ Trigger word extraction +- ✅ Wildcard Encode compatibility +- ✅ CONDITIONING output with cleaned prompts + +--- ## References - [ComfyUI](https://github.com/comfyanonymous/ComfyUI) -- [ComfyUI Lora Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) - Generates `.metadata.json` (Recommended) -- [Civitai Helper](https://github.com/butaixianran/Stable-Diffusion-Webui-Civitai-Helper) - Generates `.info` (Also supported) +- [Wildcard Encode (Inspire)](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) +- [LoRA Block Weight Theory](https://github.com/hako-mikan/sd-webui-lora-block-weight) + +--- + +**Enjoy flexible LoRA randomization with precise control! 🎲✨** diff --git a/README_LBW.md b/README_LBW.md new file mode 100644 index 0000000..52ea591 --- /dev/null +++ b/README_LBW.md @@ -0,0 +1,382 @@ +# Filtered Random LoRA Loader (LBW) + +**LoRA Block Weight (LBW) support with automatic SD1.5/SDXL detection** + +![FilteredRandomLoRALoader(LBW) Workflow Example](./images/lbw_final_single.webp) + +--- + +## Features + +### **Core Features (from Filtered Random LoRA Loader)** +- ✅ Random LoRA selection from a single folder +- ✅ Keyword filtering (AND/OR modes) +- ✅ Metadata search (filename or embedded metadata) +- ✅ Preview images (static/animated/video support) +- ✅ Trigger word extraction +- ✅ Duplicate removal by filename +- ✅ Direct serial connection support + +### **Advanced Features (LBW)** +- ✅ **LoRA Block Weight (LBW) support** +- ✅ **Automatic SD1.5/SDXL detection** +- ✅ **4 preset modes + custom input** +- ✅ **Automatic weight adjustment** + +--- + +## LoRA Block Weight (LBW) + +### **What is LBW?** + +LoRA Block Weight allows you to control which parts of the U-Net model are affected by the LoRA: + +- **INPUT blocks**: Structure, composition, layout +- **MIDDLE block**: Overall features +- **OUTPUT blocks**: Style, details, fine-tuning + +By adjusting weights for each block, you can precisely control the LoRA's effect. + +--- + +## Weight Modes + +### **1. Normal (All 1.0)** - Default +All blocks weighted at 1.0 (standard LoRA application, no LBW). + +--- + +### **2. Style Focused** +Emphasizes OUTPUT blocks only. + +**Best for:** +- Style LoRAs +- Art style transfer +- Visual aesthetic changes + +**SDXL:** `1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1` +**SD1.5:** `1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1` + +**Effect:** +- ✅ Strong style changes +- ❌ Minimal structure/character changes + +--- + +### **3. Character Focused** +Emphasizes INPUT (early) + MIDDLE + OUTPUT blocks. + +**Best for:** +- Character LoRAs +- Face/feature preservation +- Balanced application + +**SDXL:** `1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1` +**SD1.5:** `1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1` + +**Effect:** +- ✅ Strong character features +- ✅ Balanced overall effect + +--- + +### **4. Structure/Composition Only** +Emphasizes INPUT + MIDDLE blocks only. + +**Best for:** +- Pose LoRAs +- Composition LoRAs +- Layout/structure control + +**SDXL:** `1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0` +**SD1.5:** `1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0` + +**Effect:** +- ✅ Strong structure/pose changes +- ❌ Minimal style changes + +--- + +### **5. Balanced / Soft** +Soft application across selected blocks. + +**Best for:** +- Subtle effects +- Multi-LoRA workflows +- General-purpose LoRAs + +**SDXL:** `1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0` +**SD1.5:** `1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0` + +**Effect:** +- ✅ Balanced, gentle application +- ✅ Good for stacking multiple LoRAs + +--- + +### **6. Preset: Random** +Randomly selects one of the 4 presets above. + +**Best for:** +- Experimentation +- Variation generation +- Random effects + +--- + +### **7. Direct Input** +Custom weight specification. + +**Format:** +- SDXL: 20 comma-separated values +- SD1.5: 17 comma-separated values + +**Example (SDXL):** +``` +1,0.5,0.5,0.3,0.3,0.3,0.3,0.3,0.3,0.3,0.7,0.7,0.7,1,1,1,1,1,1,1 +``` + +**Auto-adjustment:** +- If too few elements: pads with 1.0 at the end +- If too many elements: truncates from the end + +--- + +## Block Structure + +### **SDXL (20 elements)** +``` +BASE, IN00-IN08 (9), MID (1), OUT00-OUT08 (9) +``` + +### **SD1.5 (17 elements)** +``` +BASE, IN01,IN02,IN04,IN05,IN07,IN08 (6), MID (1), OUT03-OUT11 (9) +``` + +--- + +## Automatic Detection + +### **LoRA Type Detection** + +The node automatically detects whether a LoRA is SD1.5 or SDXL by analyzing the model structure: + +- **SD1.5**: `output_blocks_0` to `output_blocks_11` (12 blocks) +- **SDXL**: `output_blocks_0` to `output_blocks_8` (9 blocks) + +**You don't need to specify the model type manually!** ✅ + +--- + +### **Weight Adjustment** + +If the number of weights doesn't match the detected LoRA type: + +**Example 1: Too few weights** +``` +Input: 1,0,0 (3 elements) +Detected: SDXL (requires 20) +→ Auto-adjusted: 1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +``` + +**Example 2: Too many weights** +``` +Input: 1,0,0,...,0,0,0 (25 elements) +Detected: SDXL (requires 20) +→ Auto-adjusted: 1,0,0,...,0,0,0 (first 20 only) +``` + +--- + +## Usage + +### **Basic Workflow** + +``` +[Filtered Random LoRA Loader (LBW)] + MODEL → [KSampler] + CLIP → [CLIP Text Encode] + positive_text → [Show Text] (optional, for debugging) +``` + +### **Parameters** + +#### **LoRA Settings** +- `lora_folder_path`: Path to LoRA folder +- `num_loras`: Number of LoRAs to apply (0-20) +- `model_strength`: LoRA strength for MODEL (e.g., "1.0" or "0.6-0.9") +- `clip_strength`: LoRA strength for CLIP (e.g., "1.0" or "0.6-0.9") + +#### **Keyword Filter** +- `keyword_filter`: Space-separated keywords (e.g., `style anime` or `"anime style" red`) +- `filter_mode`: AND / OR +- `search_in_metadata`: Search in JSON/embedded metadata (slower) + +#### **LBW Settings** (New!) +- `weight_mode`: Normal / 4 Presets / Random / Direct Input +- `lbw_input`: Custom weights (only for Direct Input mode) + +#### **Other** +- `trigger_word_source`: Trigger word extraction method +- `seed`: Random seed + +--- + +## Output Format + +### **LBW Syntax** + +When LBW is applied, the `positive_text` output includes LBW syntax: + +**Normal (All 1.0):** +``` + +``` + +**With LBW (Style Focused, SDXL):** +``` + +``` + +**Note:** The LBW is applied internally to the MODEL. The syntax in `positive_text` is for reference/re-use with other nodes. + +--- + +## Tips + +### **1. Preset Selection Guide** + +| LoRA Type | Recommended Preset | Why | +|-----------|-------------------|-----| +| Style/Art | Style Focused | OUTPUT blocks control style | +| Character | Character Focused | Balanced IN+MID+OUT for features | +| Pose/Composition | Structure/Composition Only | INPUT blocks control layout | +| General | Balanced / Soft | Gentle, stackable effect | + +--- + +### **2. Effect Visibility** + +The difference between presets is **more visible** with: +- ✅ Strong LoRAs (high model_strength) +- ✅ Single LoRA application (num_loras=1) +- ✅ Style or concept LoRAs + +The difference is **less visible** with: +- ⚠️ Weak LoRAs (low model_strength) +- ⚠️ Multiple LoRAs applied simultaneously +- ⚠️ LoRAs with evenly distributed training + +--- + +### **3. Zero Weight = No Effect** + +Setting all weights to 0 completely disables the LoRA effect: +``` +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +``` +This is equivalent to not applying the LoRA at all. + +--- + +### **4. Serial Connection** + +For applying different LBW settings to different LoRAs: +``` +[Filtered Random LoRA Loader (LBW)] (Style Focused) + ↓ +[Filtered Random LoRA Loader (LBW)] (Character Focused) + ↓ +[KSampler] +``` + +All LoRAs within a single node instance use the same LBW setting. + +--- + +## Requirements + +### **⚠️ Supported Models** +**This node is designed for SD1.5 and SDXL models ONLY.** + +- ✅ **Supported:** Stable Diffusion 1.5, Stable Diffusion XL (SDXL) +- ❌ **NOT Supported:** Flux, SD3, SDXL Turbo, Pony, or other architectures + +The LBW feature specifically targets SD1.5/SDXL U-Net architecture. Other model types will not work correctly. + +--- + +### **Dependencies** +- ComfyUI +- Pillow (included with ComfyUI) +- torch (included with ComfyUI) + +### **⚠️ Optional: Video Preview** +**For video preview support (.mp4, .webm, .avi, .mov), opencv-python is REQUIRED:** +```bash +pip install opencv-python +``` + +**Without opencv-python:** +- ✅ Static/animated images work +- ❌ Video files show black screen + +--- + +## Preview Images + +Supports the following formats (in priority order): + +1. **Static images** (.png, .jpg, .jpeg) +2. **Animated images** (.gif, .webp) - first frame +3. **Video files** (.mp4, .webm, .avi, .mov) - first frame (requires opencv-python) + +The node automatically searches for preview files matching the LoRA filename. + +--- + +## Disclaimer + +- No technical support provided +- No warranty or guarantee of functionality +- No guaranteed compatibility with future ComfyUI updates +- Bug reports and feature requests may not be addressed +- Use at your own risk + +--- + +## Changelog + +### v1.2.0 (2026-01-13) +- ✅ Added LoRA Block Weight (LBW) support +- ✅ Automatic SD1.5/SDXL detection +- ✅ 4 preset modes + custom input +- ✅ Automatic weight adjustment +- ✅ Video preview support (.mp4, .webm, etc.) + +### v1.1.0 (2026-01-04) +- ✅ Added Filtered Random LoRA Loader +- ✅ Keyword filtering with AND/OR modes +- ✅ Metadata search support +- ✅ Preview image display + +### v1.0.0 (2025-12-30) +- ✅ Initial release +- ✅ Random LoRA Loader (3 groups) + +--- + +## License + +MIT License + +--- + +## Author + +Your Name + +--- + +**Enjoy precise LoRA control with Block Weights!** 🎨✨ diff --git a/README_LBW_ja.md b/README_LBW_ja.md new file mode 100644 index 0000000..2f548cd --- /dev/null +++ b/README_LBW_ja.md @@ -0,0 +1,494 @@ +# Filtered Random LoRA Loader (LBW) - 詳細ガイド + +**LoRAブロックウェイト(LBW)対応版** + +![FilteredRandomLoRALoader(LBW)ワークフロー例](./images/lbw_final_single.webp) + +--- + +## 概要 + +Filtered Random LoRA Loader (LBW) は、LoRAブロックウェイト(LBW)機能を追加した高度なノードです。U-Netのどの部分にLoRAを適用するかを細かく制御できます。 + +--- + +## 主な機能 + +### **✅ LoRAブロックウェイト(LBW)対応** +- U-Netのブロックごとに個別の強度を設定 +- 画風、キャラクター、構図などを分離制御 + +### **✅ 自動SD1.5/SDXL検出** +- LoRAファイルを自動判定 +- ウェイト数を自動調整 + +### **✅ 4つのプリセットモード** +- Style Focused(画風特化) +- Character Focused(キャラ重視) +- Structure/Composition Only(構造・構図のみ) +- Balanced / Soft(バランス・穏やか) + +### **✅ カスタム入力** +- Direct Inputモードで任意のウェイトを指定 +- Preset: Randomモードでランダムプリセット選択 + +--- + +## LBWとは? + +### **LoRAブロックウェイトの仕組み** + +Stable DiffusionのU-Netは以下の3つの部分で構成されています: + +``` +INPUT blocks → 構造、構図、レイアウト + ↓ +MIDDLE block → 全体的な特徴 + ↓ +OUTPUT blocks → 画風、ディテール、細部調整 +``` + +LBWを使うと、**各ブロックに異なるLoRA強度を設定**できます。 + +--- + +## ブロック構造 + +### **SDXL(20要素)** + +| インデックス | ブロック | 役割 | +|------------|---------|------| +| 0 | BASE | 全体ベース | +| 1-9 | INPUT (IN00-08) | 構造・構図 | +| 10 | MIDDLE (M00) | 中間特徴 | +| 11-19 | OUTPUT (OUT00-08) | 画風・ディテール | + +**ウェイト例:** +``` +1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1 +BASE,IN00-08 (OFF),M00,OUT00-08 (ON) +→ OUTPUTブロックのみLoRA適用(画風特化) +``` + +--- + +### **SD1.5(17要素)** + +| インデックス | ブロック | 役割 | +|------------|---------|------| +| 0 | BASE | 全体ベース | +| 1-6 | INPUT (IN01,02,04,05,07,08) | 構造・構図 | +| 7 | MIDDLE (M00) | 中間特徴 | +| 8-16 | OUTPUT (OUT03-11) | 画風・ディテール | + +**ウェイト例:** +``` +1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1 +BASE,IN (OFF),M00,OUT (ON) +→ OUTPUTブロックのみLoRA適用(画風特化) +``` + +--- + +## プリセット詳細 + +### **1. Style Focused(画風特化)** + +**SDXL:** `1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1` +**SD1.5:** `1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1` + +**特徴:** +- OUTPUTブロックのみ有効 +- 構図を変えず画風のみ適用 +- 既存の構図を保ちたい場合に最適 + +**用途:** +- 画風LoRA(アニメ風、水彩風など) +- スタイルLoRA +- エフェクトLoRA + +--- + +### **2. Character Focused(キャラ重視)** + +**SDXL:** `1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1` +**SD1.5:** `1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1` + +**特徴:** +- INPUT前半 + MIDDLE + OUTPUT有効 +- キャラクターの特徴を保持 +- バランスの取れた適用 + +**用途:** +- キャラクターLoRA +- 人物LoRA +- コンセプトLoRA + +--- + +### **3. Structure/Composition Only(構造・構図のみ)** + +**SDXL:** `1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0` +**SD1.5:** `1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0` + +**特徴:** +- INPUT + MIDDLEブロックのみ有効 +- 画風を変えず構図のみ変更 +- ポーズや構図調整に最適 + +**用途:** +- ポーズLoRA +- 構図LoRA +- レイアウトLoRA + +--- + +### **4. Balanced / Soft(バランス・穏やか)** + +**SDXL:** `1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0` +**SD1.5:** `1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0` + +**特徴:** +- 全体的に穏やかな適用 +- OUTPUT後半を弱めて過剰適用を防止 +- 自然な仕上がり + +**用途:** +- 汎用LoRA +- 微調整用 +- 複数LoRA併用時 + +--- + +## 使い方 + +### **基本的な使い方** + +1. **ノードを配置** + ``` + [Filtered Random LoRA Loader (LBW)] + ``` + +2. **フォルダとキーワードを設定** + ``` + lora_folder_path: "path/to/loras" + keyword_filter: "anime style" + num_loras: 1 + ``` + +3. **LBWモードを選択** + ``` + weight_mode: "Style Focused" + ``` + +4. **MODEL/CLIPを接続** + ``` + MODEL → [KSampler] + CLIP → [CLIP Text Encode] + ``` + +--- + +### **Direct Inputモード** + +カスタムウェイトを直接入力: + +``` +weight_mode: "Direct Input" +lbw_input: "1,0.5,0.5,0,0,0,0,0,0,0,1,1,1,1,1,1,0.5,0.5,0.5,0.5" +``` + +**ルール:** +- カンマ区切り +- SDXL: 20要素、SD1.5: 17要素 +- 要素数が合わない場合は自動調整 + +--- + +### **Preset: Randomモード** + +毎回異なるプリセットをランダム選択: + +``` +weight_mode: "Preset: Random" +→ 実行ごとに4つのプリセットから1つ選択 +``` + +--- + +## 使用例 + +### **例1: 画風のみ変更** + +``` +[Load Checkpoint] → [Filtered Random LoRA Loader (LBW)] + weight_mode: "Style Focused" + keyword_filter: "anime" + ↓ + [KSampler] +``` + +**結果:** 構図はそのまま、画風のみアニメ風に + +--- + +### **例2: 複数LoRAで段階適用** + +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader (LBW)] (Structure/Composition Only) + keyword_filter: "pose" + ↓ +[Filtered Random LoRA Loader (LBW)] (Style Focused) + keyword_filter: "watercolor" + ↓ +[KSampler] +``` + +**結果:** ポーズ調整 → 水彩画風適用 + +--- + +### **例3: キャラ + 画風** + +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader (LBW)] (Character Focused) + keyword_filter: "character_name" + ↓ +[Filtered Random LoRA Loader (LBW)] (Style Focused) + keyword_filter: "art_style" + ↓ +[KSampler] +``` + +**結果:** キャラ特徴保持 + 画風変更 + +--- + +## ヒント・Tips + +### **1. プリセット選択ガイド** + +| LoRAタイプ | 推奨プリセット | +|-----------|--------------| +| 画風・スタイル | Style Focused | +| キャラクター | Character Focused | +| ポーズ・構図 | Structure/Composition Only | +| 汎用・微調整 | Balanced / Soft | + +--- + +### **2. 直列接続のコツ** + +- **構造 → 画風の順**が基本 +- 各ノードで異なるLBW設定可能 +- 強度も個別に調整可能 + +--- + +### **3. ウェイト調整のコツ** + +- **0**: 完全無効 +- **0.5**: 弱め +- **1.0**: 標準 +- **1.5以上**: 強め(過剰に注意) + +--- + +### **4. トラブルシューティング** + +**効果が出ない場合:** +- すべて0になっていないか確認 +- LoRAが正しく読み込まれているか確認 +- コンソールでLBW適用メッセージを確認 + +**効果が強すぎる場合:** +- model_strengthを下げる +- Balanced / Softプリセットを試す +- ウェイトを0.5~0.8に調整 + +--- + +## 自動検出 + +### **SD1.5 / SDXL自動判定** + +ノードは自動的にLoRAタイプを検出: + +``` +SDXL検出 → 20要素ウェイト使用 +SD1.5検出 → 17要素ウェイト使用 +``` + +**ユーザーは何も設定不要** ✨ + +--- + +### **ウェイト数自動調整** + +入力したウェイト数が合わない場合、自動調整: + +``` +入力: 15要素(不足) +検出: SDXL(20要素必要) +→ 末尾に1.0を5個追加 + +入力: 25要素(過剰) +検出: SDXL(20要素必要) +→ 末尾から5個カット +``` + +--- + +## 要件 + +### **⚠️ 対応モデル** +**本ノードはSD1.5とSDXLモデル専用です。** + +- ✅ **対応:** Stable Diffusion 1.5、Stable Diffusion XL(SDXL) +- ❌ **非対応:** Flux、SD3、SDXL Turbo、Pony、その他のアーキテクチャ + +LBW機能は、SD1.5/SDXLのU-Netアーキテクチャ専用に設計されています。他のモデルタイプではLBWが正常に動作しません。 + +--- + +### **依存関係** +- ComfyUI +- Pillow(ComfyUIに含まれる) +- torch(ComfyUIに含まれる) + +### **⚠️ オプション: 動画プレビュー** +**動画ファイルプレビュー(.mp4, .webm, .avi, .mov)にはopencv-pythonが必須です:** +```bash +pip install opencv-python +``` + +**opencv-pythonなしの場合:** +- ✅ 静止画/アニメ画像は動作 +- ❌ 動画ファイルは黒画面 + +--- + +## プレビュー画像 + +以下の形式に対応(優先順位順): + +1. **静止画像**(.png, .jpg, .jpeg) - 常に動作 +2. **アニメ画像**(.gif, .webp) - 1フレーム目、常に動作 +3. **動画ファイル**(.mp4, .webm, .avi, .mov) - 1フレーム目、opencv-python必要 + +**ファイルマッチング:** +- LoRAファイル名で始まるファイル(大文字小文字無視) +- 例: `style_anime.safetensors` にマッチ: + - `style_anime.png` ✅ + - `style_anime_preview.jpg` ✅ + - `STYLE_ANIME.PNG` ✅ + +--- + +## 上級者向け: カスタムプリセット + +独自のプリセットを作成したり、既存のプリセットを変更したい場合は、ソースファイルを直接編集できます。 + +### **編集方法** + +**ファイル位置:** +``` +ComfyUI/custom_nodes/RandomLoRALoader/filtered_random_lora_loader_lbw.py +``` + +**編集箇所:** 29~41行目 + +### **例1: 独自プリセット追加** + +```python +SDXL_PRESETS = { + "Style Focused": "1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1", + "Character Focused": "1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1", + "Structure/Composition Only": "1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0", + "Balanced / Soft": "1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0", + # ↓ 独自プリセット + "My Custom Mix": "0.5,0.5,0.5,0.5,0,0,0,0,0,0,1,0.8,0.8,0.8,0.8,0.8,0.5,0.5,0.5,0.5" +} +``` + +### **例2: 既存プリセット調整** + +```python +SDXL_PRESETS = { + # Style Focusedをより強く + "Style Focused": "1,0,0,0,0,0,0,0,0,0,0,1.2,1.2,1.2,1.2,1.2,1.2,1.2,1.2,1.2", + + # Character Focusedをマイルドに + "Character Focused": "0.8,0.8,0.8,0.8,0,0,0,0,0,0,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8", + ... +} +``` + +### **例3: プリセット名変更** + +```python +SDXL_PRESETS = { + "画風特化": "1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1", + "キャラ重視": "1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1", + "構図のみ": "1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0", + ... +} +``` + +### **重要な注意事項** + +⚠️ **編集前に:** +- 元ファイルのバックアップを作成 +- Python構文(カンマ、引用符)に注意 + +⚠️ **ウェイト数:** +- SDXLプリセット: 必ず20要素 +- SD1.5プリセット: 必ず17要素 +- 要素数が合わない場合は自動調整されますが、正確に指定する方が良いです + +⚠️ **編集後:** +- ComfyUIを完全に再起動して変更を適用 +- コンソールで構文エラーを確認 + +💡 **ヒント:** ファイル編集が面倒な場合は、「Direct Input」モードを使ってください! + +--- + +## 更新履歴 + +### v1.2.0(2026-01-13) +- ✅ LoRAブロックウェイト(LBW)対応追加 +- ✅ SD1.5/SDXL自動検出 +- ✅ 4つのプリセットモード+カスタム入力 +- ✅ ウェイト数自動調整 +- ✅ 動画プレビュー対応(.mp4, .webm等) + +### v1.1.0(2026-01-04) +- ✅ Filtered Random LoRA Loader追加 +- ✅ キーワードフィルタリング(AND/ORモード) +- ✅ メタデータ検索対応 +- ✅ プレビュー画像表示 + +### v1.0.0(2025-12-30) +- ✅ 初回リリース +- ✅ Random LoRA Loader(3グループ) + +--- + +## ライセンス + +MIT License + +--- + +## 作者 + +Your Name + +--- + +**ブロックウェイトで精密なLoRA制御を楽しんでください!** 🎨✨ diff --git a/README_ja.md b/README_ja.md index 0b3e7ef..3746653 100644 --- a/README_ja.md +++ b/README_ja.md @@ -2,13 +2,26 @@ **[English README](./README.md) | 日本語版** -ComfyUIでLoRAをランダムに選択・適用するカスタムノードパッケージです。2つのノードを含みます: +ComfyUIでLoRAをランダムに選択・適用するカスタムノードパッケージです。3つのノードを含みます: 1. **Random LoRA Loader** - 3フォルダから同時選択 -2. **Filtered Random LoRA Loader** - 1フォルダ + キーワードフィルタ(新機能 v1.1.0) +2. **Filtered Random LoRA Loader** - 1フォルダ + キーワードフィルタ +3. **Filtered Random LoRA Loader (LBW)** - 🆕 LBW対応 + SD1.5/SDXL自動検出(v1.2.0新機能) -![RandomLoRAloderワークフロー例](./images/RLL_final_single.webp) -![FilteredRandomLoRAloderワークフロー例](./images/filterd_final_single.webp) +![RandomLoRALoaderワークフロー例](./images/RLL_final_single.webp) +![FilteredRandomLoRALoaderワークフロー例](./images/filterd_final_single.webp) +![FilteredRandomLoRALoader(LBW)ワークフロー例](./images/lbw_final_single.webp) + +--- + +## ⚠️ 重要: 対応モデル + +**本ノードはSD1.5とSDXLモデル専用です。** + +- ✅ **対応:** Stable Diffusion 1.5、Stable Diffusion XL(SDXL) +- ❌ **非対応:** Flux、SD3、SDXL Turbo、Pony、その他のアーキテクチャ + +LBWノードのブロックウェイト機能は、SD1.5/SDXLのU-Netアーキテクチャ専用に設計されています。他のモデルタイプではLBWが正常に動作しません。 --- @@ -23,7 +36,7 @@ ComfyUIでLoRAをランダムに選択・適用するカスタムノードパッ - 固定的なLoRAカテゴリ - シンプルなフォルダベースのワークフロー -### Filtered Random LoRA Loader(新機能) +### Filtered Random LoRA Loader 1つのフォルダからキーワードフィルタで選択。動的な選択と大規模コレクションに最適。 @@ -33,116 +46,190 @@ ComfyUIでLoRAをランダムに選択・適用するカスタムノードパッ - メタデータ検索機能 - 複数インスタンスの直列接続を推奨 +### Filtered Random LoRA Loader (LBW) 🆕 v1.2.0新機能 + +LoRAブロックウェイト(LBW)対応の高度なノード。精密な効果コントロールが可能。 + +**用途:** +- 画風と構造の分離制御 +- 精密なLoRA適用 +- プロフェッショナルワークフロー +- 4つのプリセットモード + カスタム入力 + +**詳細は [README_LBW_ja.md](README_LBW_ja.md) を参照してください。** + --- -## 主な機能(両ノード共通) +## 主な機能 + +### 共通機能(全ノード) -- **3グループ対応**: 最大3つの異なるフォルダからLoRAを選択可能 - **マルチソースメタデータ読み取り**: 優先順位に従ってトリガーワードと作例プロンプトを自動取得 1. `.metadata.json`形式(ComfyUI Lora Manager)- 優先度1 2. `.info`形式(Civitai Helper)- 優先度2 - 3. **LoRA本体ファイルの埋め込みメタデータ** - 優先度3(新機能) -- **強度ランダム化**: 範囲指定で強度をランダムに変化(例: `0.4-0.8`) + 3. **LoRA本体ファイルの埋め込みメタデータ** - 優先度3 +- **強度ランダム化**: + - **マイナス値対応の範囲指定**: `0.4-0.8`、`-0.8--0.3`(v1.2.0) + - **範囲指定は0.1刻み**: 効果の違いを確認しやすい + - **固定値は小数点2桁に丸め**: `0.847` → `0.85` - **柔軟なトリガーワード取得**: - `json_combined`: 全トリガーワードパターンを結合(重複除去) - `json_random`: ランダムに1パターン選択 - `json_sample_prompt`: 作例プロンプトをランダムに取得 - - **`metadata`**: 埋め込みメタデータから直接読み込み(新機能) -- **作例プロンプトの最適化**: 作例内のLoRA記述を自動削除し、ノード設定の強度を適用 -- **2つのテキスト出力**: positive_textとnegative_textを個別に出力 -- **ComfyUI標準のseed制御**: fixed/randomize/increment/decrementに対応 + - `metadata`: 埋め込みメタデータから直接読み込み +- **作例プロンプト最適化**: LoRA構文を自動除去し、ノード設定の強度を適用 +- **デュアルテキスト出力**: positive_textとnegative_textを分離出力 +- **ComfyUI標準シード制御**: 固定/ランダム/増分/減分に対応 +- **Wildcard Encode統合**: Wildcard Encode(Inspire)とシームレスに連携 +- **LoRA構文自動除去**: additional_prompt内のLoRA構文を自動削除 +- **プレビュー画像対応**: 選択されたLoRAのプレビュー画像/動画を表示(v1.1.0) +- **重複ファイル名処理**: サブフォルダ間の重複ファイル名を除外(v1.1.0) + +### Filteredノード(Filtered & LBW) + +- **キーワードフィルタリング**: スペース区切りキーワード、フレーズ対応 +- **AND/ORモード**: 柔軟なフィルタリングロジック +- **メタデータ検索**: ファイル名または埋め込みメタデータから検索 +- **高速キャッシング**: 初回読み込み後は即座にアクセス + +### LBWノード限定 🆕 + +- **LoRAブロックウェイト対応**: 影響を受けるU-Netブロックを制御 +- **SD1.5/SDXL自動検出**: 手動でモデルタイプを選択不要 +- **4つのプリセットモード**: + - Style Focused - OUTPUTブロックのみ + - Character Focused - バランスの取れたIN+MID+OUT + - Structure/Composition Only - INPUT+MIDブロックのみ + - Balanced / Soft - 穏やかな適用 +- **Preset: Randomモード**: 4つのプリセットからランダム選択 +- **Direct Inputモード**: カスタムウェイト指定 +- **ウェイト数自動調整**: 要素数の不一致を自動処理 + +--- ## インストール -### 必要な環境 +### ⚠️ オプション: 動画プレビュー対応 -- ComfyUI(動作確認済み環境があれば、そのまま使えます) -- **追加ライブラリのインストール: 不要** ✅ +**動画ファイルプレビュー(.mp4, .webm, .avi, .mov)にはopencv-pythonが必須です:** -### 手順 +```bash +pip install opencv-python +``` + +**opencv-pythonなしの場合:** +- ✅ 静止画(.png, .jpg, .jpeg)は動作 +- ✅ アニメ画像(.gif, .webp)は動作 +- ❌ **動画ファイルは黒画面表示**(コンソールに警告メッセージ) + +opencv-pythonの有無に関わらず、LoRA機能は正常に動作します。影響を受けるのはプレビュー表示のみです。 + +### 必要要件 + +- ComfyUI(標準インストールで動作) +- Python 3.9+ +- **コア依存関係はComfyUIに含まれる** ✅ + +### インストール手順 ```bash cd ComfyUI/custom_nodes -git clone https://github.com/your-repo/RandomLoRALoader.git -# または -# 手動でRandomLoRALoaderフォルダを作成し、ファイルをコピー +git clone https://github.com/YOUR_USERNAME/RandomLoRALoader.git +# または手動でRandomLoRALoaderフォルダを作成してファイルをコピー ``` -ComfyUIを再起動してください。 +ComfyUIを再起動。 -**注意:** Python標準ライブラリとComfyUI付属ライブラリのみ使用しているため、`pip install`などの追加インストールは一切不要です。 +**注意:** コア機能にはPython標準ライブラリとComfyUIバンドルライブラリのみを使用。opencv-pythonは動画プレビューのみのオプション。 + +### アンインストール + +```bash +# custom_nodesフォルダに移動 +cd ComfyUI/custom_nodes + +# RandomLoRALoaderフォルダを削除 +rm -rf RandomLoRALoader + +# Windowsの場合 +rmdir /s RandomLoRALoader +``` + +またはRandomLoRALoaderフォルダを手動で削除してComfyUIを再起動。 + +--- ## 使い方 -### 基本的な使用方法 +### 基本的な使い方(Random LoRA Loader) -1. **ノードを配置**: ノードブラウザから「Random LoRA Loader」を検索して配置 -2. **MODEL/CLIPを接続**: ベースモデルとCLIPを入力に接続 -3. **フォルダパスを設定**: - - グループ1: スタイル用LoRAフォルダのパス - - グループ2: キャラクター用LoRAフォルダのパス(オプション) - - グループ3: コンセプト用LoRAフォルダのパス(オプション) -4. **各グループの設定**: - - `num_loras`: 各グループから選択するLoRA個数 - - `model_strength` / `clip_strength`: 適用強度(固定値またはランダム範囲) -5. **出力を接続**: - - `MODEL` / `CLIP`: KSamplerなどに接続 +1. **ノード追加**: ノードブラウザで「Random LoRA Loader」を検索 +2. **MODEL/CLIP接続**: ベースモデルとCLIPを入力に接続 +3. **フォルダパス設定**: + - グループ1: スタイルLoRAフォルダパス + - グループ2: キャラクターLoRAフォルダパス(オプション) + - グループ3: コンセプトLoRAフォルダパス(オプション) +4. **各グループ設定**: + - `num_loras`: 各グループから選択するLoRA数 + - `model_strength` / `clip_strength`: 適用強度(固定値または範囲指定) +5. **出力接続**: + - `MODEL` / `CLIP`: KSamplerに接続 - `positive` / `negative`: Set ConditioningまたはKSamplerに接続 - - `positive_text` / `negative_text`: Show Textノードで確認(推奨) + - `positive_text` / `negative_text`: Show Textに接続して確認(推奨) -### Wildcard Encode (Inspire)との連携(推奨ワークフロー) +### Wildcard Encode連携(推奨) -このノードはWildcard Encode (Inspire)と組み合わせることで、wildcardによる動的プロンプト生成とランダムLoRA選択の両方を実現できます。 +このノードはWildcard Encode(Inspire)とシームレスに連携し、動的プロンプト生成とランダムLoRA選択を組み合わせます。 -![Wildcard Encode+RandomLoRALoderワークフロー](./images/RLL_final_wce.webp) +![Wildcard Encode + Random LoRA Loader ワークフロー](./images/RLL_final_wce.webp) -#### 推奨接続例 +#### 推奨接続方法 ``` [Wildcard Encode (Inspire)] text: "__style__, {red|blue|green}, " ↓ ├─ populated_text ──→ [Random LoRA Loader] - │ additional_prompt + │ additional_prompt_positive └─ MODEL/CLIP ──────→ model/clip入力 [Random LoRA Loader] - フォルダ1: character用LoRA - フォルダ2: concept用LoRA + フォルダ1: キャラクターLoRA + フォルダ2: コンセプトLoRA ↓ MODEL/CLIP/CONDITIONING → [KSampler] ``` -#### 動作の流れ +#### 動作の仕組み -1. **Wildcard Encodeの処理**: - - wildcard展開: `__style__` → "anime style" +1. **Wildcard Encode処理**: + - ワイルドカード展開: `__style__` → "anime style" - 選択肢展開: `{red|blue|green}` → "blue" - LoRA構文処理: `` → MODELに適用 - 結果: `populated_text` = "anime style, blue, " -2. **Random LoRA Loaderの処理**: - - `populated_text`から**LoRA構文を自動削除**: "anime style, blue" +2. **Random LoRA Loader処理**: + - **LoRA構文を自動除去**: `populated_text`から → "anime style, blue" - ランダムLoRA選択: character_alice, concept_magic - 最終プロンプト: "anime style, blue, alice, blonde hair, magic circle" - - MODEL/CLIP: base_effect + character_alice + concept_magic(全て適用済み) + - MODEL/CLIP: base_effect + character_alice + concept_magic(全て適用) #### メリット -- ✅ wildcard機能をWildcard Encodeに任せる(専門ツールの利用) -- ✅ ランダムLoRA選択をこのノードに任せる(シンプルな設定) -- ✅ プロンプト情報が完全に引き継がれる -- ✅ LoRA構文が自動クリーンアップされる(ノイズなし) +- ✅ ワイルドカード機能はWildcard Encodeに委任(専門ツール) +- ✅ ランダムLoRA選択はこのノードに委任(シンプル設定) +- ✅ プロンプト情報を完全に継承 +- ✅ LoRA構文を自動クリーンアップ(ノイズなし) -### 3グループの活用例 +### 3グループの例 ``` -グループ1: style用LoRAフォルダ +グループ1: スタイルLoRA - num_loras_1: 2 - - model_strength_1: "0.6-0.9" ← ランダム - - clip_strength_1: "0.6-0.9" ← ランダム + - model_strength_1: "0.6-0.9" ← ランダム(0.1刻み) + - clip_strength_1: "0.6-0.9" ← ランダム(0.1刻み) -グループ2: character用LoRAフォルダ +グループ2: キャラクターLoRA - num_loras_2: 1 - model_strength_2: "1.0" ← 固定 - clip_strength_2: "1.0" ← 固定 @@ -151,891 +238,912 @@ ComfyUIを再起動してください。 - lora_folder_path_3: (空) - num_loras_3: 0 -→ 結果: style 2個(可変強度)+ character 1個(固定強度)= 合計3個のLoRAが適用 +→ 結果: スタイルLoRA 2個(可変強度) + キャラクターLoRA 1個(固定強度) = 合計3個 ``` -## 設定項目 +--- + +## 設定 ### 共通設定 -| 項目 | 説明 | デフォルト | -|------|------|------------| -| `token_normalization` | トークン正規化方式 | `none` | -| `weight_interpretation` | プロンプト強調記法の解釈方式 | `A1111` | -| `additional_prompt` | 追加プロンプト(全LoRAのトリガーワードと結合) | (空) | -| `trigger_word_source` | トリガーワード取得元 | `json_combined` | -| `seed` | ランダム選択のシード値 | `0` | +| 設定項目 | 説明 | デフォルト | +|---------|------|-----------| +| `token_normalization` | トークン正規化方法 | `none` | +| `weight_interpretation` | プロンプト強調記法の解釈 | `A1111` | +| `additional_prompt_positive` | 追加ポジティブプロンプト(トリガーワードと結合) | (空) | +| `additional_prompt_negative` | 追加ネガティブプロンプト | (空) | +| `trigger_word_source` | トリガーワードソース | `json_combined` | +| `seed` | ランダム選択シード | `0` | -#### additional_promptに関する重要な注意事項 +#### additional_promptに関する重要な注意 -`additional_prompt`欄の動作仕様: - -**✅ 対応している:** +**対応:** - 通常のプロンプトテキスト -- 他ノードからのテキスト入力接続(例: Wildcard Encodeの`populated_text`) +- 他ノードからのテキスト入力(例: Wildcard Encodeの`populated_text`) -**❌ 対応していない(自動削除されます):** +**非対応(自動除去):** - `` 形式のLoRA構文 -- `{a|b|c}` 形式のwildcard構文 -- `__filename__` 形式のwildcard構文 +- `{a|b|c}` 形式のワイルドカード構文 +- `__filename__` 形式のワイルドカード構文 **理由:** -- このノードはフォルダ指定によるLoRA適用を行います -- wildcard機能は専用ノード(Wildcard Encode等)に任せる設計です -- LoRA構文を記述しても解釈されず、**ファイル名が意味のあるトークンとしてノイズになります** +- このノードはフォルダ指定でLoRAを適用 +- ワイルドカード機能は専用ノード(Wildcard Encodeなど)が処理すべき +- LoRA構文を書くと**ファイル名が意味のあるトークンとしてノイズになる** **例(問題のあるケース):** ``` 入力: "1girl, , beautiful" ↓ -CLIPのトークン化: [1girl, lora, anime, style, 0, 8, beautiful] - ^^^^^ ^^^^^ - 意図しない単語がプロンプトに追加される ❌ +CLIPトークン化: [1girl, lora, anime, style, 0, 8, beautiful] + ^^^^^ ^^^^^ + 意図しない単語がプロンプトに追加 ❌ ``` **正しい使い方:** ``` -additional_prompt: "1girl, beautiful" ← LoRA構文なし ✅ +additional_prompt_positive: "1girl, beautiful" ← LoRA構文なし ✅ LoRA適用: フォルダ指定機能を使用 ✅ ``` -または、Wildcard Encodeからの接続時は自動的にLoRA構文が削除されます: +または、Wildcard Encodeから接続する場合、LoRA構文は自動除去されます: ``` [Wildcard Encode] populated_text: "1girl, , beautiful" ↓ -[このノード] additional_prompt受信 → LoRA構文自動削除 +[このノード] additional_prompt_positive受信 → LoRA構文自動除去 ↓ 最終プロンプト: "1girl, beautiful" ✅ ``` -### グループ設定(1〜3) +### グループ設定(1-3) -各グループで以下の項目を個別に設定可能: +各グループを個別に設定可能: -| 項目 | 説明 | デフォルト | -|------|------|------------| +| 設定項目 | 説明 | デフォルト | +|---------|------|-----------| | `lora_folder_path_X` | LoRAフォルダの絶対パス | (空) | -| `include_subfolders_X` | サブフォルダを含めるか | `true` | +| `include_subfolders_X` | サブフォルダを含める | `true` | | `unique_by_filename_X` | 重複ファイル名を除外 | `true` | -| `model_strength_X` | MODELへの適用強度 | `"1.0"` | -| `clip_strength_X` | CLIPへの適用強度 | `"1.0"` | -| `num_loras_X` | 選択するLoRA個数 | グループ1: `1`, グループ2/3: `0` | - -## 強度の指定方法(重要) - -強度欄には**固定値**または**ランダム範囲**を指定できます。 - -### 固定値の場合 - -``` -入力: "1.0" -→ 常に 1.0 で適用 - -入力: "0.55" -→ 常に 0.55 で適用 - -入力: "0.8" -→ 常に 0.8 で適用 -``` - -**特徴:** -- 入力した数値がそのまま使用されます -- 小数第2位まで指定可能(例: `0.55`) - -### ランダム範囲の場合 - -``` -入力: "0.4-0.8" -→ 0.4, 0.5, 0.6, 0.7, 0.8 からランダムに選択(0.1刻み) - -入力: "0.5-1.0" -→ 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 からランダムに選択 - -入力: "0.44-0.82" -→ 0.4, 0.5, 0.6, 0.7, 0.8 からランダムに選択 - (範囲は自動的に小数第1位に丸められます) -``` - -**特徴:** -- ハイフン(`-`)で範囲を指定 -- 0.1刻みで値が生成されます -- 範囲の上限・下限が小数第2位以下の場合、小数第1位に丸められます -- 毎回実行するたびに異なる値が選ばれます - -### エラー処理 - -``` -入力: "abc" または不正な形式 -→ エラーログが出力され、強度 1.0 で処理されます -``` - -**エラーメッセージ例:** -``` -[RandomLoRALoader] ❌ 強度 'abc' を解析できません、1.0を使用 -[RandomLoRALoader] 💡 使用例: '1.0' または '0.4-0.8' -``` - -### 実際の強度確認 - -実際に使用された強度は `positive_text` 出力で確認できます: - -``` -, anime style, vibrant, - ↑ ↑ - MODEL強度 CLIP強度 -``` - -**推奨:** Show Textノードを `positive_text` に接続して、実際の強度を確認してください。 - -## トリガーワード取得方式 - -### json_combined(全結合) - -全トリガーワードパターンを結合し、重複を除去します。 - -**例:** -```json -"trainedWords": [ - "character, red hair, blue eyes, uniform", - "character, red hair, blue eyes, casual clothes" -] -``` -↓ -``` -出力: character, red hair, blue eyes, uniform, casual clothes -``` - -### json_random(ランダム選択) - -トリガーワードパターンからランダムに1つを選択します。 - -**例:** -```json -"trainedWords": [ - "character, red hair, blue eyes, uniform", - "character, red hair, blue eyes, casual clothes" -] -``` -↓ -``` -出力: character, red hair, blue eyes, casual clothes (ランダム) -``` - -### json_sample_prompt(作例取得) - -JSONの作例プロンプトからランダムに1つ取得します。 - -- positive_textとnegative_textの両方が出力されます -- 作例内の``記述は自動削除されます -- ノード設定の強度で新しいLoRA記述が生成されます - -**例:** -```json -"images": [{ - "meta": { - "prompt": "1girl, , beautiful", - "negativePrompt": "bad quality, worst quality" - } -}] -``` -↓ -``` -positive_text: 1girl, beautiful (LoRA記述削除済み) -negative_text: bad quality, worst quality -``` - -### metadata(埋め込みメタデータのみ - 新機能) - -外部の`.metadata.json`や`.info`ファイルを無視して、**LoRA本体ファイルの埋め込みメタデータから直接**トリガーワードを読み込みます。 - -**対応メタデータフィールド:** -- `ss_tag_frequency`: kohya_ss形式のタグ頻度(上位20個を抽出) -- `modelspec.trigger_word`: トリガーワード -- `ss_output_name`: モデル名 - -**使用例:** -- LoRA作成者が埋め込んだメタデータのみを使用したい場合 -- 外部JSONファイルに誤った情報が含まれている場合 -- 外部メタデータファイルがないLoRAを使用する場合 - -**例:** -``` -LoRAファイルに埋め込まれたメタデータ: - ss_tag_frequency: {"dataset1": {"alice": 150, "blonde": 120, ...}} - ↓ -出力: alice, blonde, blue_eyes, ... -``` - -**注意:** 他のモード(`json_combined`、`json_random`、`json_sample_prompt`)は外部ファイルを優先し、見つからない場合のみ埋め込みメタデータにフォールバックします。 - -## 出力 - -| 出力名 | 型 | 説明 | -|--------|-----|------| -| `MODEL` | MODEL | LoRA適用後のモデル | -| `CLIP` | CLIP | LoRA適用後のCLIP | -| `positive_text` | STRING | LoRA情報とpositiveプロンプト | -| `negative_text` | STRING | negativeプロンプト(json_sample_promptのみ) | -| `positive` | CONDITIONING | positiveコンディショニング | -| `negative` | CONDITIONING | negativeコンディショニング | - -### positive_text出力例 - -``` -1girl, beautiful, -, anime style, vibrant colors, -, alice, blonde hair, blue eyes, -``` - -## 必要なファイル構成 - -**トリガーワード取得のため、メタデータファイルまたは埋め込みデータが必要です。** - -### 優先順位 - -ノードは以下の優先順位でメタデータを読み込みます: - -1. **`.metadata.json`**(ComfyUI Lora Manager形式) -2. **`.info`**(Civitai Helper形式) -3. **LoRA本体ファイルの埋め込みメタデータ**(LoRAが学習時にメタデータを含んでいれば常に利用可能) - -### 1. ComfyUI Lora Manager形式(推奨) - -``` -/path/to/lora/ -├── style_anime_v2.safetensors -├── style_anime_v2.metadata.json ← ComfyUI Lora Manager -├── character_alice.safetensors -└── character_alice.metadata.json -``` - -### 2. Civitai Helper形式 - -``` -/path/to/lora/ -├── style_anime_v2.safetensors -├── style_anime_v2.info ← Civitai Helper -├── character_alice.safetensors -└── character_alice.info -``` - -### 3. 埋め込みメタデータ(外部ファイル不要) - -``` -/path/to/lora/ -├── style_anime_v2.safetensors ← 埋め込みメタデータを含む -└── character_alice.safetensors ← 埋め込みメタデータを含む -``` - -**注意:** -- 外部ファイル(`.metadata.json`または`.info`)が存在する場合、埋め込みメタデータより優先されます(`trigger_word_source: metadata`を使用する場合を除く) -- `trigger_word_source`を`metadata`に設定すると、外部ファイルを無視して埋め込みメタデータのみを読み込みます - -### メタデータファイルの生成方法 - -**ComfyUI Lora Manager(推奨):** -- [ComfyUI Lora Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) -- `.metadata.json`ファイルを生成 - -**Civitai Helper:** -- [Civitai Helper](https://github.com/butaixianran/Stable-Diffusion-Webui-Civitai-Helper) -- `.info`ファイルを生成(こちらにも対応) - -**埋め込みメタデータ:** -- kohya_ssなどのLoRA学習ツールで自動的に作成されます -- 追加のツールは不要です - -## MODEL強度とCLIP強度について - -### 基本的な考え方 - -- **MODEL強度**: 画像そのものへの影響(画風、構図、色彩) -- **CLIP強度**: プロンプト理解への影響(トリガーワードの効き方) - -### 推奨設定 - -**初心者・一般的な使用:** -``` -model_strength: "1.0" -clip_strength: "1.0" -``` -両方同じ値にするのが基本です。 - -**上級者・微調整:** -``` -model_strength: "0.7" -clip_strength: "1.0" -``` -画風は控えめに、トリガーワードはしっかり効かせたい場合など。 - -**ランダム化で実験:** -``` -model_strength: "0.6-0.9" -clip_strength: "0.6-0.9" -``` -様々な強度の組み合わせを試したい場合。 - -## Token Normalization - -複数のプロンプトを結合する際のバランス調整: - -- **none**: トークン数が多いほど影響力大 -- **mean**: 平均値で正規化 -- **length**: 長さで正規化 -- **length+mean**: 両方を組み合わせ - -## Weight Interpretation - -プロンプト内の強調記法の解釈方式: - -- **comfy**: ComfyUI方式(絶対値、強調されすぎになりやすい) -- **A1111**: Automatic1111方式(正規化、バランス良い)← 推奨 -- **compel**: Compel方式 -- **comfy++**: ComfyUI拡張方式 -- **down_weight**: 減衰方式 - -## Seed制御 - -ComfyUIの標準`control_before_generate`機能に対応: - -- **fixed**: 固定seed(常に同じLoRAを選択、同じ強度) -- **randomize**: 毎回ランダム -- **increment**: 実行ごとに+1 -- **decrement**: 実行ごとに-1 - -## 技術的な詳細 - -### LoRA適用の仕組み - -1. 各グループからLoRAをランダム選択(全グループ共通seed) -2. 各LoRAに対して強度をパース(固定値またはランダム範囲から選択) -3. 選択されたLoRAを順次MODELとCLIPに適用 -4. トリガーワードを取得 -5. 全グループのトリガーワードを結合してCONDITIONINGを生成 - -### 強度ランダム化の仕組み - -```python -# 入力: "0.4-0.8" -# 処理: -1. 範囲を解析: min=0.4, max=0.8 -2. 0.1刻みのリスト生成: [0.4, 0.5, 0.6, 0.7, 0.8] -3. ランダムに1つ選択: 例えば 0.6 -4. MODEL/CLIPに適用 -``` - -**重要:** MODEL強度とCLIP強度は**独立してランダム選択**されます。 -- `model_strength: "0.4-0.8"`, `clip_strength: "0.4-0.8"` の場合 -- MODELが0.6、CLIPが0.7のように**異なる値**になる可能性があります - -### 重複選択について - -- 各グループ内では同じLoRAの重複選択なし -- 指定個数 > ファイル数の場合、全選択後に再選択で不足分を埋める - -### エラー処理 - -- フォルダパス空またはnum_loras=0: スキップ(エラーなし) -- 全グループ空: 空テキスト出力(エラーなし) -- JSONファイル未発見: 空文字列を返す(ログ出力) -- 強度パースエラー: 1.0で処理(ログ出力) - -## トラブルシューティング - -### additional_promptにLoRA構文を書いたのに効かない - -**症状:** -``` -additional_prompt: ", 1girl" -→ anime_styleが適用されない -``` - -**原因:** -このノードは``構文を解釈しません。LoRA構文は自動的に削除されます。 - -**対策:** -1. LoRA適用はフォルダ指定機能を使用してください -2. Wildcard Encode (Inspire)と連携する場合は、そちらでLoRA構文を処理してください - -### プロンプトに意図しない単語が含まれる - -**症状:** -``` -additional_prompt: "" -→ "anime", "style"という単語が勝手に追加される -``` - -**原因:** -v1.0.0以降では自動的にLoRA構文が削除されるため、この問題は発生しません。 -古いバージョンを使用している場合は、LoRA構文のファイル名部分がトークンとして解釈されていました。 - -**対策:** -- ノードを最新版に更新してください -- additional_promptにはLoRA構文を書かないでください - -### トリガーワードが取得できない - -**外部メタデータファイルの場合:** -- メタデータファイルが存在するか確認(`.metadata.json`または`.info`) -- JSONファイル内に`civitai.trainedWords`または`civitai.images`が存在するか確認 -- ファイル名が`{LoRAファイル名}.metadata.json`または`{LoRAファイル名}.info`と一致しているか確認 - -**埋め込みメタデータの場合:** -- `trigger_word_source`を`metadata`に設定してLoRAファイルから直接読み込む -- LoRAが学習時にメタデータを含んでいるか確認(最近のLoRAはほぼ含んでいます) -- 対応フィールド: `ss_tag_frequency`、`modelspec.trigger_word`、`ss_output_name` - -### LoRAが適用されない - -- フォルダパスが正しいか確認 -- `num_loras`が0になっていないか確認 -- ComfyUIのコンソールでエラーログを確認 - -### 強度が意図した値にならない - -- `positive_text`出力をShow Textで確認 -- ``のように実際の強度が表示されます -- ランダム範囲指定の場合、毎回異なる値になります -- コンソールで強度選択のログを確認: `[RandomLoRALoader] 強度範囲 0.4-0.8 から 0.6 を選択` - -### 強度入力でエラーが出る - -**症状:** -``` -[RandomLoRALoader] ❌ 強度 'abc' を解析できません、1.0を使用 -``` - -**対策:** -- 正しい形式で入力: `"1.0"` または `"0.4-0.8"` -- 範囲指定は必ずハイフン(`-`)で区切る -- 不正な入力でもノードはクラッシュせず、1.0で動作します - -### 初期値が表示されない - -- ComfyUIを完全に再起動 -- ブラウザのキャッシュをクリア(Ctrl+Shift+R) -- ノードを削除して新規配置 - -### コンソールに「lora key not loaded」メッセージが大量に出る - -**症状:** -``` -lora key not loaded: lora_unet_input_blocks_4_1_proj_in.alpha -lora key not loaded: lora_unet_input_blocks_4_1_proj_in.lora_down.weight -... -``` - -**原因と対策:** - -このメッセージはComfyUI本体が出力する互換性警告で、**機能には影響ありません**。LoRAファイルに含まれる一部のキーが現在のモデルと互換性がない場合に表示されますが、互換性のあるキーだけが正常に適用されます。 - -v1.0.0以降では、これらの警告メッセージを自動的に抑制するようになっています。もし大量に表示される場合は: - -1. ノードファイルが最新版か確認 -2. ComfyUIを再起動 -3. それでも表示される場合は、機能に問題がなければ無視して構いません - -## ライセンス - -MIT License - -## 更新履歴 - -### v1.0.0 (2024-12-30) -- 初回公開リリース -- 3グループ対応(異なるフォルダから選択可能) -- 強度ランダム化機能(範囲指定: `0.4-0.8`) -- 外部JSON読み取り(Civitai Helper対応) -- 作例プロンプト内のLoRA記述自動削除 -- **additional_prompt内のLoRA構文自動削除**(ノイズ防止) -- positive_text/negative_textの個別出力 -- Wildcard Encode (Inspire)との連携対応 -- 完全ローカル動作(API不要) - -## Tips & Tricks - -### Random LoRA Loader: LoRA構文フィルタとしての使用 - -LoRAを適用せず、単純にLoRA構文削除フィルタとしてこのノードを使用できます。 - -**設定:** -- 全グループの`num_loras`を`0`に設定(または全フォルダパスを空にする) -- Wildcard Encodeの`populated_text`を`additional_prompt`に接続 -- `positive_text`出力を次のノードに接続 - -**使用例:** -- Wildcard Encode出力からLoRA構文を削除 -- 他のノードに渡す前にプロンプトをクリーンアップ -- Wildcard EncodeのLoRA処理を使いつつ、互換性のために構文を削除 - -**ワークフロー例:** -``` -[Wildcard Encode (Inspire)] - text: "__style__, {red|blue|green}, " - ↓ - populated_text: "anime style, blue, " - (LoRAはWildcard EncodeによりMODELに適用済み) - ↓ -[Random LoRA Loader] - num_loras: 0, 0, 0 ← LoRA適用なし - additional_prompt ← populated_textから接続 - ↓ - positive_text: "anime style, blue," ← LoRA構文削除済み ✅ - ↓ -[次のノード] -``` - -**メリット:** -- ✅ ``構文がノイズトークンになるのを防止 -- ✅ 別途テキスト処理ノードが不要 -- ✅ LoRA構文を含むあらゆるテキストに対応 +| `model_strength_X` | MODEL適用強度 | `"1.0"` | +| `clip_strength_X` | CLIP適用強度 | `"1.0"` | +| `num_loras_X` | 選択するLoRA数 | グループ1: `1`、グループ2/3: `0` | --- -## Filtered Random LoRA Loader(新機能 v1.1.0) +## 強度指定(重要) -![FilteredRandomLoRALoder x1ワークフロー](./images/filterd_final_single.webp) +強度フィールドは**固定値**または**範囲指定**を受け付けます。**マイナス値にも対応**しています。 -### 概要 +### 固定値 -キーワードフィルタ機能を持つ単一グループLoRAローダー。複数インスタンスの直列接続を想定した設計。 +``` +入力: "1.0" +→ 常に1.0で適用 -**Random LoRA Loaderとの違い:** -- フォルダパスは1つ(3つではない) -- キーワードフィルタリング(AND/ORモード) -- メタデータ検索機能(キャッシュ付き) -- プロンプト入力が2つ(positive/negative) -- 直列接続を推奨 +入力: "0.55" +→ 0.55に丸められる(小数点2桁) + +入力: "-0.5" +→ マイナスLoRAとして-0.5で適用 ✅(v1.2.0) +``` + +### 範囲指定 + +**フォーマット:** `最小値-最大値` + +``` +入力: "0.6-0.9" +→ [0.6, 0.7, 0.8, 0.9] からランダム選択(0.1刻み) + +入力: "-0.8--0.3" +→ [-0.8, -0.7, -0.6, -0.5, -0.4, -0.3] からランダム選択 ✅(v1.2.0) + +入力: "-0.5-0.5" +→ [-0.5, -0.4, ..., 0.4, 0.5] からランダム選択 ✅ +``` + +**v1.2.0の改善:** +- ✅ **マイナス範囲対応**: `-0.8--0.3` が正しく動作 +- ✅ **全範囲で0.1刻み**: 効果の違いを確認しやすい +- ✅ **固定値は小数点2桁に丸め**: `0.847` → `0.85` + +### 動作 + +**全ノード:** +- 範囲: 0.1刻みリスト → ランダム選択 +- 固定: 小数点2桁に丸め + +**例:** +``` +model_strength_1: "0.6-0.9" +→ 各実行時に [0.6, 0.7, 0.8, 0.9] からランダム選択 +→ コンソール: "[RandomLoRALoader] Selected strength: 0.7" +``` + +--- + +## MODEL強度 vs CLIP強度 + +### MODEL強度 +- **影響範囲**: 画像そのもの(画風、構図、色、形) +- **効果**: 生成画像の視覚的特徴 +- **例**: + - 高い値: 強い画風転送 + - 低い値: 微妙な画風ヒント + +### CLIP強度 +- **影響範囲**: プロンプト解釈(概念理解、言語-画像関連付け) +- **効果**: モデルがプロンプト指示にどれだけ従うか +- **例**: + - 高い値: トリガーワードからの強いセマンティックガイダンス + - 低い値: 弱いセマンティック影響 + +### 推奨設定 + +**ほとんどのユースケース:** +``` +model_strength: 0.8 +clip_strength: 0.8 +→ バランスの取れた適用 +``` + +**微妙な画風適用:** +``` +model_strength: 0.4-0.6 +clip_strength: 0.8-1.0 +→ 軽い視覚効果、強いセマンティックガイダンス +``` + +**強い画風上書き:** +``` +model_strength: 1.0-1.2 +clip_strength: 0.6-0.8 +→ 強い視覚効果、中程度のセマンティックガイダンス +``` + +--- + +## トリガーワード取得方法 + +### json_combined(デフォルト) + +JSONメタデータから全トリガーワードパターンを結合。 + +**ソース:** +1. `activation text`フィールド +2. `ss_tag_frequency`キー(上位3つ) +3. `modelspec.description`キーワード +4. `ss_output_name`キーワード + +**例:** +```json +{ + "activation text": "anime style, detailed eyes", + "ss_tag_frequency": { + "1girl": 500, + "blue eyes": 300, + "long hair": 250 + } +} +``` +**結果:** `"anime style, detailed eyes, 1girl, blue eyes, long hair"` + +--- + +### json_random + +1つのトリガーワードパターンをランダムに選択。 + +**例:** +``` +候補: +- "anime style, detailed eyes" +- "1girl, blue eyes, long hair" +- "anime, girl, portrait" + +ランダム選択: "1girl, blue eyes, long hair" +``` + +--- + +### json_sample_prompt + +JSONメタデータから作例プロンプトを取得。 + +**ソースフィールド(優先順位順):** +1. `sample_prompts` +2. `civitai.images[0].meta.prompt` + +**処理:** +- LoRA構文(``)を自動除去 +- ノード設定の強度に置き換え +- ネガティブプロンプトも抽出(可能な場合) + +**例:** +```json +{ + "sample_prompts": "1girl, , beautiful, detailed eyes" +} +``` +**結果:** `"1girl, beautiful, detailed eyes"`(LoRA構文除去) + +--- + +### metadata + +埋め込みLoRAファイルメタデータから直接トリガーワードを読み込み。 + +**ソース:** +- Safetensorsメタデータキー +- json_combinedと同様だがLoRAファイルから直接読み込み + +**使用時:** +- 外部JSONファイルがない場合 +- オリジナルのトレーニングメタデータを使用したい場合 + +--- + +## 出力 + +全ノードは以下の出力を提供: + +| 出力 | タイプ | 説明 | +|------|------|------| +| `MODEL` | MODEL | LoRA適用済みモデル | +| `CLIP` | CLIP | LoRA適用済みCLIP | +| `positive` | CONDITIONING | ポジティブコンディショニング(LoRA構文除去) | +| `negative` | CONDITIONING | ネガティブコンディショニング | +| `positive_text` | STRING | LoRA構文付き完全ポジティブプロンプトテキスト | +| `negative_text` | STRING | 完全ネガティブプロンプトテキスト | +| `preview` | IMAGE | 選択されたLoRAのプレビュー画像バッチ(v1.1.0) | + +### テキスト出力形式 + +**positive_text例:** +``` +, , anime style, alice, blonde hair, 1girl, beautiful +``` + +**LBWノードのpositive_text例:** +``` +, anime style, 1girl, beautiful +``` + +**positive(CONDITIONING)内容:** +``` +anime style, alice, blonde hair, 1girl, beautiful +(クリーンなプロンプトのためLoRA構文除去) +``` + +### プレビュー画像(v1.1.0) + +**対応形式(優先順位順):** +1. 静止画(.png, .jpg, .jpeg) - 常に動作 +2. アニメ画像(.gif, .webp) - 1フレーム目、常に動作 +3. 動画ファイル(.mp4, .webm, .avi, .mov) - 1フレーム目、opencv-python必要 + +**ファイルマッチング:** +- LoRAファイル名で始まるファイル(大文字小文字無視) +- 例: `style_anime.safetensors` にマッチ: + - `style_anime.png` ✅ + - `style_anime_preview.jpg` ✅ + - `STYLE_ANIME.PNG` ✅ + +--- + +## Filtered Random LoRA Loader + +**キーワードフィルタリング付き単一フォルダ対応。** + +![Filtered Random LoRA Loader ワークフロー](./images/filterd_final_single.webp) + +### 主な機能 + +- **キーワードフィルタリング**: スペース区切りキーワードでLoRAを絞り込み +- **AND/ORモード**: 柔軟なフィルタリングロジック +- **フレーズマッチング**: 引用符で完全一致フレーズ +- **メタデータ検索**: ファイル名または埋め込みメタデータから検索 +- **高速キャッシング**: 初回検索後は即座にアクセス +- **直列接続対応**: 複雑なワークフロー向けに複数インスタンスを連結 ### パラメータ -#### 共通設定 -- `token_normalization` - Random LoRA Loaderと同じ -- `weight_interpretation` - Random LoRA Loaderと同じ +| パラメータ | 説明 | デフォルト | +|-----------|------|-----------| +| `lora_folder_path` | LoRAフォルダパス | (空) | +| `keyword_filter` | スペース区切りキーワードまたは引用符フレーズ | (空) | +| `filter_mode` | `AND` / `OR` | `AND` | +| `search_in_metadata` | JSON/埋め込みメタデータから検索 | `false` | +| `num_loras` | 選択するLoRA数 | `1` | +| `model_strength` | MODEL強度(固定または範囲) | `"1.0"` | +| `clip_strength` | CLIP強度(固定または範囲) | `"1.0"` | +| `include_subfolders` | サブフォルダを含める | `true` | +| `unique_by_filename` | 重複ファイル名を除外 | `true` | -#### フォルダ設定 -- `lora_folder_path` - フォルダパス(1つ) -- `include_subfolders` - サブフォルダを含む(デフォルト: True) -- `unique_by_filename` - サブフォルダ間の重複ファイル名を除外(デフォルト: True) +### キーワードフィルタ構文 -#### キーワードフィルタ -- `keyword_filter` - キーワード(カンマ区切り、例: "style, anime") -- `filter_mode` - AND(全キーワード)or OR(いずれか、デフォルト: AND) -- `search_in_metadata` - メタデータ内を検索(デフォルト: False - ファイル名のみ) - -#### LoRA設定 -- `model_strength` - MODEL強度(例: "1.0" or "0.6-0.9") -- `clip_strength` - CLIP強度(例: "1.0" or "0.6-0.9") -- `num_loras` - 選択するLoRA個数(デフォルト: 1、範囲: 0-20) - -#### トリガーワード設定 -- `trigger_word_source` - Random LoRA Loaderと同じ - -#### 追加プロンプト -- `additional_prompt_positive` - 追加ポジティブプロンプト(入力接続 or 直接記述) -- `additional_prompt_negative` - 追加ネガティブプロンプト(入力接続 or 直接記述) - -#### seed -- `seed` - ランダムシード -- `control_after_generate` - seed制御モード - -### キーワードフィルタリング - -#### ファイル名検索(デフォルト - 高速) - -LoRAファイル名のみを検索。 - -**例:** +**基本キーワード(ANDモード):** ``` -フォルダ内容: - - style_anime_v1.safetensors - - style_realistic.safetensors - - character_alice.safetensors - -keyword_filter: "anime" -→ マッチ: style_anime_v1.safetensors ✅ -``` - -#### メタデータ検索(オプション - 低速) - -ファイル名とメタデータの両方を検索: -- `model_name` -- `civitai.name` -- `civitai.trainedWords` -- `civitai.model.name` -- `civitai.model.tags` -- `tags` -- 埋め込みメタデータ - -**パフォーマンスについて:** - -初回読み込み時間(SSD環境、進捗表示あり): -- 100個: 瞬時 -- 1,000個: 約2秒 -- 5,000個: 約10秒 -- 10,000個: 約20秒 -- 20,000個: 約40秒 -- 50,000個: 約100秒(1分40秒) - -メモリ使用量: -- 10,000個: 約150MB -- 20,000個: 約300MB -- 50,000個: 約750MB - -**2回目以降:** 瞬時(キャッシュ利用) - -**推奨:** -- デフォルトのファイル名検索は常に高速 -- メタデータ検索は必要時のみ有効化 -- 大量のLoRA(5,000個以上)の場合、初回は時間がかかります -- HDD環境では初回読み込みがSSDの約3倍の時間がかかります - -**例:** -``` -ファイル: abc123.safetensors -メタデータ: {"trainedWords": ["anime style", "vibrant colors"]} - -search_in_metadata: True -keyword_filter: "anime" -→ マッチ ✅ -``` - -#### ANDモード vs ORモード - -**ANDモード(デフォルト):** -``` -keyword_filter: "style, anime" -filter_mode: "AND" -→ "style" AND "anime" の両方を含むファイルにマッチ +keyword_filter: "anime girl" +→ "anime"と"girl"の両方を含むファイルにマッチ ``` **ORモード:** ``` -keyword_filter: "style, anime" -filter_mode: "OR" -→ "style" OR "anime" のいずれかを含むファイルにマッチ +filter_mode: OR +keyword_filter: "anime realistic" +→ "anime"または"realistic"を含むファイルにマッチ ``` -### 重複ファイル名の処理 - -同じLoRAファイル名が複数のサブフォルダに存在する場合(例: オリジナルとバックアップ)、`unique_by_filename`オプションで同じLoRAが複数回選択されるのを防ぎます。 - -**例:** +**フレーズマッチング:** ``` -フォルダ構成: - /LoRA/ - ├── style/anime_v1.safetensors - └── backup/anime_v1.safetensors - -unique_by_filename: True(デフォルト) - → anime_v1.safetensorsは1つだけ選択される - → 最初に見つかったものを保持(/LoRA/style/anime_v1.safetensors) - -unique_by_filename: False - → 両方のファイルが選択される可能性あり - → 同じLoRAが異なる強度で2回適用される可能性 +keyword_filter: '"anime style" detailed' +→ 完全一致フレーズ"anime style"と単語"detailed"を含む必要 ``` -**重複検出時のログ出力:** +**複数フレーズ:** ``` -[FilteredRandomLoRALoader] Duplicate filename detected: anime_v1.safetensors - Keeping: /LoRA/style/anime_v1.safetensors - Skipping: /LoRA/backup/anime_v1.safetensors +keyword_filter: '"anime style" "detailed eyes" red' +→ "anime style"と"detailed eyes"と"red"を含む必要 ``` -**推奨:** 意図しない重複適用を避けるため、デフォルトの`True`を維持してください。 - -### メタデータ検索のパフォーマンス +### メタデータ検索 **ファイル名検索(デフォルト):** -- `search_in_metadata: False`(デフォルト) -- 10,000個以上のLoRAでも高速 -- 処理時間: <100ms +``` +search_in_metadata: false +→ 高速、ファイル名のみ検索 +``` **メタデータ検索:** -- `search_in_metadata: True` -- ファイル名とLoRAメタデータの両方を検索 -- **初回実行:** キャッシュ構築 - - 1,000個のLoRA: 約2秒 - - 10,000個のLoRA: 約7-75秒(メタデータファイルの有無による) - - コンソールに進捗表示 -- **2回目以降:** 超高速(<100ms) -- キャッシュはComfyUI再起動までl持続 +``` +search_in_metadata: true +→ 低速、JSON/埋め込みメタデータを検索 +→ 初回検索後はキャッシュ(2回目以降は即座) +``` -**パフォーマンスのヒント:** -- 初回に`include_subfolders: True`で実行すると全LoRAをキャッシュ構築 -- キャッシュ構築後は`include_subfolders: False`でもキャッシュ利用可能 -- キャッシュは全ノードインスタンスで共有 +**パフォーマンス:** +- 初回読み込み(SSD): + - 1,000ファイル: 約2秒 + - 5,000ファイル: 約10秒 + - 10,000ファイル: 約20秒 +- 2回目以降: 即座(<100ms) +- メモリ: 10,000ファイルあたり約150MB ### 使用例 -#### 基本的な使い方 +#### 例1: キャラクター選択 ``` -[Filtered Random LoRA Loader] - lora_folder_path: /loras/all/ - keyword_filter: "anime" - num_loras: 1 - ↓ -ファイル名に "anime" を含むLoRAから1個ランダム選択 +lora_folder_path: "/path/to/all_loras" +keyword_filter: "character girl" +filter_mode: AND +num_loras: 1 ``` -#### 複数インスタンスの直列接続 - -![FilteredRandomLoRALoder x2ワークフロー](./images/filterd_final_double.webp) - -``` -[Filtered Random LoRA Loader A] - folder: /loras/all/ - keyword_filter: "style, anime" - filter_mode: "AND" - num_loras: 1 - ↓ -[Filtered Random LoRA Loader B] - folder: /loras/all/ - keyword_filter: "character" - num_loras: 1 - additional_prompt_positive ← Aのpositive_text - additional_prompt_negative ← Aのnegative_text - ↓ -[Filtered Random LoRA Loader C] - folder: /loras/all/ - keyword_filter: "concept" - num_loras: 1 - additional_prompt_positive ← Bのpositive_text - additional_prompt_negative ← Bのnegative_text -``` - -#### Wildcard Encodeとの連携 - -![WildcardEncode + FilteredRandomLoRALoder ワークフロー](./images/filterd_final_wce.webp) - -``` -[Wildcard Encode] - populated_text: "1girl, beautiful" - negative_text: "bad quality" - ↓ -[Filtered Random LoRA Loader] - additional_prompt_positive ← populated_text - additional_prompt_negative ← negative_text - keyword_filter: "style" - ↓ -結合された出力 ✅ -``` - -### ベストプラクティス - -1. **説明的なファイル名を使用:** `style_anime_v1.safetensors` -2. **デフォルトモード(高速):** 通常は`search_in_metadata: False`のまま使用 -3. **メタデータ検索を有効化するタイミング:** - - ファイル名に有用なキーワードが含まれていない - - トリガーワードやタグで検索したい -4. **初回実行の最適化:** - - `include_subfolders: True`で1回実行して完全なキャッシュ構築 - - その後`False`に切り替えてフォルダスキャンを高速化 -5. **直列接続:** - - `positive_text` → 次のノードの`additional_prompt_positive`に接続 - - `negative_text` → 次のノードの`additional_prompt_negative`に接続 - -### トラブルシューティング - -**フィルタに一致するLoRAが見つからない:** -- キーワードがファイル名に含まれているか確認 -- `search_in_metadata: True`でメタデータ内も検索 -- ANDモードの代わりにORモードを試す -- フォルダパスが正しいか確認 - -**初回実行が遅い:** -- `search_in_metadata: True`の場合は正常 -- キャッシュ構築中、2回目以降は高速 -- コンソールに進捗表示 - -**キャッシュが機能しない:** -- ComfyUI再起動でキャッシュクリア -- 起動後1回実行してキャッシュ再構築 +**結果:** "character"と"girl"の両方を含むキャラクターLoRAを1つ選択 --- -## Tips & Tricks(オリジナルノード) +#### 例2: 複数インスタンス(推奨) -### LoRA構文フィルタとしての使用 +![Filtered Random LoRA Loader x2 ワークフロー](./images/filterd_final_double.webp) -LoRAを適用せず、単純にLoRA構文削除フィルタとしてこのノードを使用できます。 +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader #1] + keyword_filter: "style anime" + num_loras: 1 + ↓ +[Filtered Random LoRA Loader #2] + keyword_filter: "character" + num_loras: 1 + ↓ +[KSampler] +``` -**設定:** -- 全グループの`num_loras`を`0`に設定(または全フォルダパスを空にする) -- Wildcard Encodeの`populated_text`を`additional_prompt`に接続 -- `positive_text`出力を次のノードに接続 +**メリット:** +- カテゴリごとに異なるフィルタリング +- 独立した強度設定 +- より柔軟性が高い -**使用例:** -- Wildcard Encode出力からLoRA構文を削除 -- 他のノードに渡す前にプロンプトをクリーンアップ -- Wildcard EncodeのLoRA処理を使いつつ、互換性のために構文を削除 +--- + +#### 例3: メタデータ検索 + +``` +search_in_metadata: true +keyword_filter: "detailed eyes" +``` + +**検索対象:** +- ファイル名 +- JSON `activation text` +- JSON `ss_tag_frequency` +- JSON `modelspec.description` +- 埋め込みLoRAメタデータ + +--- + +## Filtered Random LoRA Loader (LBW) 🆕 v1.2.0新機能 + +**LoRAブロックウェイト対応の高度なノード。** + +![FilteredRandomLoRALoader(LBW)ワークフロー例](./images/lbw_final_single.webp) + +### LBWとは? + +LoRAブロックウェイトにより、U-Netのどの部分がLoRAの影響を受けるかを制御できます: + +``` +INPUTブロック → 構造、構図、レイアウト + ↓ +MIDDLEブロック → 全体的な特徴 + ↓ +OUTPUTブロック → 画風、ディテール、微調整 +``` + +### 主な機能 + +- **4つのプリセットモード**: + - **Style Focused**: OUTPUTブロックのみ(構造を変えずに画風のみ) + - **Character Focused**: バランスの取れたIN+MID+OUT(キャラクター特徴) + - **Structure/Composition Only**: INPUT+MIDブロック(画風を変えずにレイアウトのみ) + - **Balanced / Soft**: 穏やかな適用(自然な結果) +- **Preset: Random**: 4つのプリセットから毎回ランダム選択 +- **Direct Input**: カスタムウェイト指定 +- **SD1.5/SDXL自動検出**: 手動でモデルタイプを選択不要 +- **ウェイト数自動調整**: 要素数の不一致を自動処理 + +### パラメータ + +Filtered Random LoRA Loaderと同じ、加えて: + +| パラメータ | 説明 | デフォルト | +|-----------|------|-----------| +| `weight_mode` | LBWプリセットまたはDirect Input | `Normal (All 1.0)` | +| `lbw_input` | カスタムウェイト(Direct Input用) | (空) | + +### ウェイトモード + +#### Normal (All 1.0) +ブロックウェイトなしの標準LoRA適用。 + +#### Style Focused +``` +SDXL: 1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1 +SD1.5: 1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1 +``` +- OUTPUTブロックのみ +- 構造を変えずに画風のみ +- 用途: 画風LoRA、エフェクトLoRA + +#### Character Focused +``` +SDXL: 1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1 +SD1.5: 1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1 +``` +- バランスの取れたIN+MID+OUT +- キャラクター特徴を保持 +- 用途: キャラクターLoRA、人物LoRA + +#### Structure/Composition Only +``` +SDXL: 1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0 +SD1.5: 1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0 +``` +- INPUT+MIDブロックのみ +- 画風を変えずに構造のみ +- 用途: ポーズLoRA、構図LoRA + +#### Balanced / Soft +``` +SDXL: 1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0 +SD1.5: 1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0 +``` +- 穏やかな適用 +- 自然な結果 +- 用途: 汎用LoRA、マルチLoRAワークフロー + +#### Preset: Random +上記4つのプリセットから毎回ランダムに1つ選択。 + +#### Direct Input +``` +weight_mode: Direct Input +lbw_input: "1,0.5,0.5,0,0,0,0,0,0,0,1,0.8,0.8,0.8,0.8,0.8,0.5,0.5,0.5,0.5" +``` +- カスタムウェイト指定 +- SDXL: 20要素、SD1.5: 17要素 +- 要素数が合わない場合は自動調整 + +### 使用例 + +#### 例1: 画風のみ + +``` +[Filtered Random LoRA Loader (LBW)] + keyword_filter: "anime watercolor" + weight_mode: Style Focused + ↓ +[KSampler] +``` + +**結果:** 構図を変えずにアニメ水彩画風を適用 + +--- + +#### 例2: 構造 → 画風 + +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader (LBW)] + keyword_filter: "pose" + weight_mode: Structure/Composition Only + ↓ +[Filtered Random LoRA Loader (LBW)] + keyword_filter: "oil painting" + weight_mode: Style Focused + ↓ +[KSampler] +``` + +**結果:** ポーズ調整 → 油絵風適用 + +--- + +#### 例3: カスタムウェイト + +``` +weight_mode: Direct Input +lbw_input: "1,0.5,0.5,0.5,0,0,0,0,0,0,1,1,1,1,1,1,0.5,0.5,0.5,0.5" +``` + +**結果:** カスタムブロックウェイト分布 + +--- + +### 上級者向け: カスタムプリセット + +ソースファイルで直接プリセットを編集できます: + +**ファイル:** `filtered_random_lora_loader_lbw.py`(29~41行目) + +詳細なカスタマイズガイドは [README_LBW_ja.md](README_LBW_ja.md) を参照してください。 + +--- + +## Tips & Tricks + +### 複数効果の直列接続 + +ノードを直列接続して異なるLoRAカテゴリを適用: + +``` +[Load Checkpoint] + ↓ +[Filtered Random LoRA Loader (LBW)] (Structure/Composition Only) + keyword_filter: "pose" + ↓ +[Filtered Random LoRA Loader (LBW)] (Style Focused) + keyword_filter: "watercolor" + ↓ +[KSampler] +``` + +**メリット:** +- カテゴリごとに異なる設定 +- 累積的なLoRA効果 +- 最終結果のより細かい制御 + +--- + +### バリエーションのための強度ランダム化 + +固定値の代わりに範囲を使用: + +``` +model_strength_1: "0.6-0.9" +clip_strength_1: "0.6-0.9" +``` + +**効果:** +- 各生成で異なる強度を使用 +- 結果にバリエーション +- 最適な強度を見つけやすい + +--- + +### キーワードフィルタリングのベストプラクティス + +**具体的なキーワードを使用:** +``` +✅ 良い: "anime girl" +❌ 広すぎる: "anime" +``` + +**メタデータ検索と組み合わせ:** +``` +search_in_metadata: true +keyword_filter: "detailed eyes" +``` + +**精密性のためフレーズを使用:** +``` +keyword_filter: '"anime style" detailed' +→ 完全一致フレーズ"anime style"と単語"detailed"を含む必要 +``` + +--- + +### Wildcard Encode + Random LoRA Loader + +**究極の動的ワークフロー:** + +``` +[Wildcard Encode (Inspire)] + text: "__style__, __pose__, {red|blue|green}" + ↓ + populated_text ──→ [Random LoRA Loader] + additional_prompt_positive + ↓ +[KSampler] +``` + +**結果:** +- プロンプトのワイルドカード展開 +- バリエーションのためのランダムLoRA選択 +- LoRA構文なしのクリーンなプロンプト +- 最大の柔軟性 + +--- + +### 整理のためのプレビュー画像 + +プレビュー表示を有効化して: +- どのLoRAが選択されたか確認 +- 生成前にLoRAの外観を検証 +- LoRAコレクションを視覚的に整理 + +**推奨ワークフロー:** +``` +[Random LoRA Loader] + preview ──→ [Preview Image](検証用) + MODEL ──→ [KSampler] +``` + +--- + +## トラブルシューティング + +### LoRAが見つからない + +**確認事項:** +1. LoRAフォルダパスが正しい(絶対パス) +2. フォルダに`.safetensors`ファイルが存在 +3. LoRAがサブフォルダにある場合は`include_subfolders`設定 +4. コンソールでエラーメッセージを確認 + +**有効なパスの例:** +``` +Windows: C:/ComfyUI/models/loras/style +Linux/Mac: /home/user/ComfyUI/models/loras/style +``` + +--- + +### トリガーワードが見つからない + +**確認事項:** +1. メタデータファイル(`.metadata.json`または`.info`)が存在 +2. JSONファイルに必要なフィールドが含まれている: + - `activation text` + - `ss_tag_frequency` + - `sample_prompts` + - `civitai.trainedWords`または`civitai.images` +3. 異なる`trigger_word_source`設定を試す + +**JSON形式を確認:** +```json +{ + "activation text": "anime style, detailed", + "ss_tag_frequency": {...}, + "modelspec.description": "...", + "sample_prompts": "..." +} +``` + +--- + +### LoRAが適用されない + +**確認事項:** +1. `num_loras`が0でない +2. 強度値が全て0でない +3. MODEL/CLIP出力が正しく接続されている +4. コンソールでLoRA適用メッセージを確認 +5. LBWノードの場合: weight_modeが"Normal (All 1.0)"で全ウェイトが0でない + +--- + +### プレビュー画像が表示されない + +**確認事項:** +1. プレビューファイルがLoRAファイル名とマッチ +2. 動画ファイルの場合: opencv-pythonがインストール済み(`pip install opencv-python`) +3. 対応形式: .png, .jpg, .jpeg, .gif, .webp, .mp4, .webm, .avi, .mov +4. ファイルパーミッションが正しい + +**opencv-pythonなし:** +- 静止画は動作 ✅ +- アニメ画像は動作 ✅ +- 動画ファイルは黒画面 ⚠️ + +--- + +### マイナス値で強度が動作しない(v1.2.0で修正) + +**以前の問題(v1.1.0):** +``` +入力: "-0.5" +結果: 1.0 ❌(デフォルトに変換) +``` + +**v1.2.0で修正:** +``` +入力: "-0.5" +結果: -0.5 ✅(マイナスLoRAが正しく適用) + +入力: "-0.8--0.3" +結果: [-0.8, -0.7, -0.6, -0.5, -0.4, -0.3] からランダム ✅ +``` + +--- + +### Wildcard Encodeの問題 + +**ワイルドカードテキストが出力に表示される場合:** + +1. **接続を確認**: Wildcard Encodeの`populated_text`を`additional_prompt_positive`に接続 +2. **間違った出力**: `text`(入力)をこのノードに接続しない +3. **LoRA構文が表示される**: このノードが自動的に除去 **ワークフロー例:** ``` [Wildcard Encode (Inspire)] - text: "__style__, {red|blue|green}, " - ↓ - populated_text: "anime style, blue, " - (LoRAはWildcard EncodeによりMODELに適用済み) - ↓ -[Random LoRA Loader] - num_loras: 0, 0, 0 ← LoRA適用なし - additional_prompt ← populated_textから接続 - ↓ - positive_text: "anime style, blue," ← LoRA構文削除済み ✅ - ↓ -[次のノード] + populated_text ──→ [Random LoRA Loader] + ("text"ではない) additional_prompt_positive ``` -**メリット:** -- ✅ ``構文がノイズトークンになるのを防止 -- ✅ 別途テキスト処理ノードが不要 -- ✅ LoRA構文を含むあらゆるテキストに対応 +--- -## 免責事項・サポートポリシー +### 重複LoRAが選択される -### 本ノードについて +**解決策:** `unique_by_filename`を有効化 -このカスタムノードは個人的な使用目的で開発したものを公開しています。 +``` +unique_by_filename_1: true +``` -### サポートについて +異なるサブフォルダから同じLoRAを選択するのを防ぎます: +``` +/style/lora.safetensors +/backup/lora.safetensors +→ 1つだけ選択 ✅ +``` -- ❌ **技術サポートは提供しません** - - 使い方の質問 - - 環境構築の支援 - - トラブルシューティングの個別対応 +--- -- ❌ **動作保証はありません** - - 特定環境での動作確認 - - 将来のComfyUI更新への対応保証 - - データ損失等の責任 +## 詳細ドキュメント -### コミュニティ貢献について +- **[README_LBW_ja.md](README_LBW_ja.md)** - LBW完全ガイド(日本語) +- **[README_LBW.md](README_LBW.md)** - LBW complete documentation (English) +- **[README.md](README.md)** - English main documentation -以下は歓迎しますが、対応は保証しません: +--- -- ✅ バグ報告(Issue) -- ✅ プルリクエスト -- ✅ 機能提案 +## 免責事項とサポートポリシー -### 使用条件 +### 免責事項 -- 使用は完全に自己責任です -- 本番環境での使用前に十分なテストを行ってください -- 問題が発生しても作者は一切の責任を負いません +- このノードは**技術サポートなし**で提供されます +- 機能の保証はありません +- 将来のComfyUIアップデートとの互換性は保証されません +- バグレポートや機能リクエストに対応しない場合があります +- 自己責任で使用してください + +### サポート状況 + +- ❌ issueやメールでの個別サポートなし +- ❌ バグ修正や機能追加の保証なし +- ✅ コードはオープンソース - 自由にフォーク・修正可能 +- ✅ コミュニティディスカッション歓迎(返答の約束なし) + +### 問題の報告 + +サポートは保証されませんが、以下が可能です: +1. リポジトリの既存issueを確認 +2. このREADMEとトラブルシューティングセクションを確認 +3. issueを開く(対応されない場合があります) +4. 自分でフォークして修正 + +--- ## ライセンス MIT License -Copyright (c) 2025 +--- -本ソフトウェアは「現状のまま」提供され、明示的または黙示的な保証を一切行いません。 +## 更新履歴 -## 参考リンク +### v1.2.0(2026-01-13) + +#### 追加 +- ✅ **新ノード**: Filtered Random LoRA Loader (LBW) +- ✅ 4つのプリセット付きLoRAブロックウェイト(LBW)対応 +- ✅ LBW用SD1.5/SDXL自動検出 +- ✅ LBW用Preset: Randomモード +- ✅ カスタムLBWウェイト用Direct Inputモード +- ✅ LBW用ウェイト数自動調整 +- ✅ **強度のマイナス値対応**: `-0.5`、`-0.8--0.3`が正しく動作 +- ✅ **動画プレビュー対応**: .mp4, .webm, .avi, .mov(opencv-python必要) +- ✅ **強度精度の改善**: + - 範囲指定: 全ノードで0.1刻み + - 固定値: 小数点2桁に丸め + +#### 修正 +- ✅ **重大バグ**: マイナス単一値(`"-0.5"`)が正しく動作(以前は1.0に変換) +- ✅ **重大バグ**: マイナス範囲(`"-0.8--0.3"`)が正しく動作(以前はエラー) +- ✅ 正規表現パターンマッチングで強度パースを改善 + +#### 変更 +- ✅ 全3ノードで強度動作を統一 +- ✅ ノード名変更: "Filtered Random LoRA Loader (Advanced)" → "Filtered Random LoRA Loader (LBW)" +- ✅ モデル互換性警告を強調したドキュメント強化 + +### v1.1.0(2026-01-04) + +#### 追加 +- ✅ **新ノード**: Filtered Random LoRA Loader +- ✅ AND/ORモード付きキーワードフィルタリング +- ✅ キャッシング付きメタデータ検索 +- ✅ プレビュー画像出力(IMAGEタイプ) +- ✅ 重複ファイル名処理(`unique_by_filename`) +- ✅ アニメ画像対応(.gif, .webp) + +#### 変更 +- ✅ **破壊的変更**: `additional_prompt`を`additional_prompt_positive`と`additional_prompt_negative`に分割 +- ✅ 強度精度を小数点1桁に統一 +- ✅ LoRA構文除去を改善 + +#### 修正 +- ✅ `json_sample_prompt`モードでnegative_text出力を修正 +- ✅ メタデータトリガーワードにLoRA構文が表示される問題を修正 +- ✅ INPUT_TYPESの重複パラメータを修正 + +### v1.0.0(2025-12-30) + +#### 追加 +- ✅ 初回リリース +- ✅ 3グループ対応のRandom LoRA Loader +- ✅ マルチソースメタデータ読み取り +- ✅ 範囲指定付き強度ランダム化 +- ✅ トリガーワード抽出 +- ✅ Wildcard Encode互換性 +- ✅ クリーンプロンプト付きCONDITIONING出力 + +--- + +## 参考文献 - [ComfyUI](https://github.com/comfyanonymous/ComfyUI) -- [ComfyUI Lora Manager](https://github.com/willmiao/ComfyUI-Lora-Manager) - `.metadata.json`を生成(推奨) -- [Civitai Helper](https://github.com/butaixianran/Stable-Diffusion-Webui-Civitai-Helper) - `.info`を生成(対応済み) +- [Wildcard Encode (Inspire)](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) +- [LoRA Block Weight理論](https://github.com/hako-mikan/sd-webui-lora-block-weight) + +--- + +**精密な制御で柔軟なLoRAランダム化をお楽しみください!🎲✨** diff --git a/__init__.py b/__init__.py index 1e1a3e5..44563d1 100644 --- a/__init__.py +++ b/__init__.py @@ -6,14 +6,27 @@ RandomLoRALoader Custom Node for ComfyUI from .random_lora_loader import RandomLoRALoader from .filtered_random_lora_loader import FilteredRandomLoRALoader +# LBW版は任意(読み込めない場合はスキップ) +try: + from .filtered_random_lora_loader_lbw import FilteredRandomLoRALoaderLBW + HAS_LBW = True +except Exception as e: + print(f"[RandomLoRALoader] Failed to load LBW version: {e}") + HAS_LBW = False + NODE_CLASS_MAPPINGS = { "RandomLoRALoader": RandomLoRALoader, - "FilteredRandomLoRALoader": FilteredRandomLoRALoader + "FilteredRandomLoRALoader": FilteredRandomLoRALoader, } NODE_DISPLAY_NAME_MAPPINGS = { "RandomLoRALoader": "Random LoRA Loader", - "FilteredRandomLoRALoader": "Filtered Random LoRA Loader" + "FilteredRandomLoRALoader": "Filtered Random LoRA Loader", } +# LBW版が利用可能な場合のみ追加 +if HAS_LBW: + NODE_CLASS_MAPPINGS["FilteredRandomLoRALoaderLBW"] = FilteredRandomLoRALoaderLBW + NODE_DISPLAY_NAME_MAPPINGS["FilteredRandomLoRALoaderLBW"] = "Filtered Random LoRA Loader (LBW)" + __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/filtered_random_lora_loader.py b/filtered_random_lora_loader.py index c715740..e85801d 100644 --- a/filtered_random_lora_loader.py +++ b/filtered_random_lora_loader.py @@ -27,6 +27,7 @@ class FilteredRandomLoRALoader: # クラス変数(全インスタンスで共有するメタデータキャッシュ) _metadata_cache = {} + _opencv_warning_shown = False # opencv警告表示フラグ @classmethod def INPUT_TYPES(cls): @@ -662,18 +663,20 @@ class FilteredRandomLoRALoader: プレビュー画像をTensorとして読み込み(部分一致、長辺1240px) 検索方法: - LoRAファイル名(拡張子除く)で始まる画像ファイルを検索 + LoRAファイル名(拡張子除く)で始まるファイルを検索 + 優先順位: + 1. 静止画像 (.png, .jpg, .jpeg, .webp) + 2. 動画ファイル (.gif, .webp, .mp4, .webm) の1フレーム目 + 例: style_anime_v1.safetensors → style_anime_v1.png - → style_anime_v1_preview.png - → style_anime_v1_001.jpg + → style_anime_v1_preview.jpg + → style_anime_v1.gif (1フレーム目) + → style_anime_v1.mp4 (1フレーム目、opencv-pythonが必要) → STYLE_ANIME_V1.PNG (大文字小文字無視) リサイズ: 長辺を1240pxに統一(アスペクト比保持) - 例: 1024x768 → 1240x930 - 512x512 → 1240x1240 - 800x1200 → 826x1240 Returns: torch.Tensor: (H, W, C) 形式、見つからない場合はNone @@ -690,70 +693,207 @@ class FilteredRandomLoRALoader: base_name = os.path.splitext(os.path.basename(lora_path))[0] folder = os.path.dirname(lora_path) - # 画像拡張子 - image_extensions = ('.png', '.jpg', '.jpeg', '.webp', '.gif') + # ファイル拡張子(優先順位順) + static_image_exts = ('.png', '.jpg', '.jpeg') + animated_image_exts = ('.gif', '.webp') # Pillowで対応可能 + video_exts = ('.mp4', '.webm', '.avi', '.mov') # opencv-python必要 try: # 同じフォルダ内のファイルを検索 - for file in os.listdir(folder): - # 部分一致(大文字小文字無視) + files = os.listdir(folder) + + # 部分一致するファイルを収集 + matched_files = [] + for file in files: if file.lower().startswith(base_name.lower()): - # 画像ファイルか確認 - if file.lower().endswith(image_extensions): - preview_path = os.path.join(folder, file) - try: - img = Image.open(preview_path).convert('RGB') - - # 長辺を1240pxにリサイズ(アスペクト比保持) - width, height = img.size - max_size = 1240 - - if max(width, height) > max_size: - # 長辺が1240pxを超える場合はリサイズ - if width > height: - new_width = max_size - new_height = int(height * (max_size / width)) - else: - new_height = max_size - new_width = int(width * (max_size / height)) - - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - elif max(width, height) < max_size: - # 長辺が1240px未満の場合は拡大 - if width > height: - new_width = max_size - new_height = int(height * (max_size / width)) - else: - new_height = max_size - new_width = int(width * (max_size / height)) - - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # numpy配列に変換 (H, W, C) - img_array = np.array(img).astype(np.float32) / 255.0 - # Tensor化 - return torch.from_numpy(img_array) - except Exception as e: - print(f"[FilteredRandomLoRALoader] Preview load error ({preview_path}): {e}") - continue + matched_files.append(file) + + if not matched_files: + return None + + # 優先順位でソート + def get_priority(filename): + lower = filename.lower() + if any(lower.endswith(ext) for ext in static_image_exts): + return 0 # 最優先 + elif any(lower.endswith(ext) for ext in animated_image_exts): + return 1 # 次優先 + elif any(lower.endswith(ext) for ext in video_exts): + return 2 # 最後 + else: + return 999 # その他 + + matched_files.sort(key=get_priority) + + # 各ファイルを試す + for file in matched_files: + preview_path = os.path.join(folder, file) + lower_file = file.lower() + + # 静止画像 + if any(lower_file.endswith(ext) for ext in static_image_exts): + img = self._load_static_image(preview_path) + if img is not None: + return img + + # アニメーション画像(GIF/WebP) + elif any(lower_file.endswith(ext) for ext in animated_image_exts): + img = self._load_animated_image_first_frame(preview_path) + if img is not None: + return img + + # 動画ファイル(opencv-python必要) + elif any(lower_file.endswith(ext) for ext in video_exts): + img = self._load_video_first_frame(preview_path) + if img is not None: + return img + except Exception as e: print(f"[FilteredRandomLoRALoader] Folder read error: {e}") return None + def _load_static_image(self, image_path): + """静止画像を読み込み""" + try: + from PIL import Image + import numpy as np + import torch + + img = Image.open(image_path).convert('RGB') + return self._resize_and_convert_image(img) + except Exception as e: + print(f"[FilteredRandomLoRALoader] Static image load error ({image_path}): {e}") + return None + + def _load_animated_image_first_frame(self, image_path): + """アニメーション画像(GIF/WebP)の1フレーム目を読み込み""" + try: + from PIL import Image + import numpy as np + import torch + + img = Image.open(image_path) + + # 1フレーム目に移動 + if hasattr(img, 'seek'): + img.seek(0) + + img = img.convert('RGB') + return self._resize_and_convert_image(img) + except Exception as e: + print(f"[FilteredRandomLoRALoader] Animated image load error ({image_path}): {e}") + return None + + def _load_video_first_frame(self, video_path): + """動画ファイルの1フレーム目を読み込み(opencv-python必要)""" + try: + import cv2 + from PIL import Image + import numpy as np + import torch + + # OpenCVで動画を開く + cap = cv2.VideoCapture(video_path) + + if not cap.isOpened(): + return None + + # 1フレーム目を読み込み + ret, frame = cap.read() + cap.release() + + if not ret: + return None + + # BGR → RGB 変換 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # PIL Imageに変換 + img = Image.fromarray(frame_rgb) + + return self._resize_and_convert_image(img) + + except ImportError: + # opencv-pythonがインストールされていない(初回のみ警告) + if not FilteredRandomLoRALoader._opencv_warning_shown: + print("=" * 60) + print("[FilteredRandomLoRALoader] Video Preview Support") + print("=" * 60) + print(f"Video file detected: {os.path.basename(video_path)}") + print("opencv-python is not installed.") + print("") + print("To enable video preview support, install:") + print(" pip install opencv-python") + print("") + print("Static images (.png/.jpg) and animated images (.gif/.webp)") + print("will continue to work without opencv-python.") + print("=" * 60) + FilteredRandomLoRALoader._opencv_warning_shown = True + return None + except Exception as e: + print(f"[FilteredRandomLoRALoader] Video load error ({video_path}): {e}") + return None + + def _resize_and_convert_image(self, img): + """画像をリサイズしてTensorに変換""" + try: + from PIL import Image + import numpy as np + import torch + + # 長辺を1240pxにリサイズ(アスペクト比保持) + width, height = img.size + max_size = 1240 + + if max(width, height) > max_size: + # 長辺が1240pxを超える場合はリサイズ + if width > height: + new_width = max_size + new_height = int(height * (max_size / width)) + else: + new_height = max_size + new_width = int(width * (max_size / height)) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + elif max(width, height) < max_size: + # 長辺が1240px未満の場合は拡大 + if width > height: + new_width = max_size + new_height = int(height * (max_size / width)) + else: + new_height = max_size + new_width = int(width * (max_size / height)) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # numpy配列に変換 (H, W, C) + img_array = np.array(img).astype(np.float32) / 255.0 + # Tensor化 + return torch.from_numpy(img_array) + + except Exception as e: + print(f"[FilteredRandomLoRALoader] Image resize/convert error: {e}") + return None + def _get_random_strength(self, strength_str): - """強度をランダム取得(範囲指定対応)""" + """強度をランダム取得(範囲指定対応、マイナス値・マイナス範囲対応)""" + import re strength_str = strength_str.strip() - if '-' in strength_str: + # 範囲指定のパターンマッチ + range_pattern = r'^(-?\d+\.?\d*)\s*-\s*(-?\d+\.?\d*)$' + match = re.match(range_pattern, strength_str) + + if match: try: - parts = strength_str.split('-') - min_val = float(parts[0].strip()) - max_val = float(parts[1].strip()) + min_val = float(match.group(1)) + max_val = float(match.group(2)) return str(round(random.uniform(min_val, max_val), 1)) except: return "1.0" else: + # 単一値(マイナス含む) try: float(strength_str) return strength_str diff --git a/filtered_random_lora_loader_lbw.py b/filtered_random_lora_loader_lbw.py new file mode 100644 index 0000000..21e15ba --- /dev/null +++ b/filtered_random_lora_loader_lbw.py @@ -0,0 +1,1292 @@ +""" +Filtered Random LoRA Loader (Advanced) Node for ComfyUI +キーワードフィルタ + LoRA Block Weight 対応ランダムLoRA選択・適用ノード + +Author: Your Name +Version: 1.2.0 +License: MIT + +機能: +- 1つのフォルダから複数のLoRAをランダム選択 +- キーワードフィルタで絞り込み(AND/OR) +- メタデータ検索(ファイル名 or メタデータ内) +- LoRA Block Weight (LBW) 対応 +- SD1.5 / SDXL 対応 +- プリセット + カスタム設定 +- キャッシュ機能で高速化 +- 直列接続推奨 +""" + +import os +import random +import re +import json +import folder_paths +import comfy.sd + + +# LBW プリセット定義 +SDXL_PRESETS = { + "Style Focused": "1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1", + "Character Focused": "1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1", + "Structure/Composition Only": "1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0", + "Balanced / Soft": "1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0" +} + +SD15_PRESETS = { + "Style Focused": "1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1", + "Character Focused": "1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1", + "Structure/Composition Only": "1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0", + "Balanced / Soft": "1,1,1,1,0,0,0,1,1,1,1,1,0,0,0,0,0" +} + + +class FilteredRandomLoRALoaderLBW: + """キーワードフィルタ付きランダムLoRA選択・適用ノード(1グループ)""" + + # クラス変数(全インスタンスで共有するメタデータキャッシュ) + _metadata_cache = {} + _opencv_warning_shown = False # opencv警告表示フラグ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + + # 共通設定 + "token_normalization": ( + ["none", "mean", "length", "length+mean"], + {"default": "none"} + ), + "weight_interpretation": ( + ["comfy", "A1111", "compel", "comfy++", "down_weight"], + {"default": "comfy"} + ), + + # 追加プロンプト + "additional_prompt_positive": ("STRING", { + "default": "", + "multiline": True, + "placeholder": "Additional positive prompt" + }), + "additional_prompt_negative": ("STRING", { + "default": "", + "multiline": True, + "placeholder": "Additional negative prompt" + }), + + # フォルダ設定(1つのみ) + "lora_folder_path": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Path to LoRA folder" + }), + "include_subfolders": ("BOOLEAN", { + "default": True, + "label": "Include subfolders" + }), + "unique_by_filename": ("BOOLEAN", { + "default": True, + "label": "Unique by filename (exclude duplicates)" + }), + + # キーワードフィルタ設定 + "keyword_filter": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Keywords (space-separated, e.g., 'style anime' or \"anime style\" red)" + }), + "filter_mode": (["AND", "OR"], { + "default": "AND" + }), + "search_in_metadata": ("BOOLEAN", { + "default": False, + "label": "Search in metadata (slower)" + }), + + # LoRA設定 + "model_strength": ("STRING", { + "default": "1.0", + "multiline": False, + "placeholder": "e.g., 1.0 or 0.6-0.9" + }), + "clip_strength": ("STRING", { + "default": "1.0", + "multiline": False, + "placeholder": "e.g., 1.0 or 0.6-0.9" + }), + "num_loras": ("INT", { + "default": 1, + "min": 0, + "max": 20, + "step": 1 + }), + + # LoRA Block Weight (LBW) 設定 + "weight_mode": ([ + "Normal (All 1.0)", + "Style Focused", + "Character Focused", + "Structure/Composition Only", + "Balanced / Soft", + "Preset: Random", + "Direct Input" + ], { + "default": "Normal (All 1.0)" + }), + "lbw_input": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Comma-separated weights (SDXL: 20 elements, SD1.5: 17 elements)" + }), + + # トリガーワード設定 + "trigger_word_source": ( + ["json_combined", "json_random", "json_sample_prompt", "metadata"], + {"default": "json_combined"} + ), + + # seed + "seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xffffffffffffffff + }), + } + } + + RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING", "IMAGE") + RETURN_NAMES = ("MODEL", "CLIP", "positive_text", "negative_text", "positive", "negative", "preview") + FUNCTION = "load_loras" + CATEGORY = "loaders" + + def load_loras(self, model, clip, token_normalization, weight_interpretation, + additional_prompt_positive, additional_prompt_negative, + lora_folder_path, include_subfolders, unique_by_filename, + keyword_filter, filter_mode, search_in_metadata, + model_strength, clip_strength, num_loras, + weight_mode, lbw_input, + trigger_word_source, seed): + """メイン処理""" + + # seedの設定 + random.seed(seed) + + # LBW ウェイトの取得 + lbw_weights = self._get_lbw_weights(weight_mode, lbw_input) + + # 初期値 + final_positive = additional_prompt_positive.strip() + final_negative = additional_prompt_negative.strip() + + # num_loras が 0 の場合はスキップ + if num_loras == 0: + print("[FilteredRandomLoRALoaderLBW] num_loras=0, skipping LoRA application") + empty_preview = self._generate_preview_batch([]) + return self._generate_outputs(model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, empty_preview) + + # フォルダパスが空の場合はスキップ + if not lora_folder_path.strip(): + print("[FilteredRandomLoRALoaderLBW] Warning: lora_folder_path is empty, skipping") + empty_preview = self._generate_preview_batch([]) + return self._generate_outputs(model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, empty_preview) + + # LoRAファイル一覧を取得 + lora_files = self._find_lora_files(lora_folder_path, include_subfolders) + + if not lora_files: + print(f"[FilteredRandomLoRALoaderLBW] Warning: No LoRA files found in {lora_folder_path}") + empty_preview = self._generate_preview_batch([]) + return self._generate_outputs(model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, empty_preview) + + # キーワードフィルタリング + filtered_files = self._filter_lora_files( + lora_files, keyword_filter, filter_mode, search_in_metadata + ) + + if not filtered_files: + print(f"[FilteredRandomLoRALoaderLBW] Warning: No LoRAs found matching filter '{keyword_filter}'") + empty_preview = self._generate_preview_batch([]) + return self._generate_outputs(model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, empty_preview) + + # ファイル名でユニーク化(重複ファイル名を除外) + if unique_by_filename: + filtered_files = self._unique_by_filename(filtered_files) + if not filtered_files: + print(f"[FilteredRandomLoRALoaderLBW] Warning: No LoRAs after deduplication") + empty_preview = self._generate_preview_batch([]) + return self._generate_outputs(model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, empty_preview) + + # ランダム選択(重複なし、不足時は重複で埋める) + available_count = len(filtered_files) + + if num_loras <= available_count: + # 十分な数がある場合は重複なしで選択 + selected_loras = random.sample(filtered_files, num_loras) + else: + # 不足する場合は全て選択後、再選択で埋める + selected_loras = filtered_files.copy() + remaining = num_loras - available_count + + print(f"[FilteredRandomLoRALoaderLBW] Warning: Requested {num_loras} LoRAs but only {available_count} available. Adding {remaining} duplicates.") + + # 不足分をランダムに追加(重複あり) + for _ in range(remaining): + selected_loras.append(random.choice(filtered_files)) + + # 最終的にシャッフル + random.shuffle(selected_loras) + + # LoRA適用 + lora_info_parts = [] + sample_negative_parts = [] # json_sample_prompt用のnegative収集 + preview_images = [] # プレビュー画像収集 + + for lora_path in selected_loras: + # 強度を取得 + model_str = self._get_random_strength(model_strength) + clip_str = self._get_random_strength(clip_strength) + + # LoRA適用(LBW対応) + model, clip = self._apply_lora(model, clip, lora_path, model_str, clip_str, lbw_weights) + + # LoRA情報を記録(LBW対応) + lora_name = os.path.splitext(os.path.basename(lora_path))[0] + if lbw_weights: + # LBW形式: + lora_notation = f"" + else: + # 通常形式: + lora_notation = f"" + + # トリガーワード取得 + if trigger_word_source == "json_sample_prompt": + # 作例プロンプト取得(positive/negative両方) + sample_positive, sample_negative = self._get_sample_prompt_from_json(lora_path) + trigger_words = sample_positive + if sample_negative: + sample_negative_parts.append(sample_negative) + else: + trigger_words = self._get_trigger_words(lora_path, trigger_word_source) + + # 結合 + if trigger_words: + lora_info_parts.append(f"{lora_notation}, {trigger_words}") + else: + lora_info_parts.append(lora_notation) + + # プレビュー画像を読み込み + preview = self._load_preview_image_as_tensor(lora_path) + if preview is not None: + preview_images.append(preview) + + # 最終プロンプト生成 + lora_info = ", ".join(lora_info_parts) + + # positive_text + parts_positive = [] + if final_positive: + parts_positive.append(final_positive) + parts_positive.append(lora_info) + final_positive = ", ".join(parts_positive) + + # negative_text + # 1. additional_prompt_negative + # 2. json_sample_promptからのnegative + if sample_negative_parts: + sample_negative_combined = ", ".join(sample_negative_parts) + if final_negative: + final_negative = f"{final_negative}, {sample_negative_combined}" + else: + final_negative = sample_negative_combined + + # プレビュー画像バッチ生成 + preview_batch = self._generate_preview_batch(preview_images) + + return self._generate_outputs(model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, preview_batch) + + def _find_lora_files(self, folder_path, include_subfolders): + """フォルダ内のLoRAファイルを検索""" + if not os.path.exists(folder_path): + print(f"[FilteredRandomLoRALoaderLBW] Error: Folder not found: {folder_path}") + return [] + + lora_files = [] + extensions = ['.safetensors', '.pt', '.ckpt'] + + if include_subfolders: + for root, dirs, files in os.walk(folder_path): + for file in files: + if any(file.lower().endswith(ext) for ext in extensions): + lora_files.append(os.path.join(root, file)) + else: + for file in os.listdir(folder_path): + file_path = os.path.join(folder_path, file) + if os.path.isfile(file_path) and any(file.lower().endswith(ext) for ext in extensions): + lora_files.append(file_path) + + return lora_files + + def _parse_keywords(self, keyword_filter): + """ + キーワードをパース(スペース区切り、"..."でフレーズ対応) + + 例: + "anime style" → ["anime", "style"] + "anime style" red → ["anime style", "red"] + "anime style" "vibrant colors" → ["anime style", "vibrant colors"] + """ + if not keyword_filter.strip(): + return [] + + import re + + # ダブルクォート内のフレーズを抽出 + pattern = r'"([^"]+)"|(\S+)' + matches = re.findall(pattern, keyword_filter) + + # (quoted, unquoted) のタプルから値を取得 + keywords = [m[0] if m[0] else m[1] for m in matches] + + # 小文字化 + keywords = [kw.lower() for kw in keywords if kw] + + return keywords + + def _unique_by_filename(self, lora_files): + """ + ファイル名でユニーク化(重複ファイル名を除外) + + Args: + lora_files: LoRAファイルパスのリスト + + Returns: + list: ファイル名がユニークなファイルパスのリスト + + 例: + ["/path/style/anime.safetensors", "/path/backup/anime.safetensors"] + → ["/path/style/anime.safetensors"] # 最初に見つかったものを保持 + """ + seen_names = {} + unique_files = [] + + for file_path in lora_files: + filename = os.path.basename(file_path) + + if filename not in seen_names: + seen_names[filename] = file_path + unique_files.append(file_path) + else: + # 重複検出時はログ出力 + print(f"[FilteredRandomLoRALoaderLBW] Duplicate filename detected: {filename}") + print(f" Keeping: {seen_names[filename]}") + print(f" Skipping: {file_path}") + + return unique_files + + def _filter_lora_files(self, lora_files, keyword_filter, filter_mode="OR", search_in_metadata=False): + """キーワードでLoRAファイルをフィルタリング""" + if not keyword_filter.strip(): + return lora_files # フィルタなし = 全ファイル + + # キーワードをパース(スペース区切り、"..."でフレーズ対応) + keywords = self._parse_keywords(keyword_filter) + if not keywords: + return lora_files + + # メタデータ検索が無効な場合(デフォルト) + if not search_in_metadata: + # ファイル名のみ検索(超高速) + filtered = [] + for lora_path in lora_files: + filename = os.path.splitext(os.path.basename(lora_path))[0].lower() + if filter_mode == "AND": + if all(kw in filename for kw in keywords): + filtered.append(lora_path) + else: + if any(kw in filename for kw in keywords): + filtered.append(lora_path) + return filtered + + # メタデータ検索が有効な場合(初回は遅い) + total = len(lora_files) + filtered = [] + + print(f"[FilteredRandomLoRALoaderLBW] Building metadata cache for {total} files...") + + for i, lora_path in enumerate(lora_files): + # 進捗表示(100個ごと) + if i > 0 and i % 100 == 0: + print(f"[FilteredRandomLoRALoaderLBW] Progress: {i}/{total} ({int(i/total*100)}%)") + + filename = os.path.splitext(os.path.basename(lora_path))[0].lower() + search_target = filename + + # メタデータからキーワード取得(キャッシュ利用) + metadata_keywords = self._get_metadata_keywords(lora_path) + if metadata_keywords: + search_target = f"{filename} {metadata_keywords}" + + # フィルタリング + if filter_mode == "AND": + if all(kw in search_target for kw in keywords): + filtered.append(lora_path) + else: + if any(kw in search_target for kw in keywords): + filtered.append(lora_path) + + print(f"[FilteredRandomLoRALoaderLBW] Cache built. Filtered {len(filtered)}/{total} files.") + + return filtered + + def _get_metadata_keywords(self, lora_path): + """ + メタデータからキーワードを取得(キャッシュ付き) + + 検索対象: + 1. model_name(トップレベル) + 2. civitai.name + 3. civitai.trainedWords(全パターンを結合、重複除去) + 4. civitai.model.name + 5. civitai.model.tags + 6. tags(トップレベル) + 7. 埋め込みメタデータ(ss_tag_frequency等) + + Returns: + str: 検索用キーワード文字列(小文字、スペース区切り) + """ + # キャッシュ確認 + if lora_path in self._metadata_cache: + return self._metadata_cache[lora_path] + + # メタデータ読み込み + metadata = self._load_json_metadata(lora_path) + keywords_parts = [] + + if metadata: + # 1. トップレベルのmodel_name + model_name = metadata.get("model_name", "") + if model_name: + keywords_parts.append(model_name) + + # civitaiセクション + civitai = metadata.get("civitai", {}) + + # 2. civitai.name + civitai_name = civitai.get("name", "") + if civitai_name: + keywords_parts.append(civitai_name) + + # 3. civitai.trainedWords + trained_words = civitai.get("trainedWords", []) + if trained_words: + all_words = [] + for pattern in trained_words: + # カンマ区切りで分割 + words = [w.strip() for w in pattern.split(',')] + all_words.extend(words) + # 重複除去(順序維持) + unique_words = list(dict.fromkeys(all_words)) + keywords_parts.extend(unique_words) + + # civitai.modelセクション + model_info = civitai.get("model", {}) + + # 4. civitai.model.name + model_info_name = model_info.get("name", "") + if model_info_name: + keywords_parts.append(model_info_name) + + # 5. civitai.model.tags + model_tags = model_info.get("tags", []) + if model_tags: + keywords_parts.extend(model_tags) + + # 6. トップレベルのtags(通常は5と同じ内容だが念のため) + top_tags = metadata.get("tags", []) + if top_tags: + keywords_parts.extend(top_tags) + + # 重複除去(順序維持) + unique_keywords = list(dict.fromkeys(keywords_parts)) + + # 小文字化してスペース区切りで結合 + keywords = " ".join(unique_keywords).lower() + + # キャッシュに保存 + self._metadata_cache[lora_path] = keywords + + return keywords + + def _load_json_metadata(self, lora_path): + """ + メタデータファイルまたは埋め込みメタデータを読み込み + + 優先順位: + 1. .metadata.json(ComfyUI Lora Manager) + 2. .info(Civitai Helper) + 3. 埋め込みメタデータ + """ + base_path = os.path.splitext(lora_path)[0] + + # 1. .metadata.json + metadata_json_path = f"{base_path}.metadata.json" + if os.path.exists(metadata_json_path): + try: + with open(metadata_json_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Warning: Failed to load {metadata_json_path}: {e}") + + # 2. .info + info_path = f"{base_path}.info" + if os.path.exists(info_path): + try: + with open(info_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Warning: Failed to load {info_path}: {e}") + + # 3. 埋め込みメタデータ + embedded = self._load_embedded_metadata(lora_path) + if embedded: + return embedded + + return None + + def _load_embedded_metadata(self, lora_path): + """LoRAファイルから埋め込みメタデータを読み込み(Civitai形式に変換)""" + try: + from safetensors.torch import safe_open + + with safe_open(lora_path, framework="pt") as f: + metadata = f.metadata() + + if not metadata: + return None + + # Civitai形式に変換 + civitai_format = {"civitai": {}} + + # ss_tag_frequency からトリガーワード抽出 + if "ss_tag_frequency" in metadata: + try: + tag_freq_str = metadata.get("ss_tag_frequency", "{}") + tag_freq = json.loads(tag_freq_str) + + all_tags = [] + for dataset_name, tags_dict in tag_freq.items(): + sorted_tags = sorted(tags_dict.items(), key=lambda x: x[1], reverse=True) + top_tags = [tag for tag, freq in sorted_tags[:20]] + all_tags.extend(top_tags) + + if all_tags: + unique_tags = list(dict.fromkeys(all_tags)) + civitai_format["civitai"]["trainedWords"] = [", ".join(unique_tags)] + except: + pass + + # modelspec.trigger_word + if "modelspec.trigger_word" in metadata: + trigger_word = metadata.get("modelspec.trigger_word", "") + if trigger_word and "trainedWords" not in civitai_format["civitai"]: + civitai_format["civitai"]["trainedWords"] = [trigger_word] + + # ss_output_name + if "ss_output_name" in metadata: + model_name = metadata.get("ss_output_name", "") + if model_name: + civitai_format["model_name"] = model_name + + return civitai_format if civitai_format["civitai"] else None + + except Exception as e: + return None + + def _get_trigger_words(self, lora_path, trigger_word_source): + """トリガーワードを取得""" + if trigger_word_source == "metadata": + # 埋め込みメタデータのみ参照 + return self._get_trigger_words_from_embedded(lora_path) + elif trigger_word_source == "json_sample_prompt": + # 作例プロンプト取得 + sample_positive, sample_negative = self._get_sample_prompt_from_json(lora_path) + return sample_positive + elif trigger_word_source == "json_random": + # ランダム選択 + return self._get_trigger_words_random(lora_path) + else: # json_combined + # 全結合 + return self._get_trigger_words_combined(lora_path) + + def _get_trigger_words_combined(self, lora_path): + """全トリガーワードを結合""" + metadata = self._load_json_metadata(lora_path) + if not metadata: + return "" + + trained_words = metadata.get("civitai", {}).get("trainedWords", []) + if not trained_words: + return "" + + all_words = [] + for pattern in trained_words: + # LoRA構文を削除してから分割 + pattern = self._remove_lora_syntax(pattern) + words = [w.strip() for w in pattern.split(',')] + all_words.extend(words) + + unique_words = list(dict.fromkeys(all_words)) + return ", ".join(unique_words) + + def _get_trigger_words_random(self, lora_path): + """トリガーワードからランダムに1つ選択""" + metadata = self._load_json_metadata(lora_path) + if not metadata: + return "" + + trained_words = metadata.get("civitai", {}).get("trainedWords", []) + if not trained_words: + return "" + + # LoRA構文を削除 + selected = random.choice(trained_words) + return self._remove_lora_syntax(selected) + + def _get_sample_prompt_from_json(self, lora_path): + """作例プロンプトをランダムに取得""" + metadata = self._load_json_metadata(lora_path) + if not metadata: + return "", "" + + images = metadata.get("civitai", {}).get("images", []) + if not images: + return "", "" + + valid_images = [img for img in images if "meta" in img and img["meta"]] + if not valid_images: + return "", "" + + selected_image = random.choice(valid_images) + meta = selected_image["meta"] + + positive = meta.get("prompt", "") + negative = meta.get("negativePrompt", "") + + # LoRA構文削除(両方) + positive = self._remove_lora_syntax(positive) + negative = self._remove_lora_syntax(negative) + + return positive, negative + + def _get_trigger_words_from_embedded(self, lora_path): + """埋め込みメタデータから直接トリガーワード取得""" + embedded = self._load_embedded_metadata(lora_path) + if not embedded: + return "" + + trained_words = embedded.get("civitai", {}).get("trainedWords", []) + if not trained_words: + return "" + + return trained_words[0] if trained_words else "" + + def _remove_lora_syntax(self, text): + """プロンプトからLoRA構文を削除""" + pattern = r']+>' + return re.sub(pattern, '', text) + + def _load_preview_image_as_tensor(self, lora_path): + """ + プレビュー画像をTensorとして読み込み(部分一致、長辺1240px) + + 検索方法: + LoRAファイル名(拡張子除く)で始まるファイルを検索 + 優先順位: + 1. 静止画像 (.png, .jpg, .jpeg, .webp) + 2. 動画ファイル (.gif, .webp, .mp4, .webm) の1フレーム目 + + 例: style_anime_v1.safetensors + → style_anime_v1.png + → style_anime_v1_preview.jpg + → style_anime_v1.gif (1フレーム目) + → style_anime_v1.mp4 (1フレーム目、opencv-pythonが必要) + → STYLE_ANIME_V1.PNG (大文字小文字無視) + + リサイズ: + 長辺を1240pxに統一(アスペクト比保持) + + Returns: + torch.Tensor: (H, W, C) 形式、見つからない場合はNone + """ + try: + from PIL import Image + import numpy as np + import torch + except ImportError: + print("[FilteredRandomLoRALoaderLBW] PIL/torch not available for preview") + return None + + # LoRAファイル名(拡張子なし) + base_name = os.path.splitext(os.path.basename(lora_path))[0] + folder = os.path.dirname(lora_path) + + # ファイル拡張子(優先順位順) + static_image_exts = ('.png', '.jpg', '.jpeg') + animated_image_exts = ('.gif', '.webp') # Pillowで対応可能 + video_exts = ('.mp4', '.webm', '.avi', '.mov') # opencv-python必要 + + try: + # 同じフォルダ内のファイルを検索 + files = os.listdir(folder) + + # 部分一致するファイルを収集 + matched_files = [] + for file in files: + if file.lower().startswith(base_name.lower()): + matched_files.append(file) + + if not matched_files: + return None + + # 優先順位でソート + def get_priority(filename): + lower = filename.lower() + if any(lower.endswith(ext) for ext in static_image_exts): + return 0 # 最優先 + elif any(lower.endswith(ext) for ext in animated_image_exts): + return 1 # 次優先 + elif any(lower.endswith(ext) for ext in video_exts): + return 2 # 最後 + else: + return 999 # その他 + + matched_files.sort(key=get_priority) + + # 各ファイルを試す + for file in matched_files: + preview_path = os.path.join(folder, file) + lower_file = file.lower() + + # 静止画像 + if any(lower_file.endswith(ext) for ext in static_image_exts): + img = self._load_static_image(preview_path) + if img is not None: + return img + + # アニメーション画像(GIF/WebP) + elif any(lower_file.endswith(ext) for ext in animated_image_exts): + img = self._load_animated_image_first_frame(preview_path) + if img is not None: + return img + + # 動画ファイル(opencv-python必要) + elif any(lower_file.endswith(ext) for ext in video_exts): + img = self._load_video_first_frame(preview_path) + if img is not None: + return img + + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Folder read error: {e}") + + return None + + def _load_static_image(self, image_path): + """静止画像を読み込み""" + try: + from PIL import Image + import numpy as np + import torch + + img = Image.open(image_path).convert('RGB') + return self._resize_and_convert_image(img) + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Static image load error ({image_path}): {e}") + return None + + def _load_animated_image_first_frame(self, image_path): + """アニメーション画像(GIF/WebP)の1フレーム目を読み込み""" + try: + from PIL import Image + import numpy as np + import torch + + img = Image.open(image_path) + + # 1フレーム目に移動 + if hasattr(img, 'seek'): + img.seek(0) + + img = img.convert('RGB') + return self._resize_and_convert_image(img) + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Animated image load error ({image_path}): {e}") + return None + + def _load_video_first_frame(self, video_path): + """動画ファイルの1フレーム目を読み込み(opencv-python必要)""" + try: + import cv2 + from PIL import Image + import numpy as np + import torch + + # OpenCVで動画を開く + cap = cv2.VideoCapture(video_path) + + if not cap.isOpened(): + return None + + # 1フレーム目を読み込み + ret, frame = cap.read() + cap.release() + + if not ret: + return None + + # BGR → RGB 変換 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # PIL Imageに変換 + img = Image.fromarray(frame_rgb) + + return self._resize_and_convert_image(img) + + except ImportError: + # opencv-pythonがインストールされていない(初回のみ警告) + if not FilteredRandomLoRALoader._opencv_warning_shown: + print("=" * 60) + print("[FilteredRandomLoRALoaderLBW] Video Preview Support") + print("=" * 60) + print(f"Video file detected: {os.path.basename(video_path)}") + print("opencv-python is not installed.") + print("") + print("To enable video preview support, install:") + print(" pip install opencv-python") + print("") + print("Static images (.png/.jpg) and animated images (.gif/.webp)") + print("will continue to work without opencv-python.") + print("=" * 60) + FilteredRandomLoRALoader._opencv_warning_shown = True + return None + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Video load error ({video_path}): {e}") + return None + + def _resize_and_convert_image(self, img): + """画像をリサイズしてTensorに変換""" + try: + from PIL import Image + import numpy as np + import torch + + # 長辺を1240pxにリサイズ(アスペクト比保持) + width, height = img.size + max_size = 1240 + + if max(width, height) > max_size: + # 長辺が1240pxを超える場合はリサイズ + if width > height: + new_width = max_size + new_height = int(height * (max_size / width)) + else: + new_height = max_size + new_width = int(width * (max_size / height)) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + elif max(width, height) < max_size: + # 長辺が1240px未満の場合は拡大 + if width > height: + new_width = max_size + new_height = int(height * (max_size / width)) + else: + new_height = max_size + new_width = int(width * (max_size / height)) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # numpy配列に変換 (H, W, C) + img_array = np.array(img).astype(np.float32) / 255.0 + # Tensor化 + return torch.from_numpy(img_array) + + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Image resize/convert error: {e}") + return None + + def _get_lbw_weights(self, weight_mode, lbw_input): + """ + LBW ウェイトを取得(型は自動検出されるため、ここではプリセットのみ処理) + + Returns: + str: カンマ区切りのウェイト文字列、または None (Normal mode) + """ + # Normal (All 1.0) の場合は None を返す(LBW構文なし) + if weight_mode == "Normal (All 1.0)": + return None + + # Preset: Random の場合 + if weight_mode == "Preset: Random": + presets = ["Style Focused", "Character Focused", + "Structure/Composition Only", "Balanced / Soft"] + weight_mode = random.choice(presets) + print(f"[FilteredRandomLoRALoaderLBW] Random preset selected: {weight_mode}") + + # Direct Input の場合 + if weight_mode == "Direct Input": + weights = lbw_input.strip() + + # 空欄の場合はNone(Normal All 1.0として扱う) + if not weights: + print(f"[FilteredRandomLoRALoaderLBW] Direct Input is empty, using Normal (All 1.0)") + return None + + # 数値チェックのみ(要素数チェックは _apply_lora で自動調整) + try: + weight_list = [w.strip() for w in weights.split(',')] + for w in weight_list: + float(w) + return weights + except ValueError: + print(f"[FilteredRandomLoRALoaderLBW] Error: Invalid number in LBW input") + print(f"[FilteredRandomLoRALoaderLBW] Using default (All 1.0)") + return None + + # プリセットの場合(SDXLプリセットを使用、_apply_loraで自動調整) + if weight_mode in SDXL_PRESETS: + return SDXL_PRESETS[weight_mode] + else: + print(f"[FilteredRandomLoRALoaderLBW] Unknown weight_mode: {weight_mode}") + return None + + def _get_random_strength(self, strength_str): + """強度をランダム取得(範囲指定は0.1刻み、固定値は小数点2桁)""" + import re + strength_str = strength_str.strip() + + # 範囲指定のパターンマッチ + range_pattern = r'^(-?\d+\.?\d*)\s*-\s*(-?\d+\.?\d*)$' + match = re.match(range_pattern, strength_str) + + if match: + try: + # 範囲の上限下限を1桁に丸める + min_val = round(float(match.group(1)), 1) + max_val = round(float(match.group(2)), 1) + + # 0.1刻みの値のリストを生成 + values = [] + current = min_val + while current <= max_val + 0.01: # 浮動小数点誤差対策 + values.append(round(current, 1)) + current += 0.1 + + if not values: + return "1.0" + + # ランダムに1つ選択 + selected = random.choice(values) + return str(selected) + except: + return "1.0" + else: + # 単一値(小数点2桁に丸める) + try: + value = float(strength_str) + return str(round(value, 2)) + except: + return "1.0" + + def _apply_lora(self, model, clip, lora_path, model_strength, clip_strength, lbw_weights=None): + """LoRAを適用(LBW対応)""" + try: + model_strength_f = float(model_strength) + clip_strength_f = float(clip_strength) + + lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + + # LBWが指定されている場合 + if lbw_weights: + # LoRAファイルからSD1.5/SDXLを自動検出 + detected_type = self._detect_lora_type(lora) + + # ウェイトリストに変換 + weights_list = [float(w.strip()) for w in lbw_weights.split(',')] + + # 要素数チェック(検出された型に基づく) + expected_count = 20 if detected_type == "SDXL" else 17 + if len(weights_list) != expected_count: + print(f"[FilteredRandomLoRALoaderLBW] Warning: LoRA is {detected_type} but got {len(weights_list)} weights (expected {expected_count})") + print(f"[FilteredRandomLoRALoaderLBW] Adjusting weights to match LoRA type...") + + # ウェイトを調整 + if len(weights_list) < expected_count: + # 足りない場合は1.0で埋める + weights_list.extend([1.0] * (expected_count - len(weights_list))) + else: + # 多い場合は切り詰める + weights_list = weights_list[:expected_count] + + # LoRAのキーを解析してブロックウェイトを適用 + lora_with_weights = self._apply_block_weights(lora, weights_list, model_strength_f) + + # MODEL/CLIPに適用 + model_lora, clip_lora = comfy.sd.load_lora_for_models( + model, clip, lora_with_weights, 1.0, clip_strength_f + ) + else: + # 通常適用 + model_lora, clip_lora = comfy.sd.load_lora_for_models( + model, clip, lora, model_strength_f, clip_strength_f + ) + + return model_lora, clip_lora + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Warning: Failed to apply LoRA {lora_path}: {e}") + import traceback + traceback.print_exc() + return model, clip + + def _detect_lora_type(self, lora_dict): + """ + LoRAファイルからSD1.5/SDXLを自動検出 + + Args: + lora_dict: LoRAの辞書 + + Returns: + "SDXL" or "SD1.5" + """ + # input_blocksの最大番号を確認 + max_input_block = -1 + for key in lora_dict.keys(): + if "input_blocks" in key: + import re + match = re.search(r'input_blocks_(\d+)', key) + if match: + block_num = int(match.group(1)) + max_input_block = max(max_input_block, block_num) + + # SD1.5: input_blocks_0~8 (9個) + # SDXL: input_blocks_0~8 (9個) ← 同じ! + + # output_blocksの最大番号を確認 + max_output_block = -1 + for key in lora_dict.keys(): + if "output_blocks" in key: + import re + match = re.search(r'output_blocks_(\d+)', key) + if match: + block_num = int(match.group(1)) + max_output_block = max(max_output_block, block_num) + + # SD1.5: output_blocks_0~11 (12個) + # SDXL: output_blocks_0~8 (9個) + + if max_output_block >= 9: + # output_blocks が 9以上 → SD1.5 + return "SD1.5" + else: + # output_blocks が 8以下 → SDXL + return "SDXL" + + def _apply_block_weights(self, lora_dict, weights_list, base_strength): + """ + LoRAのテンソルにブロックウェイトを適用 + + Args: + lora_dict: LoRAの辞書 + weights_list: ウェイトのリスト(SD1.5: 17, SDXL: 20) + base_strength: ベース強度 + + Returns: + ウェイト適用済みのLoRA辞書 + """ + # ブロック名のマッピング(SD1.5/SDXL共通のパターン) + # キー名の例: + # "lora_unet_input_blocks_0_0.weight" → INブロック + # "lora_unet_middle_block_0.weight" → MIDブロック + # "lora_unet_output_blocks_0_0.weight" → OUTブロック + + weighted_lora = {} + + for key, value in lora_dict.items(): + weight_multiplier = base_strength + + # UNetのLoRAキーのみ処理 + if "lora_unet" in key: + block_index = self._get_block_index(key, len(weights_list)) + if block_index is not None and block_index < len(weights_list): + weight_multiplier = base_strength * weights_list[block_index] + + # ウェイトを適用 + weighted_lora[key] = value * weight_multiplier + + return weighted_lora + + def _get_block_index(self, key, num_weights): + """ + LoRAキー名からブロックインデックスを取得 + + Args: + key: LoRAのキー名 + num_weights: ウェイト数(17 or 20) + + Returns: + ブロックインデックス(0から始まる) + """ + # BASE(常に0番目) + if "time_embed" in key or "label_emb" in key: + return 0 + + # SD1.5の場合(17要素) + if num_weights == 17: + # INPUT blocks (IN01, IN02, IN04, IN05, IN07, IN08) + if "input_blocks" in key: + # input_blocks_X_... の X を抽出 + import re + match = re.search(r'input_blocks_(\d+)', key) + if match: + block_num = int(match.group(1)) + # SD1.5のINブロックマッピング + # IN01=1, IN02=2, IN04=3, IN05=4, IN07=5, IN08=6 + in_mapping = {1: 1, 2: 2, 4: 3, 5: 4, 7: 5, 8: 6} + if block_num in in_mapping: + return in_mapping[block_num] + + # MIDDLE block (M00 = index 7) + elif "middle_block" in key: + return 7 + + # OUTPUT blocks (OUT03-OUT11 = index 8-16) + elif "output_blocks" in key: + import re + match = re.search(r'output_blocks_(\d+)', key) + if match: + block_num = int(match.group(1)) + # OUT03-OUT11 → index 8-16 + if block_num >= 3 and block_num <= 11: + return 8 + (block_num - 3) + + # SDXLの場合(20要素) + elif num_weights == 20: + # INPUT blocks (IN00-IN08 = index 1-9) + if "input_blocks" in key: + import re + match = re.search(r'input_blocks_(\d+)', key) + if match: + block_num = int(match.group(1)) + if block_num <= 8: + return 1 + block_num + + # MIDDLE block (M00 = index 10) + elif "middle_block" in key: + return 10 + + # OUTPUT blocks (OUT00-OUT08 = index 11-19) + elif "output_blocks" in key: + import re + match = re.search(r'output_blocks_(\d+)', key) + if match: + block_num = int(match.group(1)) + if block_num <= 8: + return 11 + block_num + + # デフォルト: BASEとして扱う + return 0 + + + def _generate_preview_batch(self, preview_images): + """ + プレビュー画像のバッチを生成(1240pxに統一、パディング) + + Args: + preview_images: list of torch.Tensor (各々 H, W, C、長辺1240px) + + Returns: + torch.Tensor: (B, 1240, 1240, C) 形式 + """ + try: + import torch + import torch.nn.functional as F + except ImportError: + return None + + if not preview_images: + # プレビュー画像なし → 黒画像1枚(1240x1240) + black_image = torch.zeros((1, 1240, 1240, 3), dtype=torch.float32) + return black_image + + # 全て1240x1240にパディング + padded_images = [] + target_size = 1240 + + for img in preview_images: + h, w, c = img.shape + + # パディングが必要か確認 + if h == target_size and w == target_size: + padded_images.append(img) + else: + # パディング量を計算(中央配置) + pad_h = target_size - h + pad_w = target_size - w + pad_top = pad_h // 2 + pad_bottom = pad_h - pad_top + pad_left = pad_w // 2 + pad_right = pad_w - pad_left + + # (H, W, C) → (C, H, W) に変換してパディング + img_chw = img.permute(2, 0, 1) # (C, H, W) + + # F.pad: (left, right, top, bottom) + padded = F.pad(img_chw, (pad_left, pad_right, pad_top, pad_bottom), value=0) + + # (C, H, W) → (H, W, C) に戻す + padded = padded.permute(1, 2, 0) + padded_images.append(padded) + + # バッチ化 (B, 1240, 1240, C) + preview_batch = torch.stack(padded_images, dim=0) + return preview_batch + + def _generate_outputs(self, model, clip, final_positive, final_negative, + token_normalization, weight_interpretation, preview_batch): + """CONDITIONING生成と出力""" + try: + from nodes import CLIPTextEncode + + # LoRA構文を削除してクリーンなプロンプトにする + clean_positive = self._remove_lora_syntax(final_positive) if final_positive else "" + clean_negative = self._remove_lora_syntax(final_negative) if final_negative else "" + + # CONDITIONING生成 + positive_conditioning = CLIPTextEncode().encode( + clip=clip, + text=clean_positive + )[0] + + negative_conditioning = CLIPTextEncode().encode( + clip=clip, + text=clean_negative + )[0] + + return (model, clip, final_positive, final_negative, + positive_conditioning, negative_conditioning, preview_batch) + except Exception as e: + print(f"[FilteredRandomLoRALoaderLBW] Error generating outputs: {e}") + # エラー時は黒画像 + try: + import torch + black_image = torch.zeros((1, 1240, 1240, 3), dtype=torch.float32) + except: + black_image = None + return (model, clip, final_positive, final_negative, None, None, black_image) diff --git a/images/lbw_final_single.webp b/images/lbw_final_single.webp new file mode 100644 index 0000000..1640291 Binary files /dev/null and b/images/lbw_final_single.webp differ diff --git a/pyproject.toml b/pyproject.toml index 9333580..a10ff08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,37 @@ [project] -name = "randomloraloader" -description = "Randomly select and apply LoRAs with keyword filtering, preview images, and multiple folder support for ComfyUI" -version = "1.1.0" -license = {file = "LICENSE"} - -classifiers = [ - "Operating System :: OS Independent", - "Environment :: GPU :: NVIDIA CUDA", - "Environment :: GPU :: AMD ROCm", +name = "comfyui-random-lora-loader" +version = "1.2.0" +description = "Randomly select and apply LoRAs with keyword filtering, LoRA Block Weight support, and preview images for ComfyUI" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +authors = [ + {name = "Your Name", email = "your.email@example.com"} # 必要に応じて変更 ] -[project.urls] -Repository = "https://github.com/shin131002/RandomLoRALoader" +keywords = ["comfyui", "lora", "stable-diffusion", "ai", "image-generation", "block-weight", "lbw"] -[tool.comfy] -PublisherId = "" -DisplayName = "RandomLoRALoader" -Icon = "" \ No newline at end of file +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +[project.optional-dependencies] +video = ["opencv-python>=4.0.0"] + +[project.urls] +Homepage = "https://github.com/YOUR_USERNAME/RandomLoRALoader" +Repository = "https://github.com/YOUR_USERNAME/RandomLoRALoader" +Issues = "https://github.com/YOUR_USERNAME/RandomLoRALoader/issues" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["RandomLoRALoader"] diff --git a/random_lora_loader.py b/random_lora_loader.py index 615f417..731d893 100644 --- a/random_lora_loader.py +++ b/random_lora_loader.py @@ -77,6 +77,9 @@ except ImportError: class RandomLoRALoader: """ランダムLoRA選択・適用ノード(3グループ対応)""" + # クラス変数(opencv警告表示フラグ) + _opencv_warning_shown = False + @classmethod def INPUT_TYPES(cls): return { @@ -311,6 +314,7 @@ class RandomLoRALoader: - "0.55" → 0.55(そのまま) - "0.4-0.8" → 0.4, 0.5, 0.6, 0.7, 0.8からランダム(0.1刻み) - "0.44-0.82" → 0.4, 0.5, 0.6, 0.7, 0.8からランダム(範囲を1桁に丸める) + - "-0.8--0.3" → -0.8, -0.7, -0.6, ..., -0.3からランダム(マイナス範囲対応) Args: strength_str: 強度文字列 @@ -318,18 +322,19 @@ class RandomLoRALoader: Returns: float: 実際に使用する強度値 """ + import re strength_str = str(strength_str).strip() - # ハイフンがあればランダム範囲指定 - if '-' in strength_str: + # 範囲指定のパターンマッチ(マイナス値対応) + # 例: "0.6-0.9", "-0.8--0.3", "-0.5-0.5", "0.3--0.7" + range_pattern = r'^(-?\d+\.?\d*)\s*-\s*(-?\d+\.?\d*)$' + match = re.match(range_pattern, strength_str) + + if match: try: - parts = strength_str.split('-') - if len(parts) != 2: - raise ValueError("範囲指定は 'min-max' 形式で入力してください") - # 範囲の上限下限を1桁に丸める - min_val = round(float(parts[0].strip()), 1) - max_val = round(float(parts[1].strip()), 1) + min_val = round(float(match.group(1)), 1) + max_val = round(float(match.group(2)), 1) # バリデーション if min_val < -10.0 or max_val > 10.0: @@ -747,18 +752,20 @@ class RandomLoRALoader: プレビュー画像をTensorとして読み込み(部分一致、長辺1240px) 検索方法: - LoRAファイル名(拡張子除く)で始まる画像ファイルを検索 + LoRAファイル名(拡張子除く)で始まるファイルを検索 + 優先順位: + 1. 静止画像 (.png, .jpg, .jpeg, .webp) + 2. 動画ファイル (.gif, .webp, .mp4, .webm) の1フレーム目 + 例: style_anime_v1.safetensors → style_anime_v1.png - → style_anime_v1_preview.png - → style_anime_v1_001.jpg + → style_anime_v1_preview.jpg + → style_anime_v1.gif (1フレーム目) + → style_anime_v1.mp4 (1フレーム目、opencv-pythonが必要) → STYLE_ANIME_V1.PNG (大文字小文字無視) リサイズ: 長辺を1240pxに統一(アスペクト比保持) - 例: 1024x768 → 1240x930 - 512x512 → 1240x1240 - 800x1200 → 826x1240 Returns: torch.Tensor: (H, W, C) 形式、見つからない場合はNone @@ -774,57 +781,189 @@ class RandomLoRALoader: base_name = os.path.splitext(os.path.basename(lora_path))[0] folder = os.path.dirname(lora_path) - # 画像拡張子 - image_extensions = ('.png', '.jpg', '.jpeg', '.webp', '.gif') + # ファイル拡張子(優先順位順) + static_image_exts = ('.png', '.jpg', '.jpeg') + animated_image_exts = ('.gif', '.webp') # Pillowで対応可能 + video_exts = ('.mp4', '.webm', '.avi', '.mov') # opencv-python必要 try: # 同じフォルダ内のファイルを検索 - for file in os.listdir(folder): - # 部分一致(大文字小文字無視) + files = os.listdir(folder) + + # 部分一致するファイルを収集 + matched_files = [] + for file in files: if file.lower().startswith(base_name.lower()): - # 画像ファイルか確認 - if file.lower().endswith(image_extensions): - preview_path = os.path.join(folder, file) - try: - img = Image.open(preview_path).convert('RGB') - - # 長辺を1240pxにリサイズ(アスペクト比保持) - width, height = img.size - max_size = 1240 - - if max(width, height) > max_size: - # 長辺が1240pxを超える場合はリサイズ - if width > height: - new_width = max_size - new_height = int(height * (max_size / width)) - else: - new_height = max_size - new_width = int(width * (max_size / height)) - - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - elif max(width, height) < max_size: - # 長辺が1240px未満の場合は拡大 - if width > height: - new_width = max_size - new_height = int(height * (max_size / width)) - else: - new_height = max_size - new_width = int(width * (max_size / height)) - - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # numpy配列に変換 (H, W, C) - img_array = np.array(img).astype(np.float32) / 255.0 - # Tensor化 - return torch.from_numpy(img_array) - except Exception as e: - print(f"[RandomLoRALoader] Preview load error ({preview_path}): {e}") - continue + matched_files.append(file) + + if not matched_files: + return None + + # 優先順位でソート + def get_priority(filename): + lower = filename.lower() + if any(lower.endswith(ext) for ext in static_image_exts): + return 0 # 最優先 + elif any(lower.endswith(ext) for ext in animated_image_exts): + return 1 # 次優先 + elif any(lower.endswith(ext) for ext in video_exts): + return 2 # 最後 + else: + return 999 # その他 + + matched_files.sort(key=get_priority) + + # 各ファイルを試す + for file in matched_files: + preview_path = os.path.join(folder, file) + lower_file = file.lower() + + # 静止画像 + if any(lower_file.endswith(ext) for ext in static_image_exts): + img = self._load_static_image(preview_path) + if img is not None: + return img + + # アニメーション画像(GIF/WebP) + elif any(lower_file.endswith(ext) for ext in animated_image_exts): + img = self._load_animated_image_first_frame(preview_path) + if img is not None: + return img + + # 動画ファイル(opencv-python必要) + elif any(lower_file.endswith(ext) for ext in video_exts): + img = self._load_video_first_frame(preview_path) + if img is not None: + return img + except Exception as e: print(f"[RandomLoRALoader] Folder read error: {e}") return None + def _load_static_image(self, image_path): + """静止画像を読み込み""" + try: + from PIL import Image + import numpy as np + import torch + + img = Image.open(image_path).convert('RGB') + return self._resize_and_convert_image(img) + except Exception as e: + print(f"[RandomLoRALoader] Static image load error ({image_path}): {e}") + return None + + def _load_animated_image_first_frame(self, image_path): + """アニメーション画像(GIF/WebP)の1フレーム目を読み込み""" + try: + from PIL import Image + import numpy as np + import torch + + img = Image.open(image_path) + + # 1フレーム目に移動 + if hasattr(img, 'seek'): + img.seek(0) + + img = img.convert('RGB') + return self._resize_and_convert_image(img) + except Exception as e: + print(f"[RandomLoRALoader] Animated image load error ({image_path}): {e}") + return None + + def _load_video_first_frame(self, video_path): + """動画ファイルの1フレーム目を読み込み(opencv-python必要)""" + try: + import cv2 + from PIL import Image + import numpy as np + import torch + + # OpenCVで動画を開く + cap = cv2.VideoCapture(video_path) + + if not cap.isOpened(): + return None + + # 1フレーム目を読み込み + ret, frame = cap.read() + cap.release() + + if not ret: + return None + + # BGR → RGB 変換 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # PIL Imageに変換 + img = Image.fromarray(frame_rgb) + + return self._resize_and_convert_image(img) + + except ImportError: + # opencv-pythonがインストールされていない(初回のみ警告) + if not RandomLoRALoader._opencv_warning_shown: + print("=" * 60) + print("[RandomLoRALoader] Video Preview Support") + print("=" * 60) + print(f"Video file detected: {os.path.basename(video_path)}") + print("opencv-python is not installed.") + print("") + print("To enable video preview support, install:") + print(" pip install opencv-python") + print("") + print("Static images (.png/.jpg) and animated images (.gif/.webp)") + print("will continue to work without opencv-python.") + print("=" * 60) + RandomLoRALoader._opencv_warning_shown = True + return None + except Exception as e: + print(f"[RandomLoRALoader] Video load error ({video_path}): {e}") + return None + + def _resize_and_convert_image(self, img): + """画像をリサイズしてTensorに変換""" + try: + from PIL import Image + import numpy as np + import torch + + # 長辺を1240pxにリサイズ(アスペクト比保持) + width, height = img.size + max_size = 1240 + + if max(width, height) > max_size: + # 長辺が1240pxを超える場合はリサイズ + if width > height: + new_width = max_size + new_height = int(height * (max_size / width)) + else: + new_height = max_size + new_width = int(width * (max_size / height)) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + elif max(width, height) < max_size: + # 長辺が1240px未満の場合は拡大 + if width > height: + new_width = max_size + new_height = int(height * (max_size / width)) + else: + new_height = max_size + new_width = int(width * (max_size / height)) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # numpy配列に変換 (H, W, C) + img_array = np.array(img).astype(np.float32) / 255.0 + # Tensor化 + return torch.from_numpy(img_array) + + except Exception as e: + print(f"[RandomLoRALoader] Image resize/convert error: {e}") + return None + def _generate_preview_batch(self, preview_images): """ プレビュー画像のバッチを生成(1240pxに統一、パディング)