mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
fix: pre-proof marker suppression with page-1 context guard, rescue exemption, title fallback
- _PREPROOF_MARKER suppression now guarded by page==1, y_top>8%, raw_label=paragraph_title, not_header, not_footer - _is_preproof_marker() exported for health use - rescue_roles_with_document_context: pre-proof frontmatter_noise blocks are exempt from body_paragraph rescue - Page-1 title fallback: when pre-proof steals the title zone, next substantial text block becomes paper_title - Tests: running header not suppressed, page-2 not suppressed, variants pass
This commit is contained in:
parent
b33ac572e4
commit
1e310ee351
4 changed files with 127 additions and 2 deletions
|
|
@ -1180,6 +1180,10 @@ def rescue_roles_with_document_context(
|
|||
|
||||
# --- Rule 1: frontmatter_noise → body_paragraph (body section + body font)
|
||||
if block.get("role") == "frontmatter_noise":
|
||||
# Never rescue pre-proof markers — they are intentional page furniture suppression
|
||||
from paperforge.worker.ocr_roles import is_preproof_marker
|
||||
if is_preproof_marker(str(block.get("text", "") or block.get("block_content", "") or "")):
|
||||
continue
|
||||
page = block.get("page", 1) or 1
|
||||
bbox = block.get("bbox") or block.get("block_bbox") or [0, 0, 0, 0]
|
||||
page_h = block.get("page_height") or 1700
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ def run_derived_rebuild_for_keys(vault: Path, keys: list[str]) -> dict:
|
|||
metadata_dir.mkdir(parents=True, exist_ok=True)
|
||||
frontmatter_candidates = extract_frontmatter_candidates(artifacts.blocks_structured)
|
||||
page1_blocks = [block for block in structured if int(block.get("page", 0) or 0) == 1]
|
||||
resolved = resolve_metadata(source_meta, frontmatter_candidates, page1_blocks=page1_blocks)
|
||||
resolved = resolve_metadata(source_meta, frontmatter_candidates, page1_blocks=page1_blocks, structured_blocks=structured)
|
||||
write_resolved_metadata(metadata_dir / "resolved_metadata.json", resolved)
|
||||
|
||||
# Rebuild figure inventory
|
||||
|
|
@ -247,14 +247,23 @@ def _enrich_meta_from_paper_note(vault: Path, key: str, meta_path: Path) -> None
|
|||
return
|
||||
meta = read_json(meta_path) if meta_path.exists() else {}
|
||||
changed = False
|
||||
# Always re-extract authors on rebuild — source_metadata may carry stale
|
||||
# first-author-only entries from previous runs with no way to detect them
|
||||
if meta.get("authors_source") != "zotero":
|
||||
meta.pop("authors", None)
|
||||
meta.pop("authors_incomplete", None)
|
||||
meta.pop("authors_source", None)
|
||||
meta.pop("first_author", None)
|
||||
changed = True
|
||||
for field in ("title", "authors", "year", "journal", "doi"):
|
||||
if field not in meta or not meta.get(field):
|
||||
val = fm.get(field)
|
||||
if val:
|
||||
meta[field] = val
|
||||
changed = True
|
||||
if ("authors" not in meta or not meta.get("authors")) and fm.get("first_author"):
|
||||
if (not meta.get("authors")) and fm.get("first_author"):
|
||||
meta["authors"] = [str(fm["first_author"])]
|
||||
meta["first_author"] = str(fm["first_author"])
|
||||
meta["authors_incomplete"] = True
|
||||
meta["authors_source"] = "paper_note.first_author_fallback"
|
||||
changed = True
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ _TABLE_PREFIX_PATTERN = re.compile(
|
|||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
_PREPROOF_MARKER = re.compile(
|
||||
r"^(?:journal\s+)?pre-?proof\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_BACKMATTER_TITLE_DENY_LIST = {
|
||||
"generative ai statement",
|
||||
"acknowledgments",
|
||||
|
|
@ -128,6 +133,10 @@ def _heading_number_depth(text: str) -> int:
|
|||
return len([part for part in token.split(".") if part])
|
||||
|
||||
|
||||
def is_preproof_marker(text: str) -> bool:
|
||||
return bool(_PREPROOF_MARKER.match(text.strip()))
|
||||
|
||||
|
||||
def _has_figure_prefix(text: str) -> bool:
|
||||
return bool(_FIGURE_PREFIX_PATTERN.match(text.strip()))
|
||||
|
||||
|
|
@ -382,6 +391,33 @@ def assign_block_role(
|
|||
raw_label = str(block.get("block_label") or block.get("raw_label") or "").strip()
|
||||
text = str(block.get("block_content") or block.get("text") or "").strip()
|
||||
|
||||
if _PREPROOF_MARKER.match(text):
|
||||
bbox = block.get("block_bbox") or block.get("bbox") or [0, 0, 0, 0]
|
||||
y_top = bbox[1] if len(bbox) >= 4 else 0
|
||||
page_num = int(block.get("page", 0) or 0)
|
||||
page_h = float(block.get("page_height") or page_height or 1700)
|
||||
page_w = float(block.get("page_width") or page_width or 1200)
|
||||
block_width = (bbox[2] - bbox[0]) if len(bbox) >= 4 else 0
|
||||
|
||||
preproof_likely = (
|
||||
page_num == 1
|
||||
and y_top > page_h * 0.08
|
||||
and raw_label == "paragraph_title"
|
||||
)
|
||||
|
||||
is_running_header = y_top < page_h * 0.06
|
||||
is_footer = y_top > page_h * 0.94
|
||||
|
||||
if preproof_likely and not is_running_header and not is_footer:
|
||||
return RoleAssignment(
|
||||
role="frontmatter_noise",
|
||||
confidence=0.98,
|
||||
evidence=[
|
||||
"journal pre-proof marker: page 1, paragraph_title, "
|
||||
f"y={y_top:.0f}/{page_h:.0f}, width={block_width:.0f}/{page_w:.0f}"
|
||||
],
|
||||
)
|
||||
|
||||
# Panel label exclusion (single-letter figure labels like A, B, (C), A.)
|
||||
if _PANEL_LABEL_PATTERN.match(text):
|
||||
return RoleAssignment(
|
||||
|
|
@ -477,6 +513,24 @@ def assign_block_role(
|
|||
evidence=[f"page-1 zone title_zone: {text[:60]}"],
|
||||
)
|
||||
|
||||
# Fallback: page 1 after pre-proof marker — next substantial text block is the real title
|
||||
if zone is None and page_num == 1 and raw_label in ("paragraph_title", "text"):
|
||||
bbox = block.get("block_bbox") or block.get("bbox") or [0, 0, 0, 0]
|
||||
y_top = bbox[1] if len(bbox) >= 4 else 0
|
||||
ph = float(block.get("page_height") or page_height or 1700)
|
||||
already_has_preproof = any(
|
||||
str(b.get("text", "") or b.get("block_content", "") or "").strip().lower().startswith("journal pre")
|
||||
for b in page_blocks if b is not block
|
||||
)
|
||||
if already_has_preproof and y_top < ph * 0.35 and len(text) > 40:
|
||||
lower_noise = text.strip().lstrip("*•·-–—").lower()
|
||||
if lower_noise not in _BACKMATTER_TITLE_DENY_LIST:
|
||||
return RoleAssignment(
|
||||
role="paper_title",
|
||||
confidence=0.85,
|
||||
evidence=[f"page-1 title fallback after pre-proof: y={y_top:.0f}/{ph:.0f}"],
|
||||
)
|
||||
|
||||
if zone == "author_zone":
|
||||
return RoleAssignment(
|
||||
role="authors",
|
||||
|
|
|
|||
|
|
@ -658,3 +658,61 @@ def test_page1_top_title_not_misclassified_as_section_heading():
|
|||
from paperforge.worker.ocr_roles import assign_block_role
|
||||
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=[])
|
||||
assert result.role != "section_heading"
|
||||
|
||||
|
||||
def test_preproof_marker_is_frontmatter_noise():
|
||||
from paperforge.worker.ocr_roles import assign_block_role
|
||||
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=[])
|
||||
assert result.role == "frontmatter_noise"
|
||||
assert result.confidence >= 0.9
|
||||
|
||||
|
||||
def test_preproof_marker_variants():
|
||||
from paperforge.worker.ocr_roles import assign_block_role
|
||||
for text in ["Journal Pre-proof", "Pre-proof", "journal pre-proof"]:
|
||||
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=[])
|
||||
assert result.role == "frontmatter_noise"
|
||||
|
||||
|
||||
def test_preproof_running_header_not_suppressed():
|
||||
"""Pre-proof text at extreme top (like a running header) should NOT be suppressed."""
|
||||
from paperforge.worker.ocr_roles import assign_block_role
|
||||
result = assign_block_role({
|
||||
"raw_label": "paragraph_title",
|
||||
"text": "Journal Pre-proof",
|
||||
"page": 1, "page_width": 1224, "page_height": 1584,
|
||||
"block_bbox": [190, 30, 475, 55],
|
||||
}, page_blocks=[])
|
||||
assert result.role != "frontmatter_noise"
|
||||
|
||||
|
||||
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
|
||||
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=[])
|
||||
assert result.role != "frontmatter_noise"
|
||||
|
||||
|
||||
def test_real_title_after_preproof_still_works():
|
||||
from paperforge.worker.ocr_roles import assign_block_role
|
||||
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)
|
||||
assert result.role == "paper_title"
|
||||
|
|
|
|||
Loading…
Reference in a new issue