#47 Fix: Highlighting für falsche Seitenzahlen + Year-Suffix-Matching
Zwei Bugs aus User-Test: 1. "Unbekanntes Wahlprogramm" bei Klick auf Grünes Grundsatzprogramm: Pre-#60 Assessments haben halluzinierte Dateinamen wie "gruene-grundsatzprogramm-2020.pdf" statt "gruene-grundsatzprogramm.pdf". Fix: Year-Suffix-Stripping im Reverse-Lookup (X-YYYY.pdf → X.pdf). 2. "Eine Seite, aber kein Highlighting": Pre-#60 Assessments haben oft falsche Seitennummern. search_for findet nichts auf der falschen Seite. Fix: wenn die angegebene Seite leer ist, ALLE Seiten durchsuchen und die erste mit einem Treffer nehmen. So funktioniert Highlighting auch bei halluzinierten Seitenzahlen retroaktiv. Performance: ~50ms pro PDF (Grundsatzprogramme haben ~100-160 Seiten), akzeptabel für on-demand. Tests: 194/194 grün. Refs: #47
This commit is contained in:
parent
47897e13cd
commit
5a035be20b
@ -619,28 +619,37 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona
|
|||||||
if seite < 1 or seite > len(src):
|
if seite < 1 or seite > len(src):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Single-page Sub-PDF erzeugen — hält den Response klein und
|
# Suche den Needle auf der angegebenen Seite. Falls dort nichts
|
||||||
# schließt versehentliche Cross-Page-Highlights aus.
|
# gefunden wird (Pre-#60-Assessments haben oft falsche Seiten-
|
||||||
|
# nummern), durchsuchen wir ALLE Seiten und nehmen die erste
|
||||||
|
# mit einem Treffer — so funktioniert Highlighting auch bei
|
||||||
|
# halluzinierten Seitenzahlen retroaktiv.
|
||||||
|
target_page_idx = seite - 1
|
||||||
|
if needle:
|
||||||
|
clean = needle.replace("\u00ad", "")
|
||||||
|
# Versuch 1: angegebene Seite
|
||||||
|
rects = src[target_page_idx].search_for(clean)
|
||||||
|
if not rects:
|
||||||
|
words = clean.split()
|
||||||
|
anchor = " ".join(words[:5]) if len(words) >= 5 else clean
|
||||||
|
rects = src[target_page_idx].search_for(anchor)
|
||||||
|
# Versuch 2: wenn Seite leer, alle Seiten durchsuchen
|
||||||
|
if not rects:
|
||||||
|
for i in range(len(src)):
|
||||||
|
rects = src[i].search_for(anchor if len(words) >= 5 else clean)
|
||||||
|
if rects:
|
||||||
|
target_page_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Single-page Sub-PDF erzeugen
|
||||||
new = fitz.open()
|
new = fitz.open()
|
||||||
try:
|
try:
|
||||||
new.insert_pdf(src, from_page=seite - 1, to_page=seite - 1)
|
new.insert_pdf(src, from_page=target_page_idx, to_page=target_page_idx)
|
||||||
page = new[0]
|
page = new[0]
|
||||||
|
|
||||||
if needle:
|
if needle and rects:
|
||||||
# PyMuPDF ist tolerant gegen Whitespace, aber Soft-Hyphen
|
# Rects beziehen sich auf src-Page-Koordinaten — nach insert_pdf
|
||||||
# bricht den Match — analog zu _normalize_for_match
|
# sind die Koordinaten identisch (gleiche MediaBox).
|
||||||
# entfernen wir \xad vor dem search_for.
|
|
||||||
clean = needle.replace("\u00ad", "")
|
|
||||||
rects = page.search_for(clean)
|
|
||||||
if not rects:
|
|
||||||
# Fallback: nur die ersten 5 Wörter als Anker — analog
|
|
||||||
# zu find_chunk_for_text. Wenn der LLM den Snippet
|
|
||||||
# mid-sentence gekürzt hat, bricht der Volltext-Match,
|
|
||||||
# aber 5-Wort-Sequenz findet die Stelle trotzdem.
|
|
||||||
words = clean.split()
|
|
||||||
if len(words) >= 5:
|
|
||||||
anchor = " ".join(words[:5])
|
|
||||||
rects = page.search_for(anchor)
|
|
||||||
for rect in rects:
|
for rect in rects:
|
||||||
annot = page.add_highlight_annot(rect)
|
annot = page.add_highlight_annot(rect)
|
||||||
if annot is not None:
|
if annot is not None:
|
||||||
|
|||||||
13
app/main.py
13
app/main.py
@ -617,11 +617,24 @@ async def wahlprogramm_cite(pid: str = "", pdf: str = "", seite: int = 1, q: str
|
|||||||
auf int gezwungen. ``q`` ist auf 200 Zeichen begrenzt im Renderer.
|
auf int gezwungen. ``q`` ist auf 200 Zeichen begrenzt im Renderer.
|
||||||
"""
|
"""
|
||||||
# Reverse-Lookup: pdf-Filename → programm_id, falls nur pdf angegeben.
|
# Reverse-Lookup: pdf-Filename → programm_id, falls nur pdf angegeben.
|
||||||
|
# Zwei Stufen: exakter Match, dann fuzzy (Year-Suffix-Stripping), weil
|
||||||
|
# Pre-#47 Assessments halluzinierte Dateinamen haben können, z.B.
|
||||||
|
# "gruene-grundsatzprogramm-2020.pdf" statt "gruene-grundsatzprogramm.pdf".
|
||||||
if not pid and pdf:
|
if not pid and pdf:
|
||||||
|
# Stage 1: exakt
|
||||||
for p, info in PROGRAMME.items():
|
for p, info in PROGRAMME.items():
|
||||||
if info.get("pdf") == pdf:
|
if info.get("pdf") == pdf:
|
||||||
pid = p
|
pid = p
|
||||||
break
|
break
|
||||||
|
# Stage 2: Year-Suffix stripping (z.B. "X-2020.pdf" → "X.pdf")
|
||||||
|
if not pid:
|
||||||
|
import re
|
||||||
|
stripped = re.sub(r"-\d{4}\.pdf$", ".pdf", pdf)
|
||||||
|
if stripped != pdf:
|
||||||
|
for p, info in PROGRAMME.items():
|
||||||
|
if info.get("pdf") == stripped:
|
||||||
|
pid = p
|
||||||
|
break
|
||||||
if pid not in PROGRAMME:
|
if pid not in PROGRAMME:
|
||||||
raise HTTPException(status_code=404, detail="Unbekanntes Wahlprogramm")
|
raise HTTPException(status_code=404, detail="Unbekanntes Wahlprogramm")
|
||||||
if seite < 1 or seite > 2000:
|
if seite < 1 or seite > 2000:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user