adidsh commited on
Commit
39b37e0
·
verified ·
1 Parent(s): 998b1ea

Upload idp_offline.py

Browse files
Files changed (1) hide show
  1. idp_offline.py +177 -0
idp_offline.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/offline.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_ocr.py for usage.
4
+
5
+ """The two stages, and the pipeline that runs both.
6
+
7
+ IndicDocLayout page image -> PageResult (blocks, no text)
8
+ IndicBlockOCR image+layout -> PageResult (blocks with text, plus markdown)
9
+ IndicOCR page image -> both
10
+
11
+ Heavy imports live inside methods, so importing this module stays free.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+
19
+ from idp_blocks import resolve_nested_equations
20
+ from idp_reconstruct import reconstruct
21
+ from idp_types import (
22
+ CropConfig,
23
+ DedupConfig,
24
+ LayoutConfig,
25
+ PageResult,
26
+ RecognizerConfig,
27
+ )
28
+ from idp_contract import is_transcribed
29
+
30
+
31
+ def _open(image_path: str):
32
+ from PIL import Image
33
+
34
+ # Large scans and newspapers legitimately exceed PIL's decompression-bomb guard; layout
35
+ # resizes to img_size and crops are area-clamped, so compute stays bounded regardless.
36
+ Image.MAX_IMAGE_PIXELS = None
37
+ return Image.open(image_path).convert("RGB")
38
+
39
+
40
+ def _as_page(layout: PageResult | dict | str) -> PageResult:
41
+ if isinstance(layout, PageResult):
42
+ return layout
43
+ if isinstance(layout, str):
44
+ with open(layout, encoding="utf-8") as fh:
45
+ layout = json.load(fh)
46
+ return PageResult.from_record(layout)
47
+
48
+
49
+ class IndicDocLayout:
50
+ """Stage 1 -- layout and reading order. Loads torch only, never vLLM."""
51
+
52
+ def __init__(
53
+ self,
54
+ ckpt: str | None = None,
55
+ config: LayoutConfig | None = None,
56
+ dedup: DedupConfig | None = None,
57
+ backend=None,
58
+ ) -> None:
59
+ if backend is None:
60
+ from idp_layout import IndicDocLayoutBackend
61
+
62
+ backend = IndicDocLayoutBackend(ckpt, config, dedup)
63
+ self.backend = backend
64
+
65
+ def detect(self, image_path: str) -> PageResult:
66
+ image = _open(image_path)
67
+ return PageResult(
68
+ image=os.path.basename(image_path),
69
+ width=image.width,
70
+ height=image.height,
71
+ blocks=self.backend.detect(image),
72
+ )
73
+
74
+ def close(self) -> None:
75
+ self.backend.close()
76
+
77
+ def __enter__(self) -> IndicDocLayout:
78
+ return self
79
+
80
+ def __exit__(self, *exc_info) -> None:
81
+ self.close()
82
+
83
+
84
+ class IndicBlockOCR:
85
+ """Stage 2 -- per-block transcription against a layout, which may be your own."""
86
+
87
+ def __init__(
88
+ self,
89
+ ckpt: str | None = None,
90
+ config: RecognizerConfig | None = None,
91
+ dedup: DedupConfig | None = None,
92
+ crop: CropConfig | None = None,
93
+ backend=None,
94
+ ) -> None:
95
+ self.config = config or RecognizerConfig()
96
+ self.dedup = dedup or DedupConfig()
97
+ self.crop = crop or CropConfig()
98
+ if backend is None:
99
+ from idp_recognizer import HfRecognizer
100
+
101
+ backend = HfRecognizer(ckpt, self.config)
102
+ self.backend = backend
103
+
104
+ def run(self, image_path: str, layout: PageResult | dict | str) -> PageResult:
105
+ """Every block of the layout comes back, in its original order. Blocks that were not
106
+ transcribed carry ``text: ""`` rather than being dropped."""
107
+ from idp_recognizer import build_requests
108
+
109
+ image = _open(image_path)
110
+ page = _as_page(layout)
111
+ blocks = [b.copy() for b in page.blocks]
112
+
113
+ eligible = resolve_nested_equations(
114
+ [b for b in blocks if is_transcribed(b.label)], self.dedup
115
+ )
116
+ requests, orders = build_requests(eligible, image, self.crop, self.config.table_format)
117
+ texts = self.backend.transcribe(requests)
118
+ if len(texts) != len(orders):
119
+ raise RuntimeError(
120
+ f"recognizer returned {len(texts)} transcriptions for {len(orders)} crops; "
121
+ "results would be misaligned"
122
+ )
123
+
124
+ by_order = dict(zip(orders, texts, strict=True))
125
+ for block in blocks:
126
+ block.text = (by_order.get(block.order) or "").strip()
127
+
128
+ return PageResult(
129
+ image=page.image,
130
+ width=page.width,
131
+ height=page.height,
132
+ blocks=blocks,
133
+ markdown=reconstruct(blocks),
134
+ )
135
+
136
+ def close(self) -> None:
137
+ self.backend.close()
138
+
139
+ def __enter__(self) -> IndicBlockOCR:
140
+ return self
141
+
142
+ def __exit__(self, *exc_info) -> None:
143
+ self.close()
144
+
145
+
146
+ class IndicOCR:
147
+ """Both stages in one process."""
148
+
149
+ def __init__(
150
+ self,
151
+ layout_ckpt: str | None = None,
152
+ recognizer_ckpt: str | None = None,
153
+ layout_config: LayoutConfig | None = None,
154
+ recognizer_config: RecognizerConfig | None = None,
155
+ dedup: DedupConfig | None = None,
156
+ crop: CropConfig | None = None,
157
+ ) -> None:
158
+ # vLLM FIRST. Its EngineCore forks/spawns at construction and must initialise CUDA
159
+ # before the torch layout model touches the device; reversed, the child cannot re-init.
160
+ self.ocr = IndicBlockOCR(recognizer_ckpt, recognizer_config, dedup, crop)
161
+ self.layout = IndicDocLayout(layout_ckpt, layout_config, dedup)
162
+
163
+ def detect(self, image_path: str) -> PageResult:
164
+ return self.layout.detect(image_path)
165
+
166
+ def parse(self, image_path: str) -> PageResult:
167
+ return self.ocr.run(image_path, self.layout.detect(image_path))
168
+
169
+ def close(self) -> None:
170
+ self.ocr.close()
171
+ self.layout.close()
172
+
173
+ def __enter__(self) -> IndicOCR:
174
+ return self
175
+
176
+ def __exit__(self, *exc_info) -> None:
177
+ self.close()