spark-ux commited on
Commit
7b2177e
·
verified ·
1 Parent(s): 0dfea90

Copy from bodhan-ai/indic-doc-parser

Browse files
.gitattributes CHANGED
@@ -33,3 +33,26 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ weights/ocr/tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
+ assets/layout-bengali.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/layout-english-thumb.png filter=lfs diff=lfs merge=lfs -text
39
+ assets/layout-english.png filter=lfs diff=lfs merge=lfs -text
40
+ assets/layout-hindi-thumb.png filter=lfs diff=lfs merge=lfs -text
41
+ assets/layout-hindi.png filter=lfs diff=lfs merge=lfs -text
42
+ assets/layout-santali-thumb.png filter=lfs diff=lfs merge=lfs -text
43
+ assets/layout-santali.png filter=lfs diff=lfs merge=lfs -text
44
+ assets/layout-telugu.png filter=lfs diff=lfs merge=lfs -text
45
+ assets/layout-bengali-thumb.png filter=lfs diff=lfs merge=lfs -text
46
+ assets/layout-urdu-thumb.png filter=lfs diff=lfs merge=lfs -text
47
+ assets/layout-urdu.png filter=lfs diff=lfs merge=lfs -text
48
+ assets/layout-hw-english-thumb.png filter=lfs diff=lfs merge=lfs -text
49
+ assets/layout-hw-english.png filter=lfs diff=lfs merge=lfs -text
50
+ assets/layout-hw-hindi-thumb.png filter=lfs diff=lfs merge=lfs -text
51
+ assets/layout-hw-hindi.png filter=lfs diff=lfs merge=lfs -text
52
+ assets/example-ramanujan-thumb.png filter=lfs diff=lfs merge=lfs -text
53
+ assets/example-ramanujan.png filter=lfs diff=lfs merge=lfs -text
54
+ assets/layout-telugu-thumb.png filter=lfs diff=lfs merge=lfs -text
55
+ assets/diagram.png filter=lfs diff=lfs merge=lfs -text
56
+ assets/cand-hindi-maths-g10-6pr6eq-7dcd5d95-p20.png filter=lfs diff=lfs merge=lfs -text
57
+ assets/gallery-1-english-math-ramanujan.png filter=lfs diff=lfs merge=lfs -text
58
+ assets/gallery-2-telugu-novel.png filter=lfs diff=lfs merge=lfs -text
ARCHITECTURE.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture
2
+
3
+ Two models with a JSON file between them.
4
+
5
+ ```text
6
+ page image ──> IndicDocLayout ──> layout JSON ──> IndicBlockOCR ──> page JSON + markdown
7
+ ```
8
+
9
+ `IndicDocLayout` finds the blocks and orders them. `IndicBlockOCR` transcribes one block crop
10
+ per request. Everything between the two models (cropping, prompt selection, reassembly) is this
11
+ package.
12
+
13
+ ## The call path
14
+
15
+ One page, top to bottom, with the file that owns each step.
16
+
17
+ ```text
18
+ IndicDocParser.parse(image) idp_offline.py
19
+
20
+ IndicDocLayout.detect(image) idp_offline.py
21
+ _open image -> PIL RGB idp_offline.py
22
+ IndicDocLayoutBackend.detect idp_layout.py
23
+ infer -> [y0,x0,y1,x1] at 0-1000 idp_model_infer.py
24
+ convert + clamp_to_page -> pixel [x0,y0,x1,y1] idp_layout.py, blocks.py
25
+ clean_layout drop duplicate boxes idp_blocks.py
26
+ _densify order -> gap-free 0..n-1 idp_layout.py
27
+ = PageResult, every block has text None idp_types.py
28
+
29
+ IndicBlockOCR.run(image, layout) idp_offline.py
30
+ _as_page dict -> PageResult, validated
31
+ is_transcribed drop OCR_SKIP_LABELS idp_contract.py
32
+ resolve_nested_equations drop nested equations idp_blocks.py
33
+ build_requests -> [CropRequest] idp_recognizer.py
34
+ crop_for + area_clamp block -> image idp_crops.py
35
+ prompt_for(block.type) type -> prompt idp_contract.py
36
+ backend.transcribe [CropRequest] -> [str] a recognizer backend
37
+ match by order texts -> blocks idp_offline.py
38
+ reconstruct blocks -> markdown idp_reconstruct.py
39
+ = PageResult, every block has text
40
+ ```
41
+
42
+ Blocks that were skipped are not deleted. They come back with `text: ""`.
43
+
44
+ ## Files by role
45
+
46
+ **Contract and data.** The vocabulary everything else shares.
47
+
48
+ | file | holds |
49
+ | --- | --- |
50
+ | `idp_contract.py` | prompts, the label to type map, which labels are skipped |
51
+ | `idp_types.py` | `Block`, `PageResult`, and every tunable config |
52
+
53
+ **The layout model.** Torch lives here and nowhere else.
54
+
55
+ | file | holds |
56
+ | --- | --- |
57
+ | `idp_model_infer.py` | preprocessing and decode for one page |
58
+ | `idp_model_labels.py` | the 37 classes, pure Python |
59
+ | `idp_model_ppdoc.py` | the PP-DocLayoutV3 subclass |
60
+ | `idp_model_order_loss.py` | reading-order decode, training loss |
61
+
62
+ **Geometry and crops.** No model, no network.
63
+
64
+ | file | holds |
65
+ | --- | --- |
66
+ | `idp_blocks.py` | box math, `clean_layout`, nested-equation dedup |
67
+ | `idp_crops.py` | `crop_for` and `area_clamp` |
68
+
69
+ **Backends.** Two Protocols, five implementations, one per deployment.
70
+
71
+ | class | file | used by |
72
+ | --- | --- | --- |
73
+ | `IndicDocLayoutBackend` | `idp_layout.py` | the real detector |
74
+ | `JsonLayoutBackend` | `idp_layout.py` | replaying a layout file, no torch |
75
+ | `HfRecognizer` | `idp_recognizer.py` | plain transformers, the Hub package |
76
+
77
+
78
+ **Orchestration.**
79
+
80
+ | file | holds |
81
+ | --- | --- |
82
+ | `idp_offline.py` | `IndicDocLayout`, `IndicBlockOCR`, `IndicDocParser` |
83
+ | `idp_recognizer.py` | `CropRequest`, `build_requests`, the recognizer Protocol |
84
+ | `idp_reconstruct.py` | blocks to markdown, math and hyphen repair |
85
+
86
+ ## Invariants
87
+
88
+ 1. **Two box conventions.** The model emits `[y0, x0, y1, x1]` normalised to 0-1000. The
89
+ pipeline uses pixel `[x0, y0, x1, y1]`.
90
+ 2. **`order` must be gap-free and 0-based.** Transcriptions are matched back to blocks by
91
+ position, so a gap or a duplicate moves text onto the wrong block.
92
+ 3. **The crop clamp is on area, not on a side.** Pinning a side explodes elongated crops. See
93
+ `CropConfig` in `idp_types.py`.
94
+ 4. **`type` selects the prompt.** A wrong `type` changes what the model was asked to do, not
95
+ just how a block is labelled. Unrecognised labels are rejected for this reason.
README.md ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ - as
5
+ - bn
6
+ - brx
7
+ - doi
8
+ - gu
9
+ - hi
10
+ - kn
11
+ - ks
12
+ - kok
13
+ - mai
14
+ - ml
15
+ - mni
16
+ - mr
17
+ - ne
18
+ - or
19
+ - pa
20
+ - sa
21
+ - sat
22
+ - sd
23
+ - ta
24
+ - te
25
+ - ur
26
+ pipeline_tag: image-to-text
27
+ tags:
28
+ - ocr
29
+ - document-parsing
30
+ - layout-analysis
31
+ - reading-order
32
+ - indic
33
+ - vision-language-model
34
+ - qwen
35
+ - rt-detr
36
+ ---
37
+
38
+
39
+ <div align="center">
40
+
41
+ [![Pipeline](https://img.shields.io/badge/Pipeline-Layout%20%2B%20OCR-F97316?style=flat)](#the-two-models)
42
+ [![Layout](https://img.shields.io/badge/IndicDocLayout-33M-F97316?style=flat)](#the-two-models)
43
+ [![Recognizer](https://img.shields.io/badge/IndicBlockOCR-0.8B-F97316?style=flat)](#the-two-models)
44
+ [![Languages](https://img.shields.io/badge/Languages-23-F97316?style=flat)](#supported-languages)
45
+ [![License](https://img.shields.io/badge/License-Apache--2.0-F97316?style=flat)](#license)
46
+
47
+ </div>
48
+
49
+ **Document parsing for English and 22 Indian languages, printed and handwritten.** A page image
50
+ in; reading-ordered Markdown out, with math as LaTeX and tables as HTML or Markdown, plus
51
+ per-block JSON.
52
+
53
+ <div align="center">
54
+
55
+ <img src="assets/diagram.png" alt="IndicDocParser: page image to layout detection with reading order, then block-level OCR, then Markdown" width="100%">
56
+
57
+ </div>
58
+
59
+ IndicDocParser reads a document page and returns its text in reading order. It is a modular,
60
+ two-stage parser: **IndicDocLayout** detects the blocks on the page and orders them, and
61
+ **IndicBlockOCR** transcribes the textual blocks. The two stages communicate through a structured
62
+ JSON file, so either stage can be used independently or replaced with another implementation.
63
+
64
+ [`ARCHITECTURE.md`](ARCHITECTURE.md) traces one page through the whole call path, names what
65
+ each module does, and lists the invariants that break the output silently when violated.
66
+
67
+ ---
68
+
69
+ <h2 id="examples" style="color:#F97316;">Examples</h2>
70
+
71
+ Detected blocks with their reading order on the left, the transcription on the right.
72
+
73
+ <div align="center">
74
+ <img src="assets/gallery-1-english-math-ramanujan.png" alt="A page from Ramanujan's notebooks: text and display equations detected in reading order, with the transcription rendering the mathematics as LaTeX" width="100%">
75
+ <p><b>Example #1. English page with dense mathematics.</b></p>
76
+ </div>
77
+
78
+ <div align="center">
79
+ <img src="assets/gallery-2-telugu-novel.png" alt="A printed Telugu novel page: paragraph blocks and a page number detected and numbered in reading order, with the Telugu transcription beside it" width="100%">
80
+ <p><b>Example #2. Printed Telugu page.</b></p>
81
+ </div>
82
+
83
+ <div align="center">
84
+ <img src="assets/cand-hindi-maths-g10-6pr6eq-7dcd5d95-p20.png" alt="A handwritten Hindi maths exercise on ruled paper: alternating Equation and Paragraph blocks detected in reading order, with the transcription rendering the algebra as LaTeX" width="100%">
85
+ <p><b>Example #3. Handwritten Hindi maths.</b></p>
86
+ </div>
87
+
88
+ ---
89
+
90
+ <h2 id="model-summary" style="color:#F97316;">Model Summary</h2>
91
+
92
+ | | IndicDocLayout | IndicBlockOCR |
93
+ | --- | --- | --- |
94
+ | **Role** | Layout detection + reading order | Block-level text recognition |
95
+ | **Architecture** | PP-DocLayoutV3 / RT-DETR | Qwen3.5-0.8B |
96
+ | **Parameters** | 33 M | 0.8 B |
97
+ | **Precision** | fp32 | bf16 |
98
+ | **In this repo** | `weights/layout` (133 MB) | `weights/ocr` (1.7 GB) |
99
+ | **Output** | Layout JSON | Markdown + block JSON |
100
+
101
+
102
+ IndicBlockOCR uses the **Sarvam-30B tokenizer**, with a vocabulary designed to cover Indian
103
+ scripts. IndicDocLayout is a fine-tune of PP-DocLayoutV3/RT-DETR, trained with a 37-class
104
+ taxonomy designed for education-domain documents.
105
+
106
+ IndicDocLayout predicts a labelled bounding box for each detected layout element.
107
+ The 37 supported labels are:
108
+
109
+ > Advertisement, Answer, Author, Chapter-end-section, Chapter-title, Chart, Code, Contact-info, Dateline, Diagram, Equation, Expression, Flag, Folio, Footer, Footnote, Header, Image, Image-caption, Index, Infobox, List, MCQ, Page-number, Paragraph, Placeholder-text, Question, Reference, Section-title, Solved-example, Sub-section-title, Sub-sub-section-title, Table, Table-caption, Table-of-contents, Title, Website-link
110
+
111
+ ---
112
+
113
+ <h2 id="supported-languages" style="color:#F97316;">Supported languages</h2>
114
+
115
+ **Printed** page recognition is supported across English and the 22 constitutionally recognised Indian languages: Assamese, Bengali, Bodo, Dogri,
116
+ Gujarati, Hindi, Kannada, Kashmiri, Konkani, Maithili, Malayalam, Manipuri, Marathi, Nepali,
117
+ Odia, Punjabi, Sanskrit, Santali, Sindhi, Tamil, Telugu, Urdu.
118
+
119
+ **Handwriting** recognition currently supports English and 12 Indian languages: Hindi, Bengali,
120
+ Telugu, Marathi, Tamil, Gujarati, Kannada, Malayalam, Odia, Punjabi, Assamese, and Urdu.
121
+
122
+ Handwriting quality is still a work in progress, particularly across different writing styles. We are working on improving recognition and extending support to additional languages.
123
+
124
+ ---
125
+
126
+ <h2 id="usage" style="color:#F97316;">Usage</h2>
127
+
128
+ <h3 style="color:#F97316;">Installation</h3>
129
+
130
+ The repo ships an installer that reads your driver and picks matching CUDA wheels. If you work in a
131
+ virtual environment, please activate it first, as the installer installs into whichever
132
+ Python is active.
133
+
134
+ ```bash
135
+ IDP=$(python -c "from huggingface_hub import snapshot_download as d; print(d('bodhan-ai/indic-doc-parser'))")
136
+ cd "$IDP" && ./install.sh
137
+ ```
138
+
139
+ It will use `uv` if that is available, and `pip` otherwise. Where running a shell script is not
140
+ convenient, [TROUBLESHOOTING.md](TROUBLESHOOTING.md) lists the two commands it runs.
141
+
142
+ <h3 style="color:#F97316;">Basic inference</h3>
143
+
144
+ ```python
145
+ import sys
146
+ from huggingface_hub import snapshot_download
147
+
148
+ repo = snapshot_download("bodhan-ai/indic-doc-parser")
149
+ sys.path.insert(0, repo) # the code ships in the repo
150
+ from indic_doc_parser import IndicDocParser
151
+
152
+ parser = IndicDocParser.from_pretrained(repo)
153
+
154
+ page = parser.parse("page.png")
155
+ print(page["markdown"]) # reading-ordered Markdown
156
+ ```
157
+
158
+ `page` also carries the per-block detail, which you can save as follows:
159
+
160
+ ```python
161
+ import json
162
+
163
+ with open("page.json", "w", encoding="utf-8") as f:
164
+ json.dump(page, f, ensure_ascii=False, indent=2)
165
+ ```
166
+
167
+ <h3 style="color:#F97316;">Running one stage at a time</h3>
168
+
169
+ To run the two stages separately:
170
+
171
+ ```python
172
+ from indic_doc_parser import IndicDocLayout, IndicBlockOCR
173
+
174
+ layout = IndicDocLayout(f"{repo}/weights/layout").detect("page.png")
175
+ page = IndicBlockOCR(f"{repo}/weights/ocr").run("page.png", layout)
176
+ ```
177
+
178
+ `run()` takes a layout object, a dict, or the path to a layout JSON file.
179
+
180
+ ---
181
+
182
+ <h2 id="output" style="color:#F97316;">Output</h2>
183
+
184
+ `parser.parse("page.png")` returns the page metadata and its blocks in reading order:
185
+
186
+ ```json
187
+ {
188
+ "image": "sample1.png",
189
+ "width": 800,
190
+ "height": 1273,
191
+ "blocks": [
192
+ {"order": 0, "label": "Header", "type": "PageHeader",
193
+ "bbox_xyxy": [345.6, 51.7, 437.1, 114.7], "conf": 0.6, "text": ""},
194
+ {"order": 1, "label": "Page-number", "type": "PageNumber",
195
+ "bbox_xyxy": [367.8, 78.9, 413.2, 107.4], "conf": 0.747, "text": "229"},
196
+ {"order": 2, "label": "Paragraph", "type": "Text",
197
+ "bbox_xyxy": [77.9, 121.3, 711.8, 199.4], "conf": 0.863,
198
+ "text": "Thus we see that, if we can prove that twice the L.H.S. of (30) ..."}
199
+ ]
200
+ }
201
+ ```
202
+
203
+ | field | meaning |
204
+ | --- | --- |
205
+ | `order` | reading-order rank, 0-based and gap-free |
206
+ | `label` | the raw IndicDocLayout class (37-class taxonomy) |
207
+ | `type` | coarse pipeline category: `Text`, `Table`, `Equation`, `Title`, ... |
208
+ | `bbox_xyxy` | pixel box `[x0, y0, x1, y1]` |
209
+ | `conf` | detection confidence |
210
+ | `text` | transcription; `""` for blocks not sent to the recognizer |
211
+
212
+ **Note:** Figures, charts, advertisements, running headers, and footers are not sent through the recognizer
213
+ by default. They remain in the JSON with `text: ""`, so you can see what was detected and where.
214
+ Page numbers and other margin text such as folios are transcribed.
215
+
216
+ <h3 style="color:#F97316;">Schemas</h3>
217
+
218
+ Machine-readable JSON Schema for each envelope, in [`schemas/`](schemas):
219
+
220
+ | file | describes |
221
+ | --- | --- |
222
+ | `layout_output.schema.json` | The layout file: what **IndicDocLayout** writes and **IndicBlockOCR** reads. Blocks and reading order, before any text is read, so there is **no** `text` key at all. |
223
+ | `parse_output.schema.json` | The parsed page shown above. Every block now has `text`; `""` means the block was detected but deliberately not sent to the recognizer. |
224
+
225
+ A layout from your own detector must use a `label` from the 37-class taxonomy, or declare `type`
226
+ explicitly. An unrecognised label is rejected rather than silently read as prose.
227
+
228
+ <h3 style="color:#F97316;">Table format</h3>
229
+
230
+ Tables come back as HTML by default. Choose the format when you construct the parser:
231
+
232
+ ```python
233
+ parser = IndicDocParser.from_pretrained(repo) # HTML (default)
234
+ parser = IndicDocParser.from_pretrained(repo, table_format="markdown") # Markdown
235
+ ```
236
+
237
+ ---
238
+
239
+ <h2 id="performance" style="color:#F97316;">Performance</h2>
240
+
241
+ <h3 style="color:#F97316;">OmniDocBench 1.6 (english subset)</h3>
242
+
243
+ | OmniDocBench 1.6 (english subset) | Overall↑ | TextEdit↓ | FormulaCDM↑ | TableTEDS↑ | TableTEDS-S↑ | Read OrderEdit↓ |
244
+ | --- | :---: | :---: | :---: | :---: | :---: | :---: |
245
+ | PaddleOCRVL-1.6 | 96.36 | 0.03 | 98.55 | 93.37 | 96.33 | 0.09 |
246
+ | Chandra OCR 2 | 93.11 | 0.04 | 96.93 | 86.07 | 90.34 | 0.09 |
247
+ | **IndicOCR (ours)** | **92.76** | **0.04** | **97.53** | **85.10** | **90.58** | **0.11** |
248
+ | GPT-5.6-sol | 92.46 | 0.04 | 95.42 | 85.87 | 90.98 | 0.10 |
249
+ | Gemini 3.1 Pro | 91.15 | 0.06 | 95.53 | 83.46 | 88.77 | 0.13 |
250
+ | Surya OCR 2 (model) | 91.13 | 0.04 | 95.67 | 81.61 | 86.37 | 0.10 |
251
+ | Sarvam Vision | 90.08 | 0.04 | 97.62 | 76.82 | 82.01 | 0.10 |
252
+ | Gemma 31B | 86.71 | 0.09 | 89.48 | 79.79 | 85.19 | 0.19 |
253
+ | Nemotron Parse 2 | 79.12 | 0.159 | 78.94 | 74.32 | 81.09 | 0.29 |
254
+
255
+ <h3 style="color:#F97316;">olmOCR-Bench (<a href="https://huggingface.co/datasets/sarvamai/olmOCR-Bench-English" style="color:#F97316;">english subset</a>)</h3>
256
+
257
+ | OlmoOCRBench ([english subset](https://huggingface.co/datasets/sarvamai/olmOCR-Bench-English)) | Overall↑ | arxiv_math↑ | baseline↑ | headers_footers↑ | long_tiny_text↑ | multi_column↑ | old_scans↑ | old_scans_math↑ | table_tests↑ |
258
+ | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
259
+ | Chandra OCR 2 | 85.9 | 86.7 | 99.8 | 91.5 | 93.7 | 84.7 | 51 | 88.2 | 92.2 |
260
+ | Sarvam Vision | 84.3 | 86.5 | 99.6 | 96.3 | 91 | 82.2 | 49.8 | 81 | 88.3 |
261
+ | Gemini 3.1 Pro | 82.6 | 90.5 | 99 | 82.9 | 88.5 | 81.6 | 47 | 84.3 | 87.3 |
262
+ | **IndicOCR (ours)** | **82.2** | **83.2** | **99.4** | **92.9** | **89.8** | **76** | **48.3** | **77.7** | **90** |
263
+ | Surya OCR 2 (model) | 81.4 | 82.5 | 99.8 | 92.9 | 79.9 | 85.1 | 42.8 | 84.3 | 84.2 |
264
+ | Gemma 31B | 80.4 | 79 | 99.4 | 92.9 | 89.8 | 80.5 | 45.8 | 73.8 | 82.2 |
265
+ | PaddleOCRVL-1.6 | 78.7 | 85.1 | 98.4 | 96.2 | 75.3 | 83.9 | 39 | 68.3 | 83 |
266
+ | GPT | 78 | 79.3 | 93.9 | 95.4 | 87.8 | 77.4 | 43.7 | 64.6 | 82.2 |
267
+ | Nemotron Parse 2 | 68.2 | 64 | 96.7 | 90 | 79.6 | 72.8 | 31.9 | 28.6 | 81.8 |
268
+
269
+ <h3 style="color:#F97316;">IndicOCR-PR: printed accuracy by language (higher is better)</h3>
270
+
271
+ Word-level accuracy, reported as 100 x (1 - WER).
272
+
273
+ | Language | Sarvam Vision | **IndicOCR (ours)** | Gemini 3.1 Pro | SuryaOCR | Gemma 31B | Chandra OCR 2 |
274
+ | --- | :---: | :---: | :---: | :---: | :---: | :---: |
275
+ | **Overall** | 86.6 | **86.2** | 80.4 | 67.9 | 66.3 | 64.2 |
276
+ | Assamese | 89.5 | 90.2 | 90.7 | 86.4 | 70.6 | 73.5 |
277
+ | Bodo | 91.0 | 86.5 | 91.0 | 55.6 | 68.1 | 46.6 |
278
+ | Bengali | 91.6 | 91.4 | 92.5 | 81.1 | 83.9 | 79.2 |
279
+ | Dogri | 85.8 | 81.7 | 83.7 | 60.5 | 64.4 | 55.8 |
280
+ | English | 96.6 | 97.0 | 97.7 | 93.8 | 97.2 | 91.3 |
281
+ | Gujarati | 91.6 | 91.7 | 92.8 | 79.6 | 81.6 | 73.0 |
282
+ | Hindi | 95.7 | 96.0 | 96.3 | 90.3 | 93.7 | 89.3 |
283
+ | Konkani | 93.6 | 93.7 | 93.5 | 90.5 | 76.9 | 85.5 |
284
+ | Kannada | 88.8 | 88.0 | 89.8 | 75.7 | 68.3 | 69.6 |
285
+ | Kashmiri | 43.3 | 52.2 | 38.1 | 23.4 | 19.9 | 17.6 |
286
+ | Malayalam | 90.6 | 89.9 | 90.6 | 76.5 | 72.0 | 68.3 |
287
+ | Manipuri | 81.9 | 83.8 | 0.8 | 0.1 | 0.1 | 0.0 |
288
+ | Marathi | 93.9 | 93.5 | 94.5 | 84.3 | 89.1 | 83.1 |
289
+ | Maithili | 86.7 | 83.0 | 86.7 | 67.6 | 76.3 | 66.1 |
290
+ | Nepali | 92.5 | 91.5 | 93.7 | 87.6 | 87.2 | 82.1 |
291
+ | Odia | 77.5 | 75.7 | 84.8 | 64.5 | 38.7 | 62.6 |
292
+ | Punjabi | 92.2 | 93.2 | 93.5 | 86.3 | 75.1 | 84.1 |
293
+ | Sanskrit | 82.0 | 76.2 | 83.7 | 57.8 | 60.8 | 55.8 |
294
+ | Sindhi | 89.2 | 87.1 | 86.3 | 80.5 | 74.5 | 71.4 |
295
+ | Santhali | 71.9 | 74.7 | 0.2 | 0.1 | 0.2 | 0.0 |
296
+ | Tamil | 94.2 | 91.3 | 94.4 | 79.9 | 83.3 | 79.0 |
297
+ | Telugu | 84.3 | 82.3 | 85.5 | 63.1 | 66.6 | 59.6 |
298
+ | Urdu | 87.1 | 85.9 | 88.0 | 76.4 | 76.6 | 74.4 |
299
+
300
+ <h3 style="color:#F97316;">IndicOCR-HW: handwriting accuracy by language (higher is better)</h3>
301
+
302
+ Word-level accuracy, reported as 100 x (1 - WER).
303
+
304
+ | Language | Gemini 3.1 Pro | **IndicOCR (ours)** | Sarvam Vision | Gemma 31B | Chandra OCR 2 | SuryaOCR |
305
+ | --- | :---: | :---: | :---: | :---: | :---: | :---: |
306
+ | **Overall** | 72.0 | **66.7** | 55.4 | 33.9 | 24.7 | 23.0 |
307
+ | Assamese | 71.6 | 66.1 | 47.8 | 24.1 | 8.9 | 17.8 |
308
+ | Bengali | 74.8 | 71.3 | 58.3 | 35.1 | 6.6 | 10.0 |
309
+ | English | 84.4 | 80.7 | 77.7 | 78.5 | 78.2 | 72.7 |
310
+ | Gujarati | 60.0 | 55.9 | 39.2 | 23.7 | 11.8 | 11.5 |
311
+ | Hindi | 83.1 | 77.6 | 72.3 | 70.7 | 54.6 | 42.7 |
312
+ | Kannada | 73.8 | 69.6 | 57.7 | 17.2 | 11.5 | 13.2 |
313
+ | Malayalam | 63.9 | 60.5 | 45.6 | 16.0 | 15.7 | 11.8 |
314
+ | Marathi | 79.0 | 70.2 | 61.8 | 56.5 | 35.4 | 28.8 |
315
+ | Odia | 66.7 | 68.2 | 40.6 | 15.5 | 19.4 | 19.9 |
316
+ | Punjabi | 70.1 | 69.4 | 54.8 | 11.7 | 11.5 | 15.7 |
317
+ | Tamil | 80.5 | 76.8 | 60.5 | 33.4 | 18.8 | 16.8 |
318
+ | Telugu | 72.0 | 53.5 | 59.1 | 32.0 | 20.8 | 14.6 |
319
+ | Urdu | 54.4 | 46.4 | 44.4 | 25.6 | 27.6 | 22.6 |
320
+
321
+ ---
322
+
323
+ <h2 id="limitations" style="color:#F97316;">Limitations</h2>
324
+
325
+ Reading order remains a challenge for **complex, multi-column layouts**. Handwriting recognition
326
+ is also still being improved, particularly across different writing styles and writing
327
+ characteristics.
328
+
329
+ We are also extending handwriting support to additional Indic languages.
330
+
331
+
332
+ ---
333
+
334
+ <h2 id="hardware" style="color:#F97316;">Hardware</h2>
335
+
336
+ Latency and throughput numbers to follow.
337
+
338
+ ---
339
+
340
+ <h2 id="license" style="color:#F97316;">License</h2>
341
+
342
+
343
+ Released under [Bodhan Open License 1.0]().
344
+
345
+ The release incorporates components distributed under Apache 2.0, including PP-DocLayoutV3,
346
+ Qwen3.5, and the Sarvam-30B tokenizer. See the repository license and the corresponding upstream
347
+ licenses for the applicable terms and attribution requirements.
348
+ ---
349
+
350
+ <h2 id="citation" style="color:#F97316;">Citation</h2>
351
+
352
+ ```bibtex
353
+ @misc{indicdocparser2026,
354
+ title = {IndicDocParser: Multilingual Document Parsing for English and 22 Indian Languages},
355
+ author = {Bodhan.AI},
356
+ year = {2026},
357
+ url = {https://huggingface.co/bodhan-ai/indic-doc-parser}
358
+ }
359
+ ```
TROUBLESHOOTING.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # IndicDocParser: troubleshooting
2
+
3
+ Install and runtime problems, and what to do about them. The model card is
4
+ [README.md](README.md).
5
+
6
+ Nearly every install problem is the torch/torchvision/CUDA triangle.
7
+
8
+ **Installing without the script**, on Colab, Windows, or anywhere a shell script is awkward. These
9
+ are the two commands `install.sh` runs:
10
+
11
+ ```bash
12
+ pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124 # your CUDA line
13
+ pip install -r requirements.txt
14
+ ```
15
+
16
+ Keep `torch` and `torchvision` in that one command. `torchvision` pins an exact `torch`, so
17
+ installing them separately lets pip pick a mismatched pair. A plain `pip install torch` also takes
18
+ the newest wheel, currently CUDA 13, which on an older driver initialises to CPU with nothing but a
19
+ warning. Use the index for *your* CUDA: `cu121`, `cu124`, `cu128`, `cu130`, or `cpu`.
20
+
21
+ **`ImportError` after `ImportError`, or complaints that torchvision is too old.** `torchvision`
22
+ pins an exact `torch`, so if the two were installed separately pip may have paired them wrongly.
23
+ Reinstall both in one command, from one index:
24
+
25
+ ```bash
26
+ pip install --force-reinstall torch torchvision --index-url https://download.pytorch.org/whl/cu124
27
+ ```
28
+
29
+ **`CUDA initialization: The NVIDIA driver on your system is too old`, then everything runs on CPU.**
30
+ Plain `pip install torch` fetched a wheel built for a newer CUDA than your driver. Check with
31
+ `nvidia-smi`, then reinstall from the matching index. Verify with:
32
+
33
+ ```python
34
+ import torch; print(torch.__version__, torch.cuda.is_available())
35
+ ```
36
+
37
+ **`Qwen3VLVideoProcessor requires the Torchvision library`.** The recognizer needs `torchvision`;
38
+ the layout stage does not. Install it *together with* torch, from the same index.
39
+
40
+ **`RTDetrHungarianMatcher requires the scipy library`.** You are on an old revision of this repo.
41
+ Update to the current revision, which no longer builds the matcher during inference.
assets/cand-hindi-maths-g10-6pr6eq-7dcd5d95-p20.png ADDED

Git LFS Details

  • SHA256: 5310dbdf92b9a2e14983f1cbc7fdd743ef7745fea206a011bfce02e8c8dc887d
  • Pointer size: 132 Bytes
  • Size of remote file: 3.65 MB
assets/diagram.png ADDED

Git LFS Details

  • SHA256: 106190e7f13a8415e07e8bb20fce449c754e9ec87300372acc276b0236de15cd
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
assets/gallery-1-english-math-ramanujan.png ADDED

Git LFS Details

  • SHA256: 3be2e06982368c8c932fcbbfb736b385e46bb374ec8c04e4c380bef3fd5abcbe
  • Pointer size: 132 Bytes
  • Size of remote file: 2.76 MB
assets/gallery-2-telugu-novel.png ADDED

Git LFS Details

  • SHA256: 0d2d3559483ba6b4d84cacfaa508dad711ef3f1de03c9067dd020906d81ef00f
  • Pointer size: 132 Bytes
  • Size of remote file: 4.12 MB
idp_blocks.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/blocks.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Layout cleanup: geometry, duplicate suppression, nested-equation resolution.
6
+
7
+ Pure geometry on Blocks -- no PIL, no torch, no page image -- so the rules that decide what gets
8
+ transcribed stay testable with none of the GPU stack installed.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from idp_types import Block, DedupConfig
14
+ from idp_contract import HEAD_FOOT, MARGINALIA, is_transcribed
15
+
16
+ TEXTLIKE = frozenset({"Text", "Title", "SectionHeader", "Caption", "Footnote"})
17
+
18
+
19
+ def area(bbox) -> float:
20
+ return max(0.0, bbox[2] - bbox[0]) * max(0.0, bbox[3] - bbox[1])
21
+
22
+
23
+ def contained_frac(small, big) -> float:
24
+ """Fraction of ``small``'s area lying inside ``big``."""
25
+ ix0, iy0 = max(small[0], big[0]), max(small[1], big[1])
26
+ ix1, iy1 = min(small[2], big[2]), min(small[3], big[3])
27
+ inter = max(0.0, ix1 - ix0) * max(0.0, iy1 - iy0)
28
+ a = area(small)
29
+ return inter / a if a > 0 else 0.0
30
+
31
+
32
+ def clamp_to_page(bbox, width: int, height: int) -> list[float]:
33
+ return [
34
+ max(0.0, bbox[0]),
35
+ max(0.0, bbox[1]),
36
+ min(float(width), bbox[2]),
37
+ min(float(height), bbox[3]),
38
+ ]
39
+
40
+
41
+ def clean_layout(blocks: list[Block], cfg: DedupConfig | None = None) -> list[Block]:
42
+ """Drop duplicate and spurious boxes. Three rules; survivors keep input order.
43
+
44
+ 1. Nested duplicates -- a box ``cfg.contain`` inside a larger box of the same group goes.
45
+ 2. One header, one footer -- only the largest of each survives.
46
+ 3. Empty frames -- a header/footer wrapping nothing is dropped.
47
+ """
48
+ cfg = cfg or DedupConfig()
49
+ n = len(blocks)
50
+ box = lambda i: blocks[i].bbox_xyxy # noqa: E731
51
+ drop: set[int] = set()
52
+
53
+ # Header/Footer are in neither group: rules 2 and 3 govern them entirely.
54
+ groups = (
55
+ lambda label: label not in MARGINALIA,
56
+ lambda label: label in MARGINALIA and label not in HEAD_FOOT,
57
+ )
58
+ for in_group in groups:
59
+ idxs = sorted(
60
+ (i for i in range(n) if in_group(blocks[i].label)),
61
+ key=lambda i: area(box(i)),
62
+ reverse=True,
63
+ )
64
+ kept: list[int] = []
65
+ for i in idxs:
66
+ has_text = is_transcribed(blocks[i].label)
67
+ # A block is never absorbed into a container that will not itself be transcribed:
68
+ # otherwise a caption inside a Diagram is dropped for a container that is then
69
+ # skipped, losing the text. One-directional -- a non-transcribed box may go anywhere.
70
+ if any(
71
+ contained_frac(box(i), box(j)) >= cfg.contain
72
+ and (is_transcribed(blocks[j].label) or not has_text)
73
+ for j in kept
74
+ ):
75
+ drop.add(i)
76
+ else:
77
+ kept.append(i)
78
+
79
+ for label in HEAD_FOOT:
80
+ group = [i for i in range(n) if blocks[i].label == label and i not in drop]
81
+ if not group:
82
+ continue
83
+ biggest = max(group, key=lambda i: area(box(i)))
84
+ drop.update(i for i in group if i != biggest)
85
+ # Measured over range(n), NOT the survivors: a header whose only occupant was already
86
+ # removed as a nested duplicate is still occupied, and deleting it loses a real header.
87
+ wraps = any(
88
+ blocks[k].label != label and contained_frac(box(k), box(biggest)) >= cfg.wrap
89
+ for k in range(n)
90
+ )
91
+ if not wraps:
92
+ drop.add(biggest)
93
+
94
+ return [blocks[i] for i in range(n) if i not in drop]
95
+
96
+
97
+ def _nested_in(inner, outer, thresh: float) -> bool:
98
+ # The strictness check stops two boxes over the same region each claiming to contain the
99
+ # other, which would drop both.
100
+ inner_area = area(inner)
101
+ if inner_area <= 0:
102
+ return False
103
+ return contained_frac(inner, outer) >= thresh and inner_area < 0.95 * area(outer)
104
+
105
+
106
+ def _absorbs_equation(container: Block, mode: str) -> bool:
107
+ if container.type == "Equation":
108
+ # A display array is one outer box plus a box per row; collapsing keeps it one block.
109
+ return mode != "text_only"
110
+ if container.type in TEXTLIKE:
111
+ # The text prompt already renders inline math as $...$, so a separate equation box
112
+ # would emit it a second time as display math.
113
+ return mode != "eq_only"
114
+ return False
115
+
116
+
117
+ def resolve_nested_equations(blocks: list[Block], cfg: DedupConfig | None = None) -> list[Block]:
118
+ """Drop equation boxes nested in a container that already covers them.
119
+
120
+ Runs before cropping -- it needs only boxes and types, so resolving first avoids building
121
+ crops that are immediately discarded.
122
+ """
123
+ cfg = cfg or DedupConfig()
124
+ if not cfg.nest:
125
+ return list(blocks)
126
+
127
+ drop: set[int] = set()
128
+ for i, inner in enumerate(blocks):
129
+ if inner.type != "Equation":
130
+ continue
131
+ for j, container in enumerate(blocks):
132
+ if (
133
+ i != j
134
+ and _nested_in(inner.bbox_xyxy, container.bbox_xyxy, cfg.nested)
135
+ and _absorbs_equation(container, cfg.mode)
136
+ ):
137
+ drop.add(i)
138
+ break
139
+ return [b for k, b in enumerate(blocks) if k not in drop]
idp_contract.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/templates/contract.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """IndicDocParser's contract: prompts, block taxonomy, output schema.
6
+
7
+ Two vocabularies meet here and mixing them fails silently. **Labels** are what IndicDocLayout
8
+ emits (``ocr.layout.labels.CLASSES``); MARGINALIA and HEAD_FOOT match them case-SENSITIVELY.
9
+ **Types** are the coarse categories a label maps to, and select the prompt. OCR_SKIP_LABELS is
10
+ the exception: matched case-insensitively.
11
+
12
+ stdlib-only -- importable with no GPU stack and no PIL.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from enum import StrEnum
18
+
19
+
20
+ class TableFormat(StrEnum):
21
+ """HTML is the default: colspan/rowspan and in-cell breaks have no GFM spelling, so a
22
+ merged-cell table rendered as markdown silently loses its structure."""
23
+
24
+ HTML = "html"
25
+ MARKDOWN = "markdown"
26
+
27
+
28
+ TEXT_PROMPT = (
29
+ "Transcribe the text in this image. Write any mathematical expressions in LaTeX, "
30
+ "using $...$ for inline math and $$...$$ for display equations."
31
+ )
32
+ EQUATION_PROMPT = "Output only the LaTeX for this equation image."
33
+ TABLE_PROMPTS = {
34
+ TableFormat.HTML: (
35
+ "Convert this table image to HTML. Preserve the structure exactly, using colspan and "
36
+ "rowspan for merged cells and <br/> for line breaks within a cell. "
37
+ "Output only the HTML table."
38
+ ),
39
+ TableFormat.MARKDOWN: (
40
+ "Convert this table image to a GitHub-flavored markdown table. Output only the table."
41
+ ),
42
+ }
43
+
44
+
45
+ def prompt_for(block_type: str, table_format: TableFormat = TableFormat.HTML) -> str:
46
+ if block_type == "Table":
47
+ return TABLE_PROMPTS[TableFormat(table_format)]
48
+ if block_type == "Equation":
49
+ return EQUATION_PROMPT
50
+ return TEXT_PROMPT
51
+
52
+
53
+ # Labels absent from this map fall through to "Text" by design -- Question, Paragraph, Answer,
54
+ # List, MCQ, Code, Reference and the rest all carry prose.
55
+ LABEL_TO_TYPE = {
56
+ "table": "Table",
57
+ "table-caption": "Caption",
58
+ "equation": "Equation",
59
+ "expression": "Equation",
60
+ "diagram": "Figure",
61
+ "chart": "Figure",
62
+ "image": "Picture",
63
+ "image-caption": "Caption",
64
+ "title": "Title",
65
+ "chapter-title": "Title",
66
+ "section-title": "SectionHeader",
67
+ "sub-section-title": "SectionHeader",
68
+ "sub-sub-section-title": "SectionHeader",
69
+ "header": "PageHeader",
70
+ "footer": "PageFooter",
71
+ "page-number": "PageNumber",
72
+ "folio": "PageNumber",
73
+ "footnote": "Footnote",
74
+ }
75
+
76
+
77
+ def map_label(label) -> str:
78
+ """Label -> pipeline type. Unknown labels are ``Text``."""
79
+ return LABEL_TO_TYPE.get(str(label).strip().lower(), "Text")
80
+
81
+
82
+ # Exactly the types map_label can produce, less DROP_TYPES. test_contract asserts this, so a
83
+ # dead or undocumented type cannot creep in.
84
+ KEPT_BLOCK_TYPES = (
85
+ "Text",
86
+ "Title",
87
+ "SectionHeader",
88
+ "Table",
89
+ "Equation",
90
+ "Caption",
91
+ "Footnote",
92
+ "PageHeader",
93
+ "PageFooter",
94
+ "PageNumber",
95
+ )
96
+
97
+ # Never cropped, never reconstructed: pictorial regions have no text and invite hallucination.
98
+ # Deliberately narrow -- page numbers and margin text DO reach the recognizer.
99
+ DROP_TYPES = frozenset({"Figure", "Picture"})
100
+
101
+ # Cleaned as their own group so a page-spanning paragraph cannot swallow a page number.
102
+ MARGINALIA = frozenset({"Header", "Footer", "Page-number", "Folio"})
103
+ HEAD_FOOT = ("Header", "Footer")
104
+
105
+ # Never sent to the recognizer, but NOT deleted: these keep their place in the output with
106
+ # text "", so a consumer can still see what was detected and where.
107
+ OCR_SKIP_LABELS = frozenset({"header", "footer", "diagram", "image", "chart", "advertisement"})
108
+
109
+
110
+ def is_transcribed(label) -> bool:
111
+ return str(label).strip().lower() not in OCR_SKIP_LABELS
112
+
113
+
114
+ OUTPUT_BLOCK_SCHEMA = {
115
+ "order": "int -- reading-order rank (0 = first)",
116
+ "label": "str -- raw IndicDocLayout label",
117
+ "type": "str -- pipeline type (see KEPT_BLOCK_TYPES)",
118
+ "bbox_xyxy": "[float x4] -- pixel [x0, y0, x1, y1], clamped to the page",
119
+ "conf": "float -- detection confidence",
120
+ "text": "str -- transcription; '' when not sent to the recognizer",
121
+ }
122
+
123
+ OUTPUT_PAGE_SCHEMA = {
124
+ "image": "str -- source image filename",
125
+ "width": "int -- page width in pixels",
126
+ "height": "int -- page height in pixels",
127
+ "blocks": "[BLOCK] -- in reading order",
128
+ }
idp_crops.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/crops.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Turning boxes into the images the recognizer sees.
6
+
7
+ Split from ``engine.blocks`` because this is the only part that needs PIL -- keeping the
8
+ geometry PIL-free is what lets the cleanup rules be tested without an image library.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ from typing import TYPE_CHECKING
15
+
16
+ from idp_types import Block, CropConfig
17
+ from idp_contract import DROP_TYPES, is_transcribed
18
+
19
+ if TYPE_CHECKING: # pragma: no cover
20
+ from PIL.Image import Image
21
+
22
+
23
+ def area_clamp(image: Image, cfg: CropConfig | None = None) -> Image:
24
+ """Scale a crop so its area lands in ``[cfg.min_px, cfg.max_px]``, preserving aspect.
25
+
26
+ Area rather than a side: pinning a side exploded elongated crops (a 122:1 rule line became
27
+ ~32k image tokens and wedged the engine).
28
+ """
29
+ from PIL import Image as PILImage
30
+
31
+ cfg = cfg or CropConfig()
32
+ width, height = image.size
33
+ pixels = width * height
34
+ if pixels <= 0:
35
+ return image
36
+
37
+ if pixels < cfg.min_px:
38
+ scale = math.sqrt(cfg.min_px / pixels)
39
+ elif pixels > cfg.max_px:
40
+ scale = math.sqrt(cfg.max_px / pixels)
41
+ else:
42
+ return image
43
+
44
+ return image.resize(
45
+ (max(1, round(width * scale)), max(1, round(height * scale))), PILImage.LANCZOS
46
+ )
47
+
48
+
49
+ def crop_for(block: Block, page: Image, cfg: CropConfig | None = None) -> Image | None:
50
+ """The image for one block, or None if it should not be transcribed."""
51
+ if block.type in DROP_TYPES or not is_transcribed(block.label):
52
+ return None
53
+
54
+ cfg = cfg or CropConfig()
55
+ width, height = page.size
56
+ x0, y0, x1, y1 = (round(v) for v in block.bbox_xyxy)
57
+ if cfg.pad_px: # recover glyph edges a tight box clips
58
+ x0, y0 = x0 - cfg.pad_px, y0 - cfg.pad_px
59
+ x1, y1 = x1 + cfg.pad_px, y1 + cfg.pad_px
60
+ x0, y0 = max(0, x0), max(0, y0)
61
+ x1, y1 = min(width, x1), min(height, y1)
62
+
63
+ if x1 <= x0 or y1 <= y0: # rounding can collapse a thin rule to zero width
64
+ return None
65
+ return page.crop((x0, y0, x1, y1)).convert("RGB")
idp_layout.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/layout.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Layout backends: page image -> cleaned, reading-ordered blocks.
6
+
7
+ The two stages hand off a plain JSON layout, so stage 2 does not care where the layout came
8
+ from. :class:`LayoutBackend` makes that an interface rather than a claim.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
15
+
16
+ from idp_blocks import clamp_to_page, clean_layout
17
+ from idp_types import Block, DedupConfig, LayoutConfig, PageResult
18
+ from idp_contract import map_label
19
+
20
+ if TYPE_CHECKING: # pragma: no cover
21
+ from PIL.Image import Image
22
+
23
+
24
+ @runtime_checkable
25
+ class LayoutBackend(Protocol):
26
+ """``detect`` must return blocks already cleaned and densely ordered: ``order`` a gap-free
27
+ 0-based rank. Stage 2 matches transcriptions back by ``order``, so gaps mis-assign text."""
28
+
29
+ def detect(self, image: Image) -> list[Block]: ...
30
+
31
+ def close(self) -> None: ...
32
+
33
+
34
+ def _densify(blocks: list[Block]) -> list[Block]:
35
+ """Sort by the detector's reading order and renumber to a gap-free 0-based rank."""
36
+ ordered = sorted(blocks, key=lambda b: b.order)
37
+ for rank, block in enumerate(ordered):
38
+ block.order = rank
39
+ return ordered
40
+
41
+
42
+ class IndicDocLayoutBackend:
43
+ """Our finetuned PP-DocLayoutV3 with an integrated reading-order head. Torch only --
44
+ constructing this does not load vLLM, which is what lets stage 1 run alone."""
45
+
46
+ def __init__(
47
+ self,
48
+ ckpt: str | None = None,
49
+ config: LayoutConfig | None = None,
50
+ dedup: DedupConfig | None = None,
51
+ ) -> None:
52
+ from idp_model_infer import get_model
53
+
54
+ self.config = config or LayoutConfig()
55
+ self.dedup = dedup or DedupConfig()
56
+ if ckpt is None:
57
+ raise ValueError(
58
+ "no layout weights given -- "
59
+ "IndicDocParser.from_pretrained(snapshot_download(REPO))"
60
+ )
61
+ self.ckpt = ckpt
62
+ self.model = get_model(self.ckpt, device=self.config.device)
63
+
64
+ def detect(self, image: Image) -> list[Block]:
65
+ from idp_model_infer import infer
66
+
67
+ width, height = image.size
68
+ detections = infer(
69
+ self.model,
70
+ image,
71
+ conf=self.config.conf,
72
+ img_size=self.config.img_size,
73
+ device=self.config.device,
74
+ )
75
+
76
+ # The model emits [y0, x0, y1, x1] normalised to 0-1000; the pipeline works in pixel
77
+ # [x0, y0, x1, y1]. Axis swap and rescale happen here, once.
78
+ blocks = []
79
+ for det in detections:
80
+ y0, x0, y1, x1 = det["bbox"]
81
+ bbox = [x0 / 1000 * width, y0 / 1000 * height, x1 / 1000 * width, y1 / 1000 * height]
82
+ label = str(det["label"])
83
+ blocks.append(
84
+ Block(
85
+ order=det["reading_order"],
86
+ label=label,
87
+ type=map_label(label),
88
+ bbox_xyxy=[round(v, 1) for v in clamp_to_page(bbox, width, height)],
89
+ conf=round(float(det.get("score", 1.0)), 3),
90
+ )
91
+ )
92
+
93
+ return _densify(clean_layout(blocks, self.dedup))
94
+
95
+ def close(self) -> None:
96
+ self.model = None
97
+
98
+
99
+ class JsonLayoutBackend:
100
+ """Replay a layout produced elsewhere -- by stage 1, by hand, or by another detector.
101
+
102
+ Assumed already clean, so no cleanup runs; blocks are only renumbered, which makes a
103
+ hand-edited file usable without fixing ranks. Needs no torch.
104
+ """
105
+
106
+ def __init__(self, layout: str | dict | PageResult, *, strict: bool = True) -> None:
107
+ if isinstance(layout, PageResult):
108
+ self.page = layout
109
+ else:
110
+ if isinstance(layout, str):
111
+ with open(layout, encoding="utf-8") as fh:
112
+ layout = json.load(fh)
113
+ # Validates by default: this is the door a foreign layout comes through, and an
114
+ # unrecognised label would otherwise become Text without a word. `strict=False`
115
+ # replays a file written before validation existed.
116
+ self.page = PageResult.from_record(layout, strict=strict)
117
+
118
+ def detect(self, image: Image) -> list[Block]:
119
+ """Copies, so renumbering cannot write back into the stored layout. ``image`` is
120
+ accepted for interface parity and not read."""
121
+ return _densify([b.copy() for b in self.page.blocks])
122
+
123
+ def close(self) -> None:
124
+ return None
idp_model_infer.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/layout/infer.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Single-image inference for IndicDocLayout -> boxes, labels and reading order.
6
+
7
+ Emits ``[{bbox: [y0, x0, y1, x1] normalised to 0-1000, label, reading_order, score}]``. That is
8
+ the layout viewer's schema, kept verbatim so predictions render with the same box and
9
+ reading-order visualisation as the dataset viewer; ``engine.layout`` converts it to pixel
10
+ ``[x0, y0, x1, y1]``.
11
+
12
+ The decode mirrors the training-time evaluation exactly: sigmoid-max detections, and reading
13
+ order from a voting sort over the pairwise ``order_logits`` restricted to the kept boxes.
14
+ """
15
+
16
+ import numpy as np
17
+ import torch
18
+
19
+ from idp_model_labels import ID2LABEL
20
+ from idp_model_order_loss import decode_order, pairwise_scores
21
+
22
+ _CACHE = {}
23
+
24
+
25
+ def cxcywh_to_xyxy(b):
26
+ """Centre-form boxes -> corner form, preserving the input tensor's dtype and device."""
27
+ c = b.clone()
28
+ c[..., 0], c[..., 1] = b[..., 0] - b[..., 2] / 2, b[..., 1] - b[..., 3] / 2
29
+ c[..., 2], c[..., 3] = b[..., 0] + b[..., 2] / 2, b[..., 1] + b[..., 3] / 2
30
+ return c
31
+
32
+
33
+ def get_model(ckpt, device="cuda"):
34
+ """Load (and cache) an IndicDocLayout checkpoint.
35
+
36
+ The checkpoint is saved as ``PPDocLayoutV3Trainable`` -- our subclass -- so that class has to
37
+ be importable here even though nothing is trained at inference time.
38
+ """
39
+ key = (ckpt, device)
40
+ if key not in _CACHE:
41
+ from idp_model_ppdoc import PPDocLayoutV3Trainable
42
+
43
+ _CACHE[key] = PPDocLayoutV3Trainable.from_pretrained(ckpt).to(device).eval()
44
+ return _CACHE[key]
45
+
46
+
47
+ @torch.no_grad()
48
+ def infer(model, pil_img, conf=0.5, img_size=1024, device="cuda"):
49
+ """Detect blocks on one page image."""
50
+ im = pil_img.convert("RGB").resize((img_size, img_size))
51
+ # np.array (not asarray): a PIL buffer is read-only, and torch warns on every page about
52
+ # wrapping a non-writable array.
53
+ x = torch.from_numpy(np.array(im)).permute(2, 0, 1).float().div(255.0) # [3,S,S] in [0,1]
54
+ out = model(pixel_values=x[None].to(device))
55
+ scores, labels = out.logits.sigmoid().max(-1) # [1,N]
56
+ boxes = cxcywh_to_xyxy(out.pred_boxes)[0] # [N,4] normalised x0,y0,x1,y1
57
+
58
+ if getattr(out, "order_logits", None) is not None:
59
+ order_scores = out.order_logits[0]
60
+ else:
61
+ nq = model.config.num_queries
62
+ order_scores = pairwise_scores(out.last_hidden_state[:, -nq:], model.ro_q, model.ro_k)[0]
63
+
64
+ keep = (scores[0] > conf).nonzero().squeeze(-1)
65
+ if keep.numel() == 0:
66
+ return []
67
+ kept_boxes, kept_scores, kept_labels = boxes[keep], scores[0][keep], labels[0][keep]
68
+
69
+ # Reading order is decoded over the KEPT sub-block only: ranking against suppressed queries
70
+ # would leave gaps in the sequence.
71
+ sub = order_scores[keep][:, keep].cpu().float()
72
+ sequence = decode_order(sub).tolist() # positions, first -> last
73
+ rank = [0] * len(sequence)
74
+ for r, position in enumerate(sequence):
75
+ rank[position] = r + 1
76
+
77
+ content = []
78
+ for j in range(keep.numel()):
79
+ x0, y0, x1, y1 = kept_boxes[j].tolist()
80
+ content.append(
81
+ {
82
+ "bbox": [ # viewer schema: [y0, x0, y1, x1], 0-1000
83
+ round(y0 * 1000, 1),
84
+ round(x0 * 1000, 1),
85
+ round(y1 * 1000, 1),
86
+ round(x1 * 1000, 1),
87
+ ],
88
+ "label": ID2LABEL[int(kept_labels[j])],
89
+ "reading_order": rank[j],
90
+ "score": round(float(kept_scores[j]), 3),
91
+ }
92
+ )
93
+ return content
idp_model_labels.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/layout/labels.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """IndicDocLayout's 37 education-domain layout classes = SUPERSET of the printed + handwritten
6
+ taxonomies.
7
+ Order is fixed; do not reorder and only APPEND (ids are baked into checkpoints). ids 0-24 are the
8
+ original 25; 25-26 are the printed-only additions (Chapter-title, Chapter-end-section); 27-36 are
9
+ the indicdlp-v2 magazine/newspaper additions. Class weights are sqrt-inverse block frequency
10
+ (capped [0.5, 5]); optional, used only if enabled.
11
+
12
+ These are the raw *labels*. The map onto the coarse pipeline *types* that select prompts lives in
13
+ ``ocr.templates.contract`` -- see its module docstring for why the two vocabularies differ.
14
+ """
15
+
16
+ CLASSES = [
17
+ "Question",
18
+ "Paragraph",
19
+ "Answer",
20
+ "List",
21
+ "Title",
22
+ "Section-title",
23
+ "Equation",
24
+ "Table",
25
+ "Diagram",
26
+ "Image",
27
+ "MCQ",
28
+ "Infobox",
29
+ "Sub-section-title",
30
+ "Expression",
31
+ "Image-caption",
32
+ "Placeholder-text",
33
+ "Chart",
34
+ "Solved-example",
35
+ "Footnote",
36
+ "Table-caption",
37
+ "Sub-sub-section-title",
38
+ "Footer",
39
+ "Header",
40
+ "Code",
41
+ "Page-number",
42
+ "Chapter-title",
43
+ "Chapter-end-section", # 25-26: printed-only additions (superset)
44
+ # 27-36: indicdlp-v2 additions (magazine/newspaper furniture kept as their own classes;
45
+ # only populated by the indicdlp-printed source -> no cross-dataset partial-annotation conflict).
46
+ "Folio",
47
+ "Reference",
48
+ "Table-of-contents",
49
+ "Index",
50
+ "Advertisement",
51
+ "Author",
52
+ "Dateline",
53
+ "Contact-info",
54
+ "Website-link",
55
+ "Flag",
56
+ ]
57
+
58
+ LABEL2ID = {c: i for i, c in enumerate(CLASSES)}
59
+ ID2LABEL = {i: c for i, c in enumerate(CLASSES)}
60
+ NUM_CLASSES = len(CLASSES)
61
+
62
+ # sqrt-inverse-frequency weights (measured on 155,922 blocks); rare classes upweighted.
63
+ CLASS_WEIGHTS = {
64
+ "Question": 0.50,
65
+ "Paragraph": 0.50,
66
+ "Equation": 0.50,
67
+ "Answer": 0.50,
68
+ "List": 0.50,
69
+ "Section-title": 0.50,
70
+ "Title": 0.50,
71
+ "Image": 0.50,
72
+ "Table": 0.52,
73
+ "Diagram": 0.53,
74
+ "MCQ": 0.74,
75
+ "Expression": 1.00,
76
+ "Image-caption": 1.11,
77
+ "Infobox": 1.43,
78
+ "Solved-example": 1.43,
79
+ "Placeholder-text": 1.48,
80
+ "Sub-section-title": 1.64,
81
+ "Chart": 2.11,
82
+ "Footnote": 4.63,
83
+ "Table-caption": 5.0,
84
+ "Sub-sub-section-title": 5.0,
85
+ "Footer": 5.0,
86
+ "Header": 5.0,
87
+ "Code": 5.0,
88
+ "Page-number": 5.0,
89
+ "Chapter-title": 0.74,
90
+ "Chapter-end-section": 4.5, # by frequency in label_frequency.csv
91
+ }
92
+
93
+ # --- Shared label parser (used by BOTH the training blob cache AND the val/test disk path,
94
+ # so train/val/test are always consistent). Content blocks + metadata Header/Footer region
95
+ # boxes (header reads first, footer last; placeholder stubs dropped; hw dedup vs content). ---
96
+ MIN_HF_AREA = 1e-4 # drop placeholder header/footer stubs (e.g. bbox [0,0,5,10])
97
+
98
+
99
+ def _to_cxcywh(bb):
100
+ """[y0,x0,y1,x1] in 0-1000 -> clamped [cx,cy,w,h] normalized, or None if degenerate."""
101
+ try:
102
+ y0, x0, y1, x1 = [float(c) / 1000 for c in bb]
103
+ except (TypeError, ValueError):
104
+ return None # malformed bbox (e.g. nested list) -> skip this box
105
+ if x1 - x0 <= 1e-3 or y1 - y0 <= 1e-3:
106
+ return None
107
+ cx, cy, w, h = (x0 + x1) / 2, (y0 + y1) / 2, x1 - x0, y1 - y0
108
+ cx, cy = min(max(cx, 1e-4), 1 - 1e-4), min(max(cy, 1e-4), 1 - 1e-4)
109
+ w, h = min(w, 2 * min(cx, 1 - cx)), min(h, 2 * min(cy, 1 - cy))
110
+ return [cx, cy, w, h]
111
+
112
+
113
+ def _iou(a, b):
114
+ iw = max(0, min(a[0] + a[2] / 2, b[0] + b[2] / 2) - max(a[0] - a[2] / 2, b[0] - b[2] / 2))
115
+ ih = max(0, min(a[1] + a[3] / 2, b[1] + b[3] / 2) - max(a[1] - a[3] / 2, b[1] - b[3] / 2))
116
+ inter = iw * ih
117
+ ua = a[2] * a[3] + b[2] * b[3] - inter
118
+ return inter / ua if ua > 0 else 0.0
119
+
120
+
121
+ def labels_from_doc(d, drop_hf_strips=False):
122
+ """Parse a page JSON dict -> (boxes[cxcywh], cls[ids], order). THE canonical parser.
123
+ drop_hf_strips: skip metadata Header/Footer boxes that are full-width page-edge STRIPS (the
124
+ hw-vs/hw-dps placeholders like [0,0,100,1000]) — they're not real text regions and teach the
125
+ model to hallucinate top/bottom bars. Real (non-strip) header/footer regions are kept."""
126
+ cls, boxes, order = [], [], []
127
+ for b in d.get("content", []):
128
+ bb, lab = b.get("bbox"), b.get("label")
129
+ if not (isinstance(bb, list) and len(bb) == 4) or lab not in LABEL2ID:
130
+ continue
131
+ box = _to_cxcywh(bb)
132
+ if box is None:
133
+ continue
134
+ cls.append(LABEL2ID[lab])
135
+ boxes.append(box)
136
+ order.append(b.get("reading_order", len(order) + 1))
137
+ meta = d.get("metadata", {}) or {}
138
+ ords = order or [0]
139
+ for field, lab, ro in (
140
+ ("header", "Header", min(ords) - 1),
141
+ ("footer", "Footer", max(ords) + 1),
142
+ ):
143
+ bb = (meta.get(field) or {}).get("bbox")
144
+ if not (isinstance(bb, list) and len(bb) == 4):
145
+ continue
146
+ box = _to_cxcywh(bb)
147
+ if box is None or box[2] * box[3] < MIN_HF_AREA:
148
+ continue
149
+ if drop_hf_strips and box[2] > 0.9: # full-width strip flush to a page edge = placeholder
150
+ y0, y1 = box[1] - box[3] / 2, box[1] + box[3] / 2
151
+ if (field == "header" and y0 < 0.02) or (field == "footer" and y1 > 0.98):
152
+ continue
153
+ lid = LABEL2ID[lab]
154
+ if any(cls[i] == lid and _iou(boxes[i], box) > 0.5 for i in range(len(boxes))):
155
+ continue
156
+ cls.append(lid)
157
+ boxes.append(box)
158
+ order.append(ro)
159
+ return boxes, cls, order
idp_model_order_loss.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/layout/order_loss.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Locality-weighted Generalized Cross-Entropy on the antisymmetric pairwise
6
+ precedence scores. Validated in learnings (recovers arbitrary order, tau=1.0).
7
+ """
8
+
9
+ import math
10
+
11
+ import torch
12
+
13
+
14
+ def decode_order(S):
15
+ """Reading order from a pairwise score matrix, matching PP-DocLayoutV3's _get_order_seqs.
16
+ P(i before j) = sigmoid(S[i,j]) if i<j else 1-sigmoid(S[j,i]); votes[j]=#elements before j;
17
+ argsort ascending -> reading sequence (first..last). Correct for BOTH the triangular ppdoc
18
+ GlobalPointer AND an antisymmetric head (they coincide under this formula)."""
19
+ sc = torch.sigmoid(S)
20
+ votes = sc.triu(1).sum(0) + (1.0 - sc.t()).tril(-1).sum(0)
21
+ return torch.argsort(votes)
22
+
23
+
24
+ def pairwise_scores(Q, ro_q, ro_k):
25
+ """Q:[B,N,d] -> antisymmetric S:[B,N,N], S_ij>0 => query i precedes j."""
26
+ A, K = ro_q(Q), ro_k(Q) # [B,N,r]
27
+ M = A @ K.transpose(-1, -2) # M_ij = q_i^T Wq^T Wk q_j
28
+ return (M - M.transpose(-1, -2)) / math.sqrt(A.shape[-1])
29
+
30
+
31
+ def locality_gce(S, order, q=0.7, tau=3.0, eps=1e-6):
32
+ """S:[m,m] scores over matched queries, order:[m] their GT reading_order values."""
33
+ m = order.numel()
34
+ if m < 2:
35
+ return S.sum() * 0.0 # nothing to order; keep graph
36
+ P = (order.unsqueeze(1) < order.unsqueeze(0)).float() # P_ab=1 if a precedes b
37
+ p = torch.sigmoid(S)
38
+ pc = torch.where(P > 0.5, p, 1 - p).clamp(eps, 1)
39
+ gce = (1 - pc.pow(q)) / q
40
+ pos = order.argsort().argsort().float() # dense rank positions
41
+ dist = (pos.unsqueeze(1) - pos.unsqueeze(0)).abs()
42
+ W = torch.exp(-dist / tau)
43
+ # only the strict UPPER triangle carries real scores (ppdoc GlobalPointer masks a>=b to -1e4);
44
+ # training the masked lower triangle penalizes unfixable entries. Upper-tri also suffices for
45
+ # an antisymmetric head. P[i,j] for i<j = (order_i < order_j).
46
+ mask = torch.triu(torch.ones(m, m, dtype=torch.bool, device=S.device), diagonal=1)
47
+ return (gce * W)[mask].sum() / W[mask].sum().clamp_min(eps)
idp_model_ppdoc.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/layout/modeling_ppdoc.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """IndicDocLayout: trainable PP-DocLayoutV3 (document-pretrained strong init).
6
+
7
+ HF ships PPDocLayoutV3ForObjectDetection inference-only (forward raises on labels).
8
+ This subclass unblocks training: it calls the inner model with labels (which builds the
9
+ contrastive-denoising groups), reuses the base RT-DETR detection loss on its outputs, and
10
+ adds our locality-weighted GCE order loss on its (pretrained) order_logits.
11
+ Backbone + decoder + order/mask heads start from the document-pretrained checkpoint;
12
+ only the class heads are re-init'd for our 37 education classes.
13
+ """
14
+
15
+ from dataclasses import dataclass
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+ from transformers import PPDocLayoutV3Config, PPDocLayoutV3ForObjectDetection
20
+ from transformers.loss.loss_rt_detr import RTDetrHungarianMatcher
21
+ from transformers.utils import ModelOutput
22
+
23
+ from idp_model_order_loss import locality_gce
24
+
25
+
26
+ @dataclass
27
+ class PPDocOutput(ModelOutput):
28
+ loss: torch.FloatTensor | None = None
29
+ logits: torch.FloatTensor | None = None
30
+ pred_boxes: torch.FloatTensor | None = None
31
+ order_logits: torch.FloatTensor | None = None
32
+ last_hidden_state: torch.FloatTensor | None = None
33
+
34
+
35
+ class PPDocLayoutV3Trainable(PPDocLayoutV3ForObjectDetection):
36
+ def __init__(self, config):
37
+ super().__init__(config)
38
+ self.lambda_order = getattr(config, "lambda_order", 5.0)
39
+ # Built on first use, not here: the matcher is training-only, and constructing it calls
40
+ # requires_backends(["scipy"]). Eagerly, that makes scipy a hard dependency of merely
41
+ # LOADING the detector -- so inference-only installs fail on import with a library they
42
+ # will never call. It holds no parameters, so this does not change the state dict.
43
+ self._matcher = None
44
+ self.loss_type = "RTDetrForObjectDetection" # base RT-DETR loss over its outputs
45
+
46
+ @classmethod
47
+ def build(cls, ckpt, num_labels, id2label, label2id, lambda_order=5.0):
48
+ config = PPDocLayoutV3Config.from_pretrained(
49
+ ckpt, num_labels=num_labels, id2label=id2label, label2id=label2id
50
+ )
51
+ config.lambda_order = lambda_order
52
+ config.loss_type = "RTDetrForObjectDetection"
53
+ # PP-DocLayoutV3's denoising path is buggy (embed size num_labels but pads with
54
+ # num_labels -> index error); it was never run since HF blocks training. Disable it
55
+ # (optional convergence aid). Re-enable later by resizing denoising_class_embed to +1.
56
+ config.num_denoising = 0
57
+ # RT-DETR loss/matcher fields the base config lacks
58
+ defaults = {
59
+ "use_focal_loss": True,
60
+ "auxiliary_loss": True,
61
+ "weight_loss_vfl": 1.0,
62
+ "weight_loss_bbox": 5.0,
63
+ "weight_loss_giou": 2.0,
64
+ "matcher_class_cost": 2.0,
65
+ "matcher_bbox_cost": 5.0,
66
+ "matcher_giou_cost": 2.0,
67
+ "matcher_alpha": 0.25,
68
+ "matcher_gamma": 2.0,
69
+ "focal_loss_alpha": 0.25,
70
+ "focal_loss_gamma": 2.0,
71
+ "eos_coefficient": 1e-4,
72
+ }
73
+ for k, v in defaults.items():
74
+ if not hasattr(config, k):
75
+ setattr(config, k, v)
76
+ model = cls.from_pretrained(ckpt, config=config, ignore_mismatched_sizes=True)
77
+ # re-init class heads (paddle doc classes -> our education classes); keep everything else
78
+ for m in model.modules():
79
+ if (isinstance(m, nn.Linear) and m.out_features == num_labels) or (
80
+ isinstance(m, nn.Embedding) and m.num_embeddings == num_labels + 1
81
+ ):
82
+ m.reset_parameters()
83
+ return model
84
+
85
+ def _order_loss(self, order_logits, logits, pred_boxes, labels):
86
+ if self._matcher is None:
87
+ self._matcher = RTDetrHungarianMatcher(self.config) # needs scipy; training only
88
+ idx = self._matcher({"logits": logits, "pred_boxes": pred_boxes}, labels)
89
+ tot, n = 0.0, 0
90
+ for b, (src, tgt) in enumerate(idx):
91
+ if src.numel() < 2:
92
+ continue
93
+ order = labels[b]["reading_order"][tgt]
94
+ S = order_logits[b][src][:, src]
95
+ tot = tot + locality_gce(S, order)
96
+ n += 1
97
+ return tot / max(n, 1) if n else order_logits.sum() * 0.0
98
+
99
+ def forward(self, pixel_values, pixel_mask=None, labels=None, **kwargs):
100
+ outputs = self.model(pixel_values=pixel_values, pixel_mask=pixel_mask, labels=labels)
101
+ dn = outputs.denoising_meta_values if self.training else None
102
+ outputs_class = outputs.intermediate_logits
103
+ outputs_coord = outputs.intermediate_reference_points
104
+ logits, pred_boxes = outputs_class[:, -1], outputs_coord[:, -1]
105
+ order_logits = outputs.out_order_logits[:, -1] # [B, num_queries, num_queries]
106
+ loss = None
107
+ if labels is not None:
108
+ loss, _, _ = self.loss_function(
109
+ logits,
110
+ labels,
111
+ self.device,
112
+ pred_boxes,
113
+ self.config,
114
+ outputs_class,
115
+ outputs_coord,
116
+ enc_topk_logits=outputs.enc_topk_logits,
117
+ enc_topk_bboxes=outputs.enc_topk_bboxes,
118
+ denoising_meta_values=dn,
119
+ )
120
+ loss = loss + self.lambda_order * self._order_loss(
121
+ order_logits, logits, pred_boxes, labels
122
+ )
123
+ return PPDocOutput(
124
+ loss=loss,
125
+ logits=logits,
126
+ pred_boxes=pred_boxes,
127
+ order_logits=order_logits,
128
+ last_hidden_state=outputs.last_hidden_state,
129
+ )
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_doc_parser.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
+ IndicDocParser 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 IndicDocParser:
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) -> IndicDocParser:
174
+ return self
175
+
176
+ def __exit__(self, *exc_info) -> None:
177
+ self.close()
idp_recognizer.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/recognizer.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """IndicBlockOCR: crops in, transcriptions out.
6
+
7
+ Heavy imports live inside methods, so importing this module stays free -- asserted by
8
+ tests/ocr/test_ocr_lazy_import.py.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
14
+
15
+ from idp_types import CropConfig, RecognizerConfig
16
+
17
+ if TYPE_CHECKING: # pragma: no cover
18
+ from PIL.Image import Image
19
+
20
+
21
+ class CropRequest(NamedTuple):
22
+ image: Image
23
+ prompt: str
24
+
25
+
26
+ @runtime_checkable
27
+ class RecognizerBackend(Protocol):
28
+ """``transcribe`` returns one string per request, in the same order."""
29
+
30
+ def transcribe(self, requests: list[CropRequest]) -> list[str]: ...
31
+
32
+ def close(self) -> None: ...
33
+
34
+
35
+ def build_requests(blocks, page, crop_cfg: CropConfig, table_format) -> tuple[list, list]:
36
+ """Crop each block and pair it with its prompt.
37
+
38
+ Returns ``(requests, orders)`` -- the reading-order rank of each request, so transcriptions
39
+ can be matched back. Blocks that yield no crop are simply absent from both.
40
+ """
41
+ from idp_crops import area_clamp, crop_for
42
+ from idp_contract import prompt_for
43
+
44
+ requests, orders = [], []
45
+ for block in blocks:
46
+ crop = crop_for(block, page, crop_cfg)
47
+ if crop is None:
48
+ continue
49
+ requests.append(
50
+ CropRequest(area_clamp(crop, crop_cfg), prompt_for(block.type, table_format))
51
+ )
52
+ orders.append(block.order)
53
+ return requests, orders
54
+
55
+
56
+ class HfRecognizer:
57
+ """Reference recognizer on plain ``transformers`` -- no vLLM.
58
+
59
+ Exists so IndicDocParser can run anywhere ``transformers`` runs, including straight from the
60
+ Hub with ``trust_remote_code=True``. It is the *quickstart* path, not the working one:
61
+ without continuous batching it is orders of magnitude slower per block than
62
+ :class:`VllmRecognizer`, so use it to try a page, not to parse a corpus.
63
+
64
+ Output also diverges slightly from the vLLM path. Both decode greedily, but different kernels
65
+ give different logits, and a near-tie flips the argmax -- so do not expect byte-identical
66
+ transcriptions between the two backends.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ ckpt: str | None = None,
72
+ config: RecognizerConfig | None = None,
73
+ device: str = "auto",
74
+ attn_implementation: str = "sdpa",
75
+ batch_size: int = 8,
76
+ ) -> None:
77
+ import torch
78
+ from transformers import AutoModelForImageTextToText, AutoProcessor
79
+
80
+
81
+ self._torch = torch
82
+ self.config = config or RecognizerConfig()
83
+ # RecognizerConfig.batch_size sizes a vLLM chunk (~2048). Generating that many at once
84
+ # here would simply OOM; HF batches are bounded by memory, not by scheduler behaviour.
85
+ self.batch_size = batch_size
86
+ if ckpt is None:
87
+ raise ValueError(
88
+ "no recognizer weights given -- "
89
+ "IndicDocParser.from_pretrained(snapshot_download(REPO))"
90
+ )
91
+ self.ckpt = ckpt
92
+
93
+ self.processor = AutoProcessor.from_pretrained(self.ckpt)
94
+ tokenizer = self.processor.tokenizer
95
+ # Left padding so every sequence in a batch ends flush against the generation boundary.
96
+ tokenizer.padding_side = "left"
97
+
98
+ self.model = AutoModelForImageTextToText.from_pretrained(
99
+ self.ckpt,
100
+ dtype=getattr(torch, self.config.dtype),
101
+ device_map=device,
102
+ attn_implementation=attn_implementation,
103
+ )
104
+ self.model.eval()
105
+
106
+ # The checkpoint's generation_config carries eos_token_id 248044, which is an ordinary
107
+ # word piece, not a turn terminator. Left alone, generate() never stops and every block
108
+ # runs to max_new_tokens, repeating itself. vLLM does not hit this because it takes the
109
+ # tokenizer's EOS. Trust the tokenizer here too.
110
+ self.eos_token_id = tokenizer.eos_token_id
111
+ self.pad_token_id = tokenizer.pad_token_id or tokenizer.eos_token_id
112
+
113
+ def _prompt(self, text: str) -> str:
114
+ return self.processor.apply_chat_template(
115
+ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text}]}],
116
+ add_generation_prompt=True,
117
+ tokenize=False,
118
+ )
119
+
120
+ def transcribe(self, requests: list[CropRequest]) -> list[str]:
121
+ texts: list[str] = []
122
+ for i in range(0, len(requests), self.batch_size):
123
+ chunk = requests[i : i + self.batch_size]
124
+ inputs = self.processor(
125
+ text=[self._prompt(r.prompt) for r in chunk],
126
+ images=[r.image for r in chunk],
127
+ padding=True,
128
+ return_tensors="pt",
129
+ ).to(self.model.device)
130
+ prompt_len = inputs["input_ids"].shape[-1]
131
+
132
+ with self._torch.inference_mode():
133
+ out = self.model.generate(
134
+ **inputs,
135
+ max_new_tokens=self.config.max_tokens,
136
+ do_sample=False,
137
+ use_cache=True,
138
+ eos_token_id=self.eos_token_id,
139
+ pad_token_id=self.pad_token_id,
140
+ )
141
+ texts.extend(self.processor.batch_decode(out[:, prompt_len:], skip_special_tokens=True))
142
+ return [t.strip() for t in texts]
143
+
144
+ def close(self) -> None:
145
+ self.model = None
146
+ self.processor = None
idp_reconstruct.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/reconstruct.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Assemble transcribed blocks into the page's markdown.
6
+
7
+ stdlib-only -- importable with no GPU stack and no PIL.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from idp_types import Block
15
+ from idp_contract import DROP_TYPES
16
+
17
+ # Dashes as codepoints: several of the seven are indistinguishable in a source file.
18
+ # 002D HYPHEN-MINUS | 2010-2014 HYPHEN..EM DASH | 2212 MINUS SIGN
19
+ _HYPHEN_BREAK = re.compile("(\\w)[-\\u2010-\\u2014\\u2212]\\n[ \\t]*(\\w)")
20
+ _MATH_DELIMITERS = ("$", "\\[", "\\(")
21
+
22
+ # A math span: $$...$$ (display) or $...$ (inline).
23
+ _MATH_SPAN = re.compile(r"\$\$(.+?)\$\$|(?<!\$)\$(?!\$)([^$\n]+?)\$(?!\$)", re.S)
24
+
25
+ # Runs of the scripts IndicBlockOCR transcribes: Arabic (Urdu, Kashmiri, Sindhi),
26
+ # Devanagari..Malayalam, and Ol Chiki (Santali). Intervening spaces and the ZW(N)J joiners that
27
+ # Indic shaping relies on are kept inside the run so one run does not fragment into many.
28
+ # The run must both start and end on a script character, so a trailing space stays outside the
29
+ # \text{} and keeps separating it from what follows.
30
+ _NON_LATIN_RUN = re.compile(
31
+ "[\\u0600-\\u06ff\\u0900-\\u0d7f\\u1c50-\\u1c7f]"
32
+ "(?:[\\u0600-\\u06ff\\u0900-\\u0d7f\\u1c50-\\u1c7f \\u200c\\u200d]*"
33
+ "[\\u0600-\\u06ff\\u0900-\\u0d7f\\u1c50-\\u1c7f])?"
34
+ )
35
+ _TEXT_CMD = re.compile(r"\\text\{[^{}]*\}")
36
+
37
+
38
+ def dehyphenate(text: str) -> str:
39
+ """Rejoin words split by a hyphen at a line break.
40
+
41
+ Iterates to a fixpoint: re.sub matches non-overlappingly, so in "a-\\nb-\\nc" the first pass
42
+ consumes the "b" the second break needs.
43
+ """
44
+ previous = None
45
+ while previous != text:
46
+ previous = text
47
+ text = _HYPHEN_BREAK.sub(r"\1\2", text)
48
+ return text
49
+
50
+
51
+ def _repair_expression(tex: str, display: bool) -> str:
52
+ """Make one transcribed expression valid LaTeX.
53
+
54
+ Two repairs, both for things the recognizer emits that no LaTeX engine accepts:
55
+
56
+ * **Indic script in math mode.** ``$$প্রোটন = 9$$`` is invalid -- math mode has no glyphs for
57
+ those codepoints, so KaTeX, MathJax and a real TeX run all fail on it. Each run is wrapped
58
+ in ``\\text{}``, which is what the recognizer itself does when it gets it right. Runs
59
+ already inside ``\\text{}`` are left alone.
60
+ * **Bare newlines in display math**, which are a syntax error; ``\\\\`` is the row separator.
61
+ """
62
+ protected: list[str] = []
63
+
64
+ def stash(match: re.Match) -> str:
65
+ protected.append(match.group(0))
66
+ return f"\x00{len(protected) - 1}\x00"
67
+
68
+ tex = _TEXT_CMD.sub(stash, tex)
69
+ tex = _NON_LATIN_RUN.sub(lambda m: f"\\text{{{m.group(0)}}}", tex)
70
+ tex = re.sub(r"\x00(\d+)\x00", lambda m: protected[int(m.group(1))], tex)
71
+
72
+ if display:
73
+ tex = re.sub(r"\s*\n\s*", r" \\\\ ", tex.strip())
74
+ return tex
75
+
76
+
77
+ def repair_math(text: str) -> str:
78
+ """Repair every math span in a markdown string. Prose outside ``$`` is untouched."""
79
+
80
+ def fix(match: re.Match) -> str:
81
+ display = match.group(1) is not None
82
+ inner = _repair_expression(match.group(1) or match.group(2), display)
83
+ return f"$${inner}$$" if display else f"${inner}$"
84
+
85
+ return _MATH_SPAN.sub(fix, text)
86
+
87
+
88
+ def reconstruct(blocks: list[Block], repair: bool = True) -> str:
89
+ """Reading-ordered markdown. Blocks with no text contribute nothing but are not removed --
90
+ they still appear in the JSON with text "" .
91
+
92
+ ``repair=False`` emits the recognizer's math verbatim, including expressions no LaTeX engine
93
+ can render. Only useful for comparing byte-for-byte against output produced before the repair
94
+ existed.
95
+ """
96
+ kept = sorted((b for b in blocks if b.type not in DROP_TYPES), key=lambda b: b.order)
97
+
98
+ parts = []
99
+ for block in kept:
100
+ text = (block.text or "").strip()
101
+ if not text:
102
+ continue
103
+ # Bare LaTeX would render as literal source, so wrap it -- but only when the recognizer
104
+ # supplied no delimiters at all. Checking just the first character is not enough: an
105
+ # Equation block often comes back as a prose prefix followed by already-delimited math
106
+ # ("বা, $\\frac{a}{b}$"), and wrapping that produces "$$...$$$", which nothing can parse.
107
+ if block.type == "Equation" and "$" not in text and not text.startswith(_MATH_DELIMITERS):
108
+ text = f"$${text}$$"
109
+ parts.append(text)
110
+
111
+ markdown = dehyphenate("\n\n".join(parts))
112
+ return repair_math(markdown) if repair else markdown
idp_types.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GENERATED by hub/build_hub_package.py from src/bodhan_genai/ocr/engine/types.py -- do not edit.
2
+ # Vendored so this repo is self-contained: `pip install transformers torch pillow` is the
3
+ # whole install. See indic_doc_parser.py for usage.
4
+
5
+ """Plain-data types and configuration.
6
+
7
+ Every tunable that used to be an environment variable read at import time lives here instead.
8
+ That fixes a real defect: callers corrected the old module-level defaults by setting os.environ
9
+ *before* importing the engine, two shipped callers set different values, and importing it
10
+ directly gave a third pipeline. These defaults ARE the canonical recipe.
11
+
12
+ stdlib-only -- importable with no GPU stack and no PIL.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import dataclasses
18
+ from dataclasses import dataclass, field
19
+
20
+ from idp_contract import TableFormat
21
+
22
+
23
+ class LayoutSchemaError(ValueError):
24
+ """An incoming layout record cannot be interpreted unambiguously.
25
+
26
+ Raised rather than warned because the alternative is silent and wrong. A label the taxonomy
27
+ does not contain falls through to ``Text``, so a table would be sent the prose prompt and
28
+ come back as flattened text with nothing in the output saying so. Our own detector only ever
29
+ emits ``layout.labels.CLASSES``, so an unrecognised label always means either a third-party
30
+ taxonomy -- which should declare ``type`` -- or a typo. Both are worth stopping for.
31
+
32
+ ``map_label`` itself stays total: 19 of the 37 classes are deliberately absent from
33
+ ``LABEL_TO_TYPE`` and correctly mean ``Text``, so leniency belongs there. Validation belongs
34
+ here, at the boundary where foreign JSON enters.
35
+ """
36
+
37
+
38
+ def _unknown(value, candidates, kind: str) -> str:
39
+ """``unknown label 'Tabel'; did you mean 'Table'?`` -- the suggestion is most of the value."""
40
+ import difflib
41
+
42
+ match = difflib.get_close_matches(str(value), list(candidates), n=1, cutoff=0.6)
43
+ hint = f"; did you mean {match[0]!r}?" if match else ""
44
+ return f"unknown {kind} {value!r}{hint}"
45
+
46
+
47
+ @dataclass
48
+ class Block:
49
+ """One detected region. ``text`` is None before OCR, and "" for blocks deliberately not
50
+ transcribed (kept in place rather than deleted)."""
51
+
52
+ order: int
53
+ label: str
54
+ type: str
55
+ bbox_xyxy: list[float]
56
+ conf: float
57
+ text: str | None = None
58
+
59
+ def as_record(self) -> dict:
60
+ # Key order is load-bearing: json.dump writes insertion order and the regression gate
61
+ # compares byte for byte. Do not reorder.
62
+ record: dict = {
63
+ "order": self.order,
64
+ "label": self.label,
65
+ "type": self.type,
66
+ "bbox_xyxy": [round(float(v), 1) for v in self.bbox_xyxy],
67
+ "conf": round(float(self.conf), 3),
68
+ }
69
+ if self.text is not None:
70
+ record["text"] = self.text
71
+ return record
72
+
73
+ @classmethod
74
+ def problems(cls, record: dict) -> list[str]:
75
+ """Everything wrong with one incoming block record, as readable strings.
76
+
77
+ Returns rather than raises so a whole page can be reported at once. Fixing a 60-block
78
+ layout one exception at a time is the difference between one edit and sixty.
79
+ """
80
+ from idp_model_labels import CLASSES
81
+ from idp_contract import DROP_TYPES, KEPT_BLOCK_TYPES
82
+
83
+ found: list[str] = []
84
+ for key in ("order", "bbox_xyxy"):
85
+ if key not in record:
86
+ found.append(f"missing required key {key!r}")
87
+
88
+ bbox = record.get("bbox_xyxy")
89
+ if bbox is not None:
90
+ try:
91
+ if len([float(v) for v in bbox]) != 4:
92
+ found.append(f"bbox_xyxy must be 4 numbers [x0, y0, x1, y1], got {bbox!r}")
93
+ except (TypeError, ValueError):
94
+ found.append(f"bbox_xyxy must be 4 numbers [x0, y0, x1, y1], got {bbox!r}")
95
+
96
+ # An explicit `type` is the documented escape hatch for a foreign taxonomy, so the label
97
+ # is not checked when one is given -- but the type itself is. Left unchecked, `Tabel`
98
+ # is accepted verbatim and never matches `Table`, which is the same silent failure one
99
+ # level up.
100
+ declared = record.get("type")
101
+ if declared:
102
+ valid = tuple(KEPT_BLOCK_TYPES) + tuple(sorted(DROP_TYPES))
103
+ if str(declared) not in valid:
104
+ found.append(_unknown(declared, valid, "type"))
105
+ else:
106
+ label = str(record.get("label", "")).strip()
107
+ if label.lower() not in {c.strip().lower() for c in CLASSES}:
108
+ found.append(
109
+ _unknown(label, CLASSES, "label")
110
+ + ' -- use a spelling from layout.labels.CLASSES, or declare "type"'
111
+ " explicitly if your detector has its own taxonomy"
112
+ )
113
+ return found
114
+
115
+ @classmethod
116
+ def from_record(cls, record: dict, *, strict: bool = True) -> Block:
117
+ # `type` is derived when absent, so a third-party layout carrying only labels works.
118
+ from idp_contract import map_label
119
+
120
+ if strict:
121
+ found = cls.problems(record)
122
+ if found:
123
+ raise LayoutSchemaError("; ".join(found))
124
+
125
+ label = record.get("label", "")
126
+ return cls(
127
+ order=int(record["order"]),
128
+ label=str(label),
129
+ type=str(record.get("type") or map_label(label)),
130
+ bbox_xyxy=[float(v) for v in record["bbox_xyxy"]],
131
+ conf=float(record.get("conf", 1.0)),
132
+ text=record.get("text"),
133
+ )
134
+
135
+ def copy(self) -> Block:
136
+ return dataclasses.replace(self, bbox_xyxy=list(self.bbox_xyxy))
137
+
138
+
139
+ @dataclass
140
+ class PageResult:
141
+ image: str
142
+ width: int
143
+ height: int
144
+ blocks: list[Block] = field(default_factory=list)
145
+ markdown: str | None = None
146
+
147
+ def as_record(self) -> dict:
148
+ return {
149
+ "image": self.image,
150
+ "width": self.width,
151
+ "height": self.height,
152
+ "blocks": [b.as_record() for b in self.blocks],
153
+ }
154
+
155
+ @classmethod
156
+ def from_record(cls, record: dict, *, strict: bool = True) -> PageResult:
157
+ """``strict=False`` replays a layout without validating it -- for reading back files
158
+ written before validation existed, not for new integrations."""
159
+ blocks = list(record.get("blocks", []))
160
+
161
+ if strict:
162
+ errors = [
163
+ f" block[{i}] (order={b.get('order', '?')!r}): {problem}"
164
+ for i, b in enumerate(blocks)
165
+ for problem in Block.problems(b)
166
+ ]
167
+ if errors:
168
+ raise LayoutSchemaError(
169
+ f"{len(errors)} problem(s) in layout for "
170
+ f"{record.get('image', '<unknown>')!r}:\n" + "\n".join(errors)
171
+ )
172
+
173
+ return cls(
174
+ image=str(record["image"]),
175
+ width=int(record["width"]),
176
+ height=int(record["height"]),
177
+ # Already validated above; re-checking each block would repeat the work.
178
+ blocks=[Block.from_record(b, strict=False) for b in blocks],
179
+ )
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class LayoutConfig:
184
+ conf: float = 0.5 # below ~0.4, stains and page borders start scoring as blocks
185
+ img_size: int = 1024
186
+ device: str = "cuda"
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class CropConfig:
191
+ """The clamp is on area, not on a side: pinning a side exploded elongated crops (a 122:1
192
+ rule line became ~32k image tokens and wedged the engine)."""
193
+
194
+ min_px_side: int = 256 # largest single measured win; 0 disables upscaling
195
+ max_px_side: int = 1536 # token ceiling
196
+ pad_px: int = 0
197
+
198
+ @property
199
+ def min_px(self) -> int:
200
+ return self.min_px_side**2
201
+
202
+ @property
203
+ def max_px(self) -> int:
204
+ return self.max_px_side**2
205
+
206
+
207
+ #: both -- text-like OR a larger Equation | text_only -- text-like only | eq_only -- Equation only
208
+ DEDUP_MODES = ("both", "text_only", "eq_only")
209
+
210
+
211
+ @dataclass(frozen=True)
212
+ class DedupConfig:
213
+ """IndicDocLayout over-produces equation boxes nested inside the paragraphs and display
214
+ arrays that already contain them; transcribing both emits the same math twice."""
215
+
216
+ nest: bool = True
217
+ mode: str = "both"
218
+ contain: float = 0.90 # duplicate threshold in clean_layout
219
+ wrap: float = 0.5 # a header counts as occupied at this containment
220
+ nested: float = 0.70 # nested-equation threshold
221
+
222
+ def __post_init__(self) -> None:
223
+ if self.mode not in DEDUP_MODES:
224
+ raise ValueError(f"DedupConfig.mode must be one of {DEDUP_MODES}, got {self.mode!r}")
225
+
226
+
227
+ @dataclass(frozen=True)
228
+ class RecognizerConfig:
229
+ max_model_len: int = 8192
230
+ max_tokens: int = 2048
231
+ temperature: float = 0.0 # greedy: the only setting reproducible run to run
232
+ gpu_memory_utilization: float = 0.80
233
+ dtype: str = "bfloat16"
234
+ # One giant batch over tens of thousands of multi-modal requests wedges the vLLM V1
235
+ # scheduler at 100% util with no progress; ~2k chunks run clean.
236
+ batch_size: int = 2048
237
+ enforce_eager: bool = True # skips ~4 min of torch.compile on a 0.8B model
238
+ table_format: TableFormat = TableFormat.HTML
239
+
240
+ def merged(self, **overrides) -> RecognizerConfig:
241
+ """New config with non-None overrides applied; unknown keys raise."""
242
+ known = {f.name for f in dataclasses.fields(self)}
243
+ unknown = sorted(set(overrides) - known)
244
+ if unknown:
245
+ raise TypeError(f"Unknown RecognizerConfig field(s): {', '.join(unknown)}")
246
+ return dataclasses.replace(self, **{k: v for k, v in overrides.items() if v is not None})
indic_doc_parser.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """IndicDocParser -- page image in, reading-ordered Markdown and per-block JSON out.
2
+
3
+ import sys
4
+ from huggingface_hub import snapshot_download
5
+
6
+ repo = snapshot_download("bodhan-ai/indic-doc-parser")
7
+ sys.path.insert(0, repo) # the code ships in the repo
8
+ from indic_doc_parser import IndicDocParser
9
+
10
+ parser = IndicDocParser.from_pretrained(repo)
11
+ print(parser("page.png")) # markdown
12
+
13
+ ``sys.path.insert`` is needed because ``snapshot_download`` returns a cache directory, which is
14
+ not importable on its own. With the path added, this is ordinary Python -- no
15
+ ``trust_remote_code``, and nothing of ours to install.
16
+
17
+ The two stages are also public, and each runs on its own:
18
+
19
+ IndicDocLayout(f"{repo}/weights/layout").detect("page.png") # blocks and reading order
20
+ IndicBlockOCR(f"{repo}/weights/ocr").run("page.png", layout) # layout may be your own
21
+
22
+ ``layout`` there can be a PageResult, a dict, or the path to a layout JSON file, so the stages
23
+ compose across processes and you can correct a layout before transcribing it.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import importlib
29
+ from pathlib import Path
30
+
31
+
32
+ def _preflight() -> None:
33
+ """One clear error instead of a cascade of ImportErrors from inside transformers.
34
+
35
+ Every failure here is one someone has actually hit: a torch built for a CUDA line the driver
36
+ cannot run (silently CPU), a torchvision that does not match its torch, or a transformers
37
+ older than PPDocLayoutV3.
38
+ """
39
+ fix = "./install.sh (or see the Installation section of the model card)"
40
+ for name, floor in (("torch", (2, 4)), ("transformers", (5, 7))):
41
+ try:
42
+ mod = importlib.import_module(name)
43
+ except ImportError:
44
+ raise ImportError(f"IndicDocParser needs {name}. Run: {fix}") from None
45
+ got = tuple(int(p) for p in mod.__version__.split(".")[:2] if p.isdigit())
46
+ if got < floor:
47
+ raise ImportError(
48
+ f"IndicDocParser needs {name}>={'.'.join(map(str, floor))}, "
49
+ f"found {mod.__version__}. Run: {fix}"
50
+ )
51
+ # torchvision is stage 2 only, so its absence is not fatal here -- the layout stage runs
52
+ # without it. It is checked where the recognizer is built.
53
+
54
+
55
+ _preflight()
56
+
57
+ # ruff: noqa: E402 -- the preflight has to run before transformers is imported, which is the
58
+ # whole point of it; these imports pull transformers in transitively.
59
+ from idp_offline import IndicBlockOCR, IndicDocLayout
60
+ from idp_types import CropConfig, DedupConfig, LayoutConfig, RecognizerConfig, TableFormat
61
+
62
+ #: The two stages are public: either runs on its own, and IndicBlockOCR accepts a layout you
63
+ #: produced or corrected yourself.
64
+ __all__ = ["IndicBlockOCR", "IndicDocLayout", "IndicDocParser"]
65
+
66
+
67
+ class IndicDocParser:
68
+ """Both stages. Construct with :meth:`from_pretrained`, then call it on a page image."""
69
+
70
+ def __init__(
71
+ self,
72
+ path: str | Path,
73
+ device: str = "cuda",
74
+ table_format: str = "html",
75
+ dedup_mode: str = "both",
76
+ contain: float = 0.90,
77
+ min_px_side: int = 256,
78
+ max_new_tokens: int = 2048,
79
+ ) -> None:
80
+ self.path = Path(path)
81
+ self._device = device
82
+ self._layout_cfg = LayoutConfig(device=device)
83
+ self._dedup = DedupConfig(mode=dedup_mode, contain=contain)
84
+ self._crop = CropConfig(min_px_side=min_px_side)
85
+ self._rec_cfg = RecognizerConfig(
86
+ max_tokens=max_new_tokens, table_format=TableFormat(table_format)
87
+ )
88
+ # Both stages are built on first use, so detect() never loads the 1.7 GB recognizer.
89
+ self._layout = None
90
+ self._ocr = None
91
+
92
+ @classmethod
93
+ def from_pretrained(cls, path: str | Path | None = None, **kwargs) -> IndicDocParser:
94
+ """Load from a downloaded snapshot. Defaults to the directory this file lives in, which
95
+ is the snapshot itself -- so ``IndicDocParser.from_pretrained()`` also works."""
96
+ return cls(Path(path) if path else Path(__file__).resolve().parent, **kwargs)
97
+
98
+ # -- stages ------------------------------------------------------------ #
99
+
100
+ @property
101
+ def layout(self):
102
+ if self._layout is None:
103
+ self._layout = IndicDocLayout(
104
+ ckpt=str(self.path / "weights" / "layout"),
105
+ config=self._layout_cfg,
106
+ dedup=self._dedup,
107
+ )
108
+ return self._layout
109
+
110
+ @property
111
+ def recognizer(self):
112
+ if self._ocr is None:
113
+ if importlib.util.find_spec("torchvision") is None:
114
+ raise ImportError(
115
+ "The recognizer needs torchvision (its image processor uses it); the layout "
116
+ "stage does not, so detect() still works. Install it together with torch, "
117
+ "from the same index -- torchvision pins an exact torch version. See "
118
+ "./install.sh"
119
+ )
120
+ from idp_recognizer import HfRecognizer
121
+
122
+ self._ocr = IndicBlockOCR(
123
+ backend=HfRecognizer(
124
+ ckpt=str(self.path / "weights" / "ocr"),
125
+ config=self._rec_cfg,
126
+ device=self._device,
127
+ ),
128
+ config=self._rec_cfg,
129
+ dedup=self._dedup,
130
+ crop=self._crop,
131
+ )
132
+ return self._ocr
133
+
134
+ # -- public API -------------------------------------------------------- #
135
+
136
+ def detect(self, image_path: str) -> dict:
137
+ """Stage 1 only -- blocks, labels, reading order. Loads no recognizer."""
138
+ return self.layout.detect(image_path).as_record()
139
+
140
+ def parse(self, image_path: str) -> dict:
141
+ """Both stages -> ``{image, width, height, blocks, markdown}``."""
142
+ page = self.recognizer.run(image_path, self.layout.detect(image_path))
143
+ return {**page.as_record(), "markdown": page.markdown}
144
+
145
+ def __call__(self, image_path: str) -> str:
146
+ """The markdown for a page. ``parse()`` if you also want the per-block JSON."""
147
+ return self.parse(image_path)["markdown"]
install.sh ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Install IndicDocParser's dependencies into the active Python environment.
3
+ #
4
+ # ./install.sh # pick the CUDA wheels from your driver
5
+ # IDP_CUDA=cu121 ./install.sh # force a CUDA line
6
+ # IDP_CUDA=cpu ./install.sh # CPU only -- fine for layout, slow for the recognizer
7
+ #
8
+ # The one thing pip cannot work out for itself is which CUDA build you need. `pip install torch`
9
+ # takes the newest wheel, which on an older driver initialises to CPU with nothing but a warning --
10
+ # you get a working install that silently never touches the GPU. And torchvision pins an exact
11
+ # torch, so resolving the two separately is how you end up with a mismatched pair and a run of
12
+ # import errors. This installs both together, from one index.
13
+ set -euo pipefail
14
+
15
+ cd "$(dirname "$0")"
16
+ PY=${PYTHON:-python3}
17
+
18
+ # `uv venv` creates environments without pip, so `$PY -m pip` is not a safe assumption. Prefer
19
+ # whatever the environment actually has; uv is also just faster.
20
+ if command -v uv >/dev/null 2>&1; then
21
+ pip_install() { uv pip install --python "$PY" "$@"; }
22
+ elif "$PY" -m pip --version >/dev/null 2>&1; then
23
+ pip_install() { "$PY" -m pip install "$@"; }
24
+ elif "$PY" -m ensurepip --upgrade >/dev/null 2>&1; then
25
+ pip_install() { "$PY" -m pip install "$@"; }
26
+ else
27
+ echo "no installer found for $PY -- install pip (python -m ensurepip) or uv, then re-run" >&2
28
+ exit 1
29
+ fi
30
+
31
+ detect_cuda() {
32
+ command -v nvidia-smi >/dev/null 2>&1 || { echo cpu; return; }
33
+ local v
34
+ v=$(nvidia-smi 2>/dev/null | sed -n 's/.*CUDA Version: \([0-9]*\)\.\([0-9]*\).*/\1/p' | head -1)
35
+ case "$v" in
36
+ # CUDA 12.x drivers run any cu12x wheel (minor version compatibility), so one build covers
37
+ # the whole line. A 13.x wheel on a 12.x driver does NOT work -- that is the trap.
38
+ 12) echo cu124 ;;
39
+ 13|1[4-9]) echo cu130 ;;
40
+ "") echo cpu ;;
41
+ *) echo cu124 ;;
42
+ esac
43
+ }
44
+
45
+ CUDA=${IDP_CUDA:-$(detect_cuda)}
46
+ INDEX=https://download.pytorch.org/whl/$CUDA
47
+
48
+ echo "==> target: $CUDA"
49
+ [ "$CUDA" = cpu ] && echo " (no GPU detected -- layout runs fine on CPU, the recognizer is slow)"
50
+
51
+ echo "==> torch + torchvision, together, from $INDEX"
52
+ pip_install torch torchvision --index-url "$INDEX"
53
+
54
+ echo "==> everything else"
55
+ pip_install -r requirements.txt
56
+
57
+ echo "==> checking"
58
+ $PY - <<'PY'
59
+ import torch, torchvision, transformers
60
+ print(f" torch {torch.__version__}")
61
+ print(f" torchvision {torchvision.__version__}")
62
+ print(f" transformers {transformers.__version__}")
63
+ print(f" cuda {'yes, ' + torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'NO -- running on CPU'}")
64
+ import sys; sys.path.insert(0, ".")
65
+ from indic_doc_parser import IndicDocParser # noqa: F401
66
+ print(" import ok")
67
+ PY
68
+ echo "==> done"
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The model code ships in this repo; these are its only third-party dependencies.
2
+ #
3
+ # Install torch and torchvision TOGETHER, from the index for your CUDA version -- torchvision pins
4
+ # an exact torch, so resolving them separately is how you end up with a mismatched pair:
5
+ #
6
+ # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124
7
+ # pip install -r requirements.txt
8
+ #
9
+ # ./install.sh does this for you.
10
+
11
+ torch>=2.4 # transformers 5.7 floor
12
+ torchvision # stage 2 only; MUST match torch (see above)
13
+ transformers>=5.7 # PPDocLayoutV3
14
+ accelerate>=1.1
15
+ numpy>=1.17
16
+ pillow>=10.0.1
17
+ huggingface_hub>=1.0
18
+
19
+ # scipy is NOT needed for inference. Finetuning the detector needs it (the Hungarian matcher).
schemas/layout_output.schema.json ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://huggingface.co/bodhan-ai/indic-doc-parser/raw/main/schemas/layout_output.schema.json",
4
+ "title": "Layout JSON (stage-1 output, stage-2 input)",
5
+ "description": "Blocks carry no 'text' key. Absence means not transcribed yet, which is distinct from text: \"\" in a parsed page.",
6
+ "type": "object",
7
+ "required": [
8
+ "image",
9
+ "width",
10
+ "height",
11
+ "blocks"
12
+ ],
13
+ "properties": {
14
+ "image": {
15
+ "type": "string"
16
+ },
17
+ "width": {
18
+ "type": "integer",
19
+ "exclusiveMinimum": 0
20
+ },
21
+ "height": {
22
+ "type": "integer",
23
+ "exclusiveMinimum": 0
24
+ },
25
+ "blocks": {
26
+ "type": "array",
27
+ "items": {
28
+ "$ref": "#/$defs/block"
29
+ }
30
+ }
31
+ },
32
+ "$defs": {
33
+ "block": {
34
+ "type": "object",
35
+ "required": [
36
+ "order",
37
+ "bbox_xyxy"
38
+ ],
39
+ "properties": {
40
+ "order": {
41
+ "type": "integer",
42
+ "minimum": 0,
43
+ "description": "Reading-order rank. Gap-free and 0-based across the page."
44
+ },
45
+ "label": {
46
+ "type": "string",
47
+ "description": "IndicDocLayout class. Restricted to the taxonomy unless 'type' is given."
48
+ },
49
+ "type": {
50
+ "type": "string",
51
+ "enum": [
52
+ "Text",
53
+ "Title",
54
+ "SectionHeader",
55
+ "Table",
56
+ "Equation",
57
+ "Caption",
58
+ "Footnote",
59
+ "PageHeader",
60
+ "PageFooter",
61
+ "PageNumber",
62
+ "Figure",
63
+ "Picture"
64
+ ],
65
+ "description": "Pipeline category; selects the prompt. Derived from 'label' when omitted."
66
+ },
67
+ "bbox_xyxy": {
68
+ "type": "array",
69
+ "items": {
70
+ "type": "number"
71
+ },
72
+ "minItems": 4,
73
+ "maxItems": 4,
74
+ "description": "Pixel [x0, y0, x1, y1], clamped to the page."
75
+ },
76
+ "conf": {
77
+ "type": "number",
78
+ "minimum": 0,
79
+ "maximum": 1
80
+ }
81
+ },
82
+ "allOf": [
83
+ {
84
+ "if": {
85
+ "not": {
86
+ "required": [
87
+ "type"
88
+ ]
89
+ }
90
+ },
91
+ "then": {
92
+ "required": [
93
+ "label"
94
+ ],
95
+ "properties": {
96
+ "label": {
97
+ "enum": [
98
+ "Question",
99
+ "Paragraph",
100
+ "Answer",
101
+ "List",
102
+ "Title",
103
+ "Section-title",
104
+ "Equation",
105
+ "Table",
106
+ "Diagram",
107
+ "Image",
108
+ "MCQ",
109
+ "Infobox",
110
+ "Sub-section-title",
111
+ "Expression",
112
+ "Image-caption",
113
+ "Placeholder-text",
114
+ "Chart",
115
+ "Solved-example",
116
+ "Footnote",
117
+ "Table-caption",
118
+ "Sub-sub-section-title",
119
+ "Footer",
120
+ "Header",
121
+ "Code",
122
+ "Page-number",
123
+ "Chapter-title",
124
+ "Chapter-end-section",
125
+ "Folio",
126
+ "Reference",
127
+ "Table-of-contents",
128
+ "Index",
129
+ "Advertisement",
130
+ "Author",
131
+ "Dateline",
132
+ "Contact-info",
133
+ "Website-link",
134
+ "Flag"
135
+ ]
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ],
141
+ "not": {
142
+ "required": [
143
+ "text"
144
+ ]
145
+ }
146
+ }
147
+ }
148
+ }
schemas/parse_output.schema.json ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://huggingface.co/bodhan-ai/indic-doc-parser/raw/main/schemas/parse_output.schema.json",
4
+ "title": "Parsed page JSON (final output)",
5
+ "description": "Every block carries 'text'.",
6
+ "type": "object",
7
+ "required": [
8
+ "image",
9
+ "width",
10
+ "height",
11
+ "blocks"
12
+ ],
13
+ "properties": {
14
+ "image": {
15
+ "type": "string"
16
+ },
17
+ "width": {
18
+ "type": "integer",
19
+ "exclusiveMinimum": 0
20
+ },
21
+ "height": {
22
+ "type": "integer",
23
+ "exclusiveMinimum": 0
24
+ },
25
+ "blocks": {
26
+ "type": "array",
27
+ "items": {
28
+ "$ref": "#/$defs/block"
29
+ }
30
+ }
31
+ },
32
+ "$defs": {
33
+ "block": {
34
+ "type": "object",
35
+ "required": [
36
+ "order",
37
+ "bbox_xyxy",
38
+ "text"
39
+ ],
40
+ "properties": {
41
+ "order": {
42
+ "type": "integer",
43
+ "minimum": 0,
44
+ "description": "Reading-order rank. Gap-free and 0-based across the page."
45
+ },
46
+ "label": {
47
+ "type": "string",
48
+ "description": "IndicDocLayout class. Restricted to the taxonomy unless 'type' is given."
49
+ },
50
+ "type": {
51
+ "type": "string",
52
+ "enum": [
53
+ "Text",
54
+ "Title",
55
+ "SectionHeader",
56
+ "Table",
57
+ "Equation",
58
+ "Caption",
59
+ "Footnote",
60
+ "PageHeader",
61
+ "PageFooter",
62
+ "PageNumber",
63
+ "Figure",
64
+ "Picture"
65
+ ],
66
+ "description": "Pipeline category; selects the prompt. Derived from 'label' when omitted."
67
+ },
68
+ "bbox_xyxy": {
69
+ "type": "array",
70
+ "items": {
71
+ "type": "number"
72
+ },
73
+ "minItems": 4,
74
+ "maxItems": 4,
75
+ "description": "Pixel [x0, y0, x1, y1], clamped to the page."
76
+ },
77
+ "conf": {
78
+ "type": "number",
79
+ "minimum": 0,
80
+ "maximum": 1
81
+ },
82
+ "text": {
83
+ "type": "string",
84
+ "description": "Transcription. \"\" means detected but deliberately not sent to the recognizer; the block is kept, not dropped."
85
+ }
86
+ },
87
+ "allOf": [
88
+ {
89
+ "if": {
90
+ "not": {
91
+ "required": [
92
+ "type"
93
+ ]
94
+ }
95
+ },
96
+ "then": {
97
+ "required": [
98
+ "label"
99
+ ],
100
+ "properties": {
101
+ "label": {
102
+ "enum": [
103
+ "Question",
104
+ "Paragraph",
105
+ "Answer",
106
+ "List",
107
+ "Title",
108
+ "Section-title",
109
+ "Equation",
110
+ "Table",
111
+ "Diagram",
112
+ "Image",
113
+ "MCQ",
114
+ "Infobox",
115
+ "Sub-section-title",
116
+ "Expression",
117
+ "Image-caption",
118
+ "Placeholder-text",
119
+ "Chart",
120
+ "Solved-example",
121
+ "Footnote",
122
+ "Table-caption",
123
+ "Sub-sub-section-title",
124
+ "Footer",
125
+ "Header",
126
+ "Code",
127
+ "Page-number",
128
+ "Chapter-title",
129
+ "Chapter-end-section",
130
+ "Folio",
131
+ "Reference",
132
+ "Table-of-contents",
133
+ "Index",
134
+ "Advertisement",
135
+ "Author",
136
+ "Dateline",
137
+ "Contact-info",
138
+ "Website-link",
139
+ "Flag"
140
+ ]
141
+ }
142
+ }
143
+ }
144
+ }
145
+ ]
146
+ }
147
+ }
148
+ }
weights/layout/config.json ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation_dropout": 0.0,
3
+ "activation_function": "silu",
4
+ "anchor_image_size": null,
5
+ "architectures": [
6
+ "PPDocLayoutV3Trainable"
7
+ ],
8
+ "attention_dropout": 0.0,
9
+ "auxiliary_loss": true,
10
+ "backbone": null,
11
+ "backbone_config": {
12
+ "arch": "L",
13
+ "depths": [
14
+ 3,
15
+ 4,
16
+ 6,
17
+ 3
18
+ ],
19
+ "dtype": "float32",
20
+ "embedding_size": 64,
21
+ "freeze_at": 0,
22
+ "freeze_norm": true,
23
+ "freeze_stem_only": true,
24
+ "hidden_act": "relu",
25
+ "hidden_sizes": [
26
+ 256,
27
+ 512,
28
+ 1024,
29
+ 2048
30
+ ],
31
+ "initializer_range": 0.02,
32
+ "lr_mult_list": [
33
+ 0,
34
+ 0.05,
35
+ 0.05,
36
+ 0.05,
37
+ 0.05
38
+ ],
39
+ "model_type": "hgnet_v2",
40
+ "num_channels": 3,
41
+ "out_features": [
42
+ "stage1",
43
+ "stage2",
44
+ "stage3",
45
+ "stage4"
46
+ ],
47
+ "out_indices": [
48
+ 1,
49
+ 2,
50
+ 3,
51
+ 4
52
+ ],
53
+ "return_idx": [
54
+ 0,
55
+ 1,
56
+ 2,
57
+ 3
58
+ ],
59
+ "stage_downsample": [
60
+ false,
61
+ true,
62
+ true,
63
+ true
64
+ ],
65
+ "stage_downsample_strides": [
66
+ 2,
67
+ 2,
68
+ 2,
69
+ 2
70
+ ],
71
+ "stage_in_channels": [
72
+ 48,
73
+ 128,
74
+ 512,
75
+ 1024
76
+ ],
77
+ "stage_kernel_size": [
78
+ 3,
79
+ 3,
80
+ 5,
81
+ 5
82
+ ],
83
+ "stage_light_block": [
84
+ false,
85
+ false,
86
+ true,
87
+ true
88
+ ],
89
+ "stage_mid_channels": [
90
+ 48,
91
+ 96,
92
+ 192,
93
+ 384
94
+ ],
95
+ "stage_names": [
96
+ "stem",
97
+ "stage1",
98
+ "stage2",
99
+ "stage3",
100
+ "stage4"
101
+ ],
102
+ "stage_num_blocks": [
103
+ 1,
104
+ 1,
105
+ 3,
106
+ 1
107
+ ],
108
+ "stage_numb_of_layers": [
109
+ 6,
110
+ 6,
111
+ 6,
112
+ 6
113
+ ],
114
+ "stage_out_channels": [
115
+ 128,
116
+ 512,
117
+ 1024,
118
+ 2048
119
+ ],
120
+ "stem_channels": [
121
+ 3,
122
+ 32,
123
+ 48
124
+ ],
125
+ "stem_strides": [
126
+ 2,
127
+ 1,
128
+ 1,
129
+ 2,
130
+ 1
131
+ ],
132
+ "use_learnable_affine_block": false
133
+ },
134
+ "batch_norm_eps": 1e-05,
135
+ "box_noise_scale": 1.0,
136
+ "d_model": 256,
137
+ "decoder_activation_function": "relu",
138
+ "decoder_attention_heads": 8,
139
+ "decoder_ffn_dim": 1024,
140
+ "decoder_in_channels": [
141
+ 256,
142
+ 256,
143
+ 256
144
+ ],
145
+ "decoder_layers": 6,
146
+ "decoder_n_points": 4,
147
+ "disable_custom_kernels": true,
148
+ "dropout": 0.0,
149
+ "dtype": "float32",
150
+ "encode_proj_layers": [
151
+ 2
152
+ ],
153
+ "encoder_activation_function": "gelu",
154
+ "encoder_attention_heads": 8,
155
+ "encoder_ffn_dim": 1024,
156
+ "encoder_hidden_dim": 256,
157
+ "encoder_in_channels": [
158
+ 512,
159
+ 1024,
160
+ 2048
161
+ ],
162
+ "encoder_layers": 1,
163
+ "eos_coefficient": 0.0001,
164
+ "eval_size": null,
165
+ "feat_strides": [
166
+ 8,
167
+ 16,
168
+ 32
169
+ ],
170
+ "feature_strides": [
171
+ 8,
172
+ 16,
173
+ 32
174
+ ],
175
+ "focal_loss_alpha": 0.25,
176
+ "focal_loss_gamma": 2.0,
177
+ "freeze_backbone_batch_norms": true,
178
+ "global_pointer_head_size": 64,
179
+ "gp_dropout_value": 0.1,
180
+ "hidden_expansion": 1.0,
181
+ "id2label": {
182
+ "0": "Question",
183
+ "1": "Paragraph",
184
+ "2": "Answer",
185
+ "3": "List",
186
+ "4": "Title",
187
+ "5": "Section-title",
188
+ "6": "Equation",
189
+ "7": "Table",
190
+ "8": "Diagram",
191
+ "9": "Image",
192
+ "10": "MCQ",
193
+ "11": "Infobox",
194
+ "12": "Sub-section-title",
195
+ "13": "Expression",
196
+ "14": "Image-caption",
197
+ "15": "Placeholder-text",
198
+ "16": "Chart",
199
+ "17": "Solved-example",
200
+ "18": "Footnote",
201
+ "19": "Table-caption",
202
+ "20": "Sub-sub-section-title",
203
+ "21": "Footer",
204
+ "22": "Header",
205
+ "23": "Code",
206
+ "24": "Page-number",
207
+ "25": "Chapter-title",
208
+ "26": "Chapter-end-section",
209
+ "27": "Folio",
210
+ "28": "Reference",
211
+ "29": "Table-of-contents",
212
+ "30": "Index",
213
+ "31": "Advertisement",
214
+ "32": "Author",
215
+ "33": "Dateline",
216
+ "34": "Contact-info",
217
+ "35": "Website-link",
218
+ "36": "Flag"
219
+ },
220
+ "initializer_bias_prior_prob": null,
221
+ "initializer_range": 0.01,
222
+ "is_encoder_decoder": true,
223
+ "label2id": {
224
+ "Advertisement": 31,
225
+ "Answer": 2,
226
+ "Author": 32,
227
+ "Chapter-end-section": 26,
228
+ "Chapter-title": 25,
229
+ "Chart": 16,
230
+ "Code": 23,
231
+ "Contact-info": 34,
232
+ "Dateline": 33,
233
+ "Diagram": 8,
234
+ "Equation": 6,
235
+ "Expression": 13,
236
+ "Flag": 36,
237
+ "Folio": 27,
238
+ "Footer": 21,
239
+ "Footnote": 18,
240
+ "Header": 22,
241
+ "Image": 9,
242
+ "Image-caption": 14,
243
+ "Index": 30,
244
+ "Infobox": 11,
245
+ "List": 3,
246
+ "MCQ": 10,
247
+ "Page-number": 24,
248
+ "Paragraph": 1,
249
+ "Placeholder-text": 15,
250
+ "Question": 0,
251
+ "Reference": 28,
252
+ "Section-title": 5,
253
+ "Solved-example": 17,
254
+ "Sub-section-title": 12,
255
+ "Sub-sub-section-title": 20,
256
+ "Table": 7,
257
+ "Table-caption": 19,
258
+ "Table-of-contents": 29,
259
+ "Title": 4,
260
+ "Website-link": 35
261
+ },
262
+ "label_noise_ratio": 0.5,
263
+ "lambda_order": 5.0,
264
+ "layer_norm_eps": 1e-05,
265
+ "learn_initial_query": false,
266
+ "loss_type": "RTDetrForObjectDetection",
267
+ "mask_enhanced": true,
268
+ "mask_feature_channels": [
269
+ 64,
270
+ 64
271
+ ],
272
+ "matcher_alpha": 0.25,
273
+ "matcher_bbox_cost": 5.0,
274
+ "matcher_class_cost": 2.0,
275
+ "matcher_gamma": 2.0,
276
+ "matcher_giou_cost": 2.0,
277
+ "model_type": "pp_doclayout_v3",
278
+ "normalize_before": false,
279
+ "num_denoising": 0,
280
+ "num_feature_levels": 3,
281
+ "num_prototypes": 32,
282
+ "num_queries": 300,
283
+ "positional_encoding_temperature": 10000,
284
+ "tie_word_embeddings": true,
285
+ "transformers_version": "5.8.1",
286
+ "use_cache": false,
287
+ "use_focal_loss": true,
288
+ "weight_loss_bbox": 5.0,
289
+ "weight_loss_giou": 2.0,
290
+ "weight_loss_vfl": 1.0,
291
+ "x4_feat_dim": 128
292
+ }
weights/layout/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:051fa9741b713f164eef74ef25b88defa87670ab64995cfe431d413685bf0a3f
3
+ size 133301428
weights/ocr/chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- elif content is iterable and content is not mapping %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if is_system_content %}
10
+ {{- raise_exception('System message cannot contain images.') }}
11
+ {%- endif %}
12
+ {%- if do_vision_count %}
13
+ {%- set image_count.value = image_count.value + 1 %}
14
+ {%- endif %}
15
+ {%- if add_vision_id %}
16
+ {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
+ {%- endif %}
18
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
+ {%- elif 'video' in item or item.type == 'video' %}
20
+ {%- if is_system_content %}
21
+ {{- raise_exception('System message cannot contain videos.') }}
22
+ {%- endif %}
23
+ {%- if do_vision_count %}
24
+ {%- set video_count.value = video_count.value + 1 %}
25
+ {%- endif %}
26
+ {%- if add_vision_id %}
27
+ {{- 'Video ' ~ video_count.value ~ ': ' }}
28
+ {%- endif %}
29
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
+ {%- elif 'text' in item %}
31
+ {{- item.text }}
32
+ {%- else %}
33
+ {{- raise_exception('Unexpected item type in content.') }}
34
+ {%- endif %}
35
+ {%- endfor %}
36
+ {%- elif content is none or content is undefined %}
37
+ {{- '' }}
38
+ {%- else %}
39
+ {{- raise_exception('Unexpected content type.') }}
40
+ {%- endif %}
41
+ {%- endmacro %}
42
+ {%- if not messages %}
43
+ {{- raise_exception('No messages provided.') }}
44
+ {%- endif %}
45
+ {%- if tools and tools is iterable and tools is not mapping %}
46
+ {{- '<|im_start|>system\n' }}
47
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
+ {%- for tool in tools %}
49
+ {{- "\n" }}
50
+ {{- tool | tojson }}
51
+ {%- endfor %}
52
+ {{- "\n</tools>" }}
53
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
+ {%- if messages[0].role == 'system' %}
55
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
56
+ {%- if content %}
57
+ {{- '\n\n' + content }}
58
+ {%- endif %}
59
+ {%- endif %}
60
+ {{- '<|im_end|>\n' }}
61
+ {%- else %}
62
+ {%- if messages[0].role == 'system' %}
63
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
64
+ {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
+ {%- for message in messages[::-1] %}
69
+ {%- set index = (messages|length - 1) - loop.index0 %}
70
+ {%- if ns.multi_step_tool and message.role == "user" %}
71
+ {%- set content = render_content(message.content, false)|trim %}
72
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
+ {%- set ns.multi_step_tool = false %}
74
+ {%- set ns.last_query_index = index %}
75
+ {%- endif %}
76
+ {%- endif %}
77
+ {%- endfor %}
78
+ {%- if ns.multi_step_tool %}
79
+ {{- raise_exception('No user query found in messages.') }}
80
+ {%- endif %}
81
+ {%- for message in messages %}
82
+ {%- set content = render_content(message.content, true)|trim %}
83
+ {%- if message.role == "system" %}
84
+ {%- if not loop.first %}
85
+ {{- raise_exception('System message must be at the beginning.') }}
86
+ {%- endif %}
87
+ {%- elif message.role == "user" %}
88
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
+ {%- elif message.role == "assistant" %}
90
+ {%- set reasoning_content = '' %}
91
+ {%- if message.reasoning_content is string %}
92
+ {%- set reasoning_content = message.reasoning_content %}
93
+ {%- else %}
94
+ {%- if '</think>' in content %}
95
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- set reasoning_content = reasoning_content|trim %}
100
+ {%- if loop.index0 > ns.last_query_index %}
101
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
+ {%- else %}
103
+ {{- '<|im_start|>' + message.role + '\n' + content }}
104
+ {%- endif %}
105
+ {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
+ {%- for tool_call in message.tool_calls %}
107
+ {%- if tool_call.function is defined %}
108
+ {%- set tool_call = tool_call.function %}
109
+ {%- endif %}
110
+ {%- if loop.first %}
111
+ {%- if content|trim %}
112
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
+ {%- else %}
114
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
+ {%- endif %}
116
+ {%- else %}
117
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
+ {%- endif %}
119
+ {%- if tool_call.arguments is defined %}
120
+ {%- for args_name, args_value in tool_call.arguments|items %}
121
+ {{- '<parameter=' + args_name + '>\n' }}
122
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
+ {{- args_value }}
124
+ {{- '\n</parameter>\n' }}
125
+ {%- endfor %}
126
+ {%- endif %}
127
+ {{- '</function>\n</tool_call>' }}
128
+ {%- endfor %}
129
+ {%- endif %}
130
+ {{- '<|im_end|>\n' }}
131
+ {%- elif message.role == "tool" %}
132
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
133
+ {{- '<|im_start|>user' }}
134
+ {%- endif %}
135
+ {{- '\n<tool_response>\n' }}
136
+ {{- content }}
137
+ {{- '\n</tool_response>' }}
138
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
139
+ {{- '<|im_end|>\n' }}
140
+ {%- elif loop.last %}
141
+ {{- '<|im_end|>\n' }}
142
+ {%- endif %}
143
+ {%- else %}
144
+ {{- raise_exception('Unexpected message role.') }}
145
+ {%- endif %}
146
+ {%- endfor %}
147
+ {%- if add_generation_prompt %}
148
+ {{- '<|im_start|>assistant\n' }}
149
+ {%- if enable_thinking is defined and enable_thinking is true %}
150
+ {{- '<think>\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n\n</think>\n\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
weights/ocr/config.json ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "/projects/data/visionteam/sherry/qwen08b/ckpts/merges_v5/S2b",
3
+ "architectures": [
4
+ "Qwen3_5ForConditionalGeneration"
5
+ ],
6
+ "chunk_size_feed_forward": 0,
7
+ "dtype": "bfloat16",
8
+ "id2label": {
9
+ "0": "LABEL_0",
10
+ "1": "LABEL_1"
11
+ },
12
+ "image_token_id": 262155,
13
+ "is_encoder_decoder": false,
14
+ "label2id": {
15
+ "LABEL_0": 0,
16
+ "LABEL_1": 1
17
+ },
18
+ "model_type": "qwen3_5",
19
+ "output_attentions": false,
20
+ "output_hidden_states": false,
21
+ "problem_type": null,
22
+ "return_dict": true,
23
+ "text_config": {
24
+ "_name_or_path": "",
25
+ "architectures": null,
26
+ "attention_bias": false,
27
+ "attention_dropout": 0.0,
28
+ "attn_output_gate": true,
29
+ "bos_token_id": null,
30
+ "chunk_size_feed_forward": 0,
31
+ "dtype": "bfloat16",
32
+ "eos_token_id": 262146,
33
+ "full_attention_interval": 4,
34
+ "head_dim": 256,
35
+ "hidden_act": "silu",
36
+ "hidden_size": 1024,
37
+ "id2label": {
38
+ "0": "LABEL_0",
39
+ "1": "LABEL_1"
40
+ },
41
+ "initializer_range": 0.02,
42
+ "intermediate_size": 3584,
43
+ "is_encoder_decoder": false,
44
+ "label2id": {
45
+ "LABEL_0": 0,
46
+ "LABEL_1": 1
47
+ },
48
+ "layer_types": [
49
+ "linear_attention",
50
+ "linear_attention",
51
+ "linear_attention",
52
+ "full_attention",
53
+ "linear_attention",
54
+ "linear_attention",
55
+ "linear_attention",
56
+ "full_attention",
57
+ "linear_attention",
58
+ "linear_attention",
59
+ "linear_attention",
60
+ "full_attention",
61
+ "linear_attention",
62
+ "linear_attention",
63
+ "linear_attention",
64
+ "full_attention",
65
+ "linear_attention",
66
+ "linear_attention",
67
+ "linear_attention",
68
+ "full_attention",
69
+ "linear_attention",
70
+ "linear_attention",
71
+ "linear_attention",
72
+ "full_attention"
73
+ ],
74
+ "linear_conv_kernel_dim": 4,
75
+ "linear_key_head_dim": 128,
76
+ "linear_num_key_heads": 16,
77
+ "linear_num_value_heads": 16,
78
+ "linear_value_head_dim": 128,
79
+ "mamba_ssm_dtype": "float32",
80
+ "max_position_embeddings": 262144,
81
+ "mlp_only_layers": [],
82
+ "model_type": "qwen3_5_text",
83
+ "mtp_num_hidden_layers": 1,
84
+ "mtp_use_dedicated_embeddings": false,
85
+ "num_attention_heads": 8,
86
+ "num_hidden_layers": 24,
87
+ "num_key_value_heads": 2,
88
+ "output_attentions": false,
89
+ "output_hidden_states": false,
90
+ "pad_token_id": 262144,
91
+ "partial_rotary_factor": 0.25,
92
+ "problem_type": null,
93
+ "return_dict": true,
94
+ "rms_norm_eps": 1e-06,
95
+ "rope_parameters": {
96
+ "mrope_interleaved": true,
97
+ "mrope_section": [
98
+ 11,
99
+ 11,
100
+ 10
101
+ ],
102
+ "partial_rotary_factor": 0.25,
103
+ "rope_theta": 10000000,
104
+ "rope_type": "default"
105
+ },
106
+ "tie_word_embeddings": true,
107
+ "use_cache": false,
108
+ "vocab_size": 262157
109
+ },
110
+ "tie_word_embeddings": true,
111
+ "transformers_version": "5.6.2",
112
+ "use_cache": false,
113
+ "video_token_id": 262156,
114
+ "vision_config": {
115
+ "_name_or_path": "",
116
+ "architectures": null,
117
+ "chunk_size_feed_forward": 0,
118
+ "deepstack_visual_indexes": [],
119
+ "depth": 12,
120
+ "dtype": "bfloat16",
121
+ "hidden_act": "gelu_pytorch_tanh",
122
+ "hidden_size": 768,
123
+ "id2label": {
124
+ "0": "LABEL_0",
125
+ "1": "LABEL_1"
126
+ },
127
+ "in_channels": 3,
128
+ "initializer_range": 0.02,
129
+ "intermediate_size": 3072,
130
+ "is_encoder_decoder": false,
131
+ "label2id": {
132
+ "LABEL_0": 0,
133
+ "LABEL_1": 1
134
+ },
135
+ "model_type": "qwen3_5_vision",
136
+ "num_heads": 12,
137
+ "num_position_embeddings": 2304,
138
+ "out_hidden_size": 1024,
139
+ "output_attentions": false,
140
+ "output_hidden_states": false,
141
+ "patch_size": 16,
142
+ "problem_type": null,
143
+ "return_dict": true,
144
+ "spatial_merge_size": 2,
145
+ "temporal_patch_size": 2
146
+ },
147
+ "vision_end_token_id": 262154,
148
+ "vision_start_token_id": 262153
149
+ }
weights/ocr/generation_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": 262146,
4
+ "transformers_version": "5.6.2",
5
+ "use_cache": true
6
+ }
weights/ocr/model-00001-of-00001.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:40f6a5ce321d65ce18db429c9e8c8b2d2d22d4ec7d71737d150cc0a6edf9ae5f
3
+ size 1734372747
weights/ocr/model.safetensors.index.json ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "metadata": {
3
+ "total_size": 1734310016
4
+ },
5
+ "weight_map": {
6
+ "model.language_model.layers.0.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
7
+ "model.language_model.layers.0.linear_attn.A_log": "model-00001-of-00001.safetensors",
8
+ "model.language_model.layers.1.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
9
+ "model.language_model.layers.1.linear_attn.A_log": "model-00001-of-00001.safetensors",
10
+ "model.language_model.layers.2.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
11
+ "model.language_model.layers.2.linear_attn.A_log": "model-00001-of-00001.safetensors",
12
+ "model.language_model.layers.4.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
13
+ "model.language_model.layers.4.linear_attn.A_log": "model-00001-of-00001.safetensors",
14
+ "model.language_model.layers.5.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
15
+ "model.language_model.layers.5.linear_attn.A_log": "model-00001-of-00001.safetensors",
16
+ "model.language_model.layers.6.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
17
+ "model.language_model.layers.6.linear_attn.A_log": "model-00001-of-00001.safetensors",
18
+ "model.language_model.layers.8.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
19
+ "model.language_model.layers.8.linear_attn.A_log": "model-00001-of-00001.safetensors",
20
+ "model.language_model.layers.9.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
21
+ "model.language_model.layers.9.linear_attn.A_log": "model-00001-of-00001.safetensors",
22
+ "model.language_model.layers.10.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
23
+ "model.language_model.layers.10.linear_attn.A_log": "model-00001-of-00001.safetensors",
24
+ "model.language_model.layers.12.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
25
+ "model.language_model.layers.12.linear_attn.A_log": "model-00001-of-00001.safetensors",
26
+ "model.language_model.layers.13.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
27
+ "model.language_model.layers.13.linear_attn.A_log": "model-00001-of-00001.safetensors",
28
+ "model.language_model.layers.14.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
29
+ "model.language_model.layers.14.linear_attn.A_log": "model-00001-of-00001.safetensors",
30
+ "model.language_model.layers.16.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
31
+ "model.language_model.layers.16.linear_attn.A_log": "model-00001-of-00001.safetensors",
32
+ "model.language_model.layers.17.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
33
+ "model.language_model.layers.17.linear_attn.A_log": "model-00001-of-00001.safetensors",
34
+ "model.language_model.layers.18.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
35
+ "model.language_model.layers.18.linear_attn.A_log": "model-00001-of-00001.safetensors",
36
+ "model.language_model.layers.20.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
37
+ "model.language_model.layers.20.linear_attn.A_log": "model-00001-of-00001.safetensors",
38
+ "model.language_model.layers.21.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
39
+ "model.language_model.layers.21.linear_attn.A_log": "model-00001-of-00001.safetensors",
40
+ "model.language_model.layers.22.linear_attn.dt_bias": "model-00001-of-00001.safetensors",
41
+ "model.language_model.layers.22.linear_attn.A_log": "model-00001-of-00001.safetensors",
42
+ "model.language_model.layers.0.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
43
+ "model.language_model.layers.1.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
44
+ "model.language_model.layers.2.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
45
+ "model.language_model.layers.4.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
46
+ "model.language_model.layers.5.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
47
+ "model.language_model.layers.6.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
48
+ "model.language_model.layers.8.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
49
+ "model.language_model.layers.9.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
50
+ "model.language_model.layers.10.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
51
+ "model.language_model.layers.12.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
52
+ "model.language_model.layers.13.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
53
+ "model.language_model.layers.14.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
54
+ "model.language_model.layers.16.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
55
+ "model.language_model.layers.17.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
56
+ "model.language_model.layers.18.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
57
+ "model.language_model.layers.20.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
58
+ "model.language_model.layers.21.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
59
+ "model.language_model.layers.22.linear_attn.norm.weight": "model-00001-of-00001.safetensors",
60
+ "model.language_model.layers.3.self_attn.q_norm.weight": "model-00001-of-00001.safetensors",
61
+ "model.language_model.layers.3.self_attn.k_norm.weight": "model-00001-of-00001.safetensors",
62
+ "model.language_model.layers.7.self_attn.q_norm.weight": "model-00001-of-00001.safetensors",
63
+ "model.language_model.layers.7.self_attn.k_norm.weight": "model-00001-of-00001.safetensors",
64
+ "model.language_model.layers.11.self_attn.q_norm.weight": "model-00001-of-00001.safetensors",
65
+ "model.language_model.layers.11.self_attn.k_norm.weight": "model-00001-of-00001.safetensors",
66
+ "model.language_model.layers.15.self_attn.q_norm.weight": "model-00001-of-00001.safetensors",
67
+ "model.language_model.layers.15.self_attn.k_norm.weight": "model-00001-of-00001.safetensors",
68
+ "model.language_model.layers.19.self_attn.q_norm.weight": "model-00001-of-00001.safetensors",
69
+ "model.language_model.layers.19.self_attn.k_norm.weight": "model-00001-of-00001.safetensors",
70
+ "model.language_model.layers.23.self_attn.q_norm.weight": "model-00001-of-00001.safetensors",
71
+ "model.language_model.layers.23.self_attn.k_norm.weight": "model-00001-of-00001.safetensors",
72
+ "model.visual.patch_embed.proj.bias": "model-00001-of-00001.safetensors",
73
+ "model.visual.blocks.0.norm1.weight": "model-00001-of-00001.safetensors",
74
+ "model.visual.blocks.0.norm1.bias": "model-00001-of-00001.safetensors",
75
+ "model.visual.blocks.0.norm2.weight": "model-00001-of-00001.safetensors",
76
+ "model.visual.blocks.0.norm2.bias": "model-00001-of-00001.safetensors",
77
+ "model.visual.blocks.0.attn.proj.bias": "model-00001-of-00001.safetensors",
78
+ "model.visual.blocks.0.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
79
+ "model.visual.blocks.1.norm1.weight": "model-00001-of-00001.safetensors",
80
+ "model.visual.blocks.1.norm1.bias": "model-00001-of-00001.safetensors",
81
+ "model.visual.blocks.1.norm2.weight": "model-00001-of-00001.safetensors",
82
+ "model.visual.blocks.1.norm2.bias": "model-00001-of-00001.safetensors",
83
+ "model.visual.blocks.1.attn.proj.bias": "model-00001-of-00001.safetensors",
84
+ "model.visual.blocks.1.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
85
+ "model.visual.blocks.2.norm1.weight": "model-00001-of-00001.safetensors",
86
+ "model.visual.blocks.2.norm1.bias": "model-00001-of-00001.safetensors",
87
+ "model.visual.blocks.2.norm2.weight": "model-00001-of-00001.safetensors",
88
+ "model.visual.blocks.2.norm2.bias": "model-00001-of-00001.safetensors",
89
+ "model.visual.blocks.2.attn.proj.bias": "model-00001-of-00001.safetensors",
90
+ "model.visual.blocks.2.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
91
+ "model.visual.blocks.3.norm1.weight": "model-00001-of-00001.safetensors",
92
+ "model.visual.blocks.3.norm1.bias": "model-00001-of-00001.safetensors",
93
+ "model.visual.blocks.3.norm2.weight": "model-00001-of-00001.safetensors",
94
+ "model.visual.blocks.3.norm2.bias": "model-00001-of-00001.safetensors",
95
+ "model.visual.blocks.3.attn.proj.bias": "model-00001-of-00001.safetensors",
96
+ "model.visual.blocks.3.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
97
+ "model.visual.blocks.4.norm1.weight": "model-00001-of-00001.safetensors",
98
+ "model.visual.blocks.4.norm1.bias": "model-00001-of-00001.safetensors",
99
+ "model.visual.blocks.4.norm2.weight": "model-00001-of-00001.safetensors",
100
+ "model.visual.blocks.4.norm2.bias": "model-00001-of-00001.safetensors",
101
+ "model.visual.blocks.4.attn.proj.bias": "model-00001-of-00001.safetensors",
102
+ "model.visual.blocks.4.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
103
+ "model.visual.blocks.5.norm1.weight": "model-00001-of-00001.safetensors",
104
+ "model.visual.blocks.5.norm1.bias": "model-00001-of-00001.safetensors",
105
+ "model.visual.blocks.5.norm2.weight": "model-00001-of-00001.safetensors",
106
+ "model.visual.blocks.5.norm2.bias": "model-00001-of-00001.safetensors",
107
+ "model.visual.blocks.5.attn.proj.bias": "model-00001-of-00001.safetensors",
108
+ "model.visual.blocks.5.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
109
+ "model.visual.blocks.6.norm1.weight": "model-00001-of-00001.safetensors",
110
+ "model.visual.blocks.6.norm1.bias": "model-00001-of-00001.safetensors",
111
+ "model.visual.blocks.6.norm2.weight": "model-00001-of-00001.safetensors",
112
+ "model.visual.blocks.6.norm2.bias": "model-00001-of-00001.safetensors",
113
+ "model.visual.blocks.6.attn.proj.bias": "model-00001-of-00001.safetensors",
114
+ "model.visual.blocks.6.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
115
+ "model.visual.blocks.7.norm1.weight": "model-00001-of-00001.safetensors",
116
+ "model.visual.blocks.7.norm1.bias": "model-00001-of-00001.safetensors",
117
+ "model.visual.blocks.7.norm2.weight": "model-00001-of-00001.safetensors",
118
+ "model.visual.blocks.7.norm2.bias": "model-00001-of-00001.safetensors",
119
+ "model.visual.blocks.7.attn.proj.bias": "model-00001-of-00001.safetensors",
120
+ "model.visual.blocks.7.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
121
+ "model.visual.blocks.8.norm1.weight": "model-00001-of-00001.safetensors",
122
+ "model.visual.blocks.8.norm1.bias": "model-00001-of-00001.safetensors",
123
+ "model.visual.blocks.8.norm2.weight": "model-00001-of-00001.safetensors",
124
+ "model.visual.blocks.8.norm2.bias": "model-00001-of-00001.safetensors",
125
+ "model.visual.blocks.8.attn.proj.bias": "model-00001-of-00001.safetensors",
126
+ "model.visual.blocks.8.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
127
+ "model.visual.blocks.9.norm1.weight": "model-00001-of-00001.safetensors",
128
+ "model.visual.blocks.9.norm1.bias": "model-00001-of-00001.safetensors",
129
+ "model.visual.blocks.9.norm2.weight": "model-00001-of-00001.safetensors",
130
+ "model.visual.blocks.9.norm2.bias": "model-00001-of-00001.safetensors",
131
+ "model.visual.blocks.9.attn.proj.bias": "model-00001-of-00001.safetensors",
132
+ "model.visual.blocks.9.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
133
+ "model.visual.blocks.10.norm1.weight": "model-00001-of-00001.safetensors",
134
+ "model.visual.blocks.10.norm1.bias": "model-00001-of-00001.safetensors",
135
+ "model.visual.blocks.10.norm2.weight": "model-00001-of-00001.safetensors",
136
+ "model.visual.blocks.10.norm2.bias": "model-00001-of-00001.safetensors",
137
+ "model.visual.blocks.10.attn.proj.bias": "model-00001-of-00001.safetensors",
138
+ "model.visual.blocks.10.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
139
+ "model.visual.blocks.11.norm1.weight": "model-00001-of-00001.safetensors",
140
+ "model.visual.blocks.11.norm1.bias": "model-00001-of-00001.safetensors",
141
+ "model.visual.blocks.11.norm2.weight": "model-00001-of-00001.safetensors",
142
+ "model.visual.blocks.11.norm2.bias": "model-00001-of-00001.safetensors",
143
+ "model.visual.blocks.11.attn.proj.bias": "model-00001-of-00001.safetensors",
144
+ "model.visual.blocks.11.mlp.linear_fc2.bias": "model-00001-of-00001.safetensors",
145
+ "model.visual.merger.norm.weight": "model-00001-of-00001.safetensors",
146
+ "model.visual.merger.norm.bias": "model-00001-of-00001.safetensors",
147
+ "model.visual.merger.linear_fc2.bias": "model-00001-of-00001.safetensors",
148
+ "model.language_model.layers.0.input_layernorm.weight": "model-00001-of-00001.safetensors",
149
+ "model.language_model.layers.0.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
150
+ "model.language_model.layers.1.input_layernorm.weight": "model-00001-of-00001.safetensors",
151
+ "model.language_model.layers.1.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
152
+ "model.language_model.layers.2.input_layernorm.weight": "model-00001-of-00001.safetensors",
153
+ "model.language_model.layers.2.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
154
+ "model.language_model.layers.3.input_layernorm.weight": "model-00001-of-00001.safetensors",
155
+ "model.language_model.layers.3.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
156
+ "model.language_model.layers.4.input_layernorm.weight": "model-00001-of-00001.safetensors",
157
+ "model.language_model.layers.4.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
158
+ "model.language_model.layers.5.input_layernorm.weight": "model-00001-of-00001.safetensors",
159
+ "model.language_model.layers.5.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
160
+ "model.language_model.layers.6.input_layernorm.weight": "model-00001-of-00001.safetensors",
161
+ "model.language_model.layers.6.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
162
+ "model.language_model.layers.7.input_layernorm.weight": "model-00001-of-00001.safetensors",
163
+ "model.language_model.layers.7.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
164
+ "model.language_model.layers.8.input_layernorm.weight": "model-00001-of-00001.safetensors",
165
+ "model.language_model.layers.8.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
166
+ "model.language_model.layers.9.input_layernorm.weight": "model-00001-of-00001.safetensors",
167
+ "model.language_model.layers.9.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
168
+ "model.language_model.layers.10.input_layernorm.weight": "model-00001-of-00001.safetensors",
169
+ "model.language_model.layers.10.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
170
+ "model.language_model.layers.11.input_layernorm.weight": "model-00001-of-00001.safetensors",
171
+ "model.language_model.layers.11.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
172
+ "model.language_model.layers.12.input_layernorm.weight": "model-00001-of-00001.safetensors",
173
+ "model.language_model.layers.12.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
174
+ "model.language_model.layers.13.input_layernorm.weight": "model-00001-of-00001.safetensors",
175
+ "model.language_model.layers.13.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
176
+ "model.language_model.layers.14.input_layernorm.weight": "model-00001-of-00001.safetensors",
177
+ "model.language_model.layers.14.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
178
+ "model.language_model.layers.15.input_layernorm.weight": "model-00001-of-00001.safetensors",
179
+ "model.language_model.layers.15.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
180
+ "model.language_model.layers.16.input_layernorm.weight": "model-00001-of-00001.safetensors",
181
+ "model.language_model.layers.16.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
182
+ "model.language_model.layers.17.input_layernorm.weight": "model-00001-of-00001.safetensors",
183
+ "model.language_model.layers.17.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
184
+ "model.language_model.layers.18.input_layernorm.weight": "model-00001-of-00001.safetensors",
185
+ "model.language_model.layers.18.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
186
+ "model.language_model.layers.19.input_layernorm.weight": "model-00001-of-00001.safetensors",
187
+ "model.language_model.layers.19.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
188
+ "model.language_model.layers.20.input_layernorm.weight": "model-00001-of-00001.safetensors",
189
+ "model.language_model.layers.20.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
190
+ "model.language_model.layers.21.input_layernorm.weight": "model-00001-of-00001.safetensors",
191
+ "model.language_model.layers.21.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
192
+ "model.language_model.layers.22.input_layernorm.weight": "model-00001-of-00001.safetensors",
193
+ "model.language_model.layers.22.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
194
+ "model.language_model.layers.23.input_layernorm.weight": "model-00001-of-00001.safetensors",
195
+ "model.language_model.layers.23.post_attention_layernorm.weight": "model-00001-of-00001.safetensors",
196
+ "model.language_model.norm.weight": "model-00001-of-00001.safetensors",
197
+ "model.visual.blocks.0.attn.qkv.bias": "model-00001-of-00001.safetensors",
198
+ "model.visual.blocks.1.attn.qkv.bias": "model-00001-of-00001.safetensors",
199
+ "model.visual.blocks.2.attn.qkv.bias": "model-00001-of-00001.safetensors",
200
+ "model.visual.blocks.3.attn.qkv.bias": "model-00001-of-00001.safetensors",
201
+ "model.visual.blocks.4.attn.qkv.bias": "model-00001-of-00001.safetensors",
202
+ "model.visual.blocks.5.attn.qkv.bias": "model-00001-of-00001.safetensors",
203
+ "model.visual.blocks.6.attn.qkv.bias": "model-00001-of-00001.safetensors",
204
+ "model.visual.blocks.7.attn.qkv.bias": "model-00001-of-00001.safetensors",
205
+ "model.visual.blocks.8.attn.qkv.bias": "model-00001-of-00001.safetensors",
206
+ "model.visual.blocks.9.attn.qkv.bias": "model-00001-of-00001.safetensors",
207
+ "model.visual.blocks.10.attn.qkv.bias": "model-00001-of-00001.safetensors",
208
+ "model.visual.blocks.11.attn.qkv.bias": "model-00001-of-00001.safetensors",
209
+ "model.visual.blocks.0.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
210
+ "model.visual.blocks.1.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
211
+ "model.visual.blocks.2.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
212
+ "model.visual.blocks.3.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
213
+ "model.visual.blocks.4.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
214
+ "model.visual.blocks.5.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
215
+ "model.visual.blocks.6.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
216
+ "model.visual.blocks.7.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
217
+ "model.visual.blocks.8.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
218
+ "model.visual.blocks.9.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
219
+ "model.visual.blocks.10.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
220
+ "model.visual.blocks.11.mlp.linear_fc1.bias": "model-00001-of-00001.safetensors",
221
+ "model.visual.merger.linear_fc1.bias": "model-00001-of-00001.safetensors",
222
+ "model.language_model.layers.0.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
223
+ "model.language_model.layers.1.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
224
+ "model.language_model.layers.2.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
225
+ "model.language_model.layers.4.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
226
+ "model.language_model.layers.5.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
227
+ "model.language_model.layers.6.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
228
+ "model.language_model.layers.8.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
229
+ "model.language_model.layers.9.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
230
+ "model.language_model.layers.10.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
231
+ "model.language_model.layers.12.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
232
+ "model.language_model.layers.13.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
233
+ "model.language_model.layers.14.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
234
+ "model.language_model.layers.16.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
235
+ "model.language_model.layers.17.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
236
+ "model.language_model.layers.18.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
237
+ "model.language_model.layers.20.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
238
+ "model.language_model.layers.21.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
239
+ "model.language_model.layers.22.linear_attn.conv1d.weight": "model-00001-of-00001.safetensors",
240
+ "model.language_model.layers.0.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
241
+ "model.language_model.layers.0.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
242
+ "model.language_model.layers.1.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
243
+ "model.language_model.layers.1.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
244
+ "model.language_model.layers.2.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
245
+ "model.language_model.layers.2.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
246
+ "model.language_model.layers.4.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
247
+ "model.language_model.layers.4.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
248
+ "model.language_model.layers.5.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
249
+ "model.language_model.layers.5.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
250
+ "model.language_model.layers.6.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
251
+ "model.language_model.layers.6.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
252
+ "model.language_model.layers.8.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
253
+ "model.language_model.layers.8.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
254
+ "model.language_model.layers.9.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
255
+ "model.language_model.layers.9.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
256
+ "model.language_model.layers.10.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
257
+ "model.language_model.layers.10.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
258
+ "model.language_model.layers.12.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
259
+ "model.language_model.layers.12.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
260
+ "model.language_model.layers.13.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
261
+ "model.language_model.layers.13.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
262
+ "model.language_model.layers.14.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
263
+ "model.language_model.layers.14.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
264
+ "model.language_model.layers.16.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
265
+ "model.language_model.layers.16.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
266
+ "model.language_model.layers.17.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
267
+ "model.language_model.layers.17.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
268
+ "model.language_model.layers.18.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
269
+ "model.language_model.layers.18.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
270
+ "model.language_model.layers.20.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
271
+ "model.language_model.layers.20.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
272
+ "model.language_model.layers.21.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
273
+ "model.language_model.layers.21.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
274
+ "model.language_model.layers.22.linear_attn.in_proj_b.weight": "model-00001-of-00001.safetensors",
275
+ "model.language_model.layers.22.linear_attn.in_proj_a.weight": "model-00001-of-00001.safetensors",
276
+ "model.language_model.layers.3.self_attn.k_proj.weight": "model-00001-of-00001.safetensors",
277
+ "model.language_model.layers.3.self_attn.v_proj.weight": "model-00001-of-00001.safetensors",
278
+ "model.language_model.layers.7.self_attn.k_proj.weight": "model-00001-of-00001.safetensors",
279
+ "model.language_model.layers.7.self_attn.v_proj.weight": "model-00001-of-00001.safetensors",
280
+ "model.language_model.layers.11.self_attn.k_proj.weight": "model-00001-of-00001.safetensors",
281
+ "model.language_model.layers.11.self_attn.v_proj.weight": "model-00001-of-00001.safetensors",
282
+ "model.language_model.layers.15.self_attn.k_proj.weight": "model-00001-of-00001.safetensors",
283
+ "model.language_model.layers.15.self_attn.v_proj.weight": "model-00001-of-00001.safetensors",
284
+ "model.language_model.layers.19.self_attn.k_proj.weight": "model-00001-of-00001.safetensors",
285
+ "model.language_model.layers.19.self_attn.v_proj.weight": "model-00001-of-00001.safetensors",
286
+ "model.language_model.layers.23.self_attn.k_proj.weight": "model-00001-of-00001.safetensors",
287
+ "model.language_model.layers.23.self_attn.v_proj.weight": "model-00001-of-00001.safetensors",
288
+ "model.visual.blocks.0.attn.proj.weight": "model-00001-of-00001.safetensors",
289
+ "model.visual.blocks.1.attn.proj.weight": "model-00001-of-00001.safetensors",
290
+ "model.visual.blocks.2.attn.proj.weight": "model-00001-of-00001.safetensors",
291
+ "model.visual.blocks.3.attn.proj.weight": "model-00001-of-00001.safetensors",
292
+ "model.visual.blocks.4.attn.proj.weight": "model-00001-of-00001.safetensors",
293
+ "model.visual.blocks.5.attn.proj.weight": "model-00001-of-00001.safetensors",
294
+ "model.visual.blocks.6.attn.proj.weight": "model-00001-of-00001.safetensors",
295
+ "model.visual.blocks.7.attn.proj.weight": "model-00001-of-00001.safetensors",
296
+ "model.visual.blocks.8.attn.proj.weight": "model-00001-of-00001.safetensors",
297
+ "model.visual.blocks.9.attn.proj.weight": "model-00001-of-00001.safetensors",
298
+ "model.visual.blocks.10.attn.proj.weight": "model-00001-of-00001.safetensors",
299
+ "model.visual.blocks.11.attn.proj.weight": "model-00001-of-00001.safetensors",
300
+ "model.visual.patch_embed.proj.weight": "model-00001-of-00001.safetensors",
301
+ "model.visual.pos_embed.weight": "model-00001-of-00001.safetensors",
302
+ "model.visual.blocks.0.attn.qkv.weight": "model-00001-of-00001.safetensors",
303
+ "model.visual.blocks.1.attn.qkv.weight": "model-00001-of-00001.safetensors",
304
+ "model.visual.blocks.2.attn.qkv.weight": "model-00001-of-00001.safetensors",
305
+ "model.visual.blocks.3.attn.qkv.weight": "model-00001-of-00001.safetensors",
306
+ "model.visual.blocks.4.attn.qkv.weight": "model-00001-of-00001.safetensors",
307
+ "model.visual.blocks.5.attn.qkv.weight": "model-00001-of-00001.safetensors",
308
+ "model.visual.blocks.6.attn.qkv.weight": "model-00001-of-00001.safetensors",
309
+ "model.visual.blocks.7.attn.qkv.weight": "model-00001-of-00001.safetensors",
310
+ "model.visual.blocks.8.attn.qkv.weight": "model-00001-of-00001.safetensors",
311
+ "model.visual.blocks.9.attn.qkv.weight": "model-00001-of-00001.safetensors",
312
+ "model.visual.blocks.10.attn.qkv.weight": "model-00001-of-00001.safetensors",
313
+ "model.visual.blocks.11.attn.qkv.weight": "model-00001-of-00001.safetensors",
314
+ "model.language_model.layers.0.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
315
+ "model.language_model.layers.0.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
316
+ "model.language_model.layers.1.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
317
+ "model.language_model.layers.1.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
318
+ "model.language_model.layers.2.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
319
+ "model.language_model.layers.2.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
320
+ "model.language_model.layers.3.self_attn.o_proj.weight": "model-00001-of-00001.safetensors",
321
+ "model.language_model.layers.4.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
322
+ "model.language_model.layers.4.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
323
+ "model.language_model.layers.5.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
324
+ "model.language_model.layers.5.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
325
+ "model.language_model.layers.6.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
326
+ "model.language_model.layers.6.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
327
+ "model.language_model.layers.7.self_attn.o_proj.weight": "model-00001-of-00001.safetensors",
328
+ "model.language_model.layers.8.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
329
+ "model.language_model.layers.8.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
330
+ "model.language_model.layers.9.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
331
+ "model.language_model.layers.9.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
332
+ "model.language_model.layers.10.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
333
+ "model.language_model.layers.10.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
334
+ "model.language_model.layers.11.self_attn.o_proj.weight": "model-00001-of-00001.safetensors",
335
+ "model.language_model.layers.12.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
336
+ "model.language_model.layers.12.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
337
+ "model.language_model.layers.13.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
338
+ "model.language_model.layers.13.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
339
+ "model.language_model.layers.14.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
340
+ "model.language_model.layers.14.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
341
+ "model.language_model.layers.15.self_attn.o_proj.weight": "model-00001-of-00001.safetensors",
342
+ "model.language_model.layers.16.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
343
+ "model.language_model.layers.16.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
344
+ "model.language_model.layers.17.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
345
+ "model.language_model.layers.17.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
346
+ "model.language_model.layers.18.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
347
+ "model.language_model.layers.18.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
348
+ "model.language_model.layers.19.self_attn.o_proj.weight": "model-00001-of-00001.safetensors",
349
+ "model.language_model.layers.20.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
350
+ "model.language_model.layers.20.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
351
+ "model.language_model.layers.21.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
352
+ "model.language_model.layers.21.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
353
+ "model.language_model.layers.22.linear_attn.out_proj.weight": "model-00001-of-00001.safetensors",
354
+ "model.language_model.layers.22.linear_attn.in_proj_z.weight": "model-00001-of-00001.safetensors",
355
+ "model.language_model.layers.23.self_attn.o_proj.weight": "model-00001-of-00001.safetensors",
356
+ "model.visual.blocks.0.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
357
+ "model.visual.blocks.0.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
358
+ "model.visual.blocks.1.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
359
+ "model.visual.blocks.1.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
360
+ "model.visual.blocks.2.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
361
+ "model.visual.blocks.2.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
362
+ "model.visual.blocks.3.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
363
+ "model.visual.blocks.3.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
364
+ "model.visual.blocks.4.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
365
+ "model.visual.blocks.4.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
366
+ "model.visual.blocks.5.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
367
+ "model.visual.blocks.5.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
368
+ "model.visual.blocks.6.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
369
+ "model.visual.blocks.6.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
370
+ "model.visual.blocks.7.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
371
+ "model.visual.blocks.7.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
372
+ "model.visual.blocks.8.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
373
+ "model.visual.blocks.8.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
374
+ "model.visual.blocks.9.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
375
+ "model.visual.blocks.9.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
376
+ "model.visual.blocks.10.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
377
+ "model.visual.blocks.10.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
378
+ "model.visual.blocks.11.mlp.linear_fc1.weight": "model-00001-of-00001.safetensors",
379
+ "model.visual.blocks.11.mlp.linear_fc2.weight": "model-00001-of-00001.safetensors",
380
+ "model.visual.merger.linear_fc2.weight": "model-00001-of-00001.safetensors",
381
+ "model.language_model.layers.0.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
382
+ "model.language_model.layers.0.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
383
+ "model.language_model.layers.0.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
384
+ "model.language_model.layers.1.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
385
+ "model.language_model.layers.1.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
386
+ "model.language_model.layers.1.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
387
+ "model.language_model.layers.2.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
388
+ "model.language_model.layers.2.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
389
+ "model.language_model.layers.2.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
390
+ "model.language_model.layers.3.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
391
+ "model.language_model.layers.3.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
392
+ "model.language_model.layers.3.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
393
+ "model.language_model.layers.4.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
394
+ "model.language_model.layers.4.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
395
+ "model.language_model.layers.4.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
396
+ "model.language_model.layers.5.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
397
+ "model.language_model.layers.5.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
398
+ "model.language_model.layers.5.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
399
+ "model.language_model.layers.6.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
400
+ "model.language_model.layers.6.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
401
+ "model.language_model.layers.6.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
402
+ "model.language_model.layers.7.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
403
+ "model.language_model.layers.7.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
404
+ "model.language_model.layers.7.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
405
+ "model.language_model.layers.8.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
406
+ "model.language_model.layers.8.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
407
+ "model.language_model.layers.8.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
408
+ "model.language_model.layers.9.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
409
+ "model.language_model.layers.9.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
410
+ "model.language_model.layers.9.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
411
+ "model.language_model.layers.10.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
412
+ "model.language_model.layers.10.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
413
+ "model.language_model.layers.10.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
414
+ "model.language_model.layers.11.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
415
+ "model.language_model.layers.11.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
416
+ "model.language_model.layers.11.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
417
+ "model.language_model.layers.12.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
418
+ "model.language_model.layers.12.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
419
+ "model.language_model.layers.12.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
420
+ "model.language_model.layers.13.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
421
+ "model.language_model.layers.13.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
422
+ "model.language_model.layers.13.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
423
+ "model.language_model.layers.14.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
424
+ "model.language_model.layers.14.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
425
+ "model.language_model.layers.14.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
426
+ "model.language_model.layers.15.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
427
+ "model.language_model.layers.15.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
428
+ "model.language_model.layers.15.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
429
+ "model.language_model.layers.16.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
430
+ "model.language_model.layers.16.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
431
+ "model.language_model.layers.16.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
432
+ "model.language_model.layers.17.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
433
+ "model.language_model.layers.17.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
434
+ "model.language_model.layers.17.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
435
+ "model.language_model.layers.18.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
436
+ "model.language_model.layers.18.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
437
+ "model.language_model.layers.18.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
438
+ "model.language_model.layers.19.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
439
+ "model.language_model.layers.19.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
440
+ "model.language_model.layers.19.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
441
+ "model.language_model.layers.20.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
442
+ "model.language_model.layers.20.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
443
+ "model.language_model.layers.20.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
444
+ "model.language_model.layers.21.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
445
+ "model.language_model.layers.21.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
446
+ "model.language_model.layers.21.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
447
+ "model.language_model.layers.22.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
448
+ "model.language_model.layers.22.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
449
+ "model.language_model.layers.22.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
450
+ "model.language_model.layers.23.mlp.gate_proj.weight": "model-00001-of-00001.safetensors",
451
+ "model.language_model.layers.23.mlp.up_proj.weight": "model-00001-of-00001.safetensors",
452
+ "model.language_model.layers.23.mlp.down_proj.weight": "model-00001-of-00001.safetensors",
453
+ "model.language_model.layers.3.self_attn.q_proj.weight": "model-00001-of-00001.safetensors",
454
+ "model.language_model.layers.7.self_attn.q_proj.weight": "model-00001-of-00001.safetensors",
455
+ "model.language_model.layers.11.self_attn.q_proj.weight": "model-00001-of-00001.safetensors",
456
+ "model.language_model.layers.15.self_attn.q_proj.weight": "model-00001-of-00001.safetensors",
457
+ "model.language_model.layers.19.self_attn.q_proj.weight": "model-00001-of-00001.safetensors",
458
+ "model.language_model.layers.23.self_attn.q_proj.weight": "model-00001-of-00001.safetensors",
459
+ "model.language_model.layers.0.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
460
+ "model.language_model.layers.1.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
461
+ "model.language_model.layers.2.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
462
+ "model.language_model.layers.4.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
463
+ "model.language_model.layers.5.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
464
+ "model.language_model.layers.6.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
465
+ "model.language_model.layers.8.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
466
+ "model.language_model.layers.9.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
467
+ "model.language_model.layers.10.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
468
+ "model.language_model.layers.12.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
469
+ "model.language_model.layers.13.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
470
+ "model.language_model.layers.14.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
471
+ "model.language_model.layers.16.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
472
+ "model.language_model.layers.17.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
473
+ "model.language_model.layers.18.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
474
+ "model.language_model.layers.20.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
475
+ "model.language_model.layers.21.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
476
+ "model.language_model.layers.22.linear_attn.in_proj_qkv.weight": "model-00001-of-00001.safetensors",
477
+ "model.visual.merger.linear_fc1.weight": "model-00001-of-00001.safetensors",
478
+ "model.language_model.embed_tokens.weight": "model-00001-of-00001.safetensors"
479
+ }
480
+ }
weights/ocr/processor_config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor": {
3
+ "do_convert_rgb": true,
4
+ "do_normalize": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_processor_type": "Qwen2VLImageProcessor",
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "merge_size": 2,
19
+ "patch_size": 16,
20
+ "resample": 3,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": {
23
+ "longest_edge": 16777216,
24
+ "shortest_edge": 65536
25
+ },
26
+ "temporal_patch_size": 2
27
+ },
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "video_processor": {
30
+ "do_convert_rgb": true,
31
+ "do_normalize": true,
32
+ "do_rescale": true,
33
+ "do_resize": true,
34
+ "do_sample_frames": true,
35
+ "fps": 2,
36
+ "image_mean": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_std": [
42
+ 0.5,
43
+ 0.5,
44
+ 0.5
45
+ ],
46
+ "max_frames": 768,
47
+ "merge_size": 2,
48
+ "min_frames": 4,
49
+ "patch_size": 16,
50
+ "resample": 3,
51
+ "rescale_factor": 0.00392156862745098,
52
+ "return_metadata": false,
53
+ "size": {
54
+ "longest_edge": 25165824,
55
+ "shortest_edge": 4096
56
+ },
57
+ "temporal_patch_size": 2,
58
+ "video_processor_type": "Qwen3VLVideoProcessor"
59
+ }
60
+ }
weights/ocr/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa502d4d33e68344b0702e5dfd90466bc1e152d36ed906bad34c48dfd3e06d41
3
+ size 33629731
weights/ocr/tokenizer_config.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "boi_token": "<|start_of_image|>",
4
+ "bos_token": "[@BOS@]",
5
+ "clean_up_tokenization_spaces": false,
6
+ "eoi_token": "<|end_of_image|>",
7
+ "eos_token": "<|im_end|>",
8
+ "extra_special_tokens": [
9
+ "<|endoftext|>",
10
+ "<|im_start|>",
11
+ "<|im_end|>",
12
+ "<|object_ref_start|>",
13
+ "<|object_ref_end|>",
14
+ "<|box_start|>",
15
+ "<|box_end|>",
16
+ "<|quad_start|>",
17
+ "<|quad_end|>",
18
+ "<|vision_start|>",
19
+ "<|vision_end|>",
20
+ "<|image_pad|>",
21
+ "<|video_pad|>"
22
+ ],
23
+ "image_token": "<|image_pad|>",
24
+ "is_local": true,
25
+ "local_files_only": true,
26
+ "model_max_length": 1000000000000000019884624838656,
27
+ "model_specific_special_tokens": {
28
+ "boi_token": "<|start_of_image|>",
29
+ "eoi_token": "<|end_of_image|>",
30
+ "image_token": "<|image_pad|>"
31
+ },
32
+ "pad_token": "<|endoftext|>",
33
+ "padding_side": "right",
34
+ "processor_class": "Qwen3VLProcessor",
35
+ "sp_model_kwargs": null,
36
+ "spaces_between_special_tokens": false,
37
+ "tokenizer_class": "TokenizersBackend",
38
+ "unk_token": "<unk>",
39
+ "use_default_system_prompt": false
40
+ }