Skip to content

Commit 6bcbdc5

Browse files
committed
Add corpus-wide annotation audit: does ground truth claim what the poster does not?
Generalizes the check that caught poster 42, which credited the CarD-T preprint six authors where the poster prints three. Ground truth that names people or institutions absent from the poster is unreachable by any extractor, so it silently caps a poster score and, worse, can be mistaken for an extraction bug: 42 sat at LOGIC-GAP for all of Track B on exactly that misreading. Compares each annotation against the human transcription and reports authors absent from the poster, authors appearing only inside the reference list, affiliations sharing no distinctive word with the poster, and full .json creators disagreeing with _sub-json.json. Deliberately tolerant of abbreviation: posters print VTT, STScI, Technion and A. Perdomo where deposit metadata spells them out, and expanding an abbreviation is faithful rather than invented. A first cut matched on the leading phrase and flagged seven posters, nearly all of them that benign case, which buried the real signal. Across the corpus only 42 was genuinely wrong; 8228476 remains flagged because its full .json and _sub-json.json disagree on author ORDER, and the corrector anchors the banner search on the first creator (RTL, approach D).
1 parent f43ed24 commit 6bcbdc5

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
"""Corpus-wide annotation audit: does the ground truth claim things the poster
3+
does not say?
4+
5+
The test that caught poster 42, applied to all 21. For each poster, compare the
6+
annotation against the HUMAN transcription (_raw.md), which is by definition
7+
what the poster says. Anything in the annotation but absent from the
8+
transcription came from somewhere else (the paper, the Zenodo record) and is
9+
unreachable by any extractor, so it silently caps that poster's score.
10+
11+
Reports, per poster:
12+
- creators whose family name never appears in _raw.md
13+
- creators who appear ONLY inside the References section
14+
- GT affiliation strings whose leading institution phrase is absent
15+
- full .json creators disagreeing with _sub-json.json creators
16+
"""
17+
import glob
18+
import json
19+
import os
20+
import re
21+
import unicodedata
22+
23+
A = ("/home/joneill/Nextcloud/vaults/jmind/calmi2/poster_science/"
24+
"json_schema/manual_poster_annotation")
25+
EXTRA = [("gasimova(oos)", "/storage/poster-work/gasimova_clean_raw.md",
26+
"/storage/poster-work/gasimova_annotation.json", None)]
27+
28+
29+
_STOP = {"university", "department", "institute", "school", "research",
30+
"center", "centre", "college", "laboratory", "faculty", "division",
31+
"hospital", "national", "science", "sciences", "medical", "medicine",
32+
"technology", "technical", "health", "public"}
33+
34+
35+
def norm(t):
36+
t = unicodedata.normalize("NFKD", str(t))
37+
t = "".join(c for c in t if not unicodedata.combining(c))
38+
for a, b in (("’", "'"), ("‘", "'"), ("–", "-"), ("—", "-")):
39+
t = t.replace(a, b)
40+
return re.sub(r"\s+", " ", t).strip().lower()
41+
42+
43+
def refs_start(raw):
44+
"""Offset where the reference list begins, or len(raw)."""
45+
m = re.search(r"^#{1,6}\s*(references|bibliography|reference list)\b",
46+
raw, re.IGNORECASE | re.MULTILINE)
47+
return m.start() if m else len(raw)
48+
49+
50+
items = []
51+
for d in sorted(glob.glob(os.path.join(A, "*"))):
52+
if not os.path.isdir(d):
53+
continue
54+
pid = os.path.basename(d)
55+
raw = glob.glob(os.path.join(d, "*_raw.md"))
56+
ann = os.path.join(d, f"{pid}.json")
57+
sub = glob.glob(os.path.join(d, "*_sub-json.json"))
58+
if raw and os.path.exists(ann):
59+
items.append((pid, raw[0], ann, sub[0] if sub else None))
60+
items.extend(EXTRA)
61+
62+
problems = 0
63+
for pid, rawp, annp, subp in items:
64+
with open(rawp, encoding="utf-8") as fh:
65+
raw = fh.read()
66+
with open(annp, encoding="utf-8") as fh:
67+
ann = json.load(fh)
68+
nraw = norm(raw)
69+
body = norm(raw[:refs_start(raw)])
70+
msgs = []
71+
72+
creators = [c for c in ann.get("creators", []) if c.get("name")]
73+
for c in creators:
74+
nm = c["name"]
75+
fam = nm.split(",")[0].strip() if "," in nm else nm
76+
nf = norm(fam)
77+
if len(nf) < 3:
78+
continue
79+
if nf not in nraw:
80+
msgs.append(f"author ABSENT from poster: {nm!r}")
81+
elif nf not in body:
82+
msgs.append(f"author only in REFERENCES: {nm!r}")
83+
84+
seen = set()
85+
for c in creators:
86+
for a in c.get("affiliation", []):
87+
name = a.get("name") if isinstance(a, dict) else a
88+
if not name or name in seen:
89+
continue
90+
seen.add(name)
91+
# Only flag an affiliation the poster does not gesture at AT ALL.
92+
# Posters abbreviate constantly ("VTT" for "VTT Technical Research
93+
# Centre of Finland Ltd", "STScI", "Technion"), and expanding an
94+
# abbreviation is faithful, not invented. Requiring a substring
95+
# match on the leading phrase flags those and buries the real
96+
# thing. A genuinely fabricated affiliation shares no distinctive
97+
# word with the poster at all.
98+
words = [w for w in re.findall(r"[a-z]{5,}", norm(name))
99+
if w not in _STOP]
100+
if words and not any(w in nraw for w in words):
101+
msgs.append(f"affiliation ABSENT from poster: {name[:64]!r}")
102+
103+
if subp and os.path.exists(subp):
104+
with open(subp, encoding="utf-8") as fh:
105+
sub = json.load(fh)
106+
sn = [c.get("name") for c in sub.get("creators", []) if c.get("name")]
107+
fn = [c.get("name") for c in creators]
108+
if sn and sn != fn:
109+
msgs.append(f"full .json creators {fn} != sub-json {sn}")
110+
111+
if msgs:
112+
problems += 1
113+
print(f"=== {pid} ===")
114+
for m in msgs:
115+
print(" " + m)
116+
print()
117+
118+
print(f"{problems} of {len(items)} posters have annotation content "
119+
f"absent from the poster.")

0 commit comments

Comments
 (0)