Made levenshtein_distance own file, updated concept lora finding
This commit is contained in:
@@ -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.
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
@@ -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]
|
||||
@@ -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
@@ -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, )
|
||||
Reference in New Issue
Block a user