21 Commits
Author SHA1 Message Date
shin132002 6e1cf9b518 v1.5.0: Filtered nodes: add keyword output and filter_mode OFF 2026-09-29 01:24:30 +09:00
shin132002 37a2cfe7da v1.4.0: random loaders output clean positive_text, add lora_text
- positive_text no longer contains <lora:...> tags; it matches the encoded positive exactly

- New lora_text output (last slot) carries the previous positive_text content

- Filtered loader: collapse runs of commas left after removing tags
2026-09-25 06:33:12 +09:00
shin132002 3b0a271b39 Fixed an issue 2026-09-11 21:33:21 +09:00
shin132002 801b13a0d7 Update to v1.2.0 2026-01-13 22:20:07 +09:00
shin132002 83775df239 Add publisher-id to pyproject.toml 2026-01-13 22:07:54 +09:00
shin132002 d9adf51c55 Release v1.2.0 2026-01-13 21:43:53 +09:00
shin132002 57f511c06f Update pyproject.toml: version 1.1.0, clean up URLs 2026-01-05 22:35:35 +09:00
konohana 69450cf7c6 Merge pull request #2 from ComfyNodePRs/publish
Add Github Action for Publishing to Comfy Registry
2026-01-05 22:01:15 +09:00
konohana dde9ca4d1e Merge pull request #1 from ComfyNodePRs/pyproject
Add pyproject.toml for Custom Node Registry
2026-01-05 21:29:29 +09:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") 409c3044e3 chore(pyproject): Add pyproject.toml for Custom Node Registry 2026-01-04 09:09:50 +00:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") 62714a22b8 chore(publish): Add Github Action for Publishing to Comfy Registry 2026-01-04 09:09:14 +00:00
shin132002 06b7d8f6a1 v1.1.0: Add Preview Images 2026-01-04 15:29:39 +09:00
shin132002 6e4c67457f v1.1.0: Add Filtered Random LoRA Loader, Preview Images 2026-01-04 15:15:42 +09:00
shin132002 b6f815deff v1.1.0: Add Filtered Random LoRA Loader, Preview Images, and Duplicate Handling 2026-01-04 15:13:54 +09:00
shin132002 4811ecc323 v1.1.0: Add Filtered Random LoRA Loader, Preview Images, and Duplicate Handling 2026-01-04 15:08:45 +09:00
shin132002 5cd9cfa908 Add: Tips & Tricks section for LoRA syntax filter usage 2026-01-03 20:40:12 +09:00
shin132002 ce598ecbff Add: Embedded metadata support and 'metadata' mode 2026-01-03 08:40:19 +09:00
shin132002 8cc19780b3 Add: Workflow screenshots to README 2026-01-01 08:53:11 +09:00
shin132002 cc43eaca9d Add: New feature for .info 2026-01-01 08:25:24 +09:00
shin132002 c6f1567d8c Add: English and Japanese README 2026-01-01 08:06:18 +09:00
shin132002 332c153108 Fix: Handle null meta in sample prompts 2025-12-31 07:16:40 +09:00
18 changed files with 6857 additions and 493 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'shin131002' }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+30 -2
View File
@@ -4,13 +4,41 @@ __pycache__/
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
# IDE
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
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
+213
View File
@@ -0,0 +1,213 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.5.0] - 2026-09-29
### Added
#### `keyword` output (Filtered Random LoRA Loader / Filtered Random LoRA Loader (LBW))
- New **`keyword`** STRING output with the `keyword_filter` keywords the selected LoRA actually matched, joined with `_`
- AND: all keywords (`aaa bbb` → `aaa_bbb`)
- OR: only the keywords that matched (`aaa bbb` → `aaa` or `bbb`). If the LoRA contains several of them, or `num_loras` is 2 or more, every matched keyword is joined in input order
- Spaces inside a quoted phrase also become `_` (`"aaa bbb" ccc` with AND → `aaa_bbb_ccc`)
- Case is kept as typed; keywords matched in metadata (`search_in_metadata`) are included
- Empty when `filter_mode` is OFF, when `keyword_filter` is empty, when no LoRA matched, or when `num_loras` is 0
- Added as the **last** slot, so existing workflow connections stay valid. LoRA selection and all other outputs are unchanged (same seed, same result as 1.4.0)
- Random LoRA Loader (3 folders) has no keyword filter and is unchanged
#### `filter_mode`: new `OFF` option (Filtered Random LoRA Loader / Filtered Random LoRA Loader (LBW))
- `OFF` turns the keyword filter off while keeping the typed keywords, so it can be switched off for a while without deleting them. Picks the same LoRAs as an empty `keyword_filter`
- Added after `AND` / `OR`; the default stays `AND`. Saved workflows keep their `AND` / `OR` setting
---
## [1.4.0] - 2026-09-24
### Changed
#### ⚠️ `positive_text` no longer contains LoRA syntax; new `lora_text` output
- `positive_text` is now the positive prompt **without** `<lora:...>` tags — exactly what is encoded into the `positive` CONDITIONING output — so it can be fed straight into a text encoder
- The previous content, tags included, now comes from a new **`lora_text`** output, added as the **last** slot
- All other outputs keep their positions and types, so existing workflow connections stay valid (ComfyUI connects outputs by position)
- Action for existing workflows:
- `positive_text` fed a text encoder: nothing to do
- `positive_text` was used to record the LoRA syntax (saved prompts, metadata): reconnect that link to `lora_text`
- Applies to all three nodes (Random LoRA Loader, Filtered Random LoRA Loader, Filtered Random LoRA Loader (LBW))
#### ⚠️ Filtered nodes: cleaner encoded prompt
- Filtered Random LoRA Loader / (LBW) removed `<lora:...>` tags before encoding but left the separators behind, so the positive conditioning was encoded from text like `masterpiece, , trig_a, , , trig_c`
- Runs of commas and leading/trailing commas are now tidied before encoding, matching the 3-folder node. This applies to the negative prompt too (e.g. a trailing `, ` is no longer encoded)
- Because the encoded text changes, **results with the same seed may differ from 1.3.0 and earlier**. Keep using an earlier version if identical results are required
### Fixed
- Random LoRA Loader: a run of three or more commas left after removing tags from the additional prompt was only partly collapsed
---
## [1.3.0] - 2026-09-11
### Fixed
#### Symlinked folders were skipped during scanning
- Folder scans now follow symbolic links, matching ComfyUI's own behaviour (`folder_paths.recursive_search` uses `followlinks=True`)
- Previously, a subfolder that was a symlink was skipped entirely, so LoRAs behind a link never appeared as candidates — even though they showed up fine in ComfyUI's native LoRA dropdown
- Affects all three nodes:
- **Random LoRA Loader**: `glob.glob("**/*.safetensors")` replaced with `os.walk(followlinks=True)` (glob's `**` does not descend into symlinked directories)
- **Filtered Random LoRA Loader** / **Filtered Random LoRA Loader (LBW)**: `os.walk()` now passes `followlinks=True`
- Symlink loops are detected via a `realpath` set and skipped, so a cyclic link cannot hang the scan
- A folder reachable through two different links is no longer scanned twice
#### Redundant error logging on non-safetensors LoRAs
- `_load_embedded_metadata` now returns early for `.pt` / `.ckpt` files instead of calling `safe_open`, which can only read `.safetensors`
- Previously every selected `.pt` / `.ckpt` produced a spurious "metadata read error" log line
- Trigger words for these formats still come from `.metadata.json` / `.info` sidecar files as before
### Changed
#### Seed reproducibility — candidate list is now sorted
- The candidate file list is sorted before random selection
- Previously the order came straight from the filesystem, so **the same seed could produce different selections on different machines**. Sorting makes a seed reproducible across environments
- ⚠️ Because the ordering changed, an existing workflow may select a different set of LoRAs after upgrading even with a fixed seed. If a result you were relying on changes, re-roll or re-pick the seed
#### Random LoRA Loader: extension support aligned with the Filtered nodes
- Now scans `.safetensors`, `.pt`, and `.ckpt` (previously `.safetensors` only)
- All three nodes now return an identical candidate list for the same folder
- Extension matching is case-insensitive, so `.SAFETENSORS` is picked up on Linux as well (glob was case-sensitive there)
---
## [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
#### New Node: Filtered Random LoRA Loader
- Single folder with keyword filtering capabilities
- Space-separated keyword search with phrase support (`"..."`)
- AND/OR filtering modes
- Metadata search with caching (instant 2nd+ time)
- Designed for chaining multiple instances
- Supports all features from Random LoRA Loader
#### Preview Image Output (Both Nodes)
- New `preview` output (IMAGE type)
- Long edge resized to 1240px (aspect ratio preserved)
- Center-padded to 1240x1240 for batch compatibility
- Partial filename matching (case-insensitive)
- Example: `anime_v1.safetensors` matches `anime_v1.png`, `anime_v1_preview.jpg`, etc.
#### Duplicate Filename Handling (Both Nodes)
- New `unique_by_filename` parameter (default: `True`)
- Prevents selecting same LoRA from different subfolders
- Example: `/style/lora.safetensors` and `/backup/lora.safetensors` → only one selected
- Logs duplicate detections with paths
### Changed
#### Both Nodes
- **Breaking**: Split `additional_prompt` into `additional_prompt_positive` and `additional_prompt_negative`
- Unified strength precision to 1 decimal place (was 2 for Filtered, 1 for Random)
- Improved LoRA syntax removal from CONDITIONING generation
#### Filtered Random LoRA Loader
- Better handling when `num_loras` exceeds available files (fills with duplicates)
- Removed `control_after_generate` (use ComfyUI's default seed control)
### Fixed
#### Both Nodes
- Fixed negative_text output in `json_sample_prompt` mode
- Fixed LoRA syntax (`<lora:...>`) appearing in metadata trigger words
- Fixed parameter order consistency
#### Random LoRA Loader
- Fixed duplicate parameter in INPUT_TYPES
### Performance
#### Metadata Search Caching
- Initial load time (SSD):
- 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
- 2nd+ time: Instant (<100ms)
- Memory usage: ~150MB per 10,000 files
- Progress display for large collections
---
## [1.0.0] - 2025-12-30
### Initial Release
#### Random LoRA Loader
- 3-group folder selection
- Multi-source metadata reading (`.metadata.json`, `.info`, embedded)
- Strength randomization with range specification
- Multiple trigger word sources
- Sample prompt support with LoRA syntax removal
- Wildcard Encode (Inspire) compatibility
#### Core Features
- No additional dependencies (uses Python stdlib + ComfyUI)
- Automatic trigger word extraction
- CONDITIONING output with cleaned prompts
- Seed control integration
+1045 -428
View File
File diff suppressed because it is too large Load Diff
+614
View File
@@ -0,0 +1,614 @@
# 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)
---
## Overview
Filtered Random LoRA Loader (LBW) is the Filtered Random LoRA Loader with LoRA Block Weight (LBW) support added. It lets you control precisely which parts of the U-Net a LoRA is applied to.
---
## Features
### **Core Features (shared with Filtered Random LoRA Loader)**
- ✅ Random LoRA selection from a single folder
- ✅ Keyword filtering (AND/OR/OFF modes)
- ✅ Output of the matched keywords (`keyword`, v1.5.0)
- ✅ 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**: set a separate strength for each U-Net block to control style, character, composition and so on independently
- ✅ **Automatic SD1.5/SDXL detection**
- ✅ **4 preset modes + custom input**
- ✅ **Automatic weight adjustment**
---
## LoRA Block Weight (LBW)
### **What is LBW?**
LoRA Block Weight lets you control which parts of the U-Net the LoRA affects. The Stable Diffusion U-Net is made up of three parts:
```
INPUT blocks → Structure, composition, layout
↓
MIDDLE block → Overall features
↓
OUTPUT blocks → Style, details, fine-tuning
```
By adjusting the weight of 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**
Enables OUTPUT blocks only.
**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`
**Best for:**
- Style LoRAs (anime style, watercolor, etc.)
- Art style and effect LoRAs
- Changing the visual aesthetic
**Effect:**
- ✅ Strong style changes
- ❌ Minimal structure/character changes (ideal when you want to keep the existing composition)
---
### **3. Character Focused**
Enables INPUT (early) + MIDDLE + OUTPUT blocks.
**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`
**Best for:**
- Character and person LoRAs
- Concept LoRAs
- Preserving faces/features
**Effect:**
- ✅ Strong character features
- ✅ Balanced overall effect
---
### **4. Structure/Composition Only**
Enables INPUT + MIDDLE blocks 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`
**Best for:**
- Pose LoRAs
- Composition and layout LoRAs
**Effect:**
- ✅ Strong structure/pose changes
- ❌ Minimal style changes
---
### **5. Balanced / Soft**
Soft application across selected blocks. The later OUTPUT blocks are turned down to avoid over-application.
**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`
**Best for:**
- Subtle effects and fine adjustment
- Multi-LoRA workflows
- General-purpose LoRAs
**Effect:**
- ✅ Balanced, gentle application with a natural finish
- ✅ Good for stacking multiple LoRAs
---
### **6. Preset: Random**
Randomly picks one of the 4 presets above on each run.
```
weight_mode: "Preset: Random"
→ One of the 4 presets is picked on each run
```
**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):**
```
weight_mode: "Direct Input"
lbw_input: "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)
```
| Index | Block | Role |
|-------|-------|------|
| 0 | BASE | Overall base |
| 1-9 | INPUT (IN00-08) | Structure/composition |
| 10 | MIDDLE (M00) | Intermediate features |
| 11-19 | OUTPUT (OUT00-08) | Style/details |
**Weight example:**
```
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)
→ LoRA applied to OUTPUT blocks only (style focused)
```
### **SD1.5 (17 elements)**
```
BASE, IN01,IN02,IN04,IN05,IN07,IN08 (6), MID (1), OUT03-OUT11 (9)
```
| Index | Block | Role |
|-------|-------|------|
| 0 | BASE | Overall base |
| 1-6 | INPUT (IN01,02,04,05,07,08) | Structure/composition |
| 7 | MIDDLE (M00) | Intermediate features |
| 8-16 | OUTPUT (OUT03-11) | Style/details |
**Weight example:**
```
1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1
BASE,IN (OFF),M00,OUT (ON)
→ LoRA applied to OUTPUT blocks only (style focused)
```
---
## 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)
```
SDXL detected → 20-element weights are used
SD1.5 detected → 17-element weights are used
```
**You don't need to specify the model type manually!** ✅
---
### **Weight Adjustment**
If the number of weights doesn't match the detected LoRA type, it is adjusted automatically:
**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]
lora_text → [Show Text] (optional, for debugging)
```
**Steps:**
1. **Place the node**
```
[Filtered Random LoRA Loader (LBW)]
```
2. **Set the folder and keywords**
```
lora_folder_path: "path/to/loras"
keyword_filter: "anime style"
num_loras: 1
```
3. **Choose the LBW mode**
```
weight_mode: "Style Focused"
```
4. **Connect MODEL/CLIP**
```
MODEL → [KSampler]
CLIP → [CLIP Text Encode]
```
### **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")
> Folder scanning behaviour (supported extensions, symbolic links, sorted candidate order) is shared with the other nodes — see [Folder Scanning](README.md#folder-scanning) in the main README.
#### **Keyword Filter**
- `keyword_filter`: Space-separated keywords (e.g., `style anime` or `"anime style" red`)
- `filter_mode`: AND / OR / OFF (OFF turns the filter off but keeps the keywords, v1.5.0)
- `search_in_metadata`: Search in JSON/embedded metadata (slower)
#### **LBW Settings**
- `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 `lora_text` output (`positive_text` before v1.4.0) includes LBW syntax:
**Normal (All 1.0):**
```
<lora:style_anime:0.8:0.8>
```
**With LBW (Style Focused, SDXL):**
```
<lora:style_anime:0.8:0.8:lbw=1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1>
```
**Note:** The LBW is applied internally to the MODEL. The syntax in `lora_text` is for reference/re-use with other nodes.
### **Keyword Output (v1.5.0)**
Outputs the keywords the selected LoRA actually matched, joined with `_` (last output). AND mode gives all keywords (`aaa bbb` → `aaa_bbb`); OR mode gives the one that matched (`aaa` or `bbb`). Spaces inside a phrase also become `_` (`"aaa bbb" ccc` → `aaa_bbb_ccc`). Empty when `filter_mode` is OFF. See [Keyword Output in README.md](README.md#keyword-output-v150) for details.
---
## Examples
### **Example 1: Change the style only**
```
[Load Checkpoint] → [Filtered Random LoRA Loader (LBW)]
weight_mode: "Style Focused"
keyword_filter: "anime"
↓
[KSampler]
```
**Result:** The composition stays the same; only the style becomes anime-like
---
### **Example 2: Staged application with multiple LoRAs**
```
[Load Checkpoint]
↓
[Filtered Random LoRA Loader (LBW)] (Structure/Composition Only)
keyword_filter: "pose"
↓
[Filtered Random LoRA Loader (LBW)] (Style Focused)
keyword_filter: "watercolor"
↓
[KSampler]
```
**Result:** Pose adjustment → watercolor style applied
---
### **Example 3: Character + style**
```
[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]
```
**Result:** Character features preserved + style changed
---
## 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 preserves features |
| Pose/Composition | Structure/Composition Only | INPUT blocks control layout |
| General/fine adjustment | 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 whose training effect is spread evenly across blocks
---
### **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.
**Weight guide:**
- **0**: Completely off
- **0.5**: Weaker
- **1.0**: Standard
- **1.5 or more**: Stronger (watch for over-application)
---
### **4. Serial Connection**
For applying different LBW settings to different LoRAs, connect nodes in series:
```
[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.
**Tips:**
- **Structure → style** is the basic order
- Strength can also be adjusted per node
---
### **5. Troubleshooting**
**If there is no effect:**
- Check that the weights aren't all 0
- Check that the LoRA is loaded correctly
- Check the console for the LBW application message
**If the effect is too strong:**
- Lower model_strength
- Try the Balanced / Soft preset
- Adjust the weights to 0.5–0.8
---
## 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.
**File matching:**
- Files that start with the LoRA filename (case-insensitive)
- Example: matches for `style_anime.safetensors`:
- `style_anime.png` ✅
- `style_anime_preview.jpg` ✅
- `STYLE_ANIME.PNG` ✅
---
## Advanced: Custom Presets
To create your own presets or change the existing ones, you can edit the source file directly.
### **How to Edit**
**File location:**
```
ComfyUI/custom_nodes/RandomLoRALoader/filtered_random_lora_loader_lbw.py
```
**Lines to edit:** 29–41 (`SDXL_PRESETS` for SDXL, `SD15_PRESETS` for SD1.5)
### **Example 1: Add your own preset**
```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",
# ↓ Your own preset
"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"
}
```
### **Example 2: Adjust an existing preset**
```python
SDXL_PRESETS = {
# Make Style Focused stronger
"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",
# Make Character Focused milder
"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",
...
}
```
### **Example 3: Rename presets**
```python
SDXL_PRESETS = {
"Style Only": "1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1",
"Character First": "1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1",
"Composition Only": "1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0",
...
}
```
### **Important Notes**
⚠️ **Before editing:**
- Make a backup of the original file
- Watch the Python syntax (commas, quotes)
⚠️ **Number of weights:**
- SDXL presets: exactly 20 elements
- SD1.5 presets: exactly 17 elements
- Mismatched counts are auto-adjusted, but specifying them exactly is more reliable
⚠️ **After editing:**
- Fully restart ComfyUI to apply the changes
- Check the console for syntax errors
💡 **Tip:** If editing the file is a hassle, use the "Direct Input" mode!
---
## 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
konohana
---
**Enjoy precise LoRA control with Block Weights!** 🎨✨
+614
View File
@@ -0,0 +1,614 @@
# Filtered Random LoRA Loader (LBW)
**LoRAブロックウェイト(LBW)対応、SD1.5/SDXL自動検出付き**
![FilteredRandomLoRALoader(LBW)ワークフロー例](./images/lbw_final_single.webp)
---
## 概要
Filtered Random LoRA Loader (LBW) は、Filtered Random LoRA LoaderにLoRAブロックウェイト(LBW)機能を追加したノードです。U-Netのどの部分にLoRAを適用するかを細かく制御できます。
---
## 主な機能
### **基本機能(Filtered Random LoRA Loaderと共通)**
- ✅ 1つのフォルダからLoRAをランダム選択
- ✅ キーワードフィルタ(AND/OR/OFFモード)
- ✅ マッチしたキーワードの出力(`keyword`、v1.5.0)
- ✅ メタデータ検索(ファイル名または埋め込みメタデータ)
- ✅ プレビュー画像(静止画・アニメーション画像・動画に対応)
- ✅ トリガーワードの取得
- ✅ ファイル名による重複除外
- ✅ そのまま直列接続が可能
### **拡張機能(LBW)**
- ✅ **LoRAブロックウェイト(LBW)対応**:U-Netのブロックごとに個別の強度を設定し、画風・キャラクター・構図などを分離して制御
- ✅ **SD1.5/SDXLの自動検出**
- ✅ **4つのプリセットモード+カスタム入力**
- ✅ **ウェイト数の自動調整**
---
## LoRAブロックウェイト(LBW)
### **LBWとは?**
LoRAブロックウェイトを使うと、U-Netのどの部分にLoRAを効かせるかを制御できます。Stable DiffusionのU-Netは次の3つの部分で構成されています。
```
INPUT blocks → 構造、構図、レイアウト
↓
MIDDLE block → 全体的な特徴
↓
OUTPUT blocks → 画風、ディテール、細部の調整
```
ブロックごとにウェイトを調整することで、LoRAの効き方を細かく制御できます。
---
## ウェイトモード
### **1. Normal (All 1.0)** - デフォルト
すべてのブロックのウェイトが1.0(LBWを使わない通常のLoRA適用)です。
---
### **2. Style Focused(画風特化)**
OUTPUTブロックのみを有効にします。
**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`
**向いているもの:**
- 画風LoRA(アニメ風、水彩風など)
- スタイルLoRA、エフェクトLoRA
- 見た目の雰囲気を変えたい場合
**効果:**
- ✅ 画風が強く変わる
- ❌ 構図やキャラクターはほとんど変わらない(既存の構図を保ちたい場合に最適)
---
### **3. Character Focused(キャラ重視)**
INPUT前半+MIDDLE+OUTPUTを有効にします。
**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`
**向いているもの:**
- キャラクターLoRA、人物LoRA
- コンセプトLoRA
- 顔や特徴を保ちたい場合
**効果:**
- ✅ キャラクターの特徴が強く出る
- ✅ 全体的にバランスの取れた効き方
---
### **4. Structure/Composition Only(構造・構図のみ)**
INPUT+MIDDLEブロックのみを有効にします。
**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`
**向いているもの:**
- ポーズLoRA
- 構図LoRA、レイアウトLoRA
**効果:**
- ✅ 構造やポーズが強く変わる
- ❌ 画風はほとんど変わらない
---
### **5. Balanced / Soft(バランス・穏やか)**
選んだブロックに穏やかに適用します。OUTPUT後半を弱めて過剰な効きを防ぎます。
**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
**効果:**
- ✅ バランスの取れた穏やかな効き方で、自然な仕上がり
- ✅ 複数のLoRAを重ねやすい
---
### **6. Preset: Random**
上記4つのプリセットから、実行ごとに1つをランダムに選びます。
```
weight_mode: "Preset: Random"
→ 実行ごとに4つのプリセットから1つ選択
```
**向いているもの:**
- 試行錯誤
- バリエーションの生成
- ランダムな効果
---
### **7. Direct Input**
ウェイトを直接指定します。
**形式:**
- SDXL: カンマ区切りで20個の値
- SD1.5: カンマ区切りで17個の値
**例(SDXL):**
```
weight_mode: "Direct Input"
lbw_input: "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"
```
**自動調整:**
- 要素数が足りない場合:末尾を1.0で埋める
- 要素数が多すぎる場合:末尾から切り詰める
---
## ブロック構造
### **SDXL(20要素)**
```
BASE, IN00-IN08 (9), MID (1), OUT00-OUT08 (9)
```
| インデックス | ブロック | 役割 |
|------------|---------|------|
| 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要素)**
```
BASE, IN01,IN02,IN04,IN05,IN07,IN08 (6), MID (1), OUT03-OUT11 (9)
```
| インデックス | ブロック | 役割 |
|------------|---------|------|
| 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適用(画風特化)
```
---
## 自動検出
### **LoRAタイプの検出**
ノードはモデル構造を解析して、LoRAがSD1.5用かSDXL用かを自動で判定します。
- **SD1.5**: `output_blocks_0`〜`output_blocks_11`(12ブロック)
- **SDXL**: `output_blocks_0`〜`output_blocks_8`(9ブロック)
```
SDXL検出 → 20要素のウェイトを使用
SD1.5検出 → 17要素のウェイトを使用
```
**モデルの種類を手動で指定する必要はありません** ✅
---
### **ウェイト数の自動調整**
入力したウェイトの数が、検出したLoRAタイプと合わない場合は自動で調整します。
**例1: 足りない場合**
```
入力: 1,0,0(3要素)
検出: SDXL(20要素必要)
→ 自動調整: 1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
```
**例2: 多すぎる場合**
```
入力: 1,0,0,...,0,0,0(25要素)
検出: SDXL(20要素必要)
→ 自動調整: 1,0,0,...,0,0,0(先頭20要素のみ)
```
---
## 使い方
### **基本的なワークフロー**
```
[Filtered Random LoRA Loader (LBW)]
MODEL → [KSampler]
CLIP → [CLIP Text Encode]
lora_text → [Show Text](任意、確認用)
```
**手順:**
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]
```
### **パラメータ**
#### **LoRA設定**
- `lora_folder_path`: LoRAフォルダのパス
- `num_loras`: 適用するLoRAの数(0〜20)
- `model_strength`: MODELに対するLoRAの強度(例: "1.0" または "0.6-0.9")
- `clip_strength`: CLIPに対するLoRAの強度(例: "1.0" または "0.6-0.9")
> フォルダスキャンの挙動(対象拡張子、シンボリックリンクの追跡、候補リストのソート)は他のノードと共通です。詳細は[README_ja.md の「フォルダスキャンについて」](README_ja.md#フォルダスキャンについて)を参照してください。
#### **キーワードフィルタ**
- `keyword_filter`: スペース区切りのキーワード(例: `style anime` や `"anime style" red`)
- `filter_mode`: AND / OR / OFF(OFFはキーワードを残したままフィルタを無効にする、v1.5.0)
- `search_in_metadata`: JSONや埋め込みメタデータも検索対象にする(遅くなります)
#### **LBW設定**
- `weight_mode`: Normal / 4つのプリセット / Random / Direct Input
- `lbw_input`: カスタムウェイト(Direct Inputモードのみ)
#### **その他**
- `trigger_word_source`: トリガーワードの取得方法
- `seed`: ランダムシード
---
## 出力形式
### **LBW構文**
LBWを適用した場合、`lora_text`出力(v1.4.0より前は`positive_text`)にLBW構文が含まれます。
**Normal (All 1.0):**
```
<lora:style_anime:0.8:0.8>
```
**LBW適用時(Style Focused、SDXL):**
```
<lora:style_anime:0.8:0.8:lbw=1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1>
```
**注:** LBWはノード内部でMODELに適用済みです。`lora_text`の構文は、記録や他のノードでの再利用のためのものです。
### **keyword出力(v1.5.0)**
選ばれたLoRAが実際にマッチしたキーワードを`_`で連結して出力します(出力の末尾)。ANDモードは全キーワード(`aaa bbb` → `aaa_bbb`)、ORモードはマッチしたもの(`aaa`または`bbb`)です。フレーズ内のスペースも`_`になります(`"aaa bbb" ccc` → `aaa_bbb_ccc`)。`filter_mode`がOFFのときは空です。詳細は[README_ja.md の「keyword出力」](README_ja.md#keyword出力v150)を参照してください。
---
## 使用例
### **例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 | OUTPUTブロックが画風を担うため |
| キャラクター | Character Focused | IN+MID+OUTのバランスで特徴を保つため |
| ポーズ・構図 | Structure/Composition Only | INPUTブロックがレイアウトを担うため |
| 汎用・微調整 | Balanced / Soft | 穏やかで重ねやすいため |
---
### **2. 効果の見え方**
プリセットごとの違いが**見えやすい**条件:
- ✅ 強めのLoRA(model_strengthが高い)
- ✅ 1つだけ適用(num_loras=1)
- ✅ 画風・コンセプト系のLoRA
違いが**見えにくい**条件:
- ⚠️ 弱めのLoRA(model_strengthが低い)
- ⚠️ 複数のLoRAを同時に適用
- ⚠️ 学習の効き方がブロック全体に均等に分散しているLoRA
---
### **3. ウェイト0 = 効果なし**
すべてのウェイトを0にすると、LoRAの効果は完全に無効になります。
```
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
```
これはLoRAを適用しないのと同じです。
**ウェイト調整の目安:**
- **0**: 完全無効
- **0.5**: 弱め
- **1.0**: 標準
- **1.5以上**: 強め(効きすぎに注意)
---
### **4. 直列接続**
LoRAごとに異なるLBW設定を使いたい場合は、ノードを直列に接続します。
```
[Filtered Random LoRA Loader (LBW)] (Style Focused)
↓
[Filtered Random LoRA Loader (LBW)] (Character Focused)
↓
[KSampler]
```
1つのノード内で選ばれたLoRAは、すべて同じLBW設定で適用されます。
**コツ:**
- **構造 → 画風の順**が基本
- 強度もノードごとに個別に調整可能
---
### **5. トラブルシューティング**
**効果が出ない場合:**
- ウェイトがすべて0になっていないか確認
- LoRAが正しく読み込まれているか確認
- コンソールでLBW適用のメッセージを確認
**効果が強すぎる場合:**
- model_strengthを下げる
- Balanced / Softプリセットを試す
- ウェイトを0.5〜0.8に調整
---
## 要件
### **⚠️ 対応モデル**
**本ノードはSD1.5とSDXLモデル専用です。**
- ✅ **対応:** Stable Diffusion 1.5、Stable Diffusion XL(SDXL)
- ❌ **非対応:** Flux、SD3、SDXL Turbo、Pony、その他のアーキテクチャ
LBW機能は、SD1.5/SDXLのU-Netアーキテクチャ専用に設計されています。他のモデルタイプでは正常に動作しません。
---
### **依存関係**
- 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のファイル名に一致するプレビューファイルを自動で探します。
**ファイルマッチング:**
- 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行目(SDXL用が`SDXL_PRESETS`、SD1.5用が`SD15_PRESETS`)
### **例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」モードを使ってください!
---
## 免責事項
- 技術サポートは提供しません
- 動作の保証はありません
- 今後のComfyUIのアップデートとの互換性は保証しません
- バグ報告や機能要望に対応できない場合があります
- 自己責任でご利用ください
---
## 更新履歴
### 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
---
## 作者
konohana
---
**ブロックウェイトで精密なLoRA制御を楽しんでください!** 🎨✨
+1206
View File
File diff suppressed because it is too large Load Diff
+25 -1
View File
@@ -3,6 +3,30 @@ RandomLoRALoader Custom Node for ComfyUI
ランダムLoRA選択・適用ノード
"""
from .random_lora_loader import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
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,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RandomLoRALoader": "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']
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

+19
View File
@@ -0,0 +1,19 @@
[project]
name = "randomloraloader"
description = "Randomly select and apply LoRAs with keyword filtering, LoRA Block Weight support, preview images, and multiple folder support for ComfyUI"
version = "1.5.0"
license = {file = "LICENSE"}
classifiers = [
"Operating System :: OS Independent",
"Environment :: GPU :: NVIDIA CUDA",
"Environment :: GPU :: AMD ROCm",
]
[project.urls]
Repository = "https://github.com/shin131002/RandomLoRALoader"
[tool.comfy]
PublisherId = "konohana"
DisplayName = "RandomLoRALoader"
Icon = ""
+585 -62
View File
@@ -39,12 +39,15 @@ RandomLoRALoader - ComfyUI Custom Node V1
- model_strength_3: MODEL適用強度
- clip_strength_3: CLIP適用強度
- num_loras_3: 選択するLoRA個数
19. trigger_word_source: json_combined/json_random/json_sample_prompt(共通)
19. trigger_word_source: json_combined/json_random/json_sample_prompt/metadata(共通)
20. seed: ランダム選択のシード値(共通、ComfyUI標準のcontrol_before_generateで制御)
【その他仕様】
- 外部JSONファイル({LoRAファイル名}.metadata.json)からトリガーワード/作例を取得
- JSON読み取り優先順位: trainedWords / images[].meta.prompt
- メタデータ読み取り優先順位:
1. {LoRAファイル名}.metadata.json (ComfyUI Lora Manager)
2. {LoRAファイル名}.info (Civitai Helper)
3. LoRA本体ファイルの埋め込みメタデータ
- trigger_word_source="metadata"時は埋め込みメタデータのみを参照
- 同じLoRAの重複選択なし(不足時は再選択で埋める)
- json_sample_prompt選択時はpositive/negativeを分離
- 作例プロンプト内のLoRA記述(<lora:xxx:x.x>)は削除
@@ -63,9 +66,20 @@ import folder_paths
import comfy.sd
import comfy.utils
# LoRA埋め込みメタデータ読み込み用
try:
from safetensors.torch import safe_open
SAFETENSORS_AVAILABLE = True
except ImportError:
SAFETENSORS_AVAILABLE = False
print("[RandomLoRALoader] safetensors not available, embedded metadata reading disabled")
class RandomLoRALoader:
"""ランダムLoRA選択・適用ノード(3グループ対応)"""
# クラス変数(opencv警告表示フラグ)
_opencv_warning_shown = False
@classmethod
def INPUT_TYPES(cls):
return {
@@ -84,10 +98,15 @@ class RandomLoRALoader:
"default": "A1111"
}
),
"additional_prompt": ("STRING", {
"additional_prompt_positive": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Additional prompt (e.g., 1girl, beautiful)"
"placeholder": "Additional positive prompt"
}),
"additional_prompt_negative": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Additional negative prompt"
}),
# グループ1
"lora_folder_path_1": ("STRING", {
@@ -98,6 +117,10 @@ class RandomLoRALoader:
"include_subfolders_1": ("BOOLEAN", {
"default": True
}),
"unique_by_filename_1": ("BOOLEAN", {
"default": True,
"label": "Unique by filename (exclude duplicates)"
}),
"model_strength_1": ("STRING", {
"default": "1.0",
"multiline": False,
@@ -124,6 +147,10 @@ class RandomLoRALoader:
"include_subfolders_2": ("BOOLEAN", {
"default": True
}),
"unique_by_filename_2": ("BOOLEAN", {
"default": True,
"label": "Unique by filename (exclude duplicates)"
}),
"model_strength_2": ("STRING", {
"default": "1.0",
"multiline": False,
@@ -150,6 +177,10 @@ class RandomLoRALoader:
"include_subfolders_3": ("BOOLEAN", {
"default": True
}),
"unique_by_filename_3": ("BOOLEAN", {
"default": True,
"label": "Unique by filename (exclude duplicates)"
}),
"model_strength_3": ("STRING", {
"default": "1.0",
"multiline": False,
@@ -169,7 +200,7 @@ class RandomLoRALoader:
}),
# 共通設定
"trigger_word_source": (
["json_combined", "json_random", "json_sample_prompt"],
["json_combined", "json_random", "json_sample_prompt", "metadata"],
{
"default": "json_combined"
}
@@ -184,14 +215,19 @@ class RandomLoRALoader:
}
}
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING")
RETURN_NAMES = ("MODEL", "CLIP", "positive_text", "negative_text", "positive", "negative")
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING", "IMAGE", "STRING")
# 0〜6番はv1.4.0以前と位置・型とも同じ(保存済みワークフローは出力を番号で接続するため)。
# v1.4.0での変更:
# positive_text (2番) … <lora:...> を含まない。positive (CONDITIONING) にエンコード
# される文字列そのもの
# lora_text (7番) … 以前の positive_text の内容(<lora:...> 付き)。記録用
RETURN_NAMES = ("MODEL", "CLIP", "positive_text", "negative_text", "positive", "negative", "preview", "lora_text")
FUNCTION = "load_random_loras"
CATEGORY = "loaders"
def _find_lora_files(self, folder_path, include_subfolders):
"""
指定フォルダ内のLoRAファイル(.safetensors)を検索
指定フォルダ内のLoRAファイル(.safetensors / .pt / .ckpt)を検索
Args:
folder_path: 検索対象フォルダの絶対パス
@@ -204,9 +240,41 @@ class RandomLoRALoader:
print(f"[RandomLoRALoader] フォルダが存在しません: {folder_path}")
return []
pattern = "**/*.safetensors" if include_subfolders else "*.safetensors"
lora_files = glob.glob(os.path.join(folder_path, pattern), recursive=include_subfolders)
lora_files = []
# Filtered版と対象拡張子を揃えている
extensions = ['.safetensors', '.pt', '.ckpt']
if include_subfolders:
# glob の "**" はシンボリックリンクのディレクトリを辿らないため
# os.walk(followlinks=True) に置き換えている。ComfyUI本体の
# フォルダスキャン (folder_paths.recursive_search) と挙動を揃える
# ためで、これがないとリンク先にしか存在しないLoRAが、
# ComfyUIネイティブのドロップダウンには出るのにここでは
# 候補に入らない、という食い違いが起きる。
#
# realpathの集合はリンクを辿る代償。リンクが循環している場合に
# スキャンが無限ループするのを防ぎ、同じフォルダに2本のリンクが
# 張られている場合の二重スキャンも抑止する。
seen_dirs = set()
for root, dirs, files in os.walk(folder_path, followlinks=True):
real_root = os.path.realpath(root)
if real_root in seen_dirs:
dirs[:] = []
continue
seen_dirs.add(real_root)
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)
# ソートして返す。以前は列挙順がファイルシステム任せだったため、
# 同じシードでもマシンが変われば選ばれるLoRAが違っていた。
# ソートすることでシード指定が環境をまたいで再現するようになる。
lora_files = sorted(lora_files)
print(f"[RandomLoRALoader] 検出されたLoRA数: {len(lora_files)}")
return lora_files
@@ -246,6 +314,34 @@ class RandomLoRALoader:
random.shuffle(selected)
return selected
def _unique_by_filename(self, lora_files, group_name=""):
"""
ファイル名でユニーク化(重複ファイル名を除外)
Args:
lora_files: LoRAファイルパスのリスト
group_name: グループ名(ログ用)
Returns:
list: ファイル名がユニークなファイルパスのリスト
"""
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"[RandomLoRALoader] {group_name}: Duplicate filename detected: {filename}")
print(f" Keeping: {seen_names[filename]}")
print(f" Skipping: {file_path}")
return unique_files
def _parse_strength(self, strength_str):
"""
強度文字列をパースして値を返す
@@ -255,6 +351,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: 強度文字列
@@ -262,18 +359,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:
@@ -320,27 +418,125 @@ class RandomLoRALoader:
def _load_json_metadata(self, lora_path):
"""
外部JSONファイルからメタデータを読み込む
外部JSONファイルまたはLoRA埋め込みメタデータを読み込む
優先順位:
1. {filename}.metadata.json (ComfyUI Lora Manager形式)
2. {filename}.info (Civitai Helper形式)
3. LoRA本体ファイルの埋め込みメタデータ
Args:
lora_path: LoRAファイルパス (.safetensors / .pt / .ckpt)
Returns:
dict: JSONデータ(読み込み失敗時はNone)
"""
# 拡張子を除いたファイル名を取得
base_name = os.path.splitext(lora_path)[0]
# 優先順位1: .metadata.json (ComfyUI Lora Manager)
json_path_metadata = f"{base_name}.metadata.json"
if os.path.exists(json_path_metadata):
try:
with open(json_path_metadata, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"[RandomLoRALoader] JSON読み込みエラー ({json_path_metadata}): {e}")
# 優先順位2: .info (Civitai Helper)
json_path_info = f"{base_name}.info"
if os.path.exists(json_path_info):
try:
with open(json_path_info, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"[RandomLoRALoader] JSON読み込みエラー ({json_path_info}): {e}")
# 優先順位3: LoRA本体ファイルの埋め込みメタデータ
embedded_data = self._load_embedded_metadata(lora_path)
if embedded_data:
return embedded_data
# どれも見つからない
print(f"[RandomLoRALoader] メタデータが見つかりません: {base_name}")
return None
def _load_embedded_metadata(self, lora_path):
"""
LoRA本体ファイルの埋め込みメタデータを読み込む
Args:
lora_path: LoRAファイルパス (.safetensors)
Returns:
dict: JSONデータ(読み込み失敗時はNone)
dict: メタデータ(読み込み失敗時はNone)
"""
# .safetensorsを除いたファイル名を取得
base_name = os.path.splitext(lora_path)[0]
json_path = f"{base_name}.metadata.json"
if not SAFETENSORS_AVAILABLE:
return None
if not os.path.exists(json_path):
print(f"[RandomLoRALoader] JSONファイルが見つかりません: {json_path}")
# safe_open は .safetensors しか読めない。.pt/.ckpt も候補に含むように
# なったため、ここで弾いておかないと選択のたびにエラーログが出る
if not lora_path.lower().endswith(".safetensors"):
return None
if not os.path.exists(lora_path):
return None
try:
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
with safe_open(lora_path, framework="pt", device="cpu") as f:
metadata = f.metadata()
if not metadata:
return None
# ss_tag_frequency(kohya_ss形式)からトリガーワード抽出
if "ss_tag_frequency" in metadata:
tag_freq_str = metadata.get("ss_tag_frequency", "{}")
try:
tag_freq = json.loads(tag_freq_str)
# 最も頻度の高いタグセットを取得
if tag_freq:
# 各データセットのタグを結合
all_tags = []
for dataset_tags in tag_freq.values():
all_tags.extend(dataset_tags.keys())
# 重複除去
unique_tags = list(dict.fromkeys(all_tags))
# Civitai形式に変換
trigger_words = ", ".join(unique_tags[:20]) # 上位20個
return {
"civitai": {
"trainedWords": [trigger_words]
}
}
except json.JSONDecodeError:
pass
# その他のメタデータフィールドからトリガーワード抽出
if "modelspec.trigger_word" in metadata:
trigger = metadata["modelspec.trigger_word"]
return {
"civitai": {
"trainedWords": [trigger]
}
}
# ss_output_name(モデル名)
if "ss_output_name" in metadata:
output_name = metadata["ss_output_name"]
return {
"civitai": {
"trainedWords": [output_name]
}
}
return None
except Exception as e:
print(f"[RandomLoRALoader] JSON読み込みエラー ({json_path}): {e}")
print(f"[RandomLoRALoader] 埋め込みメタデータ読み込みエラー ({lora_path}): {e}")
return None
def _get_trigger_words_combined(self, lora_path):
@@ -434,8 +630,8 @@ class RandomLoRALoader:
print(f"[RandomLoRALoader] imagesが見つかりません: {lora_path}")
return "", ""
# metaを持つ画像のみをフィルタ
valid_images = [img for img in images if "meta" in img]
# metaを持つ画像のみをフィルタ(metaがNoneでないことも確認)
valid_images = [img for img in images if "meta" in img and img["meta"] is not None]
if not valid_images:
print(f"[RandomLoRALoader] metaを持つ画像が見つかりません: {lora_path}")
return "", ""
@@ -463,6 +659,29 @@ class RandomLoRALoader:
return positive, negative
def _get_trigger_words_from_embedded(self, lora_path):
"""
LoRA埋め込みメタデータから直接トリガーワードを取得
外部JSONファイルを無視して、LoRA本体ファイルのみを参照
Args:
lora_path: LoRAファイルパス
Returns:
str: トリガーワード
"""
embedded_data = self._load_embedded_metadata(lora_path)
if not embedded_data:
return ""
# civitai.trainedWordsを取得(_load_embedded_metadataで変換済み)
trained_words = embedded_data.get("civitai", {}).get("trainedWords", [])
if not trained_words:
return ""
# 最初のパターンを使用(埋め込みデータは通常1つ)
return trained_words[0] if trained_words else ""
def _load_lora(self, model, clip, lora_path, model_strength, clip_strength):
"""
LoRAをMODELとCLIPに適用
@@ -563,13 +782,284 @@ class RandomLoRALoader:
text = re.sub(r'<lora:[^>]+>', '', text)
# 連続カンマを整理
text = re.sub(r',\s*,', ',', text)
text = re.sub(r',(\s*,)+', ',', text) # 3つ以上連続しても1回で詰める
# 行頭・行末の空白とカンマを削除
text = text.strip().strip(',').strip()
return 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:
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"[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に統一、パディング)
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 _encode_prompt(self, clip, text, token_normalization, weight_interpretation):
"""
プロンプトをCLIPでエンコードしてCONDITIONINGを生成
@@ -607,22 +1097,26 @@ class RandomLoRALoader:
clip,
token_normalization,
weight_interpretation,
additional_prompt,
additional_prompt_positive,
additional_prompt_negative,
# グループ1
lora_folder_path_1,
include_subfolders_1,
unique_by_filename_1,
model_strength_1,
clip_strength_1,
num_loras_1,
# グループ2
lora_folder_path_2,
include_subfolders_2,
unique_by_filename_2,
model_strength_2,
clip_strength_2,
num_loras_2,
# グループ3
lora_folder_path_3,
include_subfolders_3,
unique_by_filename_3,
model_strength_3,
clip_strength_3,
num_loras_3,
@@ -634,7 +1128,7 @@ class RandomLoRALoader:
メイン処理:ランダムLoRA選択・適用(3グループ対応)
Returns:
tuple: (MODEL, CLIP, positive_text_output, negative_text_output, positive_conditioning, negative_conditioning)
tuple: (MODEL, CLIP, lora_text_output, negative_text_output, positive_conditioning, negative_conditioning)
"""
# seedをそのまま使用(ComfyUIのcontrol_before_generateで制御される)
print(f"[RandomLoRALoader] 使用seed: {seed}")
@@ -646,14 +1140,17 @@ class RandomLoRALoader:
all_positive_parts = []
all_negative_parts = []
# プレビュー画像収集
preview_images = []
# 3つのグループを処理
groups = [
(lora_folder_path_1, include_subfolders_1, model_strength_1, clip_strength_1, num_loras_1, "Group 1"),
(lora_folder_path_2, include_subfolders_2, model_strength_2, clip_strength_2, num_loras_2, "Group 2"),
(lora_folder_path_3, include_subfolders_3, model_strength_3, clip_strength_3, num_loras_3, "Group 3"),
(lora_folder_path_1, include_subfolders_1, unique_by_filename_1, model_strength_1, clip_strength_1, num_loras_1, "Group 1"),
(lora_folder_path_2, include_subfolders_2, unique_by_filename_2, model_strength_2, clip_strength_2, num_loras_2, "Group 2"),
(lora_folder_path_3, include_subfolders_3, unique_by_filename_3, model_strength_3, clip_strength_3, num_loras_3, "Group 3"),
]
for folder_path, include_subs, model_str, clip_str, num, group_name in groups:
for folder_path, include_subs, unique_by_name, model_str, clip_str, num, group_name in groups:
# フォルダパスが空、またはnum_lorasが0の場合はスキップ
if not folder_path.strip() or num == 0:
print(f"[RandomLoRALoader] {group_name}: スキップ(フォルダ未指定またはnum=0)")
@@ -666,6 +1163,13 @@ class RandomLoRALoader:
print(f"[RandomLoRALoader] {group_name}: LoRAファイルが見つかりませんでした")
continue
# ファイル名でユニーク化(重複ファイル名を除外)
if unique_by_name:
lora_files = self._unique_by_filename(lora_files, group_name)
if not lora_files:
print(f"[RandomLoRALoader] {group_name}: ユニーク化後にファイルがありません")
continue
# ランダムにLoRAを選択(全グループ共通のseedを使用)
selected_loras = self._select_random_loras(lora_files, num, seed)
@@ -702,39 +1206,57 @@ class RandomLoRALoader:
all_negative_parts.append(negative_prompt)
trigger_display = positive_prompt
elif trigger_word_source == "metadata":
trigger_words = self._get_trigger_words_from_embedded(lora_path)
all_positive_parts.append(trigger_words)
trigger_display = trigger_words
# テキスト出力に追加(末尾にカンマを付ける)
lora_notation = f"<lora:{lora_name}:{actual_model_str}:{actual_clip_str}>"
all_text_parts.append(f"{lora_notation}, {trigger_display},")
# プレビュー画像を読み込み
preview = self._load_preview_image_as_tensor(lora_path)
if preview is not None:
preview_images.append(preview)
# テキスト出力を結合(追加プロンプトを先頭に配置、末尾にカンマを付ける)
if additional_prompt.strip():
if additional_prompt_positive.strip():
# 追加プロンプトの末尾にカンマがなければ追加
additional_with_comma = additional_prompt.strip()
additional_with_comma = additional_prompt_positive.strip()
if not additional_with_comma.endswith(','):
additional_with_comma += ','
if all_text_parts:
# LoRAがある場合: 追加プロンプト + LoRA情報
positive_text_output = additional_with_comma + "\n" + "\n".join(all_text_parts)
lora_text_output = additional_with_comma + "\n" + "\n".join(all_text_parts)
else:
# LoRAがない場合: 追加プロンプトのみ(末尾カンマなし)
positive_text_output = additional_prompt.strip()
lora_text_output = additional_prompt_positive.strip()
else:
# 追加プロンプトなし
if all_text_parts:
# LoRAのみ
positive_text_output = "\n".join(all_text_parts)
lora_text_output = "\n".join(all_text_parts)
else:
# 何もない
positive_text_output = ""
lora_text_output = ""
# negativeテキスト出力(json_sample_promptの場合のみ内容あり)
negative_text_output = ", ".join([n for n in all_negative_parts if n])
# negativeテキスト出力
# 1. json_sample_promptからのnegative
sample_negative = ", ".join([n for n in all_negative_parts if n])
# 2. additional_prompt_negativeと結合
negative_parts = []
if additional_prompt_negative.strip():
negative_parts.append(additional_prompt_negative.strip())
if sample_negative:
negative_parts.append(sample_negative)
negative_text_output = ", ".join(negative_parts)
# positiveプロンプトを結合してCONDITIONINGを生成(追加プロンプトを含める)
final_positive_parts = []
if additional_prompt.strip():
if additional_prompt_positive.strip():
# LoRA構文を削除してクリーンなプロンプトにする
cleaned_prompt = self._remove_lora_syntax(additional_prompt.strip())
cleaned_prompt = self._remove_lora_syntax(additional_prompt_positive.strip())
if cleaned_prompt: # 削除後に空でなければ追加
final_positive_parts.append(cleaned_prompt)
final_positive_parts.extend([p for p in all_positive_parts if p])
@@ -744,7 +1266,13 @@ class RandomLoRALoader:
)
# negativeプロンプトを結合してCONDITIONINGを生成
negative_text = ", ".join([n for n in all_negative_parts if n])
final_negative_parts = []
if additional_prompt_negative.strip():
cleaned_negative = self._remove_lora_syntax(additional_prompt_negative.strip())
if cleaned_negative:
final_negative_parts.append(cleaned_negative)
final_negative_parts.extend([n for n in all_negative_parts if n])
negative_text = ", ".join(final_negative_parts)
negative_conditioning = self._encode_prompt(
clip, negative_text, token_normalization, weight_interpretation
)
@@ -752,19 +1280,14 @@ class RandomLoRALoader:
total_loras = len(all_text_parts)
if total_loras > 0:
print(f"[RandomLoRALoader] 適用完了: 合計{total_loras}個のLoRA")
elif additional_prompt.strip():
elif additional_prompt_positive.strip():
print(f"[RandomLoRALoader] 適用完了: LoRAなし、追加プロンプトのみ使用")
else:
print(f"[RandomLoRALoader] 適用完了: LoRAなし、プロンプトなし")
return (model, clip, positive_text_output, negative_text_output, positive_conditioning, negative_conditioning)
# ComfyUIへのノード登録
NODE_CLASS_MAPPINGS = {
"RandomLoRALoader": RandomLoRALoader
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RandomLoRALoader": "Random LoRA Loader"
}
# プレビュー画像バッチ生成
preview_batch = self._generate_preview_batch(preview_images)
# positive_text = positive にエンコードした文字列そのもの(<lora:...> なし)
return (model, clip, positive_text, negative_text_output, positive_conditioning,
negative_conditioning, preview_batch, lora_text_output)