diff --git a/.gitignore b/.gitignore index 4b233cc..369f1d6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ Thumbs.db # Report generati dagli script step-0/*_step0_report.txt +step-1/*_step1_report.txt + diff --git a/step-1/inspect_pdf.py b/step-1/inspect_pdf.py new file mode 100644 index 0000000..0c2bfdd --- /dev/null +++ b/step-1/inspect_pdf.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Step 1 — Ispezione automatica PDF + +Analizza il PDF pagina per pagina e produce un report con score (0–100) +e lista dei problemi per pagina. Serve per capire la qualità del documento +e mappare i problemi prima della revisione manuale (step 4). + +Uso: + python step1/inspect.py + +Output: + step1/_step1_report.txt +""" + +import re +import sys +import statistics +from collections import Counter +from datetime import datetime +from pathlib import Path + + +# ── Penalità per il calcolo dello score ─────────────────────────────────── +SYLLABIF_PENALTY = 0.3 # per occorrenza di sillabazione +COLUMN_PENALTY = 3.0 # per pagina con layout a colonne +UNICODE_PENALTY = 1.5 # per pagina con caratteri anomali +EMPTY_PENALTY = 1.0 # per pagina vuota +HEADER_FOOTER_PEN = 5.0 # fisso se intestazioni/piè ripetitivi rilevati + + +def inspect_pdf(pdf_path: str, save: bool = True) -> None: + try: + import pdfplumber + except ImportError: + print("Errore: pdfplumber non è installato.") + print(" pip install pdfplumber") + sys.exit(1) + + path = Path(pdf_path) + if not path.exists(): + print(f"Errore: file non trovato — {pdf_path}") + sys.exit(1) + + lines = [] + + def out(text=""): + lines.append(text) + print(text) + + out("Step 1 — Ispezione automatica PDF") + out(f"File: {path.name}") + out(f"Data: {datetime.now().strftime('%Y-%m-%d %H:%M')}") + out("=" * 50) + + # ── Lettura pagine ───────────────────────────────────────────────────── + with pdfplumber.open(path) as pdf: + n_pages = len(pdf.pages) + pages_text = [page.extract_text() or "" for page in pdf.pages] + + # ── Analisi per pagina ───────────────────────────────────────────────── + issues = [] # (page_num, descrizione) — page_num=0 → problema globale + deductions = 0.0 + + first_lines = [] # prima riga significativa di ogni pagina (per header) + last_lines = [] # ultima riga significativa di ogni pagina (per footer) + + for i, text in enumerate(pages_text): + page_num = i + 1 + stripped = text.strip() + + # 1. Pagina vuota + if len(stripped) < 50: + issues.append((page_num, "pagina vuota")) + deductions += EMPTY_PENALTY + continue + + page_lines = text.splitlines() + nonempty = [l.strip() for l in page_lines if l.strip()] + + # Raccogli prima/ultima riga per il controllo header/footer + if nonempty: + first_lines.append(nonempty[0]) + last_lines.append(nonempty[-1]) + + # 2. Sillabazione a fine riga (es. "estra-" + a capo) + syllabif = sum( + 1 for line in page_lines + if re.search(r'\b\w{2,}-$', line.rstrip()) + ) + if syllabif: + label = "occorrenza" if syllabif == 1 else "occorrenze" + issues.append((page_num, f"sillabazione rilevata ({syllabif} {label})")) + deductions += syllabif * SYLLABIF_PENALTY + + # 3. Layout a colonne (righe molto corte e numerose) + if len(nonempty) >= 10: + median_len = statistics.median(len(l) for l in nonempty) + short_ratio = sum(1 for l in nonempty if len(l) < median_len * 0.4) / len(nonempty) + if short_ratio > 0.35: + issues.append((page_num, f"possibile layout a colonne ({short_ratio:.0%} righe corte)")) + deductions += COLUMN_PENALTY + + # 4. Caratteri Unicode anomali + # (control chars esclusi \n \t \r, replacement char, PUA block) + anomalies = re.findall( + r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\ufffd\ue000-\uf8ff]', text + ) + if anomalies: + issues.append((page_num, f"caratteri Unicode anomali ({len(anomalies)} trovati)")) + deductions += UNICODE_PENALTY + + # ── Intestazioni e piè di pagina ripetitivi ──────────────────────────── + def _check_repetition(line_list: list, label: str) -> None: + nonlocal deductions + if not line_list: + return + threshold = max(3, len(line_list) * 0.25) + repeated = [ + (txt, cnt) for txt, cnt in Counter(line_list).items() + if cnt >= threshold and len(txt) > 3 + ] + if repeated: + deductions += HEADER_FOOTER_PEN + for txt, cnt in repeated[:3]: + issues.append((0, f"{label} ripetitivo: \"{txt[:45]}\" ({cnt} volte)")) + + _check_repetition(first_lines, "intestazione") + _check_repetition(last_lines, "piè di pagina") + + # ── Score ────────────────────────────────────────────────────────────── + score = max(0, round(100 - deductions)) + + # ── Riepilogo ────────────────────────────────────────────────────────── + pages_with_issues = len({p for p, _ in issues if p > 0}) + out() + out(f"Score: {score}/100") + out(f"Pagine totali: {n_pages}") + out(f"Pagine con problemi: {pages_with_issues}") + out() + + if issues: + global_issues = [(p, d) for p, d in issues if p == 0] + page_issues = sorted([(p, d) for p, d in issues if p > 0]) + for _, desc in global_issues: + out(f" ⚠️ {desc}") + for page_num, desc in page_issues: + out(f" Pagina {page_num:>4}: {desc}") + else: + out(" Nessun problema rilevato.") + + out() + + # ── Prossimi passi ───────────────────────────────────────────────────── + out("PROSSIMI PASSI:") + if score >= 70: + out(" → conversione con marker funzionerà bene") + elif score >= 40: + out(" → conversione possibile, attendi più errori nella revisione") + else: + out(" → qualità bassa — valuta una fonte PDF migliore") + + attention_pages = sorted({p for p, _ in issues if p > 0}) + if attention_pages: + sample = ", ".join(str(p) for p in attention_pages[:10]) + if len(attention_pages) > 10: + sample += f" … e altre {len(attention_pages) - 10}" + out(f" → attenzione alle pagine {sample} nella revisione manuale") + out() + + _maybe_save(lines, path, save) + + +def _maybe_save(lines: list, pdf_path: Path, save: bool) -> None: + if not save: + return + script_dir = Path(__file__).parent + out_file = script_dir / f"{pdf_path.stem}_step1_report.txt" + out_file.write_text("\n".join(lines), encoding="utf-8") + print(f"Report salvato in: {out_file}") + + +if __name__ == "__main__": + project_root = Path(__file__).parent.parent + sources_dir = project_root / "sources" + + if not sources_dir.exists(): + print(f"Errore: cartella sources/ non trovata in {project_root}") + sys.exit(1) + + pdfs = sorted(sources_dir.glob("*.pdf")) + if not pdfs: + print(f"Errore: nessun PDF trovato in {sources_dir}") + sys.exit(1) + + for pdf in pdfs: + inspect_pdf(str(pdf), save=True) + if len(pdfs) > 1: + print("-" * 50)