Initial commit - ComfyUI String Similarity Node

- Advanced text comparison for ComfyUI
- Multiple similarity algorithms
- Support for semantic similarity
- MIT License
This commit is contained in:
gabe-init
2025-05-25 13:19:14 -05:00
commit 37e8b905eb
5 changed files with 369 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Environment
.env
.venv
env/
venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Secrets
api_keys.json
secrets.json
*.key
+131
View File
@@ -0,0 +1,131 @@
# ComfyUI String Similarity Node
A comprehensive text comparison node for ComfyUI that provides multiple algorithms to measure string similarity, useful for OCR validation, text comparison, and quality assessment workflows.
## Features
- Multiple similarity algorithms in one node
- Detailed comparison metrics
- Support for both character-level and semantic similarity
- Useful for OCR accuracy assessment
- Compatible with ComfyUI's text processing pipeline
## Supported Algorithms
1. **Levenshtein Distance**: Edit distance between strings
2. **SequenceMatcher**: Python's built-in sequence matching
3. **Jaccard Similarity**: Set-based word comparison
4. **Cosine Similarity**: Vector space model comparison
5. **Word Error Rate (WER)**: Word-level accuracy metric
6. **Character Error Rate (CER)**: Character-level accuracy metric
7. **SentenceTransformer-MPNET**: Deep learning semantic similarity
8. **SentenceTransformer-MiniLM**: Lightweight semantic similarity
## Installation
1. Clone this repository into your ComfyUI custom_nodes folder:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/yourusername/ComfyUI-String-Similarity
```
2. Install the required dependencies:
```bash
pip install -r requirements.txt
```
3. Restart ComfyUI
## Usage
1. Add the "String Similarity Node" to your workflow
2. Connect two text inputs (actual_text and ocr_text)
3. Select the comparison algorithm
4. The node outputs a formatted string with similarity metrics
### Input Parameters
- **actual_text**: The reference or ground truth text
- **ocr_text**: The text to compare (e.g., OCR output)
- **algorithm**: Choose from 8 different similarity algorithms
### Output
- **STRING**: Formatted result showing the similarity score and additional metrics
## Algorithm Details
### Distance-Based Metrics
- **Levenshtein**: Minimum edits needed to transform one string to another
- **SequenceMatcher**: Ratio of matching subsequences
- **CER/WER**: Error rates at character and word levels
### Similarity Metrics
- **Jaccard**: Intersection over union of word sets
- **Cosine**: Angle between text vectors
### Semantic Similarity
- **SentenceTransformers**: Neural models that understand meaning
- MPNET: More accurate but slower
- MiniLM: Faster with good accuracy
## Use Cases
1. **OCR Quality Assessment**: Compare OCR output with ground truth
2. **Text Validation**: Check if generated text matches expected output
3. **Duplicate Detection**: Find similar text passages
4. **Content Matching**: Semantic similarity for paraphrases
## Example Output
```
Actual Text: 'Hello World'
OCR Text: 'Helo World'
Levenshtein Distance: 1
Levenshtein Similarity: 0.91
```
## Performance Notes
- Simple algorithms (Levenshtein, Jaccard) are fast
- SentenceTransformers require model downloading on first use
- Models are cached after initial download
## Troubleshooting
- **Import errors**: Ensure all requirements are installed
- **Model download fails**: Check internet connection
- **Memory issues**: Use MiniLM for lower memory usage
## Requirements
- ComfyUI
- Python packages: Levenshtein, scikit-learn, numpy, sentence-transformers
## License
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
from .node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+186
View File
@@ -0,0 +1,186 @@
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"
}
+5
View File
@@ -0,0 +1,5 @@
Levenshtein
scikit-learn
numpy
sentence-transformers
difflib