Files
gabe-init 37e8b905eb Initial commit - ComfyUI String Similarity Node
- Advanced text comparison for ComfyUI
- Multiple similarity algorithms
- Support for semantic similarity
- MIT License
2025-05-25 13:19:14 -05:00

187 lines
7.7 KiB
Python

import Levenshtein
from difflib import SequenceMatcher
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import ast
from sentence_transformers import SentenceTransformer
import numpy as np
class StringSimilarity:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"actual_text": ("STRING", {"forceInput": True}),
"ocr_text": ("STRING", {"forceInput": True}),
"algorithm": (["Levenshtein", "SequenceMatcher", "Jaccard", "Cosine", "WER", "CER", "SentenceTransformer-MPNET", "SentenceTransformer-MiniLM"],),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "compute_similarity"
CATEGORY = "Text Analysis"
def __init__(self):
self.mpnet_model = None
self.minilm_model = None
def compute_similarity(self, actual_text, ocr_text, algorithm):
actual_text = self.extract_string(actual_text)
ocr_text = self.extract_string(ocr_text)
if not actual_text or not ocr_text:
return ("Error: Both actual_text and ocr_text must be provided and non-empty.",)
try:
result = f"Actual Text: '{actual_text}'\nOCR Text: '{ocr_text}'\n\n"
if algorithm == "Levenshtein":
distance, similarity = self.levenshtein_similarity(actual_text, ocr_text)
result += f"Levenshtein Distance: {distance}\nLevenshtein Similarity: {similarity:.2f}"
elif algorithm == "SequenceMatcher":
similarity = self.sequence_matcher_similarity(actual_text, ocr_text)
result += f"SequenceMatcher Similarity: {similarity:.2f}"
elif algorithm == "Jaccard":
similarity = self.jaccard_similarity(actual_text, ocr_text)
result += f"Jaccard Similarity: {similarity:.2f}"
elif algorithm == "Cosine":
similarity = self.cosine_similarity_calc(actual_text, ocr_text)
result += f"Cosine Similarity: {similarity:.2f}"
elif algorithm == "WER":
wer = self.word_error_rate(actual_text, ocr_text)
result += f"Word Error Rate: {wer:.2f}"
elif algorithm == "CER":
cer = self.character_error_rate(actual_text, ocr_text)
result += f"Character Error Rate: {cer:.2f}"
elif algorithm == "SentenceTransformer-MPNET":
similarity = self.sentence_transformer_similarity(actual_text, ocr_text, "all-mpnet-base-v2")
result += f"SentenceTransformer (MPNET) Similarity: {similarity:.2f}"
elif algorithm == "SentenceTransformer-MiniLM":
similarity = self.sentence_transformer_similarity(actual_text, ocr_text, "all-MiniLM-L6-v2")
result += f"SentenceTransformer (MiniLM) Similarity: {similarity:.2f}"
else:
return (f"Error: Invalid algorithm '{algorithm}' selected.",)
return (result,)
except Exception as e:
return (f"Error in computation: {str(e)}",)
@staticmethod
def extract_string(input_text):
if isinstance(input_text, (list, tuple)):
return input_text[0] if input_text else ""
elif isinstance(input_text, str):
try:
parsed = ast.literal_eval(input_text)
if isinstance(parsed, (list, tuple)):
return parsed[0] if parsed else ""
else:
return input_text
except (ValueError, SyntaxError):
return input_text
else:
return str(input_text)
@staticmethod
def levenshtein_similarity(str1, str2):
str1, str2 = str1.lower(), str2.lower()
distance = Levenshtein.distance(str1, str2)
max_length = max(len(str1), len(str2))
similarity = 1 - (distance / max_length) if max_length > 0 else 1
return distance, similarity
@staticmethod
def sequence_matcher_similarity(str1, str2):
return SequenceMatcher(None, str1.lower(), str2.lower()).ratio()
@staticmethod
def jaccard_similarity(str1, str2):
set1 = set(str1.lower().split())
set2 = set(str2.lower().split())
intersection = len(set1.intersection(set2))
union = len(set1.union(set2))
return intersection / union if union > 0 else 1
@staticmethod
def cosine_similarity_calc(str1, str2):
vectorizer = CountVectorizer().fit_transform([str1.lower(), str2.lower()])
vectors = vectorizer.toarray()
dot_product = vectors[0].dot(vectors[1])
norm_product = (vectors[0]**2).sum()**0.5 * (vectors[1]**2).sum()**0.5
return dot_product / norm_product if norm_product > 0 else 1
@staticmethod
def word_error_rate(reference, hypothesis):
ref_words = reference.lower().split()
hyp_words = hypothesis.lower().split()
d = np.zeros((len(ref_words) + 1) * (len(hyp_words) + 1), dtype=np.uint8).reshape((len(ref_words) + 1, len(hyp_words) + 1))
for i in range(len(ref_words) + 1):
for j in range(len(hyp_words) + 1):
if i == 0:
d[0][j] = j
elif j == 0:
d[i][0] = i
for i in range(1, len(ref_words) + 1):
for j in range(1, len(hyp_words) + 1):
if ref_words[i - 1] == hyp_words[j - 1]:
d[i][j] = d[i - 1][j - 1]
else:
substitution = d[i - 1][j - 1] + 1
insertion = d[i][j - 1] + 1
deletion = d[i - 1][j] + 1
d[i][j] = min(substitution, insertion, deletion)
return d[len(ref_words)][len(hyp_words)] / len(ref_words)
@staticmethod
def character_error_rate(reference, hypothesis):
ref_chars = list(reference.lower())
hyp_chars = list(hypothesis.lower())
d = np.zeros((len(ref_chars) + 1) * (len(hyp_chars) + 1), dtype=np.uint8).reshape((len(ref_chars) + 1, len(hyp_chars) + 1))
for i in range(len(ref_chars) + 1):
for j in range(len(hyp_chars) + 1):
if i == 0:
d[0][j] = j
elif j == 0:
d[i][0] = i
for i in range(1, len(ref_chars) + 1):
for j in range(1, len(hyp_chars) + 1):
if ref_chars[i - 1] == hyp_chars[j - 1]:
d[i][j] = d[i - 1][j - 1]
else:
substitution = d[i - 1][j - 1] + 1
insertion = d[i][j - 1] + 1
deletion = d[i - 1][j] + 1
d[i][j] = min(substitution, insertion, deletion)
return d[len(ref_chars)][len(hyp_chars)] / len(ref_chars)
def sentence_transformer_similarity(self, str1, str2, model_name):
if model_name == "all-mpnet-base-v2":
if self.mpnet_model is None:
self.mpnet_model = SentenceTransformer(model_name)
model = self.mpnet_model
elif model_name == "all-MiniLM-L6-v2":
if self.minilm_model is None:
self.minilm_model = SentenceTransformer(model_name)
model = self.minilm_model
else:
raise ValueError(f"Unsupported model: {model_name}")
embeddings = model.encode([str1, str2])
return np.dot(embeddings[0], embeddings[1]) / (np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1]))
NODE_CLASS_MAPPINGS = {
"StringSimilarity": StringSimilarity
}
NODE_DISPLAY_NAME_MAPPINGS = {
"StringSimilarity": "String Similarity Node"
}