Add LoadLoraFolderNode
This commit is contained in:
@@ -143,31 +143,25 @@ Below is a table detailing the configuration options available in `config.json`,
|
||||
> 
|
||||
> </details>
|
||||
|
||||
## Load Lora Character
|
||||
> Finds and applies the closest matching Lora model to a character based on name similarity.
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - The process begins by cleaning the input character string for comparison. It then searches a JSON file for a matching character name. If a match is found, it proceeds to select the appropriate folder based on the `xl` boolean flag.
|
||||
> - To find the closest Lora model, it calculates the Levenshtein distance between the character name (in full and in parts) and the filenames of available Lora models. This ensures a case-insensitive match with the best possible model.
|
||||
> - If no lora is found it just returns the input model and the workflow can continue.
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
## Load Lora Concept
|
||||
## Load Lora Folder
|
||||
> Dynamically applies Lora models based on similarity to a provided prompt.
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - This node processes a given prompt to identify and apply the most similar Lora models of the tags found in the prompt.
|
||||
> - For each part of the prompt, the node calculates a distance between tags and available Lora model filenames. Models with a difference of under 5 is applied to the input `model` and `clip`.
|
||||
> - Note: You need the loras you want to be selected to be in a folder called `concept` for this to work, they also need to be seperated into XL and 1.5 like stated in the `Important Note`.
|
||||
> - Note: Loras need to be named very similarly to the tag, with at most 5 different characters different. The words in the lora can be seperated by spaces or underscores.
|
||||
> - This node processes a given prompt to identify and apply the most similar Lora models of the tags found in the prompt.
|
||||
> - For each part of the prompt, the node calculates a distance between tags and available Lora model filenames. Models with a difference of under 5 are applied to the input `model` and `clip`.
|
||||
> - **Example folder input:** `*master_folder, subfolder1:3, -excludefolder, subfolder2`
|
||||
> - `*master_folder` specifies all paths must include `master_folder`.
|
||||
> - `subfolder1:3` indicates up to 3 models from `subfolder1` can be loaded.
|
||||
> - `-excludefolder` ensures any path containing `excludefolder` is ignored.
|
||||
> - `subfolder2` loads models from this subfolder without a numeric limit.
|
||||
> - Note: You need the loras separated into XL and 1.5 as stated in the `Important Note`.
|
||||
> - Note: Loras need to be named very similarly to the tag, with at most 5 characters different. Words in the lora filename can be separated by spaces or underscores.
|
||||
>
|
||||
> 
|
||||
> 
|
||||
> </details>
|
||||
|
||||
|
||||
## Get Font Size Node
|
||||
> Estimates the optimal font size for text to fit within an image based on Lora information.
|
||||
> <details>
|
||||
|
||||
+3
-4
@@ -21,6 +21,7 @@ from .src.load_lora_concept import LoadLoraConceptNode
|
||||
from .src.perlin_noise import FilmGrain
|
||||
from .src.upscale_with_model_by import UpscaleImageBy
|
||||
from .src.custom_hires import CustomHires
|
||||
from .src.load_lora_from_folder import LoadLoraFolderNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Find SDXL Resolution": FindResolutionNode,
|
||||
@@ -34,14 +35,13 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Lora Stacker": LoraStackerNode,
|
||||
"Load Lora XL": LoraLoraXLNode,
|
||||
"Load Lora 1.5": LoraLora15Node,
|
||||
"Load Lora Character": LoadLoraCharacterNode,
|
||||
"Get Font Size": GetFontSizeNode,
|
||||
"Get All Styles": GetStylesNode,
|
||||
"Prompt Selector": PromptSelectNode,
|
||||
"Load Lora Concept": LoadLoraConceptNode,
|
||||
"Colored Film Grain": FilmGrain,
|
||||
"Upscale Image With Model By": UpscaleImageBy,
|
||||
"Sn0w Custom Hires": CustomHires,
|
||||
"Load Lora Folder": LoadLoraFolderNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -56,14 +56,13 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Lora Stacker": "Lora Stacker",
|
||||
"Load Lora XL": "Load Lora XL",
|
||||
"Load Lora 1.5": "Load Lora 1.5",
|
||||
"Load Lora Character": "Load Lora Character",
|
||||
"Get Font Size": "Get Font Size",
|
||||
"Get All Styles": "Get All Styles",
|
||||
"Prompt Selector": "Prompt Selector",
|
||||
"Load Lora Concept": "Load Lora Concept",
|
||||
"Colored Film Grain": "Colored Film Grain",
|
||||
"Upscale Image With Model By": "Upscale Image With Model By",
|
||||
"Sn0w Custom Hires": "Sn0w Custom Hires",
|
||||
"Load Lora Folder": "Load Lora Folder",
|
||||
}
|
||||
|
||||
current_unique_id = 0 # Global variable to track the unique ID
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
import re
|
||||
import folder_paths
|
||||
from nodes import LoraLoader
|
||||
from .sn0w import Logger, Utility
|
||||
|
||||
class LoadLoraFolderNode:
|
||||
logger = Logger()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP", ),
|
||||
"prompt": ("STRING", {"default": ""}),
|
||||
"folders": ("STRING", {"default": "character:1,concept"}),
|
||||
"xl": ("BOOLEAN", {"default": False}),
|
||||
"lora_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||
"separator": ("STRING", {"default": ", "}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP",)
|
||||
RETURN_NAMES = ("MODEL", "CLIP",)
|
||||
FUNCTION = "find_and_apply_lora"
|
||||
CATEGORY = "sn0w"
|
||||
|
||||
def clean_string(self, input_string):
|
||||
cleaned_string = input_string.replace(r'\(', '(').replace(r'\)', ')')
|
||||
cleaned_string = re.sub(r'[:]+(\d+(\.\d+)?)?', '', cleaned_string)
|
||||
cleaned_string = re.sub(r',\s*$', '', cleaned_string.strip())
|
||||
return cleaned_string.lower()
|
||||
|
||||
def normalize_folder_name(self, folder_name):
|
||||
return folder_name.strip().replace('\\', '/').lower()
|
||||
|
||||
def parse_folders(self, folder_string):
|
||||
master_folder = None
|
||||
include_folders = {}
|
||||
exclude_folders = set()
|
||||
folder_specs = folder_string.split(',')
|
||||
|
||||
for spec in folder_specs:
|
||||
parts = spec.split(':')
|
||||
folder_name = self.normalize_folder_name(parts[0])
|
||||
|
||||
if folder_name.startswith('*'):
|
||||
master_folder = folder_name[1:] # Remove the '*' to get the master folder name
|
||||
elif folder_name.startswith('-'):
|
||||
exclude_folder = folder_name[1:] # Remove the '-' to get the folder name to exclude
|
||||
exclude_folders.add(exclude_folder)
|
||||
else:
|
||||
max_count = int(parts[1]) if len(parts) > 1 else None
|
||||
include_folders[folder_name] = max_count
|
||||
|
||||
return master_folder, include_folders, exclude_folders
|
||||
|
||||
def find_and_apply_lora(self, model, clip, prompt, folders, xl, lora_strength, separator):
|
||||
if prompt is None:
|
||||
prompt = ''
|
||||
prompt_parts = [self.clean_string(part) for part in prompt.split(separator)]
|
||||
|
||||
# Retrieve paths and apply folder constraints
|
||||
full_lora_paths = folder_paths.get_filename_list("loras")
|
||||
filtered_lora_paths = folder_paths.get_filename_list("loras_xl" if xl else "loras_15")
|
||||
master_folder, include_folders, exclude_folders = self.parse_folders(folders)
|
||||
|
||||
# Filter by master folder if specified
|
||||
if master_folder:
|
||||
filtered_lora_paths = [path for path in filtered_lora_paths if master_folder in self.normalize_folder_name(path)]
|
||||
|
||||
# Filter by included and excluded folders
|
||||
lora_paths = [path for path in filtered_lora_paths if any(folder in self.normalize_folder_name(path) for folder in include_folders)]
|
||||
lora_paths = [path for path in lora_paths if not any(exclude in self.normalize_folder_name(path) for exclude in exclude_folders)]
|
||||
|
||||
lora_candidates = {}
|
||||
loaded_loras = set()
|
||||
|
||||
for prompt_part in prompt_parts:
|
||||
for lora_path in lora_paths:
|
||||
lora_filename = os.path.split(lora_path)[-1].lower()
|
||||
processed_lora_filename = lora_filename.replace(".safetensors", "").replace("_", " ")
|
||||
|
||||
if prompt_part in processed_lora_filename:
|
||||
distance = Utility.levenshtein_distance(prompt_part, processed_lora_filename)
|
||||
for full_path in full_lora_paths:
|
||||
if lora_filename in full_path.lower():
|
||||
if distance <= 5:
|
||||
if prompt_part not in lora_candidates:
|
||||
lora_candidates[prompt_part] = []
|
||||
lora_candidates[prompt_part].append({'full_path': full_path, 'distance': distance})
|
||||
self.logger.log("Distance: " + str(distance) + " Lora: " + lora_filename + " Tag: " + prompt_part, "ALL")
|
||||
break
|
||||
|
||||
for prompt_part, candidates in lora_candidates.items():
|
||||
if candidates:
|
||||
selected_candidate = min(candidates, key=lambda x: x['distance'])
|
||||
folder_name = self.normalize_folder_name(os.path.dirname(selected_candidate['full_path']))
|
||||
if selected_candidate['full_path'] not in loaded_loras and (folder_name not in include_folders or len(loaded_loras) < include_folders[folder_name]):
|
||||
self.logger.log(f"Loading Lora: {os.path.split(selected_candidate['full_path'])[-1]}", "GENERAL")
|
||||
model, clip = LoraLoader().load_lora(model, clip, selected_candidate['full_path'], lora_strength, lora_strength)
|
||||
loaded_loras.add(selected_candidate['full_path'])
|
||||
else:
|
||||
self.logger.log(f"No matching Lora found for any tags.", "GENERAL")
|
||||
|
||||
return (model, clip,)
|
||||
Reference in New Issue
Block a user