Download src/scripts/fresh_titles.py from voraseth/klon8-base: direct link, hf CLI and curl.
- Browser
- Download file 6.78 kB
-
https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/fresh_titles.py
- Command line
-
hf download hf://voraseth/klon8-base/src/scripts/fresh_titles.py
-
curl -L -o fresh_titles.py https://huggingface.co/voraseth/klon8-base/resolve/main/src/scripts/fresh_titles.py
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() | |