diff --git a/README.md b/README.md
index c39d0ab..f0d042a 100644
--- a/README.md
+++ b/README.md
@@ -143,31 +143,25 @@ Below is a table detailing the configuration options available in `config.json`,
> 
>
-## Load Lora Character
-> Finds and applies the closest matching Lora model to a character based on name similarity.
->
-> ℹ️ See More Information
->
-> - 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 Concept
+## Load Lora Folder
> Dynamically applies Lora models based on similarity to a provided prompt.
>
> ℹ️ See More Information
>
-> - 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.
>
-> 
+> 
>
+
## Get Font Size Node
> Estimates the optimal font size for text to fit within an image based on Lora information.
>
diff --git a/__init__.py b/__init__.py
index 7177986..fca08ef 100644
--- a/__init__.py
+++ b/__init__.py
@@ -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
diff --git a/src/load_lora_from_folder.py b/src/load_lora_from_folder.py
new file mode 100644
index 0000000..dc20f23
--- /dev/null
+++ b/src/load_lora_from_folder.py
@@ -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,)