indic-doc-parser / indic_doc_parser.py
spark-ux's picture
Copy from bodhan-ai/indic-doc-parser
7b2177e verified
Raw
History Blame Contribute Delete
6.13 kB
"""IndicDocParser -- page image in, reading-ordered Markdown and per-block JSON out.
import sys
from huggingface_hub import snapshot_download
repo = snapshot_download("bodhan-ai/indic-doc-parser")
sys.path.insert(0, repo) # the code ships in the repo
from indic_doc_parser import IndicDocParser
parser = IndicDocParser.from_pretrained(repo)
print(parser("page.png")) # markdown
``sys.path.insert`` is needed because ``snapshot_download`` returns a cache directory, which is
not importable on its own. With the path added, this is ordinary Python -- no
``trust_remote_code``, and nothing of ours to install.
The two stages are also public, and each runs on its own:
IndicDocLayout(f"{repo}/weights/layout").detect("page.png") # blocks and reading order
IndicBlockOCR(f"{repo}/weights/ocr").run("page.png", layout) # layout may be your own
``layout`` there can be a PageResult, a dict, or the path to a layout JSON file, so the stages
compose across processes and you can correct a layout before transcribing it.
"""
from __future__ import annotations
import importlib
from pathlib import Path
def _preflight() -> None:
"""One clear error instead of a cascade of ImportErrors from inside transformers.
Every failure here is one someone has actually hit: a torch built for a CUDA line the driver
cannot run (silently CPU), a torchvision that does not match its torch, or a transformers
older than PPDocLayoutV3.
"""
fix = "./install.sh (or see the Installation section of the model card)"
for name, floor in (("torch", (2, 4)), ("transformers", (5, 7))):
try:
mod = importlib.import_module(name)
except ImportError:
raise ImportError(f"IndicDocParser needs {name}. Run: {fix}") from None
got = tuple(int(p) for p in mod.__version__.split(".")[:2] if p.isdigit())
if got < floor:
raise ImportError(
f"IndicDocParser needs {name}>={'.'.join(map(str, floor))}, "
f"found {mod.__version__}. Run: {fix}"
)
# torchvision is stage 2 only, so its absence is not fatal here -- the layout stage runs
# without it. It is checked where the recognizer is built.
_preflight()
# ruff: noqa: E402 -- the preflight has to run before transformers is imported, which is the
# whole point of it; these imports pull transformers in transitively.
from idp_offline import IndicBlockOCR, IndicDocLayout
from idp_types import CropConfig, DedupConfig, LayoutConfig, RecognizerConfig, TableFormat
#: The two stages are public: either runs on its own, and IndicBlockOCR accepts a layout you
#: produced or corrected yourself.
__all__ = ["IndicBlockOCR", "IndicDocLayout", "IndicDocParser"]
class IndicDocParser:
"""Both stages. Construct with :meth:`from_pretrained`, then call it on a page image."""
def __init__(
self,
path: str | Path,
device: str = "cuda",
table_format: str = "html",
dedup_mode: str = "both",
contain: float = 0.90,
min_px_side: int = 256,
max_new_tokens: int = 2048,
) -> None:
self.path = Path(path)
self._device = device
self._layout_cfg = LayoutConfig(device=device)
self._dedup = DedupConfig(mode=dedup_mode, contain=contain)
self._crop = CropConfig(min_px_side=min_px_side)
self._rec_cfg = RecognizerConfig(
max_tokens=max_new_tokens, table_format=TableFormat(table_format)
)
# Both stages are built on first use, so detect() never loads the 1.7 GB recognizer.
self._layout = None
self._ocr = None
@classmethod
def from_pretrained(cls, path: str | Path | None = None, **kwargs) -> IndicDocParser:
"""Load from a downloaded snapshot. Defaults to the directory this file lives in, which
is the snapshot itself -- so ``IndicDocParser.from_pretrained()`` also works."""
return cls(Path(path) if path else Path(__file__).resolve().parent, **kwargs)
# -- stages ------------------------------------------------------------ #
@property
def layout(self):
if self._layout is None:
self._layout = IndicDocLayout(
ckpt=str(self.path / "weights" / "layout"),
config=self._layout_cfg,
dedup=self._dedup,
)
return self._layout
@property
def recognizer(self):
if self._ocr is None:
if importlib.util.find_spec("torchvision") is None:
raise ImportError(
"The recognizer needs torchvision (its image processor uses it); the layout "
"stage does not, so detect() still works. Install it together with torch, "
"from the same index -- torchvision pins an exact torch version. See "
"./install.sh"
)
from idp_recognizer import HfRecognizer
self._ocr = IndicBlockOCR(
backend=HfRecognizer(
ckpt=str(self.path / "weights" / "ocr"),
config=self._rec_cfg,
device=self._device,
),
config=self._rec_cfg,
dedup=self._dedup,
crop=self._crop,
)
return self._ocr
# -- public API -------------------------------------------------------- #
def detect(self, image_path: str) -> dict:
"""Stage 1 only -- blocks, labels, reading order. Loads no recognizer."""
return self.layout.detect(image_path).as_record()
def parse(self, image_path: str) -> dict:
"""Both stages -> ``{image, width, height, blocks, markdown}``."""
page = self.recognizer.run(image_path, self.layout.detect(image_path))
return {**page.as_record(), "markdown": page.markdown}
def __call__(self, image_path: str) -> str:
"""The markdown for a page. ``parse()`` if you also want the per-block JSON."""
return self.parse(image_path)["markdown"]