ek.ocr.tables#

Recover a normalized table from an OcrResult so ek’s table metrics can score it.

ocracy exposes no normalized table/cell type: TextBlock.level stops at block, with no cell/row/column model, so OCR table structure survives only inside the engine-specific OcrResult.raw (Textract Block objects, PaddleOCR pred_html, Azure tables[]) or in the markdown/HTML an engine emits (res.markdown for VLM engines). ek therefore can’t uniformly score table structure (TEDS / GriTS) off the OcrResult shape alone.

This module is the ek-side seam that closes that gap without touching ocracy (the dependency arrow stays ek -> ocracy, never the reverse). It turns an OcrResult-shaped object into the existing ek.metrics.tables.Table (the one TEDS/GriTS already score) – it does not define a new table type.

Because every engine stores tables differently, the per-engine extractor is an injected callable (open-closed / dependency-injection): pass parser= a (raw) -> Table | grid | html | None function, or register one under the "table_parsers" namespace and pass its name. With no parser, table_from_ocr() falls back to a few safe, documented heuristics over the result’s raw/markdown/meta and returns None when no table structure is present – it never guesses wildly.

Nothing here imports ocracy or any OCR engine: ek depends only on the OcrResult shape (.text / .raw / .markdown / .meta), so this works on any image -> OcrResult callable’s output.

Example

>>> from ek.metrics.tables import Table
>>> class R:  # a minimal OcrResult-shaped object carrying a 2-D grid in .raw
...     raw = {"tables": [[["a", "b"], ["c", "d"]]]}
>>> t = table_from_ocr(R())
>>> [[c.text for c in row] for row in t.rows]
[['a', 'b'], ['c', 'd']]
>>> has_table_structure(R())
True
>>> class Empty:  # no table anywhere in the result
...     raw = {"text": "just prose"}
>>> table_from_ocr(Empty()) is None
True

A registered third-party engine parser, used by name:

>>> from ek.registry import register
>>> @register("table_parsers", "demo")
... def _demo(raw):
...     return raw.get("cells")  # this engine stores a grid under raw['cells']
>>> class D:
...     raw = {"cells": [["x"]]}
>>> table_from_ocr(D(), parser="demo").rows[0][0].text
'x'

Module Attributes

TABLE_PARSERS_NAMESPACE

Registry namespace under which per-engine table parsers live.

Functions

engine_yields_tables(engine)

Whether engine's capability profile claims scoreable table structure.

has_table_structure(ocr_result, *[, parser])

Whether table_from_ocr() recovers a non-empty table from ocr_result.

resolve_table_parser(ref)

Resolve a table parser reference (a registered name or a callable) to a callable.

table_from_ocr(ocr_result, *[, parser])

Extract a normalized Table from an OcrResult.

ek.ocr.tables.TABLE_PARSERS_NAMESPACE = 'table_parsers'#

Registry namespace under which per-engine table parsers live. Register a parser with ek.registry.register("table_parsers", "<engine>", fn) (or an ek.table_parsers entry point) and resolve it by name via the parser= arg.

ek.ocr.tables.engine_yields_tables(engine)[source]#

Whether engine’s capability profile claims scoreable table structure.

A convenience over ek.ocr.profile(): returns the engine profile’s tables flag. ek treats the profile as a prior (Azure DI, AWS Textract, PaddleOCR and the markdown engines are expected to yield scoreable tables; Google Vision is not), to be verified empirically by actually running table_from_ocr(). Use it to skip table scoring for engines that cannot emit cells in the first place.

Return type:

bool

ek.ocr.tables.has_table_structure(ocr_result, *, parser=None)[source]#

Whether table_from_ocr() recovers a non-empty table from ocr_result.

A thin predicate over table_from_ocr() (same parser semantics): true iff a table with at least one cell can be recovered. Use it to route an OCR result to the table metrics vs the text-only CER/WER fallback.

Return type:

bool

ek.ocr.tables.resolve_table_parser(ref)[source]#

Resolve a table parser reference (a registered name or a callable) to a callable.

Mirrors how other ek strategies resolve from the registry (see ek.registry.resolve()). A str is looked up under the "table_parsers" namespace; an already-callable parser is returned unchanged.

Return type:

Callable[[Any], Any]

ek.ocr.tables.table_from_ocr(ocr_result, *, parser=None)[source]#

Extract a normalized Table from an OcrResult.

The single seam ek’s table metrics (TEDS / GriTS) use to score OCR table output uniformly. Returns the recovered Table, or None when the result carries no recoverable table structure.

Parameters:
  • ocr_result (Any) – Any OcrResult-shaped object. ek reads only its .raw (the engine-specific payload), .markdown (VLM/markdown engines), and .meta – never ocracy types, so any image -> OcrResult callable’s output works.

  • parser (Union[None, str, Callable[[Any], Any]]) –

    How to turn the raw payload into a table (dependency injection / open-closed). One of:

    • None (default): try the built-in safe heuristics – .raw is already a Table, a 2-D grid, or a <table>...</table> HTML string; or .raw/.meta is a mapping carrying one of {tables, table, cells, grid, html}; or .markdown contains an HTML <table>. Returns None if none match – it never guesses wildly.

    • a str: the name of a parser registered under "table_parsers" (per-engine extractor), resolved from ek.registry.

    • a callable (raw) -> Table | grid | html | None: an inline per-engine extractor. It receives the result’s .raw (falling back to the result itself when there is no .raw).

  • through (Any non-None value an injected parser returns is fed)

Return type:

Table | None

:param Table.coerce(): :param so a parser may yield a Table: :param a 2-D grid: :param or HTML.:

Return type:

Optional[Table]

Returns:

A non-empty Table, or None when no table structure is recoverable. An empty Table (no rows/cells) is normalized to None so the predicate has_table_structure() and downstream metrics see “no table”, not a degenerate one.