mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
fix: suppress panel labels, backmatter headings, fix figure placement — add render-hygiene audit gate
This commit is contained in:
parent
adb640c98e
commit
3bd51dee25
2 changed files with 117 additions and 4 deletions
|
|
@ -724,11 +724,15 @@ def render_fulltext_markdown(
|
|||
rfid = rf.get("reader_figure_id")
|
||||
if not rfid:
|
||||
continue
|
||||
cbid = rf.get("caption_block_id")
|
||||
page = _block_page_map.get(cbid) if cbid is not None else None
|
||||
page = None
|
||||
for item in rf.get("consumed_caption_block_ids", []):
|
||||
p = item.get("page") if isinstance(item, dict) else None
|
||||
if p is not None:
|
||||
page = p
|
||||
break
|
||||
if page is None:
|
||||
for aid in rf.get("consumed_asset_block_ids", []):
|
||||
p = _block_page_map.get(aid)
|
||||
for item in rf.get("consumed_asset_block_ids", []):
|
||||
p = item.get("page") if isinstance(item, dict) else None
|
||||
if p is not None:
|
||||
page = p
|
||||
break
|
||||
|
|
@ -963,6 +967,7 @@ def render_fulltext_markdown(
|
|||
"frontmatter_noise",
|
||||
"table_html",
|
||||
"figure_caption",
|
||||
"figure_inner_text",
|
||||
}
|
||||
if role in _SKIPPED_BODY_ROLES:
|
||||
continue
|
||||
|
|
@ -1030,6 +1035,14 @@ def render_fulltext_markdown(
|
|||
continue
|
||||
if "published online" in text.strip().lower():
|
||||
continue
|
||||
_BACKMATTER_HEADING_KEYWORDS = frozenset({
|
||||
"author contributions", "data availability", "funding", "acknowledg",
|
||||
"conflict of interest", "competing interests", "supplementary material",
|
||||
"ethics statement", "publisher", "biographies",
|
||||
})
|
||||
_heading_lower = text.strip().lower()
|
||||
if any(kw in _heading_lower for kw in _BACKMATTER_HEADING_KEYWORDS):
|
||||
continue
|
||||
if _is_bogus_heading(text):
|
||||
if text:
|
||||
lines.append(text)
|
||||
|
|
|
|||
|
|
@ -278,6 +278,106 @@ def test_reader_audit_reader_coverage_is_not_trivially_zero(rebuilt_reader_audit
|
|||
assert health.get("figure_reader_coverage_total", 0) > 0, f"{key}: reader coverage total is zero despite eligible legends"
|
||||
|
||||
|
||||
_BACKMATTER_KEYWORDS = [
|
||||
"author contributions", "data availability", "funding", "acknowledg",
|
||||
"conflict of interest", "competing interests", "supplementary material",
|
||||
"ethics statement", "publisher", "biographies",
|
||||
]
|
||||
|
||||
|
||||
def _is_panel_label(text: str) -> bool:
|
||||
return bool(re.match(r"^[A-H]\s*$", text.strip()))
|
||||
|
||||
def _is_panel_lower(text: str) -> bool:
|
||||
return bool(re.match(r"^[a-h]\s*$", text.strip()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_KEYS)
|
||||
def test_audit_fulltext_no_panel_letter_leaks(rebuilt_reader_audit_papers: dict, real_ocr_root: Path, key: str) -> None:
|
||||
lines = _fulltext_lines(real_ocr_root, key)
|
||||
leaks = []
|
||||
for i, line in enumerate(lines):
|
||||
s = line.strip()
|
||||
if _is_panel_label(s):
|
||||
if i > 0 and not lines[i - 1].strip().startswith(">") and not lines[i - 1].strip().startswith("!"):
|
||||
leaks.append(f"L{i}:{s}")
|
||||
elif _is_panel_lower(s):
|
||||
if i > 0 and not lines[i - 1].strip().startswith("##"):
|
||||
leaks.append(f"L{i}:{s}")
|
||||
assert not leaks, f"{key}: {len(leaks)} panel labels leaked into fulltext: {leaks[:10]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_KEYS)
|
||||
def test_audit_fulltext_no_backmatter_headings(rebuilt_reader_audit_papers: dict, real_ocr_root: Path, key: str) -> None:
|
||||
lines = _fulltext_lines(real_ocr_root, key)
|
||||
back_hds = []
|
||||
for i, line in enumerate(lines):
|
||||
s = line.strip().lower()
|
||||
if s.startswith("## ") and any(kw in s for kw in _BACKMATTER_KEYWORDS):
|
||||
back_hds.append(f"L{i}:{line.strip()[:80]}")
|
||||
assert not back_hds, f"{key}: {len(back_hds)} backmatter headings rendered as body headings: {back_hds[:8]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_KEYS)
|
||||
def test_audit_fulltext_no_figure_cards_after_refs(rebuilt_reader_audit_papers: dict, real_ocr_root: Path, key: str) -> None:
|
||||
lines = _fulltext_lines(real_ocr_root, key)
|
||||
ref_idx = next((i for i, l in enumerate(lines) if l.strip().lower() == "## references"), None)
|
||||
if ref_idx is None:
|
||||
return
|
||||
fig_cards_after = sum(1 for i in range(ref_idx, len(lines)) if lines[i].strip().startswith("> **Figure"))
|
||||
assert fig_cards_after == 0, f"{key}: {fig_cards_after} figure cards placed after REFERENCES section"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_KEYS)
|
||||
def test_audit_fulltext_no_body_before_introduction(rebuilt_reader_audit_papers: dict, real_ocr_root: Path, key: str) -> None:
|
||||
lines = _fulltext_lines(real_ocr_root, key)
|
||||
first_h2 = next((i for i, l in enumerate(lines) if re.match(r"^## \d+\.", l.strip()) or l.strip().lower().startswith("## introduction")), None)
|
||||
if first_h2 is None:
|
||||
return
|
||||
body_before = sum(
|
||||
1 for i in range(first_h2)
|
||||
if lines[i].strip()
|
||||
and not lines[i].strip().startswith("#")
|
||||
and not lines[i].strip().startswith("<!--")
|
||||
and not lines[i].strip().startswith(">")
|
||||
and not lines[i].strip().startswith("!")
|
||||
and not lines[i].strip().startswith("*")
|
||||
and len(lines[i].strip()) > 40
|
||||
)
|
||||
assert body_before <= 2, f"{key}: {body_before} body paragraphs appear before first structural heading"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_KEYS)
|
||||
def test_audit_fulltext_no_orphan_figure_embeds(rebuilt_reader_audit_papers: dict, real_ocr_root: Path, key: str) -> None:
|
||||
lines = _fulltext_lines(real_ocr_root, key)
|
||||
orphans = []
|
||||
for i, l in enumerate(lines):
|
||||
if l.strip().startswith("![[render/figures/"):
|
||||
nearby_hd = any(
|
||||
lines[j].strip().startswith("## ") or lines[j].strip().startswith("> **Figure")
|
||||
for j in range(max(0, i - 5), min(len(lines), i + 3))
|
||||
)
|
||||
if not nearby_hd:
|
||||
orphans.append(f"L{i}")
|
||||
assert not orphans, f"{key}: {len(orphans)} orphan figure embeds with no heading or reader card context"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ALL_KEYS)
|
||||
def test_audit_blocks_reference_items_in_reference_zone(rebuilt_reader_audit_papers: dict, real_ocr_root: Path, key: str) -> None:
|
||||
blocks = _read_jsonl(_structured_path(real_ocr_root, key))
|
||||
ref_outside = [
|
||||
b.get("block_id")
|
||||
for b in blocks
|
||||
if b.get("role") == "reference_item"
|
||||
and b.get("zone") not in ("reference_zone", "tail_nonref_hold_zone")
|
||||
]
|
||||
if ref_outside:
|
||||
total = sum(1 for b in blocks if b.get("role") == "reference_item")
|
||||
assert len(ref_outside) <= max(5, total * 0.05), (
|
||||
f"{key}: {len(ref_outside)}/{total} reference items outside reference_zone (max 5 or 5% of total)"
|
||||
)
|
||||
|
||||
|
||||
def test_reader_audit_normalized_matched_figures_keep_figure_semantics(
|
||||
rebuilt_reader_audit_papers: dict,
|
||||
real_ocr_root: Path,
|
||||
|
|
|
|||
Loading…
Reference in a new issue