#!/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()