diff --git a/__init__.py b/__init__.py index 519d5d15..120fec08 100644 --- a/__init__.py +++ b/__init__.py @@ -16,6 +16,7 @@ try: # pragma: no cover - import fallback for pytest collection from .py.nodes.lora_randomizer import LoraRandomizerLM from .py.nodes.lora_cycler import LoraCyclerLM from .py.nodes.lora_info import LoraInfoLM + from .py.nodes.lora_syntax_to_path import LoraSyntaxToPath from .py.metadata_collector import init as init_metadata_collector except ( ImportError @@ -58,6 +59,9 @@ except ( ).LoraRandomizerLM LoraCyclerLM = importlib.import_module("py.nodes.lora_cycler").LoraCyclerLM LoraInfoLM = importlib.import_module("py.nodes.lora_info").LoraInfoLM + LoraSyntaxToPath = importlib.import_module( + "py.nodes.lora_syntax_to_path" + ).LoraSyntaxToPath init_metadata_collector = importlib.import_module("py.metadata_collector").init NODE_CLASS_MAPPINGS = { @@ -78,6 +82,7 @@ NODE_CLASS_MAPPINGS = { LoraRandomizerLM.NAME: LoraRandomizerLM, LoraCyclerLM.NAME: LoraCyclerLM, LoraInfoLM.NAME: LoraInfoLM, + LoraSyntaxToPath.NAME: LoraSyntaxToPath, } WEB_DIRECTORY = "./web/comfyui" diff --git a/py/nodes/lora_loader.py b/py/nodes/lora_loader.py index f91d8090..1ac52d27 100644 --- a/py/nodes/lora_loader.py +++ b/py/nodes/lora_loader.py @@ -1,6 +1,5 @@ import importlib import logging -import re import comfy.sd # type: ignore import comfy.utils # type: ignore @@ -14,6 +13,7 @@ from .utils import ( extract_lora_name, get_loras_list, nunchaku_load_lora, + parse_lora_syntax, ) logger = logging.getLogger(__name__) @@ -189,25 +189,10 @@ class LoraTextLoaderLM: RETURN_NAMES = ("MODEL", "CLIP", "trigger_words", "loaded_loras") FUNCTION = "load_loras_from_text" - def parse_lora_syntax(self, text): - """Parse LoRA syntax from text input.""" - pattern = r"]+):([^:>]+)(?::([^:>]+))?>" - matches = re.findall(pattern, text, re.IGNORECASE) - - loras = [] - for match in matches: - model_strength = float(match[1]) - loras.append({ - "name": match[0], - "model_strength": model_strength, - "clip_strength": float(match[2]) if match[2] else model_strength, - }) - return loras - def load_loras_from_text(self, model, lora_syntax, clip=None, lora_stack=None): """Load LoRAs based on text syntax input.""" lora_entries = _collect_stack_entries(lora_stack) - for lora in self.parse_lora_syntax(lora_syntax): + for lora in parse_lora_syntax(lora_syntax): lora_path, trigger_words = get_lora_info_absolute(lora["name"]) lora_entries.append({ "name": lora["name"], diff --git a/py/nodes/lora_syntax_to_path.py b/py/nodes/lora_syntax_to_path.py new file mode 100644 index 00000000..ff864c52 --- /dev/null +++ b/py/nodes/lora_syntax_to_path.py @@ -0,0 +1,62 @@ +"""Node to resolve `` syntax to absolute file system paths. + +Takes the loaded_loras / active_loras STRING output from LoraLoaderLM or +LoraStackerLM and resolves each lora name to its absolute path on disk via +the scanner cache. Unknown names are returned as-is. +""" + +import logging + +from ..utils.utils import get_lora_info_absolute +from .utils import parse_lora_syntax + +logger = logging.getLogger(__name__) + + +class LoraSyntaxToPath: + NAME = "LoRA Syntax → Path (LoraManager)" + CATEGORY = "Lora Manager/utils" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "lora_syntax": ( + "STRING", + { + "forceInput": True, + "multiline": True, + "tooltip": ( + " formatted text from " + "loaded_loras / active_loras output" + ), + }, + ), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("paths",) + FUNCTION = "resolve" + + def resolve(self, lora_syntax: str) -> tuple[str]: + """Parse syntax and resolve each name to its absolute path.""" + if not lora_syntax or not lora_syntax.strip(): + logger.info("Received empty lora_syntax input") + return ("",) + + parsed = parse_lora_syntax(lora_syntax) + if not parsed: + logger.info("No valid entries found in input") + return ("",) + + paths: list[str] = [] + for entry in parsed: + try: + absolute_path, _ = get_lora_info_absolute(entry["name"]) + paths.append(absolute_path) + except Exception: + logger.warning("Failed to resolve lora '%s', skipping", entry["name"]) + continue + + return ("\n".join(paths),) diff --git a/py/nodes/utils.py b/py/nodes/utils.py index 6c6f15eb..12f2fc1e 100644 --- a/py/nodes/utils.py +++ b/py/nodes/utils.py @@ -36,6 +36,7 @@ any_type = AnyType("*") # Common methods extracted from lora_loader.py and lora_stacker.py import os +import re import logging import copy import sys @@ -69,6 +70,25 @@ def extract_lora_name(lora_path): return apply_lora_syntax_format(name_no_ext) +def parse_lora_syntax(text: str) -> list[dict]: + """Parse syntax from text input into a list of dicts. + + Each entry contains: name, model_strength, clip_strength. + Supports both ```` and ````. + """ + pattern = r"]+):([^:>]+)(?::([^:>]+))?>" + matches = re.findall(pattern, text, re.IGNORECASE) + loras = [] + for match in matches: + model_strength = float(match[1]) + loras.append({ + "name": match[0], + "model_strength": model_strength, + "clip_strength": float(match[2]) if match[2] else model_strength, + }) + return loras + + def get_loras_list(kwargs): """Helper to extract loras list from either old or new kwargs format""" if "loras" not in kwargs: