terbium / Guides
Worked examples
End-to-end terbium recipes: a supplier catalogue to CSV and Shopify, a folder of invoices to one CSV, a price list to feeds, triage, and a review queue.
On this page
Each example below is complete and runs on terbium 0.10.0. They were run against small generated files on 25 October 2026, and the outputs shown are from those runs. Your numbers will differ with real documents.
First principles
Every task here is the same three moves: get records out, decide which ones you trust, and write them where they need to go.
- Records out.
build_cataloggives one dict per product photo.parsegivesRecordobjects with a confidence each. Pick by the document, not by habit: photos anchor catalogue rows, tables and forms anchor everything else. - Trust. Nothing downstream should import a row blindly. Use each record’s
confidence,doc.ambiguous_records, and simple checks such as blank or repeated stock keeping unit (SKU) codes to build a review list. - Write. The catalog comma-separated values (CSV), Shopify and product information management (PIM) writers return the text and also write a file when you pass a path.
records_to_csvandrender_htmlreturn text for you to save.
A supplier catalogue to a CSV, photos and a review list
Build the table, write it, and list the rows a person should check before import: rows with no name, no SKU, or a SKU that appears on more than one product.
from collections import Counter
import terbium
rows = terbium.build_catalog("supplier.pdf", images_dir="supplier_images/")
terbium.to_catalog_csv(rows, "supplier.csv")
counts = Counter(r["sku"] for r in rows if r["sku"])
review = [
r for r in rows
if not r["name"] or not r["sku"] or counts[r["sku"]] > 1
]
print(f"{len(rows)} products, {len(review)} to review")
for r in review:
print(r["page"], r["image"], r["name"], r["sku"])
On a two-product test page where only the first product printed a code, both rows came back with that code, and the check caught it:
2 products, 2 to review
1 Anatolia_Kilim.png Anatolia Kilim RUG-101
1 Nordic_Shag.png Nordic Shag RUG-101
Why that happens is on Troubleshooting and limits.
A catalogue to a Shopify import
build_catalog returns plain dicts, and the feed writers take Record objects. Wrap each row, and let enrich_fields add the colour and material families that become Shopify tags:
import terbium
from terbium.normalize import enrich_fields
rows = terbium.build_catalog("supplier.pdf", images_dir="supplier_images/", announce=False)
records = [
terbium.Record(
sku=r["sku"],
fields=enrich_fields({
"name": r["name"],
"material": r["materials"],
"dimensions": r["dimensions"],
"image": r["image"],
}),
source_page=r["page"] - 1, # Record pages are 0-based, catalog rows 1-based
confidence=1.0, # catalog rows carry no score; choose your own
)
for r in rows
]
terbium.to_shopify_csv(records, "shopify.csv")
Image Src is filled from the image field as it is, which here is a local file name. Shopify imports images from web addresses (URLs), so upload the photos first and put each URL in image.
A folder of invoices to one CSV of line items
from pathlib import Path
import terbium
line_items = []
for pdf in sorted(Path("invoices").glob("*.pdf")):
doc = terbium.parse(str(pdf), doc_type="transaction", announce=False)
if doc.escalation:
print(pdf.name, doc.escalation)
for r in doc.records:
if r.get("record_type") == "line_item":
line_items.append({"file": pdf.name, **r.to_dict()})
with open("line_items.csv", "w", newline="", encoding="utf-8") as fh:
fh.write(terbium.records_to_csv(line_items))
Each line item carries the invoice’s header fields (number, date, customer and so on), so one flat CSV holds everything. The output, trimmed at the right:
file,sku,description,quantity,unit_price,amount,record_type,number,date,customer,total,...
inv0.pdf,,Widget A,10,80.00,800.00,line_item,INV-2041,2026-07-20,Acme Corp,800.00,...
inv1.pdf,,Widget B,2,15.00,30.00,line_item,INV-2042,2026-07-20,Acme Corp,30.00,...
The total column is the invoice total copied into each line, not a per-line value. doc_type="transaction" skips classification; drop it if the folder mixes document kinds.
The same from the shell, one CSV per invoice:
for f in invoices/*.pdf; do
terbium "$f" --type transaction --csv "${f%.pdf}.csv" --limit 0
done
A price list to Shopify and PIM JSON
A spreadsheet is classified as table and read with the product schema, which maps the column headers to product fields by meaning:
import terbium
doc = terbium.parse("pricelist.xlsx")
print(doc.doc_type, doc.stats) # table Stats(total=2, confident=2, ambiguous=0)
terbium.to_shopify_csv(doc.records, "shopify.csv")
terbium.to_pim_json(doc.records, "products.json")
For a sheet with the headers SKU, Product name, Colour, Material, Price, Dimensions and Qty, the first JavaScript Object Notation (JSON) object was:
{
"sku": "AB-100",
"confidence": 0.98,
"name": "Oak chair",
"color": "Off White",
"material": "solid oak",
"price": "$120.00",
"price_amount": 120.0,
"currency": "USD",
"dimensions": "45 x 50 x 80 cm",
"quantity": "4",
"collection": "Spring 2026",
"color_family": "white",
"material_family": "wood",
"dimensions_mm": [450.0, 500.0, 800.0]
}
collection is the sheet name. The JSON file itself is indented one value per line; the list is shown on one line here.
Triage a folder before parsing
parse classifies as its first step, but you can classify on its own when you only need to sort files. It adapts the file and scores it, without rebuilding tables:
import terbium
from terbium.documents import get_adapter
for path in ["supplier.pdf", "invoices/inv0.pdf", "pricelist.xlsx"]:
pages = get_adapter(path).parse(path)
doc_type, scores = terbium.classify(pages)
print(f"{path}: {doc_type}")
supplier.pdf: catalog
invoices/inv0.pdf: transaction
pricelist.xlsx: table
Then send catalog and lookbook files to build_catalog, and the rest to parse. Image files and scanned pages need Tesseract installed before they have words to classify.
A stricter threshold and AI only on the weak records
Raise the threshold to see more records as ambiguous, then spend artificial intelligence (AI) model calls on those only:
import terbium
doc = terbium.parse("pricelist.xlsx", threshold=0.9, announce=False)
print(len(doc.ambiguous_records), "of", doc.stats.total, "records below 0.9")
ai = terbium.AI()
if ai.available:
records = terbium.enrich_products(doc.records, ai, only_below=0.9)
enrich_products returns a new list. Records at or above only_below pass through untouched, and so does every record when no provider is available. Pass a terbium.AI object, not True.
The image manifest as a CSV
export_images returns the manifest; the command line does not write one. Save it with the csv module:
import csv
import terbium
manifest = terbium.export_images("supplier.pdf", "photos/")
if manifest:
with open("photos/manifest.csv", "w", newline="", encoding="utf-8") as fh:
writer = csv.DictWriter(fh, fieldnames=list(manifest[0]))
writer.writeheader()
writer.writerows(manifest)
page,product,collection,file,format,width_px,height_px,colorspace,bytes,dpi,dominant_color,bbox
bbox is written as a tuple such as "(43.3, 40.0, 336.7, 260.0)". The fields are described on Pulling out images.