2026-05-31 16:01:05 +00:00
from __future__ import annotations
2026-07-01 16:47:18 +00:00
import pytest
2026-05-31 16:01:05 +00:00
2026-06-12 04:13:36 +00:00
def test_assign_block_role_abstract_heading_seed_is_explicit ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = { " block_label " : " paragraph_title " , " block_content " : " Abstract " , " block_bbox " : [ 90 , 100 , 220 , 130 ] }
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " abstract_heading "
2026-06-04 15:20:49 +00:00
def test_structured_block_has_render_default_for_body ( ) - > None :
from paperforge . worker . ocr_blocks import build_structured_blocks
raw_blocks = [
{
" paper_id " : " KEY001 " ,
" page " : 1 ,
" block_id " : " p1_b1 " ,
" raw_label " : " text " ,
" raw_order " : 0 ,
" bbox " : [ 100 , 200 , 900 , 300 ] ,
" text " : " This is a body paragraph with enough text to be meaningful and avoid the short-text threshold. " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
]
2026-06-06 07:52:18 +00:00
rows , _ = build_structured_blocks ( raw_blocks )
2026-06-04 15:20:49 +00:00
assert rows [ 0 ] [ " role " ] == " body_paragraph "
assert rows [ 0 ] [ " render_default " ] is True
assert rows [ 0 ] [ " index_default " ] is True
def test_structured_block_marks_noise_as_not_renderable ( ) - > None :
from paperforge . worker . ocr_blocks import build_structured_blocks
raw_blocks = [
{
" paper_id " : " KEY001 " ,
" page " : 1 ,
" block_id " : " p1_b1 " ,
" raw_label " : " header " ,
" raw_order " : 0 ,
" bbox " : [ 1 , 2 , 100 , 20 ] ,
" text " : " Running head: SOME JOURNAL " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
]
2026-06-06 07:52:18 +00:00
rows , _ = build_structured_blocks ( raw_blocks )
2026-06-04 15:20:49 +00:00
assert rows [ 0 ] [ " role " ] in { " noise " , " page_header " }
assert rows [ 0 ] [ " render_default " ] is False
2026-05-31 16:01:05 +00:00
def test_heading_role_is_not_figure_text ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " paragraph_title " ,
" block_content " : " 5.1 Bone " ,
" block_bbox " : [ 606 , 732 , 709 , 759 ] ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1191 , page_height = 1684 )
assert role . role in { " section_heading " , " subsection_heading " }
def test_figure_caption_role_beats_body_paragraph ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Figure 4 RT-qPCR results. Temporal changes in messenger RNA... " ,
" block_bbox " : [ 373 , 1101 , 1143 , 1258 ] ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1200 , page_height = 1600 )
assert role . role == " figure_caption "
def test_paragraph_title_is_heading_prior ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " paragraph_title " ,
" block_content " : " 5 In vitro PEMF studies " ,
" block_bbox " : [ 94 , 1328 , 387 , 1356 ] ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1191 , page_height = 1684 )
assert role . role in { " section_heading " , " subsection_heading " }
assert role . confidence > = 0.7
2026-06-06 10:40:24 +00:00
def test_paragraph_title_with_trailing_dot_numbering_is_strong_heading ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " paragraph_title " ,
" block_content " : " 8. Conclusion and outlook " ,
" block_bbox " : [ 94 , 1328 , 520 , 1356 ] ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1191 , page_height = 1684 )
assert role . role == " section_heading "
assert role . confidence > = 0.7
2026-05-31 16:01:05 +00:00
def test_figure_title_is_caption_prior ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " figure_title " ,
" block_content " : " Figure 2. Representative pathology records. " ,
" block_bbox " : [ 300 , 1180 , 980 , 1250 ] ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1200 , page_height = 1600 )
2026-06-06 10:40:24 +00:00
assert role . role in { " figure_caption " , " figure_caption_candidate " }
2026-05-31 16:01:05 +00:00
assert role . confidence > = 0.8
def test_unknown_structural_for_unrecognized ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " xyz " ,
" block_bbox " : [ 100 , 100 , 200 , 120 ] ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1200 , page_height = 1600 )
assert role . role in { " body_paragraph " , " unknown_structural " }
assert role . confidence < 0.8
2026-06-05 05:31:50 +00:00
def test_stabilize_role_classifies_paper_title ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
assignment = assign_block_role (
2026-06-05 14:47:13 +00:00
block = {
" block_label " : " paragraph_title " ,
" block_content " : " Test Paper About Science " ,
" page " : 1 ,
" block_bbox " : [ 100 , 50 , 900 , 100 ] ,
} ,
2026-06-05 05:31:50 +00:00
page_blocks = [ ] ,
2026-06-05 14:47:13 +00:00
page_height = 1000 ,
2026-06-05 05:31:50 +00:00
)
assert assignment . role == " paper_title "
def test_stabilize_abstract_heading_is_not_body ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
assignment = assign_block_role (
block = { " block_label " : " paragraph_title " , " block_content " : " Abstract " } ,
page_blocks = [ ] ,
)
assert assignment . role == " abstract_heading "
def test_stabilize_frontmatter_noise_not_section_heading ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
for noise in ( " OPEN ACCESS " , " CITATION " , " COPYRIGHT " , " KEYWORDS " , " EDITED BY " , " REVIEWED BY " ) :
assignment = assign_block_role (
block = { " block_label " : " paragraph_title " , " block_content " : noise } ,
page_blocks = [ ] ,
)
assert assignment . role not in ( " section_heading " , " subsection_heading " ) , f " { noise } should not be a heading "
2026-06-05 14:47:13 +00:00
2026-06-18 17:31:45 +00:00
def test_pre_ref_headings_classified_as_section_headings ( ) - > None :
2026-06-05 14:47:13 +00:00
from paperforge . worker . ocr_roles import assign_block_role
2026-06-18 17:31:45 +00:00
for text in [ " Author contributions " , " Funding " , " Acknowledgments " , " Conflict of interest " , " Generative AI statement " ] :
2026-06-05 14:47:13 +00:00
block = {
" block_label " : " paragraph_title " ,
" block_content " : text ,
" page " : 20 ,
" block_bbox " : [ 100 , 600 , 500 , 630 ] ,
}
assignment = assign_block_role ( block , page_blocks = [ ] , page_height = 1000 )
2026-06-18 17:31:45 +00:00
assert assignment . role in { " section_heading " , " subsection_heading " , " sub_subsection_heading " } , (
f " ' { text } ' on page 20 should get a normal heading role, got { assignment . role } "
)
2026-06-05 14:47:13 +00:00
def test_references_heading_gets_reference_role ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
assignment = assign_block_role (
block = { " block_label " : " paragraph_title " , " block_content " : " References " } ,
page_blocks = [ ] ,
)
assert assignment . role == " reference_heading " , (
f " paragraph_title ' References ' should be reference_heading, got { assignment . role } "
)
assignment_text = assign_block_role (
block = { " block_label " : " text " , " block_content " : " References " } ,
page_blocks = [ ] ,
)
assert assignment_text . role == " reference_heading " , (
f " text ' References ' should be reference_heading, got { assignment_text . role } "
)
def test_body_with_noise_phrase_below_backmatter_heading ( ) - > None :
""" Text block with ' supplementary material ' under backmatter heading -> backmatter_body, not frontmatter_noise. """
from paperforge . worker . ocr_roles import assign_block_role
page_blocks = [
{
" block_label " : " paragraph_title " ,
" block_content " : " Supplementary material " ,
" block_bbox " : [ 604 , 536 , 909 , 567 ] ,
" page " : 22 ,
} ,
{
" block_label " : " text " ,
" block_content " : " The Supplementary Material for this article can be found online. " ,
" block_bbox " : [ 604 , 590 , 1096 , 662 ] ,
" page " : 22 ,
} ,
]
assignment = assign_block_role (
page_blocks [ 1 ] ,
page_blocks = page_blocks ,
page_width = 1191 ,
)
assert assignment . role == " body_paragraph " , (
f " Role layer should stay conservative and leave tail ownership to render, got { assignment . role } "
)
2026-06-09 14:10:54 +00:00
def test_pipeline_keeps_reference_zone_and_legend_family_out_of_default_body ( ) - > None :
from paperforge . worker . ocr_blocks import build_structured_blocks
raw_blocks = [
{
" paper_id " : " KEY010B " ,
" page " : 2 ,
" block_id " : " p2_b1 " ,
" raw_label " : " text " ,
" raw_order " : 0 ,
" bbox " : [ 110 , 120 , 470 , 280 ] ,
2026-06-10 04:06:16 +00:00
" text " : " Main body paragraph with stable narrative prose and enough repeated content to establish the body family anchor for the document. "
* 2 ,
2026-06-09 14:10:54 +00:00
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 9.0 , " font " : " Times " , " flags " : " " } ,
} ,
{
" paper_id " : " KEY010B " ,
" page " : 3 ,
" block_id " : " p3_b1 " ,
" raw_label " : " text " ,
" raw_order " : 0 ,
" bbox " : [ 112 , 118 , 472 , 282 ] ,
2026-06-10 04:06:16 +00:00
" text " : " Another core body paragraph repeating the same typography and width so the anchor-first body family remains dominant across the middle pages. "
* 2 ,
2026-06-09 14:10:54 +00:00
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 9.0 , " font " : " Times " , " flags " : " " } ,
} ,
{
" paper_id " : " KEY010B " ,
" page " : 5 ,
" block_id " : " p5_b1 " ,
" raw_label " : " paragraph_title " ,
" raw_order " : 0 ,
" bbox " : [ 100 , 80 , 350 , 120 ] ,
" text " : " References " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 11.0 , " font " : " Times " , " flags " : " bold " } ,
} ,
{
" paper_id " : " KEY010B " ,
" page " : 5 ,
" block_id " : " p5_b2 " ,
" raw_label " : " text " ,
" raw_order " : 1 ,
" bbox " : [ 110 , 170 , 500 , 260 ] ,
" text " : " [1] Example reference entry with journal and year details. " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 8.5 , " font " : " Times " , " flags " : " " } ,
} ,
{
" paper_id " : " KEY010B " ,
" page " : 4 ,
" block_id " : " p4_b9 " ,
" raw_label " : " text " ,
" raw_order " : 2 ,
" bbox " : [ 720 , 780 , 1030 , 860 ] ,
" text " : " Figure 1. Compact legend text for the nearby display panel. " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 8.0 , " font " : " Times " , " flags " : " " } ,
} ,
{
" paper_id " : " KEY010B " ,
" page " : 4 ,
" block_id " : " p4_b10 " ,
" raw_label " : " figure " ,
" raw_order " : 3 ,
" bbox " : [ 700 , 420 , 1040 , 760 ] ,
" text " : " " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
} ,
]
rows , _ = build_structured_blocks ( raw_blocks )
assert any ( row [ " role " ] == " reference_item " for row in rows )
2026-06-10 04:06:16 +00:00
assert not any ( row [ " role " ] == " body_paragraph " and row . get ( " style_family " ) == " legend_like " for row in rows )
2026-06-09 14:10:54 +00:00
2026-06-05 14:47:13 +00:00
def test_style_aware_unnumbered_heading_detection ( ) - > None :
""" Unnumbered headings with distinct visual style are detected as headings.
The current assign_block_role ignores span_metadata ( size , flags ) ,
relying only on block_label and text patterns . Two text blocks with
heading - like style ( different sizes , bold ) should both be detected as
headings , and a truly ordinary body block should not be promoted .
"""
from paperforge . worker . ocr_roles import assign_block_role
big_heading = {
" block_label " : " text " ,
" block_content " : " Clinical Outcomes " ,
" span_metadata " : { " size " : 16 , " flags " : " bold " } ,
" block_bbox " : [ 96 , 300 , 500 , 330 ] ,
" page " : 5 ,
}
sub_heading = {
" block_label " : " text " ,
" block_content " : " Histological Analysis " ,
" span_metadata " : { " size " : 14 , " flags " : " bold " } ,
" block_bbox " : [ 96 , 450 , 500 , 480 ] ,
" page " : 5 ,
}
body_block = {
" block_label " : " text " ,
" block_content " : " Immunohistochemical staining revealed significant differences between the treatment and control groups across all time points examined. " ,
" span_metadata " : { " size " : 10 , " flags " : " normal " } ,
" block_bbox " : [ 96 , 500 , 1090 , 620 ] ,
" page " : 5 ,
}
page_blocks = [ big_heading , sub_heading , body_block ]
big_role = assign_block_role (
big_heading ,
page_blocks = page_blocks ,
page_height = 1000 ,
)
sub_role = assign_block_role (
sub_heading ,
page_blocks = page_blocks ,
page_height = 1000 ,
)
body_role = assign_block_role (
body_block ,
page_blocks = page_blocks ,
page_height = 1000 ,
)
2026-06-06 03:21:25 +00:00
heading_roles = { " section_heading " , " subsection_heading " , " backmatter_heading " }
2026-06-10 04:06:16 +00:00
assert big_role . role in heading_roles , f " Big heading should be detected as heading, got { big_role . role } "
assert sub_role . role in heading_roles , f " Sub heading should be detected as heading, got { sub_role . role } "
2026-06-05 14:47:13 +00:00
assert body_role . role not in { " section_heading " , " backmatter_heading " , " subsection_heading " } , (
f " Body should not be promoted to heading, got { body_role . role } "
)
def test_backmatter_body_cross_column_ownership ( ) - > None :
""" Body in right column attaches to nearest right-column heading, not left-column one. """
from paperforge . worker . ocr_roles import assign_block_role
page_blocks = [
{
" block_label " : " paragraph_title " ,
" block_content " : " Conflict of interest " ,
" block_bbox " : [ 94 , 536 , 338 , 565 ] ,
" page " : 22 ,
} ,
{
" block_label " : " paragraph_title " ,
" block_content " : " Supplementary material " ,
" block_bbox " : [ 604 , 536 , 909 , 567 ] ,
" page " : 22 ,
} ,
{
" block_label " : " text " ,
" block_content " : " The Supplementary Material for this article can be found online. " ,
" block_bbox " : [ 604 , 590 , 1096 , 662 ] ,
" page " : 22 ,
} ,
]
assignment = assign_block_role (
page_blocks [ 2 ] ,
page_blocks = page_blocks ,
page_width = 1191 ,
)
assert assignment . role == " body_paragraph " , (
f " Role layer should stay conservative and leave tail ownership to render, got { assignment . role } "
)
2026-06-06 03:21:25 +00:00
def test_heading_level_from_profile_match ( ) - > None :
""" After profile aggregation, headings should be classified by
profile match , not hardcoded font size . """
pass # Placeholder — will be updated after refactor
2026-06-06 05:28:46 +00:00
def test_body_citation_not_authors ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
2026-06-06 05:28:46 +00:00
block = {
" block_label " : " text " ,
" block_content " : " In Section 5, the focus is on ES based bioelectronics $^ { 8,49}$ " ,
" block_bbox " : [ 100 , 500 , 800 , 540 ] ,
" page " : 3 ,
}
result = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert result . role == " body_paragraph " , f " Expected body_paragraph, got { result . role } "
assert result . confidence > = 0.5
def test_frontmatter_author_zone_still_works ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
2026-06-06 05:28:46 +00:00
block = {
" block_label " : " text " ,
" block_content " : " Alice Smith, Bob Jones, Charlie Brown " ,
" block_bbox " : [ 100 , 300 , 800 , 330 ] ,
" page " : 1 ,
}
result = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert result . role == " authors " , f " Expected authors, got { result . role } "
2026-06-06 10:40:24 +00:00
def test_figure_caption_candidate_for_narrative_body ( ) - > None :
""" Fig. 26c addresses ... with body-style text → figure_caption_candidate. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Fig. 26c addresses the signaling pathway in detail and demonstrates the mechanism of action for the compound. " ,
" block_bbox " : [ 100 , 500 , 900 , 550 ] ,
" page " : 3 ,
}
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " figure_caption_candidate " , (
f " Narrative body text with Fig prefix should be candidate, got { role . role } "
)
2026-06-18 17:31:45 +00:00
def test_pre_ref_heading_on_late_page_is_normal_heading ( ) - > None :
""" Funding on page 10 → normal heading role (no longer backmatter-specific). """
2026-06-06 10:40:24 +00:00
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " paragraph_title " ,
" block_content " : " Funding " ,
" block_bbox " : [ 100 , 600 , 500 , 630 ] ,
" page " : 10 ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_height = 1000 )
2026-06-18 17:31:45 +00:00
assert role . role in { " section_heading " , " subsection_heading " , " sub_subsection_heading " } , (
f " Funding on page 10 should be a normal heading, got { role . role } "
2026-06-06 10:40:24 +00:00
)
def test_figure_title_label_becomes_candidate ( ) - > None :
2026-06-10 10:18:48 +00:00
""" raw label figure_title with Fig. 2 test resolves directly to figure_caption. """
2026-06-06 10:40:24 +00:00
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " figure_title " ,
" block_content " : " Fig. 2 test " ,
" block_bbox " : [ 300 , 1100 , 700 , 1130 ] ,
" page " : 3 ,
}
role = assign_block_role ( block , page_blocks = [ ] , page_width = 1200 , page_height = 1600 )
2026-06-10 10:18:48 +00:00
assert role . role == " figure_caption " , f " figure_title label should produce figure_caption, got { role . role } "
2026-06-06 10:40:24 +00:00
def test_formal_figure_caption_still_direct ( ) - > None :
""" Figure 3. Expression of ... near media → figure_caption (unchanged). """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Figure 3. Expression of target proteins " ,
" block_bbox " : [ 200 , 1300 , 900 , 1330 ] ,
" page " : 3 ,
}
media_block = {
" block_label " : " image " ,
" block_content " : " " ,
" block_bbox " : [ 200 , 900 , 900 , 1250 ] ,
" page " : 3 ,
}
page_blocks = [ block , media_block ]
role = assign_block_role ( block , page_blocks = page_blocks , page_width = 1200 , page_height = 1600 )
2026-06-10 04:06:16 +00:00
assert role . role == " figure_caption " , f " Formal figure caption near media should be figure_caption, got { role . role } "
2026-06-06 10:40:24 +00:00
2026-06-06 16:59:22 +00:00
def test_weak_backmatter_boundary_signal_emits_candidate ( ) :
""" A paragraph with backmatter boundary text but no bold span_metadata
should emit backmatter_boundary_candidate , not backmatter_boundary_heading . """
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
2026-06-06 16:59:22 +00:00
block = {
" block_label " : " paragraph_title " ,
" block_content " : " ADDITIONAL INFORMATION " ,
" block_bbox " : [ 100 , 1200 , 500 , 1240 ] ,
" page " : 8 ,
}
page_blocks = [ block ] + [
{
" block_label " : " text " ,
" block_content " : f " Some body text { i } " ,
" block_bbox " : [ 100 , 200 + i * 100 , 500 , 260 + i * 100 ] ,
" page " : 8 ,
}
for i in range ( 5 )
]
2026-06-10 04:06:16 +00:00
result = assign_block_role ( block , page_blocks = page_blocks , page_width = 600 , page_height = 1600 )
assert result . role == " backmatter_boundary_candidate " , f " Expected backmatter_boundary_candidate, got { result . role } "
2026-06-06 16:59:22 +00:00
2026-06-07 03:45:11 +00:00
def test_author_byline_not_section_heading ( ) :
from paperforge . worker . ocr_roles import assign_block_role
# Author byline with & on page 1
2026-06-10 04:06:16 +00:00
block = {
" block_label " : " paragraph_title " ,
" block_content " : " John Smith & Jane Doe " ,
" block_bbox " : [ 100 , 50 , 500 , 80 ] ,
" page " : 1 ,
}
page_blocks = [
block ,
2026-06-07 03:45:11 +00:00
{ " block_label " : " text " , " block_content " : " Some frontmatter " , " block_bbox " : [ 100 , 100 , 500 , 130 ] , " page " : 1 } ,
2026-06-10 04:06:16 +00:00
{
" block_label " : " text " ,
" block_content " : " Some abstract content " ,
" block_bbox " : [ 100 , 200 , 500 , 230 ] ,
" page " : 1 ,
} ,
2026-06-07 03:45:11 +00:00
]
result = assign_block_role ( block , page_blocks = page_blocks , page_width = 600 , page_height = 800 )
assert result . role not in ( " section_heading " , " subsection_heading " , " sub_subsection_heading " ) , (
f " Author byline got heading role: { result . role } "
)
def test_correspondence_marker_not_heading ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
block = {
" block_label " : " paragraph_title " ,
" block_content " : " *Correspondence: john@example.com " ,
" block_bbox " : [ 100 , 500 , 500 , 530 ] ,
" page " : 1 ,
}
2026-06-07 03:45:11 +00:00
page_blocks = [ block ]
result = assign_block_role ( block , page_blocks = page_blocks , page_width = 600 , page_height = 800 )
2026-06-10 04:06:16 +00:00
assert result . role in ( " frontmatter_noise " , " unknown_structural " ) , f " Correspondence got role: { result . role } "
2026-06-07 03:45:11 +00:00
2026-06-07 04:52:02 +00:00
def test_running_header_not_heading ( ) :
""" Article-type label in top margin should be noise, not heading. """
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
2026-06-07 04:52:02 +00:00
block = {
" block_label " : " paragraph_title " ,
" block_content " : " Review Article " ,
" block_bbox " : [ 80 , 50 , 200 , 75 ] ,
" page " : 3 ,
}
page_blocks = [
block ,
{ " block_label " : " text " , " block_content " : " Some body text " , " block_bbox " : [ 80 , 200 , 500 , 240 ] , " page " : 3 } ,
]
result = assign_block_role ( block , page_blocks = page_blocks , page_width = 600 , page_height = 1700 )
assert result . role == " noise " , f " Expected noise, got { result . role } "
2026-06-07 13:35:57 +00:00
def test_page1_article_type_label_not_heading ( ) :
""" Article-type label on page 1 is frontmatter furniture, not content heading. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " paragraph_title " ,
" block_content " : " Review article " ,
" block_bbox " : [ 80 , 180 , 220 , 205 ] ,
" page " : 1 ,
}
page_blocks = [
block ,
{
" block_label " : " doc_title " ,
" block_content " : " Metabolic regulation of skeletal cell fate and function in development and disease " ,
" block_bbox " : [ 80 , 240 , 560 , 310 ] ,
" page " : 1 ,
} ,
{
" block_label " : " paragraph_title " ,
" block_content " : " Steve Stegen & Geert Carmeliet " ,
" block_bbox " : [ 80 , 340 , 500 , 365 ] ,
" page " : 1 ,
} ,
]
result = assign_block_role ( block , page_blocks = page_blocks , page_width = 600 , page_height = 1700 )
assert result . role in { " frontmatter_noise " , " noise " } , (
f " Page-1 article-type label should be frontmatter furniture, got { result . role } "
)
2026-06-07 07:58:38 +00:00
def test_doc_title_not_body_paragraph ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " doc_title " ,
" block_content " : " A Very Long Title That Would Normally Be Considered A Document Title Block With Sufficient Length " ,
" block_bbox " : [ 100 , 50 , 900 , 100 ] ,
" page " : 1 ,
}
result = assign_block_role ( block , page_blocks = [ block ] , page_width = 900 , page_height = 1200 )
2026-06-10 04:06:16 +00:00
assert result . role != " body_paragraph " , f " doc_title should not be body_paragraph, got { result . role } "
2026-06-07 07:58:38 +00:00
def test_author_byline_comma_only_not_section_heading ( ) - > None :
""" Comma-separated byline on page 1 below zone detection threshold -> NOT heading. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " paragraph_title " ,
" block_content " : " John Smith, Jane Doe " ,
" block_bbox " : [ 100 , 400 , 500 , 430 ] ,
" page " : 1 ,
}
page_blocks = [ block ]
result = assign_block_role (
block ,
page_blocks = page_blocks ,
page_width = 600 ,
page_height = 800 ,
)
assert result . role not in ( " section_heading " , " subsection_heading " , " sub_subsection_heading " ) , (
f " Comma-separated byline should not be a heading, got { result . role } "
)
2026-06-06 03:21:25 +00:00
def test_backmatter_boundary_detects_on_early_page ( ) - > None :
""" Backmatter boundary should be detectable on papers with fewer
than 8 pages , without a hard page gate . """
from paperforge . worker . ocr_roles import _is_backmatter_boundary_heading
2026-06-10 04:06:16 +00:00
2026-06-06 03:21:25 +00:00
block = {
" span_metadata " : { " size " : 12.0 , " flags " : " bold " } ,
" text " : " ADDITIONAL INFORMATION AND DECLARATIONS " ,
" page " : 5 ,
}
result = _is_backmatter_boundary_heading ( block , 5 , 10 )
2026-06-06 04:34:53 +00:00
assert result # After fix: 5/10 = 50% in second half, has container words + bold
2026-06-08 09:10:51 +00:00
def test_frontiers_figure_pipe_title_is_figure_caption ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " figure_title " ,
" text " : " FIGURE 1 | Expression of irisin is downregulated in OA cartilage. " ,
" page " : 2 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 100 , 500 , 700 , 540 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role == " figure_caption "
assert result . confidence > = 0.9
def test_frontiers_figure_pipe_text_is_figure_caption ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " text " ,
" text " : " FIGURE 2 | Treadmill exercise protocols. " ,
" page " : 2 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 100 , 500 , 700 , 540 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role == " figure_caption "
assert result . confidence > = 0.9
def test_single_letter_panel_label_not_figure_caption ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " text " ,
" text " : " A " ,
" page " : 2 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 100 , 100 , 110 , 120 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role == " figure_inner_text "
def test_parenthesized_panel_label_not_figure_caption ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " text " ,
" text " : " (B) " ,
" page " : 2 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 100 , 100 , 125 , 120 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role == " figure_inner_text "
def test_page1_roman_heading_outside_title_zone_becomes_section_heading ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " I. INTRODUCTION " ,
" page " : 1 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 261 , 967 , 457 , 988 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role == " section_heading "
assert result . confidence > = 0.8
def test_page1_alpha_heading_outside_title_zone_becomes_subsection_heading ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " A. Materials " ,
" page " : 1 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 624 , 500 , 742 , 521 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role == " subsection_heading "
assert result . confidence > = 0.8
def test_page1_top_title_not_misclassified_as_section_heading ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " I. INTRODUCTION " ,
" page " : 1 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 261 , 100 , 457 , 120 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 09:10:51 +00:00
assert result . role != " section_heading "
2026-06-08 11:15:36 +00:00
def test_preproof_marker_is_frontmatter_noise ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " Journal Pre-proof " ,
" page " : 1 ,
" page_width " : 1224 ,
" page_height " : 1584 ,
" block_bbox " : [ 190 , 206 , 475 , 246 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 11:15:36 +00:00
assert result . role == " frontmatter_noise "
assert result . confidence > = 0.9
def test_preproof_marker_variants ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
2026-06-08 11:15:36 +00:00
for text in [ " Journal Pre-proof " , " Pre-proof " , " journal pre-proof " ] :
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : text ,
" page " : 1 ,
" page_width " : 1224 ,
" page_height " : 1584 ,
" block_bbox " : [ 190 , 206 , 475 , 246 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 11:15:36 +00:00
assert result . role == " frontmatter_noise "
2026-06-08 11:38:31 +00:00
def test_preproof_running_header_is_suppressed ( ) :
""" Pre-proof text at extreme top (running header) on any page is suppressed. """
2026-06-08 11:15:36 +00:00
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " Journal Pre-proof " ,
" page " : 4 ,
" page_width " : 1224 ,
" page_height " : 1584 ,
" block_bbox " : [ 190 , 1 , 475 , 20 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 11:38:31 +00:00
assert result . role == " frontmatter_noise "
2026-06-08 11:15:36 +00:00
def test_preproof_page2_not_suppressed ( ) :
""" Pre-proof text on page 2+ should NOT be suppressed. """
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " Journal Pre-proof " ,
" page " : 2 ,
" page_width " : 1224 ,
" page_height " : 1584 ,
" block_bbox " : [ 190 , 206 , 475 , 246 ] ,
} ,
page_blocks = [ ] ,
)
2026-06-08 11:15:36 +00:00
assert result . role != " frontmatter_noise "
def test_real_title_after_preproof_still_works ( ) :
from paperforge . worker . ocr_roles import assign_block_role
2026-06-10 04:06:16 +00:00
result = assign_block_role (
{
" raw_label " : " paragraph_title " ,
" text " : " Magnetoresponsive Stem Cell Spheroid-based Cartilage Recovery Platform " ,
" page " : 1 ,
" page_width " : 1200 ,
" page_height " : 1700 ,
" block_bbox " : [ 100 , 200 , 700 , 230 ] ,
} ,
page_blocks = [ ] ,
page_height = 1700 ,
)
2026-06-08 11:15:36 +00:00
assert result . role == " paper_title "
2026-06-09 11:12:37 +00:00
def test_resolve_final_role_uses_zone_and_family_context_instead_of_default_body ( ) :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b2 " ,
" text " : " Figure 2. A long legend that sits inside the body page. " ,
" role " : " body_paragraph " ,
" zone " : " body_zone " ,
" style_family " : " legend_like " ,
" style_family_authority " : " figure_marker " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
assert resolved . role == " figure_caption_candidate "
def test_resolve_final_role_uses_families_fallback_when_block_lacks_late_context ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b2 " ,
" text " : " Figure 2. A long legend that sits inside the body page. " ,
" role " : " body_paragraph " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = {
" p4_b2 " : {
" zone " : " body_zone " ,
" style_family " : " legend_like " ,
" style_family_authority " : " figure_marker " ,
}
} ,
)
assert resolved . role == " figure_caption_candidate "
assert any ( " context_source=families " == evidence for evidence in resolved . evidence )
def test_resolve_final_role_does_not_promote_without_late_context_gate ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b2 " ,
" text " : " Figure 2. A long legend that sits inside the body page. " ,
" role " : " body_paragraph " ,
" zone " : " display_zone " ,
" style_family " : " legend_like " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
" role_confidence " : 0.6 ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " HOLD " } } ,
families = { } ,
)
assert resolved . role == " body_paragraph "
def test_resolve_final_role_requires_strong_style_family_authority ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b2 " ,
" text " : " Figure 2. A long legend that sits inside the body page. " ,
" role " : " body_paragraph " ,
" zone " : " body_zone " ,
" style_family " : " legend_like " ,
" style_family_authority " : " body_zone_candidate " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
assert resolved . role == " body_paragraph "
def test_resolve_final_role_does_not_promote_narrative_prose_even_with_strong_authority ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b9 " ,
" text " : " Figure 2 shows the control response over time. In this study we compare both cohorts. " ,
" role " : " body_paragraph " ,
" zone " : " body_zone " ,
" style_family " : " legend_like " ,
" style_family_authority " : " figure_marker " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
" role_confidence " : 0.6 ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
assert resolved . role == " body_paragraph "
def test_resolve_final_role_does_not_promote_single_sentence_narrative_prose_with_strong_authority ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b10 " ,
" text " : " Figure 2 shows the control response over time in both cohorts. " ,
" role " : " body_paragraph " ,
" zone " : " body_zone " ,
" style_family " : " legend_like " ,
" style_family_authority " : " figure_marker " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
" role_confidence " : 0.6 ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
assert resolved . role == " body_paragraph "
def test_resolve_final_role_leaves_table_handling_unchanged_in_task_7 ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p5_b3 " ,
" text " : " Table 3. Outcomes across cohorts. " ,
" role " : " body_paragraph " ,
" zone " : " body_zone " ,
" style_family " : " table_caption_like " ,
" marker_signature " : { " type " : " table_number " , " number " : 3 } ,
" role_confidence " : 0.6 ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
assert resolved . role == " body_paragraph "
def test_build_structured_blocks_applies_late_role_resolution ( monkeypatch ) - > None :
from paperforge . worker import ocr_blocks
raw_blocks = [
{
" paper_id " : " KEY001 " ,
" page " : 4 ,
" block_id " : " p4_b2 " ,
" raw_label " : " text " ,
" raw_order " : 1 ,
" bbox " : [ 100 , 500 , 950 , 620 ] ,
" text " : " A long legend that sits inside the body page without an eager prefix cue. " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 10 , " flags " : " normal " } ,
}
]
def fake_build_block_signatures ( block : dict ) - > dict :
return {
" raw_observation " : { " source " : " test " } ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
" layout_signature " : { " width " : 850 , " width_bucket " : 850 , " x_center " : 525 , " x_center_bucket " : 525 } ,
" span_signature " : { " font_family_norm " : " times " , " font_size_median " : 10 , " font_size_bucket " : 10 } ,
}
def fake_normalize_document_structure ( rows : list [ dict ] ) :
for row in rows :
row [ " zone " ] = " body_zone "
row [ " style_family " ] = " legend_like "
row [ " style_family_authority " ] = " figure_marker "
return type (
" DocStructureStub " ,
( ) ,
{
" body_family_anchor " : { " status " : " ACCEPT " } ,
" reference_family_anchor " : None ,
" body_end_page " : 4 ,
" page_layouts " : { } ,
} ,
) ( ) , rows
monkeypatch . setattr ( ocr_blocks , " build_block_signatures " , fake_build_block_signatures )
monkeypatch . setattr (
ocr_blocks ,
" discover_body_family_anchor " ,
lambda rows , page_count = None : { " status " : " ACCEPT " } ,
)
2026-06-10 04:06:16 +00:00
monkeypatch . setattr (
" paperforge.worker.ocr_document.normalize_document_structure " , fake_normalize_document_structure
)
2026-06-09 11:12:37 +00:00
monkeypatch . setattr (
" paperforge.worker.ocr_document._resolve_ambiguous_candidates " ,
lambda blocks , doc_structure , page_layouts : None ,
)
rows , _ = ocr_blocks . build_structured_blocks ( raw_blocks )
2026-06-10 04:45:40 +00:00
# seed_role is set eagerly from assign_block_role; final role stays unassigned
# because resolve_final_role is deferred to normalize_document_structure.
assert rows [ 0 ] [ " seed_role " ] == " body_paragraph "
assert rows [ 0 ] [ " role " ] == " unassigned "
2026-06-09 11:12:37 +00:00
assert rows [ 0 ] [ " marker_signature " ] [ " type " ] == " figure_number "
def test_build_structured_blocks_does_not_promote_when_anchor_gate_is_closed ( monkeypatch ) - > None :
from paperforge . worker import ocr_blocks
raw_blocks = [
{
" paper_id " : " KEY001 " ,
" page " : 4 ,
" block_id " : " p4_b2 " ,
" raw_label " : " text " ,
" raw_order " : 1 ,
" bbox " : [ 100 , 500 , 950 , 620 ] ,
" text " : " A long legend that sits inside the body page without an eager prefix cue. " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
" span_metadata " : { " size " : 10 , " flags " : " normal " } ,
}
]
def fake_build_block_signatures ( block : dict ) - > dict :
return {
" raw_observation " : { " source " : " test " } ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
" layout_signature " : { " width " : 850 , " width_bucket " : 850 , " x_center " : 525 , " x_center_bucket " : 525 } ,
" span_signature " : { " font_family_norm " : " times " , " font_size_median " : 10 , " font_size_bucket " : 10 } ,
}
def fake_normalize_document_structure ( rows : list [ dict ] ) :
for row in rows :
row [ " zone " ] = " body_zone "
row [ " style_family " ] = " legend_like "
row [ " style_family_authority " ] = " figure_marker "
return type (
" DocStructureStub " ,
( ) ,
{
" body_family_anchor " : { " status " : " HOLD " } ,
" reference_family_anchor " : None ,
" body_end_page " : 4 ,
" page_layouts " : { } ,
} ,
) ( ) , rows
monkeypatch . setattr ( ocr_blocks , " build_block_signatures " , fake_build_block_signatures )
monkeypatch . setattr (
ocr_blocks ,
" discover_body_family_anchor " ,
lambda rows , page_count = None : { " status " : " HOLD " } ,
)
2026-06-10 04:06:16 +00:00
monkeypatch . setattr (
" paperforge.worker.ocr_document.normalize_document_structure " , fake_normalize_document_structure
)
2026-06-09 11:12:37 +00:00
monkeypatch . setattr (
" paperforge.worker.ocr_document._resolve_ambiguous_candidates " ,
lambda blocks , doc_structure , page_layouts : None ,
)
rows , _ = ocr_blocks . build_structured_blocks ( raw_blocks )
2026-06-10 04:45:40 +00:00
# seed_role is set eagerly; final role stays unassigned (resolution deferred)
assert rows [ 0 ] [ " seed_role " ] == " body_paragraph "
assert rows [ 0 ] [ " role " ] == " unassigned "
2026-06-09 11:12:37 +00:00
2026-06-10 04:06:16 +00:00
def test_resolve_final_role_treats_unassigned_as_seed_role_fallback ( ) - > None :
""" When role is ' unassigned ' , resolve_final_role should use seed_role as the
starting point for resolution , not default to ' body_paragraph ' . """
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p1_b1 " ,
" text " : " Introduction " ,
" role " : " unassigned " ,
" seed_role " : " section_heading " ,
" seed_confidence " : 0.85 ,
" marker_signature " : { " type " : " none " } ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
# Should resolve to section_heading (from seed), not body_paragraph
assert resolved . role == " section_heading " , f " Expected section_heading from seed_role fallback, got { resolved . role } "
2026-06-09 11:12:37 +00:00
def test_resolve_final_role_does_not_promote_narrative_figure_mention_without_strong_authority ( ) - > None :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" block_id " : " p4_b9 " ,
" text " : " Figure 2 shows the control response over time in both cohorts. " ,
" role " : " body_paragraph " ,
" zone " : " body_zone " ,
" style_family " : " legend_like " ,
" style_family_authority " : " body_zone_candidate " ,
" marker_signature " : { " type " : " figure_number " , " number " : 2 } ,
" role_confidence " : 0.6 ,
}
resolved = resolve_final_role (
block ,
anchors = { " body_family_anchor " : { " status " : " ACCEPT " } } ,
families = { } ,
)
assert resolved . role == " body_paragraph "
2026-06-10 04:06:16 +00:00
def test_build_structured_blocks_preserves_seed_role_and_leaves_final_role_unassigned_initially ( monkeypatch ) :
""" After assign_block_role, row must carry seed_role/seed_confidence/seed_evidence
and final role must start as ' unassigned ' before late resolution . """
from paperforge . worker import ocr_blocks
raw_blocks = [
{
" paper_id " : " KEY001 " ,
" page " : 1 ,
" block_id " : " p1_b1 " ,
" raw_label " : " paragraph_title " ,
" raw_order " : 0 ,
" bbox " : [ 100 , 50 , 500 , 80 ] ,
" text " : " Introduction " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
} ,
{
" paper_id " : " KEY001 " ,
" page " : 1 ,
" block_id " : " p1_b2 " ,
" raw_label " : " text " ,
" raw_order " : 1 ,
" bbox " : [ 100 , 100 , 900 , 300 ] ,
" text " : " This is a body paragraph with enough text to be meaningful and avoid the short-text threshold. " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
} ,
]
def fake_build_block_signatures ( block : dict ) - > dict :
return {
" raw_observation " : { " source " : " test " } ,
" marker_signature " : { " type " : " none " } ,
" layout_signature " : { " width " : 800 , " width_bucket " : 800 , " x_center " : 550 , " x_center_bucket " : 550 } ,
" span_signature " : { " font_family_norm " : " times " , " font_size_median " : 10 , " font_size_bucket " : 10 } ,
}
monkeypatch . setattr ( ocr_blocks , " build_block_signatures " , fake_build_block_signatures )
rows , _ = ocr_blocks . build_structured_blocks ( raw_blocks )
for row in rows :
# seed_role must always be present after build_structured_blocks
assert " seed_role " in row , f " block { row [ ' block_id ' ] } missing seed_role "
assert " seed_confidence " in row , f " block { row [ ' block_id ' ] } missing seed_confidence "
# seed_evidence is a list (may be empty)
assert isinstance ( row . get ( " seed_evidence " , [ ] ) , list ) , f " block { row [ ' block_id ' ] } seed_evidence should be list "
# final role should not be 'unassigned' at the end (late resolution happened)
assert row [ " role " ] != " unassigned " , f " block { row [ ' block_id ' ] } final role should be resolved, not ' unassigned ' "
def test_pipeline_does_not_commit_final_semantic_role_before_zone_and_family_partition ( monkeypatch ) :
""" Zone and family partition must run AFTER role is still ' unassigned ' ,
so downstream logic can use seed_role as the authority for zone decisions . """
from paperforge . worker import ocr_blocks
raw_blocks = [
{
" paper_id " : " KEY001 " ,
" page " : 1 ,
" block_id " : " p1_b1 " ,
" raw_label " : " paragraph_title " ,
" raw_order " : 0 ,
" bbox " : [ 100 , 50 , 500 , 80 ] ,
" text " : " Abstract " ,
" page_width " : 1200 ,
" page_height " : 1600 ,
} ,
]
def fake_build_block_signatures ( block : dict ) - > dict :
return {
" raw_observation " : { " source " : " test " } ,
" marker_signature " : { " type " : " none " } ,
" layout_signature " : { " width " : 400 , " width_bucket " : 400 , " x_center " : 300 , " x_center_bucket " : 300 } ,
" span_signature " : { " font_family_norm " : " times " , " font_size_median " : 10 , " font_size_bucket " : 10 } ,
}
# Capture the state of rows at the point just after assign_block_role
# but before resolve_final_role. We verify that seed_role is set
# and role is 'unassigned' at that stage.
captured_rows_after_assign = [ ]
original_assign = ocr_blocks . assign_block_role
def capturing_assign ( block , page_blocks , page_width = 0 , page_height = 0 , style_profiles = None , role_profiles = None ) :
result = original_assign ( block , page_blocks , page_width , page_height , style_profiles , role_profiles )
# Simulate what build_structured_blocks does after assign_block_role:
# It should set seed_role and leave role as 'unassigned'
captured_rows_after_assign . append (
{
" block_id " : block . get ( " block_id " ) or block . get ( " block_content " , " " ) [ : 20 ] ,
" assigned_role " : result . role ,
}
)
return result
monkeypatch . setattr ( ocr_blocks , " build_block_signatures " , fake_build_block_signatures )
monkeypatch . setattr ( ocr_blocks , " assign_block_role " , capturing_assign )
rows , _ = ocr_blocks . build_structured_blocks ( raw_blocks )
# Verify that every block went through assign_block_role
assert len ( captured_rows_after_assign ) == len ( raw_blocks )
# Verify that the final output has seed_role set
for row in rows :
assert " seed_role " in row , f " block { row [ ' block_id ' ] } missing seed_role "
assert row [ " seed_role " ] != " unassigned " , f " seed_role should never be ' unassigned ' , got { row [ ' seed_role ' ] } "
2026-06-10 04:56:14 +00:00
def test_body_paragraph_overridden_to_reference_item_when_style_family_is_reference_like ( ) :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" role " : " body_paragraph " ,
" role_confidence " : 0.6 ,
" style_family " : " reference_like " ,
" style_family_authority " : " reference_marker " ,
" zone " : " body_zone " ,
" marker_signature " : { " type " : " reference_pattern " } ,
}
result = resolve_final_role ( block )
assert result . role == " reference_item "
assert result . confidence > = 0.78
def test_body_paragraph_overridden_to_figure_caption_when_style_family_is_legend_like ( ) :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" role " : " body_paragraph " ,
" role_confidence " : 0.6 ,
" style_family " : " legend_like " ,
" style_family_authority " : " figure_marker " ,
2026-06-28 15:19:47 +00:00
" zone " : " display_zone " ,
2026-06-10 04:56:14 +00:00
" marker_signature " : { " type " : " figure_number " } ,
" text " : " Figure 3. " ,
}
result = resolve_final_role ( block )
assert result . role == " figure_caption_candidate "
assert result . confidence > = 0.78
def test_body_paragraph_overridden_to_table_caption_when_style_family_is_table_caption_like ( ) :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" role " : " body_paragraph " ,
" role_confidence " : 0.6 ,
" style_family " : " table_caption_like " ,
" style_family_authority " : " table_marker " ,
" zone " : " body_zone " ,
" marker_signature " : { " type " : " table_number " } ,
" text " : " Table 2. " ,
}
result = resolve_final_role ( block )
assert result . role == " table_caption_candidate "
assert result . confidence > = 0.78
def test_body_paragraph_survives_when_style_family_authority_is_weak ( ) :
from paperforge . worker . ocr_roles import resolve_final_role
block = {
" role " : " body_paragraph " ,
" role_confidence " : 0.6 ,
" style_family " : " reference_like " ,
" style_family_authority " : " fallback " ,
" zone " : " body_zone " ,
" marker_signature " : { " type " : " none " } ,
}
result = resolve_final_role ( block )
assert result . role == " body_paragraph "
2026-06-14 03:51:10 +00:00
# --- Gap surface lock tests (expected to FAIL until Tasks 2-9 fix production code) ---
def test_page1_review_article_label_stays_frontmatter_noise ( ) - > None :
""" CAQ page-1 ' REVIEW ' as paragraph_title should stay frontmatter_noise. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" page " : 1 ,
" raw_label " : " paragraph_title " ,
" text " : " REVIEW " ,
" bbox " : [ 120 , 120 , 420 , 180 ] ,
" page_width " : 1200 ,
" page_height " : 1700 ,
}
role = assign_block_role ( block , page_blocks = [ block ] )
assert role . role == " frontmatter_noise "
def test_page1_author_byline_with_initial_lastname_order_is_authors ( ) - > None :
""" Author byline ' J C Buckland-Wright ' on page 1 should route to authors. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" page " : 1 ,
" raw_label " : " text " ,
" text " : " J C Buckland-Wright " ,
" bbox " : [ 120 , 300 , 640 , 350 ] ,
" page_width " : 1200 ,
" page_height " : 1700 ,
}
role = assign_block_role ( block , page_blocks = [ block ] )
assert role . role == " authors "
def test_page1_correspondence_footnote_routes_to_frontmatter_support ( ) - > None :
""" Correspondence footnote on page 1 should be frontmatter_support, not frontmatter_noise. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" page " : 1 ,
" raw_label " : " footnote " ,
" text " : " Correspondence to: Dr J C Buckland-Wright " ,
" bbox " : [ 80 , 1320 , 640 , 1420 ] ,
" page_width " : 1200 ,
" page_height " : 1700 ,
}
role = assign_block_role ( block , page_blocks = [ block ] )
assert role . role == " frontmatter_support "
2026-06-14 03:58:52 +00:00
def test_highlights_label_does_not_swallow_following_body_paragraphs ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
blocks = [
{ " block_id " : " h " , " page " : 2 , " raw_label " : " paragraph_title " , " text " : " Highlights " } ,
{ " block_id " : " b1 " , " page " : 2 , " raw_label " : " text " , " text " : " Important point one. " } ,
{ " block_id " : " b2 " , " page " : 2 , " raw_label " : " text " , " text " : " Important point two. " } ,
]
roles = [ assign_block_role ( b , page_blocks = blocks ) for b in blocks ]
# b1/b2 should still be body_paragraph, not reclassified as headings
assert roles [ 0 ] . role == " structured_insert_candidate "
assert roles [ 1 ] . role == " body_paragraph "
assert roles [ 2 ] . role == " body_paragraph "
2026-06-17 15:55:16 +00:00
def test_page1_explicit_correspondence_line_is_frontmatter_support ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" page " : 1 ,
" block_label " : " text " ,
" block_content " : " Correspondence: Prof. Smith, Department of Orthopedics, email@example.org " ,
" block_bbox " : [ 100 , 1380 , 980 , 1450 ] ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
result = assign_block_role ( block , [ block ] , page_width = 1200 , page_height = 1600 )
assert result . role == " frontmatter_support "
2026-06-18 10:01:51 +00:00
def test_first_surviving_page_equal_contribution_text_routes_to_frontmatter_support ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
title = {
" page " : 2 ,
" block_label " : " text " ,
" raw_label " : " text " ,
" block_content " : " Real Article Title " ,
" text " : " Real Article Title " ,
" block_bbox " : [ 80 , 140 , 900 , 190 ] ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
support = {
" page " : 2 ,
" block_label " : " text " ,
" raw_label " : " text " ,
" block_content " : " These authors contributed equally to this work. " ,
" text " : " These authors contributed equally to this work. " ,
" block_bbox " : [ 90 , 760 , 560 , 805 ] ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
result = assign_block_role ( support , page_blocks = [ title , support ] , page_width = 1200 , page_height = 1600 )
assert result . role == " frontmatter_support "
def test_first_surviving_page_correspondence_text_routes_to_frontmatter_support ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
title = {
" page " : 2 ,
" block_label " : " text " ,
" raw_label " : " text " ,
" block_content " : " Real Article Title " ,
" text " : " Real Article Title " ,
" block_bbox " : [ 80 , 140 , 900 , 190 ] ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
support = {
" page " : 2 ,
" block_label " : " text " ,
" raw_label " : " text " ,
" block_content " : " Corresponding author: person@example.edu " ,
" text " : " Corresponding author: person@example.edu " ,
" block_bbox " : [ 90 , 820 , 980 , 900 ] ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
result = assign_block_role ( support , page_blocks = [ title , support ] , page_width = 1200 , page_height = 1600 )
assert result . role == " frontmatter_support "
2026-07-01 16:47:18 +00:00
@pytest.mark.parametrize ( " raw_label,text,expected " , [
( " figure_title " , " Table I. Electrospun polymeric composites... " , " table_caption " ) ,
( " figure_title " , " Table II. Mechanical properties... " , " table_caption " ) ,
( " figure_title " , " Table 1. Baseline Characteristics... " , " table_caption " ) ,
( " figure_title " , " Table S1. Supplementary characteristics... " , " table_caption " ) ,
( " figure_title " , " Figure 1. Flow diagram... " , " figure_caption " ) ,
( " figure_title " , " Fig. 2. Results... " , " figure_caption " ) ,
] )
def test_figure_title_table_prefix_routes_to_table_caption ( raw_label , text , expected ) :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" raw_label " : raw_label ,
" text " : text ,
" bbox " : [ 100 , 100 , 800 , 130 ] ,
}
result = assign_block_role ( block , [ ] , 1000 , 1000 )
assert result . role == expected
2026-07-01 16:48:43 +00:00
def test_vision_footnote_this_figure_routes_to_figure_caption_candidate ( ) :
from paperforge . worker . ocr_roles import assign_block_role
block = { " raw_label " : " vision_footnote " ,
" text " : " This figure demonstrates the difference... " ,
" bbox " : [ 100 , 500 , 800 , 560 ] , " page " : 8 }
result = assign_block_role ( block , [ ] , 1000 , 1000 )
assert result . role == " figure_caption_candidate "
def test_vision_footnote_this_figure_near_media_routes_to_caption ( ) :
from paperforge . worker . ocr_roles import assign_block_role
block = { " raw_label " : " vision_footnote " ,
" text " : " This figure demonstrates... " ,
" bbox " : [ 100 , 500 , 800 , 560 ] ,
" block_bbox " : [ 100 , 500 , 800 , 560 ] , " page " : 8 }
media = { " raw_label " : " image " , " block_label " : " image " ,
" bbox " : [ 100 , 300 , 800 , 480 ] ,
" block_bbox " : [ 100 , 300 , 800 , 480 ] , " page " : 8 }
result = assign_block_role ( block , [ media ] , 1000 , 1000 )
assert result . role == " figure_caption "
def test_ordinary_vision_footnote_stays_footnote ( ) :
from paperforge . worker . ocr_roles import assign_block_role
block = { " raw_label " : " vision_footnote " ,
" text " : " Abbreviations: ICU, intensive care unit. " ,
" bbox " : [ 100 , 900 , 800 , 940 ] , " page " : 5 }
result = assign_block_role ( block , [ ] , 1000 , 1000 )
assert result . role == " footnote "
2026-07-02 17:46:09 +00:00
_PAREN_REF_TEXT = " (1) Li, T.; Shi, C.; Jin, F.; Yang, F.; Gu, L.; Wang, T.; Dong, W.; Feng, Z.-Q. Cell Activity Modulation and Its Specific Function in Load Bearing. "
_SHORT_PAREN_HEADING = " (1) Introduction "
def test_reference_pattern_accepts_long_paren_reference ( ) - > None :
from paperforge . worker . ocr_roles import _looks_like_reference
assert _looks_like_reference ( _PAREN_REF_TEXT ) is True
def test_reference_pattern_matches_short_paren_heading_pattern_only ( ) - > None :
from paperforge . worker . ocr_roles import _looks_like_reference
assert _looks_like_reference ( _SHORT_PAREN_HEADING ) is True
def test_assign_block_role_rejects_short_paren_heading ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" raw_label " : " text " ,
" text " : " (1) Introduction " ,
" page " : 3 ,
" bbox " : [ 100 , 100 , 300 , 130 ] ,
" page_width " : 1200 ,
" page_height " : 1600 ,
}
role = assign_block_role ( block , page_blocks = [ block ] ) . role
assert role != " reference_item " , f " Expected not reference_item, got { role } "
@pytest.mark.parametrize ( " text " , [
" References " , " Reference " , " REFERENCES " ,
" References and Notes " , " References and Notes ..... 42 " ,
" Bibliography " , " Cited References " ,
] )
def test_reference_heading_variants ( text : str ) - > None :
from paperforge . worker . ocr_roles import _is_reference_heading_text
assert _is_reference_heading_text ( text ) , f " Expected match: { text !r} "
@pytest.mark.parametrize ( " text " , [
" References in this article " , " Reference values " ,
" Further reading " , " References and results " ,
] )
def test_reference_heading_variants_rejects_non_headings ( text : str ) - > None :
from paperforge . worker . ocr_roles import _is_reference_heading_text
assert not _is_reference_heading_text ( text ) , f " Expected no match: { text !r} "
2026-07-04 13:55:21 +00:00
# --- PR2: Caption heuristic inline figure mention escape ---
def test_fig_shows_body_mention_not_formal_caption ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Fig. 9 shows the Mössbauer spectrum of a dehydrated sample. " ,
" block_bbox " : [ 100 , 200 , 500 , 300 ] ,
" zone " : " body_zone " ,
}
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " body_paragraph "
def test_figure_period_caption_remains_caption ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
caption_block = {
" block_label " : " text " ,
" block_content " : " Figure 1. Histological analysis of tissue sections. " ,
" block_bbox " : [ 100 , 600 , 500 , 700 ] ,
}
figure_block = {
" block_label " : " image " ,
" block_bbox " : [ 100 , 100 , 500 , 500 ] ,
}
role = assign_block_role ( caption_block , page_blocks = [ caption_block , figure_block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " figure_caption "
def test_inline_figure_mention_detects_shows ( ) - > None :
from paperforge . worker . ocr_roles import _looks_like_inline_figure_mention
assert _looks_like_inline_figure_mention ( " Fig. 9 shows the results. " )
assert not _looks_like_inline_figure_mention ( " Figure 1. Histological analysis. " )
def test_caption_syntax_detects_delimiter ( ) - > None :
from paperforge . worker . ocr_roles import _looks_like_caption_syntax
assert _looks_like_caption_syntax ( " Figure 1. Histological analysis. " )
assert _looks_like_caption_syntax ( " Fig 1: Results " )
assert not _looks_like_caption_syntax ( " Fig. 9 shows the results. " )
def test_near_media_does_not_rescue_inline_mention ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
caption_block = {
" block_label " : " text " ,
" block_content " : " Fig. 9 shows the Mössbauer spectrum. " ,
" block_bbox " : [ 100 , 600 , 500 , 700 ] ,
" zone " : " body_zone " ,
}
figure_block = {
" block_label " : " image " ,
" block_bbox " : [ 100 , 100 , 500 , 500 ] ,
}
role = assign_block_role ( caption_block , page_blocks = [ caption_block , figure_block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " body_paragraph "
2026-07-04 14:28:23 +00:00
def test_inline_mention_empty_zone_is_body ( ) - > None :
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Fig. 10 shows the data analysis results. " ,
" block_bbox " : [ 100 , 200 , 500 , 300 ] ,
" zone " : " " ,
}
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " body_paragraph "
2026-07-05 14:36:17 +00:00
def test_assign_block_role_figure_range_body_reference_is_body ( ) - > None :
""" Hyphenated ' Figure 11-10 shows... ' with raw_label=text gets body_paragraph. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Figure 11-10 shows the molecular structure of PVDF under mechanical stress. " ,
" block_bbox " : [ 100 , 200 , 800 , 230 ] ,
" page " : 1 ,
" raw_order " : 0 ,
}
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " body_paragraph " , f " Expected body_paragraph, got { role . role } "
def test_assign_block_role_figure_list_body_reference_is_body ( ) - > None :
""" ' Figures 2 and 3 show... ' with raw_label=text gets body_paragraph. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " text " ,
" block_content " : " Figures 2 and 3 show the comparison of migration rates under electrical stimulation. " ,
" block_bbox " : [ 100 , 200 , 800 , 230 ] ,
" page " : 1 ,
" raw_order " : 0 ,
}
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
assert role . role == " body_paragraph " , f " Expected body_paragraph, got { role . role } "
def test_assign_block_role_formal_caption_not_confused_with_body_ref ( ) - > None :
""" Short formal ' Figure 2. Caption... ' with figure_title label keeps figure_caption. """
from paperforge . worker . ocr_roles import assign_block_role
block = {
" block_label " : " figure_title " ,
" block_content " : " Figure 2. Cell migration under electrical stimulation. " ,
" block_bbox " : [ 100 , 100 , 700 , 130 ] ,
" page " : 1 ,
" raw_order " : 0 ,
}
role = assign_block_role ( block , page_blocks = [ block ] , page_width = 1200 , page_height = 1600 )
# figure_title label should trigger explicit mapping, not body_paragraph
assert role . role != " body_paragraph " , f " Expected not body_paragraph, got { role . role } "