diff --git a/README.md b/README.md index 1765a9d..f4e6561 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,10 @@ Many of the nodes require these paths to be in your extra_model_paths.yaml (with >
> ℹ️ See More Information > -> - **Functionality**: This node processes a given prompt to identify and apply the most similar Lora models of the tags found in the prompt. -> - **Input Processing**: The prompt is split into parts using a specified `separator`, and each part is cleaned to remove unnecessary characters and normalized for comparison. -> - **Similarity Matching**: For each part of the prompt, the node calculates a similarity ratio against available Lora model filenames. Models with a similarity ratio above a threshold (85%) are selected. Each selected Lora model is applied to the input `model` and `clip`. -> - **Outputs**: Returns the `model` and `clip`, potentially modified by multiple Lora models. +> - 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. > > ![Load Lora Concept](./imgs/load_lora_concept.png) >
diff --git a/src/levenshtein_distance.py b/src/levenshtein_distance.py new file mode 100644 index 0000000..ebe6334 --- /dev/null +++ b/src/levenshtein_distance.py @@ -0,0 +1,18 @@ +def levenshtein_distance(s1, s2): + if len(s1) < len(s2): + return levenshtein_distance(s2, s1) + + if len(s2) == 0: + return len(s1) + + previous_row = range(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] \ No newline at end of file diff --git a/src/load_lora_character.py b/src/load_lora_character.py index 5506cde..3490d3e 100644 --- a/src/load_lora_character.py +++ b/src/load_lora_character.py @@ -4,6 +4,7 @@ import re import folder_paths from nodes import LoraLoader from .print_sn0w import print_sn0w +from .levenshtein_distance import levenshtein_distance class LoadLoraCharacterNode: @classmethod @@ -23,25 +24,6 @@ class LoadLoraCharacterNode: FUNCTION = "find_and_apply_lora" CATEGORY = "sn0w" - def levenshtein_distance(self, s1, s2): - if len(s1) < len(s2): - return self.levenshtein_distance(s2, s1) - - if len(s2) == 0: - return len(s1) - - previous_row = range(len(s2) + 1) - for i, c1 in enumerate(s1): - current_row = [i + 1] - for j, c2 in enumerate(s2): - insertions = previous_row[j + 1] + 1 - deletions = current_row[j] + 1 - substitutions = previous_row[j] + (c1 != c2) - current_row.append(min(insertions, deletions, substitutions)) - previous_row = current_row - - return previous_row[-1] - def clean_string(self, input_string): # Remove parentheses and escape characters cleaned_string = re.sub(r'[\\()]', '', input_string) @@ -69,7 +51,7 @@ class LoadLoraCharacterNode: if any(word in char['name'].lower() for word in normalized_character_lower): cleaned_associated_string = self.clean_string(char['associated_string']) - distance = self.levenshtein_distance(cleaned_associated_string, cleaned_character) + distance = levenshtein_distance(cleaned_associated_string, cleaned_character) if distance < min_character_distance: # print_sn0w(f"Distance: {distance} Name: {char['name']}") @@ -101,10 +83,10 @@ class LoadLoraCharacterNode: if any(part in filename_lower for part in character_name_parts): # Calculate the Levenshtein distance for the full character name as one of the metrics. - full_name_distance = self.levenshtein_distance(character_name_lower, filename_lower) + full_name_distance = levenshtein_distance(character_name_lower, filename_lower) # Calculate the distances for each part of the character name and get the sum of them. - parts_distance = sum(self.levenshtein_distance(part, filename_lower) for part in character_name_parts) + parts_distance = sum(levenshtein_distance(part, filename_lower) for part in character_name_parts) # Use the minimum of full name distance and parts distance as the total distance. total_distance = min(full_name_distance, parts_distance) @@ -119,8 +101,8 @@ class LoadLoraCharacterNode: break elif total_distance == lowest_distance: # If the distance is the same as the lowest distance, check the first part of the character name as a final check. - parts_distance_final_new = self.levenshtein_distance(character_name_parts[0], filename_lower) - parts_distance_final_old = self.levenshtein_distance(lowest_distance_character_parts[0], filename_lower) + parts_distance_final_new = levenshtein_distance(character_name_parts[0], filename_lower) + parts_distance_final_old = levenshtein_distance(lowest_distance_character_parts[0], filename_lower) if parts_distance_final_new < parts_distance_final_old: parts_distance_final_new = total_distance closest_match = filename diff --git a/src/load_lora_concept.py b/src/load_lora_concept.py index f07f459..b3d5af6 100644 --- a/src/load_lora_concept.py +++ b/src/load_lora_concept.py @@ -1,9 +1,9 @@ import os -import json import re import folder_paths from nodes import LoraLoader from .print_sn0w import print_sn0w +from .levenshtein_distance import levenshtein_distance class LoadLoraConceptNode: @classmethod @@ -29,43 +29,6 @@ class LoadLoraConceptNode: cleaned_string = re.sub(r':\d+(\.\d+)?', '', cleaned_string) cleaned_string = re.sub(r',$', '', cleaned_string.strip()) return cleaned_string.lower() - - def similarity_ratio(self, str1, str2): - """ - Calculates a basic similarity ratio between two strings based on the length - of the longest common subsequence. This is a simplified and less accurate - approach compared to algorithms like Levenshtein distance. - - Args: - - str1: First string. - - str2: Second string. - - Returns: - - A percentage representing the similarity between the two strings. - """ - def longest_common_subsequence(s1, s2): - m, n = len(s1), len(s2) - dp = [[0] * (n + 1) for _ in range(m + 1)] - - for i in range(1, m + 1): - for j in range(1, n + 1): - if s1[i - 1] == s2[j - 1]: - dp[i][j] = dp[i - 1][j - 1] + 1 - else: - dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) - - return dp[m][n] - - # Normalize and prepare strings (remove file extension, replace underscores) - normalized_str1 = str1.replace(".safetensors", "").replace("_", " ").lower() - normalized_str2 = str2.replace(".safetensors", "").replace("_", " ").lower() - - # Calculate the length of the longest common subsequence - lcs_len = longest_common_subsequence(normalized_str1, normalized_str2) - max_len = max(len(normalized_str1), len(normalized_str2)) - - # Return the similarity ratio - return (lcs_len / max_len) * 100 if max_len > 0 else 0 def find_and_apply_lora(self, model, clip, prompt, xl, lora_strength, separator): if prompt is None: @@ -77,25 +40,34 @@ class LoadLoraConceptNode: filtered_lora_paths = folder_paths.get_filename_list("loras_xl" if xl else "loras_15") lora_paths = [path for path in filtered_lora_paths if "concept" in path.lower()] - found_full_lora_paths = [] + # Initialize a dictionary to store potential loras for each prompt part + 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() - # Remove file extension and replace underscores with spaces for comparison processed_lora_filename = lora_filename.replace(".safetensors", "").replace("_", " ") - # Calculate similarity ratio - similarity_ratio = self.similarity_ratio(prompt_part, processed_lora_filename) - if similarity_ratio >= 60: - # Find the full path from the full_lora_paths list + + if prompt_part in processed_lora_filename: + distance = levenshtein_distance(prompt_part, processed_lora_filename) for full_path in full_lora_paths: if lora_filename in full_path.lower(): - found_full_lora_paths.append(full_path) - break + if distance <= 5: + # Store the candidate with its distance and full path + if prompt_part not in lora_candidates: + lora_candidates[prompt_part] = [] + lora_candidates[prompt_part].append({'full_path': full_path, 'distance': distance}) + print_sn0w("Distance: " + str(distance) + " Lora: " + lora_filename + " Tag: " + prompt_part) + break - # Print and apply found lora paths - for path in found_full_lora_paths: - print_sn0w(f"Loading Concept Lora: {os.path.split(path)[-1].lower()}") - model, clip = LoraLoader().load_lora(model, clip, path, lora_strength, lora_strength) + # Apply the Lora with the lowest distance for each prompt_part, ensuring no duplicates + for prompt_part, candidates in lora_candidates.items(): + if candidates: + selected_candidate = min(candidates, key=lambda x: x['distance']) + if selected_candidate['full_path'] not in loaded_loras: # Check if this Lora has already been loaded + print_sn0w(f"Loading Concept Lora: {os.path.split(selected_candidate['full_path'])[-1]}") + model, clip = LoraLoader().load_lora(model, clip, selected_candidate['full_path'], lora_strength, lora_strength) + loaded_loras.add(selected_candidate['full_path']) # Mark this Lora as loaded return (model, clip, ) \ No newline at end of file