232 lines
13 KiB
Python
232 lines
13 KiB
Python
#!/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__)
|