Lightweight VLM OCR that turns scientific manuscript PDFs into clean Markdown (.md) or simple '##' headers, on CPU, GPU, or Apple Silicon.
Project description
scientificpub2md
Lightweight VLM OCR that turns scientific manuscript PDFs into clean Markdown — on CPU, GPU, or Apple Silicon.
Quick start · Formats · Engines · How it works · Troubleshooting
Why
Most PDF parsers choke on real scientific papers: they mangle multi-column
reading order, silently drop the detailed methods, or truncate long
manuscripts. scientificpub2md takes a different route — it renders each
page to an image and transcribes it one page at a time with a vision-language
model, then stitches the pages back together. Nothing is truncated however
long the paper, the reading order is the one a human sees, and every section
heading comes out as a clean Markdown header.
PDF ──render pages (PyMuPDF)──▶ page images ──VLM transcribe──▶ clean text ──▶ .md or ## headers
- Verbatim, not summarized — the model transcribes the page, it doesn't paraphrase.
- No truncation — pages are processed individually, so a 50-page paper is fully captured.
- Headings preserved — every section and sub-section becomes a Markdown header.
- Deterministic — greedy decoding means re-extracting a PDF is byte-reproducible.
- Runs anywhere — in-process on CPU, NVIDIA GPU, or Apple Silicon (MPS); or via a fast batched vLLM server.
Quick start
pip install git+https://github.com/jimnoneill/scientificpub2md
# Single PDF → Markdown (default engine: LightOnOCR-2-1B; auto-selects GPU → Apple MPS → CPU)
scientificpub2md paper.pdf
# A whole directory of PDFs into an output folder
scientificpub2md ./pdfs/ -o ./markdown/
# Simple flat "## " headers instead of structured markdown
scientificpub2md paper.pdf --format headers
# Use the Qwen3-VL-8B scientific engine (drops references, preserves methods)
pip install "scientificpub2md[qwen] @ git+https://github.com/jimnoneill/scientificpub2md"
scientificpub2md paper.pdf --engine qwen3vl
Output lands next to each PDF (paper.pdf → paper.md); use -o for an explicit file or directory.
From Python:
from scientificpub2md import pdf_to_markdown
md = pdf_to_markdown("paper.pdf") # default (lightonocr), auto device
md = pdf_to_markdown("paper.pdf", engine="qwen3vl") # Qwen3-VL-8B + scientific prompt
Two output formats
| Format | Flag | Ext | What you get |
|---|---|---|---|
| Markdown (default) | --format md |
.md |
Structured: title → #, canonical sections (Abstract, Methods, Results, …) → ##, sub-headings → ###. |
| Simple headers | --format headers |
.txt |
The clean full text with every heading flat at ##. Minimal. |
| Clean | --format clean |
.clean.txt |
Junk-stripped, flat-##, verbatim text with missing headers inferred — ready for a downstream section/feature pipeline. |
The heading levelling is deterministic (a section-name vocabulary, no extra LLM call), so output is reproducible.
The clean format — deterministic, pipeline-ready
clean is built for feeding a downstream parser (it's the layer that lets a faithful OCR like
LightOnOCR stand in for an editorial, prompt-steered VLM). It applies, with no model (every
step is a rule, so it's byte-reproducible and never rewrites words — lines are only dropped and
headings only marked):
- drops running heads/footers (lines repeating across pages), page/line numbers, horizontal rules, DOI/copyright-only lines, and front-matter banners (
OPEN ACCESS,RESEARCH ARTICLE, journal mastheads likePNAS) - scrubs back-matter sections — references, acknowledgements, funding, author contributions, competing interests, data availability, supplementary (configurable via
clean_document(..., scrub_sections=...)) - normalizes every heading to a flat
## - infers missing headers — promotes unmarked section-vocabulary lines and splits run-in headers (
Introduction: …) onto their own##line, the way a prompted VLM would
from scientificpub2md import make_backend, extract_pdf, clean_document
raw = extract_pdf("paper.pdf", make_backend("transformers", engine="lightonocr"))
text = clean_document(raw) # verbatim, junk-stripped, '## '-denoted
Two engines
Pick with --engine (or engine= in Python). They're complementary — a tiny specialized OCR model, and a steerable general VLM tuned for papers.
--engine lightonocr (default) |
--engine qwen3vl |
|
|---|---|---|
| Model | LightOnOCR-2-1B (1B, purpose-built OCR) | Qwen3-VL-8B (8B, general VLM + scientific prompt) |
| Install | base (pip install scientificpub2md) |
needs [qwen] extra (torchvision) for transformers |
| Memory | ~2–3 GB — great on a laptop | ~16 GB (bf16) |
| Speed | much faster (~5 pages/s on an H100) | slower |
| Editorial smarts | faithful full-page transcription (keeps everything) | drops references / acknowledgements / page furniture, preserves methods |
| Tables & equations | native Markdown tables + LaTeX | as plain text |
| Languages | 11 languages | English-tuned prompt |
| Best for | fast/cheap transcription, tables/math, Macs & small GPUs | scientific-corpus building, back-matter dropping, reproducibility |
macOS / Apple Silicon: both engines run on MPS, but the 8B Qwen model is heavy for most Macs — the default
lightonocris small and fast on a MacBook.
Example output (--format md)
# Emergence and evolution of heterocyte glycolipid biosynthesis in cyanobacteria
## Abstract
Heterocytes, specialized cells for nitrogen fixation in cyanobacteria, are …
## Results and Discussion
### Genomic Prediction of HG Biosynthesis
To investigate the evolution of HG biosynthesis within cyanobacteria, we searched …
Install options
Requires Python 3.10+.
# Simplest — base install (default lightonocr engine), CPU/MPS wheels:
pip install git+https://github.com/jimnoneill/scientificpub2md
# NVIDIA GPU — install a CUDA build of torch first, then the package:
pip install torch --index-url https://download.pytorch.org/whl/cu124 # match your CUDA
pip install git+https://github.com/jimnoneill/scientificpub2md
# Add the Qwen3-VL engine's transformers backend (pulls torchvision — match your torch build):
pip install "scientificpub2md[qwen] @ git+https://github.com/jimnoneill/scientificpub2md"
# Fast batched serving (advanced):
pip install "scientificpub2md[vllm] @ git+https://github.com/jimnoneill/scientificpub2md"
The
qwen3vlengine's in-process backend needstorchvision— installtorchandtorchvisionfrom the same index (e.g. both from the CUDA index) or they won't load.lightonocrand the vLLM backend need no torchvision.
| Package | Why |
|---|---|
PyMuPDF |
render PDF pages → images |
Pillow |
image handling |
transformers (≥4.57) |
runs the models (LightOnOCR needs a recent build; older falls back to remote code) |
torch (≥2.4) |
model runtime — install the CUDA build for NVIDIA GPUs |
accelerate |
device placement |
requests |
client for the optional vLLM backend |
torchvision (extra [qwen]) |
required by the Qwen3-VL processor |
vllm (extra [vllm]) |
fast batched GPU serving |
Fast batched throughput (optional)
For many papers, serve a model once and let vLLM batch the in-flight page requests:
./serve_vllm.sh # Qwen3-VL-8B on :8000
SCIPUB2MD_VLM_ID=lightonai/LightOnOCR-2-1B ./serve_vllm.sh # or LightOnOCR
scientificpub2md ./pdfs/ -o ./md/ --backend vllm --workers 16 # qwen3vl
scientificpub2md ./pdfs/ -o ./md/ --backend vllm --engine lightonocr # lightonocr
The server can be remote — set SCIPUB2MD_VLLM_URL=http://host:8000 (or pass --vllm-url).
How it works
- Render — each PDF page → a PNG with PyMuPDF (170 DPI qwen3vl / 200 DPI lightonocr).
- Transcribe — each page image → text with the chosen model, greedy-decoded (deterministic).
- Assemble — pages concatenated in order; nothing truncated.
- Format — restructured into
# / ## / ###Markdown, or flattened to simple##headers.
When something goes wrong
| Symptom | Likely cause / fix |
|---|---|
device='cuda' requested but no CUDA GPU |
No GPU visible to torch — use --device cpu/mps, or install a CUDA torch build |
| Out-of-memory loading qwen3vl | The 8B model needs ~16 GB — use --engine lightonocr or a smaller --device |
qwen3vl … needs torchvision |
pip install 'scientificpub2md[qwen]' (match torch/torchvision builds), or use --engine lightonocr |
operator torchvision::nms does not exist |
torch/torchvision mismatch — reinstall both from the same index |
| Small text / subscripts garbled | Raise --dpi (e.g. --dpi 220) |
Connection refused on --backend vllm |
No server — run ./serve_vllm.sh, or set --vllm-url |
| Very slow on CPU | Expected for the 8B model — use --engine lightonocr or a GPU |
Notes & limitations
- OCR quality tracks the model. Dense sub/superscripts, exotic glyphs, and complex tables can have errors. Raise
--dpiif small text is missed. qwen3vldrops back matter and page furniture by default (--keep-backmatterto retain).lightonocris a faithful transcriber and keeps everything (not prompt-steerable).- Tables/figures under qwen3vl come out as plain text; lightonocr reconstructs Markdown tables and LaTeX.
- Determinism assumes a fixed model + prompt + DPI.
Support
If this saved you from wrangling GROBID or hand-cleaning PDFs, contributions toward continued maintenance are welcome.
License
MIT — see LICENSE.
Born out of the PubVerse extraction pipeline, where this page-by-page VLM method replaced GROBID for building clean full-text scientific corpora.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file scientificpub2md-0.2.0.tar.gz.
File metadata
- Download URL: scientificpub2md-0.2.0.tar.gz
- Upload date:
- Size: 30.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
beb9b414cf18a051a150c8dbeb4dd1cf6e9b2d511af732cbbb09579790cff60e
|
|
| MD5 |
1d44cdc43292ca5acad2f3cf64fb584c
|
|
| BLAKE2b-256 |
36aa49d4531294f20beefe0cf4dab8acfe7070d7e90f0445276e65c3ea675340
|
Provenance
The following attestation bundles were made for scientificpub2md-0.2.0.tar.gz:
Publisher:
publish.yml on jimnoneill/scientificpub2md
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
scientificpub2md-0.2.0.tar.gz -
Subject digest:
beb9b414cf18a051a150c8dbeb4dd1cf6e9b2d511af732cbbb09579790cff60e - Sigstore transparency entry: 1960140668
- Sigstore integration time:
-
Permalink:
jimnoneill/scientificpub2md@c9957473b08cc2e6ad66966930d705384981ddc0 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/jimnoneill
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c9957473b08cc2e6ad66966930d705384981ddc0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file scientificpub2md-0.2.0-py3-none-any.whl.
File metadata
- Download URL: scientificpub2md-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e14a9d3aaf5b80a67cbb03434cbeae9ab73d6b59df48021fbea2a3fce1c925d
|
|
| MD5 |
6e183763c962ddde29ea840e9876e879
|
|
| BLAKE2b-256 |
0fab1be2a73604a9cf41081ce37bac1b00faf9954bb6ed0c5f60609811b0487c
|
Provenance
The following attestation bundles were made for scientificpub2md-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on jimnoneill/scientificpub2md
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
scientificpub2md-0.2.0-py3-none-any.whl -
Subject digest:
3e14a9d3aaf5b80a67cbb03434cbeae9ab73d6b59df48021fbea2a3fce1c925d - Sigstore transparency entry: 1960140839
- Sigstore integration time:
-
Permalink:
jimnoneill/scientificpub2md@c9957473b08cc2e6ad66966930d705384981ddc0 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/jimnoneill
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c9957473b08cc2e6ad66966930d705384981ddc0 -
Trigger Event:
release
-
Statement type: