Spaces:
Sleeping
Sleeping
Claude
refactor(arch): Sprint A3 — refactor cercles + importers (B-1, B-2, B-3, m-17)
fda1a60 unverified | """Calcul du diff mot-à-mot entre vérité terrain et sortie OCR. | |
| Produit une liste d'opérations sérialisables en JSON, consommée | |
| par le rendu JS dans le rapport HTML. | |
| Opérations possibles | |
| -------------------- | |
| {"op": "equal", "text": "mot"} | |
| {"op": "insert", "text": "mot"} -- présent dans l'OCR mais pas dans la GT | |
| {"op": "delete", "text": "mot"} -- présent dans la GT mais pas dans l'OCR | |
| {"op": "replace", "old": "…", "new": "…"} -- substitution (orange) | |
| """ | |
| from __future__ import annotations | |
| import difflib | |
| import re | |
| from typing import Any | |
| def _tokenize(text: str) -> list[str]: | |
| """Découpe le texte en tokens (mots + ponctuation + espaces).""" | |
| # Conserver les espaces comme tokens pour un rendu fidèle | |
| return re.split(r"(\s+)", text) | |
| def compute_word_diff(reference: str, hypothesis: str) -> list[dict[str, Any]]: | |
| """Calcule un diff mot-à-mot entre deux textes. | |
| Parameters | |
| ---------- | |
| reference: | |
| Texte de vérité terrain. | |
| hypothesis: | |
| Texte produit par le moteur OCR. | |
| Returns | |
| ------- | |
| list of dict | |
| Séquence d'opérations : equal, insert, delete, replace. | |
| """ | |
| ref_tokens = reference.split() | |
| hyp_tokens = hypothesis.split() | |
| matcher = difflib.SequenceMatcher(None, ref_tokens, hyp_tokens, autojunk=False) | |
| ops: list[dict[str, Any]] = [] | |
| for tag, i1, i2, j1, j2 in matcher.get_opcodes(): | |
| ref_chunk = " ".join(ref_tokens[i1:i2]) | |
| hyp_chunk = " ".join(hyp_tokens[j1:j2]) | |
| if tag == "equal": | |
| ops.append({"op": "equal", "text": ref_chunk}) | |
| elif tag == "insert": | |
| ops.append({"op": "insert", "text": hyp_chunk}) | |
| elif tag == "delete": | |
| ops.append({"op": "delete", "text": ref_chunk}) | |
| elif tag == "replace": | |
| ops.append({"op": "replace", "old": ref_chunk, "new": hyp_chunk}) | |
| return ops | |
| def compute_char_diff(reference: str, hypothesis: str) -> list[dict[str, Any]]: | |
| """Diff caractère par caractère — utile pour les tokens courts.""" | |
| matcher = difflib.SequenceMatcher(None, list(reference), list(hypothesis), autojunk=False) | |
| ops: list[dict[str, Any]] = [] | |
| for tag, i1, i2, j1, j2 in matcher.get_opcodes(): | |
| ref_chunk = reference[i1:i2] | |
| hyp_chunk = hypothesis[j1:j2] | |
| if tag == "equal": | |
| ops.append({"op": "equal", "text": ref_chunk}) | |
| elif tag == "insert": | |
| ops.append({"op": "insert", "text": hyp_chunk}) | |
| elif tag == "delete": | |
| ops.append({"op": "delete", "text": ref_chunk}) | |
| elif tag == "replace": | |
| ops.append({"op": "replace", "old": ref_chunk, "new": hyp_chunk}) | |
| return ops | |
| def diff_stats(ops: list[dict[str, Any]]) -> dict[str, int]: | |
| """Compte le nombre d'insertions, suppressions et substitutions.""" | |
| stats = {"equal": 0, "insert": 0, "delete": 0, "replace": 0} | |
| for op in ops: | |
| stats[op["op"]] += 1 | |
| return stats | |