OCR-Demo / utils /diff_engine.py
thangvckeygen's picture
Deploy 7-Model OCR Benchmark Space with Bounding Box Visualizer
b611f38
Raw
History Blame Contribute Delete
4.43 kB
"""
Word-Level, Character-Level Diff Engine & Consensus Matrix Generator
Uses difflib.SequenceMatcher to compare OCR outputs across multiple models.
Strictly processes ONLY valid successful OCR outputs.
"""
import difflib
import re
from typing import Dict, Any, List, Tuple, Optional
def tokenize_text(text: Optional[str], mode: str = "word") -> List[str]:
"""
Tokenizes text into words or characters for diffing.
"""
if not text or not isinstance(text, str):
return []
if mode == "char":
return list(text)
# Word tokenization (splits by whitespace while preserving punctuation boundaries)
return re.findall(r"\S+|\n", text)
def compute_similarity_ratio(text_a: Optional[str], text_b: Optional[str]) -> Optional[float]:
"""
Computes normalized similarity ratio between two valid texts [0.0 - 1.0].
Returns None if either text is invalid/empty.
"""
if text_a is None or text_b is None:
return None
if not text_a and not text_b:
return 1.0
matcher = difflib.SequenceMatcher(None, text_a, text_b)
return round(matcher.ratio(), 4)
def compute_diff(text_a: Optional[str], text_b: Optional[str], mode: str = "word") -> List[Dict[str, Any]]:
"""
Computes detailed word or character level diff between text_a (baseline) and text_b (comparison).
Returns list of chunks with type: 'equal' | 'delete' | 'insert' | 'replace'.
"""
if text_a is None and text_b is None:
return []
tokens_a = tokenize_text(text_a or "", mode=mode)
tokens_b = tokenize_text(text_b or "", mode=mode)
matcher = difflib.SequenceMatcher(None, tokens_a, tokens_b)
diff_chunks = []
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
chunk_a = " ".join(tokens_a[i1:i2]) if mode == "word" else "".join(tokens_a[i1:i2])
chunk_b = " ".join(tokens_b[j1:j2]) if mode == "word" else "".join(tokens_b[j1:j2])
diff_chunks.append({
"tag": tag,
"text_a": chunk_a,
"text_b": chunk_b,
"span_a": [i1, i2],
"span_b": [j1, j2]
})
return diff_chunks
def generate_consensus_matrix(model_outputs: Dict[str, str]) -> Dict[str, Any]:
"""
Builds a pairwise similarity and agreement matrix ONLY across valid successful models.
Failed/Error models must be filtered out before calling or are skipped.
"""
# Filter out empty or None texts
valid_outputs = {k: v for k, v in model_outputs.items() if v and isinstance(v, str) and v.strip()}
model_names = list(valid_outputs.keys())
n = len(model_names)
if n == 0:
return {
"models": [],
"matrix": {},
"average_agreement_pct": {},
"consensus_leader": None,
"disagreements": [],
"status": "NO_VALID_MODELS"
}
matrix = {}
average_agreement = {}
for name_a in model_names:
matrix[name_a] = {}
total_sim = 0.0
comparisons = 0
for name_b in model_names:
if name_a == name_b:
sim = 1.0
else:
sim = compute_similarity_ratio(valid_outputs[name_a], valid_outputs[name_b]) or 0.0
total_sim += sim
comparisons += 1
matrix[name_a][name_b] = round(sim * 100, 1)
if comparisons > 0:
average_agreement[name_a] = round((total_sim / comparisons) * 100, 1)
else:
average_agreement[name_a] = 100.0 # Only 1 model exists
# Determine highest consensus model
consensus_leader = max(average_agreement.items(), key=lambda x: x[1])[0] if average_agreement else None
# Flag genuine disagreements among valid models
disagreements = []
for i in range(n):
for j in range(i + 1, n):
mA = model_names[i]
mB = model_names[j]
sim = matrix[mA][mB]
if sim < 75.0:
disagreements.append({
"model_a": mA,
"model_b": mB,
"similarity_pct": sim,
"severity": "high" if sim < 50.0 else "medium"
})
return {
"models": model_names,
"matrix": matrix,
"average_agreement_pct": average_agreement,
"consensus_leader": consensus_leader,
"disagreements": disagreements,
"status": "VALID"
}