348 lines
19 KiB
Python
348 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""Ф2 — ПАНЕЛЬ ЧЕРНОВИКОВ. Главный рычаг пака.
|
||
|
||
Почему именно эта роль. Эксп-21 §16.2: тот же редактор над ДРУГИМ черновиком той же длины даёт
|
||
7.66 ошибки против 5.91 — это ~70% заголовочного эффекта топологии. Черновиком за всю историю
|
||
проекта была одна модель. Самая чувствительная роль ни разу не перебиралась.
|
||
|
||
Что меряется на КАЖДОМ жильце (заказ промта, стр.55):
|
||
(а) черновик как ТЕКСТ — абсолютный агент-суд (`judge.py`, проход `p2zh`/`p2en`);
|
||
(б) боевой редактор `A_law`-конфига ПОВЕРХ каждого черновика — доезжает ли качество до финала;
|
||
(в) частота эхо-мины — доля единиц, потребовавших ре-гена;
|
||
(г) батарея + покрытие канона (детерминированно, $0);
|
||
(д) цена/единица и проекция на книгу.
|
||
|
||
ШУМОВОЙ ПОЛ. Один арм гоняется ДВАЖДЫ end-to-end на тех же единицах — выбран ЯКОРНЫЙ
|
||
(`deepseek-v4-flash` → `deepseek-v4-pro`), потому что именно с ним сравниваются все перевесы, и
|
||
пол обязан быть измерен на боевой паре, а не на удобной. Пол применяется к Ф2 И Ф3.
|
||
|
||
Черновики ФРИЗЯТСЯ: Ф3 правит замороженный якорный черновик, клетка «dspro над flash» покупается
|
||
здесь ОДИН раз и в Ф3 переиспользуется.
|
||
|
||
Запуск: --plan состав панели и смета, $0
|
||
--drafts черновики всех жильцов (zh + en)
|
||
--edit боевой редактор поверх каждого черновика
|
||
--floor вторая генерация якорного арма end-to-end
|
||
--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 bakeoff as BO # 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 prompts as PR # noqa: E402
|
||
from roster import CANDIDATES, call_kwargs # noqa: E402
|
||
|
||
load_dotenv(REPO / "eval" / ".env")
|
||
OUT = MONEY.OUT
|
||
|
||
ANCHOR = "deepseek-v4-flash" # боевой черновик — якорь панели
|
||
EDITOR = "deepseek-v4-pro" # боевой редактор, конфиг `A_law` (full-regen + закон-блок)
|
||
MAX_TENANTS = 5 # промт: «до 5 жильцов черновика + `deepseek-v4-flash`
|
||
# (якорь)» ⇒ пять СВЕРХ якоря, панель ≤6 клеток
|
||
RETRIES = 3
|
||
|
||
|
||
def client(model: str) -> OpenAI:
|
||
base, env, *_ = CANDIDATES[model]
|
||
return OpenAI(api_key=os.environ[env], base_url=base, timeout=900)
|
||
|
||
|
||
def screen_result() -> dict:
|
||
f = OUT / "screen.json"
|
||
return json.loads(f.read_text(encoding="utf-8")) if f.exists() else {}
|
||
|
||
|
||
def tenants() -> list[str]:
|
||
"""Состав панели по ОБЪЯВЛЕННОМУ правилу (пре-рег §0.6).
|
||
|
||
Берутся прошедшие скрин кандидаты черновой роли. Если смета не влезает — панель режется так,
|
||
чтобы СОХРАНИТЬ ЦЕНОВУЮ ОСЬ: обязательны якорь, самый дешёвый прошедший и самый дорогой
|
||
прошедший; середина добирается по возрастанию цены. Причина: вопрос пака — «покупает ли
|
||
дорогой черновик качество», и панель, срезанная по цене сверху, на него не отвечает.
|
||
"""
|
||
res = screen_result()
|
||
ok = [m for m, r in res.items()
|
||
if r["verdict"] == "прошёл" and r.get("price_translator") is not None]
|
||
ok.sort(key=lambda m: res[m]["price_translator"])
|
||
if ANCHOR in ok:
|
||
ok.remove(ANCHOR)
|
||
if len(ok) <= MAX_TENANTS:
|
||
return [ANCHOR, *ok]
|
||
keep = [ok[0], ok[-1]]
|
||
for m in ok[1:-1]:
|
||
if len(keep) >= MAX_TENANTS:
|
||
break
|
||
keep.append(m)
|
||
return [ANCHOR, *sorted(keep, key=lambda m: res[m]["price_translator"])]
|
||
|
||
|
||
def units_all(part: str = "") -> list[tuple[dict, bool]]:
|
||
"""(единица, en?) — обе оси обязательны.
|
||
|
||
`part="i/n"` режет СПИСОК единиц на n равных долей для параллельных процессов. Деление
|
||
детерминированное (по позиции в уже отсортированном списке), поэтому доли не пересекаются и
|
||
ни одна единица не теряется: это проверяется `--plan`, который печатает сумму долей.
|
||
"""
|
||
us = [(u, False) for u in M.units_zh()] + [(u, True) for u in M.units_en()]
|
||
if not part:
|
||
return us
|
||
i, n = (int(x) for x in part.split("/"))
|
||
return [x for k, x in enumerate(us) if k % n == i]
|
||
|
||
|
||
# ⚠ ПОЛ КЛАДЁТСЯ ОТДЕЛЬНОЙ ПАРОЙ, а не «боевая клетка + повтор». Иначе одна половина пола
|
||
# побайтно совпала бы с армом `E0` в том же судейском пакете: судья увидел бы один текст дважды,
|
||
# а контроль «близнец» сработал бы на боевом арме. Эксп-21 обошёл это тем, что его пол лежал на
|
||
# ДРУГОМ входе (§build_unit), — здесь то же достигается двумя независимыми прогонами `p1`/`p2`.
|
||
def tag_draft(model: str, uid: str, attempt: int = 1, floor: int = 0) -> str:
|
||
base = f"tp2-floor-p{floor}" if floor else "tp2-draft"
|
||
return f"{base}-D-{model}-{uid}" + ("" if attempt == 1 else f"-r{attempt}")
|
||
|
||
|
||
def tag_edit(model: str, uid: str, floor: int = 0) -> str:
|
||
return f"{f'tp2-floor-p{floor}' if floor else 'tp2-edit'}-E-{model}-{uid}"
|
||
|
||
|
||
def draft_text(model: str, uid: str, floor: int = 0) -> str:
|
||
"""ГОДНЫЙ черновик: последняя попытка, прошедшая гейт. Пусто — годного нет."""
|
||
pre = tag_draft(model, uid, floor=floor)
|
||
best = ""
|
||
for f in sorted(OUT.glob(f"{pre}*.json")):
|
||
c = json.loads(f.read_text(encoding="utf-8")).get("content", "")
|
||
if c and not BO.draft_reject_reason(c):
|
||
best = c
|
||
return best
|
||
|
||
|
||
def edit_text(model: str, uid: str, floor: int = 0) -> str:
|
||
f = OUT / f"{tag_edit(model, uid, floor)}.json"
|
||
return json.loads(f.read_text(encoding="utf-8")).get("content", "") if f.exists() else ""
|
||
|
||
|
||
def edit_ok(model: str, uid: str, floor: int = 0) -> bool:
|
||
"""Клетка редактора ГОДНА: вендор закончил сам, а не упёрся в потолок вывода.
|
||
|
||
⛔ Заведено блокером Б8 ревью 21.08: `edit_text` отдаёт `content` независимо от `finish`, и
|
||
оборванная клетка боевой связки уехала армом `A0` в шесть судейских пачек. Потребители текста
|
||
обязаны спрашивать годность ОТДЕЛЬНО — менять контракт `edit_text` нельзя, на нём висят паки,
|
||
которым нужен текст как есть.
|
||
"""
|
||
f = OUT / f"{tag_edit(model, uid, floor)}.json"
|
||
if not f.exists():
|
||
return False
|
||
return json.loads(f.read_text(encoding="utf-8")).get("finish") == "stop"
|
||
|
||
|
||
def buy_draft(led, model: str, u: dict, en: bool, floor: int = 0) -> dict:
|
||
blk = None if en else PR.glossary_block(PR.terms_in(u["source"]))
|
||
msgs = PR.translator_msgs(u["source"], blk, en=en)
|
||
rec = {}
|
||
for attempt in range(1, RETRIES + 1):
|
||
rec = MONEY.purchase(led, tag_draft(model, u["uid"], attempt, floor), model, client(model),
|
||
call_kwargs(model, msgs), role="draft", uid=u["uid"], en=en,
|
||
tenant=model, attempt=attempt, floor=floor)
|
||
if rec.get("skipped") or not BO.draft_reject_reason(rec.get("content", "")):
|
||
return rec
|
||
print(f" ⚠ {BO.draft_reject_reason(rec['content'])} — ре-ген {attempt + 1}")
|
||
return rec
|
||
|
||
|
||
def buy_edit(led, model: str, u: dict, en: bool, floor: int = 0) -> dict:
|
||
draft = draft_text(model, u["uid"], floor)
|
||
if not draft:
|
||
return dict(skipped=False, content="", no_draft=True)
|
||
blk = None if en else PR.law_block(PR.terms_in(u["source"]))
|
||
msgs = PR.editor_msgs(u["source"], draft, blk, en=en)
|
||
return MONEY.purchase(led, tag_edit(model, u["uid"], floor), EDITOR, client(EDITOR),
|
||
call_kwargs(EDITOR, msgs), role="edit", uid=u["uid"], en=en,
|
||
tenant=model, floor=floor)
|
||
|
||
|
||
def cmd_plan() -> None:
|
||
ts = tenants()
|
||
res = screen_result()
|
||
us = units_all()
|
||
print(f"ПАНЕЛЬ Ф2: {len(ts)} жильцов черновой роли · единиц {len(us)} "
|
||
f"({sum(1 for _u, e in us if not e)} zh + {sum(1 for _u, e in us if e)} en)")
|
||
for m in ts:
|
||
r = res.get(m, {})
|
||
print(f" {m:26s} скрин={r.get('verdict', '—'):10s} $/ед transl="
|
||
f"{r.get('price_translator') or 0:.5f} edit={r.get('price_editor') or 0:.5f}")
|
||
print("\nне вошли: " + ", ".join(f"{m}({r['verdict']})" for m, r in res.items()
|
||
if m not in ts) or " —")
|
||
n_dr = len(ts) * len(us)
|
||
n_ed = len(ts) * len(us)
|
||
n_fl = 2 * len(M.units_zh())
|
||
est = sum((res.get(m, {}).get("price_translator") or 0.005) * len(us) for m in ts) \
|
||
+ (res.get(EDITOR, {}).get("price_editor") or 0.005) * len(ts) * len(us) \
|
||
+ ((res.get(ANCHOR, {}).get("price_translator") or 0.001)
|
||
+ (res.get(EDITOR, {}).get("price_editor") or 0.005)) * len(M.units_zh())
|
||
print(f"\nвызовов: черновики {n_dr} + редактуры {n_ed} + пол {n_fl} = {n_dr + n_ed + n_fl}")
|
||
print(f"смета по ФАКТИЧЕСКИМ ценам скрина: ${est:.4f} при потолке ${MONEY.CEILINGS['Ф2']:.2f}")
|
||
if est > MONEY.CEILINGS["Ф2"]:
|
||
print("⛔ СМЕТА НЕ ВЛЕЗАЕТ — резать панель по объявленному правилу или пинг владельцу")
|
||
|
||
|
||
def _selected(argv: list[str]) -> list[str]:
|
||
"""Подмножество жильцов для параллельного прогона. Клетки идемпотентны по тегу, поэтому
|
||
несколько процессов не мешают друг другу; касса читает диск, так что расход виден всем.
|
||
⚠ Гонка ограничена сверху: перерасход не больше (процессов−1)×цена одного вызова."""
|
||
only = [a.split("=", 1)[1] for a in argv if a.startswith("--only=")]
|
||
ts = tenants()
|
||
return [m for m in ts if m in only] if only else ts
|
||
|
||
|
||
def _part(argv: list[str]) -> str:
|
||
p = [a.split("=", 1)[1] for a in argv if a.startswith("--part=")]
|
||
return p[0] if p else ""
|
||
|
||
|
||
def cmd_drafts(argv: list[str] | None = None) -> None:
|
||
led = MONEY.Guarded("Ф2", default_expect=0.01)
|
||
for m in _selected(argv or []):
|
||
for u, en in units_all(_part(argv or [])):
|
||
rec = buy_draft(led, m, u, en)
|
||
if rec.get("skipped"):
|
||
print("⛔ потолок Ф2 — стоп")
|
||
return
|
||
print(f" {m:24s} {'en' if en else 'zh'} {u['uid'][:6]} "
|
||
f"finish={str(rec.get('finish')):8s} {len(rec.get('content', '')):5d} зн "
|
||
f"${rec.get('cost_usd', 0):.5f}")
|
||
time.sleep(0.15)
|
||
print(f"потрачено Ф2 ${led.spent():.6f}")
|
||
|
||
|
||
def cmd_edit(argv: list[str] | None = None) -> None:
|
||
led = MONEY.Guarded("Ф2", default_expect=0.01)
|
||
for m in _selected(argv or []):
|
||
for u, en in units_all(_part(argv or [])):
|
||
rec = buy_edit(led, m, u, en)
|
||
if rec.get("no_draft"):
|
||
print(f" {m:24s} {u['uid'][:6]} — годного черновика нет, редактура пропущена")
|
||
continue
|
||
if rec.get("skipped"):
|
||
print("⛔ потолок Ф2 — стоп")
|
||
return
|
||
print(f" E[{m:22s}] {'en' if en else 'zh'} {u['uid'][:6]} "
|
||
f"finish={str(rec.get('finish')):8s} {len(rec.get('content', '')):5d} зн "
|
||
f"${rec.get('cost_usd', 0):.5f}")
|
||
time.sleep(0.15)
|
||
print(f"потрачено Ф2 ${led.spent():.6f}")
|
||
|
||
|
||
def cmd_floor(argv: list[str] | None = None) -> None:
|
||
"""ШУМОВОЙ ПОЛ: ДВА независимых прогона якорного арма end-to-end на тех же zh-единицах.
|
||
|
||
Истинная разница между `p1` и `p2` — ноль по построению, поэтому измеренный разброс и есть
|
||
шум пайплайна (генерация черновика + генерация редактуры + судья). Он применяется к Ф2 И Ф3:
|
||
перевес ниже пола печатается вердиктом «ниже порога различимости».
|
||
"""
|
||
led = MONEY.Guarded("Ф2", default_expect=0.01)
|
||
argv = argv or []
|
||
part = _part(argv)
|
||
passes = [int(a.split("=", 1)[1]) for a in argv if a.startswith("--pass=")] or [1, 2]
|
||
zh = M.units_zh()
|
||
if part:
|
||
i, n = (int(x) for x in part.split("/"))
|
||
zh = [u for k, u in enumerate(zh) if k % n == i]
|
||
for p in passes:
|
||
for u in zh:
|
||
d = buy_draft(led, ANCHOR, u, False, floor=p)
|
||
if d.get("skipped"):
|
||
print("⛔ потолок Ф2 — стоп")
|
||
return
|
||
e = buy_edit(led, ANCHOR, u, False, floor=p)
|
||
if e.get("skipped"):
|
||
print("⛔ потолок Ф2 — стоп")
|
||
return
|
||
print(f" пол p{p} {u['uid'][:6]} черновик {len(d.get('content', '')):5d} зн · "
|
||
f"редактура {len(e.get('content', '')):5d} зн")
|
||
time.sleep(0.15)
|
||
print(f"потрачено Ф2 ${led.spent():.6f}")
|
||
|
||
|
||
def draft_cost(model: str, uid: str, floor: int = 0) -> float:
|
||
"""Цена черновика = ВСЕ попытки: ре-ген оплачен, значит он в цене роли."""
|
||
pre = tag_draft(model, uid, floor=floor)
|
||
return sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)
|
||
for f in OUT.glob(f"{pre}*.json"))
|
||
|
||
|
||
def regen_rate(model: str, en: bool = False) -> tuple[int, int]:
|
||
us = [u for u, e in units_all() if e == en]
|
||
need = 0
|
||
for u in us:
|
||
pre = tag_draft(model, u["uid"])
|
||
if len(list(OUT.glob(f"{pre}-r*.json"))):
|
||
need += 1
|
||
return need, len(us)
|
||
|
||
|
||
def cmd_score() -> None:
|
||
ts = tenants()
|
||
zh = M.units_zh()
|
||
cards = B.load_cards()
|
||
print("═══ Ф2 (в) ЭХО-МИНА: доля единиц, потребовавших ре-гена ═══")
|
||
for m in ts:
|
||
nz, tz = regen_rate(m, en=False)
|
||
ne, te = regen_rate(m, en=True)
|
||
print(f" {m:26s} zh {nz}/{tz} = {nz / max(1, tz):.0%} · en {ne}/{te} = {ne / max(1, te):.0%}")
|
||
print("\n═══ Ф2 (г) БАТАРЕЯ И ПОКРЫТИЕ КАНОНА (zh, детерминированно) ═══")
|
||
print(f"{'арм':30s}{'ед.':>5s}{'ханьцзы':>9s}{'типогр':>8s}{'потеря':>8s}"
|
||
f"{'канон':>14s}{'покрытие':>10s}")
|
||
for m in ts:
|
||
for kind in ("D", "E"):
|
||
rows, k, s = [], 0, 0
|
||
for u in zh:
|
||
t = draft_text(m, u["uid"]) if kind == "D" else edit_text(m, 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 not rows:
|
||
continue
|
||
print(f"{kind + '[' + m + ']':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"{sum(r['numbers']['lost_n'] for r in rows):8d}"
|
||
f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}")
|
||
print("\n═══ Ф2 (д) ЦЕНА/ЕДИНИЦА И ПРОЕКЦИЯ НА КНИГУ ═══")
|
||
print(f"{'жилец':26s}{'черновик':>11s}{'редактор':>11s}{'связка':>11s}{'книга 1500 ед.':>16s}")
|
||
for m in ts:
|
||
dc = [draft_cost(m, u["uid"]) for u in zh]
|
||
ec = [json.loads((OUT / f"{tag_edit(m, u['uid'])}.json").read_text(encoding="utf-8"))
|
||
.get("cost_usd", 0.0) for u in zh if (OUT / f"{tag_edit(m, u['uid'])}.json").exists()]
|
||
if not dc:
|
||
continue
|
||
d50, e50 = st.median(dc), (st.median(ec) if ec else 0.0)
|
||
print(f"{m:26s}{d50:11.5f}{e50:11.5f}{d50 + e50:11.5f}{(d50 + e50) * 1500:16.2f}")
|
||
led = MONEY.Guarded("Ф2")
|
||
print(f"\nпотрачено Ф2 ${led.spent():.6f} из ${led.ceiling:.2f} · "
|
||
f"пак ${MONEY.Pack().spent():.6f} из ${MONEY.PACK_CEILING:.2f}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
a = sys.argv[1:] or ["--plan"]
|
||
fn = {"--plan": cmd_plan, "--drafts": lambda: cmd_drafts(a), "--edit": lambda: cmd_edit(a),
|
||
"--floor": lambda: cmd_floor(a), "--score": cmd_score}.get(a[0])
|
||
fn() if fn else print(__doc__)
|