Skip to content

Commit e4fba74

Browse files
committed
Streamline character variants to one canonical form for reproducible JSON
The scrub decomposed some things (superscripts to digits) but left typographic and symbol variants untouched, so the character set the downstream LLM converts to JSON was inconsistent -- e.g. "datatype folder(s) -> modality folder(s)" kept a unicode arrow while everything around it was ASCII. Inconsistent input makes the LLM's JSON output non-reproducible. _normalize_chars collapses each variant family to a single form: - curly quotes -> straight, en/em/minus dashes -> "-", ellipsis -> "...", unicode spaces -> space, arrows -> "->"/"<-"; - box-drawing and block characters are dropped -- they are the model rendering a folder tree as |-- lines, structure not content; - NFKC folds remaining compatibility duplicates (micro sign U+00B5 vs greek mu U+03BC, any unicode super/subscript digits) to one encoding. Real content is preserved: verified 124 Hebrew characters survive on 8228476, and Greek, accented Latin (Argudin, Munz-Manor) and units (degree, euro, micro, multiplication, >=) are left intact. The step is metric-neutral by construction -- the scorer already strips non- alphanumerics, so rField/rGlobal are unchanged at 0.784/0.816 -- its value is a clean, uniform character set for the LLM->JSON stage.
1 parent 01d73f2 commit e4fba74

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

calibration/vlm/survey_chars.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python3
2+
"""Enumerate every non-ASCII character in the scrubbed VLM outputs, so the
3+
normalization map is built from what actually appears, not guesses."""
4+
import glob
5+
import os
6+
import sys
7+
import unicodedata
8+
from collections import Counter
9+
10+
REPO = "/home/joneill/Nextcloud/vaults/jmind/calmi2/poster_science/poster2json"
11+
sys.path.insert(0, os.path.join(REPO, "calibration/vlm"))
12+
from vlm_scrub import scrub
13+
14+
V = os.path.join(REPO, "calibration/vlm/out")
15+
counts = Counter()
16+
for f in glob.glob(os.path.join(V, "*.md")):
17+
t = scrub(open(f, encoding="utf-8").read())
18+
for ch in t:
19+
if ord(ch) > 127:
20+
counts[ch] += 1
21+
22+
print(f"{'char':>5} {'codepoint':>10} {'count':>6} name")
23+
for ch, n in counts.most_common(60):
24+
try:
25+
name = unicodedata.name(ch)
26+
except ValueError:
27+
name = "?"
28+
print(f"{ch!r:>5} {'U+%04X' % ord(ch):>10} {n:>6} {name}")

calibration/vlm/vlm_scrub.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
structure. Only the tabular data payload is removed; every caption survives.
1919
"""
2020
import re
21+
import unicodedata
2122

2223
_FENCE_MARKER = re.compile(r"^[ \t]*```.*$", re.MULTILINE)
2324

@@ -70,6 +71,32 @@ def _math(m):
7071
_BOLD_ONLY = re.compile(r"\*\*[^*]+\*\*$")
7172

7273

74+
# Reproducibility: the model leaves typographic variants (curly quotes, en/em
75+
# dashes, arrows) untouched while other things get decomposed, so the character
76+
# set the downstream LLM converts to JSON is inconsistent. Collapse each variant
77+
# family to ONE ASCII form, drop box-drawing art (the model rendering a folder
78+
# tree as |-- lines), and NFKC-normalize the rest -- which unifies duplicate
79+
# encodings (micro sign U+00B5 vs greek mu U+03BC, super/subscript digits) while
80+
# LEAVING real content: Hebrew/Arabic/Greek letters, accented names, units.
81+
_BOXDRAW = re.compile(r"[─-▟]+") # box-drawing + block elements
82+
_PUNCT = str.maketrans({
83+
"‘": "'", "’": "'", "‚": "'", "‛": "'",
84+
"“": '"', "”": '"', "„": '"', "‟": '"',
85+
"–": "-", "—": "-", "‒": "-", "―": "-", "−": "-",
86+
"…": "...", " ": " ", " ": " ", " ": " ", " ": " ",
87+
"→": "->", "⟶": "->", "➔": "->", "⇒": "->", "➙": "->",
88+
"←": "<-", "⟵": "<-", "⇐": "<-",
89+
})
90+
91+
92+
def _normalize_chars(text: str) -> str:
93+
text = _BOXDRAW.sub(" ", text)
94+
text = text.translate(_PUNCT)
95+
# NFKC collapses compatibility variants (micro->mu, super/subscripts) to a
96+
# single representation; combining marks on Hebrew/accents are preserved.
97+
return unicodedata.normalize("NFKC", text)
98+
99+
73100
def _strip_footer_logos(text: str) -> str:
74101
lines = text.splitlines()
75102
while lines:
@@ -112,6 +139,7 @@ def scrub(text: str) -> str:
112139
text = _IMAGE.sub("", text) # drop image placeholders
113140
text = _delatex(text) # LaTeX -> plain, keep affil markers
114141
text = _TAG.sub("", text) # remaining tags, keep inner text
142+
text = _normalize_chars(text) # one canonical char per variant family
115143
text = _TRAIL_WS.sub("\n", text)
116144
text = _BLANKS.sub("\n\n", text)
117145
text = _strip_footer_logos(text.strip()) # peel sponsor/logo lines off foot

0 commit comments

Comments
 (0)