Page preview¶
The evidence-panel page-preview service (ADR-010):
on-demand locate + render behind the two …/preview/* routes — markdown
de-decoration, trigram page scoring and pdfium highlight rects, PNG page
rendering, source resolution (containment + content-hash verification), and
the cached LibreOffice PPTX conversion.
varagity.preview
¶
Evidence-panel document page preview (ADR-010).
On-demand, server-side locate + render: given a chunk's text and its
parent document, find the one page/slide containing it (word-trigram
coverage over pdfium page text), compute highlight rectangles (pdfium
text search over de-decorated snippets), and render that page to a PNG.
No ingest-time provenance, no schema changes, no reingest — everything
keys off the content-addressed doc_id, so results are immutable and
cacheable forever.
ConversionFailed
¶
Bases: Exception
A conversion ran and failed (non-zero exit, timeout, missing output).
Source code in varagity/preview/convert.py
37 38 | |
ConversionUnavailable
¶
Bases: Exception
No converter is available to this process (degrade, never crash).
Raised when LibreOffice is absent — a host-mode run without it loses
only PPTX previews (the route answers conversion_unavailable).
Source code in varagity/preview/convert.py
29 30 31 32 33 34 | |
LocateResult
dataclass
¶
Where a chunk's text lives in its source document.
Attributes:
| Name | Type | Description |
|---|---|---|
page |
int | None
|
Best-matching page, 1-based; |
page_count |
int
|
Total pages in the document. |
rects |
list[tuple[float, float, float, float]]
|
Highlight rectangles |
coverage |
float
|
The winning page's containment score in |
Source code in varagity/preview/locate.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
PreviewSource
dataclass
¶
A document resolved to an openable PDF, or the reason it degraded.
Attributes:
| Name | Type | Description |
|---|---|---|
pdf_path |
Path | None
|
The pdfium-openable PDF (the source itself for |
reason |
str | None
|
The degradable condition when |
Source code in varagity/preview/source.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
conversion_cache_path(doc_id)
¶
Return the cached-PDF path for one document's conversion.
Content-addressed: doc_id hashes the source path and its byte
content, so a cache hit can never be stale. The cache lives in the
process's temp directory — container-ephemeral by design (a restart
re-pays one conversion per deck).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The cache path (existence not implied). |
Source code in varagity/preview/convert.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
ensure_pdf(source, doc_id, *, timeout_s)
¶
Return a PDF rendition of source, converting on first use.
Cache first — a previously converted deck answers instantly (and keeps
answering even if LibreOffice later disappears from the image). On a
miss, one soffice --headless --convert-to pdf run executes under
the module lock with a throwaway -env:UserInstallation profile,
and its output moves atomically into the cache path (a concurrent
reader never sees a partial file).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Path
|
The source document (a |
required |
doc_id
|
str
|
The document's stable id (the cache key). |
required |
timeout_s
|
int
|
Conversion timeout in seconds ( |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The pdfium-openable converted PDF. |
Raises:
| Type | Description |
|---|---|
ConversionUnavailable
|
LibreOffice ( |
ConversionFailed
|
The conversion ran and failed — non-zero exit, timeout, or no PDF produced. |
Source code in varagity/preview/convert.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
normalize_chunk_text(text)
¶
Strip markdown decoration, keeping the words as the page shows them.
Inter-word punctuation ("Firefly,", "12,400") survives — pdfium
search needs the needle's characters to match the page text exactly,
whitespace aside — while pure decoration (markers, pipes, placeholder
comments) is removed and whitespace collapses to single spaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The chunk's markdown content. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The de-decorated text, whitespace-collapsed and stripped. |
Source code in varagity/preview/normalize.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
render_page_png(pdf_path, page, *, width)
¶
Rasterize one page (1-based) to a PNG of the given pixel width.
Height follows the page's aspect ratio (scale = width / page_width
in PDF canvas units).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pdf_path
|
Path
|
A pdfium-openable PDF. |
required |
page
|
int
|
1-based page number. |
required |
width
|
int
|
Target image width in pixels. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
The PNG bytes. |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
PdfiumError
|
If the file cannot be opened as a PDF. |
Source code in varagity/preview/render.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
resolve_preview_source(info, settings)
¶
Resolve one ingested document to a PDF the preview can open.
Every failure mode is a degradable outcome, never an exception — the
routes turn reason into available:false (locate) or a 404 code
(page image), and the GUI falls back to the full-text view.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
info
|
DocumentInfo
|
The document's stored metadata ( |
required |
settings
|
Settings
|
The effective settings (kill switch, |
required |
Returns:
| Type | Description |
|---|---|
PreviewSource
|
The openable PDF path, or the reason there isn't one. |
Source code in varagity/preview/source.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
snippets(text, size=8, stride=4)
¶
Break normalized text into short pdfium-searchable needles.
Sentences of at most size whitespace tokens search whole; longer
ones become overlapping size-token windows every stride tokens
(the overlap closes the highlight gaps non-overlapping windows leave),
plus a tail window so the sentence's end is always covered. Tokens keep
their punctuation — the needle must match the page's characters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Normalized chunk text (:func: |
required |
size
|
int
|
Window length in whitespace tokens. |
8
|
stride
|
int
|
Window step in whitespace tokens. |
4
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The needles, deduplicated, in reading order. |
Source code in varagity/preview/normalize.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
words(text)
¶
Tokenize text into lowercase word tokens for page scoring.
Punctuation-insensitive by construction (\w+ runs), so chunk and
page text tokenize identically regardless of markdown-vs-layout
punctuation differences.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Any text (normalized chunk text or raw pdfium page text). |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The lowercase word tokens, in order. |
Source code in varagity/preview/normalize.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
varagity.preview.normalize
¶
Markdown de-decoration for preview text matching (ADR-010).
Chunk text is docling markdown — headings, emphasis, GFM table pipes,
<!-- image -->-style placeholder comments, list bullets, dot leaders —
while a PDF's text layer carries none of that decoration. These pure
functions reduce a chunk to the words that actually appear on the page, so
page scoring (:func:words) and pdfium snippet search (:func:snippets)
can match against the extracted page text.
normalize_chunk_text(text)
¶
Strip markdown decoration, keeping the words as the page shows them.
Inter-word punctuation ("Firefly,", "12,400") survives — pdfium
search needs the needle's characters to match the page text exactly,
whitespace aside — while pure decoration (markers, pipes, placeholder
comments) is removed and whitespace collapses to single spaces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The chunk's markdown content. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The de-decorated text, whitespace-collapsed and stripped. |
Source code in varagity/preview/normalize.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
snippets(text, size=8, stride=4)
¶
Break normalized text into short pdfium-searchable needles.
Sentences of at most size whitespace tokens search whole; longer
ones become overlapping size-token windows every stride tokens
(the overlap closes the highlight gaps non-overlapping windows leave),
plus a tail window so the sentence's end is always covered. Tokens keep
their punctuation — the needle must match the page's characters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Normalized chunk text (:func: |
required |
size
|
int
|
Window length in whitespace tokens. |
8
|
stride
|
int
|
Window step in whitespace tokens. |
4
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The needles, deduplicated, in reading order. |
Source code in varagity/preview/normalize.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
words(text)
¶
Tokenize text into lowercase word tokens for page scoring.
Punctuation-insensitive by construction (\w+ runs), so chunk and
page text tokenize identically regardless of markdown-vs-layout
punctuation differences.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Any text (normalized chunk text or raw pdfium page text). |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The lowercase word tokens, in order. |
Source code in varagity/preview/normalize.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
varagity.preview.locate
¶
Find the page and highlight rects of a chunk's text (ADR-010).
Locate-at-preview-time: no per-chunk page/bbox provenance exists at ingest (ADR-005 §5 deferred it), so the preview path scores every page of the source PDF by word-trigram containment and computes highlight rectangles via pdfium text search — deterministic, format-agnostic, and working retroactively for every already-ingested corpus.
LocateResult
dataclass
¶
Where a chunk's text lives in its source document.
Attributes:
| Name | Type | Description |
|---|---|---|
page |
int | None
|
Best-matching page, 1-based; |
page_count |
int
|
Total pages in the document. |
rects |
list[tuple[float, float, float, float]]
|
Highlight rectangles |
coverage |
float
|
The winning page's containment score in |
Source code in varagity/preview/locate.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
locate(pdf_path, chunk_text, *, min_coverage)
¶
Find the best-matching page for a chunk and its highlight rects.
Pages are scored by word-trigram containment (unigram for very short chunks); ties keep the lowest page. On the winning page, the chunk's sentences become short search needles whose pdfium match rectangles — deduplicated and capped — form the highlight overlay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pdf_path
|
Path
|
A pdfium-openable PDF (the source itself, or a converted rendition). |
required |
chunk_text
|
str
|
The chunk's markdown content, as both wire shapes deliver it to the client. |
required |
min_coverage
|
float
|
Coverage floor below which the result reports no page
( |
required |
Returns:
| Type | Description |
|---|---|
LocateResult
|
The located page, its highlight rects, and the coverage score. |
Raises:
| Type | Description |
|---|---|
PdfiumError
|
If the file cannot be opened as a PDF. |
Source code in varagity/preview/locate.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
varagity.preview.render
¶
Render one document page to a PNG (ADR-010).
The image side of the preview pair: the locate route names a page, this
renders it at PREVIEW_RENDER_WIDTH for the evidence panel's <img>.
Every pdfium call — the PIL encode included, since the bitmap buffer
belongs to pdfium — runs under :data:varagity.preview.locate.PDFIUM_LOCK.
render_page_png(pdf_path, page, *, width)
¶
Rasterize one page (1-based) to a PNG of the given pixel width.
Height follows the page's aspect ratio (scale = width / page_width
in PDF canvas units).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pdf_path
|
Path
|
A pdfium-openable PDF. |
required |
page
|
int
|
1-based page number. |
required |
width
|
int
|
Target image width in pixels. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
The PNG bytes. |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
PdfiumError
|
If the file cannot be opened as a PDF. |
Source code in varagity/preview/render.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
varagity.preview.source
¶
Resolve an ingested document to a pdfium-openable PDF (ADR-010).
The gate both preview routes share: kill switch, format eligibility,
DOCS_PATH containment (mirroring the delete route's rule — a stored
source is data, not authority), on-disk content-hash verification (an
edited-but-not-reingested file must degrade honestly, not preview the
wrong bytes), and — for PPTX — the cached LibreOffice conversion.
PreviewSource
dataclass
¶
A document resolved to an openable PDF, or the reason it degraded.
Attributes:
| Name | Type | Description |
|---|---|---|
pdf_path |
Path | None
|
The pdfium-openable PDF (the source itself for |
reason |
str | None
|
The degradable condition when |
Source code in varagity/preview/source.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
resolve_preview_source(info, settings)
¶
Resolve one ingested document to a PDF the preview can open.
Every failure mode is a degradable outcome, never an exception — the
routes turn reason into available:false (locate) or a 404 code
(page image), and the GUI falls back to the full-text view.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
info
|
DocumentInfo
|
The document's stored metadata ( |
required |
settings
|
Settings
|
The effective settings (kill switch, |
required |
Returns:
| Type | Description |
|---|---|
PreviewSource
|
The openable PDF path, or the reason there isn't one. |
Source code in varagity/preview/source.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
varagity.preview.convert
¶
PPTX→PDF conversion for previews (ADR-010).
Headless LibreOffice Impress renders a deck to a PDF whose page N is
slide N — the same identity docling relies on — so :mod:~varagity.preview.locate
and :mod:~varagity.preview.render work unchanged on the converted
artifact. Conversions are cached by doc_id (content-addressed, so a
hit can never be stale), serialized behind a module lock (LibreOffice's
profile is single-instance), and bounded by PREVIEW_CONVERT_TIMEOUT_S.
A host-mode run without LibreOffice degrades (conversion_unavailable),
never crashes.
ConversionFailed
¶
Bases: Exception
A conversion ran and failed (non-zero exit, timeout, missing output).
Source code in varagity/preview/convert.py
37 38 | |
ConversionUnavailable
¶
Bases: Exception
No converter is available to this process (degrade, never crash).
Raised when LibreOffice is absent — a host-mode run without it loses
only PPTX previews (the route answers conversion_unavailable).
Source code in varagity/preview/convert.py
29 30 31 32 33 34 | |
conversion_cache_path(doc_id)
¶
Return the cached-PDF path for one document's conversion.
Content-addressed: doc_id hashes the source path and its byte
content, so a cache hit can never be stale. The cache lives in the
process's temp directory — container-ephemeral by design (a restart
re-pays one conversion per deck).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc_id
|
str
|
The document's stable id. |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The cache path (existence not implied). |
Source code in varagity/preview/convert.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
ensure_pdf(source, doc_id, *, timeout_s)
¶
Return a PDF rendition of source, converting on first use.
Cache first — a previously converted deck answers instantly (and keeps
answering even if LibreOffice later disappears from the image). On a
miss, one soffice --headless --convert-to pdf run executes under
the module lock with a throwaway -env:UserInstallation profile,
and its output moves atomically into the cache path (a concurrent
reader never sees a partial file).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Path
|
The source document (a |
required |
doc_id
|
str
|
The document's stable id (the cache key). |
required |
timeout_s
|
int
|
Conversion timeout in seconds ( |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The pdfium-openable converted PDF. |
Raises:
| Type | Description |
|---|---|
ConversionUnavailable
|
LibreOffice ( |
ConversionFailed
|
The conversion ran and failed — non-zero exit, timeout, or no PDF produced. |
Source code in varagity/preview/convert.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |