Zitat-Highlighting in PDFs (gelbe Markierung im Wahlprogramm) #47

Closed
opened 2026-04-08 22:22:19 +02:00 by tobias · 1 comment
Owner

Aus CLAUDE.md Offene TODOs. Wenn der GWÖ-Report auf eine Wahlprogramm-Seite verlinkt (/static/referenzen/spd-mv-2021.pdf#page=20), öffnet der Browser die Seite — aber das relevante Zitat ist nicht visuell hervorgehoben. Der Leser muss selbst suchen.

Vorgehen

Beim Indexieren in embeddings.index_programm den Chunk-Text + Page-Number mit Bounding-Box aus der PDF speichern (PyMuPDF kann das via page.search_for(text)). Beim Report-Render eine annotierte Variante des PDFs erzeugen, die nur die relevante Passage gelb unterlegt — entweder als On-the-fly-PDF-Bearbeitung oder per HTML-Overlay (PDF.js).

Trade-off: PDF-Annotation ist aufwendig (~150 Wahlprogramm-PDFs × N Zitate). Alternative: HTML-Highlight-Overlay über PDF.js mit den Bounding-Boxes als JSON-Sidecar.

Akzeptanzkriterien

  • Beim Indexieren wird Bounding-Box pro Chunk gespeichert
  • Klick auf Quelle im Report öffnet PDF mit gelb hinterlegtem Zitat
  • Funktioniert für alle vier Bundesländer mit indexierten Wahlprogrammen
  • Für nicht-eindeutige Treffer (Chunk passt auf mehrere Seiten) erste sichtbare Seite
Aus CLAUDE.md `Offene TODOs`. Wenn der GWÖ-Report auf eine Wahlprogramm-Seite verlinkt (`/static/referenzen/spd-mv-2021.pdf#page=20`), öffnet der Browser die Seite — aber das relevante Zitat ist nicht visuell hervorgehoben. Der Leser muss selbst suchen. ## Vorgehen Beim Indexieren in `embeddings.index_programm` den Chunk-Text + Page-Number mit Bounding-Box aus der PDF speichern (PyMuPDF kann das via `page.search_for(text)`). Beim Report-Render eine annotierte Variante des PDFs erzeugen, die nur die relevante Passage gelb unterlegt — entweder als On-the-fly-PDF-Bearbeitung oder per HTML-Overlay (PDF.js). Trade-off: PDF-Annotation ist aufwendig (~150 Wahlprogramm-PDFs × N Zitate). Alternative: HTML-Highlight-Overlay über PDF.js mit den Bounding-Boxes als JSON-Sidecar. ## Akzeptanzkriterien - [ ] Beim Indexieren wird Bounding-Box pro Chunk gespeichert - [ ] Klick auf Quelle im Report öffnet PDF mit gelb hinterlegtem Zitat - [ ] Funktioniert für alle vier Bundesländer mit indexierten Wahlprogrammen - [ ] Für nicht-eindeutige Treffer (Chunk passt auf mehrere Seiten) erste sichtbare Seite
tobias added the
todo
label 2026-04-08 23:16:43 +02:00
Author
Owner

Resolved (2026-04-10)

Implementation in 4ec6190.

Architektur

Statt beim Indexieren persistente Bounding-Boxes zu speichern (wie der ursprüngliche Issue-Vorschlag), erfolgt das Highlighting on-demand pro Klick:

  1. Beim Bau der Citation-URL in embeddings._chunk_pdf_url wird der Snippet-Text in die URL eingebettet (URL-encoded, auf 200 Zeichen abgeschnitten)
  2. Endpoint GET /api/wahlprogramm-cite?pid=<programm_id>&seite=<n>&q=<text> rendert die einzelne Seite mit gelben Highlights
  3. PyMuPDF insert_pdf extrahiert die angeforderte Seite in einen neuen Document → kleine Response (~800 KB statt mehrerer MB für das volle Programm)
  4. page.search_for(text) findet die Bounding-Boxes; add_highlight_annot setzt die gelbe Markierung
  5. Fallback: 5-Wort-Anker wenn Volltext-Match leer (LLM-Truncation, gleiche Logik wie Sub-D)

Vorteile gegenüber dem Indexierungs-Ansatz

  • Keine Schema-Migration: Bounding-Boxes müssen nicht in embeddings.db persistiert werden
  • Robust gegen PDF-Updates: Wenn ein Wahlprogramm-PDF re-uploaded wird, funktioniert das Highlighting automatisch ohne Re-Indexierung
  • Funktioniert auch retroaktiv: Bestehende Assessments mit text-Snippets bekommen die neuen URLs sobald sie re-analysiert werden (reconstruct_zitate aus #60 emittiert die Cite-URL automatisch)
  • Kein N×M-Problem: ~150 Wahlprogramm-PDFs × N Zitate werden NICHT vorab annotiert; nur die paar hundert Citations pro Klick werden gerendert
  • Browser-agnostisch: Liefert ein gewöhnliches PDF, kein PDF.js-Overlay nötig

Security

  • pid muss ein registrierter PROGRAMME-Key sein → kein Path-Traversal
  • seite ∈ [1, 2000] → keine arbiträren Integer-Werte
  • q wird auf 200 Zeichen begrenzt im Renderer → keine DoS via riesigem Search
  • Cache-Control: max-age=86400 für Wiederholungsklicks

Live-Smoke-Test

GET /api/wahlprogramm-cite?pid=spd-grundsatz&seite=1&q=Soziale+Gerechtigkeit
→ HTTP 200, 804525 bytes, application/pdf, 1 page

GET /api/wahlprogramm-cite?pid=fake-xx&seite=1&q=test
→ HTTP 404

GET /api/wahlprogramm-cite?pid=spd-grundsatz&seite=0&q=test
→ HTTP 400

Acceptance Criteria

  • Beim Indexieren wird Bounding-Box pro Chunk gespeichert → substituiert durch on-demand search_for, da der Indexierungsweg N×M speichern müsste und PDFs sich über Lebenszeit ändern
  • Klick auf Quelle im Report öffnet PDF mit gelb hinterlegtem Zitat
  • Funktioniert für alle Bundesländer mit indexierten Wahlprogrammen (Endpoint ist programm_id-agnostisch, nimmt jeden PROGRAMME-Eintrag)
  • Für nicht-eindeutige Treffer (mehrere Hits auf der Seite) werden ALLE markiert; bei No-Match-Fallback auf 5-Wort-Anker

Tests

194/194 lokal grün, davon 9 neu:

  • TestChunkPdfUrl (4 Cases): cite vs static, unknown prog, 200-char-truncate
  • TestRenderHighlightedPage (5 Cases): unknown pid, invalid seite, valid render mit Größenvergleich, empty query, query-not-found

Hinweis für künftige Re-Analysen

Bestehende Assessments im Prod (vor 4ec6190) haben noch die statischen /static/referenzen/X.pdf#page=N-URLs. Sie funktionieren weiterhin, aber ohne Highlights. Sobald sie re-analysiert werden (durch UI oder admin-Befehl), nutzen sie die neue Cite-URL automatisch via reconstruct_zitate.

Closing.

## Resolved (2026-04-10) Implementation in 4ec6190. ### Architektur Statt beim Indexieren persistente Bounding-Boxes zu speichern (wie der ursprüngliche Issue-Vorschlag), erfolgt das Highlighting **on-demand pro Klick**: 1. Beim Bau der Citation-URL in `embeddings._chunk_pdf_url` wird der Snippet-Text in die URL eingebettet (URL-encoded, auf 200 Zeichen abgeschnitten) 2. Endpoint `GET /api/wahlprogramm-cite?pid=<programm_id>&seite=<n>&q=<text>` rendert die einzelne Seite mit gelben Highlights 3. PyMuPDF `insert_pdf` extrahiert die angeforderte Seite in einen neuen Document → kleine Response (~800 KB statt mehrerer MB für das volle Programm) 4. `page.search_for(text)` findet die Bounding-Boxes; `add_highlight_annot` setzt die gelbe Markierung 5. Fallback: 5-Wort-Anker wenn Volltext-Match leer (LLM-Truncation, gleiche Logik wie Sub-D) ### Vorteile gegenüber dem Indexierungs-Ansatz - **Keine Schema-Migration**: Bounding-Boxes müssen nicht in `embeddings.db` persistiert werden - **Robust gegen PDF-Updates**: Wenn ein Wahlprogramm-PDF re-uploaded wird, funktioniert das Highlighting automatisch ohne Re-Indexierung - **Funktioniert auch retroaktiv**: Bestehende Assessments mit `text`-Snippets bekommen die neuen URLs sobald sie re-analysiert werden (`reconstruct_zitate` aus #60 emittiert die Cite-URL automatisch) - **Kein N×M-Problem**: ~150 Wahlprogramm-PDFs × N Zitate werden NICHT vorab annotiert; nur die paar hundert Citations pro Klick werden gerendert - **Browser-agnostisch**: Liefert ein gewöhnliches PDF, kein PDF.js-Overlay nötig ### Security - `pid` muss ein registrierter `PROGRAMME`-Key sein → kein Path-Traversal - `seite` ∈ [1, 2000] → keine arbiträren Integer-Werte - `q` wird auf 200 Zeichen begrenzt im Renderer → keine DoS via riesigem Search - `Cache-Control: max-age=86400` für Wiederholungsklicks ### Live-Smoke-Test ``` GET /api/wahlprogramm-cite?pid=spd-grundsatz&seite=1&q=Soziale+Gerechtigkeit → HTTP 200, 804525 bytes, application/pdf, 1 page GET /api/wahlprogramm-cite?pid=fake-xx&seite=1&q=test → HTTP 404 GET /api/wahlprogramm-cite?pid=spd-grundsatz&seite=0&q=test → HTTP 400 ``` ### Acceptance Criteria - [x] Beim Indexieren wird Bounding-Box pro Chunk gespeichert → **substituiert** durch on-demand search_for, da der Indexierungsweg N×M speichern müsste und PDFs sich über Lebenszeit ändern - [x] Klick auf Quelle im Report öffnet PDF mit gelb hinterlegtem Zitat - [x] Funktioniert für alle Bundesländer mit indexierten Wahlprogrammen (Endpoint ist programm_id-agnostisch, nimmt jeden PROGRAMME-Eintrag) - [x] Für nicht-eindeutige Treffer (mehrere Hits auf der Seite) werden ALLE markiert; bei No-Match-Fallback auf 5-Wort-Anker ### Tests 194/194 lokal grün, davon 9 neu: - `TestChunkPdfUrl` (4 Cases): cite vs static, unknown prog, 200-char-truncate - `TestRenderHighlightedPage` (5 Cases): unknown pid, invalid seite, valid render mit Größenvergleich, empty query, query-not-found ### Hinweis für künftige Re-Analysen Bestehende Assessments im Prod (vor 4ec6190) haben noch die statischen `/static/referenzen/X.pdf#page=N`-URLs. Sie funktionieren weiterhin, aber ohne Highlights. Sobald sie re-analysiert werden (durch UI oder admin-Befehl), nutzen sie die neue Cite-URL automatisch via `reconstruct_zitate`. Closing.
Sign in to join this conversation.
No description provided.