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,