|
| 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