diff --git a/README.md b/README.md index c9a2926..b838d5b 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Pipeline completa per trasformare PDF accademici in un sistema RAG interrogabile ## Prerequisiti -### 1. MinerU — conversione PDF → Markdown strutturato +### 1. MinerU — conversione PDF → Markdown -MinerU è il tool esterno che converte i PDF in Markdown + metadati strutturati. +MinerU è il tool esterno che converte i PDF in Markdown strutturato. Repository: [https://github.com/opendatalab/MinerU](https://github.com/opendatalab/MinerU) #### Requisiti di sistema @@ -23,134 +23,41 @@ Repository: [https://github.com/opendatalab/MinerU](https://github.com/opendatal | RAM | 16 GB | 32 GB+ | | Disco | 20 GB (modelli inclusi) | 40 GB+ | | GPU | opzionale | 4–8 GB VRAM (CUDA) | -| OS | Linux, macOS, Windows | Linux / WSL2 per Docker | +| OS | Linux, macOS, Windows | Linux / WSL2 | #### Installazione -**pip (raccomandato):** - ```bash -pip install mineru[all] -# oppure con uv (più veloce): -uv pip install -U "mineru[all]" +pip install "mineru[all]" ``` -**conda:** - -```bash -conda create -n mineru python=3.11 -conda activate mineru -pip install mineru[all] -``` - -**Docker (Linux / WSL2 only):** - -```bash -# CPU -docker run --rm -v /percorso/locale:/data opendatalab/mineru mineru -p /data/doc.pdf -o /data/output - -# GPU (richiede nvidia-container-toolkit) -docker run --rm --gpus all -v /percorso/locale:/data opendatalab/mineru:cuda mineru -p /data/doc.pdf -o /data/output -``` - -Al primo avvio MinerU scarica automaticamente i modelli (~15 GB). Per forzare il download manuale: - -```bash -mineru-models-download -``` +Al primo avvio scarica automaticamente i modelli (~15 GB). #### Uso — CLI ```bash -mineru -p -o [opzioni] +mineru -p -o -b pipeline ``` -| Flag | Descrizione | Default | -|------|-------------|---------| -| `-p` | Percorso PDF, cartella di PDF, o URL | — | -| `-o` | Cartella di output | — | -| `-b` | Backend di conversione (vedi sotto) | `pipeline` | -| `-m` | Metodo di estrazione: `auto`, `txt`, `ocr` | `auto` | -| `-l` | Lingua per OCR (es. `it`, `en`, `zh`) | `en` | -| `-s` | Pagina iniziale (0-based) | 0 | -| `-e` | Pagina finale (0-based, inclusa) | ultima | -| `--formula` | Attiva/disattiva riconoscimento formule | config | -| `--table` | Attiva/disattiva riconoscimento tabelle | config | - -**Esempio tipico:** - -```bash -mineru -p articolo.pdf -o sources/articolo/ -# Output in sources/articolo/auto/ -``` - -Per estrarre solo alcune pagine: - -```bash -mineru -p documento.pdf -o sources/documento/ -s 10 -e 50 -``` - -#### Backend di conversione - -| Backend | Descrizione | Accuratezza | GPU richiesta | -|---------|-------------|-------------|---------------| -| `pipeline` | CPU-only, modelli leggeri | ~86% | No | -| `hybrid-auto-engine` | Combina pipeline + VLM selettivo | ~95%+ | Raccomandato | -| `vlm-auto-engine` | VLM completo su tutte le pagine | massima | Sì | -| `pipeline-http` / `vlm-http` | Come sopra ma via API remota | — | Remota | - -```bash -# Raccomandato se si ha una GPU con ≥ 4 GB VRAM -mineru -p doc.pdf -o output/ -b hybrid-auto-engine - -# Solo CPU -mineru -p doc.pdf -o output/ -b pipeline -``` - -#### Configurazione avanzata - -File di configurazione utente: `~/mineru.json` - -Variabili d'ambiente principali: - -| Variabile | Valori | Effetto | -|-----------|--------|---------| -| `MINERU_FORMULA_ENABLE` | `0` / `1` | Abilita/disabilita OCR formule matematiche | -| `MINERU_TABLE_ENABLE` | `0` / `1` | Abilita/disabilita riconoscimento tabelle | -| `MINERU_DEVICE` | `cpu`, `cuda`, `mps` | Forza il dispositivo di inferenza | +| Flag | Descrizione | +|------|-------------| +| `-p` | Percorso PDF o cartella | +| `-o` | Cartella di output | +| `-b` | Backend: `pipeline` (CPU), `hybrid-auto-engine` (GPU raccomandato) | +| `-m` | Metodo: `auto`, `txt`, `ocr` | #### Output di MinerU -MinerU produce per ogni PDF una cartella con questa struttura: - ``` -/ +_output/ └── auto/ - ├── .md ← Markdown grezzo (non usato dalla pipeline) - ├── _content_list_v2.json ← struttura ricca: tipo, livello, bbox [RICHIESTO] - ├── _model.json ← label di layout: doc_title, abstract… [RACCOMANDATO] - ├── _middle.json ← dati intermedi (non usato) - ├── _content_list.json ← formato v1 flat (non usato) - ├── _layout.pdf ← PDF annotato con i bounding box - ├── _span.pdf ← PDF con span di testo evidenziati - └── images/ ← immagini estratte + ├── .md ← Markdown (input della pipeline) + ├── _content_list_v2.json + ├── _model.json + └── images/ ``` -`` è il nome del documento, usato come identificatore in tutta la pipeline. - -I file usati dalla pipeline di questo repository sono: - -- **`_content_list_v2.json`** ← struttura ad albero: ogni blocco ha `type` (title/paragraph/table/list/image), `level` (per i titoli: 1=capitolo, 2=sezione), `content` con il testo, e `bbox` per la posizione sulla pagina. -- **`_model.json`** ← label semantiche per bounding box: `doc_title`, `paragraph_title`, `abstract`, `header`, `number`, `text`, `aside_text`… Usato per arricchire la classificazione del testo (es. riconoscere SOMMARIO interni, prefissi di capitolo). - -#### Limitazioni note di MinerU - -- **Testo verticale** (CJK ruotato, riviste asiatiche): qualità ridotta. -- **Testo scritto a mano**: accuratezza molto bassa. -- **Fumetti e album d'arte**: non supportati. -- **Blocchi di codice**: non sempre preservati correttamente (indentazione può essere persa). -- **PDF scansionati senza OCR**: richiede backend con OCR attivo (`-m ocr`). -- **Formule matematiche complesse**: dipendono dalla qualità del rendering PDF originale. +Il file usato dalla pipeline è **`.md`** nella cartella `auto/`. ### 2. Ollama — embedding e generazione @@ -176,99 +83,60 @@ pip install -r requirements.txt --- -## Configurazione rapida - -Prima di usare la pipeline verifica i parametri nei due file di configurazione: - -**`chunks/config.py`** — parametri di chunking: - -| Parametro | Default | Quando cambiarlo | -|-----------|---------|-----------------| -| `MAX_CHARS` | 1200 | Aumenta per chunk più lunghi (es. testi narrativi) | -| `MIN_CHARS` | 80 | Abbassa se hai molti titoli brevi | -| `FRONTMATTER_HEADINGS` | set italiano | Svuota (`set()`) per documenti non italiani | -| `OVERLAP_SENTENCES` | 1 | Aumenta a 2 se il retrieval perde contesto tra chunk | - -**`config.py`** — parametri RAG: - -| Parametro | Default | Quando cambiarlo | -|-----------|---------|-----------------| -| `OLLAMA_MODEL` | `qwen3.5:4b` | Sostituisci con il modello Ollama che vuoi usare | -| `EMBED_MODEL` | `nomic-embed-text` | Deve corrispondere al modello usato in ingestion | -| `TOP_K` | 6 | Aumenta per recuperare più contesto per domanda | -| `OLLAMA_URL` | `localhost:11434` | Cambia se Ollama gira su un altro host/porta | - -> Se cambi `EMBED_MODEL` devi rieseguire l'ingestion con `--force`. - ---- - ## Flusso completo ``` PDF │ - ▼ (MinerU — esterno) -sources//auto/ + ▼ MinerU (esterno — vedi sotto) +sources/_output/auto/.md │ ▼ python chunks/chunker.py --stem - │ Stage 1: _content_list_v2.json + _model.json → _clean.md - │ Stage 2: _clean.md → chunks//chunks.json +chunks//chunks.json │ ▼ python chunks/verify_chunks.py --stem - │ Verifica qualità chunk - │ - ▼ python chunks/fix_chunks.py --stem [se necessario] - │ Correzioni automatiche +(verifica qualità — opzionale ma consigliato) │ ▼ python ingestion/ingest.py --stem - │ Embedding → ChromaDB +chroma_db// │ ▼ python rag.py --stem - Interrogazione in linguaggio naturale +Interrogazione in linguaggio naturale ``` --- -### Passo 1 — Converti il PDF con MinerU +## Passo 1 — Converti il PDF con MinerU -Per questo step è necessario **MinerU** ([github.com/opendatalab/MinerU](https://github.com/opendatalab/MinerU)). +**Opzione A — Google Colab (nessuna installazione locale)** -**Opzione A — Google Colab (consigliata, nessuna installazione locale)** - -Usa il notebook incluso in questo repository: +Usa il notebook incluso: ``` -mineru_demo.ipynb +mineru.ipynb ``` -Aprilo su Google Colab, carica il tuo PDF nella sessione e segui le celle in ordine. Al termine scarica la cartella `output//` generata. +Aprilo su Google Colab, carica il PDF e segui le celle. Al termine scarica automaticamente lo ZIP con l'output. Decomprimi la cartella in `sources/`: + +``` +sources/ +└── _output/ + └── auto/ + ├── .md ← usato dalla pipeline + ├── _content_list_v2.json + ├── _model.json + └── images/ +``` **Opzione B — Installazione locale** ```bash -pip install "mineru[all]" -mineru -p documento.pdf -o sources// +mineru -p documento.pdf -o sources/_output/ -b pipeline ``` -Al termine, **copia la cartella completa** prodotta da MinerU dentro `sources/`. La struttura attesa è: +--- -``` -sources/ -└── / - └── auto/ - ├── .md - ├── _content_list_v2.json ← richiesto - ├── _model.json ← raccomandato - ├── _middle.json - ├── _content_list.json - ├── _layout.pdf - ├── _span.pdf - └── images/ -``` - -### Passo 2 — Chunking (Stage 1 + Stage 2) - -Un singolo comando esegue entrambe le fasi: +## Passo 2 — Chunking ```bash # Singolo documento @@ -277,119 +145,98 @@ Un singolo comando esegue entrambe le fasi: # Tutti i documenti in sources/ .venv/bin/python chunks/chunker.py -# Rigenera tutto da zero +# Rigenera anche se chunks.json esiste già .venv/bin/python chunks/chunker.py --stem --force - -# Salta Stage 1 (usa il _clean.md già presente) -.venv/bin/python chunks/chunker.py --stem --skip-optimize ``` -**Stage 1 — Ottimizzazione Markdown** (`md_optimizer.py` interno): -- Legge `_content_list_v2.json` e `_model.json` -- Riconosce la gerarchia heading (H1 = capitolo, H2 = sezione, H3 = sottosezione) -- Fonde titoli di capitolo multipli: `CAPITOLO 2` + `Il titolo` → `# CAPITOLO 2 — Il titolo` -- Rimuove TOC, frontespizio, copyright, indici e sommari interni -- Produce `sources//auto/_clean.md` +Il chunker legge `sources/_output/auto/.md` e produce `chunks//chunks.json`. -**Stage 2 — Chunking semantico**: -- Un chunk per paragrafo — mai due paragrafi nello stesso chunk -- Split a confine di frase se il paragrafo supera `MAX_CHARS` (default 1200 chars) -- Overlap di 1 frase tra chunk consecutivi per continuità contestuale -- Tabelle e liste sono blocchi atomici (non vengono mai spezzati) -- Ogni chunk include un prefisso `[Sezione > Titolo]` per il retrieval +**Regole applicate:** + +- 1 paragrafo = 1 chunk; paragrafi di sezioni diverse non si mescolano +- Split a confine di frase se il paragrafo supera `MAX_CHARS` (mai a metà frase) +- Frasi spezzate tra paragrafi consecutivi vengono ri-fuse automaticamente +- Paragrafi brevi (< `MIN_CHARS`) vengono accorpati al successivo (stesso contesto) +- Sezioni configurabili vengono saltate (indice, sommario, frontespizio) +- Tabelle, liste e code block sono blocchi atomici (non si spezzano) Output in `chunks//`: | File | Contenuto | |------|-----------| -| `chunks.json` | Lista di chunk con testo, sezione, titolo, n_chars | -| `meta.json` | Parametri usati (max_chars, overlap, strategia) | +| `chunks.json` | Chunk con testo, sezione, titolo, n_chars | +| `meta.json` | Parametri usati | | `report.json` | Statistiche e anomalie (generato da verify) | -### Passo 3 — Verifica i chunk +--- + +## Passo 3 — Verifica i chunk (opzionale) ```bash .venv/bin/python chunks/verify_chunks.py --stem ``` -| Verdict | Significato | Cosa fare | -|---------|-------------|-----------| -| `ok` | Nessun problema | Procedi alla vettorizzazione | -| `warnings_only` | Solo avvisi minori (frasi lunghe) | Puoi procedere | -| `blocked` | Chunk incompleti o senza prefisso | Esegui il fix | +| Verdict | Significato | +|---------|-------------| +| `ok` | Nessun problema — procedi | +| `warnings_only` | Solo avvisi minori — puoi procedere | +| `blocked` | Problemi strutturali — rivedi il sorgente `.md` | -### Passo 4 — Correggi i problemi (se verdict = `blocked`) +--- -```bash -# Anteprima senza applicare -.venv/bin/python chunks/fix_chunks.py --stem --dry-run - -# Applica le correzioni -.venv/bin/python chunks/fix_chunks.py --stem -``` - -Il fix risolve automaticamente: chunk incompleti (frase spezzata), chunk troppo corti (accorpati al successivo), chunk troppo lunghi (spezzati su punteggiatura). Itera fino alla convergenza o per un massimo di `FIX_MAX_ITERATIONS` passate. - -### Passo 5 — Vettorizzazione - -Verifica che Ollama sia attivo, poi: +## Passo 4 — Vettorizzazione ```bash # Singolo documento → collection omonima .venv/bin/python ingestion/ingest.py --stem -# Più documenti → un'unica collection condivisa +# Più documenti → collection condivisa (RAG cross-documento) .venv/bin/python ingestion/ingest.py --collection --stems doc1 doc2 doc3 # Rigenera dopo aver aggiornato i chunk o cambiato modello embedding .venv/bin/python ingestion/ingest.py --stem --force ``` -> Se cambi `EMBED_MODEL` in `config.py`, devi rilanciare l'ingestion con `--force`. +> Se cambi `EMBED_MODEL` in `config.py` devi rieseguire l'ingestion con `--force`. -### Passo 6 — Interrogazione +--- + +## Passo 5 — Interrogazione ```bash -# RAG interattivo +# RAG interattivo (retrieval + generazione LLM) .venv/bin/python rag.py --stem .venv/bin/python rag.py --collection -# Retrieval puro (debug, senza generazione LLM) +# Retrieval puro (debug, senza LLM) .venv/bin/python retrieve.py --stem .venv/bin/python retrieve.py --stem --top-k 10 ``` -Comandi nel loop interattivo di `retrieve.py`: - -| Input | Effetto | -|-------|---------| -| `` | Chunk più simili con score (testo troncato) | -| ` -f` | Testo completo dei chunk | -| `exit` | Termina | - --- ## Configurazione -### Parametri di chunking — `chunks/config.py` +### Chunking — `chunks/config.py` | Parametro | Default | Descrizione | |-----------|---------|-------------| -| `MAX_CHARS` | 1200 | Lunghezza massima chunk in caratteri | +| `MAX_CHARS` | 1200 | Lunghezza massima chunk (caratteri) | | `MIN_CHARS` | 80 | Soglia minima (warning sotto questa soglia) | -| `OVERLAP_SENTENCES` | 1 | Frasi di overlap tra chunk consecutivi | -| `MIN_CONTENT_CHARS` | 2500 | Soglia per distinguere frontespizio da contenuto reale | -| `FRONTMATTER_HEADINGS` | set italiano | Sezioni da rimuovere (Sommario, Indice, Autori…) — svuotare per documenti non italiani | -| `SOMMARIO_PATTERNS` | 5 lingue | Pattern regex per sommari interni da saltare | -| `CHAPTER_PREFIX_PATTERNS` | 4 lingue | Pattern per prefissi di capitolo come paragrafi separati | -| `MODEL_SKIP_LABELS` | set | Label `_model.json` da ignorare (header, number…) | +| `CONTEXT_DEPTH` | 3 | Livelli heading inclusi nel prefisso chunk (1–3) | +| `SKIP_HEADINGS` | set italiano | Sezioni saltate completamente (indice, sommario…) | +| `SKIP_PRE_HEADING` | `True` | Salta contenuto prima del primo heading (frontespizio) | +| `MERGE_SHORT_PARAGRAPHS` | `True` | Accorpa paragrafi brevi fino a `MIN_CHARS` | +| `SENTENCE_SPLIT_RE` | regex | Confine di fine frase per lo split | +| `ATOMIC_TYPES` | `table, code, list` | Blocchi mai spezzati | -### Parametri RAG — `config.py` +### RAG — `config.py` | Parametro | Default | Descrizione | |-----------|---------|-------------| | `OLLAMA_MODEL` | `qwen3.5:4b` | Modello LLM per la generazione | | `EMBED_MODEL` | `nomic-embed-text` | Modello embedding | +| `EMBED_MAX_CHARS` | 6000 | Caratteri massimi inviati al modello embedding | | `TOP_K` | 6 | Chunk recuperati per domanda | | `TEMPERATURE` | 0.2 | Creatività del modello (0 = deterministico) | | `OLLAMA_URL` | `localhost:11434` | URL server Ollama | @@ -400,34 +247,17 @@ Comandi nel loop interattivo di `retrieve.py`: ``` rag/ -├── sources/ ← output di MinerU (una cartella per documento) -│ └── /auto/ +├── sources/ ← output MinerU (una cartella per documento) +│ └── _output/auto/ ├── chunks/ -│ ├── chunker.py ← pipeline unificata (Stage 1 + Stage 2) -│ ├── md_optimizer.py ← Stage 1: JSON MinerU → _clean.md -│ ├── config.py ← tutti i parametri di chunking -│ ├── verify_chunks.py ← verifica qualità -│ └── fix_chunks.py ← correzioni automatiche +│ ├── chunker.py ← chunking da MD pulito +│ ├── config.py ← parametri di chunking +│ └── verify_chunks.py ← verifica qualità chunk ├── ingestion/ -│ └── ingest.py ← embedding → ChromaDB -├── ollama/ -│ └── check_env.py ← verifica ambiente Ollama -├── chroma_db/ ← database vettoriale (ignorato da git) -├── config.py ← parametri RAG (modelli, TOP_K, prompt) -├── rag.py ← loop RAG interattivo -└── retrieve.py ← retrieval puro (debug) +│ └── ingest.py ← embedding → ChromaDB +├── chroma_db/ ← database vettoriale (ignorato da git) +├── config.py ← parametri RAG (modelli, TOP_K, prompt) +├── rag.py ← loop RAG interattivo +├── retrieve.py ← retrieval puro (debug) +└── mineru.ipynb ← notebook Colab per conversione PDF ``` - ---- - -## Note su generalità e adattamento - -La pipeline è progettata per funzionare con **qualsiasi output MinerU**, non solo per documenti in italiano. MinerU produce sempre la stessa struttura di file (`_content_list_v2.json`, `_model.json`) indipendentemente dal documento sorgente. - -Per adattare a documenti in altre lingue o con struttura diversa, modificare in `chunks/config.py`: - -- **`FRONTMATTER_HEADINGS`**: impostare `set()` per disabilitare, o aggiungere i nomi delle sezioni da rimuovere nella lingua del documento -- **`SOMMARIO_PATTERNS`**: aggiungere/rimuovere pattern regex per i sommari interni -- **`CHAPTER_PREFIX_PATTERNS`**: aggiungere pattern per la lingua del documento -- **`MIN_CONTENT_CHARS`**: alzare se il documento ha lunghe sezioni di copyright/prefazione da escludere -- **`MIN_TOC_HEADINGS`**: abbassare se il documento ha pochi capitoli (es. 3-4) diff --git a/config.py b/config.py index 067b608..ef4f931 100644 --- a/config.py +++ b/config.py @@ -32,6 +32,11 @@ NO_THINK = True # Se cambi questo, devi rieseguire ingest.py con --force. EMBED_MODEL = "nomic-embed-text" +# Caratteri massimi inviati al modello di embedding. +# Il testo viene troncato SOLO per il vettore; il documento completo +# rimane in ChromaDB. nomic-embed-text: 8192 token ≈ 6000 char sicuri. +EMBED_MAX_CHARS: int = 6000 + # ── Ollama ──────────────────────────────────────────────────────────────────── # URL del server Ollama (default: locale sulla porta 11434). diff --git a/ingestion/ingest.py b/ingestion/ingest.py index 4a2c6d7..c9f90b6 100644 --- a/ingestion/ingest.py +++ b/ingestion/ingest.py @@ -21,6 +21,7 @@ Uso: import argparse import json +import re import sys import time import urllib.error @@ -37,16 +38,28 @@ CHUNKS_DIR = project_root / "chunks" CHROMA_DIR = project_root / "chroma_db" sys.path.insert(0, str(project_root)) -from config import EMBED_MODEL, OLLAMA_URL # noqa: E402 +from config import EMBED_MODEL, EMBED_MAX_CHARS, OLLAMA_URL # noqa: E402 EMBED_ENDPOINT = f"{OLLAMA_URL}/api/embeddings" # ─── Ollama ──────────────────────────────────────────────────────────────────── +_HTML_TAG = re.compile(r"<[^>]+>") +_HTML_ENT = re.compile(r"&[a-zA-Z0-9#]+;") + + +def _clean_for_embed(text: str) -> str: + """Rimuove tag HTML ed entità prima dell'embedding; tronca al limite del modello.""" + text = _HTML_TAG.sub(" ", text) + text = _HTML_ENT.sub(" ", text) + text = re.sub(r" {2,}", " ", text) + return text[:EMBED_MAX_CHARS] + + def embed(text: str, model: str) -> list[float]: """Chiama Ollama /api/embeddings e ritorna il vettore.""" - payload = json.dumps({"model": model, "prompt": text}).encode() + payload = json.dumps({"model": model, "prompt": _clean_for_embed(text)}).encode() req = urllib.request.Request( EMBED_ENDPOINT, data=payload,