#!/usr/bin/env python3 """exp16 — A6b: local 9B as a Z1 VERIFIER of translation pairs (research/20 §D2 A6b, §B4 Z1). $0 (stand). §B4 routes SPOT to code (A6 showed code>=local 9B) and reserves the local 9B for VERIFICATION of the stably-wrong class Z1 (蛊→«гусеница»: high termhood, zero spread, dst is a common word — invisible to the spread signal). This mini-bench asks: can a local 9B judge whether a ru rendering of a zh term IN CONTEXT is faithful? Gold is AUTOMATIC: CORRECT pairs use the owner-signed seed dst; WRONG pairs inject the Z1 failure class (plausible common-word mistranslations + cross-term swaps). Accuracy + wrong-catch rate. 30 pairs (15 correct + 15 wrong). Deterministic (temp 0, think off). Health-checked. """ from __future__ import annotations import json import re import urllib.request from pathlib import Path import exp16_common as X MODEL = "huihui_ai/qwen3.5-abliterated:9b" OLLAMA = "http://localhost:11434/api/chat" OUT = Path("/home/ubuntu/books/gu-zhenren/exp16") _opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) # Z1 verification bench: (src_term, correct_dst, wrong_dst[Z1 stably-wrong class]). Wrong = a plausible # common Russian word or a cross-term swap that the spread signal would NOT catch. BENCH = [ ("蛊", "гу", "гусеница"), # Z1 archetype: 蛊 as a common bug-word ("蛊师", "гу-мастер", "заклинатель насекомых"), ("蛊虫", "гу-червь", "личинка"), ("转", "ранг", "оборот"), # 转 polysemy: rank vs turn/revolution ("元石", "первобытный камень", "метеорит"), ("真元", "истинная ци", "чистая энергия"), ("空窍", "апертура", "полость"), ("凡人", "смертный", "простолюдин"), ("修炼", "культивация", "тренировка"), ("长生", "бессмертие", "долголетие"), ("族长", "глава клана", "старейшина"), # the D38 c3 confound as a Z1 pair ("四代族长", "Четвёртый глава рода", "четвёртый старейшина"), ("人上之人", "человек над людьми", "человек среди людей"), # the c1 catastrophe as Z1 ("方源", "Фан Юань", "Квадратный Источник"), # name mistranslated literally ("花酒行者", "Монах Цветочного Вина", "Странник цветов и вина"), ] def call_local(prompt, timeout=90): body = json.dumps({"model": MODEL, "think": False, "messages": [{"role": "user", "content": prompt + "\n/no_think"}], "stream": False, "options": {"temperature": 0.0, "num_predict": 200}}).encode() req = urllib.request.Request(OLLAMA, data=body, headers={"Content-Type": "application/json"}) try: with _opener.open(req, timeout=timeout) as r: d = json.loads(r.read()) return (d.get("message", {}) or {}).get("content", ""), None except Exception as e: return "", f"{type(e).__name__}: {str(e)[:120]}" def find_sentence(term, chunks): tn = X.norm(term) for c in chunks: for sent in re.split(r"(?<=[。!?])", c.source): if term in sent and 6 <= len(sent) <= 80: return sent.strip() return term def verdict_yes_no(text): text = re.sub(r".*?", "", text, flags=re.S).lower() # first explicit yes/да or no/нет m = re.search(r"\b(да|верно|correct|yes|правильн)\b", text) n = re.search(r"\b(нет|неверно|incorrect|no|ошибочн|неправильн)\b", text) if n and (not m or n.start() < m.start()): return "no" if m: return "yes" return "?" def main(): chunks = X.load_chunks() pairs = [] for term, ok_dst, bad_dst in BENCH: sent = find_sentence(term, chunks) pairs.append((term, sent, ok_dst, "correct")) pairs.append((term, sent, bad_dst, "wrong")) rows = [] health = {"ok": 0, "failed": 0} for term, sent, dst, gold in pairs: prompt = (f"Китайский термин «{term}» в предложении:\n{sent}\n\n" f"Предложенный русский перевод этого термина: «{dst}».\n\n" f"Верен ли этот перевод термина по смыслу в данном контексте? " f"Ответь одним словом: ДА или НЕТ, затем короткое обоснование.") out, err = call_local(prompt) if err: health["failed"] += 1 rows.append(dict(term=term, dst=dst, gold=gold, pred="failed", err=err)); continue health["ok"] += 1 pred = verdict_yes_no(out) model_says = "correct" if pred == "yes" else ("wrong" if pred == "no" else "?") rows.append(dict(term=term, dst=dst, gold=gold, pred=pred, model_says=model_says, correct=(model_says == gold))) mark = "✓" if model_says == gold else ("?" if pred == "?" else "✗") print(f" {mark} {term:<6} «{dst[:24]:<24}» gold={gold:<7} model={model_says:<7}") scored = [r for r in rows if r.get("pred") not in ("failed", "?")] acc = sum(r["correct"] for r in scored) / len(scored) if scored else 0 # wrong-catch (recall on the Z1 wrong class) + correct-accept (specificity) wrong = [r for r in scored if r["gold"] == "wrong"] correct = [r for r in scored if r["gold"] == "correct"] wrong_catch = sum(r["model_says"] == "wrong" for r in wrong) / len(wrong) if wrong else 0 correct_accept = sum(r["model_says"] == "correct" for r in correct) / len(correct) if correct else 0 print(f"\n=== A6b Z1 verifier ({MODEL}) — {len(pairs)} pairs ===") print(f"health: {health}; scored: {len(scored)}") print(f"accuracy: {acc:.3f}") print(f"wrong-catch (recall on Z1 mistranslations): {sum(r['model_says']=='wrong' for r in wrong)}/{len(wrong)} = {wrong_catch:.3f}") print(f"correct-accept (specificity): {sum(r['model_says']=='correct' for r in correct)}/{len(correct)} = {correct_accept:.3f}") missed = [r["term"] for r in wrong if r["model_says"] != "wrong"] print(f"Z1 mistranslations the local 9B FAILED to catch: {missed}") json.dump({"model": MODEL, "health": health, "accuracy": acc, "wrong_catch": wrong_catch, "correct_accept": correct_accept, "rows": rows}, open(OUT / "a6b_verify.json", "w"), ensure_ascii=False, indent=1) print(f"[written] {OUT/'a6b_verify.json'}") if __name__ == "__main__": main()