update
This commit is contained in:
@@ -174,15 +174,16 @@ def extract_text(pdf_path, max_pages=4):
|
||||
return ""
|
||||
|
||||
|
||||
def pdf_to_base64_images(pdf_path, max_pages=2, max_dim=1600, quality=80, dpi=150):
|
||||
def pdf_to_base64_images(pdf_path, first_page=1, max_pages=2, max_dim=1600, quality=80, dpi=150):
|
||||
"""
|
||||
Erste max_pages Seiten -> Base64-JPEG.
|
||||
Moderate Aufloesung (dpi=150, max_dim=1600): reicht fuer Checkbox/Handschrift
|
||||
(im Test bei dpi=150 erkannt), haelt aber die Vision-Payload klein genug,
|
||||
dass der Prompt-Cache nicht ueberlaeuft (die "making room ... MiB"-Warnungen).
|
||||
Seiten [first_page .. first_page+max_pages-1] -> Base64-JPEG.
|
||||
Moderate Aufloesung (dpi=150, max_dim=1600): reicht fuer Checkbox/Handschrift.
|
||||
first_page erlaubt das Ueberspringen einer fuehrenden Seite (z.B. die
|
||||
ueberfluessige handschriftliche Notiz-Seite bei "Notes"-Dateien).
|
||||
"""
|
||||
last_page = first_page + max_pages - 1
|
||||
try:
|
||||
images = convert_from_path(pdf_path, first_page=1, last_page=max_pages, dpi=dpi)
|
||||
images = convert_from_path(pdf_path, first_page=first_page, last_page=last_page, dpi=dpi)
|
||||
except Exception as e:
|
||||
print(f" [Bildkonvertierung-Fehler] {e}")
|
||||
return []
|
||||
@@ -202,11 +203,12 @@ def pdf_to_base64_images(pdf_path, max_pages=2, max_dim=1600, quality=80, dpi=15
|
||||
# LLM-Call (mit Guided Decoding + Vision-Budget)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def call_model(client, model, content_payload, vision_budget=None):
|
||||
def call_model(client, model, content_payload, vision_budget=None, max_tokens=768):
|
||||
# llama.cpp erzwingt JSON ueber response_format mit json_schema.
|
||||
# WICHTIG gegen Gemma-4 "<unused49>-Flood"/Runaway:
|
||||
# - enable_thinking:false per Request (zuverlaessiger als nur Server-Flag)
|
||||
# - kleines max_tokens (ein Datensatz braucht ~300-400 Tokens)
|
||||
# - max_tokens moderat (768: genug fuer volle Datensaetze mit langem
|
||||
# Background/vielen Kategorien, aber begrenzt genug gegen Runaway)
|
||||
# - repeat_penalty gegen Wiederholschleifen
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
@@ -215,7 +217,7 @@ def call_model(client, model, content_payload, vision_budget=None):
|
||||
{"role": "user", "content": content_payload},
|
||||
],
|
||||
temperature=0.0,
|
||||
max_tokens=512,
|
||||
max_tokens=max_tokens,
|
||||
response_format={
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "buyer_sheet", "schema": JSON_SCHEMA},
|
||||
@@ -305,8 +307,8 @@ def _extract_via_text(client, model, text):
|
||||
return data
|
||||
|
||||
|
||||
def _extract_via_vision(client, model, pdf_path, max_pages):
|
||||
imgs = pdf_to_base64_images(pdf_path, max_pages=max_pages)
|
||||
def _extract_via_vision(client, model, pdf_path, max_pages, first_page=1):
|
||||
imgs = pdf_to_base64_images(pdf_path, first_page=first_page, max_pages=max_pages)
|
||||
if not imgs:
|
||||
return None
|
||||
payload = [{"type": "text", "text": USER_IMG_INTRO}]
|
||||
@@ -322,33 +324,41 @@ def _extract_via_vision(client, model, pdf_path, max_pages):
|
||||
|
||||
def process_pdf(client, model, pdf_path, vision_budget=None):
|
||||
"""
|
||||
Hybrid-Strategie:
|
||||
Hybrid-Strategie mit Notes-Seitenlogik:
|
||||
- "Notes"-Dateien: Seite 1 ist eine ueberfluessige handschriftliche Notiz.
|
||||
Die Buyer-Felder stehen auf Seite 2 (Info Sheet) + 3 (CA mit Datum/Name).
|
||||
Darum bei Notes-Dateien first_page=2.
|
||||
- Normale Dateien: Info Sheet = Seite 1, CA = Seite 2. first_page=1.
|
||||
- reiner Scan (keine Textebene): nur Vision
|
||||
- grosser Scan (> MAX_SCAN_PAGES_TOTAL): nur erste Seite Vision
|
||||
- grosser Scan (> MAX_SCAN_PAGES_TOTAL): nur erste relevante Seite pruefen
|
||||
- getipptes PDF mit Textebene: Text UND Vision, dann mergen
|
||||
(Text bringt getippten Background, Vision die handschriftlichen Felder)
|
||||
"""
|
||||
n_pages = pdf_page_count(pdf_path)
|
||||
text = extract_text(pdf_path)
|
||||
has_text = len(text.strip()) >= TEXT_SCAN_THRESHOLD
|
||||
|
||||
# "Notes" im Dateinamen -> fuehrende Notiz-Seite ueberspringen.
|
||||
# Aber nur, wenn genug Seiten da sind (sonst normal ab Seite 1).
|
||||
is_notes = "notes" in os.path.basename(pdf_path).lower()
|
||||
vpage = 2 if (is_notes and n_pages >= 3) else 1
|
||||
|
||||
truncated_note = None
|
||||
try:
|
||||
if not has_text and n_pages > MAX_SCAN_PAGES_TOTAL:
|
||||
# grosser reiner Scan: nur erste Seite pruefen
|
||||
truncated_note = f"grosser Scan ({n_pages} Seiten), nur erste Seite geprueft"
|
||||
data = _extract_via_vision(client, model, pdf_path, max_pages=1)
|
||||
# grosser reiner Scan: nur die erste relevante Seite pruefen
|
||||
truncated_note = f"grosser Scan ({n_pages} Seiten), begrenzt geprueft"
|
||||
data = _extract_via_vision(client, model, pdf_path, max_pages=2, first_page=vpage)
|
||||
mode = "vision"
|
||||
elif not has_text:
|
||||
# reiner Scan
|
||||
data = _extract_via_vision(client, model, pdf_path, max_pages=2)
|
||||
data = _extract_via_vision(client, model, pdf_path, max_pages=2, first_page=vpage)
|
||||
mode = "vision"
|
||||
else:
|
||||
# Textebene vorhanden -> HYBRID: beide Pfade, dann mergen
|
||||
text_rec = _extract_via_text(client, model, text)
|
||||
vision_rec = None
|
||||
if n_pages <= MAX_SCAN_PAGES_TOTAL:
|
||||
vision_rec = _extract_via_vision(client, model, pdf_path, max_pages=2)
|
||||
vision_rec = _extract_via_vision(client, model, pdf_path, max_pages=2, first_page=vpage)
|
||||
data = merge_records(text_rec, vision_rec)
|
||||
mode = "hybrid" if vision_rec is not None else "text"
|
||||
except Exception as e:
|
||||
@@ -357,6 +367,8 @@ def process_pdf(client, model, pdf_path, vision_budget=None):
|
||||
if data is None:
|
||||
return {"_error": "JSON-Parse fehlgeschlagen", "n_pages": n_pages}, mode
|
||||
data["n_pages"] = n_pages
|
||||
if is_notes:
|
||||
data["_notes_file"] = True
|
||||
if truncated_note:
|
||||
data["_note"] = truncated_note
|
||||
return data, mode
|
||||
@@ -411,6 +423,11 @@ def main():
|
||||
print(f"[{i}/{len(pdfs)}] {fdate or '????-??-??'} {rel}")
|
||||
try:
|
||||
data, mode = process_pdf(client, args.model, pdf, args.vision_budget)
|
||||
# Bei Fehler EINMAL erneut versuchen (faengt seltene
|
||||
# nicht-deterministische Parse-Ausrutscher ab).
|
||||
if "_error" in data:
|
||||
print(f" (Fehler, ein Wiederholversuch...)")
|
||||
data, mode = process_pdf(client, args.model, pdf, args.vision_budget)
|
||||
except KeyboardInterrupt:
|
||||
print("\nAbbruch durch Nutzer. Bisheriges ist gesichert.")
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user