Documentation
¶
Overview ¶
Package ovrin turns documents into structured data.
Give it a Go struct describing what you want and a document — a PDF, a scan, a photograph — and it returns that struct populated, alongside the evidence for every value: a confidence score you can decompose, a record of which page and which region each value came from, and an explicit signal when a person should look before the data is used.
type Invoice struct {
Number string `ovrin:"invoice number,required"`
Vendor string `ovrin:"vendor company name"`
Currency string `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
Total float64 `ovrin:"total amount including tax,required,min=0"`
}
c := ovrin.New(ovrin.WithModel(model))
res, err := ovrin.Extract[Invoice](ctx, c, ovrin.File("invoice.pdf"))
if err != nil {
return err
}
if !res.Valid || res.NeedsReview {
return review.Queue(res)
}
return ledger.Post(res.Data)
Two answers, not one ¶
A non-nil error means nothing usable came back: the source could not be read, no provider was configured for it, a limit was exceeded, or the context was cancelled. It does not mean the data is good — that is Result.Valid.
A field that could not be read is marked absent rather than filled with a zero value. A payments system must be able to tell "the total is zero" from "we could not read the total", so FieldResult.Found reports presence and nothing is ever guessed to satisfy a struct.
Not a prompt with extra steps ¶
Ovrin runs a staged pipeline rather than handing a document to a model and hoping. When a PDF carries its own text, reading it is exact and nearly free; rendering those characters to pixels for a model to read back is a lossy round trip. OCR runs when there is no text layer. Vision is a third reading, not a shortcut past the pipeline.
That staging is also what makes the rest possible. Confidence is computed from named signals that fail in uncorrelated ways — whether the value appears in the document at all, how cleanly the characters read, whether it satisfies its declared rules, whether it agrees with its siblings — because a model's self-reported confidence is uncorrelated with correctness, and token logprobs are unavailable on one major provider and saturate to a constant under the constrained decoding used here.
Provider independent ¶
Three small interfaces — Model, OCR and Renderer — and no vendor is privileged. Implementations live in their own modules, so a user who wants Tesseract does not inherit a cloud SDK, and a user who wants neither inherits nothing.
This package has no external dependencies and uses no cgo, so it cross-compiles and builds static.
Untrusted input ¶
Documents arrive from claimants, suppliers and email attachments. They are parsed with finite limits on every dimension, and their text reaches a model as data, never as instruction. Ovrin does not promise that prompt injection is impossible — nobody can — but a value an injected instruction produces tends not to appear anywhere in the document, and ovrin reports that rather than accepting it silently.
See docs/pipeline.md for the nine stages, docs/schema.md for the struct tag grammar, docs/confidence.md for what the score does and does not mean, and docs/threat-model.md before processing documents from the public.
Index ¶
- Constants
- Variables
- type BatchResult
- type BreakerOption
- type Candidate
- type Cell
- type CellKind
- type Client
- type Content
- type CrossFieldResult
- type CrossFieldRule
- type CrossFields
- type DateOrder
- type Document
- type DocumentOCR
- type Error
- type Event
- type Explanation
- type FieldEvidence
- type FieldResult
- type Hook
- type Kind
- type Layout
- type Line
- type Metadata
- type Model
- type ModelRequest
- type ModelResponse
- type OCR
- type Op
- type Option
- func WithConcurrency(n int) Option
- func WithCrossField(rules ...CrossFieldRule) Option
- func WithDateOrder(d DateOrder) Option
- func WithHook(h Hook) Option
- func WithMaxDecompressedBytes(n int64) Option
- func WithMaxDepth(n int) Option
- func WithMaxObjects(n int) Option
- func WithMaxPagePixels(n int) Option
- func WithMaxPages(n int) Option
- func WithMaxReplacementRatio(r float64) Option
- func WithMaxSourceBytes(n int64) Option
- func WithMaxStreamBytes(n int64) Option
- func WithMaxTextBytes(n int64) Option
- func WithMinDecodableRatio(r float64) Option
- func WithMinTextDensity(d float64) Option
- func WithModel(m Model) Option
- func WithOCR(o OCR) Option
- func WithReading(m ReadingMode) Option
- func WithRenderer(r Renderer) Option
- func WithReviewThreshold(t float64) Option
- func WithScorer(s Scorer) Option
- type Page
- type Pair
- type Provenance
- type Reading
- type ReadingMode
- type Recognition
- type Rect
- type Ref
- type Region
- type Renderer
- type Result
- type ReviewReason
- type RuleResult
- type Scorer
- type Signal
- type Source
- type Span
- type Table
- type Tolerance
- type Usage
- type Word
Examples ¶
Constants ¶
const ( // SignalOCR is character-recognition confidence over the words backing // the value. SignalOCR = "ocr" // SignalSchema is whether the value satisfied its declared type and rules. SignalSchema = "schema" // SignalCrossField is consistency with sibling fields — line items summing // to a total, an issue date before a due date. SignalCrossField = "cross_field" // SignalAgreement is whether two independent readings produced the same // value. Available only when two readings ran. SignalAgreement = "agreement" // SignalFormat is whether the value matches the expected shape for its // kind: a date that parses, a currency code that exists. SignalFormat = "format" // SignalGrounding is whether the value actually appears in the source. // // The cheapest strong signal available, and the one that catches the // failure that matters most: a value appearing nowhere in the document it // was read from was not read from it. SignalGrounding = "grounding" )
The names of the built-in confidence signals.
These are untyped string constants rather than a named enum type, because the set is open at the edges: a custom Scorer fitted to somebody's own corpus may emit signals ovrin has never heard of, and a named type would either forbid that or need an escape hatch that defeats the point. This is the same shape as http.MethodGet — well-known values in an open set.
The six below are what ovrin's own scorer produces. A signal that does not apply to a field is absent, not zero: a text-layer PDF has no OCR signal, and scoring that as 0.0 would penalise the most accurate acquisition path ovrin has.
const ( // DefaultMaxSourceBytes bounds the source document. DefaultMaxSourceBytes int64 = 64 << 20 // 64 MiB // DefaultMaxDecompressedBytes bounds decompressed output across the whole // document. Cumulative, because a thousand streams of one mebibyte is the // same attack as one stream of a gibibyte. DefaultMaxDecompressedBytes int64 = 512 << 20 // 512 MiB // DefaultMaxStreamBytes bounds decompressed output from any single stream. DefaultMaxStreamBytes int64 = 64 << 20 // 64 MiB // DefaultMaxTextBytes bounds extracted text. DefaultMaxTextBytes int64 = 32 << 20 // 32 MiB // DefaultMaxPages bounds the page count. Also bounds spend: ten thousand // pages sent to a per-page-priced OCR provider is not a crash, it is an // invoice. DefaultMaxPages = 1000 // DefaultMaxDepth bounds recursion through the object graph. DefaultMaxDepth = 64 // DefaultMaxObjects bounds the object count. DefaultMaxObjects = 500_000 // DefaultMaxPagePixels bounds one rasterised page. DefaultMaxPagePixels = 50_000_000 // 50 M )
The default limits.
Every limit has a default and every default is finite. Ovrin parses attacker-controlled binary formats, and the resource attacks against them are documented rather than hypothetical: a 600 KB PDF whose streams are nested FlateDecode expands to ten gigabytes in memory, a cross-reference cycle recurses until the stack is gone, a media box can declare a page that rasterises larger than physical memory.
These numbers are judgement, not measurement — round values chosen to sit comfortably above real documents and comfortably below dangerous ones. They will be revised against the evaluation corpus. Raising a default is not breaking; lowering one is.
They are exported so a caller raising a limit can express it relative to the default rather than restating a number that may change.
See docs/adr/0020-resource-limits.md.
const ( // DefaultReviewThreshold is the field confidence below which a result is // flagged for review. DefaultReviewThreshold = 0.70 // DefaultMinTextDensity is the minimum characters per square inch for a // page's text layer to be considered usable. DefaultMinTextDensity = 0.5 // DefaultMaxReplacementRatio is the maximum proportion of U+FFFD // replacement characters a usable text layer may contain. DefaultMaxReplacementRatio = 0.02 // DefaultMinDecodableRatio is the minimum proportion of characters that // must map through a ToUnicode entry or a standard encoding for a text // layer to be considered usable. // // A PDF with a broken ToUnicode table can produce plausible-looking // rubbish that would poison everything downstream, so a page failing this // falls through to OCR rather than being trusted. DefaultMinDecodableRatio = 0.90 )
The default policy thresholds.
const ( WeightGrounding = 0.30 WeightAgreement = 0.25 WeightOCR = 0.20 WeightSchema = 0.15 WeightFormat = 0.05 WeightCrossField = 0.05 )
The weights the default scorer gives each signal.
They are **provisional**. Until they are calibrated against a corpus, confidence is a ranking signal — good for ordering a review queue — and not a probability. Nothing here means "correct this often", and docs/confidence.md says so at greater length.
defaults_doc_test.go asserts these equal the table in docs/confidence.md, so changing one here without changing the other turns the build red.
const ( // CapRuleFailed applies when a declared rule other than required failed. CapRuleFailed = 0.40 // CapUngrounded applies when the value appears nowhere in the source. It // is lower than CapRuleFailed because a well-formed value that is not in // the document is the more dangerous of the two: it looks correct. CapUngrounded = 0.35 // CapDisagreement applies when two readings produced different values. CapDisagreement = 0.50 // CapSuspicious applies when the source page carried content that looked // like an injection attempt. CapSuspicious = 0.60 )
The ceilings applied after the weighted mean.
A floor exists where averaging would be wrong: a value that satisfies four signals and fails its declared rule is not four-fifths correct, it is unusable. Averaging alone would let strong signals hide the one that matters.
const DefaultBreakerCooldown = 30 * time.Second
DefaultBreakerCooldown is how long a breaker stays open before it tries again.
Thirty seconds is long enough that a provider restarting or a rate limit resetting has had a chance, and short enough that recovery is not something a person has to notice and act on.
const DefaultBreakerFailures = 5
DefaultBreakerFailures is how many consecutive failures open a breaker.
Five, because a provider that has failed five times in a row is not having a bad moment. Lower trips on ordinary noise and sends traffic to a fallback that may be worse; higher spends real money and latency discovering something already known.
Variables ¶
var ( // ErrUnsupportedFormat means the source is not a format ovrin can read. // Format is determined by content, so this is not a filename problem. ErrUnsupportedFormat = errors.New("ovrin: unsupported document format") // ErrNoContent means the document was read but yielded nothing usable — // a PDF whose text layer decodes to rubbish, or a blank scan. ErrNoContent = errors.New("ovrin: no readable content in document") // ErrNoProvider means no configured provider can read this document. The // message names the ways to fix it, because the remedy is never obvious // from the condition alone. ErrNoProvider = errors.New("ovrin: no provider configured for this document") // ErrSchema means the Go type cannot be turned into a schema: an unknown // rule, an unsupported field type, a recursive type, a malformed tag. It // is raised before any provider is contacted, so a typo costs nothing. ErrSchema = errors.New("ovrin: invalid schema") // ErrLimitExceeded means a resource limit was reached. The message names // the limit and the option that raises it. See // docs/adr/0020-resource-limits.md. ErrLimitExceeded = errors.New("ovrin: resource limit exceeded") // ErrAuth means a provider rejected the credential. A fallback chain never // advances past this: a misconfigured key should fail loudly on the first // provider rather than silently degrade to the third. ErrAuth = errors.New("ovrin: provider authentication failed") // ErrRateLimit means a provider is throttling. A fallback chain advances. ErrRateLimit = errors.New("ovrin: provider rate limited") // server error. A fallback chain advances. ErrUnavailable = errors.New("ovrin: provider unavailable") // ErrBadResponse means a provider replied with something unusable — most // often JSON that does not parse. The offending bytes are attached to the // [Error] so the failure can be diagnosed rather than guessed at. ErrBadResponse = errors.New("ovrin: provider returned an unusable response") // ErrUnsupported means a provider cannot do what was asked: images sent to // a model without vision, a URL to an adapter that requires inline data. // An adapter returns this rather than quietly producing a worse answer. ErrUnsupported = errors.New("ovrin: unsupported by this provider") // ErrEncrypted means the document is encrypted. The message names the // encryption. Password support is a later decision. ErrEncrypted = errors.New("ovrin: document is encrypted") // ErrInternal means ovrin failed, rather than the document, a provider or // a limit: a broken entropy source, or a pipeline stage handed input its // contract forbids. // // The remedy is distinct and is the reason this exists — file a bug. Do // not re-scan the document, switch provider or raise a limit. See // docs/adr/0030-an-internal-failure-sentinel.md. ErrInternal = errors.New("ovrin: internal failure") // ErrBadRequest means a provider rejected a request ovrin considers valid // — most often a JSON Schema dialect it does not accept. // // This is distinct from [ErrSchema], and the distinction is the remedy: // ErrSchema means fix the struct, ErrBadRequest means change provider or // simplify the schema. ErrBadRequest = errors.New("ovrin: provider rejected the request") )
The conditions an extraction can fail on.
These are the values to test with errors.Is. Nothing in ovrin, and nothing calling it, should ever branch on the text of an error message: a provider rewording a response must not change how a program behaves. See docs/rules.md §2.2.
Functions ¶
This section is empty.
Types ¶
type BatchResult ¶
type BatchResult[T any] struct { // Index is the position of this source in the slice passed to // [ExtractBatch]. // // Results come back in that same order, so this is redundant for a caller // ranging over them — and load-bearing for one that filters, sorts, or // collects only the failures and still needs to say which document. Index int // Result is what was extracted, or nil when Err is set. Result *Result[T] // Err is why this source produced nothing, or nil. // // It is classified exactly as [Extract]'s error is: test it with // [errors.Is] against the sentinels, never by its text. Err error }
BatchResult is what one source in a batch produced.
Exactly one of Result and Err is set. They are kept as separate fields rather than collapsed into a Result with an error inside it for the reason ADR-0004 gives: an extraction that failed produced nothing usable, and a half-filled Result invites a caller to read fields that were never extracted.
func ExtractBatch ¶
func ExtractBatch[T any](ctx context.Context, c *Client, srcs []Source, opts ...Option) []BatchResult[T]
ExtractBatch extracts from many sources, several at a time.
Results are returned in the order the sources were given, whatever order they finished in. One document failing does not fail the batch: its entry carries the error and every other document is still extracted. That is the difference between a batch API and a loop — a loop that stops at the first bad scan in a thousand-file directory has thrown away nine hundred and ninety-nine good extractions.
Concurrency is bounded by WithConcurrency, which also bounds page-level work inside each extraction. The two multiply, so a batch of eight with a concurrency of four can have thirty-two page reads in flight; set it with the provider's rate limit in mind rather than the machine's core count.
Cancelling ctx stops sources that have not started. Sources already running observe the cancellation through their own provider calls and return whatever error that produced, so a cancelled batch reports per document what happened to it rather than one error for the whole run.
Passing no sources returns nil. That is not an error: a directory with no documents in it is an ordinary thing to point this at.
type BreakerOption ¶
type BreakerOption func(*breaker)
BreakerOption configures a breaker.
func WithBreakerCooldown ¶
func WithBreakerCooldown(d time.Duration) BreakerOption
WithBreakerCooldown sets how long the breaker stays open. A value of zero or less is ignored: a breaker that reopens immediately has not broken anything.
func WithBreakerFailures ¶
func WithBreakerFailures(n int) BreakerOption
WithBreakerFailures sets how many consecutive failures open the breaker. A value below one is ignored, because a breaker that opens on zero failures is a provider that is never called.
type Candidate ¶
type Candidate struct {
// Value is this reading's answer.
Value any
// Reading is which reading produced it.
Reading Reading
// Source is where in the document it came from.
Source Provenance
}
Candidate is one reading's answer for a field, when readings disagreed.
Disagreement is recorded rather than resolved. Two readings fail in uncorrelated ways, so when they differ at least one is definitely wrong and silently preferring either is the failure this exists to prevent.
type Cell ¶
type Cell struct {
// Row is the 0-based index of the cell's first row.
Row int
// Column is the 0-based index of the cell's first column.
Column int
// RowSpan is how many rows the cell covers. Zero and one both mean one:
// providers differ on whether they report a span of one, and normalising
// it here means every adapter does not have to.
RowSpan int
// ColumnSpan is how many columns the cell covers, with zero meaning one,
// as for RowSpan.
ColumnSpan int
// Kind is what the provider said the cell is.
Kind CellKind
// Text is the cell's content as the provider read it, unnormalised.
//
// It is document content. It belongs on a field or in front of a person,
// never in a log line, an error, an event or a metric (rule §2.5, §7.5).
// Use [Table.Ref] when you need to say which cell you mean.
Text string
// Box is the cell's region on the page. A zero Rect means the provider
// gave no geometry.
Box Rect
// Confidence is the provider's own for this cell, on 0..1.
Confidence float64
}
Cell is one cell of a Table.
func (Cell) Columns ¶
Columns returns how many columns the cell covers, treating an unreported span of zero as one, as for Cell.Rows.
func (Cell) Covers ¶
Covers reports whether the cell occupies a grid position, spans included.
It is the operation every lookup in a merged table needs: the value under "Quantity" may be stored at row 3 and asked for at row 4 because the row above it spans two, and a caller comparing Row and Column directly would conclude the position is empty.
func (Cell) Rows ¶
Rows returns how many rows the cell covers, treating an unreported span of zero as one.
It exists so that the "zero and one both mean one" rule Cell.RowSpan states is applied in one place. Every caller that walks a merged table needs it, and a caller that reads RowSpan directly gets a table one row short wherever a provider left the span unreported.
type CellKind ¶
type CellKind string
CellKind is what a provider said a cell is.
The set is closed and is the intersection of what the supported providers report, so all of them map onto it without loss. A provider that says nothing leaves CellUnknown, which is not CellData: "this is a data cell" and "this provider does not label cells" are different facts, and collapsing them would make a header row silently wrong rather than silently absent.
const ( // CellUnknown is a provider that does not label cells. CellUnknown CellKind = "" // CellData is a cell the provider labelled as content. CellData CellKind = "data" // CellColumnHeader labels the column it sits above. CellColumnHeader CellKind = "column_header" // CellRowHeader labels the row it sits beside. CellRowHeader CellKind = "row_header" )
The cell kinds.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client holds the providers, limits and policy an extraction runs under.
Build one and share it: every method is safe for concurrent use by multiple goroutines, and an Extract call never mutates it. A service processing trusted internal documents and untrusted public uploads should hold two, with different limits.
func New ¶
New returns a Client configured by opts.
The only required option is WithModel. New panics if given a nil Model, because that is programmer error at construction and the alternative is a nil dereference on the first extraction, thousands of lines from the mistake. Omitting WithModel entirely is configuration rather than a mistake, and surfaces as ErrNoProvider from Extract.
type Content ¶
type Content struct {
// Reading is which reading produced this content.
Reading Reading
// Page is 1-based.
Page int
// Text is set when Reading is text or OCR.
Text string
// Image is set when Reading is vision. Raw bytes, never base64 — encoding
// is the adapter's job, and doing it twice corrupts the image.
Image []byte
// MediaType is the IANA media type, required when Image is set.
MediaType string
}
Content is one piece of material handed to a Model. It is always untrusted.
type CrossFieldResult ¶
type CrossFieldResult struct {
// Name identifies the rule, for a review reason. A name you chose, never
// a value from the document.
Name string
// Fields are the field keys the rule read, so a reviewer knows where to
// look. Keys, never values (docs/rules.md §7.5).
Fields []string
// Applicable reports whether the rule could run at all. A rule whose
// inputs were not extracted has not failed — the missing field is already
// reported by its own required rule, and counting it again would punish a
// document twice for one absence.
Applicable bool
// Passed reports the outcome, and is meaningful only when Applicable.
Passed bool
// Message says why not, and is empty when Passed. It describes the
// inconsistency and never states the amounts: these strings reach logs.
Message string
}
CrossFieldResult is the outcome of one cross-field rule.
Separate from RuleResult because a cross-field rule is not a rule on a field: it names the several fields it read, and it can be inapplicable in a way a field rule cannot.
type CrossFieldRule ¶
type CrossFieldRule interface {
// Name identifies the rule in results and review reasons. It is a name
// you choose, and it appears in a ReviewReason, so it must not contain a
// value from the document.
Name() string
// Check reports the outcome.
Check(f CrossFields) CrossFieldResult
}
CrossFieldRule checks one consistency property across sibling fields.
It is the check no single field can make: line items adding to a total, an issue date before a due date. Those catch the misread digit that every other signal accepts, because a wrong number is still a number and still passes its type, its format and its range.
A rule reads values and returns a verdict. It cannot fail, cannot do I/O and cannot see the document — an inconsistency is a finding, not an error (docs/adr/0004-partial-results.md).
func Before ¶
func Before(earlier, later string) CrossFieldRule
Before returns a rule requiring that one date is not after another.
Equal dates pass: an invoice issued and due the same day is a document, not an inconsistency.
func CrossFieldFunc ¶
func CrossFieldFunc(name string, check func(CrossFields) CrossFieldResult) CrossFieldRule
CrossFieldFunc returns a rule from a function.
The extension point. Consistency worth checking is specific to a document type, and the three rules above are the ones common enough to ship rather than an attempt at a complete set.
func Sum ¶
func Sum(total string, tol Tolerance, parts ...string) CrossFieldRule
Sum returns a rule requiring that named fields add up to a total.
The everyday case is a subtotal and a tax adding to a total: the single most checkable claim on an invoice.
ovrin.Sum("total", ovrin.Tolerance{Absolute: 0.01}, "subtotal", "vat")
func SumItems ¶
func SumItems(total, slice string, tol Tolerance, leaves ...string) CrossFieldRule
SumItems returns a rule requiring that a slice's line items add up to a total.
The leaf fields are multiplied together within each element, so a line's quantity and unit price make its amount:
ovrin.SumItems("total", "items", tol, "quantity", "unit_price")
type CrossFields ¶
CrossFields is the converted value of every extracted field, keyed by the path used in Result.Fields: "total", "vendor.name", "items[0].unit_price".
Only values that converted appear. A rule therefore never sees a fabricated zero, and a missing key means the value was not read — which is what CrossFieldResult.Applicable is for.
func (CrossFields) Count ¶
func (f CrossFields) Count(slice string) int
Count returns how many elements of a slice field were extracted.
func (CrossFields) Number ¶
func (f CrossFields) Number(key string) (float64, bool)
Number returns a numeric field, and whether it was read.
type DateOrder ¶
type DateOrder string
DateOrder resolves ambiguous numeric dates such as 03/04/2026.
The zero value does not guess. An ambiguous date is flagged for review instead, because silently reading 3 April as 4 March is exactly the kind of confidently wrong answer this library exists to catch.
const ( // DateOrderUnknown flags ambiguous dates rather than resolving them. DateOrderUnknown DateOrder = "" // DayFirst reads 03/04/2026 as 3 April. DayFirst DateOrder = "dmy" // MonthFirst reads 03/04/2026 as 4 March. MonthFirst DateOrder = "mdy" // YearFirst reads 2026/03/04 as 4 March. YearFirst DateOrder = "ymd" )
type Document ¶
type Document struct {
// Kind is the detected format.
Kind Kind
// Pages is the page count, or 0 when it is not yet known.
//
// Detection resolves it only where the format fixes it structurally — a
// PNG is one page. Counting the pages of a PDF means parsing the PDF,
// which is a later stage, and 0 means "not yet known" rather than a
// placeholder 1 (docs/rules.md §8.5).
Pages int
// Size is the length of Data, in bytes.
Size int64
// Data is the document itself.
//
// A [Renderer] and a [DocumentOCR] are asked to read this document, so
// they have to be able to reach it — an earlier version of this type
// carried only metadata, which made both seams unimplementable. The bytes
// are already in memory by the time a Document exists, so carrying them
// costs a slice header rather than a copy.
//
// Treat it as read-only. It is shared with the pipeline, and it is
// untrusted (docs/rules.md §7.1).
Data []byte
}
Document is a Source whose format has been identified.
It is what the pipeline works on after detection, and what a Renderer and a DocumentOCR receive.
type DocumentOCR ¶
type DocumentOCR interface {
OCR
// RecogniseDocument reads every page, returning one Recognition per page
// in page order.
RecogniseDocument(ctx context.Context, doc Document) ([]*Recognition, error)
}
DocumentOCR is an OCR provider that also accepts a whole document.
Cloud providers that rasterise server-side implement this, and it is what lets a scanned PDF be processed with no local renderer at all — the route that makes scanned documents work before render/pdfium exists.
type Error ¶
type Error struct {
// Op is the pipeline stage that failed.
Op Op
// Provider names the adapter involved, if one was.
Provider string
// Page is 1-based, and zero when the failure is not page-specific.
Page int
// Field is the schema field, when the failure is field-specific. It is a
// field name, never a field value.
Field string
// Kind is the sentinel this error is an instance of.
Kind error
// Message adds detail. It never contains document content.
Message string
// contains filtered or unexported fields
}
Error carries the detail behind one of the sentinels above.
Kind holds the sentinel, so errors.Is finds it. Unwrap also returns the underlying cause, so a single value answers both "what kind of failure was this?" and "was it ultimately a cancelled context?".
Message never contains content read from the document. A document is somebody's invoice or medical record, and an error string is a log line that ends up in systems nobody audited. See docs/rules.md §2.5 and §7.5.
func (*Error) Error ¶
Error implements the error interface.
The sentinel's own "ovrin: " prefix is trimmed so it is not printed twice.
func (*Error) Unwrap ¶
Unwrap returns both the sentinel and the underlying cause, so that errors.Is(err, ovrin.ErrRateLimit) and errors.Is(err, context.Canceled) can both succeed against the same value.
func (*Error) WithCause ¶
WithCause attaches an underlying error, which becomes reachable through errors.Is and errors.As without appearing in the message.
Adapters live in their own modules and must be able to build an Error that carries both a sentinel and the transport error underneath it — that is the whole promise of the multi-error Error.Unwrap, and without an exported setter an adapter could only satisfy it by printing the cause into the message, which is how a provider quoting a prompt back puts document content in a log line.
There is deliberately no matching getter. ADR-0019 settles that the cause is reached through Unwrap, and a second way to ask the same question is a second thing to keep in step.
type Event ¶
type Event struct {
// Op is the stage that ran.
Op Op
// Provider names the adapter that served it, if one did.
Provider string
// Page is 1-based, and zero for whole-document stages.
Page int
// Attempt is 1 for the first try.
Attempt int
// Duration is how long the stage took.
Duration time.Duration
// Err is non-nil if the stage failed. A failed stage still emits an event.
Err error
// Bytes read or produced by the stage.
Bytes int64
// Pages in the document.
Pages int
// Fields is a count. Not the names, and certainly not the values.
Fields int
// Usage is what the stage consumed.
Usage Usage
// Confidence is the aggregate, set on the final stage only.
Confidence float64
// Review reports whether the result needs a person, set on the final
// stage only.
Review bool
}
Event is one pipeline stage, reported as it completes.
There is deliberately no field an extracted value could occupy — no map[string]any, no Raw, no free-text note. A span attribute carrying a value would ship somebody's national ID number to an observability vendor they never heard of, and a rule saying "do not do that" gets violated the first time it is convenient. Field counts are here; field values are not representable. See docs/adr/0021-observability.md and docs/rules.md §7.5.
type Explanation ¶
type Explanation struct {
Field string
Value any
Found bool
Confidence float64
// Signals are every input to Confidence, with its weight and a one-line
// note.
Signals []Signal
// Provenance is where the value came from.
Provenance []Provenance
// Candidates holds competing readings, if any.
Candidates []Candidate
// Validation is each rule, whether it passed, and why not.
Validation []RuleResult
// Reasons is why this field needs review, if it does.
Reasons []ReviewReason
}
Explanation is the decomposition of one field's result.
A value rather than formatted text, because the consumers are review queues, audit stores, dashboards and JSON APIs, none of which want a string. The terminal rendering is Explanation.String.
It is assembled from what the pipeline already recorded, so it cannot disagree with the Result it came from.
func (*Explanation) String ¶
func (e *Explanation) String() string
String renders an Explanation for a person reading a terminal.
This format is not part of the compatibility promise. Anyone parsing it has taken a dependency that will break; the struct fields are the stable interface.
type FieldEvidence ¶
type FieldEvidence struct {
// Field is the field key.
Field string
// Value is what was extracted.
Value any
// Found reports whether the field was present at all.
Found bool
// Reading is which reading produced the value.
Reading Reading
// OCRConfidence is the mean confidence of the words backing the value, or
// nil when the value did not come from OCR.
OCRConfidence *float64
// Grounding is on 0..1: 1.0 verbatim, 0.8 normalised, 0.5 derived, 0.0 not
// found in the source.
Grounding float64
// Ambiguous reports whether a declared format parsed under more than one
// reading — 03/04/2026, which is a valid date in two conventions.
//
// It exists so the format signal can drop without going to zero. The text
// is a well-formed date; what is unknown is which date it is. Scoring that
// as a format failure would say the value is malformed, which is a
// different and wrong claim, and would rank it below values that are
// genuinely unparseable (docs/schema.md, "Ambiguous dates").
Ambiguous bool
// Provenance is where the value came from.
Provenance []Provenance
// Candidates holds competing values when two readings disagreed.
Candidates []Candidate
// Agreement is whether two independent readings produced the same value,
// or nil when only one reading ran.
//
// Nil rather than zero, because "no second opinion was taken" and "the
// second opinion differed" are opposite facts and scoring them alike would
// penalise every single-reading extraction (docs/confidence.md §Signals).
Agreement *float64
// AgreementNote says which, in one line, for the signal's Note.
AgreementNote string
// Validation is each rule and its outcome.
Validation []RuleResult
// Suspicious reports whether the source page carried content that looked
// like an injection attempt.
Suspicious bool
}
FieldEvidence is everything the pipeline collected about one field.
It is the input to a Scorer, and it is deliberately everything rather than a summary: a caller fitting a scorer to their own labelled documents should not be limited to the signals ovrin happened to think of.
type FieldResult ¶
type FieldResult struct {
// Value is what was extracted.
Value any
// Found reports presence.
//
// This is not Value != zero, and the distinction is the point: a payments
// system must be able to tell "the total is zero" from "we could not read
// the total".
//
// Found does not promise Value is usable. A field the model answered with
// something that could not be converted to its declared type is Found with
// a nil Value and Valid false, because "answered unusably" and "not
// answered at all" are different facts and both are worth having. Check
// Valid, or take Value through a comma-ok assertion, before using it.
Found bool
// Confidence is on 0..1.
Confidence float64
// Valid reports whether every rule on this field passed.
Valid bool
// Signals are the inputs that produced Confidence.
Signals []Signal
// Provenance is where the value came from.
Provenance []Provenance
// Candidates holds every competing value when two readings disagreed.
// Value holds the higher-confidence one, so a caller who ignores this
// still gets the better answer.
Candidates []Candidate
// Validation is each declared rule and whether it passed.
//
// Distinct from Errors: a rule that failed appears in both, but Validation
// also records the rules that passed, which is what makes a confidence
// score checkable by hand rather than merely reported.
Validation []RuleResult
// Errors says why the field is not Valid, or why it was not Found.
Errors []error
}
FieldResult is one field, and the evidence for it.
This type carries []error and so does not marshal usefully to JSON. Use Result.Explain for a value that does.
type Hook ¶
Hook receives an Event for each pipeline stage.
Hooks run synchronously on the calling goroutine. A hook that blocks slows the extraction and a hook that does I/O will, which is the caller's responsibility — making it asynchronous is one line in your own hook, and doing it here would hide ordering and leak a goroutine per client.
The core emits hooks and depends on nothing. OpenTelemetry lives in its own module, so a user who wants no telemetry carries none and a user on a different stack writes five lines instead of adopting OTel.
type Kind ¶
type Kind string
Kind is a document format.
It is always determined by content. A file named invoice.pdf that is actually a JPEG is common enough — mail systems rename things — that trusting the name is how a parser gets handed input it was not written for.
type Layout ¶
type Layout struct {
// Tables are in reading order, top to bottom then left to right.
Tables []Table
// Pairs are in reading order.
Pairs []Pair
}
Layout is the structure a provider recognised on one page: its tables and its key-value pairs.
One Layout per page, matching Recognition, so a provider that rasterises server-side and returns one recognition per page returns one of these per page too and nothing has to be re-split.
An empty Layout and no Layout are different facts. An empty one is a provider that looked and found no structure; no Layout at all is a provider that does not report structure. That is why Recognition.Layout is a pointer, and it is the whole reason this type exists rather than two slices on Recognition: a caller deciding whether to fall back to reading the page as prose needs to tell "there are no tables here" from "nobody looked".
Nothing in ovrin requires a provider to fill this in. A table detected and reported is a table the model is told about in the page content; a table nobody detected is prose, which still extracts, only with less to go on.
func (Layout) At ¶
At returns the cell r names, and whether there was one.
A position no cell covers returns false rather than a zero Cell, because a sparse table's empty position is a place the provider read nothing and an empty Cell would claim it read an empty string. Spans are honoured: a cell covering four positions is returned for all four.
func (Layout) Check ¶
Check reports the first structural mistake in the layout, or nil.
It is what an adapter's own tests run over the layouts they build from recorded provider responses, and it is the reason those tests can be shared: "the cells are inside the table, nothing overlaps, and every confidence is a probability" is the same requirement whichever provider produced them. An adapter that mapped a percentage without dividing it, or flattened a merged cell into two overlapping ones, fails here rather than in a confidence score nobody can explain six weeks later.
Every error wraps ErrBadResponse, because an incoherent layout is a response nothing can be done with. It is deliberately not three new sentinels of its own: ovrin has one error vocabulary and adding to it for one subsystem is how a caller ends up with two (ADR-0027). The message names which check failed and where.
The errors name page, table and cell indexes and nothing else. A cell's text is document content and never appears in one (rule §2.5, §7.5).
func (*Layout) Order ¶
func (l *Layout) Order()
Order puts a provider's output into the order the rest of ovrin assumes, and fills in the boxes a provider left empty.
It exists so that "cells are in reading order" is something an adapter achieves by calling one function rather than something each adapter reimplements — three implementations of reading order is three orders, and the difference only shows up on the documents nobody tested with. It is the counterpart of Layout.Check: Check says whether the structure is coherent, Order says how it is arranged.
The sorts are stable, so a provider that already emits a defensible order keeps it wherever this has no opinion. Order mutates the receiver's slices in place and does not copy: a Layout is built once by an adapter and handed on, and copying every cell to sort it would be the largest allocation on the path for no benefit.
type Metadata ¶
type Metadata struct {
// Readings is which readings were taken, in order.
Readings []Reading
// Providers names the adapter that served each stage, so a result carries
// the evidence of where its content went — which matters when a fallback
// chain means that was not decided in advance.
Providers map[Op]string
// Kind is the detected format.
Kind Kind
// Pages in the document.
Pages int
// Usage is the total across every provider call, the second attempt
// included when Retried is true.
Usage Usage
// Retried reports whether the model was asked a second time because its
// first reply was malformed — not JSON, or a value that could not be the
// type the field declares.
//
// It is not a warning. A retried extraction that came back clean is as
// good as one that never needed asking twice; what it tells you is that
// this provider, prompt or document produced a bad reply once, which is
// worth knowing in aggregate when choosing between providers.
Retried bool
// Duration is the wall time of the extraction.
Duration time.Duration
}
Metadata records how a result was produced.
type Model ¶
type Model interface {
Generate(ctx context.Context, req ModelRequest) (*ModelResponse, error)
}
Model produces structured JSON from document content.
One method, deliberately. Ovrin makes exactly one kind of call — given this content and this JSON Schema, return an object matching the schema — so a chat abstraction would make every adapter author implement messages, roles, tool calling and streaming that ovrin never uses.
Prompt construction stays on this side of the seam. An injected instruction in a document must not be able to reach a position where a model reads it as a directive, and that property holds identically across every provider only because the core builds the request. See docs/adr/0007-model-seam.md and docs/adr/0017-untrusted-document-content.md.
Implementations must be safe for concurrent use by multiple goroutines.
func BreakModel ¶
func BreakModel(m Model, opts ...BreakerOption) Model
type ModelRequest ¶
type ModelRequest struct {
// Instruction is built by ovrin from the schema. It never contains
// document content.
Instruction string
// Content is the untrusted material, already delimited and labelled.
Content []Content
// Schema is the JSON Schema the reply must satisfy, as bytes so an adapter
// can pass it to a provider verbatim.
//
// It is emitted fully expanded, with no $ref, additionalProperties false,
// and every property listed in required — the narrowest dialect the major
// providers agree on. A provider that still rejects it must surface
// [ErrBadRequest] naming the construct, never silently relax it.
Schema []byte
// Temperature is nil for the provider's default. Extraction wants
// determinism, so ovrin sets it low rather than leaving it unset.
Temperature *float64
}
ModelRequest is one extraction call.
Instruction and Content never mix. An adapter maps Instruction to the provider's system role and Content to the user role, or to the nearest equivalent; it never concatenates them.
type ModelResponse ¶
type ModelResponse struct {
// JSON is the raw reply. It is not unmarshalled by the adapter: a model
// returning invalid JSON must produce one ovrin error with the offending
// bytes attached, rather than a different error per provider.
JSON []byte
// Usage is what the call consumed.
Usage Usage
// Raw is the provider's own response, for callers willing to type-assert.
Raw any
}
ModelResponse is what a provider returned.
type OCR ¶
type OCR interface {
// Recognise reads one page.
Recognise(ctx context.Context, page Page) (*Recognition, error)
// Name identifies the provider, and appears in [Provenance.Method] so a
// result records which provider produced each value.
Name() string
}
OCR recovers text and layout from a rasterised page.
Recognition is not a string. Ovrin needs word positions for provenance and per-word confidence as a scoring signal, so a seam returning text alone would discard the inputs two other subsystems are built on.
Implementations must be safe for concurrent use by multiple goroutines.
func BreakOCR ¶
func BreakOCR(o OCR, opts ...BreakerOption) OCR
BreakOCR wraps an OCR so that a provider which is failing consistently is left alone for a while instead of being asked again on every page.
This is a decorator, not a change to OCRChain, for the reason ADR-0018 gives: fallback policy belongs outside the pipeline, so a caller who wants different policy writes it rather than arguing with ovrin about the built-in one. It composes:
ovrin.OCRChain(
ovrin.BreakOCR(primary),
ovrin.BreakOCR(secondary),
)
What it does ¶
After DefaultBreakerFailures consecutive failures the breaker opens and every call returns ErrUnavailable without contacting the provider, for DefaultBreakerCooldown. It then admits exactly one trial call: if that succeeds the breaker closes, and if it fails the cooldown starts again. One trial rather than all of them, because a provider that is still down should cost one request to discover, not a thundering herd of them.
A refusal is ErrUnavailable deliberately. That is a condition OCRChain advances on, so a chain of broken providers moves to the next one rather than stopping — which is the entire point of putting a breaker in a chain.
What it counts ¶
Only failures the provider is responsible for. ErrAuth, ErrBadRequest, ErrUnsupported and ErrSchema do not open a breaker: they will fail identically after any cooldown, so counting them would hide a misconfiguration behind a circuit-breaker message instead of surfacing it. Cancellation is the caller's, not the provider's, and is not counted either.
Every state change is reported through the hook a Client was built with. A breaker that opens silently is the failure ADR-0018 exists to prevent, one level down.
The returned OCR is safe for concurrent use.
func OCRChain ¶
OCRChain returns an OCR that tries each provider in order.
A chain is an ordinary provider, so the pipeline cannot tell the difference and no fallback logic lives in the core. That also means a caller who wants circuit breaking, weighted routing or cost-aware selection writes their own implementation and passes it to WithOCR.
It advances on ErrRateLimit, ErrUnavailable and unclassified transport errors. It never advances on ErrAuth, ErrBadRequest, ErrUnsupported or ErrSchema: a misconfigured credential should fail loudly on the first provider rather than silently degrade to the third.
Every attempt is reported through the hook, because the dangerous failure mode of fallback is a system running on its worst provider for three weeks with nobody aware. Exhausting the chain returns an error wrapping every attempt, not only the last.
type Op ¶
type Op string
Op names a stage of the extraction pipeline.
The same vocabulary is used by Error and by Event, so an operator reading a trace and a developer reading an error see the same word, and both can look it up in docs/pipeline.md. See docs/adr/0027-twelve-sentinels-and-one-op-vocabulary.md.
const ( // OpUnknown is the zero value. An Error or Event that does not know its // stage says so rather than claiming one. OpUnknown Op = "" OpDetect Op = "detect" OpAcquire Op = "acquire" OpRender Op = "render" OpOCR Op = "ocr" OpNormalise Op = "normalise" OpSchema Op = "schema" OpPrompt Op = "prompt" OpGenerate Op = "generate" OpValidate Op = "validate" OpGround Op = "ground" OpScore Op = "score" )
The pipeline stages, in the order a document passes through them.
OpRender and OpOCR happen within acquisition; they are named separately because a failure in either is worth distinguishing from a failure to choose a reading at all.
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option configures a Client, or a single Extract call.
The interface is closed — apply is unexported — so options cannot be implemented outside this package. That keeps the configuration surface a thing ovrin controls, and it keeps godoc honest: an exported func type over an unexported struct would render as func(*config), naming a type the reader cannot see.
func WithConcurrency ¶
WithConcurrency bounds page-level parallelism.
The default is min(4, GOMAXPROCS), so ovrin does not monopolise a host it shares. Model calls are not parallelised across pages: extraction needs the whole document.
func WithCrossField ¶
func WithCrossField(rules ...CrossFieldRule) Option
WithCrossField declares rules checked across fields after extraction.
They produce the SignalCrossField signal, and a failure sets NeedsReview with a reason naming the rule.
Rules are declared here rather than in a struct tag because a rule spans several fields and has no natural home on any one of them — and because ADR-0006 fixes the tag vocabulary at five rules, none of which could express "these three fields must add up".
func WithDateOrder ¶
WithDateOrder resolves ambiguous numeric dates for a corpus whose convention you know. Without it they are flagged rather than guessed.
func WithHook ¶
WithHook sets a function called once per pipeline stage.
Hooks run synchronously on the calling goroutine.
func WithMaxDecompressedBytes ¶
WithMaxDecompressedBytes bounds decompressed output across the document.
func WithMaxDepth ¶
WithMaxDepth bounds recursion through the document's object graph.
func WithMaxObjects ¶
WithMaxObjects bounds the number of objects in a document.
func WithMaxPagePixels ¶
WithMaxPagePixels bounds the size of one rasterised page.
func WithMaxReplacementRatio ¶
WithMaxReplacementRatio sets the maximum proportion of U+FFFD characters a usable text layer may contain.
func WithMaxSourceBytes ¶
WithMaxSourceBytes bounds the source document.
func WithMaxStreamBytes ¶
WithMaxStreamBytes bounds decompressed output from any single stream.
func WithMaxTextBytes ¶
WithMaxTextBytes bounds extracted text.
func WithMinDecodableRatio ¶
WithMinDecodableRatio sets the minimum proportion of decodable characters a usable text layer must contain.
func WithMinTextDensity ¶
WithMinTextDensity sets the minimum characters per square inch for a text layer to be considered usable.
func WithModel ¶
WithModel sets the model that turns document content into structured JSON. Required.
func WithOCR ¶
WithOCR sets the provider used for pages with no usable text layer.
Without one, a scanned document reaches a vision-capable model if there is one, and otherwise fails with ErrNoProvider.
func WithReading ¶
func WithReading(m ReadingMode) Option
WithReading selects how a document is read.
The default, ReadingAuto, tries the text layer first because when it works it is exact and nearly free. ModeBoth runs two independent readings and compares them, which roughly doubles cost and is the strongest quality signal available.
func WithRenderer ¶
WithRenderer sets the renderer used to rasterise pages for OCR.
Not needed for images, nor for a DocumentOCR provider that accepts a PDF and rasterises server-side.
func WithReviewThreshold ¶
WithReviewThreshold sets the field confidence below which a result is flagged for review.
func WithScorer ¶
WithScorer replaces the confidence scorer.
The default is a weighted mean over the signals that applied, with hard floors. A caller with labelled documents can fit a better one to their own corpus; the consequence is that confidence is then comparable within that deployment rather than across organisations.
type Page ¶
type Page struct {
// Number is 1-based.
Number int
// Image is the rasterised page.
Image image.Image
// Width and Height are the page size in points, which is what lets a
// provider return coordinates in points regardless of the DPI it was
// given.
Width float64
Height float64
// DPI is the resolution the page was rendered at.
DPI int
}
Page is one rasterised page, handed to an OCR provider.
type Pair ¶
type Pair struct {
// Page is 1-based.
Page int
// Key is the label.
Key Region
// Value is what the label labels.
//
// Its Text may be empty: a form field the provider found and read nothing
// in is a fact about the document, not an absent pair. Dropping it would
// turn "this box was blank" into "there is no such box".
Value Region
// Confidence is the provider's own for the association, on 0..1.
//
// It is about the pairing and not about either region's text. A provider
// that reports confidence per region and none for the pairing leaves this
// zero rather than averaging two numbers that mean something else.
Confidence float64
}
Pair is a label and the thing it labels — a form field, in effect.
func (Pair) Box ¶
Box is the region a review interface highlights for the whole pair.
A label and the value it labels are routinely on different lines — that separation is what makes a pair worth reporting rather than five words in reading order — so the pair's region is the union of the two and not either one of them.
type Provenance ¶
type Provenance struct {
// Reading is which reading produced the value.
Reading Reading
// Page is 1-based.
Page int
// Box is the region on the page, or nil if the reading gave no geometry.
// Always present for OCR, usually for the text layer, rarely for vision.
Box *Rect
// Span is the range in the normalised text, or nil if unknown.
Span *Span
// Method names the reading and the provider that served it, as
// "text-layer", "ocr:tesseract" or "vision:gpt-5.2".
Method string
// Exact reports whether the value appears verbatim in the source, rather
// than having been reformatted or derived.
Exact bool
}
Provenance records where a value came from.
It is what makes human review practical — an interface can highlight the region instead of making a reviewer search a 40-page scan — and it is what grounding is built on. It cannot be reconstructed after extraction, which is why it is always collected rather than being an option.
A nil Box or Span means the position is not known, never that the value is not in the document. Some values are legitimately not groundable: a total computed from line items, a date normalised from prose.
type Reading ¶
type Reading string
Reading is how a value was actually read. It describes the past, and appears on Provenance and Candidate.
A value is read by exactly one reading. Requesting more than one is a different type, ReadingMode, so that a Provenance claiming two readings at once is not representable. See docs/adr/0028-reading-and-readingmode.md.
const ( // ReadingUnknown is the zero value, for a value whose origin was not // recorded. It is never a claim that the origin does not exist. ReadingUnknown Reading = "" // ReadingText is a PDF's own text layer: exact, and nearly free. ReadingText Reading = "text" // ReadingOCR is optical character recognition of a rasterised page. ReadingOCR Reading = "ocr" // ReadingVision is a multimodal model reading a page image. ReadingVision Reading = "vision" )
type ReadingMode ¶
type ReadingMode string
ReadingMode selects how a document should be read. It describes an intention, and is the argument to WithReading.
Distinct from Reading because ModeBoth has no meaning as a record of what happened: two readings produce two Candidate values, not one Provenance claiming both.
const ( // ReadingAuto tries the text layer, then OCR, then vision, using the first // that can serve the page. It is the default, and it is what most callers // want: cost scales with document difficulty rather than document count. ReadingAuto ReadingMode = "auto" // ModeText uses the text layer only, and fails rather than falling back. ModeText ReadingMode = "text" // ModeOCR rasterises and recognises, even where a text layer exists. ModeOCR ReadingMode = "ocr" // ModeVision sends page images to the model. ModeVision ReadingMode = "vision" // ModeBoth runs two independent readings and compares them field by field. // Roughly doubles cost and latency, which is why it is not the default; // disagreement between readings is the strongest signal ovrin has that a // value should not be trusted. ModeBoth ReadingMode = "both" )
type Recognition ¶
type Recognition struct {
// Words are in reading order.
Words []Word
// Lines group the words by baseline.
Lines []Line
// Confidence is the provider's own, over the whole page, on 0..1.
Confidence float64
// Language is the detected language, or empty if the provider does not
// report one.
Language string
// Layout is the structure the provider recognised on this page — its
// tables and key-value pairs — or nil for a provider that does not report
// structure.
//
// The pointer is load-bearing. An empty Layout is a provider that looked
// and found nothing; nil is a provider that does not look. A caller
// deciding whether to treat the page as a table or as prose needs to tell
// those apart, and a plain slice cannot say it.
Layout *Layout
// Raw is the provider's own response, for callers willing to type-assert.
// Providers that detect tables or key-value pairs expose them here; ovrin
// itself uses words and lines.
Raw any
// Usage is what recognising this page consumed.
//
// OCR providers bill per page rather than per token, and without a place
// to report that the cost of a reading cannot reach [Metadata.Usage] or a
// metric at all: the seam would be the one stage of the pipeline whose
// spend is invisible. A provider that does not meter a request leaves this
// zero rather than guessing a page count.
Usage Usage
}
Recognition is what an OCR provider read from one page.
Every implementation normalises to this shape: coordinates in page points with the origin top left, confidence on 0..1, and words in reading order rather than in whatever order the provider's API returned them.
type Rect ¶
Rect is a region of a page, in points, with the origin at the top left.
That origin is neither PDF's (bottom left) nor an image format's (pixels, top left). One convention had to be chosen and adapters normalise to it, so the confidence engine and any review interface are written against one shape.
type Ref ¶
type Ref struct {
// Page is 1-based; Table is the index within the page's Layout; Row and
// Column are 0-based, as on [Cell].
Page int
Table int
Row int
Column int
}
Ref locates one cell by position alone.
It is the loggable form of a claim about a table: "page 4, table 1, row 3, column 2" says which value is meant without repeating the value, so it can go in a provenance entry, an event, or a review interface under the rule that document content never reaches any of them (rule §2.5, §7.5).
type Region ¶
type Region struct {
// Text is what the provider read, unnormalised. It is document content;
// see [Cell.Text].
Text string
// Box is the region on the page. A zero Rect means no geometry.
Box Rect
// Confidence is the provider's own, on 0..1.
Confidence float64
}
Region is a run of recognised text and where it was.
type Renderer ¶
type Renderer interface {
Render(ctx context.Context, doc Document, page, dpi int) (image.Image, error)
}
Renderer rasterises a document page to an image, so it can be given to an OCR provider or a vision model.
There is no default implementation, and that is the hardest constraint in the project: rasterising PDF means implementing a large part of the PDF imaging model, and nobody has done it well in pure Go. The recommended implementation runs PDFium as WebAssembly, which needs no cgo and cross-compiles, at the cost of speed. A renderer using cgo must say so on the first line of its package documentation.
Most extractions never need one. A PDF with a text layer needs no rasterising, an image is already an image, and a DocumentOCR provider rasterises server-side. See docs/adr/0010-no-cgo-in-core.md.
Implementations must be safe for concurrent use by multiple goroutines.
type Result ¶
type Result[T any] struct { // Data holds every field that was read, whether or not Valid. // // A field that could not be read is left at its zero value here and marked // absent in Fields. Nothing is ever guessed to satisfy the struct. Data T // Valid reports whether every validation rule passed. Valid bool // Confidence is the aggregate over fields, weighted by whether a field is // required, so a missing optional field does not drag down a clean // document. // // Until the weights are calibrated this is a ranking signal, not a // probability. It orders a review queue well; it does not mean "correct // this often". Confidence float64 // Fields holds one entry per schema field, including fields that were not // found. // // Keys are the Go field path in snake case, not the description from the // ovrin tag: a field declared UnitPrice float64 `ovrin:"price per unit"` // is Fields["unit_price"]. The tag describes the field to the model and is // free to be reworded; the Go name is the caller's own identifier and does // not change under them when a description is improved. // // A nested struct is keyed with a dot ("vendor.name") and a slice field // additionally contributes one entry per extracted element ("items[0]"), // so the number of keys depends on what was read. See docs/schema.md. Fields map[string]FieldResult // NeedsReview reports whether a person should look before this is used. NeedsReview bool // Reasons says why, one entry per triggering condition. Reasons []ReviewReason // Metadata records how the result was produced. Metadata Metadata }
Result is what an extraction produced.
The error returned alongside it and the Valid field answer different questions. A non-nil error means nothing usable came back and Result is nil. Valid reports whether every validation rule passed, which is independent: data can come back that is real, useful and not yet good enough to act on.
See docs/adr/0004-partial-results.md.
func Extract ¶
Extract reads src and returns it as a T.
T is a struct whose fields carry `ovrin:"…"` tags describing what to extract. Reflection over it happens once per Client per type, and a malformed schema is ErrSchema raised before any provider is contacted, so a typo costs nothing.
opts override the Client's configuration for this call only; the Client is not modified, so concurrent extractions with different options cannot interfere. Options that configure a provider — WithModel, WithOCR, WithRenderer, WithHook — are rejected here rather than silently ignored.
A non-nil error means nothing usable came back and the Result is nil. It does not mean the data is good: that is Result.Valid, and the two are independent.
res, err := ovrin.Extract[Invoice](ctx, c, ovrin.File("invoice.pdf"))
if err != nil {
return err
}
if !res.Valid || res.NeedsReview {
return review.Queue(res)
}
return ledger.Post(res.Data)
Example ¶
Extracting an invoice from an image.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"image"
"image/color"
"image/png"
"github.com/BAGOMBEKA-JOB-DEV/ovrin"
)
// Invoice is the schema used throughout these examples.
type Invoice struct {
Number string `ovrin:"invoice number,required"`
Vendor string `ovrin:"vendor company name"`
Currency string `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
Total float64 `ovrin:"total amount including tax,required,min=0"`
}
// fakeModel returns a fixed reply, so the examples are deterministic and run
// offline. A real programme passes ovrinskyl.OpenAI(key, model) — the interface
// is the same either way, which is the point of the seam.
type fakeModel struct{ reply map[string]any }
func (m fakeModel) Generate(context.Context, ovrin.ModelRequest) (*ovrin.ModelResponse, error) {
b, _ := json.Marshal(m.reply)
return &ovrin.ModelResponse{JSON: b, Usage: ovrin.Usage{InputTokens: 812, OutputTokens: 46}}, nil
}
// fakeOCR returns fixed words with positions, standing in for a real provider.
// It matters that it returns *text*: grounding checks an extracted value
// against the document it came from, and a vision reading has no source text
// to check against.
type fakeOCR struct{ words []string }
func (fakeOCR) Name() string { return "example" }
func (o fakeOCR) Recognise(context.Context, ovrin.Page) (*ovrin.Recognition, error) {
rec := &ovrin.Recognition{Confidence: 0.97}
x := 10.0
for i, w := range o.words {
rec.Words = append(rec.Words, ovrin.Word{
Text: w,
Box: ovrin.Rect{MinX: x, MinY: 100, MaxX: x + 40, MaxY: 112},
Confidence: 0.97,
Line: 0,
})
x += 45
_ = i
}
return rec, nil
}
// pngBytes is a tiny valid PNG, so the examples have a real image source.
func testPNG() []byte {
m := image.NewRGBA(image.Rect(0, 0, 4, 4))
m.Set(0, 0, color.RGBA{A: 255})
var buf bytes.Buffer
_ = png.Encode(&buf, m)
return buf.Bytes()
}
func main() {
c := ovrin.New(
ovrin.WithModel(fakeModel{reply: map[string]any{
"number": "INV-2026-0417", "vendor": "Kampala Supplies Ltd",
"currency": "UGX", "total": 2500000.0,
}}),
ovrin.WithOCR(fakeOCR{words: []string{"INV-2026-0417", "Kampala", "Supplies", "Ltd", "UGX", "2,500,000"}}),
)
res, err := ovrin.Extract[Invoice](context.Background(), c, ovrin.Bytes(testPNG()))
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(res.Data.Number)
fmt.Println(res.Data.Currency, res.Data.Total)
fmt.Println("valid:", res.Valid)
}
Output: INV-2026-0417 UGX 2.5e+06 valid: true
func (*Result[T]) Explain ¶
func (r *Result[T]) Explain(field string) (*Explanation, bool)
Explain returns the decomposition of one field, and whether that field exists in the schema.
The key is the field path as it appears in Result.Fields: "total", "vendor.name", "items[0].unit_price".
Example (Fabrication) ¶
A value the model invented is caught, because it appears nowhere in the document it was supposedly read from. This is the failure ovrin exists to notice: the value is well formed, it satisfies every rule, and it is wrong.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"image"
"image/color"
"image/png"
"github.com/BAGOMBEKA-JOB-DEV/ovrin"
)
// Invoice is the schema used throughout these examples.
type Invoice struct {
Number string `ovrin:"invoice number,required"`
Vendor string `ovrin:"vendor company name"`
Currency string `ovrin:"currency code,required,enum=UGX|USD|EUR|GBP"`
Total float64 `ovrin:"total amount including tax,required,min=0"`
}
// fakeModel returns a fixed reply, so the examples are deterministic and run
// offline. A real programme passes ovrinskyl.OpenAI(key, model) — the interface
// is the same either way, which is the point of the seam.
type fakeModel struct{ reply map[string]any }
func (m fakeModel) Generate(context.Context, ovrin.ModelRequest) (*ovrin.ModelResponse, error) {
b, _ := json.Marshal(m.reply)
return &ovrin.ModelResponse{JSON: b, Usage: ovrin.Usage{InputTokens: 812, OutputTokens: 46}}, nil
}
// fakeOCR returns fixed words with positions, standing in for a real provider.
// It matters that it returns *text*: grounding checks an extracted value
// against the document it came from, and a vision reading has no source text
// to check against.
type fakeOCR struct{ words []string }
func (fakeOCR) Name() string { return "example" }
func (o fakeOCR) Recognise(context.Context, ovrin.Page) (*ovrin.Recognition, error) {
rec := &ovrin.Recognition{Confidence: 0.97}
x := 10.0
for i, w := range o.words {
rec.Words = append(rec.Words, ovrin.Word{
Text: w,
Box: ovrin.Rect{MinX: x, MinY: 100, MaxX: x + 40, MaxY: 112},
Confidence: 0.97,
Line: 0,
})
x += 45
_ = i
}
return rec, nil
}
// pngBytes is a tiny valid PNG, so the examples have a real image source.
func testPNG() []byte {
m := image.NewRGBA(image.Rect(0, 0, 4, 4))
m.Set(0, 0, color.RGBA{A: 255})
var buf bytes.Buffer
_ = png.Encode(&buf, m)
return buf.Bytes()
}
func main() {
c := ovrin.New(
// The document says 2,500,000. The model reports 9,999,999.
ovrin.WithModel(fakeModel{reply: map[string]any{
"number": "INV-2026-0417", "vendor": "Kampala Supplies Ltd",
"currency": "UGX", "total": 9999999.0,
}}),
ovrin.WithOCR(fakeOCR{words: []string{"INV-2026-0417", "Kampala", "Supplies", "Ltd", "UGX", "2,500,000"}}),
)
res, _ := ovrin.Extract[Invoice](context.Background(), c, ovrin.Bytes(testPNG()))
total := res.Fields["total"]
fmt.Printf("value %.0f\n", total.Value)
fmt.Printf("confidence %.2f\n", total.Confidence)
fmt.Println("review:", res.NeedsReview)
for _, r := range res.Reasons {
if r.Field == "total" {
fmt.Println("reason:", r.Why)
}
}
// The cap is a ceiling, not a floor: grounding contributes 0.0 at weight
// 0.30 and the rules contribute 1.0 at weight 0.15, so the mean is already
// 0.33 and CapUngrounded never has to bind.
//
Output:
type ReviewReason ¶
type ReviewReason struct {
// Field is the field key, as in [Result.Fields].
Field string
// Why is a short cause, such as "value not found in source; may be
// inferred" or "readings disagree".
Why string
}
ReviewReason names a field that needs a person, and why.
It carries a field name and a cause, never the value: a review queue is a system that stores things, and document values should reach it because the caller decided to put them there, not because ovrin leaked them in a reason string.
type RuleResult ¶
type RuleResult struct {
// Rule is the rule as written in the tag: "required", "min=0",
// "format=date".
Rule string
// Passed reports the outcome.
Passed bool
// Message says why not, and is empty when Passed.
Message string
}
RuleResult is one validation rule and its outcome.
Message is a string rather than an error so that an Explanation marshals to JSON — which is the point of Result.Explain returning data rather than formatted text.
type Scorer ¶
type Scorer interface {
// Score returns the confidence and the signals that produced it. The
// signals must account for the score: a caller must be able to check the
// arithmetic.
Score(f FieldEvidence) (confidence float64, signals []Signal)
}
Scorer combines evidence into a confidence score.
Pluggable because a user with labelled documents can fit a better scorer to their own corpus than any default will manage. The consequence is that confidence is comparable within a deployment, not across organisations.
type Signal ¶
type Signal struct {
// Name is one of the Signal* constants.
Name string
// Value is on 0..1.
Value float64
// Weight is this signal's share of the score, after redistribution across
// the signals that actually applied.
Weight float64
// Note says why, in one line: "found verbatim, page 1", "12 backing words,
// mean 0.97".
Note string
}
Signal is one named input to a confidence score.
Every score decomposes into these. No number is produced that a caller cannot take apart, because a confidence figure nobody can interrogate is a figure nobody should act on.
type Source ¶
type Source interface {
// contains filtered or unexported methods
}
Source is an unread document.
The interface is closed — it has an unexported method — so the only Sources are the ones Reader, Bytes and File return. An open interface would let a caller supply something no pipeline stage knows how to read, turning a compile-time error into a runtime one.
func Bytes ¶
Bytes returns a Source reading from b.
The slice is not copied and must not be modified until Extract returns.
func File ¶
File returns a Source reading the file at path.
Opening is deferred to Extract, so a missing file surfaces as an extraction error alongside every other failure rather than needing a separate check.
func Reader ¶
Reader returns a Source reading from r.
This is the primary constructor: a document usually arrives as a stream — an upload, a network body — and buffering it before ovrin can check it against the source-size limit would defeat the limit.
The reader is consumed once. If it is an io.Closer, ovrin does not close it; that is the caller's, since the caller opened it.
type Span ¶
Span is a byte range into the normalised text.
Bytes rather than runes: Go strings are bytes, converting to []rune costs a copy, and every caller would convert back.
type Table ¶
type Table struct {
// Page is 1-based, matching [Line.Page].
Page int
// Box is the table's region on the page. A zero Rect means the provider
// gave no geometry for the table as a whole.
Box Rect
// Rows and Columns are the table's declared size, as the provider counted
// it rather than derived from Cells: a table whose last row is empty still
// has that row, and deriving the size would silently lose it.
Rows int
Columns int
// Cells are in reading order, by row then by column.
//
// A table with merged cells has fewer entries than Rows*Columns, and a
// sparse one fewer still. A position no cell covers is a position the
// provider read nothing at, which is a fact about the document and not a
// gap to be filled with an empty string.
Cells []Cell
// Confidence is the provider's own for the table as a whole, on 0..1.
Confidence float64
}
Table is one table a provider found.
func (Table) At ¶
At returns the cell covering a grid position within the table, spans included, and whether there was one.
The second result is false when nothing covers the position, which is the honest answer for a sparse table: an empty Cell would be indistinguishable from a cell the provider read as empty, and the two are different facts about the document.
It is a linear scan of Cells rather than a materialised grid, because a table's declared size is the provider's number and building a grid from it would allocate for a table declaring two billion rows. Layout.Check is where that number is bounded before anything is allocated from it.
type Tolerance ¶
type Tolerance struct {
// Absolute is the largest acceptable difference in the values' own units.
Absolute float64
// Relative is the largest acceptable difference as a fraction of the
// larger value: 0.005 is half a percent.
Relative float64
}
Tolerance bounds how far two amounts may differ and still be consistent.
Both an absolute and a relative bound, because neither alone is right: an absolute cent covers rounding on one line and fails across a hundred, and a relative fraction is meaningless near zero.
type Usage ¶
Usage counts what an extraction consumed.
Tokens are what models bill; page units are what OCR providers bill. Both are here because a pipeline that uses both should be costable from one value.
type Word ¶
type Word struct {
Text string
// Box is in page points, origin top left.
Box Rect
// Confidence is on 0..1. A provider that reports no per-word confidence
// sets the page confidence here and records that it did, rather than
// fabricating 1.0.
Confidence float64
// Line indexes into [Recognition.Lines].
Line int
}
Word is one recognised word.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package eval measures extraction quality against a committed corpus.
|
Package eval measures extraction quality against a committed corpus. |
|
corpusgen
command
Command corpusgen writes the seed evaluation corpus.
|
Command corpusgen writes the seed evaluation corpus. |
|
schema
Package schema holds the Go structs each corpus category is extracted against.
|
Package schema holds the Go structs each corpus category is extracted against. |
|
examples
|
|
|
receipt
module
|
|
|
internal
|
|
|
adaptertest
Package adaptertest is the contract suite every ovrin adapter must pass.
|
Package adaptertest is the contract suite every ovrin adapter must pass. |
|
compare
Package compare answers one question: are these two values, read independently, the same value?
|
Package compare answers one question: are these two values, read independently, the same value? |
|
detect
Package detect identifies a document's format by content and enforces every resource limit before a byte of it is allocated.
|
Package detect identifies a document's format by content and enforces every resource limit before a byte of it is allocated. |
|
ground
Package ground searches the normalised text for an extracted value and produces the grounding confidence signal.
|
Package ground searches the normalised text for an extracted value and produces the grounding confidence signal. |
|
img
Package img decodes an image source into pages the pipeline can read.
|
Package img decodes an image source into pages the pipeline can read. |
|
jsonschema
Package jsonschema turns an internal schema.Schema into the JSON Schema bytes that cross the Model seam.
|
Package jsonschema turns an internal schema.Schema into the JSON Schema bytes that cross the Model seam. |
|
normalise
Package normalise turns raw positioned page content into one text stream while keeping a mapping from every output byte back to where it came from.
|
Package normalise turns raw positioned page content into one text stream while keeping a mapping from every output byte back to where it came from. |
|
office
Package office reads the text of a DOCX, XLSX or CSV, with line structure but deliberately without invented geometry.
|
Package office reads the text of a DOCX, XLSX or CSV, with line structure but deliberately without invented geometry. |
|
pdf
Package pdf reads the text layer of a PDF, with a box for every word.
|
Package pdf reads the text layer of a PDF, with a box for every word. |
|
prompt
Package prompt builds the model request, and is the boundary between text recovered from an attacker-controlled file and a system that follows instructions.
|
Package prompt builds the model request, and is the boundary between text recovered from an attacker-controlled file and a system that follows instructions. |
|
retry
Package retry constructs the one follow-up request ovrin makes when a model returns a reply that does not satisfy the schema.
|
Package retry constructs the one follow-up request ovrin makes when a model returns a reply that does not satisfy the schema. |
|
sandbox
Package sandbox serves a provider's wire protocol over a real socket, adversarially, with no credential and no cost.
|
Package sandbox serves a provider's wire protocol over a real socket, adversarially, with no credential and no cost. |
|
schema
Package schema turns a Go struct into the description of what to extract.
|
Package schema turns a Go struct into the description of what to extract. |
|
validate
Package validate converts extracted values into their Go types and checks them against the rules a schema declares.
|
Package validate converts extracted values into their Go types and checks them against the rules a schema declares. |
|
model
|
|
|
skyl
module
|
|
|
ocr
|
|
|
azure
module
|
|
|
google
module
|
|
|
tesseract
module
|
|
|
textract
module
|
|
|
otel
module
|
|
|
render
|
|
|
pdfium
module
|