Document Block Processing Pipeline#
Overview#
The block processing pipeline converts raw layout model detections into structured, paragraph-level blocks stored in middle.json. It runs in two sequential stages per document:
- Per-page: Raw layout detections →
preproc_blocks(viaMagicModel) - Document-level:
preproc_blocks→para_blocks(viapara_split+ finalizers)
The handoff point is finalize_middle_json, which orchestrates the full document-level post-processing sequence after all pages have been processed.
Stage 1: Raw Block Construction (preproc_blocks)#
Entry point: page_model_info_to_page_info() constructs a MagicModel instance for each page.
MagicModel.__init__ runs four steps in sequence :
__fix_axis()— Rescales all bounding boxes from model coordinates back to PDF coordinates and drops degenerate boxes (width or height ≤ 2 pixels) .__post_process()— Re-indexes layout detections and separatesinline_formulaandocr_textentries into dedicated span lists .__build_page_blocks()— Maps PP-DocLayoutV2 label strings toBlockTypeenums , attaches text/formula spans to text-type blocks viaSpanBlockMatcher, and converts visual-type blocks (image, table, chart, interline equation) into single-span line structures .__classify_visual_blocks()— Groups orphaned captions and footnotes with their nearest visual parent (image/table/chart/code), producing two-layer composite blocks. Captions/footnotes that can't be matched are re-typed asBlockType.TEXT.
__build_return_blocks() then splits page_blocks into two lists :
preproc_blocks— all content blocks passed downstreamdiscarded_blocks— headers, footers, page numbers, aside text, page footnotes
The caller retrieves these via magic_model.get_preproc_blocks() and magic_model.get_discarded_blocks() .
Stage 2: Paragraph Merging (para_blocks)#
Entry point: para_split(page_info_list) — called as part of finalize_middle_json_from_preproc().
para_split operates across all pages at once and writes results back into each page's para_blocks key .
Block Grouping#
__process_blocks() first partitions the flat block list into groups, breaking at any ABSTRACT, INTERLINE_EQUATION, DOC_TITLE, or PARAGRAPH_TITLE block — these act as natural paragraph boundaries that prevent merging across section titles.
List vs. Text Classification#
__is_list_or_index_block() re-classifies each TEXT block within a group as LIST, INDEX, or TEXT using geometric and textual heuristics :
- Index blocks: ≥80% of lines start or end with a digit
- Centered list blocks: most lines don't touch either edge and are centered
- List blocks: ≥2 lines flush left with ragged right edges, end-punctuation patterns, or numeric-start patterns
- Language is detected on the block text to adjust right-edge thresholds for CJK vs. Latin scripts
Merge Heuristics#
Within each group, blocks are iterated in reverse order and conditionally merged into the next block :
| Block pair | Merge function | Key conditions |
|---|---|---|
TEXT + TEXT | __merge_2_text_blocks() | Last line of prev block fills its width (not ragged right); last span doesn't end with sentence-terminal punctuation (.!?。!?:;…); next block's first character is not a digit or uppercase letter; neither block is more than 2× the other's width; at least one block has >1 line |
VERTICAL_TEXT + VERTICAL_TEXT | __merge_2_vertical_text_blocks() | Analogous height-based rules for top-to-bottom CJK text |
LIST + LIST | __merge_2_list_blocks() | Always merges (no geometric guard) |
INDEX + INDEX | __merge_2_list_blocks() | Always merges |
Cross-page merges are tagged with SplitFlag.CROSS_PAGE on affected spans .
Blocks whose lines were merged away have SplitFlag.LINES_DELETED = True set and are excluded from the final para_blocks output.
Post-para_split Finalizers#
finalize_middle_json_from_preproc() runs these steps in order :
optimize_formula_number_blocks()— resolves formula number placementpara_split()— the main paragraph merge stepcross_page_table_merge()— stitches tables split across pagesapply_title_leveling_to_pdf_info()— assigns heading hierarchy levels_post_block_process()— renamesDOC_TITLE→TITLE(level 1),PARAGRAPH_TITLE→TITLE(level 2),VERTICAL_TEXT→TEXT
Key Data Structures#
| Field | Where set | What it contains |
|---|---|---|
preproc_blocks | MagicModel.__build_return_blocks() | Raw content blocks per page; lines contain spans |
discarded_blocks | MagicModel.__build_return_blocks() | Headers, footers, page numbers, aside text |
para_blocks | para_split() | Merged paragraph blocks; subset/superset of preproc_blocks after merge |
SplitFlag.CROSS_PAGE | __merge_2_*_blocks() | Span-level flag marking cross-page merges |
SplitFlag.LINES_DELETED | __merge_2_*_blocks() | Block-level tombstone for blocks absorbed into a neighbor |
Key Source Files#
| File | Role |
|---|---|
pipeline_magic_model.py | Per-page block construction and classification |
para_split.py | Document-level paragraph merge logic |
model_json_to_middle_json.py | Orchestration: per-page conversion + finalize_middle_json |