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:
+44
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Levenshtein
|
||||
scikit-learn
|
||||
numpy
|
||||
sentence-transformers
|
||||
difflib
|
||||
Reference in New Issue
Block a user