Quickstart
Quickstart
Section titled “Quickstart”A five-minute tour. We’ll go from the one-liner to a full custom recipe that OCRs a scanned tabular form and writes Excel.
Install
Section titled “Install”pip install "scriva[openai,excel]"Extras pull optional engine adapters and exporters: openai,
anthropic, bedrock, tesseract, paddle, azure, excel,
pdf, parquet, vector-cache. The core library has no mandatory
dependency on any OCR engine.
Set credentials
Section titled “Set credentials”export OPENAI_API_KEY=sk-...The one-liner
Section titled “The one-liner”import scrivatext = scriva.read("scan.png")scriva.read picks a sensible Worker and Orchestrator recipe based
on the extras you installed and returns the recognised text. A few
common knobs:
scriva.read("scan.png") # -> strscriva.read("scan.pdf", as_="json") # -> dictscriva.read("scan.png", as_="dataframe") # -> pandas.DataFramescriva.read("scan.png", to="out.xlsx") # writes file, returns Pathscriva.read("scan.png", language="ja", worker="anthropic")That is the entire API for the 80% case. The rest of this page is for when you want to inspect, swap, or extend.
Loading PDFs
Section titled “Loading PDFs”For multi-page inputs the loader exposes a few knobs:
from scriva import Document
doc = Document.load( "scan.pdf", pages=None, # Sequence[int] | slice | None — None = all pages dpi=300, # raster resolution for non-text PDFs password=None, # encrypted PDFs renderer="pdfium", # "pdfium" | "poppler")PDFs without an embedded text layer are rasterised at dpi and then
treated as page images by the rest of the recipe. PDFs with a
text layer are still rasterised — scriva does not currently
short-circuit on embedded text; treat that as an OCR cache, not a
substitute. The pdf extra installs pypdfium2;
renderer="poppler" requires the poppler binary on $PATH.
pages accepts a list ([0, 2, 5]) or a slice (slice(0, 10)).
One-page PDFs do not need this — recipe("file.pdf") always works.
Build a Worker
Section titled “Build a Worker”A Worker converts one crop into one Recognition. Build it by
chaining decorators on an engine factory:
from scriva import Workerfrom scriva.prompts import Prompt
worker = ( Worker.openai("gpt-4o") .prompt(Prompt.cell) .cache(".scriva_cache") .score(method="rendering") .retry(times=3) .parallel(max=8))Each call returns a new Worker; chaining never mutates. See worker.md for the full reference.
Specialised Workers
Section titled “Specialised Workers”from scriva import Worker
Worker.number() # numbers / currencyWorker.date() # locale-aware datesWorker.checkbox() # checked / unchecked / ambiguousWorker.handwriting() # cursive / printed handwritingWorker.signature() # presence + similarity, not textWorker.barcode() # 1D / 2D / QRWorker.skip() # short-circuit; returns text=NoneBuild an Orchestrator recipe
Section titled “Build an Orchestrator recipe”The Orchestrator owns the flow around the Worker. Chain steps in the natural order:
from scriva import Orchestrator
recipe = ( Orchestrator() .deskew() .crop(margins="auto") .split.grid() .classify(blank=True, merged=True) .recognize(worker) .reconstruct.grid() .export.xlsx("out.xlsx") .options(page_concurrency=4))
result = recipe("scan.png")print(f"Recognised {len(result.regions_with_text())} cells, " f"mean confidence {result.mean_confidence:.2f}")A few things to notice:
- Recipes are reusable.
recipeis immutable; call it on as many sources as you like. recipe(doc)is sync. Internally async; the call blocks until done. Useawait recipe.aio(doc)from an async caller, orasync for page in recipe.stream(doc): …for streaming.- Cache is a string.
worker.cache(".scriva_cache")becomes aFileSystemCache. See caching.md forCache.layered(...), Redis, and friends.
Per-cell-type Workers
Section titled “Per-cell-type Workers”Forms mix data types. Dispatch by region.kind:
worker_text = Worker.openai("gpt-4o").cache(".scriva_cache")
recipe = ( Orchestrator() .deskew() .split.grid() .classify(blank=True, merged=True) .recognize.by_kind({ "text": worker_text, "number": Worker.number(), "date": Worker.date(), "checkbox": Worker.checkbox(), "blank": Worker.skip(), }) .reconstruct.grid() .export.xlsx("out.xlsx"))region.kind is set by .classify(...). Override or extend with
.classify(kinds=...) — see postprocessors.md.
Get text out
Section titled “Get text out”The DocumentResult knows how to serialise itself, so you don’t
need to wire an exporter just to print things:
result.render() # str — reading-order textresult.to_dict() # dictresult.to_dataframe() # pandasresult.to_excel("out.xlsx") # writes file, returns Pathresult.show() # matplotlib viz of boxes (optional extra)Use the in-recipe .export.xlsx(...) when you want the write to
happen during the run (it emits an event and respects
page_concurrency); use result.to_excel(...) for ad-hoc
serialisation.
Inspect what happened
Section titled “Inspect what happened”Every step emits structured events. Pass a callback to see them live:
recipe("scan.png", on_event=print)Or stream them yourself:
async for event in recipe.events("scan.png"): print(event.stage, event.kind, event.payload)Sample output:
preprocess startedpreprocess finished ms=42 step=deskew angle=-0.8split finished rows=12 cols=6 cells=72classify finished blank=14 merged=3recognize started total=58recognize progress done=10 total=58 cache_hits=3 worker=textrecognize finished ms=4910reconstruct finished shape=grid regions=58export finished path=out.xlsx format=xlsx bytes=18432See Architecture › Observability for SSE and async-iterator patterns.
Swap any piece
Section titled “Swap any piece”from scriva import Workerrecipe = recipe.replace("recognize", Worker.anthropic("claude-opus-4-7"))Want a different splitter? Implement the protocol — five lines:
from scriva import Splitter, Layout, Region
class MySplitter(Splitter): async def split(self, page) -> Layout: ...…or skip the class entirely with the decorator:
from scriva import splitter, Layout, Region
@splitterasync def my_splitter(page): boxes = my_model.predict(page.image) return Layout.from_regions([Region(bbox=b, role="data") for b in boxes], page=page)
recipe = recipe.replace("split", my_splitter)The same pattern works for @worker, @preprocessor, @classifier,
@reconstruct, and @exporter.
What to read next
Section titled “What to read next”- Concepts — the two pillars you just used.
- Worker — the full Worker reference.
- Orchestrator — the full Orchestrator reference.
- Domains — pre-built recipes you can grab off the shelf.