ocracy

ocracy — one facade over many OCR engines, plus a ledger to choose between them.

OCR (“read the text in this image”) is solved a dozen different ways: local engines (Tesseract, EasyOCR, PaddleOCR), cloud APIs (Google Vision, AWS Textract, Azure), and VLM-based readers — each with its own install, API, pricing, language coverage, and quirks. ocracy gives you:

  1. A uniform facade. Call ocr() and get the same OcrResult back no matter which backend ran:

    import ocracy
    result = ocracy.ocr("scan.png")          # default (first installed) backend
    print(result)                            # -> the recognized text
    result = ocracy.ocr("scan.png", backend="easyocr", languages=["en", "fr"])
    

    Convenience: read_text() returns just the string.

  2. A ledger / gallery of every engine we researched — not only the ones with a working facade — so you can choose with eyes open:

    ocracy.catalog                                   # the whole ledger
    ocracy.find(is_local=True, open_source=True)     # filter it
    ocracy.find(handwriting="yes", is_remote=True)
    ocracy.catalog.to_dataframe()                    # browse as a table
    

    The ledger lives in data (ocracy/data/backends.json), not code.

  3. Tools to build new facades. The catalog is large; ocracy ships a facade for a curated subset and gives you the machinery (and a SKILL) to add any other one in minutes:

    from ocracy.make_backend import scaffold_backend, validate_adapter
    scaffold_backend("mathpix")    # generate a backend package from the ledger
    validate_adapter("tesseract")  # smoke-test an adapter end to end
    

Three tiers of access, from simplest to most powerful:

ocracy.ocr(img)                                # facade, default backend
ocracy.services.tesseract.read(img, lang="fra")  # pick a backend
ocracy.services.tesseract.adapter              # raw engine adapter
class ocracy.BBox(x0: float, y0: float, x1: float, y1: float, polygon: Tuple[Tuple[float, float], ...] | None = None)[source]

An axis-aligned bounding box in pixel coordinates (origin = top-left).

polygon optionally carries the original (possibly rotated) vertices as a sequence of (x, y) points; the axis-aligned x0/y0/x1/y1 are always populated (derived from the polygon if a backend only gives one).

property as_tuple: Tuple[float, float, float, float]

(x0, y0, x1, y1) — the convention PIL’s crop expects.

classmethod from_polygon(points: Sequence) BBox[source]

Build a box from polygon vertices [(x, y), ...].

The axis-aligned extent is computed from the vertices and the original polygon is preserved for callers that care about rotation.

property xywh: Tuple[float, float, float, float]

(x, y, width, height) — the convention many drawing libs expect.

class ocracy.BackendInfo(record: dict, *, implemented: bool = False)[source]

One backend’s ledger entry — a read-only, attribute-and-dict accessible record.

Wraps the raw record dict so that new fields added to the JSON are available immediately (via attribute or key access) without code changes, while a few commonly used fields get typed properties for convenience and discoverability.

class ocracy.BaseOcrAdapter(config: dict)[source]

Optional base class for backend adapters.

Stores the config, builds a kwarg translator from config['param_map'], and implements read as: translate normalized kwargs -> native kwargs -> _read(). Subclasses implement _read() and return an OcrResult.

Adapters are not required to subclass this — the registry only needs an Adapter class with a read(image, **kwargs) method — but doing so removes the boilerplate.

class ocracy.Catalog(path: str | Path | None = None, *, _records: List[dict] | None = None)[source]

A filterable, dict-like collection of BackendInfo, keyed by id.

Loaded lazily from DEFAULT_LEDGER_PATH (override via the path argument or the OCRACY_LEDGER environment variable). filter() returns a new Catalog over the matching subset, so filters compose:

catalog.filter(is_remote=True).filter(pricing_model="free_tier_then_paid")
can(capability: str) Catalog[source]

Backends with a non-plain-text capability (e.g. "math", "tables").

Matches either the beyond_text list or a <capability> field set to "yes" (e.g. handwriting, tables, math_formula).

compare(ids: Iterable[str] | None = None, *, fields: Iterable[str] = ('name', 'is_local', 'is_remote', 'open_source', 'pricing_model', 'price_note', 'accuracy_tier', 'languages_count', 'handwriting', 'math_formula', 'tables', 'best_for')) List[dict][source]

A trimmed, side-by-side view of selected backends and fields.

filter(*, predicate: Callable[[BackendInfo], bool] | None = None, implemented: bool | None = None, **criteria: Any) Catalog[source]

Return a new Catalog of backends matching every criterion.

Parameters:
  • predicate – An arbitrary BackendInfo -> bool callable.

  • implemented – If set, keep only (un)implemented backends.

  • **criteriafield=value constraints. value may be a set/list/tuple meaning “one of”. Free-text fields (languages_note, beyond_text, output_formats) use a case-insensitive substring match.

Example:

catalog.filter(is_local=True, open_source=True, handwriting="yes")
supports_language(language: str) Catalog[source]

Backends whose languages_note mentions language (name or code).

to_dataframe(*, columns: Iterable[str] | None = None)[source]

Return a pandas DataFrame of the catalog (pandas imported lazily).

class ocracy.OcrResult(text: str, blocks: List[TextBlock] = <factory>, backend: str = '', raw: Any = None, meta: dict = <factory>)[source]

The normalized result of reading an image with any backend.

text is the headline payload: the full recognized text in reading order. blocks carries the structured units (with boxes/confidences) when the backend provides them. raw is the untouched backend output. meta holds cross-cutting extras (languages, page count, a Markdown rendering, timing, …).

Progressive disclosure:

result = ocracy.ocr("scan.png")
print(result)              # -> the text
result.text                # -> the same string
for line in result:        # -> iterate TextBlocks (lines by default)
    print(line.text, line.confidence)
result.words               # -> only word-level blocks
result.mean_confidence     # -> average confidence, if available
result.raw                 # -> engine-specific structure
at_level(level: str) List[TextBlock][source]

Blocks at a given granularity ("word", "line", …).

filter_confidence(min_confidence: float) OcrResult[source]

Return a copy keeping only blocks at or above min_confidence.

Blocks without a confidence are dropped. text is rebuilt from the surviving blocks (joined by newline).

classmethod from_blocks(blocks: List[TextBlock], *, backend: str = '', raw: Any = None, text: str | None = None, joiner: str = '\n', **meta: Any) OcrResult[source]

Build a result from structured blocks.

If text is not given it is synthesized by joining the blocks’ text in their given order with joiner (callers should pass blocks already in reading order, or pre-join and pass text explicitly).

classmethod from_text(text: str, *, backend: str = '', raw: Any = None, **meta: Any) OcrResult[source]

Build a minimal result from just a text string (no geometry).

property markdown: str | None

Markdown rendering if the backend produced one (else None).

property mean_confidence: float | None

Mean confidence over blocks that report one, or None.

class ocracy.Requirements(backend_id: str, implemented: bool, available: bool, is_local: bool, is_remote: bool, pip_command: str, extra: str | None = None, system: List[str] = <factory>, system_note: str | None = None, gpu: str | None = None, weights: str | None = None, heavy: bool = False, alternative: str | None = None, credentials: List[str] = <factory>, notes: List[str] = <factory>)[source]

What a backend needs to run — structured for an agent to act on.

instructions() str[source]

An agent-/human-readable, copy-pasteable install plan.

class ocracy.ServiceCollection[source]

Lazy mapping of backend ids -> ServiceHandle.

Supports dict-style (services['tesseract']) and attribute-style (services.tesseract) access.

class ocracy.TextBlock(text: str, bbox: BBox | None = None, confidence: float | None = None, level: str = 'line', language: str | None = None, meta: dict = <factory>)[source]

One recognized unit of text.

text

The recognized string for this unit.

Type:

str

bbox

Where it was found (pixel coordinates), if the backend reports it.

Type:

ocracy.base.BBox | None

confidence

Recognition confidence in [0, 1] (normalized by ocracy from whatever scale the backend used), if available.

Type:

float | None

level

Granularity — one of LEVELS (“word”, “line”, …).

Type:

str

language

Detected/declared language code for this unit, if any.

Type:

str | None

meta

Backend-specific extras (font size, style, page index, …).

Type:

dict

ocracy.available_backends() List[str][source]

Implemented backends whose dependency is importable right now.

ocracy.backend_ids(level: str = 'all', *, info: Dict[str, dict] | None = None, test_image: Any = None) List[str][source]

Sorted backend ids at a readiness level (one of LEVELS).

Pass a precomputed info (from backend_info()) to avoid recomputation — important for "tested", which otherwise re-runs OCR.

ocracy.backend_info(ids: Iterable[str] | None = None, *, run_tests=False, test_image: Any = None) Dict[str, dict][source]

Per-backend status: every ledger field plus implemented/set_up/tested.

Parameters:
  • ids – Which backends (default: every ledger id).

  • run_tests – Which set-up backends to actually OCR-test. True = all set-up (real API calls for remotes!); False = none (tested is None); or an iterable of ids to limit testing to those.

  • test_image – Image to test with (default: a generated one, shared across all).

Returns:

{id: {..., "name", "website", "implemented", "set_up", "tested"}} where tested is True/False/None (None = not attempted).

ocracy.check(backend_id: str) bool[source]

Is backend_id importable / usable right now? (no install, no network).

ocracy.doctor() dict[source]

Report which implemented backends are usable now and what the rest need.

Returns {"available": [...], "missing": {id: one-line install hint}}.

ocracy.find(**criteria) Catalog[source]

Filter the ledger; shorthand for ocracy.catalog.Catalog.filter().

Example:

ocracy.find(is_local=True, open_source=True, handwriting="yes")
ocracy.find(implemented=True)        # only backends ocracy can run now
ocracy.get_config(backend_id: str) dict[source]

A backend’s BACKEND_CONFIG without loading its adapter.

ocracy.get_default_backend(capability: str = 'read', *, require_available: bool = True) str[source]

Pick a sensible default backend id for a capability.

Strategy (OCR-tuned): prefer a backend explicitly flagged default_for the capability and whose dependency is importable; then any importable backend for the capability; then — if require_available is False or nothing is installed — the first registered candidate (using it will raise a helpful install error).

ocracy.install(backend_id: str, *, yes: bool = False, gpu: bool = False, verify: bool = True, upgrade: bool = False) dict[source]

Plan (and optionally run) the pip install for a backend.

With yes=False (default) this is a dry run: it returns the plan without changing anything — call result['requirements'].instructions() to show it. With yes=True it runs pip install for the backend’s extra in the current interpreter, then (if verify) checks importability.

System dependencies and GPU wheels are surfaced, never run automatically (they need sudo/brew or an environment-specific CUDA choice) — run those yourself from result['requirements'].system / .gpu.

ocracy.is_set_up(backend_id: str) bool[source]

Ready to run here & now.

Requires the engine/client to be importable (local and remote), and — for a remote backend — its (primary) credential env var to resolve. So a remote whose key is set but whose client library isn’t installed is not set up (it couldn’t actually run). Returns False for ledger-only backends.

ocracy.is_tested(backend_id: str, *, image: Any = None) bool[source]

Actually run OCR on image (a generated one if None); True iff it worked.

For remote backends this performs a real API call. Returns False if the backend isn’t set up or the run fails.

ocracy.list_backends(capability: str | None = None) List[str][source]

Sorted ids of implemented backends, optionally filtered by capability.

A backend matches capability if it appears in the backend’s capabilities list (the primary "read" is implied for all).

ocracy.make_block(text: str, *, bbox: Any = None, confidence: float | None = None, conf_scale: float = 1.0, level: str = 'word', language: str | None = None, **meta: Any) TextBlock[source]

Build a normalized TextBlock.

bbox may be any shape accepted by as_bbox(). confidence is normalized to [0, 1] via conf_scale (e.g. conf_scale=100 for percent-scale engines).

ocracy.names_with_sites(ids: Iterable[str], *, info: Dict[str, dict] | None = None) str[source]

"Name (website), Name2 (website2), ..." for ids (name, not id).

ocracy.ocr(image: str | Path | bytes | PILImage | NDArray, *, backend: str = None, **kwargs) OcrResult[source]

Read text from an image with any backend, returning a normalized result.

Parameters:
  • image – A path, http(s) URL, bytes, PIL image, or numpy array.

  • backend – Backend id (see list_backends()). Defaults to the first installed implemented backend (see get_default_backend()).

  • **kwargs – Normalized, backend-translated options (e.g. languages). Unknown options for the chosen backend are warned about and dropped.

Returns:

An OcrResult (str(result) is the text).

ocracy.read_text(image: str | Path | bytes | PILImage | NDArray, *, backend: str = None, **kwargs) str[source]

Like ocr() but returns just the recognized text string.

ocracy.register_backend(backend_id: str, config: dict, adapter: Any = None) None[source]

Register a backend at runtime (for third-party plugins).

Parameters:
  • backend_id – Unique identifier.

  • configBACKEND_CONFIG-shaped dict (needs at least name).

  • adapter – Optional pre-instantiated adapter; else loaded lazily from config['module_path'] when first used.

ocracy.requirements(backend_id: str, *, gpu: bool = False) Requirements[source]

Return structured install Requirements for backend_id.

Works for both implemented backends (uses the ocracy[extra] install and the recipe) and ledger-only backends (falls back to the ledger’s python_install string). Pass gpu=True to surface GPU wheel guidance.

ocracy.scaffold_backend(backend_id: str, *, dest: str | Path | None = None, overwrite: bool = False, ledger: Any = None, extra_overrides: dict | None = None) Path[source]

Create a new ocracy/backends/<id>/ package from the template.

Pre-fills config.py from the backend’s ledger entry (if any) so you only have to flesh out param_map and implement adapter.py’s _read.

Parameters:
  • backend_id – The ledger id (e.g. "easyocr", "google-vision"). The on-disk module name uses underscores; the config id keeps the id verbatim.

  • dest – Target directory (defaults to ocracy/backends/<id_underscored>).

  • overwrite – Allow writing into an existing non-empty directory.

  • ledger – A Catalog to read the record from (defaults to the shipped catalog).

  • extra_overrides – Extra config-key overrides applied on top of the record.

Returns:

The path to the created backend package.

ocracy.services = <ServiceCollection backends=['aws-textract', 'azure-document-intelligence', 'claude-vision', 'easyocr', 'google-vision', 'gpt-4o-vision', 'mathpix', 'mistral-ocr', 'ocr-space', 'ocrmac', 'paddleocr', 'pix2tex-latex-ocr', 'rapidocr', 'tesseract', 'trocr-handwritten']>

Singleton service collection for per-backend access (services.tesseract).

ocracy.status_table(ids: Iterable[str] | None = None, *, info: Dict[str, dict] | None = None, columns=None, run_tests=False, test_image: Any = None) str[source]

Render an aligned Markdown table of backend status (also plain-text readable).

Rows are ordered tested → set-up → implemented → listed, then by name, so the “live” backends float to the top. Pass a precomputed info to avoid re-running tests; otherwise run_tests controls which set-up backends get OCR-tested.

ocracy.validate_adapter(backend_id: str, *, image: Any = None, expect_text: str | None = None) dict[source]

Smoke-test a backend adapter end to end, returning a report dict.

Loads the adapter (reporting unavailability instead of raising), runs read on a generated (or supplied) image, and checks the contract: an OcrResult came back with text and/or blocks. Never raises on a recognition mismatch — it returns what happened so callers can decide.