Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e1cf9b518 | ||
|
|
37a2cfe7da | ||
|
|
3b0a271b39 | ||
|
|
801b13a0d7 | ||
|
|
83775df239 | ||
|
|
d9adf51c55 | ||
|
|
57f511c06f | ||
|
|
69450cf7c6 | ||
|
|
dde9ca4d1e | ||
|
|
409c3044e3 | ||
|
|
62714a22b8 | ||
|
|
06b7d8f6a1 | ||
|
|
6e4c67457f | ||
|
|
b6f815deff |
@@ -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 }}
|
||||||
@@ -4,13 +4,41 @@ __pycache__/
|
|||||||
*$py.class
|
*$py.class
|
||||||
*.so
|
*.so
|
||||||
.Python
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
# IDE
|
# IDEs
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.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
|
||||||
|
|||||||
@@ -2,6 +2,137 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [1.1.0] - 2026-01-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -63,7 +194,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
|
### Initial Release
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,614 @@
|
|||||||
|
# Filtered Random LoRA Loader (LBW)
|
||||||
|
|
||||||
|
**LoRA Block Weight (LBW) support with automatic SD1.5/SDXL detection**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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!** 🎨✨
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
# Filtered Random LoRA Loader (LBW)
|
||||||
|
|
||||||
|
**LoRAブロックウェイト(LBW)対応、SD1.5/SDXL自動検出付き**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
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制御を楽しんでください!** 🎨✨
|
||||||
@@ -6,14 +6,27 @@ RandomLoRALoader Custom Node for ComfyUI
|
|||||||
from .random_lora_loader import RandomLoRALoader
|
from .random_lora_loader import RandomLoRALoader
|
||||||
from .filtered_random_lora_loader import FilteredRandomLoRALoader
|
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 = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"RandomLoRALoader": RandomLoRALoader,
|
"RandomLoRALoader": RandomLoRALoader,
|
||||||
"FilteredRandomLoRALoader": FilteredRandomLoRALoader
|
"FilteredRandomLoRALoader": FilteredRandomLoRALoader,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"RandomLoRALoader": "Random LoRA Loader",
|
"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']
|
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class FilteredRandomLoRALoader:
|
|||||||
|
|
||||||
# クラス変数(全インスタンスで共有するメタデータキャッシュ)
|
# クラス変数(全インスタンスで共有するメタデータキャッシュ)
|
||||||
_metadata_cache = {}
|
_metadata_cache = {}
|
||||||
|
_opencv_warning_shown = False # opencv警告表示フラグ
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -78,7 +79,9 @@ class FilteredRandomLoRALoader:
|
|||||||
"multiline": False,
|
"multiline": False,
|
||||||
"placeholder": "Keywords (space-separated, e.g., 'style anime' or \"anime style\" red)"
|
"placeholder": "Keywords (space-separated, e.g., 'style anime' or \"anime style\" red)"
|
||||||
}),
|
}),
|
||||||
"filter_mode": (["AND", "OR"], {
|
# OFF(v1.5.0): キーワードを残したままフィルタだけ無効にする。末尾に追加。
|
||||||
|
# 保存済みワークフローは値("AND"/"OR")で保存されているので影響なし
|
||||||
|
"filter_mode": (["AND", "OR", "OFF"], {
|
||||||
"default": "AND"
|
"default": "AND"
|
||||||
}),
|
}),
|
||||||
"search_in_metadata": ("BOOLEAN", {
|
"search_in_metadata": ("BOOLEAN", {
|
||||||
@@ -119,8 +122,16 @@ class FilteredRandomLoRALoader:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING", "IMAGE")
|
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING", "IMAGE", "STRING", "STRING")
|
||||||
RETURN_NAMES = ("MODEL", "CLIP", "positive_text", "negative_text", "positive", "negative", "preview")
|
# 0〜6番はv1.4.0以前と位置・型とも同じ(保存済みワークフローは出力を番号で接続するため)。
|
||||||
|
# v1.4.0での変更:
|
||||||
|
# positive_text (2番) … <lora:...> を含まない。positive (CONDITIONING) にエンコード
|
||||||
|
# される文字列そのもの
|
||||||
|
# lora_text (7番) … 以前の positive_text の内容(<lora:...> 付き)。記録用
|
||||||
|
# v1.5.0での追加:
|
||||||
|
# keyword (8番) … 選ばれたLoRAが実際にマッチしたキーワードを "_" で連結したもの
|
||||||
|
# (_build_keyword_text 参照)。新しいピンは常に末尾に足す
|
||||||
|
RETURN_NAMES = ("MODEL", "CLIP", "positive_text", "negative_text", "positive", "negative", "preview", "lora_text", "keyword")
|
||||||
FUNCTION = "load_loras"
|
FUNCTION = "load_loras"
|
||||||
CATEGORY = "loaders"
|
CATEGORY = "loaders"
|
||||||
|
|
||||||
@@ -263,8 +274,14 @@ class FilteredRandomLoRALoader:
|
|||||||
# プレビュー画像バッチ生成
|
# プレビュー画像バッチ生成
|
||||||
preview_batch = self._generate_preview_batch(preview_images)
|
preview_batch = self._generate_preview_batch(preview_images)
|
||||||
|
|
||||||
|
# keyword 出力(選ばれたLoRAが実際にマッチしたキーワード)
|
||||||
|
keyword_text = self._build_keyword_text(selected_loras, keyword_filter, filter_mode, search_in_metadata)
|
||||||
|
if keyword_text:
|
||||||
|
print(f"[FilteredRandomLoRALoader] Matched keyword: {keyword_text}")
|
||||||
|
|
||||||
return self._generate_outputs(model, clip, final_positive, final_negative,
|
return self._generate_outputs(model, clip, final_positive, final_negative,
|
||||||
token_normalization, weight_interpretation, preview_batch)
|
token_normalization, weight_interpretation, preview_batch,
|
||||||
|
keyword_text)
|
||||||
|
|
||||||
def _find_lora_files(self, folder_path, include_subfolders):
|
def _find_lora_files(self, folder_path, include_subfolders):
|
||||||
"""フォルダ内のLoRAファイルを検索"""
|
"""フォルダ内のLoRAファイルを検索"""
|
||||||
@@ -276,7 +293,22 @@ class FilteredRandomLoRALoader:
|
|||||||
extensions = ['.safetensors', '.pt', '.ckpt']
|
extensions = ['.safetensors', '.pt', '.ckpt']
|
||||||
|
|
||||||
if include_subfolders:
|
if include_subfolders:
|
||||||
for root, dirs, files in os.walk(folder_path):
|
# followlinks=True はComfyUI本体のフォルダスキャン
|
||||||
|
# (folder_paths.recursive_search) と同じ挙動。これがないと
|
||||||
|
# シンボリックリンクのサブフォルダが丸ごとスキップされ、
|
||||||
|
# ComfyUIネイティブのLoRAドロップダウンには出てくるのに
|
||||||
|
# ここでは候補に入らない、という食い違いが起きる。
|
||||||
|
#
|
||||||
|
# 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:
|
for file in files:
|
||||||
if any(file.lower().endswith(ext) for ext in extensions):
|
if any(file.lower().endswith(ext) for ext in extensions):
|
||||||
lora_files.append(os.path.join(root, file))
|
lora_files.append(os.path.join(root, file))
|
||||||
@@ -286,33 +318,74 @@ class FilteredRandomLoRALoader:
|
|||||||
if os.path.isfile(file_path) and any(file.lower().endswith(ext) for ext in extensions):
|
if os.path.isfile(file_path) and any(file.lower().endswith(ext) for ext in extensions):
|
||||||
lora_files.append(file_path)
|
lora_files.append(file_path)
|
||||||
|
|
||||||
return lora_files
|
# ソートして返す。以前は列挙順がファイルシステム任せだったため、
|
||||||
|
# 同じシードでもマシンが変われば選ばれるLoRAが違っていた。
|
||||||
|
# ソートすることでシード指定が環境をまたいで再現するようになる。
|
||||||
|
return sorted(lora_files)
|
||||||
|
|
||||||
def _parse_keywords(self, keyword_filter):
|
def _split_keywords(self, keyword_filter):
|
||||||
"""
|
"""
|
||||||
キーワードをパース(スペース区切り、"..."でフレーズ対応)
|
キーワードを入力どおりの大文字小文字で分割(スペース区切り、"..."でフレーズ対応)
|
||||||
|
|
||||||
例:
|
例:
|
||||||
"anime style" → ["anime", "style"]
|
anime style → ["anime", "style"]
|
||||||
"anime style" red → ["anime style", "red"]
|
"anime style" red → ["anime style", "red"]
|
||||||
"anime style" "vibrant colors" → ["anime style", "vibrant colors"]
|
"anime style" "vibrant colors" → ["anime style", "vibrant colors"]
|
||||||
"""
|
"""
|
||||||
if not keyword_filter.strip():
|
if not keyword_filter.strip():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# ダブルクォート内のフレーズを抽出
|
# ダブルクォート内のフレーズを抽出
|
||||||
pattern = r'"([^"]+)"|(\S+)'
|
pattern = r'"([^"]+)"|(\S+)'
|
||||||
matches = re.findall(pattern, keyword_filter)
|
matches = re.findall(pattern, keyword_filter)
|
||||||
|
|
||||||
# (quoted, unquoted) のタプルから値を取得
|
# (quoted, unquoted) のタプルから値を取得
|
||||||
keywords = [m[0] if m[0] else m[1] for m in matches]
|
return [m[0] if m[0] else m[1] for m in matches if m[0] or m[1]]
|
||||||
|
|
||||||
|
def _parse_keywords(self, keyword_filter):
|
||||||
|
"""フィルタ用のキーワード(大文字小文字を区別しないので小文字化)"""
|
||||||
|
return [kw.lower() for kw in self._split_keywords(keyword_filter)]
|
||||||
|
|
||||||
|
def _build_keyword_text(self, selected_loras, keyword_filter, filter_mode, search_in_metadata):
|
||||||
|
"""
|
||||||
|
keyword 出力用の文字列。選ばれたLoRAが実際にマッチしたキーワードを返す
|
||||||
|
|
||||||
# 小文字化
|
キーワードは入力順・重複なしで "_" 連結。フレーズ内のスペースも "_" にする
|
||||||
keywords = [kw.lower() for kw in keywords if kw]
|
("aaa bbb" → aaa_bbb)。大文字小文字は入力のまま。マッチ判定の対象は
|
||||||
|
_filter_lora_files と同じ(ファイル名、search_in_metadata がONならメタデータも)。
|
||||||
|
|
||||||
return keywords
|
AND: 選ばれたLoRAは全キーワードにマッチしているので全部出る
|
||||||
|
(aaa bbb → aaa_bbb、"aaa bbb" ccc → aaa_bbb_ccc)
|
||||||
|
OR : ヒットしたものだけ出る(aaa または bbb)。1つのLoRAが両方を含めば
|
||||||
|
aaa_bbb、num_loras が2以上なら選ばれた全LoRAのヒットをまとめる
|
||||||
|
filter_mode が OFF、keyword_filter が空、LoRAが1つも選ばれなかった場合は空文字。
|
||||||
|
"""
|
||||||
|
if filter_mode == "OFF":
|
||||||
|
return ""
|
||||||
|
keywords = self._split_keywords(keyword_filter)
|
||||||
|
if not keywords or not selected_loras:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
for lora_path in dict.fromkeys(selected_loras):
|
||||||
|
target = os.path.splitext(os.path.basename(lora_path))[0].lower()
|
||||||
|
if search_in_metadata:
|
||||||
|
metadata_keywords = self._get_metadata_keywords(lora_path)
|
||||||
|
if metadata_keywords:
|
||||||
|
target = f"{target} {metadata_keywords}"
|
||||||
|
targets.append(target)
|
||||||
|
|
||||||
|
hits, seen = [], set()
|
||||||
|
for kw in keywords:
|
||||||
|
key = kw.lower()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
if any(key in t for t in targets):
|
||||||
|
label = re.sub(r"\s+", "_", kw.strip())
|
||||||
|
if label:
|
||||||
|
hits.append(label)
|
||||||
|
return "_".join(hits)
|
||||||
|
|
||||||
def _unique_by_filename(self, lora_files):
|
def _unique_by_filename(self, lora_files):
|
||||||
"""
|
"""
|
||||||
@@ -347,7 +420,7 @@ class FilteredRandomLoRALoader:
|
|||||||
|
|
||||||
def _filter_lora_files(self, lora_files, keyword_filter, filter_mode="OR", search_in_metadata=False):
|
def _filter_lora_files(self, lora_files, keyword_filter, filter_mode="OR", search_in_metadata=False):
|
||||||
"""キーワードでLoRAファイルをフィルタリング"""
|
"""キーワードでLoRAファイルをフィルタリング"""
|
||||||
if not keyword_filter.strip():
|
if filter_mode == "OFF" or not keyword_filter.strip():
|
||||||
return lora_files # フィルタなし = 全ファイル
|
return lora_files # フィルタなし = 全ファイル
|
||||||
|
|
||||||
# キーワードをパース(スペース区切り、"..."でフレーズ対応)
|
# キーワードをパース(スペース区切り、"..."でフレーズ対応)
|
||||||
@@ -517,6 +590,11 @@ class FilteredRandomLoRALoader:
|
|||||||
|
|
||||||
def _load_embedded_metadata(self, lora_path):
|
def _load_embedded_metadata(self, lora_path):
|
||||||
"""LoRAファイルから埋め込みメタデータを読み込み(Civitai形式に変換)"""
|
"""LoRAファイルから埋め込みメタデータを読み込み(Civitai形式に変換)"""
|
||||||
|
# safe_open は .safetensors しか読めない。候補には .pt/.ckpt も
|
||||||
|
# 含まれるため、ここで弾いておかないと選択のたびに無駄な例外が出る
|
||||||
|
if not lora_path.lower().endswith(".safetensors"):
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from safetensors.torch import safe_open
|
from safetensors.torch import safe_open
|
||||||
|
|
||||||
@@ -654,26 +732,33 @@ class FilteredRandomLoRALoader:
|
|||||||
|
|
||||||
def _remove_lora_syntax(self, text):
|
def _remove_lora_syntax(self, text):
|
||||||
"""プロンプトからLoRA構文を削除"""
|
"""プロンプトからLoRA構文を削除"""
|
||||||
pattern = r'<lora:[^>]+>'
|
if not text:
|
||||||
return re.sub(pattern, '', text)
|
return text
|
||||||
|
text = re.sub(r'<lora:[^>]+>', '', text)
|
||||||
|
# v1.4.0: 構文を消した跡に残る ", ," を詰める(以前はそのままエンコードしていた)。
|
||||||
|
# positive_text 出力として表に出るようになったため、3フォルダ版と同じ後処理に揃えた
|
||||||
|
text = re.sub(r',(\s*,)+', ',', text)
|
||||||
|
return text.strip().strip(',').strip()
|
||||||
|
|
||||||
def _load_preview_image_as_tensor(self, lora_path):
|
def _load_preview_image_as_tensor(self, lora_path):
|
||||||
"""
|
"""
|
||||||
プレビュー画像をTensorとして読み込み(部分一致、長辺1240px)
|
プレビュー画像をTensorとして読み込み(部分一致、長辺1240px)
|
||||||
|
|
||||||
検索方法:
|
検索方法:
|
||||||
LoRAファイル名(拡張子除く)で始まる画像ファイルを検索
|
LoRAファイル名(拡張子除く)で始まるファイルを検索
|
||||||
|
優先順位:
|
||||||
|
1. 静止画像 (.png, .jpg, .jpeg, .webp)
|
||||||
|
2. 動画ファイル (.gif, .webp, .mp4, .webm) の1フレーム目
|
||||||
|
|
||||||
例: style_anime_v1.safetensors
|
例: style_anime_v1.safetensors
|
||||||
→ style_anime_v1.png
|
→ style_anime_v1.png
|
||||||
→ style_anime_v1_preview.png
|
→ style_anime_v1_preview.jpg
|
||||||
→ style_anime_v1_001.jpg
|
→ style_anime_v1.gif (1フレーム目)
|
||||||
|
→ style_anime_v1.mp4 (1フレーム目、opencv-pythonが必要)
|
||||||
→ STYLE_ANIME_V1.PNG (大文字小文字無視)
|
→ STYLE_ANIME_V1.PNG (大文字小文字無視)
|
||||||
|
|
||||||
リサイズ:
|
リサイズ:
|
||||||
長辺を1240pxに統一(アスペクト比保持)
|
長辺を1240pxに統一(アスペクト比保持)
|
||||||
例: 1024x768 → 1240x930
|
|
||||||
512x512 → 1240x1240
|
|
||||||
800x1200 → 826x1240
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
torch.Tensor: (H, W, C) 形式、見つからない場合はNone
|
torch.Tensor: (H, W, C) 形式、見つからない場合はNone
|
||||||
@@ -690,70 +775,207 @@ class FilteredRandomLoRALoader:
|
|||||||
base_name = os.path.splitext(os.path.basename(lora_path))[0]
|
base_name = os.path.splitext(os.path.basename(lora_path))[0]
|
||||||
folder = os.path.dirname(lora_path)
|
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:
|
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().startswith(base_name.lower()):
|
||||||
# 画像ファイルか確認
|
matched_files.append(file)
|
||||||
if file.lower().endswith(image_extensions):
|
|
||||||
preview_path = os.path.join(folder, file)
|
if not matched_files:
|
||||||
try:
|
return None
|
||||||
img = Image.open(preview_path).convert('RGB')
|
|
||||||
|
# 優先順位でソート
|
||||||
# 長辺を1240pxにリサイズ(アスペクト比保持)
|
def get_priority(filename):
|
||||||
width, height = img.size
|
lower = filename.lower()
|
||||||
max_size = 1240
|
if any(lower.endswith(ext) for ext in static_image_exts):
|
||||||
|
return 0 # 最優先
|
||||||
if max(width, height) > max_size:
|
elif any(lower.endswith(ext) for ext in animated_image_exts):
|
||||||
# 長辺が1240pxを超える場合はリサイズ
|
return 1 # 次優先
|
||||||
if width > height:
|
elif any(lower.endswith(ext) for ext in video_exts):
|
||||||
new_width = max_size
|
return 2 # 最後
|
||||||
new_height = int(height * (max_size / width))
|
else:
|
||||||
else:
|
return 999 # その他
|
||||||
new_height = max_size
|
|
||||||
new_width = int(width * (max_size / height))
|
matched_files.sort(key=get_priority)
|
||||||
|
|
||||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
# 各ファイルを試す
|
||||||
elif max(width, height) < max_size:
|
for file in matched_files:
|
||||||
# 長辺が1240px未満の場合は拡大
|
preview_path = os.path.join(folder, file)
|
||||||
if width > height:
|
lower_file = file.lower()
|
||||||
new_width = max_size
|
|
||||||
new_height = int(height * (max_size / width))
|
# 静止画像
|
||||||
else:
|
if any(lower_file.endswith(ext) for ext in static_image_exts):
|
||||||
new_height = max_size
|
img = self._load_static_image(preview_path)
|
||||||
new_width = int(width * (max_size / height))
|
if img is not None:
|
||||||
|
return img
|
||||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
||||||
|
# アニメーション画像(GIF/WebP)
|
||||||
# numpy配列に変換 (H, W, C)
|
elif any(lower_file.endswith(ext) for ext in animated_image_exts):
|
||||||
img_array = np.array(img).astype(np.float32) / 255.0
|
img = self._load_animated_image_first_frame(preview_path)
|
||||||
# Tensor化
|
if img is not None:
|
||||||
return torch.from_numpy(img_array)
|
return img
|
||||||
except Exception as e:
|
|
||||||
print(f"[FilteredRandomLoRALoader] Preview load error ({preview_path}): {e}")
|
# 動画ファイル(opencv-python必要)
|
||||||
continue
|
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:
|
except Exception as e:
|
||||||
print(f"[FilteredRandomLoRALoader] Folder read error: {e}")
|
print(f"[FilteredRandomLoRALoader] Folder read error: {e}")
|
||||||
|
|
||||||
return None
|
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):
|
def _get_random_strength(self, strength_str):
|
||||||
"""強度をランダム取得(範囲指定対応)"""
|
"""強度をランダム取得(範囲指定対応、マイナス値・マイナス範囲対応)"""
|
||||||
|
import re
|
||||||
strength_str = strength_str.strip()
|
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:
|
try:
|
||||||
parts = strength_str.split('-')
|
min_val = float(match.group(1))
|
||||||
min_val = float(parts[0].strip())
|
max_val = float(match.group(2))
|
||||||
max_val = float(parts[1].strip())
|
|
||||||
return str(round(random.uniform(min_val, max_val), 1))
|
return str(round(random.uniform(min_val, max_val), 1))
|
||||||
except:
|
except:
|
||||||
return "1.0"
|
return "1.0"
|
||||||
else:
|
else:
|
||||||
|
# 単一値(マイナス含む)
|
||||||
try:
|
try:
|
||||||
float(strength_str)
|
float(strength_str)
|
||||||
return strength_str
|
return strength_str
|
||||||
@@ -832,15 +1054,16 @@ class FilteredRandomLoRALoader:
|
|||||||
return preview_batch
|
return preview_batch
|
||||||
|
|
||||||
def _generate_outputs(self, model, clip, final_positive, final_negative,
|
def _generate_outputs(self, model, clip, final_positive, final_negative,
|
||||||
token_normalization, weight_interpretation, preview_batch):
|
token_normalization, weight_interpretation, preview_batch,
|
||||||
|
keyword_text=""):
|
||||||
"""CONDITIONING生成と出力"""
|
"""CONDITIONING生成と出力"""
|
||||||
|
# LoRA構文を削除してクリーンなプロンプトにする。clean_positive は positive_text
|
||||||
|
# 出力にもなるので、エラー時の戻り値でも使えるよう try の外で作る
|
||||||
|
clean_positive = self._remove_lora_syntax(final_positive) if final_positive else ""
|
||||||
|
clean_negative = self._remove_lora_syntax(final_negative) if final_negative else ""
|
||||||
try:
|
try:
|
||||||
from nodes import CLIPTextEncode
|
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生成
|
# CONDITIONING生成
|
||||||
positive_conditioning = CLIPTextEncode().encode(
|
positive_conditioning = CLIPTextEncode().encode(
|
||||||
clip=clip,
|
clip=clip,
|
||||||
@@ -852,8 +1075,10 @@ class FilteredRandomLoRALoader:
|
|||||||
text=clean_negative
|
text=clean_negative
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
return (model, clip, final_positive, final_negative,
|
# positive_text = エンコードした文字列そのもの / lora_text = <lora:...> 付き(旧 positive_text)
|
||||||
positive_conditioning, negative_conditioning, preview_batch)
|
return (model, clip, clean_positive, final_negative,
|
||||||
|
positive_conditioning, negative_conditioning, preview_batch, final_positive,
|
||||||
|
keyword_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[FilteredRandomLoRALoader] Error generating outputs: {e}")
|
print(f"[FilteredRandomLoRALoader] Error generating outputs: {e}")
|
||||||
# エラー時は黒画像
|
# エラー時は黒画像
|
||||||
@@ -862,7 +1087,8 @@ class FilteredRandomLoRALoader:
|
|||||||
black_image = torch.zeros((1, 1240, 1240, 3), dtype=torch.float32)
|
black_image = torch.zeros((1, 1240, 1240, 3), dtype=torch.float32)
|
||||||
except:
|
except:
|
||||||
black_image = None
|
black_image = None
|
||||||
return (model, clip, final_positive, final_negative, None, None, black_image)
|
return (model, clip, clean_positive, final_negative, None, None, black_image, final_positive,
|
||||||
|
keyword_text)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 80 KiB |
@@ -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 = ""
|
||||||
@@ -77,6 +77,9 @@ except ImportError:
|
|||||||
class RandomLoRALoader:
|
class RandomLoRALoader:
|
||||||
"""ランダムLoRA選択・適用ノード(3グループ対応)"""
|
"""ランダムLoRA選択・適用ノード(3グループ対応)"""
|
||||||
|
|
||||||
|
# クラス変数(opencv警告表示フラグ)
|
||||||
|
_opencv_warning_shown = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
@@ -212,14 +215,19 @@ class RandomLoRALoader:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING", "IMAGE")
|
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING", "CONDITIONING", "CONDITIONING", "IMAGE", "STRING")
|
||||||
RETURN_NAMES = ("MODEL", "CLIP", "positive_text", "negative_text", "positive", "negative", "preview")
|
# 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"
|
FUNCTION = "load_random_loras"
|
||||||
CATEGORY = "loaders"
|
CATEGORY = "loaders"
|
||||||
|
|
||||||
def _find_lora_files(self, folder_path, include_subfolders):
|
def _find_lora_files(self, folder_path, include_subfolders):
|
||||||
"""
|
"""
|
||||||
指定フォルダ内のLoRAファイル(.safetensors)を検索
|
指定フォルダ内のLoRAファイル(.safetensors / .pt / .ckpt)を検索
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
folder_path: 検索対象フォルダの絶対パス
|
folder_path: 検索対象フォルダの絶対パス
|
||||||
@@ -232,9 +240,41 @@ class RandomLoRALoader:
|
|||||||
print(f"[RandomLoRALoader] フォルダが存在しません: {folder_path}")
|
print(f"[RandomLoRALoader] フォルダが存在しません: {folder_path}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
pattern = "**/*.safetensors" if include_subfolders else "*.safetensors"
|
lora_files = []
|
||||||
lora_files = glob.glob(os.path.join(folder_path, pattern), recursive=include_subfolders)
|
# 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)}")
|
print(f"[RandomLoRALoader] 検出されたLoRA数: {len(lora_files)}")
|
||||||
return lora_files
|
return lora_files
|
||||||
|
|
||||||
@@ -311,6 +351,7 @@ class RandomLoRALoader:
|
|||||||
- "0.55" → 0.55(そのまま)
|
- "0.55" → 0.55(そのまま)
|
||||||
- "0.4-0.8" → 0.4, 0.5, 0.6, 0.7, 0.8からランダム(0.1刻み)
|
- "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.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:
|
Args:
|
||||||
strength_str: 強度文字列
|
strength_str: 強度文字列
|
||||||
@@ -318,18 +359,19 @@ class RandomLoRALoader:
|
|||||||
Returns:
|
Returns:
|
||||||
float: 実際に使用する強度値
|
float: 実際に使用する強度値
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
strength_str = str(strength_str).strip()
|
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:
|
try:
|
||||||
parts = strength_str.split('-')
|
|
||||||
if len(parts) != 2:
|
|
||||||
raise ValueError("範囲指定は 'min-max' 形式で入力してください")
|
|
||||||
|
|
||||||
# 範囲の上限下限を1桁に丸める
|
# 範囲の上限下限を1桁に丸める
|
||||||
min_val = round(float(parts[0].strip()), 1)
|
min_val = round(float(match.group(1)), 1)
|
||||||
max_val = round(float(parts[1].strip()), 1)
|
max_val = round(float(match.group(2)), 1)
|
||||||
|
|
||||||
# バリデーション
|
# バリデーション
|
||||||
if min_val < -10.0 or max_val > 10.0:
|
if min_val < -10.0 or max_val > 10.0:
|
||||||
@@ -384,12 +426,12 @@ class RandomLoRALoader:
|
|||||||
3. LoRA本体ファイルの埋め込みメタデータ
|
3. LoRA本体ファイルの埋め込みメタデータ
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
lora_path: LoRAファイルパス (.safetensors)
|
lora_path: LoRAファイルパス (.safetensors / .pt / .ckpt)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: JSONデータ(読み込み失敗時はNone)
|
dict: JSONデータ(読み込み失敗時はNone)
|
||||||
"""
|
"""
|
||||||
# .safetensorsを除いたファイル名を取得
|
# 拡張子を除いたファイル名を取得
|
||||||
base_name = os.path.splitext(lora_path)[0]
|
base_name = os.path.splitext(lora_path)[0]
|
||||||
|
|
||||||
# 優先順位1: .metadata.json (ComfyUI Lora Manager)
|
# 優先順位1: .metadata.json (ComfyUI Lora Manager)
|
||||||
@@ -432,6 +474,11 @@ class RandomLoRALoader:
|
|||||||
if not SAFETENSORS_AVAILABLE:
|
if not SAFETENSORS_AVAILABLE:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# safe_open は .safetensors しか読めない。.pt/.ckpt も候補に含むように
|
||||||
|
# なったため、ここで弾いておかないと選択のたびにエラーログが出る
|
||||||
|
if not lora_path.lower().endswith(".safetensors"):
|
||||||
|
return None
|
||||||
|
|
||||||
if not os.path.exists(lora_path):
|
if not os.path.exists(lora_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -735,7 +782,7 @@ class RandomLoRALoader:
|
|||||||
text = re.sub(r'<lora:[^>]+>', '', text)
|
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()
|
text = text.strip().strip(',').strip()
|
||||||
@@ -747,18 +794,20 @@ class RandomLoRALoader:
|
|||||||
プレビュー画像をTensorとして読み込み(部分一致、長辺1240px)
|
プレビュー画像をTensorとして読み込み(部分一致、長辺1240px)
|
||||||
|
|
||||||
検索方法:
|
検索方法:
|
||||||
LoRAファイル名(拡張子除く)で始まる画像ファイルを検索
|
LoRAファイル名(拡張子除く)で始まるファイルを検索
|
||||||
|
優先順位:
|
||||||
|
1. 静止画像 (.png, .jpg, .jpeg, .webp)
|
||||||
|
2. 動画ファイル (.gif, .webp, .mp4, .webm) の1フレーム目
|
||||||
|
|
||||||
例: style_anime_v1.safetensors
|
例: style_anime_v1.safetensors
|
||||||
→ style_anime_v1.png
|
→ style_anime_v1.png
|
||||||
→ style_anime_v1_preview.png
|
→ style_anime_v1_preview.jpg
|
||||||
→ style_anime_v1_001.jpg
|
→ style_anime_v1.gif (1フレーム目)
|
||||||
|
→ style_anime_v1.mp4 (1フレーム目、opencv-pythonが必要)
|
||||||
→ STYLE_ANIME_V1.PNG (大文字小文字無視)
|
→ STYLE_ANIME_V1.PNG (大文字小文字無視)
|
||||||
|
|
||||||
リサイズ:
|
リサイズ:
|
||||||
長辺を1240pxに統一(アスペクト比保持)
|
長辺を1240pxに統一(アスペクト比保持)
|
||||||
例: 1024x768 → 1240x930
|
|
||||||
512x512 → 1240x1240
|
|
||||||
800x1200 → 826x1240
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
torch.Tensor: (H, W, C) 形式、見つからない場合はNone
|
torch.Tensor: (H, W, C) 形式、見つからない場合はNone
|
||||||
@@ -774,57 +823,189 @@ class RandomLoRALoader:
|
|||||||
base_name = os.path.splitext(os.path.basename(lora_path))[0]
|
base_name = os.path.splitext(os.path.basename(lora_path))[0]
|
||||||
folder = os.path.dirname(lora_path)
|
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:
|
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().startswith(base_name.lower()):
|
||||||
# 画像ファイルか確認
|
matched_files.append(file)
|
||||||
if file.lower().endswith(image_extensions):
|
|
||||||
preview_path = os.path.join(folder, file)
|
if not matched_files:
|
||||||
try:
|
return None
|
||||||
img = Image.open(preview_path).convert('RGB')
|
|
||||||
|
# 優先順位でソート
|
||||||
# 長辺を1240pxにリサイズ(アスペクト比保持)
|
def get_priority(filename):
|
||||||
width, height = img.size
|
lower = filename.lower()
|
||||||
max_size = 1240
|
if any(lower.endswith(ext) for ext in static_image_exts):
|
||||||
|
return 0 # 最優先
|
||||||
if max(width, height) > max_size:
|
elif any(lower.endswith(ext) for ext in animated_image_exts):
|
||||||
# 長辺が1240pxを超える場合はリサイズ
|
return 1 # 次優先
|
||||||
if width > height:
|
elif any(lower.endswith(ext) for ext in video_exts):
|
||||||
new_width = max_size
|
return 2 # 最後
|
||||||
new_height = int(height * (max_size / width))
|
else:
|
||||||
else:
|
return 999 # その他
|
||||||
new_height = max_size
|
|
||||||
new_width = int(width * (max_size / height))
|
matched_files.sort(key=get_priority)
|
||||||
|
|
||||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
# 各ファイルを試す
|
||||||
elif max(width, height) < max_size:
|
for file in matched_files:
|
||||||
# 長辺が1240px未満の場合は拡大
|
preview_path = os.path.join(folder, file)
|
||||||
if width > height:
|
lower_file = file.lower()
|
||||||
new_width = max_size
|
|
||||||
new_height = int(height * (max_size / width))
|
# 静止画像
|
||||||
else:
|
if any(lower_file.endswith(ext) for ext in static_image_exts):
|
||||||
new_height = max_size
|
img = self._load_static_image(preview_path)
|
||||||
new_width = int(width * (max_size / height))
|
if img is not None:
|
||||||
|
return img
|
||||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
||||||
|
# アニメーション画像(GIF/WebP)
|
||||||
# numpy配列に変換 (H, W, C)
|
elif any(lower_file.endswith(ext) for ext in animated_image_exts):
|
||||||
img_array = np.array(img).astype(np.float32) / 255.0
|
img = self._load_animated_image_first_frame(preview_path)
|
||||||
# Tensor化
|
if img is not None:
|
||||||
return torch.from_numpy(img_array)
|
return img
|
||||||
except Exception as e:
|
|
||||||
print(f"[RandomLoRALoader] Preview load error ({preview_path}): {e}")
|
# 動画ファイル(opencv-python必要)
|
||||||
continue
|
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:
|
except Exception as e:
|
||||||
print(f"[RandomLoRALoader] Folder read error: {e}")
|
print(f"[RandomLoRALoader] Folder read error: {e}")
|
||||||
|
|
||||||
return None
|
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):
|
def _generate_preview_batch(self, preview_images):
|
||||||
"""
|
"""
|
||||||
プレビュー画像のバッチを生成(1240pxに統一、パディング)
|
プレビュー画像のバッチを生成(1240pxに統一、パディング)
|
||||||
@@ -947,7 +1128,7 @@ class RandomLoRALoader:
|
|||||||
メイン処理:ランダムLoRA選択・適用(3グループ対応)
|
メイン処理:ランダムLoRA選択・適用(3グループ対応)
|
||||||
|
|
||||||
Returns:
|
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で制御される)
|
# seedをそのまま使用(ComfyUIのcontrol_before_generateで制御される)
|
||||||
print(f"[RandomLoRALoader] 使用seed: {seed}")
|
print(f"[RandomLoRALoader] 使用seed: {seed}")
|
||||||
@@ -1047,18 +1228,18 @@ class RandomLoRALoader:
|
|||||||
additional_with_comma += ','
|
additional_with_comma += ','
|
||||||
if all_text_parts:
|
if all_text_parts:
|
||||||
# LoRAがある場合: 追加プロンプト + LoRA情報
|
# 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:
|
else:
|
||||||
# LoRAがない場合: 追加プロンプトのみ(末尾カンマなし)
|
# LoRAがない場合: 追加プロンプトのみ(末尾カンマなし)
|
||||||
positive_text_output = additional_prompt_positive.strip()
|
lora_text_output = additional_prompt_positive.strip()
|
||||||
else:
|
else:
|
||||||
# 追加プロンプトなし
|
# 追加プロンプトなし
|
||||||
if all_text_parts:
|
if all_text_parts:
|
||||||
# LoRAのみ
|
# LoRAのみ
|
||||||
positive_text_output = "\n".join(all_text_parts)
|
lora_text_output = "\n".join(all_text_parts)
|
||||||
else:
|
else:
|
||||||
# 何もない
|
# 何もない
|
||||||
positive_text_output = ""
|
lora_text_output = ""
|
||||||
|
|
||||||
# negativeテキスト出力
|
# negativeテキスト出力
|
||||||
# 1. json_sample_promptからのnegative
|
# 1. json_sample_promptからのnegative
|
||||||
@@ -1107,4 +1288,6 @@ class RandomLoRALoader:
|
|||||||
# プレビュー画像バッチ生成
|
# プレビュー画像バッチ生成
|
||||||
preview_batch = self._generate_preview_batch(preview_images)
|
preview_batch = self._generate_preview_batch(preview_images)
|
||||||
|
|
||||||
return (model, clip, positive_text_output, negative_text_output, positive_conditioning, negative_conditioning, preview_batch)
|
# positive_text = positive にエンコードした文字列そのもの(<lora:...> なし)
|
||||||
|
return (model, clip, positive_text, negative_text_output, positive_conditioning,
|
||||||
|
negative_conditioning, preview_batch, lora_text_output)
|
||||||
|
|||||||