mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
- Update package.json test script to run both JS and Vue tests - Simplify LoraCyclerLM output by removing redundant lora name fallback - Extend Vitest config to include TypeScript test files - Add Vue testing dependencies and setup for component testing - Implement comprehensive test suite for BatchQueueSimulator component - Add test setup file with global mocks for ComfyUI modules
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""
|
|
Lora Cycler Node - Sequentially cycles through LoRAs from a pool.
|
|
|
|
This node accepts optional pool_config input to filter available LoRAs, and outputs
|
|
a LORA_STACK with one LoRA at a time. Returns UI updates with current/next LoRA info
|
|
and tracks the cycle progress which persists across workflow save/load.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from ..utils.utils import get_lora_info
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LoraCyclerLM:
|
|
"""Node that sequentially cycles through LoRAs from a pool"""
|
|
|
|
NAME = "Lora Cycler (LoraManager)"
|
|
CATEGORY = "Lora Manager/randomizer"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"cycler_config": ("CYCLER_CONFIG", {}),
|
|
},
|
|
"optional": {
|
|
"pool_config": ("POOL_CONFIG", {}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("LORA_STACK",)
|
|
RETURN_NAMES = ("LORA_STACK",)
|
|
|
|
FUNCTION = "cycle"
|
|
OUTPUT_NODE = False
|
|
|
|
async def cycle(self, cycler_config, pool_config=None):
|
|
"""
|
|
Cycle through LoRAs based on configuration and pool filters.
|
|
|
|
Args:
|
|
cycler_config: Dict with cycler settings (current_index, model_strength, clip_strength, sort_by)
|
|
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
|
|
from ..services.lora_service import LoraService
|
|
|
|
# Extract settings from cycler_config
|
|
current_index = cycler_config.get("current_index", 1) # 1-based
|
|
model_strength = float(cycler_config.get("model_strength", 1.0))
|
|
clip_strength = float(cycler_config.get("clip_strength", 1.0))
|
|
sort_by = "filename"
|
|
|
|
# Dual-index mechanism for batch queue synchronization
|
|
execution_index = cycler_config.get("execution_index") # Can be None
|
|
# next_index_from_config = cycler_config.get("next_index") # Not used on backend
|
|
|
|
# Get scanner and service
|
|
scanner = await ServiceRegistry.get_lora_scanner()
|
|
lora_service = LoraService(scanner)
|
|
|
|
# Get filtered and sorted LoRA list
|
|
lora_list = await lora_service.get_cycler_list(
|
|
pool_config=pool_config, sort_by=sort_by
|
|
)
|
|
|
|
total_count = len(lora_list)
|
|
|
|
if total_count == 0:
|
|
logger.warning("[LoraCyclerLM] No LoRAs available in pool")
|
|
return {
|
|
"result": ([],),
|
|
"ui": {
|
|
"current_index": [1],
|
|
"next_index": [1],
|
|
"total_count": [0],
|
|
"current_lora_name": [""],
|
|
"current_lora_filename": [""],
|
|
"error": ["No LoRAs available in pool"],
|
|
},
|
|
}
|
|
|
|
# Determine which index to use for this execution
|
|
# If execution_index is provided (batch queue case), use it
|
|
# Otherwise use current_index (first execution or non-batch case)
|
|
if execution_index is not None:
|
|
actual_index = execution_index
|
|
else:
|
|
actual_index = current_index
|
|
|
|
# Clamp index to valid range (1-based)
|
|
clamped_index = max(1, min(actual_index, total_count))
|
|
|
|
# Get LoRA at current index (convert to 0-based for list access)
|
|
current_lora = lora_list[clamped_index - 1]
|
|
|
|
# Build LORA_STACK with single LoRA
|
|
lora_path, _ = get_lora_info(current_lora["file_name"])
|
|
if not lora_path:
|
|
logger.warning(
|
|
f"[LoraCyclerLM] Could not find path for LoRA: {current_lora['file_name']}"
|
|
)
|
|
lora_stack = []
|
|
else:
|
|
# Normalize path separators
|
|
lora_path = lora_path.replace("/", os.sep)
|
|
lora_stack = [(lora_path, model_strength, clip_strength)]
|
|
|
|
# Calculate next index (wrap to 1 if at end)
|
|
next_index = clamped_index + 1
|
|
if next_index > total_count:
|
|
next_index = 1
|
|
|
|
# Get next LoRA for UI display (what will be used next generation)
|
|
next_lora = lora_list[next_index - 1]
|
|
next_display_name = next_lora["file_name"]
|
|
|
|
return {
|
|
"result": (lora_stack,),
|
|
"ui": {
|
|
"current_index": [clamped_index],
|
|
"next_index": [next_index],
|
|
"total_count": [total_count],
|
|
"current_lora_name": [current_lora["file_name"]],
|
|
"current_lora_filename": [current_lora["file_name"]],
|
|
"next_lora_name": [next_display_name],
|
|
"next_lora_filename": [next_lora["file_name"]],
|
|
},
|
|
}
|