- prepare_corpus.py: split double-page scans (split_spreads.py), re-OCR with OCRmyPDF/Tesseract only where the existing text layer is worse, Docling layout extraction with heading hierarchy, QA report per PDF - docling_loader.py: cleanup (margin noise, split paragraphs, footnote marks, OCR comma/period repair), page mapping back to the original PDF, chunk headers with author, title and section - local_rag.py: bge-m3 embeddings, configurable chunking and loader, index settings check (index_info.json), hybrid/union BM25 search with Reciprocal Rank Fusion and bge-reranker-v2-m3 cross-encoder; query instruction text removed after it lowered recall - eval_retrieval.py: CLI options, same retrieval path as the chat, tolerant file name matching; goldsets and evaluation reports for the Kafka corpus - grobid_test.py, grobid_qa.py, extract_docling.py, compare_extractors.py: extractor tests and quality checks - .gitignore: corpus PDFs, vector stores and extractor output stay local - server.py: uses ./vectorstore_docling_hdr Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
4.9 KiB
Python
110 lines
4.9 KiB
Python
"""
|
|
PDFs mit Docling extrahieren, zum Vergleich mit GROBID und PyPDFLoader.
|
|
|
|
Vier Varianten:
|
|
docling nutzt die vorhandene Textschicht des PDFs (bei Scans: deren OCR)
|
|
docling_ocr erkennt jede Seite neu per OCR (Apple Vision über ocrmac, Deutsch),
|
|
ignoriert also die mitgelieferte, oft schlechte OCR-Schicht.
|
|
Achtung: lässt bei diesen Scans ganze Absätze aus.
|
|
docling_tess wie docling_ocr, aber mit Tesseract und dem deutschen Modell
|
|
aus tessdata/deu.traineddata (tessdata_best). Verliert ebenfalls Text:
|
|
Doclings eigener OCR-Ablauf ist bei diesen Doppelseiten-Scans unzuverlässig.
|
|
docling_ocrmypdf
|
|
Arbeitsteilung: OCRmyPDF (Tesseract, deutsch, mit Geraderichten) legt
|
|
eine neue Textschicht ins PDF (extract_out/ocrmypdf/), Docling liest
|
|
diese dann ohne eigene OCR und erkennt die Struktur.
|
|
|
|
Läuft in einer eigenen Umgebung, weil Docling PyTorch und Layout-Modelle mitbringt:
|
|
conda activate docling
|
|
python extract_docling.py # Variante docling_tess, Titel aus TITLES
|
|
python extract_docling.py docling # andere Variante
|
|
|
|
Ausgabe: extract_out/<variante>/<pdf>.json (verlustfrei, für grobid_qa.py)
|
|
und .md (zum Lesen). Vorhandene Ergebnisse werden übersprungen.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from docling.datamodel.base_models import InputFormat
|
|
from docling.datamodel.pipeline_options import (
|
|
OcrMacOptions, OcrMode, PdfPipelineOptions, TesseractCliOcrOptions)
|
|
from docling.document_converter import DocumentConverter, PdfFormatOption
|
|
|
|
PDF_DIR = Path("data")
|
|
OUT_ROOT = Path("extract_out")
|
|
TESSDATA = Path("tessdata").resolve() # enthält deu.traineddata
|
|
# Docling prüft die verfügbaren Tesseract-Sprachen ohne Rücksicht auf `path`,
|
|
# deshalb das Modellverzeichnis auch über die Umgebungsvariable bekanntmachen.
|
|
os.environ["TESSDATA_PREFIX"] = str(TESSDATA)
|
|
VARIANTS = ("docling", "docling_ocr", "docling_tess", "docling_ocrmypdf")
|
|
OCRMYPDF_DIR = OUT_ROOT / "ocrmypdf" # neu erkannte PDFs für docling_ocrmypdf
|
|
# Auswahl für den Vergleich: Dateinamen beginnen mit diesen Präfixen
|
|
TITLES = [
|
|
"Beissner,", # Aufsatz, viele Fußnoten
|
|
"Fiechter,", # GROBID: alles ein Absatz
|
|
"Trost,", # sehr schlechte OCR-Schicht
|
|
"Harzer,", # GROBID: Fließtext als Abbildung
|
|
"Binder,_Hartmut_KafkaHandbuch_Band2_AText", # großes Buch
|
|
]
|
|
|
|
|
|
def converter(variant: str) -> DocumentConverter:
|
|
opts = PdfPipelineOptions()
|
|
opts.do_table_structure = True
|
|
if variant == "docling_ocr":
|
|
opts.do_ocr = True
|
|
opts.ocr_options = OcrMacOptions(mode=OcrMode.FULL_PAGE, lang=["de-DE"])
|
|
elif variant == "docling_tess":
|
|
opts.do_ocr = True
|
|
opts.ocr_options = TesseractCliOcrOptions(mode=OcrMode.FULL_PAGE, lang=["deu"], path=str(TESSDATA))
|
|
else: # docling, docling_ocrmypdf: Textschicht lesen
|
|
opts.do_ocr = False
|
|
return DocumentConverter(format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=opts)})
|
|
|
|
|
|
def ocrmypdf(pdf: Path) -> Path:
|
|
"""Legt mit OCRmyPDF eine neue deutsche Textschicht an (überspringt Vorhandenes)."""
|
|
OCRMYPDF_DIR.mkdir(parents=True, exist_ok=True)
|
|
target = OCRMYPDF_DIR / pdf.name
|
|
if not target.exists():
|
|
subprocess.run(
|
|
[str(Path(sys.executable).parent / "ocrmypdf"), "--force-ocr", "-l", "deu", "--deskew",
|
|
"--output-type", "pdf", "--jobs", "4", "--quiet", str(pdf), str(target)],
|
|
check=True)
|
|
return target
|
|
|
|
|
|
def main():
|
|
variant = sys.argv[1] if len(sys.argv) > 1 else "docling_tess"
|
|
if variant not in VARIANTS:
|
|
sys.exit(f"Variante muss eine von {', '.join(VARIANTS)} sein")
|
|
out_dir = OUT_ROOT / variant
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
pdfs = [p for p in sorted(PDF_DIR.glob("*.pdf")) if any(p.name.startswith(t) for t in TITLES)]
|
|
conv = converter(variant)
|
|
for i, pdf in enumerate(pdfs, 1):
|
|
target = out_dir / f"{pdf.stem}.json"
|
|
if target.exists():
|
|
print(f"[{i}/{len(pdfs)}] {pdf.name}: vorhanden, übersprungen", flush=True)
|
|
continue
|
|
print(f"[{i}/{len(pdfs)}] {pdf.name} ...", end=" ", flush=True)
|
|
t0 = time.time()
|
|
try:
|
|
source = ocrmypdf(pdf) if variant == "docling_ocrmypdf" else pdf
|
|
doc = conv.convert(source).document
|
|
except Exception as e: # ein kaputtes PDF soll den Lauf nicht abbrechen
|
|
print(f"FEHLER: {e}", flush=True)
|
|
continue
|
|
doc.save_as_json(target)
|
|
doc.save_as_markdown(out_dir / f"{pdf.stem}.md")
|
|
print(f"fertig ({time.time() - t0:.0f} s)", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|