BAGOMBEKA-JOB-DEV/ovrin

Turn documents into structured data. Ovrin is a Go library that reads PDFs, scans and images and returns a typed Go struct — with per-field confidence, a record of where every value came from, and an explicit signal when a human should look at it

Go

1

93 commits

updated Sep 30, 2026

See the code

README

ovrin

CI Go Reference Go Report Card License: Apache 2.0

go get github.com/BAGOMBEKA-JOB-DEV/ovrin@v1.0.0

Package: github.com/BAGOMBEKA-JOB-DEV/ovrin · v1.0.0 · Go 1.22+ · zero dependencies · no cgo

Turn documents into structured data.

Ovrin is a Go library that reads PDFs, scans and images and returns a typed Go struct — with per-field confidence, a record of where every value came from, and an explicit signal when a human should look at it.

Define what you want:

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"`
}

Ask for it:

res, err := ovrin.Extract[Invoice](ctx, client, ovrin.File("invoice.pdf"))
if err != nil {
    return err
}

fmt.Println(res.Data.Total)        // 2500.00, a float64
fmt.Println(res.Confidence)        // 0.96
fmt.Println(res.NeedsReview)       // false

Ovrin handles the rest: detecting the format, reading the text layer, rasterising and running OCR when there isn't one, normalising the content, constraining the model to your schema, validating the result, checking that every value actually appears in the document, and scoring what it found.


Why ovrin

Typed, not map[string]anyres.Data.Total is a float64 at compile time. Rename a field and the compiler finds every use.
A pipeline, not a promptText layer first, OCR on demand, vision as a distinct reading. Staged extraction measurably beats handing a model raw pages.
Confidence you can decomposeEvery score breaks down into named signals. No number is produced that you cannot take apart.
Every value points backPage, bounding box and source span for each field. Review interfaces can highlight; auditors can check.
Fabrication is detectedValues that appear nowhere in the document are flagged, not returned as fact.
Provider independentThree small interfaces. Bring OpenAI, Anthropic, Gemini, Tesseract, Textract, Ollama, or your own.
Zero dependencies in the corego get pulls nothing. No cgo. Cross-compiles and builds static.
Untrusted input by defaultDocuments are parsed with finite limits and prompted as data, never as instruction.

What it is not

Ovrin is not "send a PDF to a model and get JSON back". That takes an afternoon, costs more, and is measurably less accurate on real documents. It also cannot tell you how confident to be, where a value came from, or whether the model invented it — which is the part that matters when the extracted number is a payment.


Install

go get github.com/BAGOMBEKA-JOB-DEV/ovrin

The core has no external dependencies. You add exactly the providers you use, and nothing else enters your go.sum:

go get github.com/BAGOMBEKA-JOB-DEV/ovrin/model/skyl      # OpenAI, Anthropic, Gemini, Ollama, …
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/ocr/tesseract   # local OCR
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/ocr/google      # Cloud Vision / Document AI
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/render/pdfium   # rasterise scanned PDFs, no cgo
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/otel            # OpenTelemetry

Go 1.22 or newer. (That is a language floor. For the toolchain to build with, see SECURITY.md.)

Development

Every command in this project is a make target. Nothing is hidden in a script or a CI file — the Makefile is the single definition, and CI calls these same targets, so a green run on your machine is a green run on the build.

Setting up

git clone https://github.com/BAGOMBEKA-JOB-DEV/ovrin.git
cd ovrin

make setup      # commit sign-off hook + golangci-lint and govulncheck at CI's versions
make check      # the whole gate, across all nine modules

That is the entire setup. No credentials are needed to build or test — the default suite runs against in-process fakes and loopback servers, offline (ADR-0022).

Run make with no arguments at any time to list every target.

The two you will use most

CommandWhat it does
make checkThe gate to pass before opening a pull request: gofmt, build, go vet under every build tag, tests with the race detector, tests over real sockets, go mod tidy, golangci-lint, govulncheck, documentation checks — for every module.
make ciEverything above plus what only CI used to do: the coverage floor, the zero-dependency assertion, and the cgo-free cross-compile.

Every target

Getting started

CommandWhat it does
make / make helpList every target
make setupInstall the sign-off hook and both tools
make hooksJust the Signed-off-by commit hook
make toolsJust golangci-lint and govulncheck, at the versions CI pins

Build and test

CommandWhat it does
make buildCompile every module
make testThe offline suite, with the race detector
make test-sandboxThe same over real sockets, against an adversarial fake server
make test-coverThe suite with a coverage profile — what CI runs
make cover-floorAssert coverage is at or above 85%
make cover-htmlOpen the coverage profile in a browser
make benchBenchmarks (render/pdfium)
make fuzzEvery fuzz target; FUZZTIME=5m make fuzz to run longer
make test-integrationAgainst real providers. Costs money
make evalAccuracy against the corpus. Needs OPENAI_API_KEY, costs money

Quality — each is one step of CI

CommandWhat it does
make fmtFormat every module
make fmt-checkFail if anything is not gofmt'd
make vetgo vet under every build tag
make lintgolangci-lint
make actionsValidate the GitHub Actions workflow files
make vulngovulncheck
make tidy / make tidy-checkgo mod tidy; the check fails if it left a diff
make deps-checkAssert the core has zero external dependencies
make crossAssert it builds with CGO_ENABLED=0 for linux/arm64, darwin/arm64, windows/amd64

Documentation and generated files

CommandWhat it does
make docsCheck links, citations, ADR hygiene and API references
make apiRegenerate api/ovrin.txt from the source
make corpusRegenerate the synthetic evaluation corpus
make reportRegenerate the committed no-run evaluation report

Running and releasing

CommandWhat it does
make run-exampleExtract the example receipt with a real model. Needs OPENAI_API_KEY
make release-check VERSION=v1.0.0Report whether the tree is fit to tag. Never tags, never pushes. Takes a module-prefixed tag too, e.g. model/skyl/v1.0.0
make cleanRemove build and coverage output

Docker — the toolchain pinned, nothing to install

CommandWhat it does
make docker-buildBuild the image
make docker-ciThe whole gate, in a container
make docker-shellA shell with the toolchain, your checkout mounted
make docker-testJust the test suites
make docker-test-offlineThe suite with --network=none, proving it needs no network
make docker-exampleThe receipt example. Pass OPENAI_API_KEY through
make docker-evalThe evaluation harness, with eval/report mounted back out
make docker-cleanRemove the images

The container is worth knowing about for two specific reasons.

It ships Tesseract's English language data, so the six engine-backed tests in ocr/tesseract that skip on a machine without a language pack actually run there.

And it pins the Go toolchain. make vuln reports vulnerabilities in the standard library of whichever Go you are running, not only in ovrin — so on an older toolchain it fails with a long list that no change to this repository can fix. If that happens, either upgrade Go or run make docker-ci, which uses a pinned modern one. See SECURITY.md for why the go 1.22 in go.mod is a language floor and not a claim that 1.22.0 is safe to run this on.

Working on one module

The repository is nine Go modules. Every target loops over all of them; pass MODULES to narrow it, which is exactly how CI's matrix invokes them:

make test MODULES=ocr/azure
make build MODULES="ocr/azure ocr/textract"

Environment variables

None are needed for make check. These matter only for the targets that contact a real provider:

VariableUsed byNotes
OPENAI_API_KEYmake run-example, make evalRequired by both; they refuse to start without it
OPENAI_BASE_URLmake evalDefaults to https://api.openai.com/v1
OVRIN_MODELmake run-exampleDefaults to gpt-5.2
OVRIN_EVAL_MODELmake evalDefaults to gpt-5.2

Adapters never read the environment themselves — every credential is a function argument (rules.md §6.4). The variables above are read by the example programme and the evaluation harness, which are programmes rather than library code.

Inputs

Inputv0.1How
PDF with a text layeryesread directly — exact and nearly free
PNG, JPEG, TIFFyesOCR or vision
Scanned PDFyes, via cloud OCRproviders that accept a PDF rasterise server-side
Scanned PDF, offlineyesrender/pdfium rasterises locally, ocr/tesseract reads — neither needs cgo or a network
DOCX, XLSX, CSVyesread directly; no OCR and no renderer

Document types are never hardcoded. Invoices, receipts, government forms, transcripts, bank statements, medical forms and contracts are all the same mechanism — you write a struct.


Reading a result

type Result[T any] struct {
    Data        T                        // typed, partially populated
    Valid       bool                     // every validation rule passed
    Confidence  float64
    Fields      map[string]FieldResult   // one per schema field
    NeedsReview bool
    Reasons     []ReviewReason
    Metadata    Metadata
}

err != nil means nothing usable came back. It does not mean the data is good — that is Valid. A field that could not be read is marked absent and is never filled with a zero value, because a payments system must be able to tell "the total is zero" from "we could not read the total".

res, err := ovrin.Extract[Invoice](ctx, client, ovrin.File("invoice.pdf"))
if err != nil {
    return err                              // unreadable, no provider, limit hit
}
if !res.Valid || res.NeedsReview {
    return review.Queue(res)                // usable, but not automatically
}
return ledger.Post(res.Data)

Ask why:

e, _ := res.Explain("total")
fmt.Println(e)
Field:       total
Value:       2500.00
Confidence:  0.99

Signals
  grounding    1.00  ×0.30   found verbatim, page 1
  ocr          0.97  ×0.20   12 backing words, mean 0.97
  schema       1.00  ×0.15   float64, min=0 satisfied
  cross_field  1.00  ×0.05   line items sum to total
  format       1.00  ×0.05   parsed as currency
  agreement       —          only one reading

Provenance
  ocr:tesseract   page 1   box (412,688)-(486,702)   exact

Validation
  required  pass
  min=0     pass

Documentation

DocumentWhat it covers
Getting startedFirst extraction, end to end
The ideaThe problem, the goals, and the non-goals
ArchitectureModules, seams, and which way the arrows point
PipelineAll nine stages in detail
SchemasThe tag grammar and the rule vocabulary
ConfidenceSignals, weights, and what the number does not mean
ExplainabilityProvenance, review, and audit
ObservabilityHooks, spans and metric names — all of them API
Threat modelPrompt injection, resource limits, exfiltration
Data handlingWhat leaves the process, and to whom
ProvidersWriting an adapter
Feature matrixWhat each provider supports — and silently ignores
EvaluationHow accuracy is measured
RoadmapWhat is next, and what is deliberately deferred
RulesThe engineering rules this codebase is held to
Decisions32 ADRs — why it is like this
GlossaryTerms used throughout

Contributors and coding agents should start with AGENTS.md.


Status

v1.0.0 — the API is stable. Nine Go modules, the core with zero dependencies, on top of thirty-two architecture decision records, most of them written before the code. A breaking change now requires a v2 (ADR-0032).

That is a promise about compatibility, not about accuracy. The two are separable and this project keeps them separate, because a version number is a poor place to hide a caveat:

The API will not break without a v2Yes. api/ovrin.txt is a contract, checked on every commit
Published accuracy figureNone. The evaluation corpus is synthetic, and no run has been committed (ADR-0023)
Confidence is a calibrated probabilityNo. It is a ranking signal. It orders a review queue well; it does not mean "correct this often" (docs/confidence.md)
Used in production outside this projectNot that we know of. If you do, please say so

Released: the core at v1.0.0, and the seven adapters and the example each at <path>/v1.0.0. Modules version independently (ADR-0024), so they may diverge from here.

If you are deciding whether to depend on this, docs/validating.md is written for you and includes the reasons not to.

Contributing

Read CONTRIBUTING.md and docs/rules.md. Commits are Conventional Commits and must be signed off (DCO). The most valuable contribution right now is a document for the evaluation corpus that we are legally allowed to redistribute.

License

Apache-2.0. See LICENSE and NOTICE.

anthropic
docker
docker-compose
document-processing
documents
golang
ocr-engine
ocr-recognition
ocr-text-reader
ollama
open-source

BAGOMBEKA-JOB-DEV/ovrin

Turn documents into structured data. Ovrin is a Go library that reads PDFs, scans and images and returns a typed Go struct — with per-field confidence, a record of where every value came from, and an explicit signal when a human should look at it

Go

1

93 commits

updated Sep 30, 2026

See the code

README

ovrin

CI Go Reference Go Report Card License: Apache 2.0

go get github.com/BAGOMBEKA-JOB-DEV/ovrin@v1.0.0

Package: github.com/BAGOMBEKA-JOB-DEV/ovrin · v1.0.0 · Go 1.22+ · zero dependencies · no cgo

Turn documents into structured data.

Ovrin is a Go library that reads PDFs, scans and images and returns a typed Go struct — with per-field confidence, a record of where every value came from, and an explicit signal when a human should look at it.

Define what you want:

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"`
}

Ask for it:

res, err := ovrin.Extract[Invoice](ctx, client, ovrin.File("invoice.pdf"))
if err != nil {
    return err
}

fmt.Println(res.Data.Total)        // 2500.00, a float64
fmt.Println(res.Confidence)        // 0.96
fmt.Println(res.NeedsReview)       // false

Ovrin handles the rest: detecting the format, reading the text layer, rasterising and running OCR when there isn't one, normalising the content, constraining the model to your schema, validating the result, checking that every value actually appears in the document, and scoring what it found.


Why ovrin

Typed, not map[string]anyres.Data.Total is a float64 at compile time. Rename a field and the compiler finds every use.
A pipeline, not a promptText layer first, OCR on demand, vision as a distinct reading. Staged extraction measurably beats handing a model raw pages.
Confidence you can decomposeEvery score breaks down into named signals. No number is produced that you cannot take apart.
Every value points backPage, bounding box and source span for each field. Review interfaces can highlight; auditors can check.
Fabrication is detectedValues that appear nowhere in the document are flagged, not returned as fact.
Provider independentThree small interfaces. Bring OpenAI, Anthropic, Gemini, Tesseract, Textract, Ollama, or your own.
Zero dependencies in the corego get pulls nothing. No cgo. Cross-compiles and builds static.
Untrusted input by defaultDocuments are parsed with finite limits and prompted as data, never as instruction.

What it is not

Ovrin is not "send a PDF to a model and get JSON back". That takes an afternoon, costs more, and is measurably less accurate on real documents. It also cannot tell you how confident to be, where a value came from, or whether the model invented it — which is the part that matters when the extracted number is a payment.


Install

go get github.com/BAGOMBEKA-JOB-DEV/ovrin

The core has no external dependencies. You add exactly the providers you use, and nothing else enters your go.sum:

go get github.com/BAGOMBEKA-JOB-DEV/ovrin/model/skyl      # OpenAI, Anthropic, Gemini, Ollama, …
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/ocr/tesseract   # local OCR
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/ocr/google      # Cloud Vision / Document AI
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/render/pdfium   # rasterise scanned PDFs, no cgo
go get github.com/BAGOMBEKA-JOB-DEV/ovrin/otel            # OpenTelemetry

Go 1.22 or newer. (That is a language floor. For the toolchain to build with, see SECURITY.md.)

Development

Every command in this project is a make target. Nothing is hidden in a script or a CI file — the Makefile is the single definition, and CI calls these same targets, so a green run on your machine is a green run on the build.

Setting up

git clone https://github.com/BAGOMBEKA-JOB-DEV/ovrin.git
cd ovrin

make setup      # commit sign-off hook + golangci-lint and govulncheck at CI's versions
make check      # the whole gate, across all nine modules

That is the entire setup. No credentials are needed to build or test — the default suite runs against in-process fakes and loopback servers, offline (ADR-0022).

Run make with no arguments at any time to list every target.

The two you will use most

CommandWhat it does
make checkThe gate to pass before opening a pull request: gofmt, build, go vet under every build tag, tests with the race detector, tests over real sockets, go mod tidy, golangci-lint, govulncheck, documentation checks — for every module.
make ciEverything above plus what only CI used to do: the coverage floor, the zero-dependency assertion, and the cgo-free cross-compile.

Every target

Getting started

CommandWhat it does
make / make helpList every target
make setupInstall the sign-off hook and both tools
make hooksJust the Signed-off-by commit hook
make toolsJust golangci-lint and govulncheck, at the versions CI pins

Build and test

CommandWhat it does
make buildCompile every module
make testThe offline suite, with the race detector
make test-sandboxThe same over real sockets, against an adversarial fake server
make test-coverThe suite with a coverage profile — what CI runs
make cover-floorAssert coverage is at or above 85%
make cover-htmlOpen the coverage profile in a browser
make benchBenchmarks (render/pdfium)
make fuzzEvery fuzz target; FUZZTIME=5m make fuzz to run longer
make test-integrationAgainst real providers. Costs money
make evalAccuracy against the corpus. Needs OPENAI_API_KEY, costs money

Quality — each is one step of CI

CommandWhat it does
make fmtFormat every module
make fmt-checkFail if anything is not gofmt'd
make vetgo vet under every build tag
make lintgolangci-lint
make actionsValidate the GitHub Actions workflow files
make vulngovulncheck
make tidy / make tidy-checkgo mod tidy; the check fails if it left a diff
make deps-checkAssert the core has zero external dependencies
make crossAssert it builds with CGO_ENABLED=0 for linux/arm64, darwin/arm64, windows/amd64

Documentation and generated files

CommandWhat it does
make docsCheck links, citations, ADR hygiene and API references
make apiRegenerate api/ovrin.txt from the source
make corpusRegenerate the synthetic evaluation corpus
make reportRegenerate the committed no-run evaluation report

Running and releasing

CommandWhat it does
make run-exampleExtract the example receipt with a real model. Needs OPENAI_API_KEY
make release-check VERSION=v1.0.0Report whether the tree is fit to tag. Never tags, never pushes. Takes a module-prefixed tag too, e.g. model/skyl/v1.0.0
make cleanRemove build and coverage output

Docker — the toolchain pinned, nothing to install

CommandWhat it does
make docker-buildBuild the image
make docker-ciThe whole gate, in a container
make docker-shellA shell with the toolchain, your checkout mounted
make docker-testJust the test suites
make docker-test-offlineThe suite with --network=none, proving it needs no network
make docker-exampleThe receipt example. Pass OPENAI_API_KEY through
make docker-evalThe evaluation harness, with eval/report mounted back out
make docker-cleanRemove the images

The container is worth knowing about for two specific reasons.

It ships Tesseract's English language data, so the six engine-backed tests in ocr/tesseract that skip on a machine without a language pack actually run there.

And it pins the Go toolchain. make vuln reports vulnerabilities in the standard library of whichever Go you are running, not only in ovrin — so on an older toolchain it fails with a long list that no change to this repository can fix. If that happens, either upgrade Go or run make docker-ci, which uses a pinned modern one. See SECURITY.md for why the go 1.22 in go.mod is a language floor and not a claim that 1.22.0 is safe to run this on.

Working on one module

The repository is nine Go modules. Every target loops over all of them; pass MODULES to narrow it, which is exactly how CI's matrix invokes them:

make test MODULES=ocr/azure
make build MODULES="ocr/azure ocr/textract"

Environment variables

None are needed for make check. These matter only for the targets that contact a real provider:

VariableUsed byNotes
OPENAI_API_KEYmake run-example, make evalRequired by both; they refuse to start without it
OPENAI_BASE_URLmake evalDefaults to https://api.openai.com/v1
OVRIN_MODELmake run-exampleDefaults to gpt-5.2
OVRIN_EVAL_MODELmake evalDefaults to gpt-5.2

Adapters never read the environment themselves — every credential is a function argument (rules.md §6.4). The variables above are read by the example programme and the evaluation harness, which are programmes rather than library code.

Inputs

Inputv0.1How
PDF with a text layeryesread directly — exact and nearly free
PNG, JPEG, TIFFyesOCR or vision
Scanned PDFyes, via cloud OCRproviders that accept a PDF rasterise server-side
Scanned PDF, offlineyesrender/pdfium rasterises locally, ocr/tesseract reads — neither needs cgo or a network
DOCX, XLSX, CSVyesread directly; no OCR and no renderer

Document types are never hardcoded. Invoices, receipts, government forms, transcripts, bank statements, medical forms and contracts are all the same mechanism — you write a struct.


Reading a result

type Result[T any] struct {
    Data        T                        // typed, partially populated
    Valid       bool                     // every validation rule passed
    Confidence  float64
    Fields      map[string]FieldResult   // one per schema field
    NeedsReview bool
    Reasons     []ReviewReason
    Metadata    Metadata
}

err != nil means nothing usable came back. It does not mean the data is good — that is Valid. A field that could not be read is marked absent and is never filled with a zero value, because a payments system must be able to tell "the total is zero" from "we could not read the total".

res, err := ovrin.Extract[Invoice](ctx, client, ovrin.File("invoice.pdf"))
if err != nil {
    return err                              // unreadable, no provider, limit hit
}
if !res.Valid || res.NeedsReview {
    return review.Queue(res)                // usable, but not automatically
}
return ledger.Post(res.Data)

Ask why:

e, _ := res.Explain("total")
fmt.Println(e)
Field:       total
Value:       2500.00
Confidence:  0.99

Signals
  grounding    1.00  ×0.30   found verbatim, page 1
  ocr          0.97  ×0.20   12 backing words, mean 0.97
  schema       1.00  ×0.15   float64, min=0 satisfied
  cross_field  1.00  ×0.05   line items sum to total
  format       1.00  ×0.05   parsed as currency
  agreement       —          only one reading

Provenance
  ocr:tesseract   page 1   box (412,688)-(486,702)   exact

Validation
  required  pass
  min=0     pass

Documentation

DocumentWhat it covers
Getting startedFirst extraction, end to end
The ideaThe problem, the goals, and the non-goals
ArchitectureModules, seams, and which way the arrows point
PipelineAll nine stages in detail
SchemasThe tag grammar and the rule vocabulary
ConfidenceSignals, weights, and what the number does not mean
ExplainabilityProvenance, review, and audit
ObservabilityHooks, spans and metric names — all of them API
Threat modelPrompt injection, resource limits, exfiltration
Data handlingWhat leaves the process, and to whom
ProvidersWriting an adapter
Feature matrixWhat each provider supports — and silently ignores
EvaluationHow accuracy is measured
RoadmapWhat is next, and what is deliberately deferred
RulesThe engineering rules this codebase is held to
Decisions32 ADRs — why it is like this
GlossaryTerms used throughout

Contributors and coding agents should start with AGENTS.md.


Status

v1.0.0 — the API is stable. Nine Go modules, the core with zero dependencies, on top of thirty-two architecture decision records, most of them written before the code. A breaking change now requires a v2 (ADR-0032).

That is a promise about compatibility, not about accuracy. The two are separable and this project keeps them separate, because a version number is a poor place to hide a caveat:

The API will not break without a v2Yes. api/ovrin.txt is a contract, checked on every commit
Published accuracy figureNone. The evaluation corpus is synthetic, and no run has been committed (ADR-0023)
Confidence is a calibrated probabilityNo. It is a ranking signal. It orders a review queue well; it does not mean "correct this often" (docs/confidence.md)
Used in production outside this projectNot that we know of. If you do, please say so

Released: the core at v1.0.0, and the seven adapters and the example each at <path>/v1.0.0. Modules version independently (ADR-0024), so they may diverge from here.

If you are deciding whether to depend on this, docs/validating.md is written for you and includes the reasons not to.

Contributing

Read CONTRIBUTING.md and docs/rules.md. Commits are Conventional Commits and must be signed off (DCO). The most valuable contribution right now is a document for the evaluation corpus that we are legally allowed to redistribute.

License

Apache-2.0. See LICENSE and NOTICE.

anthropic
docker
docker-compose
document-processing
documents
golang
ocr-engine
ocr-recognition
ocr-text-reader
ollama
open-source

Languages

Go

98.3%