From 6bdaddb366fa400f194b4cc5e21f084a2bd9e8bc Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 7 Jun 2026 11:45:11 +0800 Subject: [PATCH] fix: prevent author bylines from becoming section headings --- paperforge/worker/ocr_roles.py | 21 +++++++++++++++++++++ tests/test_ocr_roles.py | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/paperforge/worker/ocr_roles.py b/paperforge/worker/ocr_roles.py index bbcf58e6..069cf24c 100644 --- a/paperforge/worker/ocr_roles.py +++ b/paperforge/worker/ocr_roles.py @@ -482,6 +482,27 @@ def assign_block_role( f"backmatter {'boundary heading' if role_name == 'backmatter_boundary_heading' else 'boundary candidate'}: {text[:60]}" ], ) + # Author-like heading guard: prevent bylines from becoming headings + _is_author_byline = ( + re.search(r"&|,.*,", text) + and len(text.split()) <= 15 + and not _has_heading_numbering(text) + and not any(text.lower().startswith(w) for w in ["abstract", "introduction", "methods", "results", "discussion", "conclusion", "references"]) + ) + if _is_author_byline and page_num == 1: + return RoleAssignment( + role="frontmatter_noise", + confidence=0.5, + evidence=[f"author byline on page 1, not heading: {text[:60]}"], + ) + + if re.search(r"(?:correspondence|orcid|@)", text.lower()) and len(text.split()) <= 20 and page_num == 1: + return RoleAssignment( + role="frontmatter_noise", + confidence=0.5, + evidence=[f"correspondence/noise on page 1: {text[:60]}"], + ) + if _has_heading_numbering(text): depth = _heading_number_depth(text) return RoleAssignment( diff --git a/tests/test_ocr_roles.py b/tests/test_ocr_roles.py index 86277a06..006f4f5b 100644 --- a/tests/test_ocr_roles.py +++ b/tests/test_ocr_roles.py @@ -490,6 +490,32 @@ def test_weak_backmatter_boundary_signal_emits_candidate(): ) +def test_author_byline_not_section_heading(): + from paperforge.worker.ocr_roles import assign_block_role + + # Author byline with & on page 1 + block = {"block_label": "paragraph_title", "block_content": "John Smith & Jane Doe", "block_bbox": [100, 50, 500, 80], "page": 1} + page_blocks = [block, + {"block_label": "text", "block_content": "Some frontmatter", "block_bbox": [100, 100, 500, 130], "page": 1}, + {"block_label": "text", "block_content": "Some abstract content", "block_bbox": [100, 200, 500, 230], "page": 1}, + ] + 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 + + block = {"block_label": "paragraph_title", "block_content": "*Correspondence: john@example.com", "block_bbox": [100, 500, 500, 530], "page": 1} + page_blocks = [block] + result = assign_block_role(block, page_blocks=page_blocks, page_width=600, page_height=800) + assert result.role in ("frontmatter_noise", "unknown_structural"), ( + f"Correspondence got role: {result.role}" + ) + + 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."""