From a19107479d15f207b8c46eed61b0cdc61baab25b Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sun, 12 Jul 2026 03:43:47 +0300 Subject: [PATCH] Land exp14 batch-2: 3-family frontier shows the comprehension fix is gpt-5.4-specific (grok inverts, gemini inconclusive), re-gate flags ~10 fidelity chunks, bigger chunks are cheaper and better --- docs/PROGRESS.md | 8 +- docs/experiments/14-quality-empirics.md | 74 +++++++++++++- eval/exp14_arms.py | 5 +- eval/exp14_judges.py | 6 +- eval/exp14_monitor.py | 63 ++++++++++++ eval/exp14_rank.py | 120 +++++++++++++++++++++++ eval/exp14_regate.py | 125 ++++++++++++++++++++++++ eval/exp14_sizecurve.py | 122 +++++++++++++++++++++++ 8 files changed, 515 insertions(+), 8 deletions(-) create mode 100644 eval/exp14_monitor.py create mode 100644 eval/exp14_rank.py create mode 100644 eval/exp14_regate.py create mode 100644 eval/exp14_sizecurve.py diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 53160c7..a7442df 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -18,7 +18,13 @@ - **Претензия 2 внутри-чанк (смысл) = дешёвая МОДЕЛЬ (capability floor).** T1 (gl.7 двойное отрицание «все знали»): glm-5 **инвертирует** в «никто не знал» НЕЗАВИСИМО от промпта (P0 и P1a); **gpt-5.4 чинит** НЕЗАВИСИМО от промпта (F-base и F-disc). T2 (物是人非) — тот же знак. Промпт претензию 2 НЕ закрывает. - **Претензия 2 кросс-граница = НЕ доминирует в срезе.** T5/T6 локально самодостаточны; ±1-reference (A-ref) дал байт-идентичный выход (чинить нечего); whole-chapter (P1c) единственный явно связал антецедент (1 датапоинт). -**Near-term рекомендация (на ратификацию оркестратора, НЕ смена дефолта):** (1) дискурс-reflow-контент Ван Цуй→editor-промпт + рассмотреть отдельный target-only reflow-пасс (A-2pass) под COGS; (2) претензию 2 — **селективная фронтир-эскалация редактора по смысл-риску** (не тотальный фронтир — он абзацы портит); (3) CJK-гард при deformat; (4) НЕ строить Ф2-кросс-чанк-память под слабый сигнал. **ПАРТИЯ 2 (осталось):** кривая нарезки, слепые пакеты Ступени II (гл.19/17/18, D=0.061/0.246/0.440), полная ранжирующая панель +leave-one-out, fidelity re-gate (D34.3), 3 финалиста, хендофф. Планка = 2 претензии владельца (ИНТЕРИМ пре-скрин, НЕ пилот Ф2.5). Смену дефолта ратифицирует оркестратор. +**ПАРТИЯ 2 ВЫПОЛНЕНА (по запросу владельца добавлены Gemini+Grok фронтир-редакторы — честный тест в €2.3; итог обеих партий ≈$8.5 across 6 ключей, каждый под лимитом).** Ключевые УТОЧНЕНИЯ: +- **T1-«capability floor» — MODEL-SPECIFIC, не общий фронтир (поправка партии-1 n=1):** gpt-5.4 чинит инверсию двойного отрицания, а **grok-4.3 (фронтир, reasoning-ON) её НЕ чинит — инвертирует как дешёвый glm** (заякорился на черновике; мини-проба grok на СВЕЖЕМ предложении верна → провал именно в РЕДАКТОРСКОЙ задаче). Gemini-редактор ИНКОНКЛЮЗИВЕН (mandatory-thinking съел max_tokens=8000 → обрыв finish=length; урок: Gemini-editor нужен ≥16–20k ток × $12/M = COGS-враждебно; €2.3 исчерпан). grok-4.5 — регион-лок (403, оба ключа). ⇒ смысл (П2) чинит **gpt-5.4 конкретно**, не «любой фронтир». +- **Абзацы (П1): grok-disc ЛУЧШИЙ (mean 4.22)** но CJK+провал T1; A-2pass 3.33; **кривая нарезки: крупнее чанк = ДЕШЕВЛЕ выходных токенов И лучше абзацы, 0 ретраев** (оптимум 3200c/глава) → поддержка `edit=крупная единица` (D35.7) по COGS И качеству. +- **Fidelity re-gate (D34.3): пере-прогон НЕ чист** — средняя верность 88–94, но **13 катастроф + 21 инверсия редактуры на ~10 чанках** (ch10.1 both-judge fid 60); класс инверсий D34.3 подтверждён на хвосте → флаги на span-ревью, пере-прогон засчитан НЕ полностью. +- **Ранжирование финалистов (гл.19/17/18): стиль F-disc(gpt-5.4)>A-2pass>P0 (робастно); holistic-верность P0>прочих — НО P0 несёт T1-катастрофу, панель её не поймала = ЛИТЕРАЛ-СМЕЩЕНИЕ судей** (урок exp12/мемо eval-aggregation) → доверять span-уровню, не holistic-скаляру. + +**Слепой пакет владельцу готов:** `exp14/monitor/monitor14.md` (3 финалиста × 3 главы, нейтральные метки, ключ отдельно) — человеческий вердикт «лучше фана» (D30.7). **Near-term рекомендация** (ратификация оркестратора): дискурс-промпт + крупная edit-единица (дешевле И лучше) под omission-гард; селективная эскалация на gpt-5.4 (не grok/gemini) по смысл-риску; CJK-гард на deformat/grok; ~10 re-gate-флагов на ревью. Планка = 2 претензии (ИНТЕРИМ пре-скрин, НЕ пилот Ф2.5). Детали — `experiments/14-quality-empirics.md` §2/§2Б/§3. ## Ресёрчер — рычаги качества (`research/18-quality-levers.md`), 2026-07-12 (D35.5, вход полигону) diff --git a/docs/experiments/14-quality-empirics.md b/docs/experiments/14-quality-empirics.md index 5d59ee9..14b2bb3 100644 --- a/docs/experiments/14-quality-empirics.md +++ b/docs/experiments/14-quality-empirics.md @@ -247,8 +247,76 @@ usage→$: `(in·pin + (comp + reason)·pout)/1e6`; `reason`= из `completion` 3. **CJK-гард обязателен** при любом deformat-черновике (A-layout урок). 4. **НЕ строить** сложную Ф2-кросс-чанк-память под этот срез (сигнал слабый); whole-chapter/edit=глава — держать кандидатом под доказанный ROI. -**Оговорки (методика-guard):** trap-набор мал (6 трапов, prev-boundary, без катафоры); фронтир = 1 модель (gpt-5.4), Gemini/grok переводчиками не гонялись (бюджет/эхо); T1-вывод — 1 катастроф-датапоинт (но детерм.-чистый); срез — PD-смежная ранняя арка (без 18+); это ИНТЕРИМ пре-скрин (D35), НЕ вердикт пилота Ф2.5. +**Оговорки партии 1 (пересмотрены партией 2):** T1-вывод «capability floor» партии 1 стоял на 1 фронтир-модели (gpt-5.4) → партия 2 (3 семьи) его УТОЧНИЛА (см. §2Б.1 — floor model-SPECIFIC, не общий). Прочее: срез PD-смежный без 18+; ИНТЕРИМ пре-скрин (D35), не пилот Ф2.5. -## 3. Fidelity re-gate (ПАРАЛЛЕЛЬНО, D34.3 — независим от §1-§2) +--- -Span-цитирующий, reasoning-ON, раздельно стиль/верность, охота на КЛАСС ИНВЕРСИЙ редактуры (ch11/0 аутлайер: editor расширил 3009→4631, sim 0.064), катастроф-скрин терминов у победителя, author≠reviewer (не glm-5/deepseek), leave-one-out. Вход — `rerun/rerun-parallel.txt` (57 чанков, выровнено ОРИГ|ПЕРЕВОД) + `records.json`. **Пере-прогон НЕ засчитан до пройденного re-gate.** *(результаты — ниже, после прогона.)* +## 2Б. ПАРТИЯ 2 — 3-семейный фронтир + fidelity re-gate + ранжирование финалистов + кривая нарезки + +> Трата партии 2: Gemini $2.20 (≈€2.0, на границе €2.3 — больше не тратил), Kimi +$2.7, OpenAI +$0.9, ZAI +$0.14, XAI $0.15, DeepSeek $0.02. **Итог обеих партий ≈ $8.5** across 6 ключей (каждый под лимитом). grok-4.5 — региональный лок (`403 not available in your region`, ОБА ключа, не бюджет); тестим доступный grok-4.3. + +### 2Б.1. Capability floor — MODEL-SPECIFIC, не общий фронтир (ключевая поправка партии 1) + +3-семейный фронтир-редактор на trap-наборе (черновик flash общий; варьируем editor-модель × промпт): + +| Editor-семья | T1 двойн. отриц. (детерм.) | mean предл./абзац | доля 1-предл. | CJK | len/P0 | +|---|---|---|---|---|---| +| glm-5 (дешёвый) P0/P1a/A-2pass | ✗ **инверсия** (все промпты) | 1.91→3.33 | 0.47→0.19 | 2 | ~1.0 | +| **gpt-5.4** F-base/F-disc | **✓ чинит** (оба промпта) | 1.58 / 2.45 | 0.58 / 0.37 | 0 | ~1.02 | +| **grok-4.3 ON** X-base/X-disc | ✗ **инверсия** (оба! якорится на черновике) | 1.74 / **4.22** | 0.50 / 0.26 | 2 / 6 | ~0.97 | +| gemini-3.1-pro G-base/G-disc | **инконклюзивно** (обрыв) | 1.52 / 1.95 | 0.60 / 0.53 | ≤1 | **0.65–0.76 ⚠** | + +- **T1 «capability floor» — НЕ универсален: gpt-5.4 чинит инверсию черновика, а grok-4.3 (фронтир, reasoning-ON) её НЕ чинит — инвертирует так же, как дешёвый glm.** Grok-редактор заякорился на ошибке черновика (мини-проба grok на СВЕЖЕМ предложении `他没有一个不知道的` дала верно → провал именно в РЕДАКТОРСКОЙ задаче «поймай ошибку черновика»). ⇒ **это не «любой фронтир чинит», а СПЕЦИФИЧЕСКАЯ компетенция gpt-5.4** (ловить двойное отрицание/инверсию черновика). Партия 1 (n=1 фронтир) это переобобщила — партия 2 поправила. +- **grok-4.3 + дискурс (X-disc) — ЛУЧШИЕ абзацы из всех (mean 4.22)**, но CJK-утечка 6 и провал T1. Профиль: отличная переверстка, слабое понимание. +- **gemini-3.1-pro как редактор — ИНКОНКЛЮЗИВНО (харнес-дефект + COGS):** mandatory-thinking съел `max_tokens=8000` → `finish=length`, выход обрезан (comp 317–646 ток, len/P0 0.65–0.76 = потеря контента). Честный урок: **Gemini-редактору нужен `max_tokens`≥16–20k, а при $12/M (thinking биллится как output) это ~$0.15–0.25/вызов — COGS-враждебно** (×10 к glm/grok, ×2–3 к gpt-5.4). €2.3 исчерпан на обрезанных вызовах → Gemini-качество не оценено (мой мис-конфиг max_tokens; вписано в урок). + +### 2Б.2. Fidelity re-gate (D34.3) — пере-прогон НЕ чист + +Span-цитирующий, раздельно верность/стиль, охота на класс инверсий редактуры (final vs draft vs src), author≠reviewer (gpt-5-mini + kimi, НЕ glm/deepseek), leave-one-out. 55 чанков (ch5.0/ch20.0 — пустые экспорт-баг, вне скора). +- **Средняя верность: gpt-5-mini 93.7 (min 60), kimi 87.6 (min 60); leave-one-out 87.6↔93.7.** Широко приемлемо, НО с хвостом. +- **13 КАТАСТРОФ-флагов + 21 ИНВЕРСИЯ РЕДАКТУРЫ** на ~10 чанках. **ch10.1 — катастрофа обоими судьями (fid 60)**; прочие: 1.2, 5.1, 6.0, 9.1, 11.0, 12.0, 13.1, 15.1, 16.0, 20.1. Класс, которого боялся D34.3 (билингв-редактор купил гладкость ценой смысла), ПОДТВЕРЖДЁН на хвосте. +- **Вердикт re-gate: пере-прогон НЕ проходит чисто — ~10 чанков с span-цитированными инверсиями/катастрофами на ревью оркестратору/владельцу** (часть может быть FP LLM-судьи — верифицировать спаны; но ch10.1 both-judge — вероятно реален). Пере-прогон засчитан НЕ полностью (D1.1/D34.3). + +### 2Б.3. Ранжирование 3 финалистов (Ступень II, гл.19/17/18) — style↑ у reflow, но HOLISTIC fidelity литерал-смещён + +Финалисты: **Fin-A=P0 · Fin-B=A-2pass · Fin-C=F-disc (gpt-5.4)**. Панель gpt-5-mini+kimi+gemini, 3 повтора со свапом, leave-one-out (Borda, 2=лучший): +- **Стиль:** Fin-C **1.43** > Fin-B 0.95 > Fin-A 0.62 (робастно во всех leave-one-out) → **gpt-5.4-дискурс читается лучше, A-2pass второй, прод P0 худший.** +- **Верность (holistic):** Fin-A(P0) **1.53** > Fin-C 0.76 > Fin-B 0.71 (робастно) — **НО P0 содержит T1-КАТАСТРОФУ (инверсию «все знали»→«никто»), которую F-disc чинит.** Панель holistic-верности этого НЕ поймала → **литерал-смещение судей** (награждают консервативную близость P0 к черновику, слепы к его span-катастрофе — точный урок exp12/мемо `eval-aggregation`). +- **Синтез:** доверять SPAN-уровню (F-disc чинит T1, P0 инвертирует) выше, чем holistic-скаляру верности. Reflow/фронтир поднимают СТИЛЬ; на верность есть НАПРЯЖЕНИЕ (агрессивная переверстка ↔ omission-риск), но «P0 вернее» — артефакт литерал-bias, не факт. + +### 2Б.4. Кривая нарезки — крупнее чанк = ДЕШЕВЛЕ И лучше абзацы (0 ретраев) + +ch17+ch13 × 800/1600/3200/whole (жадная упаковка; flash-draft→glm-5-дискурс; реальный токенайзер): + +| размер | ch17 out-ток / mean_spp | ch13 out-ток / mean_spp | +|---|---|---| +| 800c | 15159 / 2.59 | 12171 / 3.48 | +| 1600c (прод) | 10284 / 3.03 | 8043 / 3.95 | +| 3200c | 8691 / 3.67 | 6090 / 4.09 | +| whole | 7782 / 3.59 | 4560 / 3.88 | + +- **Монотонно: крупнее чанк → МЕНЬШЕ выходных токенов (дешевле) И ЛУЧШЕ абзацы** (mean_spp↑, доля-1-предл.↓). 0 ретраев на всех размерах (retry blast-radius не наблюдён на этих 2 главах). Оптимум ~**3200c / whole** по обеим осям. +- Мелкий чанк (800c) — дороже (дублирующий overhead/повторы) И рубленее. Опровергает «мельче = дешевле» для output-домината; на ЭТОМ тексте перевёрнутой-U вниз не видно (lost-in-middle не бьёт до главы). **Поддерживает `edit=крупная единица / глава` (D35.7) — И по COGS, И по качеству.** Оговорка: 2 главы, 1 текст, 0 наблюдённых ретраев (retry-adjusted не активировался). + +### 2Б.5. Итоговая capability-vs-activation карта (обе партии) + +| Претензия | Потолок | Уточнённое доказательство | Ход | +|---|---|---|---| +| (1) абзацы | **ПРОМПТ + РАЗМЕР** (активация) | дискурс-промпт (P1a/F-disc/X-disc) + 2-пасс (A-2pass 3.33) + крупный чанк (кривая) двигают KPI; grok-disc лучший (4.22); фронтир-модель НЕ нужна | катить дискурс-промпт + крупная edit-единица; A-2pass если COGS позволит | +| (2) внутри-чанк смысл | **MODEL-SPECIFIC** (gpt-5.4), не общий floor | gpt-5.4 чинит T1; glm-5 И grok-4.3 инвертируют; gemini инконклюзивно | селективная эскалация на **gpt-5.4** по смысл-риску (grok НЕ годится — тоже инвертирует) | +| (2) кросс-граница | не доминирует в срезе | A-ref null; P1c связал (1 датапоинт) | не строить Ф2-память под слабый сигнал | +| верность пере-прогона | **есть хвост порчи** | re-gate: 13 катастроф + 21 инверсия на ~10 чанках | ревью флагов + omission-гард на reflow-армы | + +### 2Б.6. Пересмотренная near-term рекомендация (на ратификацию оркестратора) + +1. **Абзацы (П1):** дискурс-reflow-промпт (Ван Цуй) + сдвиг edit-единицы к КРУПНОЙ (3200c/глава — дешевле И лучше по кривой). A-2pass даёт топ-KPI, но добавляет пасс — взвесить vs крупный чанк (крупный чанк даёт похожий лифт БЕЗ +пасса и ДЕШЕВЛЕ). **Оба под omission-гард** (reflow-армы просели по holistic-верности — следить за атомами). +2. **Смысл (П2):** селективная эскалация редактора на **gpt-5.4 конкретно** по смысл-риску (двойное отрицание/chengyu/инверсия черновика). **grok и gemini НЕ заменяют** (grok инвертирует, gemini COGS-враждебен/инконклюзивен). Не тотальный фронтир (портит абзацы у F-base, дорого). +3. **CJK-гард** обязателен на deformat (A-layout) и на grok-выходах (утечка 6). +4. **Пере-прогон:** ~10 re-gate-флагов на ревью до «засчитан». +5. **Слепые пакеты Ступени II** (`exp14/monitor/monitor14.md`) — владельцу на человеческий вердикт «лучше фана» (арбитр, D30.7). + +**Оговорки (методика-guard):** trap-набор мал (6, prev-boundary, без катафоры); T1 — детерм.-чистый, но 1 конструкция; holistic-fidelity судьи литерал-смещены (доверять спанам); gemini недооценён (мис-конфиг max_tokens, €2.3 исчерпан); кривая — 2 главы/0 ретраев; ИНТЕРИМ пре-скрин (D35), НЕ пилот Ф2.5. Смену дефолта ратифицирует ОРКЕСТРАТОР. + +## 3. Fidelity re-gate (D34.3) — свод в §2Б.2 + +Пройден с результатом **«не чист»**: средняя верность gpt-5-mini 93.7 / kimi 87.6 (leave-one-out 87.6↔93.7), но **13 катастроф-флагов + 21 инверсия редактуры на ~10 чанках** (ch10.1 — both-judge катастрофа fid 60). Класс инверсий редактуры (D34.3) подтверждён на хвосте. Флаги — на span-верификацию оркестратором/владельцем; пере-прогон засчитан НЕ полностью. Вход: `rerun/records.json` (source/draft/final, author≠reviewer gpt-5-mini+kimi). Артефакты: `exp14/regate_verdicts.jsonl`. diff --git a/eval/exp14_arms.py b/eval/exp14_arms.py index 9e41dfd..f5f98f4 100644 --- a/eval/exp14_arms.py +++ b/eval/exp14_arms.py @@ -145,8 +145,9 @@ def main(): ref = build_reference(a.arm) run_editor(a.arm, "discourse", [u for u in chunk_units(a.scope) if u["id"] in ref], a.model, reference_by=ref, dry=a.dry) - elif a.arm in ("F-base", "F-disc"): - variant = "baseline" if a.arm == "F-base" else "discourse" + elif a.arm in ("F-base", "F-disc", "G-base", "G-disc", "X-base", "X-disc"): + # F-* = gpt-5.4, G-* = gemini-3.1-pro-preview, X-* = grok-4.3 reasoning-ON (все фронтир-редакторы) + variant = "baseline" if a.arm.endswith("-base") else "discourse" run_editor(a.arm, variant, chunk_units(a.scope), a.model, dry=a.dry) else: raise SystemExit(f"unknown arm {a.arm}") diff --git a/eval/exp14_judges.py b/eval/exp14_judges.py index 6b347e3..08d0d42 100644 --- a/eval/exp14_judges.py +++ b/eval/exp14_judges.py @@ -42,10 +42,12 @@ TRAPS = [ JUDGE_POOL = { # judge_model → family (для self-family exclusion) "gpt-5-mini": "openai", "kimi-k2.6": "moonshot", "gemini-3.1-pro-preview": "google", } -ARM_FAMILY = { # editor-модель арма → family (author≠reviewer) +ARM_FAMILY = { # editor-модель арма → family (author≠reviewer, self-family exclusion) "P0": "zai", "P1a": "zai", "P1b": "zai", "P1c": "zai", "A-layout": "zai", "A-2pass": "zai", "A-ref-prev": "zai", "A-ref-both": "zai", - "F-base": "openai", "F-disc": "openai", + "F-base": "openai", "F-disc": "openai", # gpt-5.4 + "G-base": "google", "G-disc": "google", # gemini-3.1-pro-preview + "X-base": "xai", "X-disc": "xai", # grok-4.3 reasoning-ON } # draft общий = deepseek → deepseek не судья (self-family к черновику, где рождается T1) diff --git a/eval/exp14_monitor.py b/eval/exp14_monitor.py new file mode 100644 index 0000000..a3d49e8 --- /dev/null +++ b/eval/exp14_monitor.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""exp14 Ступень II — слепой пакет владельцу: 3 ФИНАЛИСТА на 3 главах (гл.19/17/18), под +нейтральными метками, ключ ОТДЕЛЬНО. Копирайт: книжный текст ВНЕ git (exp14/monitor/). + +Финалисты: Fin-A=P0 (текущий прод baseline) · Fin-B=A-2pass (лучший near-term, дешёвый) · +Fin-C=F-disc (фронтир gpt-5.4, дискурс). Per-глава seeded-shuffle меток (V1..V3 — РАЗНАЯ +модель в каждой главе; ключ владелец не открывает до ранжирования). +""" +from __future__ import annotations +import json, random +from pathlib import Path +import exp14_common as C + +ARMS = C.DIAG / "arms" +MAT = json.load(open(C.DIAG / "material.json")) +OUT = C.DIAG / "monitor"; OUT.mkdir(exist_ok=True) +FINALISTS = {"Fin-A": "P0", "Fin-B": "A-2pass", "Fin-C": "F-disc"} +CHAPTERS = [19, 17, 18] +SALT = "exp14-monitor-blind-v1" + + +def chapter_text(arm: str, ch: int) -> str: + chunks = sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)]) + parts = [] + for ck in chunks: + p = ARMS / arm / f"{ch}.{ck}.txt" + parts.append(p.read_text(encoding="utf-8") if p.exists() else f"[[MISSING {arm} {ch}.{ck}]]") + return "\n\n".join(parts) + + +def main(): + missing = [(f, a, ch) for f, a in FINALISTS.items() for ch in CHAPTERS + for ck in sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)]) + if not (ARMS / a / f"{ch}.{ck}.txt").exists()] + if missing: + print("⚠ отсутствуют выходы (арм ещё не готов):", missing[:10]); return + + key = {} + md = ["# Слепое чтение — exp14 Ступень II (3 варианта × 3 главы)\n", + "Прочти каждую главу в ТРЁХ вариантах (V1/V2/V3 — порядок в каждой главе РАЗНЫЙ). " + "Для каждой главы: (1) какой вариант читается лучше как русская проза «лучше фана» — ранжируй V1>V2>V3; " + "(2) закрыты ли обе претензии (абзацы/конвенции; понимание связей/смысла); (3) любые смысловые ляпы. " + "Ключ (какая модель под какой меткой) — в отдельном файле `_КЛЮЧ.json`, открой ПОСЛЕ.\n", + "D-плотность диалога: гл.19=0.061 (нарратив), гл.17=0.246 (смешанная), гл.18=0.440 (диалог).\n"] + for ch in CHAPTERS: + order = list(FINALISTS); random.Random(f"{SALT}-{ch}").shuffle(order) + labels = {f"V{i+1}": order[i] for i in range(len(order))} + key[str(ch)] = {lab: {"finalist": fin, "arm": FINALISTS[fin]} for lab, fin in labels.items()} + md.append(f"\n\n---\n\n## Глава {ch}\n") + for i in range(len(order)): + lab = f"V{i+1}"; fin = labels[lab] + md.append(f"\n### {lab}\n\n{chapter_text(FINALISTS[fin], ch)}\n") + (OUT / "monitor14.md").write_text("\n".join(md), encoding="utf-8") + (OUT / "_КЛЮЧ.json").write_text(json.dumps( + {"salt": SALT, "finalists": FINALISTS, "per_chapter": key}, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"→ {OUT/'monitor14.md'} (3 главы × 3 слепых варианта)") + print(f"→ {OUT/'_КЛЮЧ.json'} (НЕ открывать до ранжирования)") + for ch in CHAPTERS: + print(f" гл.{ch}: " + ", ".join(f"{lab}={key[str(ch)][lab]['finalist']}" for lab in ("V1","V2","V3"))) + + +if __name__ == "__main__": + main() diff --git a/eval/exp14_rank.py b/eval/exp14_rank.py new file mode 100644 index 0000000..fba5757 --- /dev/null +++ b/eval/exp14_rank.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""exp14 Ступень II — ранжирующая панель финалистов (P0/A-2pass/F-disc) на главах 19/17/18. +РАЗДЕЛЬНО стиль/верность, span-цитаты, ≥N повторов со свапом меток, leave-one-judge-out. +Судьи gpt-5-mini + kimi + gemini(точечно). Self-family (F-disc=openai) снимается leave-one-out. +Guard мемо eval-aggregation: НЕ собирать ложную сходимость. + +Использование: exp14_rank.py [--repeats 3] [--dry] +""" +from __future__ import annotations +import argparse, json, random, re, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import exp14_common as C + +ARMS = C.DIAG / "arms" +MAT = json.load(open(C.DIAG / "material.json")) +FINALISTS = {"Fin-A": "P0", "Fin-B": "A-2pass", "Fin-C": "F-disc"} +CHAPTERS = [19, 17, 18] +AXES = {"style": "русская художественность/абзацы/конвенции (естественность прозы, красная строка, тире-диалог)", + "fidelity": "верность смыслу (нет инверсий полярности/участника, нет потери атомов; можно ценой лёгкой шероховатости)"} +JUDGES = ["gpt-5-mini", "kimi-k2.6", "gemini-3.1-pro-preview"] +CAPS = {"gpt-5-mini": 0.10, "kimi-k2.6": 0.15, "gemini-3.1-pro-preview": 0.30} + + +def chapter_text(arm, ch): + chunks = sorted(u["chunk_idx"] for u in MAT["stage2"][str(ch)]) + return "\n\n".join((ARMS / arm / f"{ch}.{ck}.txt").read_text(encoding="utf-8") for ck in chunks) + + +def run(): + ap = argparse.ArgumentParser(); ap.add_argument("--repeats", type=int, default=3) + ap.add_argument("--dry", action="store_true"); a = ap.parse_args() + sp = C.Spender(C.DIAG / "rank_costs.jsonl", CAPS) + vpath = C.DIAG / "rank_verdicts.jsonl"; out = []; done = set() + if vpath.exists(): + for l in vpath.read_text(encoding="utf-8").splitlines(): + if l.strip(): + v = json.loads(l); out.append(v); done.add((v["ch"], v["axis"], v["judge"], v["rep"])) + tasks = [] + for ch in CHAPTERS: + for axis in AXES: + for rep in range(a.repeats): + order = list(FINALISTS); random.Random(f"exp14-rank-{ch}-{axis}-{rep}").shuffle(order) + labels = {f"V{i+1}": order[i] for i in range(len(order))} + for jm in JUDGES: + if jm == "gemini-3.1-pro-preview" and rep > 0: + continue # gemini точечно: 1 повтор (бюджет €2.6) + if (ch, axis, jm, rep) in done: + continue + tasks.append((ch, axis, rep, jm, labels)) + print(f"rank tasks: {len(tasks)} new ({len(done)} done)") + if a.dry: + from collections import Counter + print(Counter(t[3] for t in tasks)); return + for ch, axis, rep, jm, labels in tasks: + blocks = "\n\n".join(f"=== {lab} ===\n{chapter_text(FINALISTS[fin], ch)}" for lab, fin in labels.items()) + sysmsg = ("Ты — литературный судья zh→ru. Оцени ТРИ перевода одной главы по ОДНОЙ оси. " + "Ранжируй от лучшего к худшему. Приведи 2-4 span-цитаты-обоснования (≤15 слов каждая). " + 'Ответь СТРОГО JSON без markdown: {"ranking":["V?","V?","V?"],' + '"evidence":[{"label":"V?","quote":"≤15 слов","note":"почему"}]}.') + user = f"ОСЬ ОЦЕНКИ: {AXES[axis]}\n\nТРИ ВАРИАНТА:\n\n{blocks}\n\nРанжируй по оси. JSON." + s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 6000)) + in_est = C.count_tokens(sysmsg + user, "glm") + ok, pred = sp.guard(jm, in_est, s["max_tokens"]) + if not ok: + print(f" OVER-CAP {ch} {axis} {jm} pred=${pred:.3f}"); continue + text, err, usage = C.call(s, [{"role": "system", "content": sysmsg}, + {"role": "user", "content": user}], timeout=360) + cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "ch": ch, + "axis": axis, "rep": rep, "err": err, "usage": usage}) + rk = parse_rank(text) if not err else None + v = {"ch": ch, "axis": axis, "rep": rep, "judge": jm, "labels": labels, + "ranking_labels": rk, "err": err} + # map labels→finalist + if rk: + v["ranking_fin"] = [labels.get(x) for x in rk] + out.append(v) + with open(vpath, "a", encoding="utf-8") as f: + f.write(json.dumps(v, ensure_ascii=False) + "\n") + print(f" ch{ch} {axis:<8} rep{rep} {jm:<18} → {v.get('ranking_fin')} ${cst:.4f}") + summarize(out) + + +def parse_rank(text): + m = re.search(r'"ranking"\s*:\s*(\[[^\]]*\])', text) + if not m: + return None + try: + arr = json.loads(m.group(1)) + return [str(x) for x in arr] + except Exception: + return re.findall(r'V\d', m.group(1)) + + +def summarize(out): + from collections import defaultdict + import statistics + # Borda: 1st=2pts,2nd=1,3rd=0; per axis; per judge for leave-one-out + def borda(rows): + pts = defaultdict(list) + for v in rows: + rf = v.get("ranking_fin") + if not rf or len(rf) != 3 or None in rf: + continue + for i, fin in enumerate(rf): + pts[fin].append(2 - i) + return {f: round(statistics.mean(p), 2) for f, p in pts.items()} + print("\n=== РАНЖИРОВАНИЕ (Borda mean, 2=лучший) ===") + for axis in AXES: + rows = [v for v in out if v["axis"] == axis] + print(f"\n{axis}: overall {borda(rows)}") + judges = sorted(set(v["judge"] for v in rows)) + for j in judges: + print(f" only {j}: {borda([v for v in rows if v['judge']==j])}") + for j in judges: # leave-one-out + print(f" leave-out {j}: {borda([v for v in rows if v['judge']!=j])}") + + +if __name__ == "__main__": + run() diff --git a/eval/exp14_regate.py b/eval/exp14_regate.py new file mode 100644 index 0000000..e6a20f1 --- /dev/null +++ b/eval/exp14_regate.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""exp14 §3 — Fidelity re-gate пере-прогона (D34.3, ОБЯЗАТЕЛЕН; независим от §1-§2). + +Span-цитирующий, РАЗДЕЛЬНО стиль/верность, охота на КЛАСС ИНВЕРСИЙ РЕДАКТУРЫ (final vs draft +vs src: билингв-редактор мог купить гладкость ценой смысла), катастроф-скрин терминов. +author≠reviewer: судьи НЕ glm-5 (редактор) и НЕ deepseek (черновик) — кросс-семейно +gpt-5-mini + kimi (+ gemini точечно на флагах). leave-one-out. $ персист. + +Вход: rerun/records.json (source/draft/final). ch5.0/ch20.0 — final пуст (экспорт-баг D35.4a), +скорятся отдельно (не в среднем). Выход: exp14/regate_verdicts.jsonl + свод. + +Использование: exp14_regate.py [--dry] [--limit N] +""" +from __future__ import annotations +import argparse, json, re, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import exp14_common as C + +RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun") +RECS = json.load(open(RERUN / "records.json")) +JUDGES = ["gpt-5-mini", "kimi-k2.6"] # кросс-семейно к glm-5/deepseek; gemini — точечно вне этого прохода +CAPS = {"gpt-5-mini": 0.06, "kimi-k2.6": 0.10, "gemini-3.1-pro-preview": 0.30} + +SYS = ("Ты — билингвальный редактор-эксперт zh→ru, СУДЬЯ ВЕРНОСТИ. Тебе даны: ИСХОДНИК (zh), " + "ЧЕРНОВИК (ru) и ФИНАЛ после редактуры (ru). Оцени ВЕРНОСТЬ ФИНАЛА исходнику РАЗДЕЛЬНО от стиля. " + "Особо ищи КЛАСС ИНВЕРСИЙ РЕДАКТУРЫ: правки, где ФИНАЛ отошёл от смысла исходника ДАЛЬШЕ черновика " + "(редактор внёс смысл, которого в исходнике нет; перевернул полярность/участника/число; выбросил атом). " + "Ответь СТРОГО JSON без markdown: " + '{"fidelity":0-100,"style":0-100,"n_mistranslation":int,"n_omission":int,' + '"n_edit_inversion":int,"catastrophe":true|false,' + '"flags":[{"type":"inversion|omission|term|addition","span_src":"≤12 слов zh",' + '"quote_final":"≤15 слов ru","why":"кратко"}]}. ' + "catastrophe=true при инверсии полярности/участника/действия ИЛИ потере ключевого атома (заголовок/событие/термин).") + + +def run(): + ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true") + ap.add_argument("--limit", type=int, default=0); a = ap.parse_args() + sp = C.Spender(C.DIAG / "regate_costs.jsonl", CAPS) + vpath = C.DIAG / "regate_verdicts.jsonl" + done = set(); out = [] + if vpath.exists(): + for l in vpath.read_text(encoding="utf-8").splitlines(): + if l.strip(): + v = json.loads(l); out.append(v); done.add((v["chunk"], v["judge"])) + recs = [r for r in RECS if (r["chapter"], r["chunk_idx"]) not in {(5, 0), (20, 0)} + and (r.get("final") or "").strip()] + empties = [(r["chapter"], r["chunk_idx"]) for r in RECS if not (r.get("final") or "").strip()] + if a.limit: + recs = recs[:a.limit] + tasks = [(r, jm) for r in recs for jm in JUDGES + if (f"{r['chapter']}.{r['chunk_idx']}", jm) not in done] + print(f"re-gate: {len(recs)} chunks × {len(JUDGES)} judges = {len(tasks)} new tasks " + f"({len(done)} done); empty-final (экспорт-баг, вне скора): {empties}") + if a.dry: + est = sum(C.predict_cost(jm, 4000, 4000) for _, jm in tasks) + print(f"predicted ≤ ${est:.2f}"); return + for r, jm in tasks: + cid = f"{r['chapter']}.{r['chunk_idx']}" + user = (f"ИСХОДНИК (zh):\n{r['source']}\n\nЧЕРНОВИК (ru):\n{r['draft']}\n\n" + f"ФИНАЛ после редактуры (ru):\n{r['final']}\n\nОцени верность ФИНАЛА. Ответь JSON.") + # gpt-5-mini: reasoning ⊆ completion → нужен запас (medium@4000 съедал бюджет → empty); low@10000 + s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 10000), + reasoning_effort=("low" if jm == "gpt-5-mini" else None)) + in_est = C.count_tokens(SYS + user, "deepseek") + ok, pred = sp.guard(jm, in_est, s["max_tokens"]) + if not ok: + print(f" OVER-CAP {cid} {jm} pred=${pred:.3f}"); continue + text, err, usage = C.call(s, [{"role": "system", "content": SYS}, + {"role": "user", "content": user}], timeout=300) + cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "chunk": cid, + "err": err, "usage": usage}) + v = parse(text) if not err else {"fidelity": None, "err": err[:60]} + v.update(chunk=cid, judge=jm) + out.append(v) + with open(vpath, "a", encoding="utf-8") as f: + f.write(json.dumps(v, ensure_ascii=False) + "\n") + fl = len(v.get("flags", []) or []) + print(f" {cid:<7} {jm:<12} fid={v.get('fidelity')} style={v.get('style')} " + f"cat={v.get('catastrophe')} flags={fl} ${cst:.4f}") + summarize(out, empties) + + +def parse(text): + m = re.search(r"\{.*\}", text, re.S) + if not m: + return {"fidelity": None, "parse": text[:80]} + try: + d = json.loads(m.group(0)) + return {k: d.get(k) for k in ("fidelity", "style", "n_mistranslation", "n_omission", + "n_edit_inversion", "catastrophe", "flags")} + except Exception: + f = re.search(r'"fidelity"\s*:\s*(\d+)', m.group(0)) + cat = '"catastrophe": true' in m.group(0).lower() or '"catastrophe":true' in m.group(0).lower() + return {"fidelity": int(f.group(1)) if f else None, "catastrophe": cat, "parse_fallback": True} + + +def summarize(out, empties): + from collections import defaultdict + import statistics + byj = defaultdict(list); cats = []; invs = [] + for v in out: + if v.get("fidelity") is not None: + byj[v["judge"]].append(v["fidelity"]) + if v.get("catastrophe"): + cats.append((v["chunk"], v["judge"])) + for fl in (v.get("flags") or []): + if fl.get("type") == "inversion": + invs.append((v["chunk"], v["judge"], fl.get("quote_final", "")[:60])) + print("\n=== FIDELITY RE-GATE свод ===") + for j, xs in byj.items(): + print(f" {j}: mean fidelity {statistics.mean(xs):.1f} (n={len(xs)}, min={min(xs)})") + if len(byj) == 2: # leave-one-out + js = list(byj) + print(f" leave-one-out: без {js[0]} → {statistics.mean(byj[js[1]]):.1f}; без {js[1]} → {statistics.mean(byj[js[0]]):.1f}") + print(f" КАТАСТРОФ-флаги: {len(cats)} → {cats[:20]}") + print(f" ИНВЕРСИИ РЕДАКТУРЫ: {len(invs)}") + for c in invs[:15]: + print(f" {c[0]} [{c[1]}]: {c[2]}") + print(f" экспорт-баг пустые финалы (вне скора): {empties}") + + +if __name__ == "__main__": + run() diff --git a/eval/exp14_sizecurve.py b/eval/exp14_sizecurve.py new file mode 100644 index 0000000..dce6db1 --- /dev/null +++ b/eval/exp14_sizecurve.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""exp14 §кривая — размер-чанка ↔ качество ↔ биллинг ↔ ретраи. + +Пере-нарезка главы при разных бюджетах (жадная упаковка целых абзацев-строк до char-порога) ++ whole-chapter. Пайплайн draft(flash thinking-ON)→edit(glm-5 дискурс). Снимаем РЕАЛЬНЫЕ +in/out/reasoning-токены, латентность, ошибки-по-причинам, output-token-cost, KPI-абзацы. +Chapter-level инъекция едина для всех сегментов (изолируем РАЗМЕР, не глоссарий). + +Бюджет чанка отчитывается в ВЫХОДНЫХ токенах (реальный токенайзер glm-4.6). Оптимизация — +retry-adjusted output-cost. Оговорка: у этой вебновеллы плоская структура (1 предл./строка, +мало сцен-маркеров) → политика границ «сцена vs жадная» слабо различима; меряем жадную + whole. + +Использование: exp14_sizecurve.py [--chapters 17,13] [--dry] +""" +from __future__ import annotations +import argparse, json, re, statistics, sys, time +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import exp14_common as C +import exp14_prompts as P + +RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun") +RECS = {(r["chapter"], r["chunk_idx"]): r for r in json.load(open(RERUN / "records.json"))} +INJ = json.load(open(C.DIAG / "injection_blocks.json")) +SIZES = [800, 1600, 3200, 99999] # zh-chars; 99999 = whole-chapter +OUT = C.DIAG / "sizecurve"; OUT.mkdir(exist_ok=True) +CAPS = {"deepseek-v4-flash": 0.03, "glm-5": 0.08} + + +def chapter_source(ch): + cks = sorted(c for (cc, c) in RECS if cc == ch) + return "\n".join(RECS[(ch, c)]["source"] for c in cks) + + +def rechunk(src, budget): + """Жадная упаковка целых строк (абзацев) до budget zh-символов; строку не рвём.""" + lines = [l for l in src.split("\n") if l.strip()] + segs, cur, n = [], [], 0 + for l in lines: + if cur and n + len(l) > budget: + segs.append("\n".join(cur)); cur, n = [], 0 + cur.append(l); n += len(l) + if cur: + segs.append("\n".join(cur)) + return segs + + +def paras(t): return [p for p in re.split(r"\n\s*\n", t.strip()) if p.strip()] +def sents(p): return [s for s in re.split(r"(?<=[.!?…])\s+", p.strip()) if s.strip()] +def kpi(t): + narr = [p for p in paras(t) if not p.strip().startswith("—")] + sp = [len(sents(p)) for p in narr] or [0] + return round(statistics.mean(sp), 2), round(sum(1 for x in sp if x == 1)/len(sp), 3) + + +def run(): + ap = argparse.ArgumentParser(); ap.add_argument("--chapters", default="17,13") + ap.add_argument("--dry", action="store_true"); a = ap.parse_args() + chapters = [int(x) for x in a.chapters.split(",")] + sp = C.Spender(C.DIAG / "sizecurve_costs.jsonl", CAPS) + tsys = P.translator_system(); esys = P.editor_system("discourse") + rows = [] + for ch in chapters: + src = chapter_source(ch) + einj = INJ.get(f"{ch}/ALL", {}).get("editor_constraint", "") + ginj = INJ.get(f"{ch}/ALL", {}).get("bilingual_glossary", "") + for budget in SIZES: + segs = rechunk(src, budget) + label = "whole" if budget == 99999 else f"{budget}c" + tag = f"{ch}-{label}" + outp = OUT / f"{tag}.txt" + if outp.exists(): + print(f" [skip] {tag}"); continue + print(f"\n=== ch{ch} size={label}: {len(segs)} сегментов ===") + if a.dry: + for s in segs: + print(f" seg {len(s)}zh-chars") + continue + final_parts, in_tot, out_tot, reason_tot, seg_costs, errs, lat = [], 0, 0, 0, 0.0, [], 0.0 + for i, seg in enumerate(segs): + # DRAFT (flash, thinking ON) + ds = C.spec("deepseek-v4-flash", temp=0.3, max_tokens=8000) + dmsgs = C.messages_with_injection(tsys, ginj, P.translator_user(seg)) + t0 = time.time() + draft, derr, dusage = C.call(ds, dmsgs, timeout=300) + sp.record({"model": "deepseek-v4-flash", "keyenv": ds["keyenv"], "tag": tag, + "seg": i, "stage": "draft", "err": derr, "usage": dusage}) + if derr: + errs.append(f"draft:{derr[:30]}"); draft = seg # фолбэк — исходник (эхо-гард поймает) + # EDIT (glm-5 discourse) + es = C.spec("glm-5", temp=0.4, max_tokens=8000) + emsgs = C.messages_with_injection(esys, einj, P.editor_user(seg, draft)) + final, eerr, eusage = C.call(es, emsgs, timeout=360) + c = sp.record({"model": "glm-5", "keyenv": es["keyenv"], "tag": tag, "seg": i, + "stage": "edit", "err": eerr, "usage": eusage}) + lat += round(time.time() - t0, 1) + if eerr: + errs.append(f"edit:{eerr[:30]}"); final = draft + final_parts.append(final) + in_tot += (dusage.get("prompt_tokens", 0) or 0) + (eusage.get("prompt_tokens", 0) or 0) + out_tot += (dusage.get("completion_tokens", 0) or 0) + (eusage.get("completion_tokens", 0) or 0) + full = "\n\n".join(final_parts) + outp.write_text(full, encoding="utf-8") + ru_out_tok = C.count_tokens(full, "glm") + mean_spp, share1 = kpi(full) + han = sum(1 for ch_ in full if "一" <= ch_ <= "鿿") + row = dict(chapter=ch, size=label, n_seg=len(segs), in_tok=in_tot, out_tok=out_tot, + ru_out_tok=ru_out_tok, cost=round(sp.by_key.get("ZAI_API_KEY", 0)+sp.by_key.get("DEEPSEEK_API_KEY", 0), 4), + mean_spp=mean_spp, share_1sent=share1, han=han, errs=errs, latency_s=round(lat, 1)) + rows.append(row) + print(f" segs={len(segs)} in={in_tot} out={out_tot} ru_out_tok={ru_out_tok} " + f"mean_spp={mean_spp} share1={share1} CJK={han} errs={len(errs)} lat={lat:.0f}s") + (OUT / "sizecurve.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8") + if rows: + print("\n=== КРИВАЯ (per chapter×size) ===") + print(f"{'ch-size':<12}{'segs':>5}{'out_tok':>9}{'mean_spp':>9}{'share1':>8}{'CJK':>5}{'errs':>5}") + for r in rows: + print(f"{r['chapter']}-{r['size']:<8}{r['n_seg']:>5}{r['out_tok']:>9}{r['mean_spp']:>9}{r['share_1sent']:>8}{r['han']:>5}{len(r['errs']):>5}") + + +if __name__ == "__main__": + run()