# Architecture Two models with a JSON file between them. ```text page image ──> IndicDocLayout ──> layout JSON ──> IndicBlockOCR ──> page JSON + markdown ``` `IndicDocLayout` finds the blocks and orders them. `IndicBlockOCR` transcribes one block crop per request. Everything between the two models (cropping, prompt selection, reassembly) is this package. ## The call path One page, top to bottom, with the file that owns each step. ```text IndicDocParser.parse(image) idp_offline.py IndicDocLayout.detect(image) idp_offline.py _open image -> PIL RGB idp_offline.py IndicDocLayoutBackend.detect idp_layout.py infer -> [y0,x0,y1,x1] at 0-1000 idp_model_infer.py convert + clamp_to_page -> pixel [x0,y0,x1,y1] idp_layout.py, blocks.py clean_layout drop duplicate boxes idp_blocks.py _densify order -> gap-free 0..n-1 idp_layout.py = PageResult, every block has text None idp_types.py IndicBlockOCR.run(image, layout) idp_offline.py _as_page dict -> PageResult, validated is_transcribed drop OCR_SKIP_LABELS idp_contract.py resolve_nested_equations drop nested equations idp_blocks.py build_requests -> [CropRequest] idp_recognizer.py crop_for + area_clamp block -> image idp_crops.py prompt_for(block.type) type -> prompt idp_contract.py backend.transcribe [CropRequest] -> [str] a recognizer backend match by order texts -> blocks idp_offline.py reconstruct blocks -> markdown idp_reconstruct.py = PageResult, every block has text ``` Blocks that were skipped are not deleted. They come back with `text: ""`. ## Files by role **Contract and data.** The vocabulary everything else shares. | file | holds | | --- | --- | | `idp_contract.py` | prompts, the label to type map, which labels are skipped | | `idp_types.py` | `Block`, `PageResult`, and every tunable config | **The layout model.** Torch lives here and nowhere else. | file | holds | | --- | --- | | `idp_model_infer.py` | preprocessing and decode for one page | | `idp_model_labels.py` | the 37 classes, pure Python | | `idp_model_ppdoc.py` | the PP-DocLayoutV3 subclass | | `idp_model_order_loss.py` | reading-order decode, training loss | **Geometry and crops.** No model, no network. | file | holds | | --- | --- | | `idp_blocks.py` | box math, `clean_layout`, nested-equation dedup | | `idp_crops.py` | `crop_for` and `area_clamp` | **Backends.** Two Protocols, five implementations, one per deployment. | class | file | used by | | --- | --- | --- | | `IndicDocLayoutBackend` | `idp_layout.py` | the real detector | | `JsonLayoutBackend` | `idp_layout.py` | replaying a layout file, no torch | | `HfRecognizer` | `idp_recognizer.py` | plain transformers, the Hub package | **Orchestration.** | file | holds | | --- | --- | | `idp_offline.py` | `IndicDocLayout`, `IndicBlockOCR`, `IndicDocParser` | | `idp_recognizer.py` | `CropRequest`, `build_requests`, the recognizer Protocol | | `idp_reconstruct.py` | blocks to markdown, math and hyphen repair | ## Invariants 1. **Two box conventions.** The model emits `[y0, x0, y1, x1]` normalised to 0-1000. The pipeline uses pixel `[x0, y0, x1, y1]`. 2. **`order` must be gap-free and 0-based.** Transcriptions are matched back to blocks by position, so a gap or a duplicate moves text onto the wrong block. 3. **The crop clamp is on area, not on a side.** Pinning a side explodes elongated crops. See `CropConfig` in `idp_types.py`. 4. **`type` selects the prompt.** A wrong `type` changes what the model was asked to do, not just how a block is labelled. Unrecognised labels are rejected for this reason.