File size: 13,242 Bytes
7d3eb8f | 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 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """Experiment 1 (director, 2026-09-28): more evidence for the collocation veto, on extra development topics.
Same pipeline as 129 (runs/CSEL/VETOREF_PREREG.md) on topics that never touched test150: the user's everyday
benchmark topics (data/prompts/everyday_bench.jsonl, U001..U222) minus corpus titles (train or test split),
minus every evaluation set, normalised to Thai letters. Arms, per topic:
T = Typhoon pick (rank 0 of the 095 top-8 ranking) R1 = T + round-1 located refill
V = veto pick (122: bottom half by WC line PLL vetoed) VR = V + round-1 located refill
Refill = done/105 verbatim: weakest line = argmin copy-masked canvas line mean, K=16 re-draws with seeds
sha256(id|line|k), best topic-keeping form-passing variant replaces the line only if >= 0.5 higher.
Where V == T the VR poem is the R1 poem by construction (same id, same seeds); only V != T picks are refilled
a second time.
Subcommands (D = run dir, default runs/VETOMORE):
topics write data/prompts/vetomore_topics.jsonl (the disjointness asserts live here)
picks D/typhoon_picks.jsonl from D/typhoon_top8.jsonl (rank 0, as runs/CSEL/typhoon_picks.jsonl)
arms after colloc_veto.py: D/pick_T.jsonl (all topics) and D/pick_V.jsonl (V != T only)
locmap D/{T,V}_scores.jsonl -> D/{T,V}_line.json (argmin canvas line mean)
select accept rule for both arms -> D/r1.jsonl, D/vr.jsonl; changed = VR != R1 (form-driven and unrefilled
excluded, reported); drift gates on changed VR vs R1 (form >= 98%, stutters <= +0.10, cross-line
repeats <= +0.50); writes D/changed_ids.json and D/PASS only if the gates pass (exit 2 = killed,
exit 3 = nothing changed)
export judge inputs on the changed topics: D/judge_{round1,vetoref}.jsonl (id/prompt/poem only), plus the
6 test30_pilot topics whose VR != R1 in runs/VETOREF (129 excluded them as tuning topics):
D/judge_pilot_{round1,vetoref}.jsonl
"""
import os, re, sys, json, glob, collections
sys.path.insert(0, "."); sys.path.insert(0, "scripts")
BENCH = "data/prompts/everyday_bench.jsonl"
OUT_TOPICS = "data/prompts/vetomore_topics.jsonl"
EVAL_SETS = ["fresh120", "test150", "test30_pilot", "dev30_pilot", "test_topics", "seen_topics", "ul_train_titles",
"test", "train", "dev"]
def rows(p): return [json.loads(l) for l in open(p, encoding="utf-8") if l.strip()]
def norm(t): return re.sub(r"[^ก-๙]", "", t or "")
def topics(d=None):
corpus = set()
for f in ("data/modern_bot_train.jsonl", "data/modern_bot_test.jsonl"):
for r in rows(f):
for k in ("topic", "title"):
if r.get(k): corpus.add(norm(r[k]))
ev = set()
for s in EVAL_SETS:
p = f"data/prompts/{s}.jsonl"
if os.path.exists(p): ev |= {norm(r["text"]) for r in rows(p)}
U = rows(BENCH)
keep = [u for u in U if norm(u["text"]) and norm(u["text"]) not in corpus and norm(u["text"]) not in ev]
keys = [norm(u["text"]) for u in keep]
assert len(set(keys)) == len(keys), "duplicate normalised topics"
# asserts (tests): no overlap with corpus titles or any evaluation set, fresh120 in particular
fresh = {norm(r["text"]) for r in rows("data/prompts/fresh120.jsonl")}
assert not (set(keys) & corpus) and not (set(keys) & ev) and not (set(keys) & fresh)
with open(OUT_TOPICS, "w", encoding="utf-8") as f:
for u in keep: f.write(json.dumps({"id": u["id"], "form": "topic", "text": u["text"], "set": "VETOMORE",
"group": u.get("group")}, ensure_ascii=False) + "\n")
print(f" {len(U)} benchmark topics; corpus titles {sum(norm(u['text']) in corpus for u in U)}, "
f"evaluation sets {sum(norm(u['text']) in ev for u in U)} -> {len(keep)} extra development topics")
def picks(d):
top = [r for r in rows(f"{d}/typhoon_top8.jsonl") if r["sel_rank"] == 0]
with open(f"{d}/typhoon_picks.jsonl", "w", encoding="utf-8") as f:
for r in sorted(top, key=lambda r: r["topic_id"]):
rec = {k: v for k, v in r.items() if k not in ("topic_id", "seed")}; rec["id"] = r["topic_id"]
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
print(f" {len(top)} Typhoon picks")
def arms(d):
T = {r["id"]: r for r in rows(f"{d}/typhoon_picks.jsonl")}
V = {r["id"]: r for r in rows(f"{d}/veto_picks.jsonl")}
assert set(T) == set(V), "veto picks do not cover the topics"
assert all(V[i]["prompt"] == T[i]["prompt"] for i in T)
with open(f"{d}/pick_T.jsonl", "w", encoding="utf-8") as f:
for i in sorted(T): f.write(json.dumps({"id": i, "prompt": T[i]["prompt"], "poem": T[i]["poem"]}, ensure_ascii=False) + "\n")
ch = sorted(i for i in T if V[i]["poem"] != T[i]["poem"])
with open(f"{d}/pick_V.jsonl", "w", encoding="utf-8") as f:
for i in ch: f.write(json.dumps({"id": i, "prompt": V[i]["prompt"], "poem": V[i]["poem"]}, ensure_ascii=False) + "\n")
fd = sorted(i for i in ch if V[i].get("form_driven"))
json.dump({"veto_changed": ch, "form_driven": fd}, open(f"{d}/veto_changed.json", "w"), indent=0)
print(f" {len(T)} topics; veto pick differs on {len(ch)} ({len(fd)} form-driven)")
def locmap(d):
for a in ("T", "V"):
P_ = rows(f"{d}/pick_{a}.jsonl")
if not P_: # no veto change at all: arm V is skipped (review)
json.dump({}, open(f"{d}/{a}_line.json", "w")); print(f" arm {a}: no picks, skipped"); continue
S = rows(f"{d}/{a}_scores.jsonl")
assert [p["id"] for p in P_] == [s["id"] for s in S] and all(p["poem"] == s["poem"] for p, s in zip(P_, S)), \
f"{a}: scores do not belong to the picks"
m = {s["id"]: min(range(4), key=lambda i: s["score"]["line_mean"][i]) + 1 for s in S if s["score"]}
miss = [s["id"] for s in S if not s["score"]]
print(f" arm {a}: {len(m)} located, {len(miss)} without a score (kept unchanged, as round 1)")
assert len(miss) <= max(3, len(S) // 20), "too many picks fail to round-trip"
json.dump(m, open(f"{d}/{a}_line.json", "w"), indent=0)
def accept(cands, scores, loc):
"""done/105's accept rule, verbatim (also vetoref_select.select)."""
from syl_align import syls_aligned as SA
from canvas_mlm import STOP_SYL
assert len(cands) == len(scores) and all(c["id"] == s["id"] and c["poem"] == s["poem"] for c, s in zip(cands, scores)), \
"scores do not belong to these candidates"
topic_of = {c["id"]: {x for x in SA(c.get("prompt") or "", 12) if x not in STOP_SYL and len(x) > 1} for c in cands}
orig, var, unscored = {}, collections.defaultdict(list), []
for c, s in zip(cands, scores):
if c["variant"] == "orig":
if s["score"]: orig[c["id"]] = (c, s["score"]["line_mean"])
else: unscored.append(c)
elif s["score"]: var[c["id"]].append((c, s["score"]["line_mean"]))
best = {}
for i, (o, om) in orig.items():
L = loc[i] - 1
keep = set(SA(o["poem"].split("\n")[L], 12)) & topic_of[i]
ok = [(c, m) for c, m in var[i] if keep <= set(SA(c["poem"].split("\n")[L], 12))]
b = o
if ok:
c, m = max(ok, key=lambda x: x[1][L])
if m[L] >= om[L] + 0.5: b = c
best[i] = b
for c in unscored: best[c["id"]] = c
return best, {c["id"] for c in unscored}
def select(d):
from klon8 import prosody as P
from syl_align import syls_aligned as SA
from canvas_mlm import STOP_SYL
R1, unT = accept(rows(f"{d}/T_cands.jsonl"), rows(f"{d}/T_cands_scores.jsonl"), {k: int(v) for k, v in json.load(open(f"{d}/T_line.json")).items()})
vc = json.load(open(f"{d}/veto_changed.json"))
if vc["veto_changed"]:
VRc, unV = accept(rows(f"{d}/V_cands.jsonl"), rows(f"{d}/V_cands_scores.jsonl"), {k: int(v) for k, v in json.load(open(f"{d}/V_line.json")).items()})
else: VRc, unV = {}, set()
VR = {i: VRc.get(i, R1[i]) for i in R1} # V == T: the R1 poem by construction
assert set(VRc) == set(vc["veto_changed"]) and len(R1) == len(rows(f"{d}/pick_T.jsonl"))
excl = set(vc["form_driven"]) | unV | unT
changed = sorted(i for i in R1 if VR[i]["poem"] != R1[i]["poem"] and i not in excl)
def stut(p): return sum(a == b for ln in p.split("\n") for a, b in zip(SA(ln, 12), SA(ln, 12)[1:]))
def xrep(p):
ls = [set(x for x in SA(l, 12) if x not in STOP_SYL and len(x) > 1) for l in p.split("\n")]
c = collections.Counter(x for s_ in ls for x in s_); return sum(v - 1 for v in c.values() if v > 1)
m = lambda f, ps: sum(map(f, ps)) / max(len(ps), 1)
new_p, old_p = [VR[i]["poem"] for i in changed], [R1[i]["poem"] for i in changed]
form = 100.0 if not new_p else 100 * sum(bool(P.score(p, n_bot_expected=1)["all_pass"]) for p in new_p) / len(new_p)
rep = {"stutters": (m(stut, new_p), m(stut, old_p)), "cross-line repeats": (m(xrep, new_p), m(xrep, old_p))}
print(f" {len(R1)} topics; R1 refilled {sum(r['variant'] != 'orig' for r in R1.values())}; VR differs from R1 on "
f"{len(changed)} endpoint topics (excluded: form-driven {sorted(set(vc['form_driven']))}, unrefilled {sorted(unV | unT)})")
print(f" changed poems: form {form:.0f}%; " + "; ".join(f"{k} {a:.3f} (R1 {b:.3f})" for k, (a, b) in rep.items()))
P.save_cache()
for f_ in ("r1.jsonl", "vr.jsonl", "changed_ids.json", "PASS"):
if os.path.exists(f"{d}/{f_}"): os.remove(f"{d}/{f_}")
if not changed: print(" nothing changed: nothing to judge"); sys.exit(3)
TOL = {"stutters": 0.10, "cross-line repeats": 0.50}
dead = (["form"] if form < 98 else []) + [k for k, (a, b) in rep.items() if a > b + TOL[k]]
print(" KILLED before judging: " + ", ".join(dead) if dead else " pre-judge gates PASS")
if dead: sys.exit(2)
for name, A in (("r1", R1), ("vr", VR)):
with open(f"{d}/{name}.jsonl", "w", encoding="utf-8") as f:
for i in sorted(A): f.write(json.dumps({"id": i, "prompt": A[i]["prompt"], "poem": A[i]["poem"],
"variant": A[i]["variant"]}, ensure_ascii=False) + "\n")
json.dump({"endpoint": changed, "form_driven": sorted(set(vc["form_driven"])), "unrefilled": sorted(unV | unT)},
open(f"{d}/changed_ids.json", "w"), indent=0)
open(f"{d}/PASS", "w").write("gates passed\n")
def export(d):
assert os.path.exists(f"{d}/PASS"), "gates did not pass"
ids = json.load(open(f"{d}/changed_ids.json"))["endpoint"]
for name, src in (("round1", f"{d}/r1.jsonl"), ("vetoref", f"{d}/vr.jsonl")):
A = {r["id"]: r for r in rows(src)}
with open(f"{d}/judge_{name}.jsonl", "w", encoding="utf-8") as f:
for i in ids: f.write(json.dumps({"id": i, "prompt": A[i]["prompt"], "poem": A[i]["poem"]}, ensure_ascii=False) + "\n")
# the 6 pilot (test30) topics 129 left out as tuning topics: already decoded and refilled in runs/VETOREF
pil = {r["id"] for r in rows("data/prompts/test30_pilot.jsonl")}
ep = json.load(open("runs/VETOREF/endpoint.json"))
excl = set(ep["form_driven"]) | set(ep["not_refilled"])
vr = {r["id"]: r for r in rows("runs/VETOREF/best.jsonl")}; r1 = {r["id"]: r for r in rows("runs/LOCREF/best.jsonl")}
pc = sorted(i for i in pil if vr[i]["poem"] != r1[i]["poem"] and i not in excl and i not in ep["endpoint"])
# the same drift gates on the pilot pairs (review); failing pairs are dropped and reported, not judged
from syl_align import syls_aligned as SA
from canvas_mlm import STOP_SYL
from klon8 import prosody as P
def stut(p): return sum(a == b for ln in p.split("\n") for a, b in zip(SA(ln, 12), SA(ln, 12)[1:]))
def xrep(p):
ls = [set(x for x in SA(l, 12) if x not in STOP_SYL and len(x) > 1) for l in p.split("\n")]
c = collections.Counter(x for s_ in ls for x in s_); return sum(v - 1 for v in c.values() if v > 1)
m = lambda f, ps: sum(map(f, ps)) / max(len(ps), 1)
np_, op_ = [vr[i]["poem"] for i in pc], [r1[i]["poem"] for i in pc]
form = 100.0 if not np_ else 100 * sum(bool(P.score(q, n_bot_expected=1)["all_pass"]) for q in np_) / len(np_)
dead = (["form"] if form < 98 else []) + (["stutters"] if m(stut, np_) > m(stut, op_) + 0.10 else []) + \
(["cross-line repeats"] if m(xrep, np_) > m(xrep, op_) + 0.50 else [])
print(f" pilot pairs: form {form:.0f}%; stutters {m(stut, np_):.3f} vs {m(stut, op_):.3f}; repeats {m(xrep, np_):.3f} vs {m(xrep, op_):.3f}"
+ (f" - KILLED ({', '.join(dead)}): pilot pairs not exported" if dead else " - gates PASS"))
if dead: pc = []
for name, A in (("round1", r1), ("vetoref", vr)):
with open(f"{d}/judge_pilot_{name}.jsonl", "w", encoding="utf-8") as f:
for i in pc: f.write(json.dumps({"id": i, "prompt": A[i]["prompt"], "poem": A[i]["poem"]}, ensure_ascii=False) + "\n")
print(f" judge inputs: {len(ids)} extra-topic pairs, {len(pc)} pilot pairs {pc}")
if __name__ == "__main__":
cmd, d = sys.argv[1], (sys.argv[2] if len(sys.argv) > 2 else "runs/VETOMORE")
{"topics": topics, "picks": picks, "arms": arms, "locmap": locmap, "select": select, "export": export}[cmd](d)
|