Bornite
Document extraction, refined.Rust
Complex filesContent you can use
Open source · MIT / Apache-2.0

Every document,
read properly.

Bornite turns PDFs (digital or scanned), Office files, email, archives, images, CAD and GIS files, and recordings into clean text, Markdown, HTML or a structured JSON tree. OCR, layout, table and formula models ship inside one binary and run only on the pages that need them. No JVM, no Python, no GPU required.

One binary. One API.Start extracting
# start the server
$ docker run -d -p 8080:8080 \
    ghcr.io/jbreed/bornite

# send any file, get Markdown back
$ curl --data-binary @report.pdf \
    "localhost:8080/extract?format=markdown"

# photos of pages, slides, mail, zip files: same call
$ curl --data-binary @scan.png \
    "localhost:8080/extract?format=text"
See it work

A page goes in. Structure comes out.

Below is page 2 of a test PDF in the repository and what Bornite returns for it in each output format.

Page 2 of the test PDF: a running header, a paragraph, a figure with its caption, the heading 2. Pricing, the caption Table 1: Price schedule, a three-row price table, the heading 2.1 Payment, two paragraphs, and Page 2 at the foot.
Input: tests/fixtures/report.pdf, page 2, rendered
Figure 1:
The contractor shall document the lighting before each …

<!-- image -->

_Figure 1: Site map of the facility_

# 2. Pricing

_Table 1: Price schedule_

| CLIN | Description | Price |
| --- | --- | --- |
| 0001 | Janitorial, daily | $1,200 |
| 0002 | Window cleaning | $300 |

## 2.1 Payment

The contractor shall replace the loading dock each quarter …

The contractor shall repair the fire alarm on request and …

With no model, a figure is kept as a placeholder with its caption. Every tab is the output of Bornite 0.2.1 for this page, fetched again on each site build. Long paragraphs and some HTML metadata are shortened (…), and the JSON leaves out each block's source and the page's quality figures. The figure description replays one recorded answer from a Qwen vision model, so it reads the same each time. format=ndjson streams the JSON page by page.

Headings keep their levelSections become # and ##, and each block in the JSON points to the heading that holds it.
Tables stay tablesRows and cells, not a run of words, with the caption linked to the table it names.
Page furniture kept apartRunning headers, footers and page numbers stay out of the text and Markdown, and are kept in the JSON as page_footer blocks.
Every page says how it was readEach page records its route (text layer, OCR or vision) and each file its time by stage.
Accuracy

olmOCR-Bench, CPU only

BorniteOther toolsWhiskers: 95% confidence interval where measured

By kind of page

Pass rate, %. Darker green is closer to 100; bold is the best score in each column.

Speed

Models only where the page needs them

Bornite reads a born-digital page from its text layer and sends only scanned pages, tables and math to its models.

Table rows kept, eleven file types

Features

What each tool does out of the box

Beyond the benchmarks

What a score doesn't show

Benchmarks measure text on PDF pages. Bornite also describes images, transcribes recordings, streams long documents and survives hostile files. Everything here is built, on by default unless marked optional, and documented in the repository.

Every page takes the cheapest path that reads it right

Each page is routed on its own, so one scanned page in a long report doesn't slow down the other ninety-nine.

  • A page with a good text layer is read from it in milliseconds. Garbled text from broken fonts is caught by a quality check and sent to OCR.
  • Scanned pages go to OCR (PP-OCRv6). Pages with tables, formulas or columns also go through the layout, table and formula models.
  • Each page records the route it took, so you can see why it cost what it did.

Figures described by the vision model you choose optional

Charts, diagrams and photos get a written description, and pages OCR read poorly get a second read. It stays off until you name a model, and files go only to the model you name.

  • Works with hosted models (OpenAI, Anthropic, Gemini) or your own server (Ollama, vLLM, llama.cpp, LM Studio, or anything OpenAI-compatible), for example a Qwen-VL model on your own GPU.
  • The model is asked for every label, callout and dimension exactly as written, and to write [unreadable] rather than guess.
  • A slow model never holds a file up. Each call has a time limit and each file a budget. A page without an answer keeps its text and is marked vision_pending, so it can be sent again later.

Recordings become timestamped text, with speakers

Audio and video files are transcribed into one paragraph per stretch of speech, with its time and speaker, on the CPU or a GPU.

  • Silero VAD finds the speech, so music and silence never reach the recogniser. NVIDIA Parakeet TDT transcribes 25 European languages, with punctuation and capitals.
  • Speaker labels come from pyannote segmentation and TitaNet voiceprints, matched across the whole recording.
  • Decoding is pure Rust and streams, so a three-hour recording uses no more memory than a one-minute one.

Long documents stream, and pick up where they stopped

A thousand-page file doesn't have to finish to be useful, and a time limit never throws away pages already read.

  • format=ndjson sends each page as soon as it is read, so a caller keeps every finished page even if the connection drops.
  • A document that runs out of time returns the pages it read, plus resume_pages to ask for the rest with pages=4-.
  • timings_ms breaks each file's time down by stage (queue, text layer, OCR, page models, vision).

A bad file fails alone

Every file is parsed in a separate, locked-down worker process. A file that crashes a parser, hangs, or tries to eat all the memory fails only itself.

  • Each worker has a deadline and a memory cap, writes no core dumps, and on Linux is locked down with Landlock. Unless a vision model is set, it can't open a network connection.
  • A file that ran out of memory is retried once with twice the memory, and one that crashed is retried with less work.
  • Limits stop zip bombs, and every parser family is fuzzed continuously.

Every machine gives the same answer

When machines pool their CPUs and GPUs, a result shouldn't depend on which machine read the page.

  • The models ship with a digest of how they were made (MODEL_SET) and checksums for every file. A node won't start on another set, and the server refuses its join.
  • A node must also run the server's exact build (the same commit, not just the same version number), or its join is refused.
  • Nodes dial out over TLS with client certificates, lease jobs with heartbeats, and hand work back if they go quiet.
Scaling

Start on one machine. Add CPUs and GPUs from anywhere on your network.

Clients always talk to one server. Any other machine can run bornite node to lend its cores and GPUs: it pulls jobs from the server, runs them in its own sandboxed workers and sends the documents back. Long PDFs are split into page chunks that spread over every machine.

Step 1 · one machine

Run the server

docker run -p 8080:8080 \
  ghcr.io/jbreed/bornite

Up to one sandboxed worker per core, as many as fit in memory at 2 GiB each. A crash, hang or memory blow-up in one file never takes the service down.

Step 2 · add a GPU

Use the CUDA image

docker run --gpus all -p 8080:8080 \
  ghcr.io/jbreed/bornite:latest-cuda

Workers keep their CPU lanes and a GPU lane takes the next model run when it is free, so a GPU never makes it slower.

Step 3 · pool machines

Enroll a node

# on the server: a one-time code
bornite pool enroll worker-1 ...
# on the machine
bornite node enroll --code ...
bornite node   # Ctrl+C to leave

The server makes a one-time code for each machine (after bornite pool init and serve --pool-listen; see the pooling guide). While bornite node runs, the machine helps; stop it and it leaves cleanly.

No job is lostJobs are leased with a 2 s heartbeat. A node that goes quiet for 10 s hands its jobs back to the front of the queue.
Optional spot checksPooling machines you don't control? Turn on spot checks and the server re-runs a share of each node's results, quarantining a node that keeps disagreeing.
Least accessA node fetches only the files leased to it, and revoking it cuts it off at its next call.
Same answers everywhereEvery node must run the server's exact build and model set, so output does not depend on which machine read the page.
Formats

One call for every file type

Bornite detects the type from the bytes, so you send any file to the same endpoint. Files inside files are read too, each with its own path such as /bundle.zip/docs/report.pdf, including objects pasted into Office documents.

PDFText layer, scanned pages (OCR), tables, formulas, metadata, XMP, outline, form fields, annotations, attachments, encryption; broken cross-reference tables rebuilt, and a cut-off scan keeps the page images that arrived whole
Word, Excel, PowerPointDOCX, DOC, XLSX, XLSM, XLSB, XLS, PPTX, PPT. Decks: slides in order with titles, tables, chart data, speaker notes, reviewer comments with authors and replies, hidden slides and slide count, and slide pictures read by OCR, for PPT as for PPTX (PPT charts come out as their embedded workbook or chart)
OpenDocumentODT, ODS, ODP, and ODF charts embedded in other files
AccessMDB (Access 97 to 2003) and ACCDB (2007 and later): every user table, with its column names and rows, including long memo text. Encrypted databases are reported as encrypted
OneNoteSections (.one) saved by OneNote 2010 and later: page titles, paragraphs, picture alt text and authors; pictures go to OCR and attached files are read in turn. Earlier typing snapshots OneNote keeps are dropped. The OneDrive download package is reported as unsupported
Apple iWorkPages, Numbers and Keynote (2013 and later): body text, headers, footnotes, text boxes, speaker notes, comments and table cell text. Password-protected documents are reported as encrypted
Other word processors and publishingWordPerfect 5 and 6 or later (.wpd, .wp5, .wp6: text, tables, document summary), Microsoft Works 6-9 (.wps), Publisher (.pub: story text), with summary properties
Web and textHTML (main-content detection), XML, SVG (drawn text, title, description), plain text (encoding detection), CSV, TSV, RTF
EmailEML, mbox, Outlook MSG, Outlook data files (PST/OST: every folder's messages with attachments), TNEF (winmail.dat), S/MIME signed messages (.p7m, .p7s, with signers), MHTML saved pages, with attachments
ArchivesZIP, 7z, TAR, CAB, JAR, EPUB, and GZIP, bzip2, xz or zstd streams (including .tar.*), with passwords tried on encrypted ZIP and 7z. RAR is reported, not read
Fixed layout and diagramsXPS and OpenXPS pages (text runs in reading order, page sizes), Visio .vsdx (shape text and shape data per page; legacy .vsd is reported, not read yet)
CADDXF text: notes, title blocks, attributes, dimension overrides, table cells, layer names. MicroStation V7 DGN text elements and tag values (V8 DGN: summary properties). STEP header (part, author, organization, CAD system) and products. Revit file info and family descriptions. Other compound-file CAD (SolidWorks, Inventor): summary properties. DWG text (R13 through 2018 except 2007-2009): notes, title blocks, attributes, dimension overrides, layer names, summary properties
GISShapefile attribute tables (DBF), Esri file geodatabase tables (.gdbtable), with geometry summed up as type and extent; index parts (.sbn, .atx, .gdbtablx, ...) are recognised as having no text
ImagesPNG, JPEG, JPEG 2000, TIFF, BMP, GIF, WebP and HEIC/HEIF (iPhone and Android photos) (OCR, EXIF metadata; phone photos turned upright by their EXIF orientation); camera raws (DNG, CR2, CR3, NEF, ARW, ORF, RW2, RAF) through the camera's embedded full-size preview; AVIF EXIF only for now, with needs_decoder=av1
Audio and videoMP4, MOV, M4A, MKV, MP3, WAV, FLAC, Ogg Vorbis, AIFF, CAF with AAC, MP3, FLAC, Vorbis, ALAC or PCM sound (transcribed with timestamps). Opus (WebM) and WMA are not decoded yet
Get started

A container or a single binary. Nothing else to install.

Each release ships as container images and as Linux tarballs of the binary with its models, built from the same commit. Pin a version such as X.Y.Z in production.

Container · CPU

amd64 and arm64

docker run -d -p 8080:8080 \
  ghcr.io/jbreed/bornite

Docker picks the right architecture. On macOS and Windows, run the container; native builds aren't published yet.

Container · NVIDIA GPU

amd64, CUDA

docker run -d --gpus all -p 8080:8080 \
  ghcr.io/jbreed/bornite:latest-cuda

Driver 580 or later. No GPU is needed for the best results; with one, models run faster, and the image falls back to the CPU without one.

Linux binary

x64, arm64, x64 CUDA

tar -I zstd -xf \
  bornite-vX.Y.Z-linux-x64.tar.zst
./bornite serve --port 8080

glibc 2.36 or later. Every download is covered by SHA256SUMS, with an SPDX SBOM of the crates in the binary.

HTTP API

Send the file as the request body. Any language that can make an HTTP request can use it.

CallReturns
POST /extractThe JSON document tree, or format=text, markdown, html, or ndjson page by page
POST /metadataMetadata of the file and every file inside it
POST /detectThe media type
GET /health, /metricsHealth and Prometheus metrics

Options: pages=3-10,40-, ocr=auto|off|force, password, vision, timeout. Set BORNITE_TOKEN to require a bearer token. Errors are JSON with a kind.

Command line

The same binary, with --help on every command.

bornite extract --format markdown report.pdf
bornite detect file.bin
bornite serve --host 0.0.0.0 --port 8080
bornite jsonl < requests.jsonl   # batch protocol
bornite node                     # lend this machine to a pool
bornite health