pagemark
pagemark converts PDFs to Markdown with vision-language models.
Use it with any OpenAI-compatible endpoint. You provide the base_url, model,
and api_key. pagemark renders each page, grounds the model with the PDF text
layer, and checks the result for common failures: repetition loops, truncation,
dropped columns, and hallucinated tables. If a check fails, it retries with a
stricter strategy and falls back to the text layer if the model still cannot
recover.
If one page fails, the document still finishes. pagemark records warnings for that page instead of aborting the whole conversion.
Prerequisites
- Python 3.10+
- An OpenAI-compatible endpoint serving a vision-language model (Ollama, vLLM, LM Studio, any hosted provider, etc.)
Install
pip install pagemark # library only
pip install "pagemark[cli]" # with CLI
Quick start
from pagemark import Pagemark
client = Pagemark(
base_url="http://localhost:11434/v1", # OpenAI-compatible API
api_key="your-key", # API key
)
doc = client.convert("paper.pdf", model="qwen3-vl:8b")
print(doc.markdown)
doc.save("out.md", assets_dir="out_assets/")
api_key is optional. pagemark resolves it from the explicit argument first,
then from OPENAI_API_KEY. If neither exists, it raises an error that names the
base_url.
Async
aconvert() is the implementation behind convert(). convert() wraps it with
asyncio.run(). Pages run concurrently, bounded by a semaphore. The default
limit is 4.
from pagemark import Pagemark
client = Pagemark(
base_url="http://localhost:11434/v1", # OpenAI-compatible API
api_key="your-key", # API key
)
doc = await client.aconvert(
"paper.pdf",
model="qwen3-vl:8b",
concurrency=8,
)
Page selection
Convert selected pages instead of the whole document:
doc = client.convert("paper.pdf", model="qwen3-vl:8b", pages="1-5,9,12-")
pages accepts "3", "1-5", "1-5,9,12-", or a list[int]. Out-of-range
indices raise an error instead of being silently clipped.
Bring your own model
If you need settings the constructor does not expose, pass a pre-configured
BaseChatModel. That covers custom timeouts, proxies, non-OpenAI LangChain
backends like ChatAnthropic or ChatOllama, callbacks, and similar options.
from langchain_openai import ChatOpenAI
from pagemark import Pagemark
my_model = ChatOpenAI(
model="...",
base_url="...",
api_key="...",
timeout=120,
max_retries=5,
)
client = Pagemark(chat_model=my_model)
doc = client.convert("paper.pdf")
CLI
pagemark convert paper.pdf -o paper.md \
--model qwen3-vl:8b \
--base-url http://localhost:11434/v1
pagemark convert paper.pdf -o paper.md --json paper.json \
--model qwen3-vl:8b \
--base-url http://localhost:11434/v1
pagemark doctor --base-url http://localhost:11434/v1 --model qwen3-vl:8b
doctor checks whether the endpoint is reachable, whether the model is present,
and where the API key came from.
How it works
pdf -> render + anchor -> prompt (profile) -> ChatOpenAI -> guardrails -> parse -> assemble -> Document
pypdfium2 per-model langchain retry ladder md-it stitching md + json
- Render each page to a PNG with pypdfium2, downscaled to the profile's pixel budget.
- Extract the PDF text layer as an anchor for the model. Long pages are middle-elided to about 6k characters.
- Send the image and anchor text to the VLM.
- Check every response with five guardrails.
- Retry with stricter strategies when a guardrail fails.
- Assemble the pages into a
Documentwith typed blocks, then stitch across pages by stripping running headers and footers, joining split paragraphs, and dehyphenating text.
Guardrails
| Check | What it catches |
|---|---|
| Repetition | n-gram loops, collapsed gzip ratio |
| Truncation | finish_reason == "length" |
| Coverage | anchor tokens missing from output; catches silently dropped columns |
| Hallucination | high rate of output tokens absent from anchor (born-digital pages) |
| Refusal | "I'm sorry", "I cannot", near-empty output on a non-blank page |
Escalation ladder
When any guardrail fails, pagemark retries with more aggressive strategies:
| Step | Strategy | Confidence ceiling |
|---|---|---|
| 1 | Normal render, standard prompt | 0.95 |
| 2 | Reseed: temperature=0.3, seed=42 |
0.85 |
| 3 | High DPI: DPI x1.5 (capped at 400), pixels x1.5 | 0.75 |
| 4 | Fallback prompt: simpler instruction | 0.60 |
| 5 | Text layer fallback | 0.30 |
Each page includes source, confidence, and warnings, so downstream
consumers can filter by quality.
Output
convert() returns a Document with:
markdown: the full stitched Markdown outputpages: list ofPageobjects, each with:markdown: per-page Markdownblocks: typed blocks (heading, paragraph, table, figure, code, list, formula, caption, footnote)source:"vlm"or"text_layer"(if the model couldn't handle the page)confidence: 0-1 scorewarnings: what went wrong, if anything
assets: embedded images extracted from the PDFmodel_dump_json(): full structured output as JSONsave(path, assets_dir=...): writes the Markdown file and extracted images
License
MIT. Runtime dependencies are permissively licensed - mostly MIT/Apache-2.0/BSD, with a few under MPL-2.0 (certifi, orjson) and PSF-2.0 (typing-extensions). No GPL/LGPL/AGPL dependencies in the tree.
Metadata
Release files for pagemark 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pagemark-0.1.0.tar.gz | 23.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pagemark-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.0 kB
Release files / pagemark-0.1.0.tar.gz
| Download URL | pagemark-0.1.0.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
da3ef62444286626072d3cd7a2c72ab6e494cd60f96776398a85dad717efb0eb
|
|
BLAKE2b-256 checksum How to use checksums |
1daf67ee5b40ee741ea479910bfecfefc60a3963bb548ad05582e6a11de1bbc7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / pagemark-0.1.0-py3-none-any.whl
| Download URL | pagemark-0.1.0-py3-none-any.whl |
|---|---|
| Size | 21.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6662684265814718eca54f7e1662578ef35450ead29c14f1978bfa87b3e22ff4
|
|
BLAKE2b-256 checksum How to use checksums |
ec975671f7f93b1f0caa91a4efb7cf7bb073b9933c3bbb42068b74686f43a125
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log