Citadel is a research-paper review tool. Upload a PDF and the backend extracts its references, scores each citation's relevance with an LLM, builds citation/author/venue graphs, flags anomalies (low-relevance citations, suspicious author/venue communities, statcheck mismatches), and the frontend visualizes the results alongside the PDF.
The Python package is CITation; the FastAPI app is CITation.main:app.
- Backend — FastAPI app in backend/src/CITation/.
Exposes a WebSocket (
/ws/process_pdf) that runs the full pipeline on an uploaded PDF, plus REST endpoints for batch abstract extraction. Pipeline steps live in data/ (extraction, enrichment, graph building) and anomalies/ (relevance, author/venue suspicion, statcheck). - Frontend — React + Ant Design app in src/. PDF viewer in src/components/pdfContainer/, graph and anomaly visualizations in src/components/visContainer/.
- GROBID (optional but recommended) — used for reference extraction, citation-mention location, formula coordinate extraction (for statcheck highlights), and abstract recovery for missing-metadata references.
- PDF → markdown cascade — when markdown is needed (GROBID missing or
unable to locate citation mentions), the backend tries
olmocr first and falls back to
pymupdf4llm. olmocr is not a hard dependency; install it separately if you want to use it.
- Python ≥ 3.11 with
uv - Node.js (for the React frontend)
OPENAI_API_KEYset in the environmentS2_API_KEYset in the environment (Semantic Scholar API key)- Docker (optional, to run GROBID locally)
Install Python and JS dependencies:
uv sync
npm install(Optional) start GROBID:
docker run --rm -p 8070:8070 lfoppiano/grobid:0.8.0Override the GROBID URL with GROBID_URL if it's not on localhost:8070.
(Optional) install olmocr for higher-fidelity PDF → markdown conversion.
It's heavy (vision model, expects a GPU), so it's not in pyproject.toml.
Add it to this project's venv if you want it:
uv add olmocrThe backend invokes it via python -m olmocr.pipeline by default; override
with OLMOCR_CMD (e.g. OLMOCR_CMD="my-olmocr-wrapper") and the per-PDF
timeout with OLMOCR_TIMEOUT (seconds, default 900). When olmocr isn't
available the backend silently falls back to pymupdf4llm.
In separate terminals:
# Backend (FastAPI, http://localhost:8000)
uv run fastapi dev backend/src/CITation/main.py
# Frontend (React, http://localhost:3000)
npm startThe frontend talks to the backend at localhost:8000 by default; override with
the BACKEND_URL env var when starting npm start. CORS is configured for
localhost:3000 only.
If you're deploying this on a web-facing server, you should have the frontend built in production mode and served by the Python API. To do this, first run
npm run build, then copy the resulting build folder into backend/src/CITation. Then run uv run fastapi run.
WebSocket /ws/process_pdf— main pipeline. The client sends paper metadata (JSON), file metadata (JSON), then PDF bytes. The server streamsinfoprogress events and a finalendevent with{citations, authors, venues, anomalous, authorGraph}.POST /api/extract_metadata— pre-fills the upload form. Accepts a PDF multipart upload, reads the first two pages with PyMuPDF, and asksgpt-4o-mini(JSON mode) for{title, authors, year}. The frontend calls this as soon as a PDF is dropped so the user reviews/edits rather than retypes manuscript metadata; fields stay editable and a warning surfaces if extraction fails.POST /api/extract_abstract— single-PDF abstract extraction via GROBID. Requires GROBID.POST /api/extract_abstracts— batch version.
Anomaly payloads now include explicit anchor metadata so the frontend can resolve each issue to the exact citation marker or statistical expression in the rendered PDF, instead of relying on page-only navigation.
- Citation anomalies expose
anchors, where each anchor represents one detected in-text marker occurrence with{page, marker_bbox, page_width, page_height, ref_label, sentence}. - Citation records returned from
WebSocket /ws/process_pdfexposecite_positions, one entry per in-text[N]marker. The PDF viewer uses these to draw marker-level overlays and associate them with issue IDs. - When GROBID coordinates are missing, the frontend falls back to the anchor sentence and marker label to resolve the highlight in the PDF text layer.
- The anomalies list and detail card now derive their page label from every
anchor of the issue: a single page renders as
Page: N, multiple pages asPages: i, j, k, …. This also fixes a bug where a paragraph that cited several references on different pages could show the wrong page for one of them —assign_scores_to_enriched_paperspreviously rebuilt each mention dict from a text-only assessment lookup, which droppedoccurrencesand let one ref's page overwrite another's. The lookup is now keyed by(ref, excerpt)and the original mention (including itsoccurrences) is preserved. - Multi-ref groups like
[22,30,32]are now parsed correctly. GROBID emits these as three sibling<ref>tags whose visible labels are"[22,","30,", and"32]". The previous_extract_numeric_labelusedre.fullmatch(r"\[?(\d+)\]?", …), which failed on the first two (trailing comma) and fell back to GROBID'starget="#b…"attribute — that mapping is off-by-one in some papers, so marker "22" was being bucketed undermentions[21]and surfaced as a stray anchor on page 2. The label parser now usesre.match(r"\[?(\d+)", …)(leading digit run), so each marker in a list binds to the visible number; author-year style labels like"Smith 2020 [5]"still skip the visible-label path because they don't start with[or a digit. - The anomalies list is sorted by page-then-category: items are ordered
by the earliest page they appear on, then within a page by category
severity (
testFailure→selfCitation→citationRing→unreferenced→lowRelevancy). Issues with no page (e.g. unreferenced bibliography entries) sink to the bottom; insertion order from the pipeline breaks ties. ref_id(which becomescite_numbershown in the UI) is recovered through a three-step fallback when the<biblStruct>has no<label>element: first the leading[N]of<note type="raw_reference">, then axml:id → PDF labelmap harvested from the fulltext endpoint's<ref type="bibr" target="#bN">[K]</ref>markers, and only finallyenumerate(..., 1). GROBID can drop a non-publication entry (e.g. a software citation like[1] 2008-2026. GROBID. https://github.com/kermitt2/grobid…) from the bibl list and also omit<label>and strip the[N]prefix from the raw reference text — that combination defeats both the label and the raw-reference fallbacks, so the fulltext label map is what keepscite_numberaligned with the source PDF numbering. The pipeline now issues the fulltext call beforeprocessReferencesand threads the map throughextract_references_with_grobid(..., label_map=…).
uv run pytestBackend tests live in backend/tests/. Mark slow tests with
@pytest.mark.slow.
The top-level *.py files (statcheck.py, synthetic.py, stackinggraph.py,
pubmedPaperExtraction.py, florMatrix.py) and backend_scripts/
are earlier standalone tools that predate the integrated FastAPI pipeline.
They are kept for reference but are not part of the running application.
See backend_scripts/README.md for the historical
script-by-script pipeline.