Skip to content

Commit 9db2408

Browse files
committed
Rescue prose cells from data tables; strip figure panel-label scaffolding
Two scrub refinements from an adversarially-verified analysis pass. Data tables: _table_repl dropped a short-median table whole, which lost 8228568's interview-quote tables (Reason/Participant columns are short so the median is short, but the Example column holds the poster's actual Results as long quotes). That starved the downstream 8B and produced no JSON. The data-table branch now RESCUES cells that are genuine free text (>=40 chars, <=2 bullets, >=50% letters) and still drops short/numeric/ bulleted-grid cells. Table classification (median length) is unchanged, so layout tables -- and 4448680, whose earlier blanket keep-all crashed word capture -- are byte-identical. 8228568 input mass restored 3035->4402 chars with its quotes back. Scaffold: drop lone figure panel labels ("A"/"B"/"C") and an orphaned code-fence language token ("plaintext") the model emits on their own lines; they match no caption and dilute per-field precision. The bare-number-run variant was rejected in review because it deletes real F1 result values on 8228476. Extraction-stage rField (calibration harness): out 0.784 -> 0.788, w 0.929 -> 0.946, rGlobal 0.816 -> 0.827; out_rope2464 0.746 -> 0.751. No per-poster regression. Scrubbed LightOnOCR now leads pdfplumber 0.788 vs 0.762 on the extraction stage.
1 parent e4fba74 commit 9db2408

1 file changed

Lines changed: 41 additions & 2 deletions

File tree

calibration/vlm/vlm_scrub.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,59 @@ def _strip_footer_logos(text: str) -> str:
116116
# -- which the model uses to reproduce a poster's column grid -- holds whole
117117
# prose paragraphs. Median cell length separates them.
118118
_DATA_CELL_MAXLEN = 45
119+
_BULLET = re.compile(r"[•▪◦‣·∙]|<br\b")
120+
_PROSE_CELL_MINLEN = 40
121+
122+
123+
def _cell_text(c):
124+
return re.sub(r"<[^>]+>", "", _IMAGE.sub("", c)).strip() # drop ![img] then tags
125+
126+
127+
def _is_prose_cell(raw):
128+
"""A table cell that is genuine free text (a quote, a finding), not a short
129+
label, a number, or a bulleted grid column."""
130+
breaks = len(_BULLET.findall(raw)) # count on RAW (bullets/<br> intact)
131+
txt = _cell_text(raw)
132+
if len(txt) < _PROSE_CELL_MINLEN: # short label / numeric cell
133+
return False
134+
if breaks > 2: # bulleted grid column (the 4448680 noise)
135+
return False
136+
letters = sum(ch.isalpha() for ch in txt)
137+
return bool(txt) and letters >= 0.5 * len(txt) # not a pure-numeric grid
119138

120139

121140
def _table_repl(m):
122-
"""Drop a data table (short cells); keep a layout table's prose."""
141+
"""Layout table -> keep every cell's prose. Data table -> drop the grid but
142+
RESCUE any long free-text cells (interview quotes, findings) that would
143+
otherwise be lost with the table; a poster's Results sometimes live in an
144+
Example column. Table CLASSIFICATION (median cell length) is unchanged, so
145+
layout tables are handled byte-identically to before."""
123146
cells = _CELL.findall(m.group(1))
124147
if not cells:
125148
return ""
126149
lens = sorted(len(re.sub(r"<[^>]+>", "", c).strip()) for c in cells)
127150
median = lens[len(lens) // 2]
128151
if median <= _DATA_CELL_MAXLEN:
129-
return "" # data within an image
152+
kept = [_cell_text(c) for c in cells if _is_prose_cell(c)]
153+
return ("\n\n".join(kept) + "\n") if kept else ""
130154
# layout grid: keep the cell text as paragraphs, one per cell
131155
return "\n\n".join(re.sub(r"<[^>]+>", "", c).strip() for c in cells) + "\n"
132156

133157

158+
# Figure/chart scaffolding the VLM reads out of image regions: lone panel labels
159+
# ("A"/"B"/"C") and an orphaned code-fence language token emitted without its
160+
# backticks ("plaintext"). These survive as their own lines, match no caption,
161+
# and drag per-field ROUGE-L precision down. (The bare-number-run variant was
162+
# rejected in review — it deletes real F1 result values on 8228476.)
163+
_SCAFFOLD_LABEL = re.compile(
164+
r"^(?:[A-Za-z][.)]?|plaintext|html|python|json|text|markdown|yaml|css|bash|sql)$")
165+
166+
167+
def _strip_scaffold(text: str) -> str:
168+
return "\n".join(ln for ln in text.splitlines()
169+
if not (ln.strip() and _SCAFFOLD_LABEL.match(ln.strip())))
170+
171+
134172
def scrub(text: str) -> str:
135173
if not text:
136174
return text
@@ -142,6 +180,7 @@ def scrub(text: str) -> str:
142180
text = _normalize_chars(text) # one canonical char per variant family
143181
text = _TRAIL_WS.sub("\n", text)
144182
text = _BLANKS.sub("\n\n", text)
183+
text = _strip_scaffold(text) # drop lone panel labels / orphan fence tokens
145184
text = _strip_footer_logos(text.strip()) # peel sponsor/logo lines off foot
146185
return text.strip()
147186

0 commit comments

Comments
 (0)