#!/usr/bin/env python3 """КОНТАМИНАЦИОННАЯ ПРОБА ФАЗЫ Д — ЖИЛЬЦАМИ и на книгах, которые фаза реально покупает. Платный. Заказ (Д3 и Д6) требует пробу «ЖИЛЬЦАМИ» — правило эксп-22 §1.1: гонять теми, кто работает, а не удобной моделью. Непригоден по пробе → СТОП и пинг, замену без слова владельца не искать. Риг НЕ пере-изобретается: берётся `eval/role_topology/contamination.py` и настраивается через свои глобали — тем же приёмом, каким `money_d` настраивает кассу, а `sud` — судейский риг. Метод пробы остаётся его: УЗНАВАНИЕ («что за произведение и кто автор») плюс КЛОУЗ ПО ИМЕНИ (из отрывка вырезано имя собственное, модель называет пропущенное). Пороги — его же, объявленные до данных: узнавание ≥3/5 = «знает о книге», клоуз ≥2/5 = «знает конкретику, то есть текст был в обучении». ⚠ ЧТО ЗДЕСЬ ДОБАВЛЕНО И ЗАЧЕМ. 1. **Книги фазы.** Прошлый прогон мерил `gu-zhenren`, `jinpingmei`, `fifty_shades`, `kristoff` и СТАРУЮ японскую книгу. Новая японская (`enkan_no_hate`, замена по слову владельца после срабатывания гардрейла 18+) не мерена ВООБЩЕ, а Д6 покупается именно на ней. 2. **Жильцы вместо удобной модели.** Прошлый прогон гнался `gpt-5.6-luna`. Это не жилец: черновики делает `deepseek-v4-flash`, редактуру `deepseek-v4-pro`. Знание книги ИМИ и есть то, что завышает качество перевода, а знание посторонней модели не значит ничего. 3. **Бюджет вывода поднят.** Редакция 1 прошлой пробы звала flash с 16000 токенов и получала 0–46 знаков: у DeepSeek размышление тарифицируется и считается ВНУТРИ бюджета, поэтому на короткий ответ ничего не оставалось (стена quirks §10; ровно эта же стена сегодня съела 14 клеток арма A1B). Ответ здесь нужен КОРОТКИЙ, а думать модель будет много — значит бюджет должен вмещать размышление ЦЕЛИКОМ, иначе «НЕ ЗНАЮ» будет означать «не успел сказать». ⚠ Если и при поднятом бюджете жилец возвращает пустоту — это НЕ «книга чиста», а непригодность пробы на этом жильце, и она объявляется отдельно, а не растворяется в нуле. 4. **Положительный контроль обязателен на КАЖДОМ жильце.** Нулевой результат неинтерпретируем: «модель не знает книгу» и «проба не работает на этой модели» дают одну картину. Контроль — `jinpingmei` (минский канон, public domain, заведомо в претрейне). Не сработал контроль — аннулируется весь прогон этого жильца, а не отдельная книга. Запуск: contam_d.py --dry # план, $0 contam_d.py # платный """ from __future__ import annotations import importlib.util import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") ZONE = Path(__file__).resolve().parent for d in ("dovodka", "role_topology", "tenant_panel"): sys.path.insert(0, str(REPO / "eval" / d)) def _load(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) sys.modules[name] = mod spec.loader.exec_module(mod) return mod MONEY = _load("money_d", ZONE / "money_d.py") CT = _load("contam21", REPO / "eval" / "role_topology" / "contamination.py") ROSTER = _load("roster_c", REPO / "eval" / "tenant_panel" / "roster.py") ED = _load("editors_c", REPO / "eval" / "tenant_panel" / "editors.py") # ⚠⚠ КАССУ ПЕРЕ-НАСТРАИВАЕМ ПОСЛЕ ВСЕХ ЧУЖИХ ЗАГРУЗОК — та же мина, что описана в `contour.py`. # `buy` — СИНГЛТОН в `sys.modules`, и побеждает тот, кто настроил его последним: `contamination` # и `editors` тянут кассу эксп-21/22 и перебивают `BUY.OUT` обратно на `~/books/tenant-panel`. # Отказ бесшумный и дорогой — проверено исполнением 14.08: 7 клеток пробы ушли в ЧУЖОЙ каталог, # касса фазы показывала $0, а гейт потолка сторожил границу, к которой покупки не относились. MONEY.M22.configure(ZONE=ZONE, OUT=MONEY.OUT, TAGS=MONEY.TAGS, CEILINGS=MONEY.CEILINGS, PACK_CEILING=MONEY.PACK_CEILING, GLOB_ALL="dv-*.json", ROSTER=ROSTER) if MONEY.M22.BUY.OUT != MONEY.OUT: # ассерт, а не надежда raise SystemExit(f"⛔ buy.OUT={MONEY.M22.BUY.OUT} против кассы {MONEY.OUT} — " "покупки уедут в чужой каталог, СТОП") # ⚠ БЮДЖЕТ ВЫВОДА. У DeepSeek размышление тарифицируется и считается ВНУТРИ бюджета, поэтому # короткий ответ при тесном капе не помещается вовсе: прошлая редакция звала flash с 16000 и # получала 0–46 знаков. Ответ здесь нужен в две строки, думать модель будет много — кап должен # вмещать размышление ЦЕЛИКОМ, иначе пустота будет означать «не успел сказать», а не «не знаю». MAX_OUT = 32000 # Глушить thinking у DeepSeek ЗАПРЕЩЕНО (эхо-мина, гардрейл проекта). Конфигурацию на модель # даёт `roster.call_kwargs` — своих `reasoning_effort` здесь не ставим НИКОГДА. SYS = ("Тебе дают отрывок художественного текста, в котором одно имя собственное заменено на " "[???]. Ответь РОВНО двумя строками и ничем больше:\n" "ПРОИЗВЕДЕНИЕ: <название и автор, либо НЕ ЗНАЮ>\n" "ИМЯ: <что стоит вместо [???], либо НЕ ЗНАЮ>\n" "Не пересказывай отрывок и не продолжай его.") # Жильцы: те, кто реально работает на парах. Проба на каждом отдельно. RESIDENTS = ("deepseek-v4-flash", "deepseek-v4-pro") # Книги, которые фаза Д покупает, плюс обязательный положительный контроль. BOOKS_D = { "enkan-ja": dict(path=Path.home() / "books" / "enkan_no_hate_ja.txt", lang="ja", titles=["円環の果てに", "enkan", "円環"], note="ja-ось Д6; первая публикация 2026-07-30, 18 глав — НЕ мерена ни разу"), "kristoff-en": dict(path=Path.home() / "books" / "Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub", lang="en", titles=["empire of the dawn", "empire of the vampire", "кристофф", "kristoff"], note="en-ось Д3; luna узнала СЕРИЮ 1 раз из 5 — жильцами не мерена"), "jinpingmei": dict(path=Path.home() / "books" / "jinpingmei" / "jinpingmei-ctext-zhs.txt", lang="zh", titles=["金瓶梅", "золотой лотос", "jin ping mei"], note="ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ — обязателен на каждом жильце"), } # Книга → фаза кассы. Положительный контроль относим к ФД-B: он покупается один раз, а прогон # Д6 возьмёт те же теги из кэша за $0. PHASE = {"kristoff-en": "ФД-B", "jinpingmei": "ФД-B", "enkan-ja": "ФД-C"} PREFIX = {"ФД-B": "dv-b", "ФД-C": "dv-c"} def probe(res: str, book: str, dry: bool) -> dict: """Проба одного жильца на одной книге. Возвращает счёт узнавания и клоуза.""" m = BOOKS_D[book] text = CT.read_book(m["path"]) names = CT.frequent_names(text, m["lang"]) ps = CT.passages(text, names, CT.N_PASSAGES) ph = PHASE[book] led = MONEY.Guarded(ph, default_expect=0.01) rec_hits = cloze_hits = empty = 0 for i, (prime, gold) in enumerate(ps, 1): tg = f"{PREFIX[ph]}-contam-{res.replace('.', '')}-{book}-{i}" if dry: continue msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": prime}] r = MONEY.purchase(led, tg, res, ED.client(res), ROSTER.call_kwargs(res, msgs, max_out=MAX_OUT)) if r.get("skipped"): raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tg} — СТОП и вопрос владельцу") ans = r.get("content") or "" if not ans.strip(): empty += 1 continue work, name = CT.parse(ans) if work and "НЕ ЗНАЮ" not in work.upper() and any(t.lower() in work.lower() for t in m["titles"]): rec_hits += 1 if name and "НЕ ЗНАЮ" not in name.upper() and gold and gold.lower() in name.lower(): cloze_hits += 1 return dict(recognition=rec_hits, cloze=cloze_hits, empty=empty, n=len(ps)) def main() -> int: dry = "--dry" in sys.argv print("КОНТАМИНАЦИОННАЯ ПРОБА ФАЗЫ Д — жильцами\n") print(f"{'книга':14s}{'язык':>6s} что это") for b, m in BOOKS_D.items(): ok = "" if m["path"].exists() else " ⛔ ФАЙЛА НЕТ" print(f"{b:14s}{m['lang']:>6s} {m['note']}{ok}") print(f"\nжильцы: {' · '.join(RESIDENTS)}") print("пороги (объявлены ДО данных, взяты из рига эксп-21): " "узнавание ≥3/5 = знает о книге · клоуз ≥2/5 = знает конкретику") only = [a.split("=", 1)[1] for a in sys.argv if a.startswith("--books=")] books = only[0].split(",") if only else list(BOOKS_D) print(f"\nкниги прогона: {' · '.join(books)}") if dry: print("--dry: покупок нет; каждая книга = 5 отрывков × 2 жильца") return 0 res_tbl: dict[tuple[str, str], dict] = {} for res in RESIDENTS: for b in books: res_tbl[(res, b)] = probe(res, b, dry=False) print(f"\n{'жилец':20s}{'книга':14s}{'узнавание':>11s}{'клоуз':>8s}{'пусто':>7s} вердикт") bad = 0 for (res, b), v in res_tbl.items(): known = v["recognition"] >= 3 concrete = v["cloze"] >= 2 verdict = ("ЗНАЕТ ТЕКСТ" if concrete else "знает о книге" if known else "чисто") print(f" {res:18s}{b:14s}{v['recognition']:>7d}/{v['n']}{v['cloze']:>6d}/{v['n']}" f"{v['empty']:>7d} {verdict}") if b != "jinpingmei" and concrete: bad += 1 print("\n=== ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ (без него нулевой результат неинтерпретируем) ===") for res in RESIDENTS: c = res_tbl.get((res, "jinpingmei")) if not c: print(f" {res}: контроль не гнался — прогон этого жильца НЕИНТЕРПРЕТИРУЕМ") bad += 1 continue ok = c["recognition"] >= 3 # ⚠ Пустота при поднятом капе — НЕ «книга чиста», а непригодность пробы на этом жильце. if c["empty"] == c["n"]: print(f" {res}: ПУСТЫЕ ОТВЕТЫ на контроле — проба на этом жильце НЕПРИГОДНА, " "нули по остальным книгам не читать") bad += 1 elif not ok: print(f" {res}: контроль ПРОВАЛЕН ({c['recognition']}/{c['n']}) — прогон жильца " "аннулируется целиком, а не отдельная книга") bad += 1 else: print(f" {res}: контроль сработал ({c['recognition']}/{c['n']}) — нули значимы") if bad: print("\n⛔ проба даёт СТОП: см. строки выше. Замену материала без слова владельца " "не ищем (заказ Д3/Д6).") else: print("\n[OK ] материал фазы пригоден: жильцы конкретики книг не знают") return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())