Made levenshtein_distance own file, updated concept lora finding

This commit is contained in:
sn0w12
2024-02-20 23:40:03 +01:00
parent c2be3d2b78
commit df5df6cc04
4 changed files with 49 additions and 78 deletions
+3 -4
View File
@@ -126,11 +126,10 @@ Many of the nodes require these paths to be in your extra_model_paths.yaml (with
> <details>
> <summary>ℹ️ <i>See More Information</i></summary>
>
> - **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)
> </details>
+18
View File
@@ -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]
+6 -24
View File
@@ -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
+22 -50
View File
@@ -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, )