#!/usr/bin/env python3 """Ф3 — ПАНЕЛЬ РЕДАКТОРОВ поверх ОДНОЙ якорной базы. База — ЗАМОРОЖЕННЫЙ боевой черновик `deepseek-v4-flash` из Ф2 (тот самый, что прошёл гейт эха). Все редакторы правят побайтно один текст, поэтому контраст между ними чист от разницы черновиков — ровно того конфаунда, который эксп-21 назвал носителем 70% эффекта. Конфигурация редактора одинакова у всех: full-regen боевым `editor.md` + ЗАКОН-БЛОК (D39.104). Единственное различие клеток — модель. Это проверяется `--isolation` до покупок. `gemini-3.1-pro-preview` — ОБЯЗАТЕЛЬНЫЙ кандидат промта: он идёт в Ф3 независимо от скрина (гипотеза эксп-04 «gemini-pro — аутлаер прозы»), скрин-числа при нём печатаются. Вторая база (лучший черновик Ф2) — ОПЦИЯ промта: берётся только если Ф2 дал отрыв выше пола И смета влезает; решение и его основание печатаются `--plan`. Запуск: --plan состав панели, смета, изоляция, $0 --isolation чем клетки различаются — сверка сообщений, $0 --run покупка редактур --score батарея, канон, цена, $0 """ from __future__ import annotations import json import os import statistics as st import sys import time from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") ZONE = Path(__file__).resolve().parent sys.path.insert(0, str(ZONE)) sys.path.insert(0, str(REPO / "eval" / "role_topology")) from dotenv import load_dotenv # noqa: E402 from openai import OpenAI # noqa: E402 import bank as BANK # noqa: E402 import battery as B # noqa: E402 import material as M # noqa: E402 import money as MONEY # noqa: E402 import panel as P2 # noqa: E402 import prompts as PR # noqa: E402 from roster import CANDIDATES, call_kwargs # noqa: E402 load_dotenv(REPO / "eval" / ".env") OUT = MONEY.OUT BATTLE = "deepseek-v4-pro" # R0 — боевой редактор, база сравнения MANDATORY = "gemini-3.1-pro-preview" # обязательный кандидат промта MAX_EDITORS = 5 # ⚠ ПЕР-МОДЕЛЬНЫЙ ТАЙМАУТ И ЗАПРЕТ SDK-РЕТРАЕВ. Поймано исполнением: `gemini-3.1-pro-preview` # держал ОДИН вызов 70.8 минуты без ошибки — таймаут 900 с умножался на внутренние ретраи # клиента, и клетка не закрывалась вовсе (за то же время он отдал 1 клетку из 16). Это тот же # класс, что его серия HTTP 503 на скрине: модель в этой роли на проводе не работоспособна. # Здесь она получает жёсткую границу, чтобы отказ БЫЛ ИЗМЕРЕН, а не съел прогон. SLOW = {"gemini-3.1-pro-preview": 300} # ⚠ ПОДМНОЖЕСТВО ЕДИНИЦ ДЛЯ МЕДЛЕННОЙ МОДЕЛИ, объявлено ДО покупки: первые 8 по стабильному # порядку `units_zh`. Основание — замер, а не удобство: при наблюдённой скорости 16 клеток # недостижимы, а 8 дают парную статистику с честно объявленной мощностью. SLOW_UNITS = 8 def client(model: str) -> OpenAI: base, env, *_ = CANDIDATES[model] return OpenAI(api_key=os.environ[env], base_url=base, timeout=SLOW.get(model, 900), max_retries=0) RESERVE = "glm-5" # ратифицированный резерв D39.22 = арм B эксп-21 def panel() -> list[str]: """Состав панели Ф3 по ОБЪЯВЛЕННОМУ (во фризе Ф3, ДО покупок) правилу. R0 боевой редактор — база всех контрастов; R1 обязательный кандидат промта (гипотеза эксп-04 «gemini-pro — аутлаер прозы»); R2 резерв эксп-21 `glm-5` — ПРЕЕМСТВЕННОСТЬ: только он позволяет пере-снять контраст `A_law/B` на неконтаминированном материале, а без него эксп-22 не связан с эксп-21 ничем; R3 самый ДЕШЁВЫЙ прошедший редактор — отвечает «хватает ли дешёвого»; R4 самый ДОРОГОЙ прошедший — отвечает «покупает ли дорогой качество». Порядок фиксирован: имена армов R0..R4 впечатываются в ключ судейского прохода, и сдвиг порядка после выпуска заданий сделал бы семейство неповторимым. """ res = P2.screen_result() out = [BATTLE, MANDATORY, RESERVE] ok = [m for m, r in res.items() if r["verdict"] == "прошёл" and m not in out and r.get("price_editor") is not None] ok.sort(key=lambda m: res[m]["price_editor"]) for m in ([ok[0]] if ok else []) + ([ok[-1]] if len(ok) > 1 else []): if m not in out: out.append(m) return out[:MAX_EDITORS] def base_text(uid: str) -> str: """Якорная база: замороженный боевой черновик Ф2.""" return P2.draft_text(P2.ANCHOR, uid) def tag(model: str, uid: str) -> str: return f"tp3-ed-{model}-{uid}" def text_of(arm: str, uid: str) -> str: """Текст клетки Ф3. ⚠ R0 (боевой редактор) НЕ покупается заново: его клетка над якорным черновиком уже куплена в Ф2 (`tp2-edit`), и повторная покупка была бы вторым замером той же вещи. Промт это и предписывает: «клетка dspro-над-flash покупается в Ф2 ОДИН раз».""" if arm == "F": return base_text(uid) model = panel()[int(arm[1:])] if model == BATTLE: return P2.edit_text(P2.ANCHOR, uid) f = OUT / f"{tag(model, uid)}.json" return json.loads(f.read_text(encoding="utf-8")).get("content", "") if f.exists() else "" def msgs_for(model: str, u: dict) -> list[dict]: return PR.editor_msgs(u["source"], base_text(u["uid"]), PR.law_block(PR.terms_in(u["source"]))) def cmd_isolation() -> None: """Клетки обязаны различаться ТОЛЬКО моделью. Проверяется сборкой сообщений, а не верой.""" u = M.units_zh()[0] ms = {m: msgs_for(m, u) for m in panel()} ref = ms[BATTLE] bad = 0 for m, v in ms.items(): same = [a["content"] == b["content"] for a, b in zip(ref, v, strict=True)] ok = all(same) and len(ref) == len(v) bad += not ok print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {m:26s} сообщения совпадают с базой " f"({len(v)} блоков)") print(f" закон-блок в сообщениях: {'да' if any('КАНОНИЧЕСКИЕ ПЕРЕВОДЫ' in x['content'] for x in ref) else 'НЕТ'}") print(f" база — замороженный черновик {P2.ANCHOR}, {len(base_text(u['uid']))} знаков") print(f"\n{'ИЗОЛЯЦИЯ ЧИСТА' if not bad else f'ПРОВАЛОВ: {bad}'}") def cmd_plan() -> None: pan, res, us = panel(), P2.screen_result(), M.units_zh() print(f"ПАНЕЛЬ Ф3: {len(pan)} редакторов × {len(us)} единиц поверх ОДНОЙ якорной базы") est = 0.0 for i, m in enumerate(pan): r = res.get(m, {}) price = r.get("price_editor") # ⚠ У модели, чьи редакторские клетки скрина не состоялись (503 провайдера), цены НЕТ. # Подставлять сюда общий дефолт нельзя: `gemini-3.1-pro-preview` дороже него в семь раз, # и смета фазы вышла бы заниженной втрое. Берём оценку кассы по ЭТОЙ модели. est_price = price if price is not None else MONEY.model_expect(m) free = " (куплено в Ф2, переиспользуется)" if m == BATTLE else "" if m != BATTLE: est += est_price * len(us) print(f" R{i} {m:26s} скрин={r.get('verdict', 'не скринился'):12s} " f"$/ед={est_price:.5f}{'' if price is not None else ' (оценка кассы)'}{free}") print(f"\nсмета ${est:.4f} при потолке ${MONEY.CEILINGS['Ф3']:.2f}") if est > MONEY.CEILINGS["Ф3"]: print("⛔ СМЕТА НЕ ВЛЕЗАЕТ — сократить панель или пинг владельцу ДО покупок") def cmd_run(argv: list[str] | None = None) -> None: """`--only=<модель>` разводит процессы по клеткам. Без него параллельные процессы идут по панели в ОДНОМ порядке и упираются в один замок: денег это не тратит, но и не ускоряет.""" argv = argv or [] only = [a.split("=", 1)[1] for a in argv if a.startswith("--only=")] led = MONEY.Guarded("Ф3", default_expect=0.03) for m in (x for x in panel() if not only or x in only): if m == BATTLE: print(f" {m}: клетка куплена в Ф2 (tp2-edit) — не перекупается") continue us = M.units_zh()[:SLOW_UNITS] if m in SLOW else M.units_zh() for u in us: if not base_text(u["uid"]): print(f" ⚠ {u['uid'][:6]}: якорной базы нет — клетка пропущена") continue rec = MONEY.purchase(led, tag(m, u["uid"]), m, client(m), call_kwargs(m, msgs_for(m, u)), role="editor3", uid=u["uid"], editor=m) if rec.get("skipped"): print("⛔ потолок Ф3 — стоп") return print(f" {m:26s} {u['uid'][:6]} finish={str(rec.get('finish')):9s} " f"{len(rec.get('content', '')):5d} зн ${rec.get('cost_usd', 0):.5f}") time.sleep(0.15) print(f"потрачено Ф3 ${led.spent():.6f}") def cmd_score() -> None: pan, us, cards = panel(), M.units_zh(), B.load_cards() print(f"{'арм':30s}{'ед.':>5s}{'ханьцзы':>9s}{'типогр':>8s}{'канон':>14s}" f"{'покрытие':>10s}{'$/ед':>10s}") for i, m in enumerate(pan + ["БАЗА"]): rows, k, s, costs = [], 0, 0, [] for u in us: t = base_text(u["uid"]) if m == "БАЗА" else text_of(f"R{i}", u["uid"]) if not t.strip(): continue rows.append(B.run_unit(B.Unit(source=u["source"], draft=t, final=t, cards=cards))) kk, ss = BANK.coverage(u["source"], t) k += kk s += ss if m == "БАЗА": costs.append(P2.draft_cost(P2.ANCHOR, u["uid"])) elif m == BATTLE: f = OUT / f"{P2.tag_edit(P2.ANCHOR, u['uid'])}.json" if f.exists(): costs.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)) else: f = OUT / f"{tag(m, u['uid'])}.json" if f.exists(): costs.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)) if not rows: continue label = "БАЗА (черновик)" if m == "БАЗА" else f"R{i} {m}" print(f"{label:30s}{len(rows):5d}" f"{sum(r['cjk_leak']['han_chars'] for r in rows):9d}" f"{sum(r['typography']['violations'] for r in rows):8d}" f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}" f"{(st.median(costs) if costs else 0):>10.5f}") led = MONEY.Guarded("Ф3") print(f"\nпотрачено Ф3 ${led.spent():.6f} из ${led.ceiling:.2f}") if __name__ == "__main__": a = sys.argv[1:] or ["--plan"] fn = {"--plan": cmd_plan, "--isolation": cmd_isolation, "--run": lambda: cmd_run(a), "--score": cmd_score}.get(a[0]) fn() if fn else print(__doc__)