diff --git a/conversione/pipeline.py b/conversione/pipeline.py index 0a6014b..03cde62 100644 --- a/conversione/pipeline.py +++ b/conversione/pipeline.py @@ -349,6 +349,12 @@ def apply_transforms(text: str) -> tuple[str, dict]: "toc_rimosso": False, "n_immagini_rimosse": 0, "n_accenti_corretti": 0, + "n_moltiplicazioni_corrette": 0, + "n_micro_corretti": 0, + "n_br_rimossi": 0, + "n_formule_rimossi": 0, + "n_garbage_headers_rimossi": 0, + "n_frontmatter_rimossi": 0, "n_dotleader_rimossi": 0, "n_header_concat_fixati": 0, "n_articoli_estratti": 0, @@ -357,12 +363,26 @@ def apply_transforms(text: str) -> tuple[str, dict]: "n_header_allcaps": 0, "n_sezioni_numerate": 0, "n_paragrafi_uniti": 0, + "n_tabsep_rimossi": 0, } # 0. Rimuovi riferimenti immagini (artefatti opendataloader-pdf) stats["n_immagini_rimosse"] = len(re.findall(r"!\[[^\]]*\]\([^)]*\)", text)) text = re.sub(r"!\[[^\]]*\]\([^)]*\)\s*", "", text) + # 0_br. Rimuovi tag
residui da tabelle e blocchi formula PDF + # Nelle celle di tabella produce spazio; nel testo inline elimina rumore. + stats["n_br_rimossi"] = len(re.findall(r"
", text, re.IGNORECASE)) + text = re.sub(r"
\s*", " ", text, flags=re.IGNORECASE) + + # 0_tabsep. Rimuovi separatori tabella PDF: "| |" (riga vuota) e "|---|" (separatore). + # Nascono da tabelle non strutturate nel PDF. Rimossi PRIMA del merge paragrafi + # (step 5) altrimenti "|---|" viene fuso con il paragrafo successivo producendo + # righe tipo "|---| Una caratterizzazione analoga...". + _pat_tabsep = re.compile(r"(?m)^\|\s*\|\s*$|^\|---\|?\s*$") + stats["n_tabsep_rimossi"] = len(_pat_tabsep.findall(text)) + text = _pat_tabsep.sub("", text) + # 0a. Fix artefatti backtick da PDF LaTeX: `e→è, e`→è, sar`a→sarà, ecc. # I PDF prodotti da LaTeX estraggono gli accenti gravi come backtick separati # dalla vocale accentata. Esempi: "`e" → "è", "puo`" → "può", "sar`a" → "sarà" @@ -383,6 +403,30 @@ def apply_transforms(text: str) -> tuple[str, dict]: text = re.sub(r"`", "", text) stats["n_accenti_corretti"] += n_bt_orfani + # 0a2. Fix segno di moltiplicazione "→× (encoding font PDF non-standard) + # Esempi: 2"107 → 2×107, 2"(10-2 m)3 → 2×(10-2 m)3 + # Lookbehind SOLO su cifra: evita falsi positivi tipo t1"t0 (→ limite) + # o h"hf (→ differenza) dove la lettera prima della " non indica prodotto. + _n_cross = len(re.findall(r'(?<=[0-9])"(?=[0-9(])', text)) + text = re.sub(r'(?<=[0-9])"(?=[0-9(])', '×', text) + stats["n_moltiplicazioni_corrette"] = _n_cross + + # 0a3. Fix prefisso micro !→µ prima di unità SI note + # "1 !m" → "1 µm", "1 !A" → "1 µA", "3 !s-1" → "3 µs-1" + # Pattern stretto: cifra + spazio opzionale + ! + lettera unità SI a scelta ristretta. + # Non tocca "4! steradianti" (spazio dopo !) né "mol!K" (non preceduto da cifra). + _SI_UNITS_RE = r'[mAsgVWFHTKNJClΩ]' + _n_micro = len(re.findall(rf'\d\s*!(?={_SI_UNITS_RE})', text)) + text = re.sub(rf'(\d)\s*!({_SI_UNITS_RE})', r'\1 µ\2', text) + stats["n_micro_corretti"] = _n_micro + + # 0a4. Rimuovi label formule inline [N.M] — es. [3.4], [10.7], [5.25] + # Non aggiungono valore semantico per il RAG; restano come rumore numerico. + # Preserva [N] senza punto (riferimenti bibliografici/note legittime). + n_form_before = len(re.findall(r"\[\d+\.\d+\]", text)) + text = re.sub(r"\s*\[\d+\.\d+\]\s*", " ", text) + stats["n_formule_rimossi"] = n_form_before + # 0b_pre. Rimuovi righe con dot-leader (voci di indice/sommario) # Esempi: "- 1.1 Alfabeto greco . . . . . . 1", "3.4 Continuità . . . . 205" # Pattern: almeno 3 occorrenze di ". " consecutive nella riga @@ -624,6 +668,9 @@ def apply_transforms(text: str) -> tuple[str, dict]: i += 1 text = "\n\n".join(merged) + # Secondo pass: rimuovi prefisso |---| eventualmente rimasto dopo il merge paragrafi + text = re.sub(r"(?m)^\|---\|\s*", "", text) + # 6. Normalizza whitespace multiplo interno alle righe lines = text.split("\n") text = "\n".join( @@ -655,6 +702,61 @@ def apply_transforms(text: str) -> tuple[str, dict]: text, n_titoli = _merge_title_headers(text) stats["n_titoli_uniti"] = n_titoli + # 9c. Rimuovi garbage headers: header ### senza parole reali o con solo + # abbreviazioni matematiche. Esempi: "### ( vm)", "### #", "### ! =", + # "### (am)", "### 2. Il valore di hf si deter- mina risolvendo mg(h!hf)" + # Questi nascono da espressioni matematiche scambiate per titoli di sezione. + # Il corpo rimane nel testo e viene accorpato alla sezione precedente. + def _is_garbage_header(content: str) -> bool: + # Header con prefisso "..." — frammento di formula (es. "...Di", "...vi") + if content.lstrip().startswith("..."): + return True + # Nessuna sequenza alfabetica ≥ 2 char + if not re.search(r"[A-Za-zÀ-ÿ]{2,}", content): + return True + # Abbreviazione corta in parentesi opzionali: "(vm)", "( am)", "(am)" + if re.fullmatch(r"\(?\s*[A-Za-z]{1,4}\s*\)?", content.strip()): + return True + # Header molto lungo (>60ch) con artefatti formula inline + if len(content) > 60 and re.search(r"[!%#]\w|\w[!%#]|\b\w+-\s*\w", content): + return True + return False + + lines = text.split("\n") + new_lines = [] + for line in lines: + m = re.match(r"^#{1,6} (.+)$", line) + if m and _is_garbage_header(m.group(1)): + stats["n_garbage_headers_rimossi"] += 1 + continue + new_lines.append(line) + text = "\n".join(new_lines) + text = re.sub(r"\n{3,}", "\n\n", text) + + # 9d. Rimuovi sezioni frontmatter: header senza numero + corpo corto con + # URL, email, affiliazione, copyright, edizione — metadati non-contenuto. + _FM_RE = re.compile( + r"https?://|www\.|@[A-Za-z]|\bUniversit[àa]\b|\bDipartimento\b|" + r"\bCopyright\b|\bLicenza\b|\bEdizione\b|" + r"protetto da|tutti i diritti", + re.IGNORECASE, + ) + blocks = re.split(r"\n{2,}", text) + cleaned = [] + for i, block in enumerate(blocks): + stripped = block.strip() + if not re.match(r"^### ", stripped) or re.match(r"^### \d", stripped): + cleaned.append(block) + continue + body = blocks[i + 1].strip() if i + 1 < len(blocks) else "" + is_fm_body = len(body) < 250 and _FM_RE.search(body) + is_fm_hdr = _FM_RE.search(stripped) + if is_fm_body or is_fm_hdr: + stats["n_frontmatter_rimossi"] += 1 + continue + cleaned.append(block) + text = re.sub(r"\n{3,}", "\n\n", "\n\n".join(cleaned)) + return text, stats @@ -831,10 +933,13 @@ def build_report( return hits residui = { - "backtick": _scan(r"`"), - "dotleader": _scan(r"(?:\. ){3,}"), - "url": _scan(r"^(https?://|www\.)\S+"), - "immagini": _scan(r"!\[[^\]]*\]\([^)]*\)"), + "backtick": _scan(r"`"), + "dotleader": _scan(r"(?:\. ){3,}"), + "url": _scan(r"^(https?://|www\.)\S+"), + "immagini": _scan(r"!\[[^\]]*\]\([^)]*\)"), + "br_inline": _scan(r"
"), + "simboli_encoding":_scan(r'(?<=[0-9A-Za-z])[!"](?=[0-9A-Za-z])'), + "formule_inline": _scan(r"\[\d+\.\d+\]"), } # ── Composizione report ─────────────────────────────────────────────── @@ -856,14 +961,20 @@ def build_report( "long_sections_list": long_secs, }, "residui": { - "backtick": len(residui["backtick"]), - "dotleader": len(residui["dotleader"]), - "url": len(residui["url"]), - "immagini": len(residui["immagini"]), - "backtick_esempi": residui["backtick"], - "dotleader_esempi": residui["dotleader"], - "url_esempi": residui["url"], - "immagini_esempi": residui["immagini"], + "backtick": len(residui["backtick"]), + "dotleader": len(residui["dotleader"]), + "url": len(residui["url"]), + "immagini": len(residui["immagini"]), + "br_inline": len(residui["br_inline"]), + "simboli_encoding": len(residui["simboli_encoding"]), + "formule_inline": len(residui["formule_inline"]), + "backtick_esempi": residui["backtick"], + "dotleader_esempi": residui["dotleader"], + "url_esempi": residui["url"], + "immagini_esempi": residui["immagini"], + "br_inline_esempi": residui["br_inline"], + "simboli_encoding_esempi": residui["simboli_encoding"], + "formule_inline_esempi": residui["formule_inline"], }, }