#!/usr/bin/env python3 """Проба A (промт POLYGON_EDITOR_WIRE_PROBE §A): РЕДАКТОРСКИЙ провод под доктриной закона D39.104. Отличие от inject_probe.py (тот мерил ТРАНСЛЯТОР-провод): здесь роль — РЕДАКТОР, вход байт-близко к edit-стадии шиппинг-конфига c1: messages = [system(editor.md CORE, few_shot ДРОПНУТ), system(банк-блок), user(text+draft)] model=deepseek-v4-pro · temperature=0.4 · max_tokens=16000 · reasoning_effort НЕ СЛАТЬ. Формат строк блока — как editorLines (membank/memory.go:798-829): "- src → «dst»". Армы: A0 (без блока) · A1 (единый закон-блок, ВЕРНАЯ строка) · A2 (тот же блок, НЕВЕРНАЯ строка 元海→«Юаньхай» во ВСЕХ 6 окнах — межоконная консистентность) · AM (метаязыковое окно, закон+оговорка против закона-без-оговорки) · A5 (посаженные дефекты черновика, без блока). Черновики — арм B транслятор-сырья research/24 §C ($0, уже персистированы). Сырьё durable ~/books/gu-zhenren/bank-arbitration/, префикс edp-*. """ from __future__ import annotations import argparse import json import os import re import sys import time import unicodedata from pathlib import Path from dotenv import load_dotenv from openai import OpenAI REPO = Path("/home/ubuntu/projects/textmachine") HERE = Path(__file__).resolve().parent RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration" SRC = Path.home() / "books" / "gu-zhenren" / "coldrun-a" / "guzhenren-ch1-10.gb18030.txt" load_dotenv(REPO / "eval" / ".env") sys.path.insert(0, str(HERE)) from inject_probe import TERMS, pick_windows, norm # noqa: E402 (тот же реестр термов и те же окна) # --- Провод: ровно edit-стадия pipeline-c1.yaml ------------------------------------------------- MODEL = "deepseek-v4-pro" TEMPERATURE = 0.4 # pipeline-c1.yaml stages[edit].temperature MAX_TOKENS = 16000 # models.yaml:166 capabilities.min_max_tokens (флор v4-pro) FEW_SHOT = False # pipeline-c1.yaml stages[edit].few_shot: false → блок ---FEWSHOT--- дропается # reasoning_effort НЕ шлём вовсе: deepseek = ReasoningNone (llm/capability.go:177-180), # reasoning:"off" в конфиге ⇒ ключа на проводе НЕТ ⇒ вендор-дефолт high (quirks 00 §3а). PRICE_IN, PRICE_CACHED, PRICE_OUT = 0.435, 0.003625, 0.87 # models.yaml deepseek-v4-pro HARD_STOP_USD = 0.28 # пре-рег: живой стоп ниже потолка $0.30 EDITOR = REPO / "backend/prompts/zh-ru/editor.md" TRANSLATOR = REPO / "backend/prompts/zh-ru/translator.md" USER_SEP = "\n---USER---\n" FEWSHOT_SEP = "\n---FEWSHOT---\n" # --- §B: кандидат-редакции заголовков (в движок НЕ вносятся) ------------------------------------ # База — действующий editor_header (backend/internal/lang/data/injection.txt строка 7), дословно. EDITOR_HEADER_BASE = ( "КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть " "передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по " "контексту; не вводи иных вариантов и не меняй ничего другого)" ) CLAUSE = ( "; глоссарий обязателен для употреблений термина КАК термина: игру слов, разбор знаков, " "буквальное прочтение передавай по смыслу пассажа, сохраняя узнаваемость канонической формы" ) HEADER_LAW_CLAUSE = EDITOR_HEADER_BASE + CLAUSE + ":" # единый закон-блок §B (A1/A2/AM-clause) HEADER_LAW_PLAIN = EDITOR_HEADER_BASE + ":" # закон без оговорки (AM-noclause) WRONG_TERM = "元海" # единственный терм, присутствующий во ВСЕХ 6 окнах → метрика консистентности AM_PARAS = (517, 522) # метаязыковое окно: 蛊名月光 (имя как имя) + 弯弯如月 (буквальная луна) RENDER_VARS = {"source_lang": "zh", "target_lang": "ru", "genre": "вебновелла", "audience": "взрослые читатели вебновелл", "title": "蛊真人", "venuti": "0.60", "honorifics": "keep", "transcription": "palladius", "footnotes": "minimal"} def strip_comments(s: str) -> str: """Зеркало render.go:99-116 (stripPromptComments): SHA берётся с канонической формы.""" out, rest = [], s while True: i = rest.find("") if j < 0: raise ValueError("unterminated comment") rest = rest[j + 3:] def load_template(path: Path) -> tuple[str, str, str]: """Зеркало render.go LoadPromptTemplate: (core system, fewshot, user).""" canon = strip_comments(path.read_text(encoding="utf-8")) sys_part, user = canon.split(USER_SEP, 1) head = sys_part.split(FEWSHOT_SEP, 1) core = head[0].strip() few = head[1].strip() if len(head) == 2 else "" return core, few, user.strip() def render(tpl: str, text: str, draft: str) -> str: """Зеркало render.go Render: ОДИН проход, значения не пере-сканируются.""" vals = dict(RENDER_VARS, text=text, draft=draft) out, rest = [], tpl while True: i = rest.find("{{") if i < 0: out.append(rest) return "".join(out) out.append(rest[:i]) rest = rest[i:] end = rest.find("}}") if end < 0: raise ValueError("unterminated placeholder") name = rest[2:end] if name not in vals: raise ValueError(f"unknown placeholder {{{{{name}}}}}") out.append(vals[name]) rest = rest[end + 2:] def editor_messages(text: str, draft: str, block: str | None) -> list[dict]: core, few, user_tpl = load_template(EDITOR) system = core + ("\n\n" + few if FEW_SHOT and few else "") # SystemFor(fewShotOn) msgs = [{"role": "system", "content": render(system, text, draft)}] if block and block.strip(): msgs.append({"role": "system", "content": block}) msgs.append({"role": "user", "content": render(user_tpl, text, draft)}) return msgs def editor_block(terms: list[str], header: str, wrong: str | None) -> str: """Формат editorLines (memory.go:815): '- ' + src + ' → «' + dst + '»'.""" lines = [] for t in terms: gold, bad, _, _ = TERMS[t] dst = bad if t == wrong else gold lines.append(f"- {t} → «{dst}»") return header + "\n" + "\n".join(lines) # --- A5: посаженные дефекты черновика (дословный список — во фризе пре-рега) --------------------- # (класс, что заменить в черновике, на что, regex «дефект ещё в выходе», regex «починено по смыслу») DEFECTS = { 0: [("k1", "также поразительна", "также взгрумительна", r"взгрумительн", r"поразительн|порази|впечатл|изумител|поразит"), ("k2", "они не выдержат такого непрерывного расхода", "они легко выдержат такой непрерывный расход", r"легко выдерж", r"не выдерж|не выстоят|не хват|иссяк|не выносят")], 1: [("k1", "сокровище природы", "мреязное сокровище природы", r"мреязн", r"сокровищ"), ("k2", "на глазах неуклонно поднимался", "на глазах неуклонно опускался", r"неуклонно опуска|неуклонно понижа|неуклонно снижа", r"поднима|повыша|нараста|расту|рос\b|привстава|прибыва|подъём|увеличива")], 2: [("k1", "словно натянутая струна", "словно натянутая струмель", r"струмел", r"струн"), ("k2", "Увы, тот так и не показался", "Увы, тот в первую же ночь и показался", r"в первую же ночь и показа", r"так и не показа|не появ|не показыва|не давал себя|не показал")], 3: [("k1", "но при этом необычайно густой", "но при этом необычайно гущавой", r"гущав", r"густ|вязк|плотн"), ("k2", "Фан Юань, не оборачиваясь, развернулся и пошёл обратно", "Фан Юань обернулся и остался стоять на месте", r"остал\w{0,3} стоять на месте", r"развернул|поверн\w+ (назад|обратно)|пошёл обратно|двинул\w* обратно|побрёл обратно|пошел обратно")], 4: [("k1", "волна дерзновенной отваги", "волна дерзновяжной отваги", r"дерзновяжн", r"дерзнов|отваг|воодушевл|решимост"), ("k2", "была вполне приемлемой платой", "была совершенно недопустимой утратой", r"недопустим\w* утрат", r"приемлем|допустим|не столь велик|стоила того|можно смириться|терпим")], 5: [("k1", "оттенок суровой удали", "оттенок суровяглой удали", r"суровягл", r"суров|удал|лих|дерзк"), ("k2", "он наконец не смог идти дальше", "он зашагал ещё быстрее", r"зашагал ещ[её] быстрее", r"не смог идти|не мог идти|остановил|не в силах|дальше не|встал\b|замер")], } def planted_draft(k: int, draft: str) -> tuple[str, list]: out = draft for cls, old, new, bad_rx, fix_rx in DEFECTS[k]: if old not in out: raise SystemExit(f"w{k}: посадка не нашла якорь {old!r} в черновике") out = out.replace(old, new, 1) return out, DEFECTS[k] # --- окна --------------------------------------------------------------------------------------- def draft_of(k: int) -> str: return json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))["content"] def am_window() -> tuple[str, list[str]]: paras = [p for p in SRC.read_text(encoding="gb18030").split("\n") if p.strip()] buf = "\n".join(paras[AM_PARAS[0]:AM_PARAS[1]]) + "\n" terms = [t for t in TERMS if t in buf] return buf, terms # --- вызовы ------------------------------------------------------------------------------------- def client() -> OpenAI: return OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", timeout=900) def call(cl, msgs, tag, model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, extra=None, prices=(PRICE_IN, PRICE_CACHED, PRICE_OUT)) -> dict: kw = dict(model=model, messages=msgs, max_tokens=max_tokens, temperature=temperature) if extra: kw["extra_body"] = extra t0 = time.time() r = cl.chat.completions.create(**kw) ch = r.choices[0] u = r.usage pt, ct = u.prompt_tokens or 0, u.completion_tokens or 0 cached = getattr(getattr(u, "prompt_tokens_details", None), "cached_tokens", 0) or 0 pin, pcache, pout = prices cost = (pt - cached) / 1e6 * pin + cached / 1e6 * pcache + ct / 1e6 * pout rec = dict(tag=tag, model=model, model_returned=r.model, finish=ch.finish_reason, ts=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), temperature=temperature, max_tokens=max_tokens, extra_body=extra or {}, prompt_tokens=pt, cached_tokens=cached, completion_tokens=ct, reasoning_chars=len(getattr(ch.message, "reasoning_content", None) or ""), cost_usd=round(cost, 6), latency_s=round(time.time() - t0, 1), messages=msgs, content=ch.message.content or "") (RAW / f"{tag}.json").write_text(json.dumps(rec, ensure_ascii=False, indent=1), encoding="utf-8") print(f"[{tag}] finish={ch.finish_reason} in={pt}(c{cached}) out={ct} " f"${cost:.6f} {rec['latency_s']}s content={len(rec['content'])}c") return rec def slug_check() -> None: cl = client() ids = [m.id for m in cl.models.list().data] print(f"/models deepseek: {ids}") if MODEL not in ids: raise SystemExit(f"слаг {MODEL} НЕ листится — СТОП") def build_arms(am_draft: str | None): """Полный план вызовов: [(tag, messages, note)].""" wins = pick_windows() plan = [] for k, (i, buf, terms) in enumerate(wins): d = draft_of(k) plan.append((f"edp-A0-w{k}", editor_messages(buf, d, None), "без блока")) for k, (i, buf, terms) in enumerate(wins): d = draft_of(k) plan.append((f"edp-A1-w{k}", editor_messages(buf, d, editor_block(terms, HEADER_LAW_CLAUSE, None)), "закон+оговорка, ВЕРНАЯ")) for k, (i, buf, terms) in enumerate(wins): d = draft_of(k) plan.append((f"edp-A2-w{k}", editor_messages(buf, d, editor_block(terms, HEADER_LAW_CLAUSE, WRONG_TERM)), f"закон+оговорка, НЕВЕРНАЯ {WRONG_TERM}")) if am_draft is not None: buf, terms = am_window() plan.append(("edp-AM-clause", editor_messages(buf, am_draft, editor_block(terms, HEADER_LAW_CLAUSE, None)), "метаязык: закон+оговорка")) plan.append(("edp-AM-noclause", editor_messages(buf, am_draft, editor_block(terms, HEADER_LAW_PLAIN, None)), "метаязык: закон БЕЗ оговорки")) for k, (i, buf, terms) in enumerate(wins): d, _ = planted_draft(k, draft_of(k)) plan.append((f"edp-A5-w{k}", editor_messages(buf, d, None), "посаженные дефекты, без блока")) return plan def cmd_plan(): """Смета ДО первого платного вызова. Токены — калибровка cl100k против фактических prompt_tokens сырья §C (печатается), выход — полоса от содержательного до потолка.""" import tiktoken enc = tiktoken.get_encoding("cl100k_base") # --- калибровка на фактическом сырье §C (транслятор-армы, prompt_tokens известны) --- core_t, few_t, user_t = load_template(TRANSLATOR) ratios = [] for k, (i, buf, terms) in enumerate(pick_windows()): rec = json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8")) s = render(core_t, buf, "") + "\n" + rec["injection"] + "\n" + rec["user"] ratios.append(rec["prompt_tokens"] / len(enc.encode(s))) calib = sum(ratios) / len(ratios) print(f"калибровка cl100k→deepseek prompt_tokens: ×{calib:.3f} (по 6 вызовам §C, разброс " f"{min(ratios):.3f}..{max(ratios):.3f})") plan = build_arms(am_draft="ЗАГЛУШКА " * 200) tot_in = 0 for tag, msgs, note in plan: n = int(len(enc.encode("\n".join(m["content"] for m in msgs))) * calib) tot_in += n print(f" {tag:18s} in≈{n:5d} ток. — {note}") # выход: нижняя оценка — только содержательный текст (≈ длина черновика), верхняя — потолок lo_out = 26 * 900 mid_out = 26 * 4000 hi_out = 26 * MAX_TOKENS def money(o): return tot_in / 1e6 * PRICE_IN + o / 1e6 * PRICE_OUT print(f"\nвсего вход ≈{tot_in} ток. → ${tot_in/1e6*PRICE_IN:.4f}") print(f"выход: нижняя {lo_out} ток. ${money(lo_out):.4f} · ожидаемая {mid_out} ток. " f"${money(mid_out):.4f} · АБСОЛЮТНЫЙ ПОТОЛОК {hi_out} ток. ${money(hi_out):.4f}") print(f"ПОТОЛОК ПРОБЫ $0.30; живой стоп харнесса ${HARD_STOP_USD} (проверяется ПЕРЕД каждым вызовом)") print(f"+ 1 транслятор-вызов (черновик AM-окна, deepseek-v4-flash low) ≈ $0.002") def cmd_amdraft(): """Черновик метаязыкового окна тем же проводом, что арм B §C (flash, low, temp 0, верный блок).""" from inject_probe import render_translator, block_for buf, terms = am_window() system, user = render_translator(buf) inj = block_for(terms, wrong=None, marked=False) cl = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", timeout=600) msgs = [{"role": "system", "content": system}, {"role": "system", "content": inj}, {"role": "user", "content": user}] call(cl, msgs, "edp-AMdraft", model="deepseek-v4-flash", temperature=0, max_tokens=16000, extra={"reasoning_effort": "low"}, prices=(0.14, 0.0028, 0.28)) def cmd_run(only: list[str]): am = None f = RAW / "edp-AMdraft.json" if f.exists(): am = json.load(open(f, encoding="utf-8"))["content"] planted = RAW / "edp-AMdraft-planted.txt" if planted.exists(): # пре-рег ветка «черновик канонизировал» → посадка объявлена am = planted.read_text(encoding="utf-8") plan = [p for p in build_arms(am) if not only or any(p[0].startswith(f"edp-{o}") for o in only)] cl = client() total = 0.0 for tag, msgs, note in plan: if (RAW / f"{tag}.json").exists(): print(f"[{tag}] уже персистирован — пропуск") continue if total >= HARD_STOP_USD: print(f"СТОП: накоплено ${total:.4f} ≥ ${HARD_STOP_USD}") break rec = call(cl, msgs, tag) total += rec["cost_usd"] time.sleep(0.5) print(f"TOTAL этого запуска: ${total:.6f}") # --- скоринг ------------------------------------------------------------------------------------ def hits(out_norm: str, term: str) -> tuple[bool, bool]: """gold/wrong по regex харнесса. ⚠ РАЗВЯЗКА КОНФАУНДА (оговорка приёмки research/24 (а)): голд-rx 真元 (r'истинн\\w* ци') матчится ВНУТРИ верного перевода 元海 «море истинной ци». Перед поиском 真元 вырезаем все вхождения голд-формы 元海.""" gold, bad, grx, brx = TERMS[term] hay = out_norm if term == "真元": hay = re.sub(TERMS["元海"][2], " ", hay) return bool(re.search(grx, hay)), bool(re.search(brx, out_norm)) def out_of(tag: str) -> str | None: f = RAW / f"{tag}.json" if not f.exists(): return None return norm(json.load(open(f, encoding="utf-8"))["content"]) def cmd_score(): wins = pick_windows() print("=== A0/A1/A2: следование закону по ВЕРНЫМ строкам блока (терм ≠ 元海) ===") per_arm = {} for arm in ("A0", "A1", "A2"): g = n = 0 detail = [] for k, (i, buf, terms) in enumerate(wins): o = out_of(f"edp-{arm}-w{k}") if o is None: continue for t in terms: if t == WRONG_TERM: continue gh, wh = hits(o, t) g += gh n += 1 detail.append((k, t, gh, wh)) per_arm[arm] = (g, n, detail) print(f" арм {arm}: {g}/{n} канонических форм в выходе") print("\n=== строка 元海 (в A2 инъецирована НЕВЕРНОЙ «Юаньхай») ===") for arm in ("A0", "A1", "A2"): rows = [] for k, (i, buf, terms) in enumerate(wins): o = out_of(f"edp-{arm}-w{k}") if o is None: continue gh, wh = hits(o, WRONG_TERM) rows.append((k, gh, wh)) gold_n = sum(1 for _, gh, _ in rows if gh) wrong_n = sum(1 for _, _, wh in rows if wh) print(f" арм {arm}: gold {gold_n}/{len(rows)} · wrong {wrong_n}/{len(rows)} " + " ".join(f"w{k}:(g={int(gh)},w={int(wh)})" for k, gh, wh in rows)) print("\n консистентность A2 (одна строка, 6 окон): " + describe_consistency([(k,) + hits(out_of(f"edp-A2-w{k}"), WRONG_TERM) for k in range(6) if out_of(f"edp-A2-w{k}") is not None])) print("\n=== A5: фикс-рейт посаженных дефектов (арм без блока) ===") for cls in ("k1", "k2"): fixed = tot = 0 rows = [] for k in range(6): o = out_of(f"edp-A5-w{k}") if o is None: continue for c, old, new, bad_rx, fix_rx in DEFECTS[k]: if c != cls: continue still = bool(re.search(bad_rx, o)) restored = bool(re.search(fix_rx, o)) tot += 1 fixed += (not still) rows.append(f"w{k}:{'FIX' if not still else 'ЖИВ'}{'/восст' if restored else ''}") print(f" класс {cls}: исправлено {fixed}/{tot} " + " ".join(rows)) print("\n=== AM: метаязыковое окно ===") for tag in ("edp-AM-clause", "edp-AM-noclause"): f = RAW / f"{tag}.json" if not f.exists(): continue c = json.load(open(f, encoding="utf-8"))["content"] print(f"--- {tag} ---\n{c}\n") def cmd_eyes(): """Глазная сверка A5: по каждой из 12 посадок печатает посаженную фразу и ОКРЕСТНОСТЬ в выходе редактора. Пре-рег: при расхождении с regex авторитет — глаз.""" for k in range(6): f = RAW / f"edp-A5-w{k}.json" if not f.exists(): continue out = json.load(open(f, encoding="utf-8"))["content"] print(f"\n########## w{k} ##########") for cls, old, new, bad_rx, fix_rx in DEFECTS[k]: n = norm(out) still = bool(re.search(bad_rx, n)) restored = bool(re.search(fix_rx, n)) print(f"--- {cls}: посажено {new!r}") print(f" было в черновике: {old!r}") print(f" regex: дефект_жив={still} смысл_восстановлен={restored}") # окрестность: ищем по якорному слову посадки и по якорю оригинала anchors = [w for w in re.split(r"\W+", new) if len(w) > 4][:4] shown = set() for a in anchors: for m in re.finditer(re.escape(a), out, re.I): s = max(0, m.start() - 120) frag = out[s:m.end() + 120].replace("\n", " ") if frag not in shown: shown.add(frag) print(f" …{frag}…") break if not shown: print(" (якорей посадки в выходе нет — фрагмент переписан целиком)") def describe_consistency(rows) -> str: if not rows: return "нет данных" accepted = sum(1 for _, gh, wh in rows if wh) displaced = sum(1 for _, gh, wh in rows if wh and not gh) return (f"неверная форма принята в {accepted}/{len(rows)} окнах, из них полностью вытеснила " f"верную в {displaced}/{len(rows)}; единогласие " f"{max(accepted, len(rows) - accepted)}/{len(rows)}") def cmd_selfcheck(): """Ре-ран §C по персистированному сырью: (1) воспроизводим таблицу research/24 §C ТЕМ ЖЕ методом, (2) показываем эффект развязки конфаунда 真元/元海.""" wins = pick_windows() from inject_probe import WRONG_PRIORITY for fix in (False, True): print(f"--- конфаунд {'РАЗВЯЗАН' if fix else 'как в §C'} ---") for arm in "ABCD": g = n = 0 wg = wn = 0 for k, (i, buf, terms) in enumerate(wins): cand = [t for t in WRONG_PRIORITY if t in terms] wrong = cand[k % len(cand)] f = RAW / f"inj-w{k}-{arm}.json" if not f.exists(): continue o = norm(json.load(open(f, encoding="utf-8"))["content"]) for t in terms: gold, bad, grx, brx = TERMS[t] hay = o if fix and t == "真元": hay = re.sub(TERMS["元海"][2], " ", hay) gh, wh = bool(re.search(grx, hay)), bool(re.search(brx, o)) if t == wrong: wn += 1 wg += wh else: n += 1 g += gh print(f" арм {arm}: верные строки {g}/{n} · неверная принята {wg}/{wn}") def main(): ap = argparse.ArgumentParser() ap.add_argument("--plan", action="store_true", help="смета, без вызовов") ap.add_argument("--slugcheck", action="store_true") ap.add_argument("--amdraft", action="store_true", help="1 транслятор-вызов: черновик AM-окна") ap.add_argument("--run", nargs="*", default=None, help="A0 A1 A2 AM A5 (пусто = все)") ap.add_argument("--score", action="store_true") ap.add_argument("--selfcheck", action="store_true", help="ре-ран §C по сырью, $0") ap.add_argument("--eyes", action="store_true", help="глазная сверка 12 посадок A5") ap.add_argument("--dump", help="напечатать messages одного тега (байт-сверка провода)") a = ap.parse_args() if a.plan: cmd_plan() elif a.slugcheck: slug_check() elif a.amdraft: cmd_amdraft() elif a.run is not None: cmd_run(a.run) elif a.score: cmd_score() elif a.selfcheck: cmd_selfcheck() elif a.eyes: cmd_eyes() elif a.dump: am = None f = RAW / "edp-AMdraft.json" if f.exists(): am = json.load(open(f, encoding="utf-8"))["content"] for tag, msgs, note in build_arms(am or "ЧЕРНОВИК-ЗАГЛУШКА"): if tag == a.dump: for m in msgs: print(f"----- role={m['role']} ({len(m['content'])} симв) -----") print(m["content"]) else: ap.print_help() if __name__ == "__main__": main()