klon8-base / src /scripts /fresh_titles.py
voraseth's picture
Upload folder using huggingface_hub
7d3eb8f verified
Raw History Blame Contribute Delete
6.78 kB
"""Fresh held-out titles for the final confirmation (runs/queue/PLAN.md step 4; runs/CSEL/CONFIRM_PREREG.md).
The reserved set, as recorded when the dev pilot was drawn (runs/CSEL/WORDRHYME_PREREG.md): the hygiene-passing
TEST-split titles outside data/prompts/test_topics.jsonl, never used in development. Each needs a human control
built exactly as for test150 (runs/SPLIT/judge/p*_htest.jsonl): the opening post's first stanza under that title
(data/modern_bot_test_prov.jsonl: post == 0, stanza == 0), spaces stripped; with several such stanzas the one
from the lowest thread id.
Exclusions, all by the decoration-free title key of scripts/split_titles.py: every text in data/prompts/*.jsonl
(test_topics, test150, pilots, seen_topics, benches, train-title lists ...), every "topic:" line in any judge
batch under runs/ and any runs/*.jsonl prompt field already written (so a title that was ever decoded, judged or
used to tune is out). Selection: seeded shuffle, first N.
Usage: fresh_titles.py [--n 120] [--seed 7] (takes up to n; asserts >= 100) -> data/prompts/fresh120.jsonl, data/pools/fresh120_human.jsonl
"""
import sys, os, re, json, glob, random, argparse, collections, unicodedata
sys.path.insert(0, "."); sys.path.insert(0, os.path.dirname(__file__))
from klon8 import prosody as P
THAI_ONLY = re.compile(r"[ก-๛]+")
KW = ("สุขสันต์", "วันเกิด", "กลบท", "บอร์ด", "สมาชิก", "ขอบคุณ", "ประกาศ", "ชมรม", "ยินดีต้อนรับ",
"อวยพร", "แนะนำตัว", "ทดสอบ", "บทกวี", "เรื่องสั้น", "ตอนที่", "ภาค", "ฝากตัว", "คุยกัน",
"ห้อง", "กระทู้", "รับสมัคร", "เชิญ",
# forum noise found by the purpose review (2026-09-28): birthday posts, reply prefixes, meta titles
"Happy", "Birthday", "HBD", "Re:", "RE:", "re:", "หัวข้อนี้")
LATIN_OR_DIGIT = re.compile(r"[A-Za-z0-9\u0e50-\u0e59]")
def key(t): # scripts/split_titles.py:key - titles that differ only by decoration are one thread
return re.sub(r"[^฀-๿]", "", unicodedata.normalize("NFC", t or ""))
def usable(t): # scripts/split_titles.py:usable (the judged-bench exclusion is covered by USED below)
if not THAI_ONLY.fullmatch(unicodedata.normalize("NFC", t)): return False
if any(k in t for k in KW): return False
return 6 <= len(t) <= 22 and len(P.g2p_line(t)) >= 2
def used_keys():
used = set()
for f in glob.glob("data/prompts/*.jsonl"):
if os.path.basename(f).startswith("fresh"): continue
for l in open(f, encoding="utf-8"):
if l.strip():
r = json.loads(l)
for k in ("text", "prompt", "topic", "title"):
if isinstance(r.get(k), str): used.add(key(r[k]))
for f in glob.glob("runs/**/batch_*.txt", recursive=True):
for l in open(f, encoding="utf-8", errors="ignore"):
m = re.match(r"item \S+\s+topic:\s*(.*)", l)
if m: used.add(key(m.group(1)))
for f in glob.glob("runs/**/*.jsonl", recursive=True):
if "/CONFIRM/" in f or os.path.getsize(f) > 200_000_000: continue
try:
for l in open(f, encoding="utf-8"):
if '"prompt"' not in l: continue
p = json.loads(l).get("prompt")
if isinstance(p, str): used.add(key(p))
except (UnicodeDecodeError, json.JSONDecodeError):
continue
used.discard("")
return used
def main():
ap = argparse.ArgumentParser(); ap.add_argument("--n", type=int, default=120); ap.add_argument("--seed", type=int, default=7)
ap.add_argument("--out", default="data/prompts/fresh120.jsonl"); ap.add_argument("--human", default="data/pools/fresh120_human.jsonl")
a = ap.parse_args()
train_keys = {key(json.loads(l)["title"]) for l in open("data/modern_bot_train.jsonl", encoding="utf-8")}
prov = [json.loads(l) for l in open("data/modern_bot_test_prov.jsonl", encoding="utf-8")]
opening = collections.defaultdict(list)
for r in prov:
if r.get("post") == 0 and r.get("stanza") == 0: opening[r["title"]].append(r)
EX = "data/prompts/fresh_exclusion_keys.json"
if os.path.exists(EX): # frozen at first build so reruns cannot drift (blind review)
used = set(json.load(open(EX, encoding="utf-8")))
else:
used = used_keys(); json.dump(sorted(used), open(EX, "w", encoding="utf-8"), ensure_ascii=False, indent=0)
# a title that LOSES Latin letters or digits when stripped is dropped: the stripped topic would differ in
# meaning from the raw title the human wrote to ("one one", "VS", "+ benuser", "VITAS")
cand = sorted(t for t in opening if not any(k in t for k in KW) and not LATIN_OR_DIGIT.search(t)
and usable(key(t)) and key(t) not in used and key(t) not in train_keys)
random.Random(a.seed).shuffle(cand)
pick, seen = [], set()
for t in cand:
if key(t) in seen: continue # one title per decoration-free key
seen.add(key(t)); pick.append(t)
if len(pick) == a.n: break
assert len(pick) >= 50, f"only {len(pick)} fresh titles available"
# the checks the reviews will want, asserted here
assert not ({key(t) for t in pick} & used), "a fresh title was used before"
assert not ({key(t) for t in pick} & train_keys), "a fresh title is a train title"
by_title = collections.defaultdict(list)
for r in prov: by_title[r["title"]].append(r)
os.makedirs(os.path.dirname(a.human), exist_ok=True)
with open(a.out, "w", encoding="utf-8") as f, open(a.human, "w", encoding="utf-8") as h:
for i, t in enumerate(pick, 1):
op = sorted(opening[t], key=lambda r: r["id"])[0] # lowest source id, as the test150 controls
fid = f"F{i:03d}"
f.write(json.dumps({"id": fid, "form": "topic", "text": key(t), "raw_title": t, "set": "FRESH",
"raw_clean": bool(usable(t)),
"n_human_bot": len(by_title[t]), "syl": len(P.g2p_line(key(t)))}, ensure_ascii=False) + "\n")
h.write(json.dumps({"id": fid, "prompt": key(t), "poem": op["poem"].replace(" ", ""), "src_id": op["id"], "board": op.get("board"),
"thread": op["thread"]}, ensure_ascii=False) + "\n")
P.save_cache()
print(f"{len(cand)} unused hygiene-passing held-out titles with an opening-post first stanza; kept {len(pick)} "
f"(seed {a.seed}); {len(used)} used title keys excluded -> {a.out}, {a.human}")
if __name__ == "__main__":
main()