#!/usr/bin/env python3 """exp15 — kimi enrichment of T-ana/T-ell straddles AT flat-A0-30 boundaries (guard 1: kimi ONLY as targeted enrichment on the boundaries, deterministic-first). For each greedy boundary we give kimi the TAIL of chunk_i and the HEAD of chunk_{i+1} and ask ONLY: is there an anaphora (esp. zh zero-subject) or ellipsis in the HEAD whose antecedent/elided element is in the TAIL (i.e. it straddles THIS cut)? Batched (several boundaries per call) to bound kimi cost. Family-isolated miner (Moonshot), temp 1, 24k + retry. Run: python enrich_ana.py --run """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import exp15_llm as L from chunker import greedy_chunks, split_source_sentences SD = Path("/home/ubuntu/books/gu-zhenren/exp15") FLAT = SD / "S2prime_big_flat.txt" OUT = SD / "ana_enrichment.json" LEDGER = SD / "enrich_costs.jsonl" MINER = "kimi-k2.6" BATCH = 6 # boundaries per kimi call TAIL_N, HEAD_N = 3, 3 SYS = ( "Ты — эксперт по китайско-русскому переводу и связности. Тебе дают несколько ГРАНИЦ нарезки текста " "(ранобэ 蛊真人): для каждой — ХВОСТ предыдущего чанка и ГОЛОВА следующего. Для КАЖДОЙ границы найди, " "есть ли в ГОЛОВЕ анафора (особенно китайское НУЛЕВОЕ подлежащее — предложение без явного субъекта, " "субъект тянется из ХВОСТА) ИЛИ эллипсис, чей антецедент/опущенный элемент — в ХВОСТЕ (т.е. зависимость " "ПЕРЕСЕКАЕТ именно эту границу). Если да — верни объект; если нет — пропусти границу.\n" 'Формат (JSON-массив): [{"boundary_idx":ЦЕЛОЕ,"type":"T-ana|T-ell",' '"antecedent_span":"цитата из ХВОСТА","dependent_span":"цитата из ГОЛОВЫ",' '"phenomenon":"какая зависимость и почему разрез её ломает",' '"expected_ru":"что должен сделать верный перевод (референт/род/восстановленное сказуемое)"}]\n' "Только реальные пересекающие границу случаи; цитаты дословные. Без текста вне JSON." ) def boundaries(): text = FLAT.read_text(encoding="utf-8") ch = greedy_chunks(text) out = [] for i in range(len(ch) - 1): tail = "".join(split_source_sentences(ch[i])[-TAIL_N:]).strip() head = "".join(split_source_sentences(ch[i + 1])[:HEAD_N]).strip() out.append({"boundary_idx": i, "tail": tail, "head": head}) return out def parse_array(t): m = re.search(r"\[.*\]", t or "", re.S) if not m: return None try: return json.loads(m.group(0)) except Exception: return None def main(): ap = argparse.ArgumentParser(); ap.add_argument("--run", action="store_true"); a = ap.parse_args() b = boundaries() print(f"{len(b)} boundaries, batch {BATCH} -> {(len(b)+BATCH-1)//BATCH} kimi calls") if not a.run: return sp = L.Spender(str(LEDGER), per_call_cap=0.28, hard_cap=2.5) found = [] NUDGE = {"role": "user", "content": "Не рассуждай долго. Сразу выведи ТОЛЬКО компактный валидный JSON-массив, без markdown-ограды."} for k in range(0, len(b), BATCH): batch = b[k:k + BATCH] user = "\n\n".join(f"ГРАНИЦА {x['boundary_idx']}:\n[ХВОСТ]: {x['tail']}\n[ГОЛОВА]: {x['head']}" for x in batch) msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": user}] in_est = (len(SYS) + len(user)) // 2 ok, pred, why = sp.gate(MINER, in_est, L.MODELS[MINER]["max_tokens"]) print(f"[gate] batch {k//BATCH} (bnd {batch[0]['boundary_idx']}-{batch[-1]['boundary_idx']}) pred=${pred:.4f} {'OK' if ok else why}") if not ok: continue r = L.call(MINER, msgs, temp=1.0, max_tokens=24000, timeout=600) sp.record({"batch": k // BATCH, "usage": r.get("usage"), "cost": r.get("cost", 0.0), "err": r.get("err"), "latency_s": r.get("latency_s")}) cands = parse_array(r.get("text", "")) if cands is None: r2 = L.call(MINER, msgs + [NUDGE], temp=1.0, max_tokens=24000, timeout=600) sp.record({"batch": k // BATCH, "retry": True, "usage": r2.get("usage"), "cost": r2.get("cost", 0.0), "err": r2.get("err")}) cands = parse_array(r2.get("text", "")) if not r2.get("err") else None cands = cands or [] found.extend(cands) with open(str(OUT) + ".partial.jsonl", "a", encoding="utf-8") as pf: pf.write(json.dumps({"batch": k // BATCH, "n": len(cands), "cands": cands}, ensure_ascii=False) + "\n") print(f" batch {k//BATCH}: {len(cands)} straddles cost=${r.get('cost',0):.4f}") from collections import Counter by = Counter(c.get("type") for c in found) OUT.write_text(json.dumps({"n": len(found), "by_type": dict(by), "straddles": found}, ensure_ascii=False, indent=2), encoding="utf-8") print(f"\nenriched straddles: {dict(by)} (total {len(found)}) spend ${sp.total:.4f} -> {OUT}") if __name__ == "__main__": main()