File size: 4,921 Bytes
e379257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""JSONL data format (same as the report). One record per line:

{"id", "images": [path,...], "state": str|obj, "questions": {qid: {type, instructions, criteria}},

 "teacher": {qid: {option_id: prob}}  (optional; teacher distribution, e.g. Clef via Workers AI or local)

 "gold":    {qid: option_id}          (optional; ground-truth label, e.g. from synthetic generator)

 "meta":    {...}}

Image paths are resolved relative to the JSONL file.

For noul questions option ids are "true"/"false". A teacher value may be a bare float = p(true)

(that's what the Workers AI / SystemOne response gives), it is expanded automatically.



Box questions (Jev 1.14 `box`, see glint/box.py): questions[qid] = {"type": "box", "instructions": ...};

gold[qid] = {"top", "bottom", "left", "right"} as fractions (0-1) of image height/width from the top-left,

optionally with "present": bool ({"present": false} alone is fine). Box gold comes from ground truth, never

from the teacher; teacher entries for box questions are ignored. present=false -> no box loss for that question.

Presence itself is a separate noul question, by convention `<qid>_visible`; if it has no gold, it is derived

from `present` (`derive_presence_gold`)."""
from __future__ import annotations
import json, random
from pathlib import Path
from PIL import Image
from .box import is_box_question

def load_jsonl(path):
    path = Path(path)
    rows = []
    with open(path, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                ex = json.loads(line)
                ex["_base"] = str(path.parent)
                rows.append(ex)
    return rows

def normalize_teacher(ex: dict) -> dict:
    out = {}
    for qid, v in (ex.get("teacher") or {}).items():
        q = ex["questions"].get(qid)
        if q is None or is_box_question(q):
            continue
        if isinstance(v, (int, float)):            # noul: p(true)
            v = {"true": float(v), "false": 1.0 - float(v)}
        elif isinstance(v, dict) and "probabilities" in v:   # SystemOne choice/score answer
            v = v["probabilities"]
        elif isinstance(v, dict) and "noul" in v:
            v = {"true": float(v["noul"]), "false": 1.0 - float(v["noul"])}
        out[qid] = {str(k): float(p) for k, p in v.items()}
    return out

def open_images(ex: dict):
    base = Path(ex.get("_base", "."))
    return [Image.open(p if Path(p).is_absolute() else base / p).convert("RGB") for p in ex.get("images", [])]

def to_record(ex: dict, augment: bool = False) -> dict:
    # encode_record は choice の選択肢を option_id でソートするので選択肢順シャッフルは無意味。
    # 増強は質問(フィールド)順のシャッフルのみ。
    items = list(ex["questions"].items())
    if augment:
        random.shuffle(items)
    return {"id": ex.get("id", "unknown"), "state": ex.get("state", ""), "images": open_images(ex),
            "questions": dict(items), **({"prompt_style": ex["prompt_style"]} if ex.get("prompt_style") else {})}

def teacher_questions(questions: dict) -> dict:
    """Questions a Clef teacher can answer (drops `box` questions, which Clef does not support)."""
    return {qid: q for qid, q in questions.items() if not is_box_question(q)}

def normalize_box_gold(ex: dict) -> dict:
    """{qid: [top, bottom, left, right] or None (= not present / no gold box)} for every box question with gold.

    Accepts {"top",..} or {"box": {"top",..}} with optional "present"; swaps inverted edges, clamps to [0, 1]."""
    out = {}
    for qid, q in ex["questions"].items():
        g = (ex.get("gold") or {}).get(qid)
        if not is_box_question(q) or g is None:
            continue
        if not isinstance(g, dict):
            raise ValueError(f"{ex.get('id')}/{qid}: box gold must be an object, got {g!r}")
        b = g.get("box", g)
        if g.get("present", True) is False or not all(k in b for k in ("top", "bottom", "left", "right")):
            out[qid] = None
            continue
        t, bo, l, r = (min(max(float(b[k]), 0.0), 1.0) for k in ("top", "bottom", "left", "right"))
        out[qid] = [min(t, bo), max(t, bo), min(l, r), max(l, r)]
    return out

def derive_presence_gold(ex: dict) -> dict:
    """gold, plus `<qid>_visible` = "true"/"false" from a box gold's `present` when that noul question exists

    and has no gold of its own. Returns the original dict when there is nothing to add."""
    gold = ex.get("gold") or {}
    extra = {}
    for qid, q in ex["questions"].items():
        g, vis = gold.get(qid), f"{qid}_visible"
        if is_box_question(q) and isinstance(g, dict) and vis in ex["questions"] and vis not in gold:
            extra[vis] = "false" if g.get("present", True) is False else "true"
    return {**gold, **extra} if extra else gold