2026-04-16 15:53:46 +02:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
conversione/validate.py — Validazione batch di tutti gli stem convertiti
|
|
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
Legge i report.json prodotti da pipeline.py, stampa una tabella di stato
|
|
|
|
|
|
e assegna un voto (0-100) a ogni documento per misurare la bontà del
|
|
|
|
|
|
Markdown prodotto.
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
Voto:
|
|
|
|
|
|
90-100 A — ottimo, pronto per il chunker
|
|
|
|
|
|
75-89 B — buono, qualche sezione lunga ma accettabile
|
|
|
|
|
|
60-74 C — accettabile, anomalie minori da verificare
|
|
|
|
|
|
40-59 D — da rivedere, problemi strutturali o residui evidenti
|
|
|
|
|
|
0-39 F — da riprocessare, struttura assente o testo corrotto
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
|
|
|
|
|
Uso:
|
|
|
|
|
|
python conversione/validate.py # tutti gli stem
|
|
|
|
|
|
python conversione/validate.py analisi1 # stem specifico
|
2026-04-16 16:05:03 +02:00
|
|
|
|
python conversione/validate.py --stem analisi1
|
|
|
|
|
|
python conversione/validate.py --analisi1 # compatibilità
|
2026-04-16 15:53:46 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
2026-04-16 16:05:03 +02:00
|
|
|
|
import argparse
|
2026-04-16 15:53:46 +02:00
|
|
|
|
import sys
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
# ─── Punteggio ───────────────────────────────────────────────────────────────
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
def _score(r: dict) -> int:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Calcola un punteggio 0-100 sulla qualità del Markdown prodotto.
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
Penalità:
|
|
|
|
|
|
- struttura assente o piatta → -40 / -15
|
|
|
|
|
|
- backtick residui nel testo → -2 per occorrenza (max -30)
|
|
|
|
|
|
- URL / watermark residui → -5 per occorrenza (max -15)
|
|
|
|
|
|
- immagini residue → -5 per occorrenza (max -10)
|
|
|
|
|
|
- dot-leader residui → -5 per occorrenza (max -10)
|
|
|
|
|
|
- header senza titolo (bare) → -3 per occorrenza (max -15)
|
|
|
|
|
|
- troppe sezioni > 1500 chars → -5 / -10 (in % sul totale h3)
|
|
|
|
|
|
"""
|
|
|
|
|
|
score = 100
|
2026-04-16 15:53:46 +02:00
|
|
|
|
structure = r.get("structure", {})
|
|
|
|
|
|
anomalie = r.get("anomalie", {})
|
|
|
|
|
|
residui = r.get("residui", {})
|
|
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
livello = structure.get("livello_struttura", 0)
|
|
|
|
|
|
n_h3 = max(structure.get("n_h3", 0), 1)
|
|
|
|
|
|
|
|
|
|
|
|
# Struttura
|
|
|
|
|
|
if livello == 0:
|
|
|
|
|
|
score -= 40
|
|
|
|
|
|
elif livello == 1:
|
|
|
|
|
|
score -= 15
|
|
|
|
|
|
|
|
|
|
|
|
# Residui nel testo
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# Anomalie strutturali
|
|
|
|
|
|
score -= min(15, anomalie.get("bare_headers", 0) * 3)
|
|
|
|
|
|
|
|
|
|
|
|
# Sezioni troppo lunghe (in % sul totale delle sezioni ###)
|
|
|
|
|
|
long_ratio = anomalie.get("long_sections", 0) / n_h3
|
|
|
|
|
|
if long_ratio > 0.6:
|
|
|
|
|
|
score -= 10
|
|
|
|
|
|
elif long_ratio > 0.35:
|
|
|
|
|
|
score -= 5
|
|
|
|
|
|
|
|
|
|
|
|
return max(0, score)
|
|
|
|
|
|
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
def _grade(score: int) -> str:
|
|
|
|
|
|
if score >= 90: return "A"
|
|
|
|
|
|
if score >= 75: return "B"
|
|
|
|
|
|
if score >= 60: return "C"
|
|
|
|
|
|
if score >= 40: return "D"
|
|
|
|
|
|
return "F"
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
# ─── CLI ─────────────────────────────────────────────────────────────────────
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
def _normalize_target(token: str) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Normalizza un target CLI in stem:
|
|
|
|
|
|
- analisi1
|
|
|
|
|
|
- --analisi1 (compatibilità)
|
|
|
|
|
|
- conversione/analisi1/report.json
|
|
|
|
|
|
- analisi1.pdf / analisi1.md / report.json
|
|
|
|
|
|
"""
|
|
|
|
|
|
raw = token.strip()
|
|
|
|
|
|
if not raw:
|
|
|
|
|
|
return raw
|
|
|
|
|
|
|
|
|
|
|
|
# Compatibilità con invocazione tipo: --analisi1
|
|
|
|
|
|
if raw.startswith("--") and len(raw) > 2:
|
|
|
|
|
|
raw = raw[2:]
|
|
|
|
|
|
|
|
|
|
|
|
p = Path(raw)
|
|
|
|
|
|
|
|
|
|
|
|
# Path diretto al report
|
|
|
|
|
|
if p.name == "report.json" and p.parent.name:
|
|
|
|
|
|
return p.parent.name
|
|
|
|
|
|
|
|
|
|
|
|
name = p.name
|
|
|
|
|
|
if name.endswith((".pdf", ".md", ".json")):
|
|
|
|
|
|
name = Path(name).stem
|
|
|
|
|
|
|
|
|
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_cli_args(argv: list[str]) -> list[str]:
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
|
description="Valida i report Markdown prodotti in conversione/<stem>/report.json"
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"targets",
|
|
|
|
|
|
nargs="*",
|
|
|
|
|
|
help="Stem, file o path da validare (es: analisi1 oppure conversione/analisi1/report.json)",
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"-s",
|
|
|
|
|
|
"--stem",
|
|
|
|
|
|
action="append",
|
|
|
|
|
|
default=[],
|
|
|
|
|
|
help="Stem specifico (ripetibile, es: --stem analisi1 --stem nietzsche)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
args, unknown = parser.parse_known_args(argv)
|
|
|
|
|
|
|
|
|
|
|
|
targets = [*args.targets, *args.stem]
|
|
|
|
|
|
|
|
|
|
|
|
# Compatibilità: `python validate.py --analisi1`
|
|
|
|
|
|
for tok in unknown:
|
|
|
|
|
|
if tok.startswith("--") and len(tok) > 2:
|
|
|
|
|
|
targets.append(tok[2:])
|
|
|
|
|
|
else:
|
|
|
|
|
|
parser.error(f"Argomento non riconosciuto: {tok}")
|
|
|
|
|
|
|
|
|
|
|
|
stems = []
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
for t in targets:
|
|
|
|
|
|
stem = _normalize_target(t)
|
|
|
|
|
|
if not stem or stem in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(stem)
|
|
|
|
|
|
stems.append(stem)
|
|
|
|
|
|
|
|
|
|
|
|
return stems
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── Validazione ─────────────────────────────────────────────────────────────
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
|
|
|
|
|
def validate(stems: list[str], project_root: Path) -> None:
|
|
|
|
|
|
conv_dir = project_root / "conversione"
|
|
|
|
|
|
|
|
|
|
|
|
if stems:
|
|
|
|
|
|
paths = [conv_dir / s / "report.json" for s in stems]
|
|
|
|
|
|
else:
|
|
|
|
|
|
paths = sorted(conv_dir.glob("*/report.json"))
|
|
|
|
|
|
|
|
|
|
|
|
if not paths:
|
|
|
|
|
|
print("Nessun report.json trovato in conversione/*/")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for path in paths:
|
|
|
|
|
|
if not path.exists():
|
|
|
|
|
|
rows.append({"stem": path.parent.name, "_missing": True})
|
|
|
|
|
|
continue
|
|
|
|
|
|
r = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
rows.append(r)
|
|
|
|
|
|
|
|
|
|
|
|
# ── Intestazione ─────────────────────────────────────────────────────
|
2026-04-16 16:05:03 +02:00
|
|
|
|
col_stem = max(len(r.get("stem", "stem")) for r in rows) + 2
|
2026-04-16 15:53:46 +02:00
|
|
|
|
header = (
|
|
|
|
|
|
f"{'stem':<{col_stem}}"
|
|
|
|
|
|
f"{'h2':>4}{'h3':>5} "
|
|
|
|
|
|
f"{'strategia':<20}"
|
|
|
|
|
|
f"{'bare':>5}{'corte':>6}{'lunghe':>7}"
|
|
|
|
|
|
f"{'backtick':>9}{'dotlead':>8}{'url':>4}"
|
2026-04-16 16:05:03 +02:00
|
|
|
|
f" {'voto':>4} {'grade'}"
|
2026-04-16 15:53:46 +02:00
|
|
|
|
)
|
|
|
|
|
|
sep = "─" * len(header)
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(header)
|
|
|
|
|
|
print(sep)
|
|
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
scores = []
|
|
|
|
|
|
scored_docs = []
|
|
|
|
|
|
|
2026-04-16 15:53:46 +02:00
|
|
|
|
# ── Righe ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
if r.get("_missing"):
|
|
|
|
|
|
print(f"{r['stem']:<{col_stem}} (report.json non trovato)")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
stem = r.get("stem", "?")
|
|
|
|
|
|
structure = r.get("structure", {})
|
|
|
|
|
|
anomalie = r.get("anomalie", {})
|
|
|
|
|
|
residui = r.get("residui", {})
|
|
|
|
|
|
|
|
|
|
|
|
h2 = structure.get("n_h2", 0)
|
|
|
|
|
|
h3 = structure.get("n_h3", 0)
|
|
|
|
|
|
strat = structure.get("strategia_chunking", "?")
|
|
|
|
|
|
bare = anomalie.get("bare_headers", 0)
|
|
|
|
|
|
corte = anomalie.get("short_sections", 0)
|
|
|
|
|
|
lunghe = anomalie.get("long_sections", 0)
|
|
|
|
|
|
backtick = residui.get("backtick", 0)
|
|
|
|
|
|
dotlead = residui.get("dotleader", 0)
|
|
|
|
|
|
url = residui.get("url", 0)
|
2026-04-16 16:05:03 +02:00
|
|
|
|
|
|
|
|
|
|
s = _score(r)
|
|
|
|
|
|
g = _grade(s)
|
|
|
|
|
|
scores.append(s)
|
|
|
|
|
|
scored_docs.append((stem, s, g))
|
2026-04-16 15:53:46 +02:00
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{stem:<{col_stem}}"
|
|
|
|
|
|
f"{h2:>4}{h3:>5} "
|
|
|
|
|
|
f"{strat:<20}"
|
|
|
|
|
|
f"{bare:>5}{corte:>6}{lunghe:>7}"
|
|
|
|
|
|
f"{backtick:>9}{dotlead:>8}{url:>4}"
|
2026-04-16 16:05:03 +02:00
|
|
|
|
f" {s:>4} {g}"
|
2026-04-16 15:53:46 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-16 16:05:03 +02:00
|
|
|
|
# ── Riepilogo ─────────────────────────────────────────────────────────
|
2026-04-16 15:53:46 +02:00
|
|
|
|
print(sep)
|
2026-04-16 16:05:03 +02:00
|
|
|
|
if scores:
|
|
|
|
|
|
media = sum(scores) / len(scores)
|
|
|
|
|
|
grade_media = _grade(int(media))
|
|
|
|
|
|
print(f"Documenti: {len(scores)} "
|
|
|
|
|
|
f"Voto medio: {media:.0f}/100 {grade_media} "
|
|
|
|
|
|
f"(A≥90 B≥75 C≥60 D≥40 F<40)")
|
|
|
|
|
|
if len(scored_docs) == 1:
|
|
|
|
|
|
stem, score, grade = scored_docs[0]
|
|
|
|
|
|
print(f"Voto finale Markdown ({stem}): {score}/100 {grade}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
voti = ", ".join(
|
|
|
|
|
|
f"{stem}={score}/100 {grade}"
|
|
|
|
|
|
for stem, score, grade in scored_docs
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"Voti Markdown: {voti}")
|
2026-04-16 15:53:46 +02:00
|
|
|
|
print()
|
2026-04-16 16:05:03 +02:00
|
|
|
|
print("Penalità: struttura assente −40, backtick residui −2/cad, "
|
|
|
|
|
|
"bare headers −3/cad, sezioni >1500ch >35% −5")
|
2026-04-16 15:53:46 +02:00
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
project_root = Path(__file__).parent.parent
|
2026-04-16 16:05:03 +02:00
|
|
|
|
stems = _parse_cli_args(sys.argv[1:])
|
2026-04-16 15:53:46 +02:00
|
|
|
|
validate(stems, project_root)
|