mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
- Add `# type: ignore` comments to comfy.sd and folder_paths imports - Remove unused imports: os, random, and extract_lora_name - Clean up import statements across checkpoint_loader, lora_randomizer, and unet_loader nodes
205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
"""
|
|
Lora Randomizer Node - Randomly selects LoRAs from a pool with configurable settings.
|
|
|
|
This node accepts optional pool_config input to filter available LoRAs, and outputs
|
|
a LORA_STACK with randomly selected LoRAs. Returns UI updates with new random LoRAs
|
|
and tracks the last used combination for reuse.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from ..utils.utils import get_lora_info
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LoraRandomizerLM:
|
|
"""Node that randomly selects LoRAs from a pool"""
|
|
|
|
NAME = "Lora Randomizer (LoraManager)"
|
|
CATEGORY = "Lora Manager/randomizer"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"randomizer_config": ("RANDOMIZER_CONFIG", {}),
|
|
"loras": ("LORAS", {}),
|
|
},
|
|
"optional": {
|
|
"pool_config": ("POOL_CONFIG", {}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("LORA_STACK",)
|
|
RETURN_NAMES = ("LORA_STACK",)
|
|
|
|
FUNCTION = "randomize"
|
|
OUTPUT_NODE = False
|
|
|
|
def _preprocess_loras_input(self, loras):
|
|
"""
|
|
Preprocess loras input to handle different widget formats.
|
|
|
|
Args:
|
|
loras: Input from widget, either:
|
|
- List of LoRA dicts (expected format)
|
|
- Dict with '__value__' key containing the list
|
|
|
|
Returns:
|
|
List of LoRA dicts
|
|
"""
|
|
if isinstance(loras, dict) and "__value__" in loras:
|
|
return loras["__value__"]
|
|
return loras
|
|
|
|
async def randomize(self, randomizer_config, loras, pool_config=None):
|
|
"""
|
|
Randomize LoRAs based on configuration and pool filters.
|
|
|
|
Args:
|
|
randomizer_config: Dict with randomizer settings (count, strength ranges, roll_mode)
|
|
loras: List of LoRA dicts from LORAS widget (includes locked state)
|
|
pool_config: Optional config from LoRA Pool node for filtering
|
|
|
|
Returns:
|
|
Dictionary with 'result' (LORA_STACK tuple) and 'ui' (for widget display)
|
|
"""
|
|
from ..services.service_registry import ServiceRegistry
|
|
|
|
loras = self._preprocess_loras_input(loras)
|
|
|
|
roll_mode = randomizer_config.get("roll_mode", "always")
|
|
logger.debug(f"[LoraRandomizerLM] roll_mode: {roll_mode}")
|
|
|
|
# Dual seed mechanism for batch queue synchronization
|
|
# execution_seed: seed for generating execution_stack (= previous next_seed)
|
|
# next_seed: seed for generating ui_loras (= what will be displayed after execution)
|
|
execution_seed = randomizer_config.get("execution_seed", None)
|
|
next_seed = randomizer_config.get("next_seed", None)
|
|
|
|
if roll_mode == "fixed":
|
|
ui_loras = loras
|
|
execution_loras = loras
|
|
else:
|
|
scanner = await ServiceRegistry.get_lora_scanner()
|
|
|
|
# Generate execution_loras from execution_seed (if available)
|
|
if execution_seed is not None:
|
|
# Use execution_seed to regenerate the same loras that were shown to user
|
|
execution_loras = await self._generate_random_loras_for_ui(
|
|
scanner, randomizer_config, loras, pool_config, seed=execution_seed
|
|
)
|
|
else:
|
|
# First execution: use loras input (what user sees in the widget)
|
|
execution_loras = loras
|
|
|
|
# Generate ui_loras from next_seed (for display after execution)
|
|
ui_loras = await self._generate_random_loras_for_ui(
|
|
scanner, randomizer_config, loras, pool_config, seed=next_seed
|
|
)
|
|
|
|
execution_stack = self._build_execution_stack_from_input(execution_loras)
|
|
|
|
return {
|
|
"result": (execution_stack,),
|
|
"ui": {"loras": ui_loras, "last_used": execution_loras},
|
|
}
|
|
|
|
def _build_execution_stack_from_input(self, loras):
|
|
"""
|
|
Build LORA_STACK tuple from input loras list for execution.
|
|
|
|
Args:
|
|
loras: List of LoRA dicts with name, strength, clipStrength, active
|
|
|
|
Returns:
|
|
List of tuples (lora_path, model_strength, clip_strength)
|
|
"""
|
|
lora_stack = []
|
|
for lora in loras:
|
|
if not lora.get("active", False):
|
|
continue
|
|
|
|
# Get file path
|
|
lora_path, trigger_words = get_lora_info(lora["name"])
|
|
if not lora_path:
|
|
logger.warning(
|
|
f"[LoraRandomizerLM] Could not find path for LoRA: {lora['name']}"
|
|
)
|
|
continue
|
|
|
|
# Normalize path separators
|
|
lora_path = lora_path.replace("/", os.sep)
|
|
|
|
# Extract strengths (convert to float to prevent string subtraction errors)
|
|
model_strength = float(lora.get("strength", 1.0))
|
|
clip_strength = float(lora.get("clipStrength", model_strength))
|
|
|
|
lora_stack.append((lora_path, model_strength, clip_strength))
|
|
|
|
return lora_stack
|
|
|
|
async def _generate_random_loras_for_ui(
|
|
self, scanner, randomizer_config, input_loras, pool_config=None, seed=None
|
|
):
|
|
"""
|
|
Generate new random loras for UI display.
|
|
|
|
Args:
|
|
scanner: LoraScanner instance
|
|
randomizer_config: Dict with randomizer settings
|
|
input_loras: Current input loras (for extracting locked loras)
|
|
pool_config: Optional pool filters
|
|
seed: Optional seed for deterministic randomization
|
|
|
|
Returns:
|
|
List of LoRA dicts for UI display
|
|
"""
|
|
from ..services.lora_service import LoraService
|
|
|
|
# Parse randomizer settings (convert numeric values to float to prevent type errors)
|
|
count_mode = randomizer_config.get("count_mode", "range")
|
|
count_fixed = int(randomizer_config.get("count_fixed", 5))
|
|
count_min = int(randomizer_config.get("count_min", 3))
|
|
count_max = int(randomizer_config.get("count_max", 7))
|
|
model_strength_min = float(randomizer_config.get("model_strength_min", 0.0))
|
|
model_strength_max = float(randomizer_config.get("model_strength_max", 1.0))
|
|
use_same_clip_strength = randomizer_config.get("use_same_clip_strength", True)
|
|
clip_strength_min = float(randomizer_config.get("clip_strength_min", 0.0))
|
|
clip_strength_max = float(randomizer_config.get("clip_strength_max", 1.0))
|
|
use_recommended_strength = randomizer_config.get(
|
|
"use_recommended_strength", False
|
|
)
|
|
recommended_strength_scale_min = float(
|
|
randomizer_config.get("recommended_strength_scale_min", 0.5)
|
|
)
|
|
recommended_strength_scale_max = float(
|
|
randomizer_config.get("recommended_strength_scale_max", 1.0)
|
|
)
|
|
|
|
# Extract locked LoRAs from input
|
|
locked_loras = [lora for lora in input_loras if lora.get("locked", False)]
|
|
|
|
# Use LoraService to generate random LoRAs
|
|
lora_service = LoraService(scanner)
|
|
result_loras = await lora_service.get_random_loras(
|
|
count=count_fixed,
|
|
model_strength_min=model_strength_min,
|
|
model_strength_max=model_strength_max,
|
|
use_same_clip_strength=use_same_clip_strength,
|
|
clip_strength_min=clip_strength_min,
|
|
clip_strength_max=clip_strength_max,
|
|
locked_loras=locked_loras,
|
|
pool_config=pool_config,
|
|
count_mode=count_mode,
|
|
count_min=count_min,
|
|
count_max=count_max,
|
|
use_recommended_strength=use_recommended_strength,
|
|
recommended_strength_scale_min=recommended_strength_scale_min,
|
|
recommended_strength_scale_max=recommended_strength_scale_max,
|
|
seed=seed,
|
|
)
|
|
|
|
return result_loras
|