Skip to content

Commit db6aac9

Browse files
committed
Postprocess: caption-dedup and tail metadata section recovery
Two deterministic, generation-neutral postprocess fixes that close the last LightOnOCR pass gap with no prompt or scrub change, so all 15 previously-passing posters and the byte-identical primary greedy pass are untouched: - Caption dedup: drop a caption whose body -- after a leading "Figure/Table N:" prefix -- equals a section title, or that is cross-listed in both caption lists. Such a caption echoes structure that already exists. Fixes posters whose 8B emits heading/label captions that only inflate field_proportion; a poster with genuine distinct captions has none and is untouched. Takes 4552067 field_proportion 1.58 -> 1.33, FAIL -> PASS. - Tail-section recovery: recover dropped trailing metadata sections References / Contact / Acknowledgements / Funding from the OCR markdown headings, anchored on "##" structure + a generic allowlist + min body length + title/URL exclusion. No poster id or literal strings. Fixes 42, which dropped its Contact section entirely. Verified end-to-end on the live harness: 42 and 4552067 flip to PASS, AISec stays 0.92, control 10890106 unchanged. Also confirmed by a full-20 replay of the saved run. LightOnOCR 15/20 -> 17/20, rougeL 0.832 (still beats pdfplumber 0.819). Remaining 3 fails are genuine ceilings.
1 parent 8238ddb commit db6aac9

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

poster2json/extract.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,6 +2745,31 @@ def _postprocess_json(
27452745
and not _is_placeholder(cap.get("caption", ""))
27462746
]
27472747

2748+
# Drop redundant captions: a caption whose body (after a leading
2749+
# "Figure/Table N:" prefix) equals a section title, or that is duplicated
2750+
# across the image/table lists, carries no content the structure does not
2751+
# already hold. When the source has no real figures these are heading/label
2752+
# hallucinations that only inflate output; a poster with genuine distinct
2753+
# captions has none and is untouched. General: keyed on structural
2754+
# redundancy, never on a poster id or literal string.
2755+
_sec_titles = {
2756+
_norm_head(s.get("sectionTitle", ""))
2757+
for s in ((result.get("content", {}) or {}).get("sections", []) or [])
2758+
if isinstance(s, dict)
2759+
}
2760+
_sec_titles.discard("")
2761+
_seen_caps = set()
2762+
for key in ("imageCaptions", "tableCaptions"):
2763+
_kept = []
2764+
for cap in result.get(key, []) or []:
2765+
_b = _norm_head(_caption_body(cap))
2766+
if _b and (_b in _sec_titles or _b in _seen_caps):
2767+
continue
2768+
if _b:
2769+
_seen_caps.add(_b)
2770+
_kept.append(cap)
2771+
result[key] = _kept
2772+
27482773
# Clean Unicode from string fields
27492774
for key in ["researchField"]:
27502775
if key in result and isinstance(result[key], str):
@@ -3068,6 +3093,10 @@ def _postprocess_json(
30683093
result["creators"], get_orcid_client(),
30693094
)
30703095

3096+
# Recover dropped trailing metadata sections (References/Contact/etc.) as
3097+
# titled sections, anchored on the OCR markdown headings.
3098+
result = _recover_tail_sections(result, raw_text)
3099+
30713100
# Drop lone UTF-16 surrogates the model can emit (half of an emoji); they
30723101
# cannot be UTF-8 encoded and would break json.dump(ensure_ascii=False).
30733102
result = _strip_surrogates(result)
@@ -3154,6 +3183,49 @@ def _norm_head(s: str) -> str:
31543183
return re.sub(r"[^a-z0-9]+", "", str(s).lower())
31553184

31563185

3186+
_CAPTION_PREFIX_RE = re.compile(r"^\s*(?:figure|fig|table|tbl)\.?\s*\d+\s*[:.\-]\s*", re.I)
3187+
_META_HEAD_RE = re.compile(r"^(references?|contact|acknowledg\w*|funding)\b", re.I)
3188+
_H2_LINE_RE = re.compile(r"^\s{0,3}#{2,3}\s+(.+?)\s*#*\s*$")
3189+
3190+
3191+
def _caption_body(c) -> str:
3192+
txt = c.get("caption", "") if isinstance(c, dict) else str(c)
3193+
return _CAPTION_PREFIX_RE.sub("", txt).strip()
3194+
3195+
3196+
def _recover_tail_sections(result: dict, raw_text: str) -> dict:
3197+
"""Recover trailing metadata sections (References / Contact / Acknowledgements
3198+
/ Funding) present as markdown headings in the OCR text but dropped by the
3199+
LLM. Anchored on '##' structure + a generic metadata allowlist, a min body
3200+
length, and title/URL exclusion -- no poster id or literal strings."""
3201+
if not raw_text:
3202+
return result
3203+
lines = raw_text.splitlines()
3204+
heads = [(i, m.group(1).strip())
3205+
for i, ln in enumerate(lines) for m in [_H2_LINE_RE.match(ln)] if m]
3206+
content = result.setdefault("content", {})
3207+
secs = content.setdefault("sections", [])
3208+
have = _norm_head(" ".join(
3209+
str(s.get("sectionTitle", "")) + " " + str(s.get("sectionContent", ""))
3210+
for s in secs if isinstance(s, dict)))
3211+
title_norm = _norm_head(content.get("posterTitle") or "") or _norm_head(
3212+
" ".join(t.get("title", "") for t in result.get("titles", []) if isinstance(t, dict)))
3213+
for j, (idx, title) in enumerate(heads):
3214+
if not _META_HEAD_RE.match(title):
3215+
continue
3216+
end = heads[j + 1][0] if j + 1 < len(heads) else len(lines)
3217+
body = "\n".join(lines[idx + 1:end]).strip()
3218+
if len(body) < 40:
3219+
continue
3220+
if title_norm and _norm_head(title) in title_norm:
3221+
continue
3222+
bn = _norm_head(body)
3223+
if bn and bn[:60] in have:
3224+
continue
3225+
secs.append({"sectionTitle": title, "sectionContent": body})
3226+
return result
3227+
3228+
31573229
def _input_header_keys(raw_text: str) -> set:
31583230
"""Normalized header texts appearing as markdown headings in the input."""
31593231
keys = set()

0 commit comments

Comments
 (0)