105 lines
6.2 KiB
Python
105 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
||
"""exp14b — судейский проход по SOFT-классам (e чэнъюй, f кореференция) × армы C/F/X.
|
||
≥2 КРОСС-семейных span-цитирующих судьи, self-family exclusion (урок D37: exp14 фронтир
|
||
судил ОДИН судья). leave-one-out. Катастроф-скрин к победителю. $ персист, честный учёт.
|
||
|
||
Судьи: gpt-5-mini(openai) + kimi(moonshot) + mistral(mistral). Для F(gpt) gpt-5-mini исключён.
|
||
"""
|
||
from __future__ import annotations
|
||
import argparse, json, re, sys
|
||
from pathlib import Path
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
import exp14_common as C
|
||
|
||
ARMS = Path("/home/ubuntu/books/gu-zhenren/exp14b/arms")
|
||
OUT = Path("/home/ubuntu/books/gu-zhenren/exp14b")
|
||
ARM_FAMILY = {"C": "zai", "F": "openai", "X": "xai",
|
||
"D": "deepseek", "K": "moonshot", "M": "mistral"} # K→kimi-судья excl; M→mistral-судья excl
|
||
JUDGE_FAMILY = {"gpt-5-mini": "openai", "kimi-k2.6": "moonshot", "mistral-large-2512": "mistral"}
|
||
ALL_ARMS = ("C", "F", "X", "D", "K", "M")
|
||
CAPS = {"gpt-5-mini": 0.05, "kimi-k2.6": 0.08, "mistral-large-2512": 0.05}
|
||
|
||
SOFT = [
|
||
("e1", "8.1", "物是人非", "контраст «вещи/места ПРЕЖНИЕ — люди уже НЕ ТЕ»; плоское «всё изменилось» теряет половину (物是)."),
|
||
("e2", "2.1", "韬光养晦", "«скрывать блеск, выжидать» — таиться/копить силы, не выпячиваться; не «развиваться/трудиться»."),
|
||
("f1", "16.0", "这促使它不得不外出,寻找野生的酒囊花", "субъект «它» = ВИННЫЙ ЧЕРВЬ (酒虫); это ОН вынужден выйти искать дикие酒囊花. Не спутать субъект."),
|
||
("f2", "11.1", "他们的身形隐没在阴影中…分不清男女", "двое (пол НЕЯСЕН) в тени; НЕ навязывать род/пол, НЕ выдумывать кто это."),
|
||
]
|
||
SYS = ("Ты — билингвальный эксперт zh→ru. Оцени, верно ли русский перевод передаёт смысл ОДНОГО "
|
||
"указанного китайского фрагмента. Ответь СТРОГО JSON без markdown: "
|
||
'{"verdict":"correct|partial|wrong","catastrophe":true|false,'
|
||
'"evidence":"дословная цитата из перевода ≤15 слов","note":"кратко"}. '
|
||
"catastrophe=true при подмене субъекта/участника или перевёрнутом смысле.")
|
||
|
||
|
||
def run():
|
||
ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true"); a = ap.parse_args()
|
||
sp = C.Spender(OUT / "judge_costs.jsonl", CAPS)
|
||
vpath = OUT / "soft_verdicts.jsonl"; out = []; done = set()
|
||
if vpath.exists():
|
||
for l in vpath.read_text(encoding="utf-8").splitlines():
|
||
if l.strip():
|
||
v = json.loads(l); out.append(v); done.add((v["trap"], v["arm"], v["judge"]))
|
||
tasks = []
|
||
for tid, cid, span, meaning in SOFT:
|
||
for arm in ALL_ARMS:
|
||
fp = ARMS / arm / f"{cid}.txt"
|
||
if not fp.exists(): continue
|
||
for jm in JUDGE_FAMILY:
|
||
if JUDGE_FAMILY[jm] == ARM_FAMILY[arm]: continue # self-family exclusion
|
||
if (tid, arm, jm) in done: continue
|
||
tasks.append((tid, cid, span, meaning, arm, jm, fp.read_text(encoding="utf-8")))
|
||
print(f"soft-judge tasks: {len(tasks)} new ({len(done)} done)")
|
||
if a.dry:
|
||
from collections import Counter; print(Counter(t[5] for t in tasks)); return
|
||
errs = {"empty": 0, "http": 0, "other": 0}
|
||
for tid, cid, span, meaning, arm, jm, text in tasks:
|
||
user = (f"КИТАЙСКИЙ ФРАГМЕНТ: {span}\nДОПУСТИМЫЙ СМЫСЛ: {meaning}\n\n"
|
||
f"РУССКИЙ ПЕРЕВОД ОТРЫВКА:\n{text}\n\nОцени передачу ТОЛЬКО этого фрагмента. JSON.")
|
||
s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 6000))
|
||
in_est = C.count_tokens(SYS + user, "glm")
|
||
ok, pred = sp.guard(jm, in_est, s["max_tokens"])
|
||
if not ok:
|
||
print(f" OVER {tid} {arm} {jm}"); continue
|
||
t, err, usage = C.call(s, [{"role": "system", "content": SYS}, {"role": "user", "content": user}], timeout=300)
|
||
cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "trap": tid, "arm": arm, "err": err, "usage": usage})
|
||
if err:
|
||
errs["empty" if "empty" in err else ("http" if "http" in err else "other")] += 1
|
||
v = parse(t) if not err else {"verdict": "ERR"}
|
||
v.update(trap=tid, arm=arm, judge=jm)
|
||
out.append(v)
|
||
with open(vpath, "a", encoding="utf-8") as f: f.write(json.dumps(v, ensure_ascii=False) + "\n")
|
||
print(f" {tid} {arm} {jm:<18} → {v.get('verdict'):<8} cat={v.get('catastrophe')} ${cst:.4f}")
|
||
print(f"\n=== ошибки судей (честно): {errs} ===")
|
||
summarize(out)
|
||
|
||
|
||
def parse(text):
|
||
m = re.search(r"\{.*\}", text, re.S)
|
||
if not m: return {"verdict": "PARSE"}
|
||
try:
|
||
d = json.loads(m.group(0))
|
||
return {"verdict": str(d.get("verdict", "?")).lower(), "catastrophe": bool(d.get("catastrophe")),
|
||
"evidence": (d.get("evidence") or "")[:100], "note": (d.get("note") or "")[:100]}
|
||
except Exception:
|
||
vv = re.search(r'"verdict"\s*:\s*"(\w+)"', m.group(0))
|
||
return {"verdict": vv.group(1).lower() if vv else "PARSE", "catastrophe": "true" in m.group(0).lower()}
|
||
|
||
|
||
def summarize(out):
|
||
from collections import defaultdict
|
||
agg = defaultdict(lambda: defaultdict(list))
|
||
for v in out: agg[v["trap"]][v["arm"]].append(v)
|
||
print("\n=== SOFT-классы: вердикты (арм: судьи) ===")
|
||
for tid, cid, span, meaning in SOFT:
|
||
if tid not in agg: continue
|
||
print(f"\n{tid} ch{cid} [{span}]:")
|
||
for arm in ALL_ARMS:
|
||
vs = agg[tid].get(arm)
|
||
if not vs: continue
|
||
zv = [(v["judge"][:4], v["verdict"], "CAT" if v.get("catastrophe") else "") for v in vs]
|
||
print(f" {arm}: {zv}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
run()
|