A vendor sends a 200 page catalogue as a Portable Document Format (PDF) file. You need it as rows: stock keeping unit (SKU), name, materials, image. The modern instinct is to hand the whole file to a vision model and let it read.
It works. It also bills you for all 200 pages, including the 180 that were a clean, machine-readable table you could have parsed for free, and it gives you no way to know which of the 4,000 rows it invented.
That second part is the real problem. The cost is annoying. The uniform confidence is dangerous. A model returns a well-formed row for a page it read perfectly and a well-formed row for a page it guessed at, and nothing in the output tells them apart.
terbium is built on the opposite premise: solve what you can algorithmically, score how sure you are, and escalate only the parts that are genuinely ambiguous.
A document carries content, not structure
This is what makes document parsing harder than it looks. A PDF does not contain a table. It contains a set of text runs, each with coordinates.
When you look at a spec sheet, your visual system rebuilds a grid from alignment: these six strings share a left edge, so they are a column; these four share a baseline, so they are a row. That reconstruction is so automatic that it is easy to forget it happened.
A text extractor does not do it. It walks the runs in whatever order they were written and joins them. The columns collapse into one line, the grid is gone, and you get back a paragraph that contains all the right words in nearly the wrong order.
This is why “just extract the text” fails on exactly the documents you most want to parse. Prose survives flattening. Tables do not. And business documents are almost entirely tables: catalogues, invoices, price lists, schedules, financial grids, size-by-finish matrices.
Rebuilding the grid from geometry
terbium keeps the coordinates and rebuilds the structure the way your eye does. Each format has an adapter that normalizes its input into positioned words plus images. From there the reconstruction pass does the work that text extraction throws away:
- Cluster words by x-position to recover columns, and by baseline to recover rows.
- Strip headers and footers that repeat on every page, which otherwise become phantom rows.
- Split two-page spreads, where a single logical table has been cut down the gutter.
- Rebuild matrices, the grids where both axes carry meaning.
The detector is content-agnostic on purpose. It works on any column-aligned table. Furniture catalogues are the worked example in the docs because that domain drove the early versions, not because the algorithm knows what a sofa is.
Scoring yourself, honestly
Reconstruction alone is not enough, because a reconstruction can be wrong and still look fine. So every table gets a confidence between 0 and 1, computed from properties you can observe:
- Grid regularity. Do the column edges line up consistently down the page, or is the alignment ragged in a way that suggests the clustering guessed?
- Header presence. Is there a row that reads like a header for the columns beneath it?
- Fill. How much of the reconstructed grid actually contains values? A grid that is mostly empty usually means the column boundaries were drawn in the wrong places.
Those three are deliberately structural. None of them requires understanding the content, which keeps the scorer honest: it cannot convince itself a table is good because the words in it look plausible.
The score is the most important output, more important than the rows, because it turns a black box into something you can build a process around:
doc = terbium.parse("pricelist.xlsx", schema="product")
print(doc.stats)
# Stats(total=725, confident=712, ambiguous=13)
Thirteen rows need a human or a model. Seven hundred and twelve do not. You know the size of your problem before you have spent anything on it. Compare that with a pipeline that returns 725 rows of undifferentiated confidence and leaves you to find the bad ones when a customer complains.
The escalation ladder
Only after scoring does a model enter the picture, and only for the pages that scored below the threshold. The whole loop:
FILE -> ADAPT -> RECONSTRUCT -> SCORE -> [ESCALATE]
| | | |
pdf/pptx/ columns, rows, confidence hard pages only:
xlsx/csv matrices from per record AI if key, else
geometry "add a key" message
Escalation sends each of those pages to a suitable model tier instead of reaching for the largest model by reflex. Most ambiguous pages are ambiguous in small ways, and a cheap model resolves them. Reserving the expensive tier for the pages that need it is the same argument as reserving the model at all.
If you did not supply a key, terbium does not silently return worse output. It tells you, in plain words, that a key would resolve these specific pages. That matters: a parser that quietly degrades is a parser you cannot trust in a pipeline, because you cannot tell a bad day from a bad file.
The catalogue case
The flagship path is a single call, and it needs no key on a clean file:
import terbium
rows = terbium.build_catalog("vendor_catalogue.pdf", images_dir="images/")
terbium.to_catalog_csv(rows, "catalogue.csv")
# rows: {"sku": "RG-1001", "name": "Anatolia Kilim",
# "materials": "wool", "image": "Anatolia_Kilim.jpeg", "page": 12}
What makes this work is that in a catalogue, the photo is the anchor. Every product has one, it is unambiguous, and the label beneath it is almost always the name. So terbium extracts each image, names it from the text directly below, and mines the surrounding text for the SKU and the materials.
That gets clean catalogues out complete with zero model calls. The hard case is a visual lookbook that buries the name in a display title and the material in a paragraph of body copy. That is where you pass ai=terbium.AI(), so a vision model reads the photo plus the page text and fills the blanks.
Same engine, different documents:
rows = terbium.parse("invoice.pdf") # line items, totals, vendor
cv = terbium.parse("maria_cv.pdf", schema="resume") # sections, experience, skills
And from the shell, because half of this work happens in a terminal at the end of a day:
terbium catalogue.pdf --csv out.csv # product table + images/, no AI
terbium lookbook.pdf --images out/ # product photos + manifest.csv
terbium invoice.pdf --csv rows.csv --html report.html
Why this is a doctrine, not an optimisation
It would be easy to read all of this as cost engineering. Lower cost is a consequence, but it is not the argument.
The argument is that the intelligence belongs in the logic, not the model call. A deterministic parser has properties a model does not. It is reproducible: the same file gives the same rows tomorrow. It is debuggable: a wrong column is a bug with a location, not a prompt to reword. It is auditable: you can explain to a vendor why their SKU came out the way it did. And it is honest about failure, because a confidence score is a claim you can check.
A model has exactly one property the parser does not: it can read a page whose structure was never recoverable from geometry in the first place. That is a real and valuable property. It is also a narrow one, and paying for it on every page means paying for it on the 90 percent where it adds nothing.
Put another way: the deterministic path handles the cases you can specify, the model handles the residue, and the confidence score is the interface between them. Remove the score and you no longer have a system. You have two parsers and a coin flip.
What it does not do
When I wrote this, terbium read PDF, PowerPoint (PPTX), Excel (XLSX) and comma-separated values (CSV) files. Version 0.10.0 (GitHub release, 30 September 2026) added image files. File formats has the current list.
A scanned page with no text layer is a genuinely different problem. Without a model, terbium gets words from one only through optical character recognition (OCR), which needs a local Tesseract install. That optional lane arrived in 0.9.4.
It does not understand your business rules: it will faithfully return a SKU that is wrong in your enterprise resource planning (ERP) system, because it is a parser and not a validator. And on a document with no recoverable structure, it says so instead of inventing one, which is the behaviour you want, if not always the one you were hoping for.
pip install terbium-parse. Python 3.9+, MIT licensed, sponsored by NodeMaven. The Python Package Index (PyPI) still serves 0.9.7; Installation shows how to get 0.10.0. Source at github.com/velofy/terbium, docs at velofy.co/terbium.