Add LoadLoraFolderNode

This commit is contained in:
Lucas Sköld
2024-04-23 14:17:35 +02:00
parent 7e9b8c1a44
commit cdbc67dee0
3 changed files with 122 additions and 22 deletions
+12 -18
View File
@@ -143,31 +143,25 @@ Below is a table detailing the configuration options available in `config.json`,
> ![Example](./imgs/lora_paths_example.png)
> </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.
>
> ![Load Lora Character](./imgs/load_lora_character.png)
> </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.
>
> ![Load Lora Concept](./imgs/load_lora_concept.png)
> ![Load Lora Folder](./imgs/load_lora_Folder.png)
> </details>
## Get Font Size Node
> Estimates the optimal font size for text to fit within an image based on Lora information.
> <details>
+3 -4
View File
@@ -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
+107
View File
@@ -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,)