diff --git a/app/embeddings.py b/app/embeddings.py index 0140bd0..b67b52d 100644 --- a/app/embeddings.py +++ b/app/embeddings.py @@ -619,28 +619,37 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona if seite < 1 or seite > len(src): return None - # Single-page Sub-PDF erzeugen — hält den Response klein und - # schließt versehentliche Cross-Page-Highlights aus. + # Suche den Needle auf der angegebenen Seite. Falls dort nichts + # 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() 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] - if needle: - # PyMuPDF ist tolerant gegen Whitespace, aber Soft-Hyphen - # bricht den Match — analog zu _normalize_for_match - # 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) + if needle and rects: + # Rects beziehen sich auf src-Page-Koordinaten — nach insert_pdf + # sind die Koordinaten identisch (gleiche MediaBox). for rect in rects: annot = page.add_highlight_annot(rect) if annot is not None: diff --git a/app/main.py b/app/main.py index 0a86961..0d34b68 100644 --- a/app/main.py +++ b/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. """ # 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: + # Stage 1: exakt for p, info in PROGRAMME.items(): if info.get("pdf") == pdf: pid = p 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: raise HTTPException(status_code=404, detail="Unbekanntes Wahlprogramm") if seite < 1 or seite > 2000: