Python library
Use chew() to extract a paper or a batch, and Chewer to reuse a pipeline
across calls. import bibr stays light: pipeline stages and their ML
dependencies are loaded lazily.
One file
import bibr
result = bibr.chew("paper.pdf")
result.title # metadata fields pass through as attributes
result.references # list of dicts (alias for the schema's "bib" table)
result.save("paper.json")
chew() returns a Result, an attribute-based view over the export dict
with a validated v12 PaperExport model available as result.model.
Table-shaped keys (author, affiliation, funding, text, section,
url, bib, xref, figure, table, eq, metadata_match, bib_match)
come back as Records, a list subclass
with a .df convenience property for pandas:
result.references.df # bib rows as a pandas DataFrame
result.authors.df # alias for the "author" table
result.data # the raw export dict backing the Result
result.model.metadata.title # typed Pydantic model access
references, authors, and sections are friendly aliases for the
schema's bib, author, and section tables; metadata fields (title,
doi, …), source fields (file_name, sha256, input_format), and
remaining top-level keys (paper_id, extraction, …) resolve as attributes
too.
bibr.Result(data) also wraps a saved export dict, such as
json.loads(Path("paper.json").read_text()). It accepts an export from any
12.x release, including a newer one that adds fields or bumps the minor
schema_version or adds enum values. Unknown keys are kept in result.data
and in the model's model_extra. A schema_version with a different major version (11.x,
13.x) raises a validation error.
Batch
Directories and lists of paths run as a batch on a single pipeline, reusing resources according to the selected memory mode:
results = bibr.chew(["a.pdf", "b.pdf"]) # list of paths -> order-aligned list
results = bibr.chew("papers/") # sorted supported files, non-recursive
A failure in one file doesn't abort the batch — its slot holds a
ChewFailure instead of a Result, in place, so the output list always
lines up with the input:
good = [r for r in results if r.ok] # ChewFailure.ok is False
for r in results:
if isinstance(r, bibr.ChewFailure):
print(r.path, r.error, r.failed_stage)
ChewFailure.outage is True when an OCR or LLM service was down or
unreachable, or a model or server could not start: the failure most likely
says nothing about the file, which may well succeed later. If the pipeline crashes on a
chunk of files, the files it left unfinished run again one by one, and only a
file that crashes it on its own fails, with error_code="chunk_error". Files
that share a stem (a/paper.pdf, b/paper.pdf, paper.xml) get the ids
bibr batch gives them, <stem>-<sha256[:8]>, so a batch's results can be
joined and passed to bibr.write_tables().
For a list input, results keep the input order. For a directory input,
files are processed in sorted order and anything that isn't
.pdf/.docx/.xml/.html/.htm/.epub is skipped. An empty list returns []; a directory
with no supported files raises ValueError. Single-file failures
raise an exception instead of returning ChewFailure.
For a fixed return type, use chew_file() and chew_many():
result = bibr.chew_file("paper.pdf") # Result
results = bibr.chew_many(["a.pdf", "b.pdf"]) # list[Result | ChewFailure]
Their async equivalents are achew_file() and achew_many(). The same
four methods are available on Chewer. File-specific methods reject
directories; batch-specific methods take an explicit sequence of paths.
Corpus tables (Parquet)
bibr.write_tables() writes any number of results, export dicts or export
JSON files as one Parquet file per table: paper.parquet (one row per paper),
one file per record and match table (author, bib, text, bib_match, …)
and extraction_* files for the processing lists. Every row starts with
paper_id, and the column types come from the schema, so every file has the
same columns however many papers it holds. Failed slots of a batch are skipped,
and so are JSON files that are not 12.x exports (a receipt, an export of
another major version); the report lists them in skipped.
report = bibr.write_tables(bibr.chew("papers/"), "tables/")
import pandas as pd
refs = pd.read_parquet("tables/bib.parquet") # every paper's references
papers = pd.read_parquet("tables/paper.parquet") # titles, DOIs, file hashes, …
In R: arrow::read_parquet("tables/bib.parquet"). The CLI equivalent is
bibr tables results/ --out tables/, which leaves out what bibr chew -o
writes beside each export x.json (x.core.json, x.json.receipt.json,
x.json.enrichment.json, _quarantine/), and bibr batch writes the same
files to <out>/tables/ after every run. paper_id joins the tables, so it
must be unique across the corpus. It defaults to the input file's name without
its extension, so a/paper.pdf and b/paper.pdf clash: chew them in one batch
call (or with bibr batch), which gives such files <stem>-<sha256[:8]> ids,
or pass paper_id= to a single-file call.
Options
chew() (and Chewer, below) accept keyword options that mirror the bibr
chew CLI flags:
| Option | CLI equivalent | Description |
|---|---|---|
ocr |
--ocr |
OCR backend, e.g. "paddle" (default selector), "paddle-http", "paddle-vllm", or an explicit "glm-*" compatibility backend |
llm |
--llm |
LLM backend: "cloud", or a managed local server ("local", "vllm", "vllm-mlx", "rapid-mlx", "llama-cpp", "llmster") |
memory |
--memory |
Memory mode: "aggressive", "balanced", "keep_all" (default: selected from hardware) |
refs |
--refs |
Reference parser: "ner" (default), "llm" (batched LLM), "llm-chunked" (region-aligned chunk parsing), "ref-llm" (dedicated fine-tuned parser), or "off" / False to skip references |
ref_seg |
--ref-seg |
Reference segmentation strategy: "geom" (default), "region", "llm", "crf" |
no_llm |
--no-llm |
Skip metadata LLM calls, equation extraction, citation linking, and Crossref; native metadata survives. OCR still uses the selected backend |
device |
--device |
Force compute device: "cuda", "mps", "cpu" |
crossref |
--crossref / --no-crossref |
Tri-state: True runs Crossref/resolver reference enrichment for this call, False skips it, omitted/None follows CROSSREF_ENRICH (off by default) |
equations |
--no-equations (inverted) |
Enable/disable equation extraction |
pages |
--pages |
Page range to process, 1-based (e.g. "1-5") |
figure_images |
--figure-images |
Include figure images in the output, as data: URIs |
include_regions |
--regions |
Include the extraction.regions debug payload (per-region bbox/font/content) |
include_region_meta |
--region-meta |
Include extraction.text_regions: per-sentence layout features (bbox, font_size, region_type, …) keyed by text_id (opt-in v4-training metadata, distinct from extraction.regions) |
ocr_url |
--ocr-url |
URL for an external OCR server: Paddle (paddle-http) unless ocr (or, without ocr, OCR_BACKEND) names a GLM backend, then glm-http; an explicit cloud vision ocr ignores it |
ocr_model |
--ocr-model |
OCR model path or served model alias |
ocr_profile |
--ocr-profile |
"paddle" or "glm"; required when a custom model alias does not identify its family |
start_page, end_page |
--pages |
Lower-level zero-based, inclusive page indices; use these or pages, not both (both raise TypeError) |
paper_id |
--paper-id |
Paper ID override (single-file calls only) |
batch_size |
--batch-size |
Files per chunk in batch processing (batch calls only) |
consolidate |
--consolidate |
Merge accepted Crossref matches into bib before export: True / "fill" fills only missing fields, "replace" also overwrites disagreeing ones, but only from a match carrying the reference's printed DOI (a match's catch-all bib_type other fills a missing type but never replaces a printed one); False forces it off |
settings |
.env / environment |
A GlobalSettings instance copied into the pipeline at construction |
refs="off" is equivalent to bibr chew paper.pdf --refs off. It keeps
core metadata and equations while skipping reference segmentation, parsing,
bibliographic citation linking, and inline Crossref enrichment.
Backend and processing defaults also come from .env or environment
settings such as LLM_PROVIDER, REF_SEG_STRATEGY, and
CROSSREF_CONSOLIDATE. File-specific options such as paper_id and pages
are call arguments. See the Settings reference.
Use a settings snapshot to configure independent pipelines without mutating
the global Settings object:
from bibr.config import GlobalSettings
settings = GlobalSettings()
settings.crossref.enrich = False
result = bibr.chew_file("paper.pdf", settings=settings)
Chewer snapshots settings when it is constructed, even though its pipeline
is built on the first call. Later changes to the source settings do not
change an existing session.
To merge Crossref matches after the fact instead of at extraction time, call
.consolidate() on a Result — it returns a new Result and leaves the
original extracted fields untouched. This only merges matches already
present in bib_match; it does not query Crossref:
result = bibr.chew("paper.pdf")
enriched = result.consolidate() # mode="fill" by default
enriched = result.consolidate("replace")
Reference lists
Reference-manager files
result.to_csl_json(), result.to_bibtex() and result.to_ris() give the
reference list in the formats reference managers import: CSL-JSON (Zotero,
Mendeley, Pandoc and other citeproc tools), BibTeX and RIS (EndNote, Zotero,
Mendeley). bibr.write_references() writes one file from one result or many,
with results, export dicts or export JSON paths as for write_tables(); it
takes the format from the file's extension (.json, .bib, .ris) unless
format= says otherwise, and cite keys are unique across the file:
items = result.to_csl_json() # a list of dicts
bibr.write_references(result, "refs.bib")
bibr.write_references(bibr.chew("papers/"), "refs.ris")
bibr.write_references(results, "refs.txt", format="csl-json", prefer="record")
The export itself is never changed. Which values are written:
- Printed only. Each field is what the reference printed. A field it does not print (the DOI, the pages, the journal) stays empty: bibr never adds a value the paper did not print. The registry record only feeds the check note.
prefer="record"(opt-in). A matched record's values are taken over the printed ones, and the record fills what the reference does not print (a missing DOI, journal or page range). Each such entry gets a note naming the values that came from the record ("bibr: from the Crossref record, not the paper: pages, DOI"). This needs enrichment (--crossref); without it there is no record and the output is as printed.- Only a matching record. With
prefer="record", a record is used only when it agrees with the reference: bibr compares the two when writing (title, first author, year and DOI after folding case, accents and punctuation, plus the volume, issue and pages from verification). A reference whose record disagrees with it, or that has no record, is written as printed; without enrichment, every value is the printed one. - Names. The printed list is split into names (with
prefer="record", a matched record's family and given names are used instead). "Smith, J. A., & Doe, B.", "Smith JA, Doe B", "J. A. Smith and B. Doe", "John Smith, Jane Doe", Chicago's "Smith, John, and Jane Doe" and "Smith, J.; Doe, A." are read, with particles ("van der Berg") and suffixes ("Jr.") kept with their name. Two full names in a row are always two people. An organization ("World Health Organization") is one name: a CSLliteral, a braced BibTeX name, one RISAUline. A list that fits none of these styles is written as one such name rather than split wrongly. "et al." becomes "and others" in BibTeX and is left out of CSL-JSON and RIS. A rule ("——") repeats the previous reference's authors. - Pages. An abbreviated last page is written in full ("1267–89" as 1267–1289); an article number ("e0123456") is the first page only.
- Notes. When a reference and its record disagree, a note names the
fields that differ, after any note the reference printed.
check_note=Falseleaves it out. In CSL-JSON the note isnote, which Zotero keeps in the Extra field.
bib_type |
CSL-JSON | BibTeX | RIS |
|---|---|---|---|
journal_article |
article-journal |
@article |
JOUR |
book |
book |
@book |
BOOK |
book_chapter |
chapter |
@incollection |
CHAP |
conference_paper |
paper-conference |
@inproceedings |
CPAPER |
report |
report |
@techreport |
RPRT |
thesis |
thesis |
@phdthesis |
THES |
dataset |
dataset |
@misc |
DATA |
software |
software |
@misc |
COMP |
preprint |
article |
@misc |
UNPB |
other |
document |
@misc |
GEN |
The printed type is used (with prefer="record", a matched record's type
unless that type is the catch-all other), and a reference without a type that names a journal with a volume, issue or page is a
journal article. Dates, bare
lowercase DOIs, URLs, editors, edition, series, PMID, arXiv identifiers and
"in press" (CSL status, BibTeX pubstate) are written too. Cite keys are
the first author's family name and the year (Smith2020, then Smith2020a,
Smith2020b), folded to ASCII; keys that differ only in case count as the same
key, as they do in BibTeX.
The CLI equivalent is bibr references results/ -o refs.bib (--format,
--prefer record, --no-check-note; without -o it writes to standard
output). bibr serve answers GET /papers/jobs/{id}/references?format= (see the
REST API), and the MCP
servers have export_references and, over stdio, save_references (see
MCP).
Warm sessions
Calling chew() repeatedly builds and tears down a pipeline (and its
models) every time. For repeated calls over time — notebooks, queue workers
— Chewer keeps a pipeline warm across calls instead:
with bibr.Chewer(ocr="paddle") as chewer:
r1 = chewer.chew("a.pdf")
r2 = chewer.chew("b.pdf") # reuses the pipeline and retained models
Chewer takes the same options as chew(), except paper_id and
batch_size, which move to .chew() / .achew() per call. The pipeline is
built lazily on the first call and released on close() / context exit; you
can also manage it explicitly:
chewer = bibr.Chewer(ocr="paddle")
try:
r1 = chewer.chew("a.pdf")
r2 = chewer.chew("b.pdf")
finally:
chewer.close()
Use one mode per instance: sync .chew() calls drive a private event loop
that async calls don't share. Inside a running event loop, use the async
form instead:
Run one call at a time on an instance that manages local servers (local OCR,
llm="local"): await each achew() before starting the next, because bibr may
stop one managed server to free GPU memory for the next stage.
Async
Inside Jupyter or any other already-running event loop, chew() raises —
use the async twin instead:
achew() mirrors chew()'s signature and options exactly, including batch
input (await bibr.achew(["a.pdf", "b.pdf"])) and per-call paper_id /
batch_size.
OCR runtime selection and diagnostics
ocr="paddle" is the default automatic selector. It chooses one runtime at
startup. Linux x86_64 tries paddle-vllm when an NVIDIA GPU with at least
8 GB VRAM is detected, followed by glm-llama; other Linux machines go
directly to glm-llama. Apple Silicon tries paddle-rapid-mlx,
paddle-mlx-vlm, glm-rapid-mlx, then glm-llama.
Fallback is startup-only, so a ready Paddle runtime is not silently replaced
with GLM for a failed recognition call. Pass a concrete ocr="glm-*" value
when GLM is the deliberate choice.
The normal external-Paddle configuration uses the served alias and profile:
Custom ocr_model aliases must also set OCR_PROFILE (paddle or glm) so
the model prompt and normalizer are unambiguous. Paddle table output is decoded
from OTSL to HTML and formulas are normalized to their LaTeX body. To inspect
those transformations, request include_regions=True; extraction.regions
preserves the canonical content and raw_ocr_content. The export
extraction.ocr and OCR cache identity retain the selected
backend/model/profile.
The equivalent per-call configuration is:
result = bibr.chew_file(
"paper.pdf",
ocr="paddle-http",
ocr_url="http://localhost:8080/v1",
ocr_model="my-paddle-model",
ocr_profile="paddle",
)
An ocr_url follows the same rule as bibr chew --ocr-url: a bare URL
connects to a Paddle server (paddle-http), and ocr="glm" (or
"glm-http", or any "glm-*" backend) connects to a GLM server
(glm-http) instead. Without ocr, the configured OCR_BACKEND decides
the same way, so OCR_BACKEND=glm-http keeps GLM. An explicit cloud vision
backend (ocr="gemini", "openai" or "anthropic") ignores ocr_url,
logs a warning and drops the URL; set OCR_VISION_BASE_URL to change its
endpoint. A cloud vision OCR_BACKEND in .env never overrides a URL you
pass: the run connects to that server as paddle-http and sends no page
image to the cloud provider.
Escape hatch: LocalPipeline
chew() and Chewer cover single calls, batches, and warm sessions. For
full control over the pipeline — custom orchestration, holding a pipeline
across a larger application, or driving process_chunk() directly — build
LocalPipeline yourself:
import asyncio
from bibr import LocalPipeline
async def main():
pipeline = LocalPipeline(memory_mode="balanced")
try:
return await pipeline.process_file("paper.pdf")
finally:
await pipeline.aclose()
data = asyncio.run(main())
The returned dict matches the bibr v12.3 JSON schema —
chew() wraps that same dict in a Result view. See the
Architecture guide for how the pipeline stages
(validate, input, structure, extract, enrich, export) fit together.
Memory modes
The memory option (--memory on the CLI) controls how aggressively
models are loaded and unloaded to fit available GPU/RAM:
| Mode | Description | Use when |
|---|---|---|
balanced |
Keeps layout + segmenter loaded; OCR stays resident unless a local LLM needs the memory | Default when neither low-memory condition below applies |
aggressive |
Loads/unloads models between phases | Auto-selected for ≤8 GB system RAM, a CUDA GPU below 12 GB of VRAM that a managed local LLM server shares, or one of 8 GB or less |
keep_all |
Keeps models loaded for the duration of the run | Explicit opt-in when all selected models fit in memory |
Leave memory unset to get the auto-detected default; pass it explicitly
(or set PIPELINE_MEMORY_MODE) to override.