mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat(pr3): add column-aware same-page reference boundary
This commit is contained in:
parent
59b303dd66
commit
53a5336b6a
2 changed files with 121 additions and 2 deletions
|
|
@ -522,6 +522,52 @@ def _block_y_bottom(block: dict) -> float:
|
|||
return bbox[3] if bbox else 0.0
|
||||
|
||||
|
||||
|
||||
def _block_column_band(block: dict, page_width: float) -> int | None:
|
||||
"""Classify a block into left (0), right (1), or center/mixed (None) column band.
|
||||
Covers full-width blocks by checking width threshold.
|
||||
"""
|
||||
bbox = block.get("bbox") or block.get("block_bbox") or [0, 0, 0, 0]
|
||||
if len(bbox) < 4 or page_width <= 0:
|
||||
return None
|
||||
x0, x1 = bbox[0], bbox[2]
|
||||
block_width = x1 - x0
|
||||
# Full-width blocks span both columns → compatible with everything
|
||||
if block_width >= page_width * 0.8:
|
||||
return None # full-width → treat as center/ambiguous
|
||||
cx = (x0 + x1) / 2.0
|
||||
if cx < page_width * 0.45:
|
||||
return 0
|
||||
if cx > page_width * 0.55:
|
||||
return 1
|
||||
return None
|
||||
|
||||
|
||||
def _is_full_width_ref_heading(block: dict, page_width: float) -> bool:
|
||||
"""True if the reference heading spans full page width (single-column layout)."""
|
||||
bbox = block.get("bbox") or block.get("block_bbox") or [0, 0, 0, 0]
|
||||
if len(bbox) < 4 or page_width <= 0:
|
||||
return False
|
||||
block_width = bbox[2] - bbox[0]
|
||||
return block_width >= page_width * 0.8
|
||||
|
||||
|
||||
def _is_in_same_reference_column(block: dict, ref_heading_block: dict | None, page_width: float) -> bool:
|
||||
"""Check if block shares the same column band as the reference heading.
|
||||
None ref_heading_block -> True (conservative).
|
||||
Full-width ref heading -> True (page-level, not column-level).
|
||||
"""
|
||||
if ref_heading_block is None:
|
||||
return True
|
||||
ref_band = _block_column_band(ref_heading_block, page_width)
|
||||
if ref_band is None:
|
||||
return True
|
||||
block_band = _block_column_band(block, page_width)
|
||||
if block_band is None:
|
||||
return True
|
||||
return block_band == ref_band
|
||||
|
||||
|
||||
_REFERENCE_ZONE_MARKER_TYPES: frozenset[str] = frozenset({
|
||||
"reference_numeric_bracket",
|
||||
"reference_numeric_dot",
|
||||
|
|
@ -1246,6 +1292,23 @@ def _is_below_same_page_reference_heading(block: dict, refs_start_page: int | No
|
|||
return _block_y_top(block) > ref_heading_top
|
||||
|
||||
|
||||
|
||||
|
||||
def _page_width_for_zone_block(block: dict, page_blocks: list[dict], body_anchor: dict | None = None) -> float:
|
||||
"""Derive page_width from block, page_blocks, or body_anchor. Fallback 1200."""
|
||||
pw = block.get("page_width", 0) or 0
|
||||
if pw:
|
||||
return float(pw)
|
||||
for b in page_blocks:
|
||||
bw = b.get("page_width", 0) or 0
|
||||
if bw:
|
||||
pw = max(pw, float(bw))
|
||||
if pw:
|
||||
return pw
|
||||
if body_anchor and isinstance(body_anchor, dict):
|
||||
pw = body_anchor.get("page_width", 0) or 0
|
||||
return float(pw) if pw else 1200.0
|
||||
|
||||
def infer_zones(
|
||||
blocks: list[dict],
|
||||
anchors: dict[str, dict] | None,
|
||||
|
|
@ -1524,6 +1587,13 @@ def infer_zones(
|
|||
ref_heading_top = _block_y_top(block)
|
||||
break
|
||||
|
||||
# Derive page_width for column-aware reference boundary checks
|
||||
_zone_page_width = _page_width_for_zone_block(
|
||||
ref_heading_block if ref_heading_block else (blocks[0] if blocks else {}),
|
||||
blocks,
|
||||
anchors,
|
||||
)
|
||||
|
||||
body_blocks = [
|
||||
block
|
||||
for block in blocks
|
||||
|
|
@ -1539,7 +1609,10 @@ def infer_zones(
|
|||
)
|
||||
and (
|
||||
(body_end_page is None or int(block.get("page", 0) or 0) <= body_end_page)
|
||||
or _is_above_same_page_reference_heading(block, refs_start_page, ref_heading_top if ref_heading_block else None)
|
||||
or (
|
||||
_is_above_same_page_reference_heading(block, refs_start_page, ref_heading_top if ref_heading_block else None)
|
||||
and _is_in_same_reference_column(block, ref_heading_block, _zone_page_width)
|
||||
)
|
||||
)
|
||||
and not _is_reference_item_candidate(block)
|
||||
and not _is_reference_heading_candidate(block)
|
||||
|
|
@ -1555,6 +1628,7 @@ def infer_zones(
|
|||
block
|
||||
for block in blocks
|
||||
if _is_below_same_page_reference_heading(block, refs_start_page, ref_heading_top if ref_heading_block else None)
|
||||
and _is_in_same_reference_column(block, ref_heading_block, _zone_page_width)
|
||||
and not _is_reference_item_candidate(block)
|
||||
and not _is_reference_heading_candidate(block)
|
||||
and block.get("block_id") is not None
|
||||
|
|
|
|||
|
|
@ -5555,4 +5555,49 @@ def test_frontmatter_support_below_body_start_rescued_by_width() -> None:
|
|||
assert "corresp" in fm_main["block_ids"], \
|
||||
"narrow frontmatter_support block should be in frontmatter_main_zone"
|
||||
assert "corresp" not in body["block_ids"], \
|
||||
"narrow frontmatter_support block should NOT be in body_zone"
|
||||
"narrow frontmatter_support block should NOT be in body_zone"
|
||||
|
||||
def test_block_column_band_left_right() -> None:
|
||||
from paperforge.worker.ocr_document import _block_column_band
|
||||
pw = 1200.0
|
||||
left = {"bbox": [100, 100, 500, 200]}
|
||||
right = {"bbox": [700, 100, 1100, 200]}
|
||||
center = {"bbox": [200, 100, 1000, 200]}
|
||||
full = {"bbox": [50, 100, 1150, 200]}
|
||||
assert _block_column_band(left, pw) == 0
|
||||
assert _block_column_band(right, pw) == 1
|
||||
assert _block_column_band(center, pw) is None
|
||||
assert _block_column_band(full, pw) is None # full-width -> None
|
||||
|
||||
|
||||
def test_is_in_same_reference_column() -> None:
|
||||
from paperforge.worker.ocr_document import _is_in_same_reference_column
|
||||
pw = 1200.0
|
||||
ref_heading = {"bbox": [700, 100, 1100, 150]} # right column
|
||||
left_block = {"bbox": [100, 400, 500, 500]} # left column
|
||||
right_block = {"bbox": [700, 400, 1100, 500]} # right column
|
||||
assert not _is_in_same_reference_column(left_block, ref_heading, pw)
|
||||
assert _is_in_same_reference_column(right_block, ref_heading, pw)
|
||||
assert _is_in_same_reference_column(left_block, None, pw) # None ref -> True
|
||||
|
||||
|
||||
def test_is_in_same_reference_column_full_width_ref() -> None:
|
||||
from paperforge.worker.ocr_document import _is_in_same_reference_column
|
||||
pw = 1200.0
|
||||
full_ref = {"bbox": [50, 100, 1150, 150]} # full-width
|
||||
left_block = {"bbox": [100, 400, 500, 500]}
|
||||
assert _is_in_same_reference_column(left_block, full_ref, pw) # full-width -> page-level
|
||||
|
||||
|
||||
def test_same_page_tail_column_aware() -> None:
|
||||
from paperforge.worker.ocr_document import infer_zones
|
||||
blocks = [
|
||||
{"page": 3, "role": "reference_heading", "text": "REFERENCES", "bbox": [700, 100, 1100, 140], "block_id": "p3_h1"},
|
||||
{"page": 3, "role": "reference_item", "text": "[1] Some ref", "bbox": [700, 150, 1100, 180], "block_id": "p3_r1"},
|
||||
{"page": 3, "role": "body_paragraph", "text": "Conclusions continue...", "bbox": [100, 300, 500, 350], "block_id": "p3_b1"},
|
||||
]
|
||||
anchors = {"body_family_anchor": {"status": "ACCEPT", "sample_pages": [2]}}
|
||||
region_bus = infer_zones(blocks, anchors)
|
||||
# Left-column body below ref heading but in different column -> NOT in tail zone
|
||||
assert "p3_b1" not in region_bus["tail_nonref_hold_zone"]["block_ids"], \
|
||||
"Left-column body should not be in tail_nonref_hold_zone despite being below ref heading"
|
||||
|
|
|
|||
Loading…
Reference in a new issue