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"],
},
}