Skip to content

Repository files navigation

ExplainMyPDF

Version française

Read a scientific paper with an LLM beside it: the PDF on the left, the chat on the right, and a panel where the model writes runnable Python to animate the paper's mathematics.

Highlight a passage or frame a figure, and ask for an explanation. Every answer cites its pages; clicking a citation jumps to the exact spot and highlights it.

Everything runs locally against LM Studio. Nothing leaves your machine unless you deliberately configure a cloud provider.


Getting started

Double-click install.bat, once. It looks for Python 3.11 or newer — installing it through winget if it finds none — creates an isolated environment in .venv, installs the dependencies, checks that the application actually loads, and puts a shortcut on your Desktop. Allow a few minutes and roughly 300 MB the first time.

Everything stays inside the project folder: uninstalling means deleting it.

Two things remain, deliberately outside the installer's scope:

  1. LM Studiohttps://lmstudio.ai
  2. A model that reads text and images, for example google/gemma-4-12b-qat, loaded and then served from the Developer tab (Start Server).

After that, day to day, double-click ExplainMyPDF.bat or the Desktop shortcut: it starts the server, opens the browser at the right address, and warns you if LM Studio is not running.

Another port if you need one: ExplainMyPDF.bat 8123.

Manual installation, if you prefer
conda create -n explainmypdf python=3.11 -y
conda activate explainmypdf; pip install -r requirements.txt

ExplainMyPDF.bat accepts either: it uses the project's .venv when one exists, and falls back to the explainmypdf conda environment otherwise.

The command-line equivalent is .\run.ps1, then open http://127.0.0.1:8000. During development, .\run.ps1 -Reload restarts the server on every change to the Python code.

Stopping

The safest way is the red cross at the top left. It asks for confirmation, shuts the server down cleanly, and shows a screen saying so.

Closing the .bat window works too, but not every time: the server can outlive it. And since a relaunched .bat then finds a healthy server and simply reopens the tab, you believe you have restarted while still running the old code — which makes a backend change perfectly invisible. As a last resort:

Get-NetTCPConnection -LocalPort 8000 -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }

Language

The dropdown in the top bar switches the interface and the language of the answers — French or English. The choice is remembered; on first launch it follows the browser's.

There are two languages in this application, and conflating them is the trap:

  • The reader's, which that dropdown drives. It travels with every question and decides the wording of every instruction, and therefore the language of the answer. All of its text lives in two files: backend/app/context/phrasing.py for what the model is told, frontend/js/i18n.js for what the reader sees. Adding a language means one entry in each.
  • The paper's, which follows no setting at all. The extraction heuristics — caption patterns, "where …" clauses, defining verbs, bibliography detection — assume English, because scientific papers are in English. A German reader of an English paper is served correctly by English-tuned extraction; making it follow the dropdown would break the common case to serve one that does not exist.

One practical consequence: the French → English hint table in retrieval.py exists only because the reader and the paper disagree. An English reader of an English paper shares its vocabulary and needs none of it. A third language would have no table and would lean on the embeddings, which are multilingual by construction — and the status panel warns you when no embedding model is loaded, because lexical-only search across two languages does not work.

Connecting a model

The application talks to LM Studio by default, on http://localhost:1234. There is no model name to configure: it asks the server what is loaded and uses that. Switching models in LM Studio is enough — no file to edit, no restart.

For another provider, copy .env.example to .env:

EMP_PROVIDER What it needs
lmstudio LM Studio, server started from the Developer tab (default)
openai EMP_OPENAI_API_KEY
anthropic EMP_ANTHROPIC_API_KEY

The selector at the top right switches without restarting. Unreachable backends stay visible, greyed out, with the reason — an empty menu would look like a bug in the application rather than a server that was never started.

Connecting LM Studio, in practice

  1. Download a model in LM Studio.
  2. Developer tab → Start Server (port 1234 by default). Without this step the application sees nothing: LM Studio only exposes its API once the server runs.
  3. Load the model, or leave it to load on demand.
  4. In ExplainMyPDF, pick lmstudio in the model selector.

To check the state at any point:

powershell -ExecutionPolicy Bypass -File .\scripts\watch-lmstudio.ps1

And to measure what the model is actually worth on your paper:

.\.venv\Scripts\python.exe scripts\try_model.py --provider lmstudio

That bench replays a series of questions and reports, for each: time to first token, throughput, the size of the context sent, and above all how many citations actually anchor in the PDF. That is the measure that matters — a citation that does not anchor is an invented one.

Keeping a cloud provider configured has a concrete use: when a local answer is poor, replaying the same question on a large model tells you whether the fault lies with the model or with the context assembled for it.

Load the model with a single inference slot

This is the setting that changes speed, by a wide margin:

lms load google/gemma-4-12b-qat --context-length 8192 --gpu max --parallel 1

Measured on one machine, same question, same code — only --parallel changes:

--parallel Time to first token
4 (default) 22 s, 22 s, 32 s
1 6.1 s, 5.6 s, 6.6 s

The reason: llama.cpp splits the context window across slots. With 8192 tokens and 4 slots, each gets only 2048 — less than ExplainMyPDF's prompt, which must then be recomputed in full for every question. One slot is enough for one reader.

(The prompt structure was reorganised — stable prefix first, variable part after — on the theory that it would help. The measurement is unambiguous: the gain comes from the slot count, not from that.)

Reasoning models

Gemma 4 thinks before answering, and its working arrives on a separate channel (reasoning_content). ExplainMyPDF shows it in a collapsible block instead of mixing it into the answer — citations and code are extracted from the answer alone, never from the draft.

Thinking is off by default, via the Thinking button in the top bar. Measured on gemma-4-12b-qat, RTX 4070 Laptop 8 GB:

Question With thinking Without
Explain the DTW equation 220 s, 7/7 citations anchored 75 s, 5/5
Best AUC using PLI 153 s, 1/1 27 s, 3/3

Quality does not drop; the length of the reasoning, however, varies wildly — between 1700 and 3500 tokens across runs of the same question, enough to exhaust the budget and cut the answer off mid-code. When that happens the application says so explicitly, rather than letting truncated code pass for broken code.

Turn thinking on for a genuinely hard question, not by default.

Choosing a model size

On 8 GB of VRAM the comfortable range is 7–12 B quantised to 4 bits. Figure analysis needs a multimodal model; without one, framed regions are useless. A single multimodal model covers both jobs.

What the application does

  • Viewer — server-side rendering with PyMuPDF, and a selectable text layer rebuilt from word boxes.
  • Layout-aware extraction — columns found by their gutters, correct reading order, end-of-line hyphenation rejoined, printed page numbers recovered (the sample paper starts at page 741, behind a repository cover sheet).
  • Clickable objects — figures, tables and equations detected and matched to their captions. One click; no need to frame anything by hand.
  • Selection — highlight text, or hold Alt and drag to frame a region that travels to the model as an image.
  • Anchored citations[p.744 "exact words"] becomes a clickable chip. An invented citation fails to anchor, and that shows.
  • Code panel — Python from the answer lands in an editor, runs, and renders figures and animations. If it crashes, a button sends the traceback back to the model to fix. Animations are capped at 150 frames: a model asked to animate a matrix will happily write one frame per cell, which never finishes.
  • Explanation level — general public / student / researcher.
  • Outline — extracted table of contents and abstract, clickable.
  • Notation — a glossary of the symbols and acronyms the paper defines, each linked to where it defines them. See below.
  • Inspectable context — under every answer, the exact prompt the model received. This is what makes a bad answer diagnosable.
  • Persistent session — the conversation survives a reload and exports to Markdown.
  • Status — a discreet dot in the top bar opens a panel saying which model will answer, whether it reads images, the loaded context size, the search mode and disk usage. This is where silent failures become visible.

The glossary

Two sources, told apart on screen:

  • Extracted from the text (default) — acronyms introduced properly, as in "Phase-Lag Index (PLI)", and symbols defined in the where clause that follows an equation: |.|, ∆Φ(tk), i, j. These are quotations.
  • Completed by the model (Complete with the model, optional) — the acronyms the paper uses without ever spelling them out, the ones a reader cannot look up. Shown in italics, because they are inferences, not quotations.

The distinction is not cosmetic: an extracted definition can be checked against the PDF, an inferred one needs your judgement.

Structure

backend/app/
  pdf/layout.py      columns, lines, equations, tables, watermark detection
  pdf/document.py    parsed document, page rendering, crops
  llm/               providers: LM Studio / OpenAI, Anthropic
  context/           BM25 retrieval, prompt assembly, citations, phrasing
  api/               documents, chat (SSE), code execution
frontend/            HTML/CSS/JS, no build step
frontend/vendor/     KaTeX — the only vendored dependency
frontend/tests/      browser bench for the Markdown renderer
tests/               regression tests on extraction and citations

One coordinate system runs through the whole stack: PDF points, exactly as PyMuPDF reports them. The frontend converts only at display time, multiplying by the zoom. A rectangle can therefore travel from a mouse drag to a crop request to a citation highlight without ever being reinterpreted.

Mathematical notation

Models write formulas in LaTeX. They are typeset by KaTeX, vendored in frontend/vendor/katex/: the published release (Apache 2.0), trimmed to the woff2 fonts, 550 KB. Nothing to install and no CDN — the app stays usable offline. To update it, replace katex.min.js, katex.min.css and fonts/*.woff2 with those from a newer release.

Formulas are lifted out of the text before HTML escaping, exactly like citations and for the same reason: LaTeX is full of <, > and & that KaTeX must receive verbatim. That also puts them out of reach of the Markdown pass, which would otherwise read the stars in a * b * c as italics.

The $…$ delimiter needs care, because French writes amounts as "0.006 $" with the sign after the number. Three guards separate the two cases: no whitespace just inside either delimiter, no newline in the formula, and no digit right after the closing $. Those cases are locked down in the browser bench.

Running code: what is guaranteed and what is not

The script runs in a separate process, in an empty directory, with a timeout, and its output is captured. That is enough to contain an infinite loop or a careless file write.

It is not a security boundary. The code runs with your privileges. The editor above the Run button is not decorative: read what the model wrote before you run it.

Retrieval

Two methods run in parallel and their rankings are fused:

  • BM25 (lexical) — unbeatable on the acronyms and proper nouns a methods section is made of: "PLI", "Sakoe-Chiba", "OFR".
  • Embeddings (semantic) — indispensable across languages: you write in French, the paper is in English. "how are the probability scores obtained?" finds nothing lexically.

Embeddings switch on by themselves if LM Studio exposes an embedding model (nomic-embed-text ships with LM Studio by default). Otherwise search stays lexical and says so under every answer. The index builds in the background on the first question and is cached per document, so it is free afterwards.

nomic-embed-text-v1.5 is mostly anglophone: it crosses into French but by thin margins. A multilingual model (bge-m3, multilingual-e5-large, nomic-embed-text-v2-moe) does better, and is picked up automatically if you load one instead.

Three things are kept out of retrieval, because they match everything and answer nothing: the repository cover page, the front matter (authors, affiliations, keywords) and the bibliography.

Validated on

scripts/check_layouts.py runs extraction over a batch of PDFs and reports health signals (columns, reading order, outline, objects found):

.\.venv\Scripts\python.exe scripts\check_layouts.py paper1.pdf paper2.pdf

Tested on five documents with different layouts: two columns with a cover sheet and offset pagination (BIOSIGNALS), two columns with clean numbering (an NGO report), single column (Scientific Reports), a 27-page MDPI review, and a 167-page thesis manuscript — the last analysed in 16 s, with 69 figures, 109 equations and 32 tables detected.

Known limitations

  • MDPI review: no objects detected — its figures and tables are drawn without rules or images — and paragraphs stay long. Not crippling in practice: framing a region by hand sends the image and the exact words underneath it, so the numbers remain reliable and quotable. What detection adds on top is the clickable target and the table's presence in retrieval; without it, you have to know where to look.
  • Table 6 of the sample paper is not detected: its rules do not form the expected pattern. The other six are.
  • Seven tables with near-identical vocabulary cannot be told apart by retrieval alone. Naming the object ("in Table 2…") triggers a direct lookup, and a retrieved passage that refers to a table brings it along.
  • Scanned PDFs — no OCR. A PDF with no text layer yields nothing.
  • The layout heuristics are calibrated on a two-column paper. They degrade gracefully on a single column, but an exotic format will need adjustment.

Tests

.\.venv\Scripts\python.exe -m pytest tests -q

The repository includes 131697.pdf, the paper every heuristic was calibrated on, because without it the suite can verify almost nothing: conftest.py looks for it at the root and cleanly skips the tests that depend on it when absent — 21 of 74 survive. With it, the repository verifies itself from the moment it is cloned.

Most tests encode a bug that actually happened. Extraction heuristics fail silently — a deleted equation or two interleaved columns still produce plausible text, whose effect only surfaces three layers later as a strange answer. Hence the locking down.

The Markdown renderer has its own bench, in the browser since there is no Node: open /static/tests/markdown.html while the server is running. Each case is rendered with the real renderer in the real CSS, and then the resulting DOM is inspected — counting elements rather than comparing strings is deliberate: every display bug in this project survived assertions on HTML and only appeared against a real layout.

Licence

GNU AGPL-3.0 — full text in LICENSE.

This is not an arbitrary choice. PDF extraction rests entirely on PyMuPDF, distributed under a dual AGPL-3.0 or Artifex commercial licence. Any redistribution of ExplainMyPDF — a binary as much as a copy of the folder — therefore places the whole under the AGPL, and requires offering the corresponding source to recipients. Publishing the repository satisfies that obligation by construction.

Using the software for yourself carries no obligation: they attach to distribution only.

The other dependencies are permissive (BSD, MIT, PSF), as is KaTeX (Apache-2.0), vendored in frontend/vendor/katex/.

About

Read a scientific paper alongside a local LLM: PDF viewer, chat with anchored citations, and a panel where the model writes and runs Python to animate the paper's mathematics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages