diff --git a/conversione/validate.py b/conversione/validate.py index 51702d1..b9d71be 100644 --- a/conversione/validate.py +++ b/conversione/validate.py @@ -15,6 +15,7 @@ Uso: python conversione/validate.py # tutti gli stem python conversione/validate.py analisi1 # stem specifico python conversione/validate.py a b c # stem multipli + python conversione/validate.py --detail analisi1 # mostra dettaglio penalità """ import argparse @@ -28,45 +29,72 @@ from pathlib import Path _GRADES = [(90, "A"), (75, "B"), (60, "C"), (40, "D"), (0, "F")] -def _score(r: dict) -> int: +def _score(r: dict) -> tuple[int, list[str]]: """ - Calcola un punteggio 0-100 sulla qualità del Markdown prodotto. + Calcola un punteggio 0-100 sulla qualità del clean.md ai fini della + suddivisione in chunk e vettorizzazione. + Restituisce (score, lista_penalità_applicate). - Penalità: - struttura assente / piatta → −40 / −15 - backtick residui → −2/cad (max −30) - URL / watermark → −5/cad (max −15) - immagini residue → −5/cad (max −10) - dot-leader residui → −5/cad (max −10) - bare headers → −3/cad (max −15) - sezioni >1500ch >35/60% → −5 / −10 + Penalità struttura (il chunker non può operare senza header): + struttura assente (livello 0) → −40 + struttura piatta (livello 1) → −15 + + Penalità residui (finiscono nei vettori e degradano il retrieval): + backtick → −2/cad (max −20) + dot-leader → −5/cad (max −10) + URL / watermark → −5/cad (max −15) + immagini residue → −5/cad (max −10) +
inline (artefatti tabelle) → −2/cad (max −15) + simboli encoding (!/" residui) → −1/cad (max −10) + formule inline [N.M] → −1/cad (max −8) + + Penalità anomalie: + bare headers → −3/cad (max −15) + + Non penalizzate (il chunker le normalizza): + sezioni corte, sezioni lunghe, mediana, p25 """ - score = 100 + score = 100 + detail = [] structure = r.get("structure", {}) anomalie = r.get("anomalie", {}) residui = r.get("residui", {}) livello = structure.get("livello_struttura", 0) - n_h3 = max(structure.get("n_h3", 0), 1) + # ── Struttura ───────────────────────────────────────────────────────── if livello == 0: score -= 40 + detail.append("struttura assente −40") elif livello == 1: score -= 15 + detail.append("struttura piatta −15") - score -= min(30, residui.get("backtick", 0) * 2) - score -= min(15, residui.get("url", 0) * 5) - score -= min(10, residui.get("immagini", 0) * 5) - score -= min(10, residui.get("dotleader", 0) * 5) - score -= min(15, anomalie.get("bare_headers", 0) * 3) + # ── Residui ─────────────────────────────────────────────────────────── + def _pen(key: str, per_item: int, cap: int, label: str) -> None: + n = residui.get(key, 0) + if n: + p = min(cap, n * per_item) + nonlocal score + score -= p + detail.append(f"{label} ×{n} −{p}") - long_ratio = anomalie.get("long_sections", 0) / n_h3 - if long_ratio > 0.60: - score -= 10 - elif long_ratio > 0.35: - score -= 5 + _pen("backtick", 2, 20, "backtick") + _pen("dotleader", 5, 10, "dot-leader") + _pen("url", 5, 15, "url") + _pen("immagini", 5, 10, "immagini") + _pen("br_inline", 2, 15, "
inline") + _pen("simboli_encoding", 1, 10, "simboli encoding") + _pen("formule_inline", 1, 8, "formule inline") - return max(0, score) + # ── Anomalie ────────────────────────────────────────────────────────── + n_bare = anomalie.get("bare_headers", 0) + if n_bare: + p = min(15, n_bare * 3) + score -= p + detail.append(f"bare headers ×{n_bare} −{p}") + + return max(0, score), detail def _grade(score: int) -> str: @@ -75,7 +103,7 @@ def _grade(score: int) -> str: # ─── Validazione ───────────────────────────────────────────────────────────── -def validate(stems: list[str], project_root: Path) -> None: +def validate(stems: list[str], project_root: Path, detail: bool = False) -> None: conv_dir = project_root / "conversione" paths = ( @@ -99,9 +127,10 @@ def validate(stems: list[str], project_root: Path) -> None: header = ( f"{'stem':<{col}}" f"{'h2':>4}{'h3':>5} " - f"{'strategia':<20}" + f"{'strategia':<18}" f"{'bare':>5}{'corte':>6}{'lunghe':>7}" - f"{'backtick':>9}{'dotlead':>8}{'url':>4}" + f"{'btk':>5}{'br':>4}{'enc':>4}{'url':>4}" + f"{'med':>6}" f" {'voto':>4} grade" ) sep = "─" * len(header) @@ -115,26 +144,33 @@ def validate(stems: list[str], project_root: Path) -> None: print(f"{r['stem']:<{col}} (report.json non trovato)") continue - st = r.get("structure", {}) - an = r.get("anomalie", {}) - res = r.get("residui", {}) - s = _score(r) + st = r.get("structure", {}) + an = r.get("anomalie", {}) + res = r.get("residui", {}) + dist = r.get("distribution", {}) + s, pen = _score(r) scores.append(s) print( f"{r['stem']:<{col}}" f"{st.get('n_h2', 0):>4}" f"{st.get('n_h3', 0):>5} " - f"{st.get('strategia_chunking','?'):<20}" + f"{st.get('strategia_chunking','?'):<18}" f"{an.get('bare_headers', 0):>5}" f"{an.get('short_sections', 0):>6}" f"{an.get('long_sections', 0):>7}" - f"{res.get('backtick', 0):>9}" - f"{res.get('dotleader', 0):>8}" - f"{res.get('url', 0):>4}" + f"{res.get('backtick', 0):>5}" + f"{res.get('br_inline', 0):>4}" + f"{res.get('simboli_encoding', 0):>4}" + f"{res.get('url', 0):>4}" + f"{dist.get('mediana', 0):>6}" f" {s:>4} {_grade(s)}" ) + if detail and pen: + for p in pen: + print(f" {'':>{col}} ↳ {p}") + # ── Riepilogo ───────────────────────────────────────────────────────── print(sep) if scores: @@ -145,8 +181,8 @@ def validate(stems: list[str], project_root: Path) -> None: f"(A≥90 B≥75 C≥60 D≥40 F<40)" ) print( - "\nPenalità: struttura assente −40, backtick −2/cad, " - "bare headers −3/cad, sezioni >1500ch >35% −5\n" + "\nColonne: bare=header vuoti corte=sez<150ch lunghe=sez>1500ch " + "btk=backtick br=
inline enc=simboli encoding med=mediana chars\n" ) @@ -163,5 +199,10 @@ if __name__ == "__main__": metavar="STEM", help="stem da validare (es: analisi1). Ometti per tutti.", ) + parser.add_argument( + "--detail", "-d", + action="store_true", + help="mostra dettaglio penalità per ogni documento", + ) args = parser.parse_args() - validate(args.stems, Path(__file__).parent.parent) + validate(args.stems, Path(__file__).parent.parent, detail=args.detail)