fix(conversione): 5 fix robustezza e precisione transform
- _t_remove_footnotes: rimuove marcatori superscript inline e righe corpo-nota (¹ testo, [N] testo) — nuovo transform in posizione early - _t_numbered_sections: esclude voci bibliografiche (anno, pp., vol., DOI, ISBN) dalla promozione a ### header - _t_remove_toc: intercetta voci con numero pagina finale nel contesto TOC — rimosso _t_remove_toc_page_list standalone - _t_remove_frontmatter: limitata alle prime ~20% sezioni del documento - _t_remove_recurring_lines: soglia 3->5, Counter spostato a top-level
This commit is contained in:
+48
-5
@@ -30,6 +30,7 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from collections import Counter
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -378,6 +379,31 @@ def _t_remove_images(text: str) -> tuple[str, int]:
|
|||||||
return text, n
|
return text, n
|
||||||
|
|
||||||
|
|
||||||
|
# Superscript Unicode: ¹²³⁴⁵⁶⁷⁸⁹⁰
|
||||||
|
_SUPERSCRIPT_RE = re.compile(r'[\u00b9\u00b2\u00b3\u2070\u2074-\u2079]+')
|
||||||
|
# Riga corpo-nota: inizia con superscript o [N]
|
||||||
|
_FOOTNOTE_BODY_RE = re.compile(
|
||||||
|
r'^([\u00b9\u00b2\u00b3\u2070\u2074-\u2079]+\s+|\[\d{1,3}\]\s+)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _t_remove_footnotes(text: str) -> tuple[str, int]:
|
||||||
|
"""Rimuovi marcatori footnote superscript inline e righe corpo-nota."""
|
||||||
|
lines = text.split("\n")
|
||||||
|
result, count = [], 0
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
# Corpo nota: riga breve che inizia con ¹ o [N]
|
||||||
|
if stripped and _FOOTNOTE_BODY_RE.match(stripped) and len(stripped) < 300:
|
||||||
|
count += 1
|
||||||
|
continue
|
||||||
|
cleaned = _SUPERSCRIPT_RE.sub("", line)
|
||||||
|
if cleaned != line:
|
||||||
|
count += 1
|
||||||
|
result.append(cleaned)
|
||||||
|
return "\n".join(result), count
|
||||||
|
|
||||||
|
|
||||||
def _t_fix_br(text: str) -> tuple[str, int]:
|
def _t_fix_br(text: str) -> tuple[str, int]:
|
||||||
n = len(re.findall(r"<br>", text, re.IGNORECASE))
|
n = len(re.findall(r"<br>", text, re.IGNORECASE))
|
||||||
text = re.sub(r"<br>\s*", " ", text, flags=re.IGNORECASE)
|
text = re.sub(r"<br>\s*", " ", text, flags=re.IGNORECASE)
|
||||||
@@ -585,6 +611,9 @@ def _t_remove_toc(text: str) -> tuple[str, int]:
|
|||||||
if _in_toc:
|
if _in_toc:
|
||||||
if re.match(r"^\s*$", line) or re.match(r"^\s*[-*+]\s+\d", line):
|
if re.match(r"^\s*$", line) or re.match(r"^\s*[-*+]\s+\d", line):
|
||||||
continue
|
continue
|
||||||
|
# Voce TOC con numero pagina finale (sicuro: siamo già in contesto TOC)
|
||||||
|
if re.match(r"^\s*[-*+]\s+.{2,70}\s+\d{1,3}\s*$", line):
|
||||||
|
continue
|
||||||
_in_toc = False
|
_in_toc = False
|
||||||
new_lines.append(line)
|
new_lines.append(line)
|
||||||
return "\n".join(new_lines), 1 if removed else 0
|
return "\n".join(new_lines), 1 if removed else 0
|
||||||
@@ -637,6 +666,13 @@ def _t_allcaps_to_headers(text: str) -> tuple[str, int]:
|
|||||||
return "\n\n".join(new_blocks), count
|
return "\n\n".join(new_blocks), count
|
||||||
|
|
||||||
|
|
||||||
|
_BIB_MARKERS_RE = re.compile(
|
||||||
|
r'\b(pp?\.|vol\.|n\.\s*\d|ed\.|edn\.|ISBN|DOI|arXiv)\b'
|
||||||
|
r'|\b(19|20)\d{2}\b',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _t_numbered_sections(text: str, has_exercises: bool = False) -> tuple[str, int]:
|
def _t_numbered_sections(text: str, has_exercises: bool = False) -> tuple[str, int]:
|
||||||
"""Converti sezioni numerate 'N. testo' / '- N. testo' / '- N testo' → ### header."""
|
"""Converti sezioni numerate 'N. testo' / '- N. testo' / '- N testo' → ### header."""
|
||||||
count = 0
|
count = 0
|
||||||
@@ -646,6 +682,8 @@ def _t_numbered_sections(text: str, has_exercises: bool = False) -> tuple[str, i
|
|||||||
content = m.group(2).strip()
|
content = m.group(2).strip()
|
||||||
if content.endswith(".") and len(content) > 40:
|
if content.endswith(".") and len(content) > 40:
|
||||||
return m.group(0)
|
return m.group(0)
|
||||||
|
if _BIB_MARKERS_RE.search(content):
|
||||||
|
return m.group(0)
|
||||||
count += 1
|
count += 1
|
||||||
return f"### {m.group(1)}.\n\n{content}"
|
return f"### {m.group(1)}.\n\n{content}"
|
||||||
|
|
||||||
@@ -912,8 +950,14 @@ def _t_remove_frontmatter(text: str) -> tuple[str, int]:
|
|||||||
blocks = re.split(r"\n{2,}", text)
|
blocks = re.split(r"\n{2,}", text)
|
||||||
cleaned = []
|
cleaned = []
|
||||||
count = 0
|
count = 0
|
||||||
|
total = len(blocks)
|
||||||
|
cutoff = max(5, min(15, int(total * 0.20)))
|
||||||
for i, block in enumerate(blocks):
|
for i, block in enumerate(blocks):
|
||||||
stripped = block.strip()
|
stripped = block.strip()
|
||||||
|
# Frontmatter compare solo nelle prime sezioni del documento
|
||||||
|
if i >= cutoff:
|
||||||
|
cleaned.append(block)
|
||||||
|
continue
|
||||||
if not re.match(r"^### ", stripped) or re.match(r"^### \d", stripped):
|
if not re.match(r"^### ", stripped) or re.match(r"^### \d", stripped):
|
||||||
cleaned.append(block)
|
cleaned.append(block)
|
||||||
continue
|
continue
|
||||||
@@ -959,15 +1003,14 @@ def _t_fix_math_symbols(text: str) -> tuple[str, int]:
|
|||||||
|
|
||||||
|
|
||||||
def _t_remove_recurring_lines(text: str) -> tuple[str, int]:
|
def _t_remove_recurring_lines(text: str) -> tuple[str, int]:
|
||||||
"""Rimuovi righe corte che si ripetono ≥3 volte (header/footer di pagina)."""
|
"""Rimuovi righe corte che si ripetono ≥5 volte (header/footer di pagina)."""
|
||||||
from collections import Counter
|
|
||||||
lines = text.split("\n")
|
lines = text.split("\n")
|
||||||
short_lines = [
|
short_lines = [
|
||||||
ln.strip() for ln in lines
|
ln.strip() for ln in lines
|
||||||
if 3 < len(ln.strip()) < 80 and not ln.strip().startswith("#")
|
if 3 < len(ln.strip()) < 80 and not ln.strip().startswith("#")
|
||||||
]
|
]
|
||||||
freq = Counter(short_lines)
|
freq = Counter(short_lines)
|
||||||
recurring = {ln for ln, c in freq.items() if c >= 3}
|
recurring = {ln for ln, c in freq.items() if c >= 5}
|
||||||
if not recurring:
|
if not recurring:
|
||||||
return text, 0
|
return text, 0
|
||||||
result, count = [], 0
|
result, count = [], 0
|
||||||
@@ -994,6 +1037,7 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
|||||||
("n_immagini_rimosse", _t_remove_images),
|
("n_immagini_rimosse", _t_remove_images),
|
||||||
("n_br_rimossi", _t_fix_br),
|
("n_br_rimossi", _t_fix_br),
|
||||||
("n_tabsep_rimossi", _t_fix_tabsep),
|
("n_tabsep_rimossi", _t_fix_tabsep),
|
||||||
|
("n_note_rimosse", _t_remove_footnotes),
|
||||||
("n_accenti_corretti", _t_fix_accents),
|
("n_accenti_corretti", _t_fix_accents),
|
||||||
("n_moltiplicazioni_corrette", _t_fix_multiplication),
|
("n_moltiplicazioni_corrette", _t_fix_multiplication),
|
||||||
("n_micro_corretti", _t_fix_micro),
|
("n_micro_corretti", _t_fix_micro),
|
||||||
@@ -1009,7 +1053,6 @@ def apply_transforms(text: str) -> tuple[str, dict]:
|
|||||||
(None, _t_remove_header_bold),
|
(None, _t_remove_header_bold),
|
||||||
(None, _t_normalize_allcaps_headers),
|
(None, _t_normalize_allcaps_headers),
|
||||||
("toc_rimosso", _t_remove_toc),
|
("toc_rimosso", _t_remove_toc),
|
||||||
("n_toc_page_list_rimossi", _t_remove_toc_page_list),
|
|
||||||
("n_header_allcaps", _t_allcaps_to_headers),
|
("n_header_allcaps", _t_allcaps_to_headers),
|
||||||
("n_sezioni_numerate", partial(_t_numbered_sections, has_exercises=_has_ex)),
|
("n_sezioni_numerate", partial(_t_numbered_sections, has_exercises=_has_ex)),
|
||||||
("n_ambienti_matematici", _t_extract_math),
|
("n_ambienti_matematici", _t_extract_math),
|
||||||
@@ -1344,6 +1387,7 @@ def run(stem: str, project_root: Path, force: bool) -> bool:
|
|||||||
clean_text, t_stats = apply_transforms(raw_text)
|
clean_text, t_stats = apply_transforms(raw_text)
|
||||||
reduction = 100 * (1 - len(clean_text) / len(raw_text)) if raw_text else 0
|
reduction = 100 * (1 - len(clean_text) / len(raw_text)) if raw_text else 0
|
||||||
print(f" ✅ Immagini rimosse: {t_stats['n_immagini_rimosse']}")
|
print(f" ✅ Immagini rimosse: {t_stats['n_immagini_rimosse']}")
|
||||||
|
print(f" Note rimossa: {t_stats['n_note_rimosse']}")
|
||||||
print(f" Accenti corretti: {t_stats['n_accenti_corretti']}")
|
print(f" Accenti corretti: {t_stats['n_accenti_corretti']}")
|
||||||
print(f" Dot-leader rimossi: {t_stats['n_dotleader_rimossi']}")
|
print(f" Dot-leader rimossi: {t_stats['n_dotleader_rimossi']}")
|
||||||
print(f" Header concat fixati: {t_stats['n_header_concat_fixati']}")
|
print(f" Header concat fixati: {t_stats['n_header_concat_fixati']}")
|
||||||
@@ -1352,7 +1396,6 @@ def run(stem: str, project_root: Path, force: bool) -> bool:
|
|||||||
print(f" Ambienti matematici: {t_stats['n_ambienti_matematici']}")
|
print(f" Ambienti matematici: {t_stats['n_ambienti_matematici']}")
|
||||||
print(f" Titoli header uniti: {t_stats['n_titoli_uniti']}")
|
print(f" Titoli header uniti: {t_stats['n_titoli_uniti']}")
|
||||||
print(f" TOC rimosso: {'sì' if t_stats['toc_rimosso'] else 'no'}")
|
print(f" TOC rimosso: {'sì' if t_stats['toc_rimosso'] else 'no'}")
|
||||||
print(f" TOC voci pagina rim.: {t_stats['n_toc_page_list_rimossi']}")
|
|
||||||
print(f" Versi poesia riprist.: {t_stats['n_versi_ripristinati']}")
|
print(f" Versi poesia riprist.: {t_stats['n_versi_ripristinati']}")
|
||||||
print(f" Header verso demotati: {t_stats['n_header_verso_demotati']}")
|
print(f" Header verso demotati: {t_stats['n_header_verso_demotati']}")
|
||||||
print(f" ALL-CAPS → ##: {t_stats['n_header_allcaps']}")
|
print(f" ALL-CAPS → ##: {t_stats['n_header_allcaps']}")
|
||||||
|
|||||||
Reference in New Issue
Block a user