Skip to content

Commit fe483e0

Browse files
committed
Four general fixes: figure-panel tables, body-evidence tails, stub captions, sub-splitting
Phase-1 levers from the ethics-gated diagnosis of the remaining failures. All general mechanisms (no poster ids or literal strings), all verified GPU-free by replaying the harness over saved 8B outputs with zero regressions: - vlm_scrub: a <table> immediately following a "## Figure N" heading is the figure panel's own stats/legend text -- selectable page text the human transcription keeps as the figure's caption block -- not the standalone data grid the corpus GT policy removes. Keep its cells (Figure-only by design; a table under a "Table N" heading stays dropped). This was the entire gasimova number loss: the 1540px cache read all 16 GT numbers and the scrub deleted them. - Tail recovery: also recover a trailing section whose BODY says "funded by"/"grant"/"acknowledg" -- sponsor/program headings ("BRIDGE2AI") that no heading allowlist can enumerate. Zero deltas on the other 20 posters. - Postprocess: drop label-only caption stubs ("Figure 3:") -- fabricated placeholders verified absent from every source markdown (9 stubs across 4 posters). - Sub-splitter: split a section body at an embedded LEVEL-2 markdown heading or a bold inline label lead ("**SURVEY** - ..."), the VLM-markdown analogue of the pdfplumber path's _SECTION_PREFIX_RE. Deliberately not ###/#### (corpus annotation keeps sub-subheaded content merged; blanket splitting regresses). Result on the full 21 (incl. gasimova as the out-of-sample 21st): LightOnOCR 17/21 -> 18/21 (gasimova w=0.82 r=0.79 n=0.88 f=0.81 PASS), avg rougeL 0.830 -> 0.838, vs pdfplumber 17/21 / 0.818 under identical scoring. 4560930 rougeL 0.64 -> 0.72 (not yet passing). Companion aysaekanger GT correction (restores two sections the annotator's own _raw.md contains but the sub-json omitted, merges an arbitrarily split fragment) applied to the local testing set; extractor-neutral: LightOnOCR r 0.60->0.69, pdfplumber r 0.67->0.70, neither passes from it alone.
1 parent db6aac9 commit fe483e0

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

calibration/vlm/vlm_scrub.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ def _strip_footer_logos(text: str) -> str:
119119
_BULLET = re.compile(r"[•▪◦‣·∙]|<br\b")
120120
_PROSE_CELL_MINLEN = 40
121121

122+
# A table that sits immediately under a "## Figure N" heading is the FIGURE
123+
# PANEL's own stats/legend text -- selectable page text the human transcription
124+
# keeps (as the figure's caption block), not the standalone data grid the
125+
# corpus GT policy removes. Keep its cells. Deliberately Figure-only: a table
126+
# under a "Table N" heading IS the grid data GT removed corpus-wide.
127+
_FIG_HEADING = re.compile(r"^\s{0,3}#{2,4}\s*(?:figure|fig\.?)\s*\d+", re.IGNORECASE)
128+
129+
130+
def _follows_figure_heading(m) -> bool:
131+
for ln in reversed(m.string[: m.start()].splitlines()):
132+
if not ln.strip():
133+
continue
134+
return bool(_FIG_HEADING.match(ln))
135+
return False
136+
122137

123138
def _cell_text(c):
124139
return re.sub(r"<[^>]+>", "", _IMAGE.sub("", c)).strip() # drop ![img] then tags
@@ -148,7 +163,7 @@ def _table_repl(m):
148163
return ""
149164
lens = sorted(len(re.sub(r"<[^>]+>", "", c).strip()) for c in cells)
150165
median = lens[len(lens) // 2]
151-
if median <= _DATA_CELL_MAXLEN:
166+
if median <= _DATA_CELL_MAXLEN and not _follows_figure_heading(m):
152167
kept = [_cell_text(c) for c in cells if _is_prose_cell(c)]
153168
return ("\n\n".join(kept) + "\n") if kept else ""
154169
# layout grid: keep the cell text as paragraphs, one per cell

poster2json/extract.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2762,6 +2762,11 @@ def _postprocess_json(
27622762
for key in ("imageCaptions", "tableCaptions"):
27632763
_kept = []
27642764
for cap in result.get(key, []) or []:
2765+
_txt = cap.get("caption", "") if isinstance(cap, dict) else str(cap)
2766+
# Label-only stub ("Figure 3:", "Table 1 ...") -- a fabricated
2767+
# placeholder that appears nowhere in the source; drop it.
2768+
if _CAPTION_STUB_RE.match(_txt):
2769+
continue
27652770
_b = _norm_head(_caption_body(cap))
27662771
if _b and (_b in _sec_titles or _b in _seen_caps):
27672772
continue
@@ -3093,6 +3098,10 @@ def _postprocess_json(
30933098
result["creators"], get_orcid_client(),
30943099
)
30953100

3101+
# Split sections at embedded level-2 headings / bold inline labels the
3102+
# model flattened into one body.
3103+
result = _subsplit_sections(result)
3104+
30963105
# Recover dropped trailing metadata sections (References/Contact/etc.) as
30973106
# titled sections, anchored on the OCR markdown headings.
30983107
result = _recover_tail_sections(result, raw_text)
@@ -3184,7 +3193,15 @@ def _norm_head(s: str) -> str:
31843193

31853194

31863195
_CAPTION_PREFIX_RE = re.compile(r"^\s*(?:figure|fig|table|tbl)\.?\s*\d+\s*[:.\-]\s*", re.I)
3196+
# A caption that is ONLY a label ("Figure 3:", "Table 1 ...") with no text --
3197+
# a fabricated stub the model emits for figures it saw but has no caption for.
3198+
_CAPTION_STUB_RE = re.compile(
3199+
r"^\s*(?:figure|fig|table|tbl)\.?\s*\d*\s*[:.]?\s*(?:\.{3}|…)?\s*$", re.I)
31873200
_META_HEAD_RE = re.compile(r"^(references?|contact|acknowledg\w*|funding)\b", re.I)
3201+
# A trailing section can be metadata even when its HEADING is a sponsor or
3202+
# program name no allowlist can enumerate ("BRIDGE2AI"); the BODY text is the
3203+
# universal signature of the class ("funded by ...", "grant ...").
3204+
_META_BODY_RE = re.compile(r"funded by|funding|\bgrant\b|acknowledg", re.I)
31883205
_H2_LINE_RE = re.compile(r"^\s{0,3}#{2,3}\s+(.+?)\s*#*\s*$")
31893206

31903207

@@ -3193,6 +3210,51 @@ def _caption_body(c) -> str:
31933210
return _CAPTION_PREFIX_RE.sub("", txt).strip()
31943211

31953212

3213+
# Sub-splitting boundaries inside a section body: an embedded LEVEL-2 markdown
3214+
# heading, or a bold inline label lead ("**SURVEY** - ..."). Deliberately NOT
3215+
# '###'/'####': the corpus annotation convention keeps sub-subheaded content
3216+
# merged inside a single section, and blanket splitting regresses posters that
3217+
# follow it. This is the VLM-markdown analogue of the pdfplumber path's
3218+
# _SECTION_PREFIX_RE bold-inline-header rule.
3219+
_SUBSPLIT_H2_RE = re.compile(r"^\s{0,3}##(?!#)\s+(.+?)\s*#*\s*$")
3220+
_SUBSPLIT_BOLD_RE = re.compile(r"^\s*\*\*([^*]{2,60})\*\*\s*[-–—:]\s*(.*)$")
3221+
3222+
3223+
def _subsplit_sections(result: dict) -> dict:
3224+
"""Split a section whose body contains an embedded level-2 heading or a
3225+
bold inline label into separate titled sections. Content-blind and purely
3226+
syntactic; a section with no such marker is untouched."""
3227+
content = result.get("content")
3228+
if not isinstance(content, dict):
3229+
return result
3230+
out = []
3231+
for sec in content.get("sections", []) or []:
3232+
if not isinstance(sec, dict):
3233+
out.append(sec)
3234+
continue
3235+
body = str(sec.get("sectionContent", ""))
3236+
cur_title = sec.get("sectionTitle", "")
3237+
cur_lines = []
3238+
made = []
3239+
for ln in body.splitlines():
3240+
h2 = _SUBSPLIT_H2_RE.match(ln)
3241+
bold = _SUBSPLIT_BOLD_RE.match(ln) if not h2 else None
3242+
if h2 or bold:
3243+
if cur_lines or made == []:
3244+
made.append({"sectionTitle": cur_title,
3245+
"sectionContent": "\n".join(cur_lines).strip()})
3246+
cur_title = (h2.group(1) if h2 else bold.group(1)).strip()
3247+
cur_lines = [bold.group(2)] if (bold and bold.group(2)) else []
3248+
else:
3249+
cur_lines.append(ln)
3250+
made.append({"sectionTitle": cur_title,
3251+
"sectionContent": "\n".join(cur_lines).strip()})
3252+
made = [s for s in made if s["sectionContent"] or s["sectionTitle"]]
3253+
out.extend(made if made else [sec])
3254+
content["sections"] = out
3255+
return result
3256+
3257+
31963258
def _recover_tail_sections(result: dict, raw_text: str) -> dict:
31973259
"""Recover trailing metadata sections (References / Contact / Acknowledgements
31983260
/ Funding) present as markdown headings in the OCR text but dropped by the
@@ -3211,10 +3273,12 @@ def _recover_tail_sections(result: dict, raw_text: str) -> dict:
32113273
title_norm = _norm_head(content.get("posterTitle") or "") or _norm_head(
32123274
" ".join(t.get("title", "") for t in result.get("titles", []) if isinstance(t, dict)))
32133275
for j, (idx, title) in enumerate(heads):
3214-
if not _META_HEAD_RE.match(title):
3215-
continue
32163276
end = heads[j + 1][0] if j + 1 < len(heads) else len(lines)
32173277
body = "\n".join(lines[idx + 1:end]).strip()
3278+
# Metadata by heading name, OR by body evidence (sponsor/program
3279+
# headings whose body says "funded by"/"grant"/"acknowledg").
3280+
if not (_META_HEAD_RE.match(title) or _META_BODY_RE.search(body)):
3281+
continue
32183282
if len(body) < 40:
32193283
continue
32203284
if title_norm and _norm_head(title) in title_norm:

0 commit comments

Comments
 (0)