Download idp_recognizer.py from spark-ux/indic-doc-parser: direct link, hf CLI and curl.
- Browser
- Download file 5.62 kB
-
https://huggingface.co/spark-ux/indic-doc-parser/resolve/main/idp_recognizer.py
- Command line
-
hf download hf://spark-ux/indic-doc-parser/idp_recognizer.py
-
curl -L -o idp_recognizer.py https://huggingface.co/spark-ux/indic-doc-parser/resolve/main/idp_recognizer.py
5.62 kB
| # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/recognizer.py -- do not edit. | |
| # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the | |
| # whole install. See indic_doc_parser.py for usage. | |
| """IndicBlockOCR: crops in, transcriptions out. | |
| Heavy imports live inside methods, so importing this module stays free -- asserted by | |
| tests/ocr/test_ocr_lazy_import.py. | |
| """ | |
| from __future__ import annotations | |
| from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable | |
| from idp_types import CropConfig, RecognizerConfig | |
| if TYPE_CHECKING: # pragma: no cover | |
| from PIL.Image import Image | |
| class CropRequest(NamedTuple): | |
| image: Image | |
| prompt: str | |
| class RecognizerBackend(Protocol): | |
| """``transcribe`` returns one string per request, in the same order.""" | |
| def transcribe(self, requests: list[CropRequest]) -> list[str]: ... | |
| def close(self) -> None: ... | |
| def build_requests(blocks, page, crop_cfg: CropConfig, table_format) -> tuple[list, list]: | |
| """Crop each block and pair it with its prompt. | |
| Returns ``(requests, orders)`` -- the reading-order rank of each request, so transcriptions | |
| can be matched back. Blocks that yield no crop are simply absent from both. | |
| """ | |
| from idp_crops import area_clamp, crop_for | |
| from idp_contract import prompt_for | |
| requests, orders = [], [] | |
| for block in blocks: | |
| crop = crop_for(block, page, crop_cfg) | |
| if crop is None: | |
| continue | |
| requests.append( | |
| CropRequest(area_clamp(crop, crop_cfg), prompt_for(block.type, table_format)) | |
| ) | |
| orders.append(block.order) | |
| return requests, orders | |
| class HfRecognizer: | |
| """Reference recognizer on plain ``transformers`` -- no vLLM. | |
| Exists so IndicDocParser can run anywhere ``transformers`` runs, including straight from the | |
| Hub with ``trust_remote_code=True``. It is the *quickstart* path, not the working one: | |
| without continuous batching it is orders of magnitude slower per block than | |
| :class:`VllmRecognizer`, so use it to try a page, not to parse a corpus. | |
| Output also diverges slightly from the vLLM path. Both decode greedily, but different kernels | |
| give different logits, and a near-tie flips the argmax -- so do not expect byte-identical | |
| transcriptions between the two backends. | |
| """ | |
| def __init__( | |
| self, | |
| ckpt: str | None = None, | |
| config: RecognizerConfig | None = None, | |
| device: str = "auto", | |
| attn_implementation: str = "sdpa", | |
| batch_size: int = 8, | |
| ) -> None: | |
| import torch | |
| from transformers import AutoModelForImageTextToText, AutoProcessor | |
| self._torch = torch | |
| self.config = config or RecognizerConfig() | |
| # RecognizerConfig.batch_size sizes a vLLM chunk (~2048). Generating that many at once | |
| # here would simply OOM; HF batches are bounded by memory, not by scheduler behaviour. | |
| self.batch_size = batch_size | |
| if ckpt is None: | |
| raise ValueError( | |
| "no recognizer weights given -- " | |
| "IndicDocParser.from_pretrained(snapshot_download(REPO))" | |
| ) | |
| self.ckpt = ckpt | |
| self.processor = AutoProcessor.from_pretrained(self.ckpt) | |
| tokenizer = self.processor.tokenizer | |
| # Left padding so every sequence in a batch ends flush against the generation boundary. | |
| tokenizer.padding_side = "left" | |
| self.model = AutoModelForImageTextToText.from_pretrained( | |
| self.ckpt, | |
| dtype=getattr(torch, self.config.dtype), | |
| device_map=device, | |
| attn_implementation=attn_implementation, | |
| ) | |
| self.model.eval() | |
| # The checkpoint's generation_config carries eos_token_id 248044, which is an ordinary | |
| # word piece, not a turn terminator. Left alone, generate() never stops and every block | |
| # runs to max_new_tokens, repeating itself. vLLM does not hit this because it takes the | |
| # tokenizer's EOS. Trust the tokenizer here too. | |
| self.eos_token_id = tokenizer.eos_token_id | |
| self.pad_token_id = tokenizer.pad_token_id or tokenizer.eos_token_id | |
| def _prompt(self, text: str) -> str: | |
| return self.processor.apply_chat_template( | |
| [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}], | |
| add_generation_prompt=True, | |
| tokenize=False, | |
| ) | |
| def transcribe(self, requests: list[CropRequest]) -> list[str]: | |
| texts: list[str] = [] | |
| for i in range(0, len(requests), self.batch_size): | |
| chunk = requests[i : i + self.batch_size] | |
| inputs = self.processor( | |
| text=[self._prompt(r.prompt) for r in chunk], | |
| images=[r.image for r in chunk], | |
| padding=True, | |
| return_tensors="pt", | |
| ).to(self.model.device) | |
| prompt_len = inputs["input_ids"].shape[-1] | |
| with self._torch.inference_mode(): | |
| out = self.model.generate( | |
| **inputs, | |
| max_new_tokens=self.config.max_tokens, | |
| do_sample=False, | |
| use_cache=True, | |
| eos_token_id=self.eos_token_id, | |
| pad_token_id=self.pad_token_id, | |
| ) | |
| texts.extend(self.processor.batch_decode(out[:, prompt_len:], skip_special_tokens=True)) | |
| return [t.strip() for t in texts] | |
| def close(self) -> None: | |
| self.model = None | |
| self.processor = None | |