Paper Anchor: an open-source paper reader that pins every answer to the page
A self-hosted two-pane reader for papers: every claim the LLM makes is a clickable citation that jumps back to the highlighted source on the PDF. MIT, bring your own key, embeddings can stay entirely local.
The problem is not accuracy, it is that you cannot check
Reading a paper with an LLM usually goes like this: paste it in, get a summary that reads perfectly reasonable, then start to doubt it. To confirm that a particular sentence is really in the paper, you have to go back and keyword-search the PDF — which is the exact work you were trying to skip.
So the summary has negative value. It costs you an extra pass, and on the days you have no energy for that pass, it hands you confidence you have not earned. An answer you cannot trace back to the source is a liability, not a feature.
Paper Anchor is that sentence, built.
What it is
PDF on the left, conversation on the right, wired together:
- Anchored citations — every claim in an answer carries a
[C12]chip. Click it and the PDF jumps to the page and highlights the source blocks. Bbox-level, not just a page number. - Select-to-ask — select any passage in the PDF and a floating menu offers explain / translate / challenge / ask. The selected chunk and its neighbours are force-fed into retrieval, so the model cannot wander off into another section.
- Auto digest — on upload you get a structured overview: research question, method, findings, contributions, limitations. Every point clicks back to the source.
- It says when the paper is silent — ask about something the paper does not cover and you get "not stated in this paper" plus the nearest passage, not an invention.
Why the anchor has to be bbox-level
Because a page number is not an answer. On a dense two-column page, "see page 7" and "go find it yourself" are the same instruction.
Every span PyMuPDF returns carries coordinates. Those coordinates travel all the way into the chunk metadata and into the vector store, so a retrieval hit can be turned straight back into a highlight rectangle on screen. Nothing along that path abstracts them away — which is also why this project does not use LangChain. Most retrieval frameworks draw their abstraction boundary at the text layer and treat layout as noise. For most RAG applications that trade is correct; here it throws away the entire product.
A regression eval guards it: backend/scripts/eval_citations.py, 3 papers × 5 questions, checking that every answer carries structured citations, valid page numbers, and anchors that actually highlight. Currently 15/15 on NIM deepseek-v4-flash, and 15/15 on a local Ollama qwen3.8:27b with bge-m3.
No vendor lock-in, and it can stay entirely offline
Chat is a single code path: POST {LLM_BASE_URL}/chat/completions. OpenAI, NVIDIA NIM, OpenRouter, Together, Groq, or a local vLLM / Ollama / LM Studio server are all two env vars and a model name away.
Embeddings go one step further. Leave EMBED_API_KEY empty and the built-in BAAI/bge-m3 runs in-process through the ONNX runtime that fastembed ships (~2.2GB downloaded on first use). Pair that with a local chat endpoint and nothing leaves the machine at all.
Running it
git clone https://github.com/Grayidea-bit/paper-anchor && cd paper-anchor
cp .env.example .env # fill LLM_API_KEY; EMBED_API_KEY can stay empty
docker compose up -d # web :5173 / api :8000
Open http://localhost:5173 and upload a PDF.
Who it is not for, yet
Better said plainly:
- No authentication. This is built for one person on a machine they trust. Compose binds the ports to
127.0.0.1and does not publish the database port to the host at all. Putting it on a LAN or the internet means adding your own authenticating reverse proxy in front. - Single worker only. The backup and restore mutexes and the settings cache are per-process state; extra workers break those guarantees quietly.
- No scanned PDFs. No text layer means no coordinates, and there is no OCR yet — the upload fails with a clear message instead of half-working.
- First token can take 20–40s. The default model is a reasoning model and its thinking phase counts. Switch to a non-reasoning model if latency matters more than depth.
Open source
MIT, at github.com/Grayidea-bit/paper-anchor. The README exists in English and 繁體中文; architecture notes and the milestone log live in docs/.
Issues and PRs are welcome. What I would most like to hear is where reading a paper actually stalls for you — so far this tool has only solved the one place it stalled for me.