Skip to content

Commit 5a7fa46

Browse files
committed
Bound best-of fallback; figure-caption prompt rule; VLM loop-collapse scrub
Three general improvements to the LightOnOCR pipeline, validated on the full 20: - Best-of fallback bound: only spend the penalty-free fallback generation when it could plausibly win (retry ran away, or dropped 2+ source headers). A clean retry covering all-but-one header is kept as-is. Cuts a doomed fallback on already-recovered runaways (5128504 1287s -> 581s) with no outcome change. - EXTRACTION_PROMPT: figure captions take the poster's "Figure N:" line, not the Markdown image auto-description; never write the bracketed alt-text into a caption or section. General rule for all figures. Recovers AISec rougeL 0.70 -> 0.92. - vlm_scrub._collapse_loops: collapse a VLM decode runaway (a short token/line repeated >5x) to one instance before the other passes. O(n), fires only on pathological repetition; touched only isporeu of 19 caches (33316 -> 12791 chars, KBA x4109 -> x4), the rest byte-stable. Full-20 LightOnOCR (ranks 1-4): rougeL 0.792 -> 0.830 (now BEATS pdfplumber 0.819), field_proportion 0.856 -> 0.939, pass 13 -> 15/20. Known: rank-3 caption extraction inflates 4552067 field_proportion to 1.58 (content fine, rougeL 0.81); refinement pending.
1 parent d6df9bc commit 5a7fa46

2 files changed

Lines changed: 53 additions & 8 deletions

File tree

calibration/vlm/vlm_scrub.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,50 @@ def _strip_scaffold(text: str) -> str:
169169
if not (ln.strip() and _SCAFFOLD_LABEL.match(ln.strip())))
170170

171171

172+
def _collapse_loops(text: str, k: int = 5) -> str:
173+
"""Collapse a VLM decode runaway -- a short token or line repeated many
174+
times in a row -- to a single instance. LightOnOCR (greedy, no repetition
175+
penalty) occasionally loops on fine print (e.g. one poster's cache is 63%
176+
'KBA;' repeated ~4000x), and the raw markdown otherwise flows straight to
177+
the structuring LLM and drowns the real content. Only a run of >k identical
178+
short tokens/lines is collapsed, so normal prose (which never repeats the
179+
same short token 6+ times consecutively) is untouched. O(n), no regex
180+
backtracking, so it is safe on a 30k-char runaway."""
181+
if not text:
182+
return text
183+
# 1) runs of identical (stripped) lines -> keep one
184+
lines = text.split("\n")
185+
out, i = [], 0
186+
while i < len(lines):
187+
j = i
188+
while j < len(lines) and lines[j].strip() == lines[i].strip():
189+
j += 1
190+
if lines[i].strip() and (j - i) > k:
191+
out.append(lines[i])
192+
else:
193+
out.extend(lines[i:j])
194+
i = j
195+
# 2) runs of an identical short token within a line -> keep one
196+
collapsed = []
197+
for ln in out:
198+
toks, res, i = ln.split(" "), [], 0
199+
while i < len(toks):
200+
j = i
201+
while j < len(toks) and toks[j] == toks[i]:
202+
j += 1
203+
if toks[i] and len(toks[i]) <= 20 and (j - i) > k:
204+
res.append(toks[i])
205+
else:
206+
res.extend(toks[i:j])
207+
i = j
208+
collapsed.append(" ".join(res))
209+
return "\n".join(collapsed)
210+
211+
172212
def scrub(text: str) -> str:
173213
if not text:
174214
return text
215+
text = _collapse_loops(text) # collapse VLM decode runaways first
175216
text = _FENCE_MARKER.sub("", text) # drop ``` markers, keep caption prose
176217
text = _TABLE.sub(_table_repl, text) # drop data tables, keep layout prose
177218
text = _IMAGE.sub("", text) # drop image placeholders

poster2json/extract.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,7 +1588,7 @@ def _generate(
15881588
4. Each section must have its OWN "sectionTitle" and "sectionContent"
15891589
5. Copy ALL poster text EXACTLY into sections - do not paraphrase, summarize, or skip any text. Every line of the poster text below must appear in your output.
15901590
6. "Key Findings" ≠ "References": Key Findings = discoveries/results; References = numbered citations with authors/years
1591-
7. Figure/table captions belong in imageCaptions/tableCaptions, NOT inside sectionContent.
1591+
7. Figure/table captions belong in imageCaptions/tableCaptions, NOT inside sectionContent. A caption is the poster's own "Figure N: ..." / "Table N: ..." descriptive line. If a figure has BOTH a "Figure N:" caption line AND a generated image-description in Markdown image syntax "![description](...)", put the "Figure N:" CAPTION LINE in imageCaptions and DISCARD the "![...]" description text; never write the "![...]" alt-text into imageCaptions or a section. This only affects which text becomes a caption; it does not change how sections are split.
15921592
8. Text without a clear header (e.g. contact info, URLs, footer text) is still a section — use "sectionTitle": "" with the verbatim text as "sectionContent". Do NOT skip any poster text.
15931593
15941594
JSON SCHEMA (all top-level fields are REQUIRED):
@@ -3271,7 +3271,7 @@ def extract_json_with_retry(
32713271
# penalty on the retry (not the primary) so healthy posters are
32723272
# untouched.
32733273
log(f"Retrying with max_tokens={MAX_RETRY_TOKENS}")
3274-
r_retry, _ = _generate(
3274+
r_retry, retry_eos = _generate(
32753275
model, tokenizer, prompt, MAX_RETRY_TOKENS,
32763276
repetition_penalty=RETRY_REPETITION_PENALTY,
32773277
)
@@ -3281,13 +3281,17 @@ def extract_json_with_retry(
32813281

32823282
# A short penalty retry can summarize away a real tail (References /
32833283
# Acknowledgements / contact) that IS present in the source, closing
3284-
# cleanly yet dropping content. Unless the retry already covers every
3285-
# input header, also run the penalty-free shorter fallback and keep
3286-
# whichever candidate retains the most source headers -- a candidate
3287-
# that dropped a header cannot beat one that kept it.
3284+
# cleanly yet dropping content -- so also run the penalty-free shorter
3285+
# fallback and keep whichever candidate retains the most source headers
3286+
# (a candidate that dropped a header cannot beat one that kept it).
3287+
# But only spend that fallback generation when it could plausibly win:
3288+
# the retry itself ran away (unclean) OR it dropped 2+ source headers.
3289+
# A clean retry already covering all-but-one header is kept as-is -- the
3290+
# fallback could add at most one header and usually just burns a doomed
3291+
# generation (measured: +15-20 min on already-recovered runaways).
32883292
n_heads = len(input_keys)
3289-
retry_full = n_heads and _header_coverage(res_retry, input_keys) >= n_heads
3290-
if not retry_full:
3293+
retry_cov = _header_coverage(res_retry, input_keys)
3294+
if n_heads and (not retry_eos or retry_cov < n_heads - 1):
32913295
log("Using fallback shorter prompt (best-of candidate)")
32923296
fallback_prompt = FALLBACK_PROMPT.format(raw_text=raw_text)
32933297
r_fb, _ = _generate(model, tokenizer, fallback_prompt, MAX_RETRY_TOKENS)

0 commit comments

Comments
 (0)