Land exp12 quality diagnosis with orchestrator review banner: mono-editor passivity confirmed, unanimity and fidelity-gate claims corrected, glossary v2 status trap flagged

This commit is contained in:
Claude (backend session) 2026-07-11 05:05:40 +03:00
parent 4952673b22
commit 75db9e144f
11 changed files with 1382 additions and 1 deletions

View file

@ -823,7 +823,9 @@ keep-alive (Ф12), инъекция глоссария (`selective`), пор
**Раунд 2 (чтение владельца состоялось, §4 отчёта):** вердикт — **ни один из 7 не дотягивает до чтения** («машинный, модели не следят за сюжетом»). Новое: (1) **⚠ gemini (A5) ТЕЧЁТ преамбулой** в выход («Вот отредактированный перевод…», wire-verified 4 чанка) → дефект продакшн-редактора + раздул мою метрику активности A5; glm/grok чисто → **gemini понижен**. (2) **Глоссарий — сквозная первоклассная проблема** (владелец прав): все армы делят один сид; 修炼/人祖 — GAP (не в сиде, «совершенствование»/«Первопредок» = выбор модели → засидить культивация/Рен-Зу?), 蛊师/蛊虫/甲乙丙丁/花酒行者 — lock (гу-мастер/гу-тварь/разряд-Г/Винный-Странник → переголосовать с владельцем); таблица current→pref в §4.2, пере-курация = моя зона после утверждения. (3) **Идея «модель гуглит термины» (владелец) — в доках НЕТ**; ближайшее D25.5 (веб=недоверенные данные, webfetch=Ф3-блокер) → автолукап = Ф3+, near-term = засид глоссария из фан-конвенций; **пинг оркестратору** зафиксировать owner-proposal в архитектуру. (4) **Кандидат-фикс** (glm-билингв+реверстка+конвенции, `diag/arms/CAND/`) — конвенции ставит, БЕЗ утечки, но реверстку абзацев делает нестабильно → layout надёжнее отдельным reflow-пассом/сильной моделью. (5) **Монитор** `diag/reading/monitor.html` (локальный, вне хостинга — копирайт) — оригинал+варианты+кандидат, кнопки копировать для флагман-сверки владельца. Рекомендация §3 дополнена: **4-й равноправный фикс — пере-курация глоссария**; gemini понижен.
**Ждём:** (1) **флагман-сверка владельца** через `diag/reading/monitor.html` (Fable 5/5.6 + любительский перевод) — арбитр конфига. (2) **Пере-курация глоссария** — утверждение владельцем терминов (§4.2) → засид GAP + переголос lock → resnapshot. (3) **Оркестратору:** ратификация флипа D1 (моно→билингв) + бриф вёрстки + фиксация owner-proposal «автолукап терминов = Ф3» — D-блоком; дефолт сам не трогаю. Полигон №4 (сид мн.ч.) — второй приоритет, не начат.
**Раунд 3 (флагман-сверка состоялась, §5 отчёта):** владелец прогнал монитор через ДВА фронтир-флагмана (GPT + Claude) слепо + любительский релейный перевод (контроль). Un-blind по ключу монитора (`diag/reading/_РАСШИФРОВКА_ФЛАГМАНОВ.md`; полный разбор Claude — `ОЦЕНКА_ФЛАГМАНА.md`). **Корректность проверена:** оба читали `monitor.html` (8 вариантов — столько только у монитора) + `translate.txt`, ключи не открывали, конфаундинга меток монитор↔пакеты нет (чистые 8-ранги), «переработки» выведены чтением и un-blind'ятся ровно в {A5,A2,CAND} = доказательство слепоты; единственный de-blind — gemini сам палит утечкой (мой стриппер снял ведущую форму, пропустил хвостовую в `A5/5_1`). **Конфиг-итог (оба флагмана СОГЛАСНЫ):** gemini-билингв (GPT 7.7 / Claude ранг 1.0 — лучшее ЯДРО, но течёт) > glm-билингв (7.3/2.7) ≈ КАНДИДАТ (7.2/2.3) ≫ **glm-МОНО=этап A (6.2/6.0, низ — подтверждает «нечитаемо»)** ≈ черновик/grok-no-op (~5). **Тройная триангуляция** (механика §2.2 + судьи §2.3 + 2 флагмана): билингв-переработка ≫ моно/пассив; практич. выбор — glm-билингв/КАНДИДАТ. **Флагманы добавили сверх:** (а) никто не publishable («крепкий сетевой», не издательская проза); (б) чтобы победить фан — 3 вещи: принудительный глоссарий + евро-вёрстка с тире + сноски на аллюзии; (в) нужен **output-санитайзер** (утёкшие заметки/непереведённые слова/латиница-в-кириллице/диакритики/битые словоформы — новый класс дефектов вне гейтов); (г) сквозные ошибки ЧЕРНОВИКА deepseek (族长≠家老, 本命蛊, 长生=бессмертие не долголетие, 失神, 花酒行者 теряет 花); (д) экспертные глоссарии от обоих — материал для пере-курации. **Стратегия честно:** сценарий A+B — инкремент даёт «лучше фана», но «издательский уровень» требует более сильного ЯДРА (переводчик, пилот Ф2.5) → приоритет ядра растёт.
**Ждём:** (1) **Пере-курация глоссария** — утверждение владельцем терминов (§4.2 + флагман-таблицы: врата→апертура, 真元, 本命蛊→жизненный гу, 长生→бессмертие, 族长/家老; спорное — Первопредок/Жэнь Цзу, культивация/совершенствование — за владельцем) → засид GAP + переголос lock → resnapshot. (2) **Оркестратору:** ратификация флипа D1 (моно→билингв) + бриф вёрстки + **новые задачи**: output-санитайзер (класс «мгновенной нечитаемости»), сноски-на-аллюзии, и — по сигналу флагманов «издательский уровень требует ядра» — поднять приоритет выбора базового переводчика/пилота Ф2.5; фиксация owner-proposal «автолукап терминов = Ф3». Дефолт сам не трогаю. (3) Полигон №4 (сид мн.ч.) — второй приоритет, не начат.
### 2026-07-10 — Сессия «erotica-срез 18+ + fix-лист + финализация сида» (закрытие D14.4) ЗАВЕРШЕНА

View file

@ -0,0 +1,259 @@
# Эксперимент 12. Диагностика качества 25 глав (蛊真人 zh→ru) — где теряется читаемость
> **⚠ Ревью-шапка оркестратора (12.07, D30; тело ниже не редактировалось).** Вердикт адверсариального ревью (4 оси: реплика метрик активности / аудит судейского слоя / root-cause против промптов и store / scope+COGS+мемо): **ACCEPT_WITH_FIXES — принято; ратификация D30.** Ядро диагноза подтверждено независимой репликой: пассивность моно-редактора (3/6 grok-чанков байт-идентичны черновику; медиана draft→final 0.978 по всем 54; порядок армов leak-robust под тремя метриками), «Сохраняй разбивку на абзацы» дословно в `translator.md:9`/`editor.md:8`, дефекты сцены смерти — в ЧЕРНОВИКЕ, риг A1≈прод (2/6 байт-идентичны боевым финалам). Поправки чтения:
> 1. **«A2 — единогласный #1 у всех судей» — НЕВЕРНО**: gpt-5-mini ставил A5 первым 9/9 повторов; среди судей, видевших оба арма, A5 бьёт A2 12/18; #1 A2 держится на gemini (который A5 структурно не судил — self-family) и grok. Выбор glm-5-кандидата стоит на **цене (×13 дешевле gemini) и отсутствии утечек**, не на единогласии.
> 2. **«Fidelity-гейт „гладко-неверное" ПРОЙДЕН / верность 5.0» — снято**: скалярные голоса без спан-цитирования (промпт судьи запрещал рационале), grok судил reasoning-off (конфиг, который §2.2 сам доказал инертным), gpt-5-mini дал 5.0 ВСЕМ армам включая сырой черновик; A2 при этом сам ВНЁС инверсию смысла («пожертвуй мной» — в черновике нет) и порчу («молва считал»), сохранил «долголетие». Читать: «относительного падения верности сжатые голоса не задетектировали; корробировано точечными zh-кейсами». **Re-gate верности — обязательное условие приёмки пере-прогона** (span-цитирующий судья reasoning-ON + охота на класс инверсий; «порчи правки» — в скоуп санитайзера).
> 3. Утечка A5 — **6/6 чанков** (ведущие преамбулы во всех + хвост в 5_1; до 33% символов чанка), не «4»; судейские оценки A5 — нижняя граница (вход контаминирован). Активность A5 после стрипа 0.310.33 — ранжирование не меняется.
> 4. §2.2: значения таблицы — **средние** по 6 чанкам, не «медианы» (медианы: A2 0.099, A5 0.381; порядок тот же). CAND гнался на temp=0.5 (боевой 0.4) — не дисклоужено; «конвенции ставит» — заслуга механизма глоссария, не CAND-поведения; reflow-слияние реально в 2/6 чанков (гл.5 — нет).
> 5. **Глоссарий v2 — мина статусов**: спорные термины (修炼/культивация, 人祖/Первопредок и др.) помечены ⚑ только в note, но несут `status:approved` → пайплайн замапил бы их в CONFIRMED-hard-constraints ДО подписи владельца (`memory.go:419/473`); 花酒行者 и 本命蛊 тихо повышены draft→approved, причём 花酒行者 перезаписывает высказанное пожелание владельца. **До применения: спорные → `status:draft`** (D30.5). Сам v2-сид и владельческая делегация в отчёте не раскрыты (только докстринг скрипта) — дефект провенанса, зафиксирован.
> 6. Бюджет: финал ≈**$2.262.27** из $5 (отчётное «$2.22» не включает CAND ~$0.044 — скрипт выбрасывает usage; впредь каждый платный скрипт персистит usage/cost). Пре-регистрация: gpt-5-mini аггрегирован полной панелью (пре-рег: только tie-breaker) и один grok-вердикт неполный (A0 выпал) — пересчёт ревью: выводы не меняются; дисклоужено здесь.
> 7. Мемо: «одна PD-книга» — неверно (蛊真人 — копирайтная вебновелла; корректный гейт — «современный вебновелл-срез ВНЕ претрейна»); §5.2 COGS не подписан as-is: **флип D1 = +1525% (~$0.830.86/ранобэ; замерено: вход редактора +60% токенов, книга +19%)**; «$1.52» = опция Б (селективный апекс exp08 + память) — отдельное позднее решение владельца. Два стратегических клейма («publishable чистым LLM = фронтир-предел», «серия-консистентность = самый дорогой слой») — рабочие гипотезы.
**Статус:** безκ-режим, **N=1 читатель (владелец)**, **пре-скрин** пилота Ф2.5 — НЕ пилот. Результаты не выдавать за пилотный вердикт (D26.2, D25.2-гейт билингв-якоря в силе).
**Мандат:** D26 (качество-первым). Триггер: владелец прочитал 25 глав этапа A (draft `deepseek-v4-flash` → editor `glm-5` **моно**) → **нечитаемо, слабейшее звено — редактура**. Задача: локализовать, где теряется качество, и найти **конфиг-кандидата** для пере-прогона 25 глав. Смену дефолта ратифицирует оркестратор (D-блоком) — не эта сессия.
**Зона:** `eval/` + этот файл + секция PROGRESS «Полигон». Бэкенд не трогаю (там пакет №5). Артефакты приёмки — только копией; сэмплы/выходы — `/home/ubuntu/books/gu-zhenren/diag/` (ВНЕ git; в доке — числа и цитаты ≤15 слов).
---
## 0. Онбординг-факты (контракт, на которых стоит дизайн)
- **exp04 (D3):** слепой бейк-офф редакторов на 4 PD-фрагментах, БИЛИНГВАЛЬНО, 2 судьи: стиль **gemini > grok > kimi > glm**; верность **grok > gemini > glm > kimi**; value **grok ≫**. НО (D13.5) слепота exp04 структурно сломана (ключ в том же артефакте) → grok-дефолт **провизорен**; и всё мерено билингвально на классике из претрейна.
- **D1/D17:** редактор Ф1 **монолингвальный** (не видит исходник) — гипотеза «исходник в контексте ⇒ кальки» ПОНИЖЕНА до гипотезы (в литературе не подтверждена; кальки документированы как болезнь ЧЕРНОВИКА). D13.1: mono-рефайнмент теряет accuracy с первого прохода (2.2…5.6 MQM, arXiv:2605.13368), general-рефайнмент С исходником даёт тот же fluency при accuracy ~flat.
- **glm-5 как редактор никем не выбирался лучшим** — это замена D20.3 (нет чистого xAI-ключа); GLM-линейка — слабейшая в exp04 (мерян glm-4.6), glm-5 не перемерян.
- **grok-биллинг quirks:** редактор-роль (ru-вход) → `reasoning_effort:"none"` (плоско в теле), `reasoning_tokens=0`. gemini-3.1-pro: thinking обязателен (биллится как output), `max_tokens ≥8000`. glm: `extra_body {thinking:{type:disabled}}`.
- **xAI-ключ:** текущий (платный, без data-sharing) допустим для «eval + приёмочная книга владельца» (D19.4/D20.3). Чистый ключ нужен пилоту/боевым книгам, НЕ exp12.
---
## 1. ПРЕ-РЕГИСТРАЦИЯ (заморожена 2026-07-11, ДО платных вызовов)
Всё ниже зафиксировано до генерации армов и оценки. Промпты армов **заморожены** — никакого промпт-инжиниринга по ходу (урок exp04: сломанная слепота уже раз дала дефолт-редактора).
### 1.1. Материал — 3 чистые главы, выбор по формуле (не черри-пик)
**Frame:** ЧИСТЫЕ главы этапа A = все чанки `disposition=ok` на ОБЕИХ стадиях (draft+edit), `flag_reason` пуст. Из 25: главы 1 (эхо cjk_artifact) и 10 (length) исключены как флагнутые → frame = **23 главы** {29, 1125}.
**Метрика на ИСХОДНИКЕ zh (детерминированная):** `D` = доля символов внутри кавычек «“…”» (U+201C…U+201D) от не-пробельных символов главы. Диалого-плотность.
**Правило выбора (frozen):** `high = argmax D`, `low = argmin D`, `mid = ближайшая к медиане D`. Тай-брейк — меньший номер главы. Скрипт: `eval/exp12_extract.py` (воспроизводимо).
| Регистр | Глава | D | чанков | zh-симв. | реализованный характер |
|---|---|---|---|---|---|
| **диалого-плотная** | **гл. 7** | 0.490 | 2 | 2701 | диалог (испытание таланта: реплики, оценка ранга) |
| **смешанная** (медиана) | **гл. 5** | 0.222 | 2 | 2050 | нарратив+диалог (первое пробуждение гу) |
| **экспозиция** | **гл. 14** | 0.022 | 2 | 3040 | описание/нарратив (почти без прямой речи) |
Медиана D по frame = 0.2215 (гл. 5 попала ровно на медиану). Оговорка честно: **чистого «экшена»/боя в главах 225 нет** (ранняя арка деревни Цинмао — экспозиция и диалог; бои гу — позже), поэтому реализованная ось разнообразия = **диалого-плотность (high/mid/low)**, спан регистра, который реально присутствует в срезе. Гл. 18 (известный alias-дрейф 古月方源, D24.2) правилом НЕ выбрана — конфаунд исключён естественно.
**Наблюдение (уже видно из инъекции):** гл. 7 несёт **1317** констрейнт-терминов/чанк (плотная терминосистема рангов), гл. 5 — 3, гл. 14 — 69. Гл. 7 — лучший стенд гипотезы A6 «инъекция деревянит стиль».
### 1.2. Армы (на ОДНИХ И ТЕХ ЖЕ черновиках; редактор-стадия варьируется)
Черновик A0 и его глоссарная инъекция общие для всех редакторских армов. Реконструкция инъекции — из `retrieval_state.injected_ids` приёмочного store, воспроизводит ровно блок, что видел боевой glm-5 (`renderEditorConstraintBlock`/`renderGlossaryBlock`, `eval/exp12_blocks.py`): editor-констрейнт = CONFIRMED dst-формы «- «dst»»; билингв-глоссарий = «src → dst». Sticky (A5) внутри главы = union chunk0chunk1.
| Арм | Редактор | Режим | Констрейнты | Источник | Стоимость |
|---|---|---|---|---|---|
| **A0** | — (черновик deepseek-v4-flash) | — | — | store | $0 |
| **A1** | glm-5 | моно | да | **store (= финал этапа A)** | $0 |
| **A1** | glm-5 | моно | да | rig (контроль верности рига) | ~$0.06 |
| **A2** | glm-5 | билингв | да | rig | платно |
| **A3** | grok-4.3 | моно | да | rig | платно |
| **A4** | grok-4.3 | билингв | да | rig | платно |
| **A5** | gemini-3.1-pro-preview | билингв | да | rig | платно |
| **A6** | grok-4.3 | моно | **НЕТ** | rig | платно |
**A1 (контроль):** glm-5-моно моим ригом с реконструированным блоком — QC, что rig ≈ store (иначе A1↔A2-контраст конфаундится источником блока). НЕ в пакете чтения владельца.
**Промпты (заморожены):**
- **Моно-редактор** (A1,A1,A3,A6): боевой `backend/prompts/editor.md` **байт-в-байт** (system с рендер-варами book.yaml: genre=вебновелла, audience=взрослые…, title=蛊真人, source_lang=zh, honorifics=keep, venuti=0.60). Layout: `system(шаблон)``system(editor-констрейнт-блок)``user(«Черновик…\n\n{draft}»)` (render.go). A6 — тот же промпт БЕЗ второго system-сообщения (нет констрейнт-блока).
- **Билингв-редактор** (A2,A4,A5): тот же system + **одна пре-регистрированная строка**: `«Тебе также дан ИСХОДНЫЙ ТЕКСТ: сверяйся с ним по смыслу и исправляй искажения черновика, сохраняя полноту.»`; инъекция = билингв-глоссарий (src→dst); user = `«ИСХОДНЫЙ ТЕКСТ (zh):\n\n{source}\n\n---\n\nЧерновик перевода для редактуры:\n\n{draft}»`. Минимальная модификация, БЕЗ итераций.
**Параметры (заморожены, по quirks):** все редакторы `temperature=0.4` (боевая edit-стадия). glm-5: `extra_body {thinking:{type:disabled}}`, `max_tokens=8000`. grok-4.3: `extra_body {reasoning_effort:"none"}`, `max_tokens=8000`. gemini-3.1-pro-preview: `max_tokens=16000` (thinking обязателен, биллится как output). Слаги сверяю live `/models` перед вызовами; аномалия → вендор-дока, не гадание.
### 1.3. Бюджет (пре-регистрирован)
Редакторских вызовов: 6 армов (A1,A2,A3,A4,A5,A6) × 3 главы × 2 чанка = **36 вызовов**. Оценка выхода ~2.54k токенов/вызов. Прайсы (models.yaml): glm-5 ~$3.2/M, grok-4.3 ~$2.5/M out, gemini-3.1-pro ~$12/M out (+thinking). Оценка армов: glm 12×~$0.012 + grok 18×~$0.010 + gemini 6×~$0.05(+think) ≈ **$0.61.1**. Судьи (см. 1.5): ~2 семейства × 3 главы × (rank-свапы + fidelity) ≈ **$0.51.5** (gemini thinking — главный драйвер). **Кап $5.** Санити-стоп при 80% капа.
### 1.4. Оценка — слой 1: слепое чтение владельца (ГЛАВНЫЙ сигнал)
- Пакет по каждой главе: 7 вариантов (A0,A1,A2,A3,A4,A5,A6) в **случайном порядке**, метки `V1…V7` **пере-рандомизированы ПО ФРАГМЕНТАМ** (соль на главу), ключ соответствий — в ОТДЕЛЬНОМ файле вне пакета (`diag/reading_pack/KEY.*`, не в пакете чтения). Слепота свята (D13.5).
- Формат ответа владельца на главу: **ранжирование V1…V7** + бинарный гейт **«читаемо да/нет»** по каждому + свободные пометки. Целевое время: **~12 часа** на все 3 главы.
- A1 и любые служебные тексты в пакет чтения НЕ кладутся.
### 1.5. Оценка — слой 2: LLM-судьи (ВТОРИЧНО)
- **2 кросс-семейных судьи:** gemini-3.1-pro-preview + grok-4.3. **Семейство не судит свои выходы** (D13.3г): grok НЕ судит A3/A4/A6; gemini НЕ судит A5. glm/deepseek-армы судят оба. Плюс **нейтральный тай-брейкер** gpt-5-mini (OpenAI, армов нет → судит все) — только для позиций, где кросс-семейное правило оставляет 1 судью.
- **Ось СТИЛЬ:** listwise-ранжирование доступных судье армов на главу, презентация СЛЕПАЯ (метки пере-рандомизированы), **≥3 повтора со свапом порядка предъявления**, агрегат — **медиана ранга**.
- **Ось ВЕРНОСТЬ (билингвально, против zh-исходника!):** каждый судья оценивает каждый арм 15 против исходника, ≥3 повтора, медиана. Раздельно от стиля.
- **Гейт «гладко-неверное» (D1.1):** арм, выигравший ЧТЕНИЕ владельца, обязателен к билингв-сверке смысла — если он в низах верности, это фиксируется явно, «гладко-неверное» не побеждает молча.
- **Слой 3 (третичный, ultracode):** слепая читательская панель Claude-агентов (Anthropic — вне армов, кросс-семейно; свободна от grok/gemini self-judging) — доп. ранжирование по стилю на тех же слепых пакетах. Информативно, не замена слоям 12.
### 1.6. Root-cause разбор текущих 25 глав
Владелец даёт **1020 худших мест** своего чтения → каждое классифицирую по фактам (draft vs final vs исходник): `ошибка черновика (смысл)` / `редактор ухудшил/не спас` / `скованность от инъекции терминов` / `оформление диалогов/пунктуация` / `другое`. Распределение + примеры (цитаты ≤15 слов) — в отчёт.
До получения мест владельца — **предварительный автоматический разбор** (draft↔final diff по всем 25 главам: где редактор менял мало/много, дефис-диалоги, канцелярит-маркеры) как затравка.
---
## 2. РЕЗУЛЬТАТЫ
Армы: 36 вызовов, 0 ошибок, все finish=stop, **$0.779**. Судьи: 27 вызовов, **$1.31**. Демо §2.6: 1 вызов ~$0.13. **Итого потрачено ≈ $2.22 из капа $5.** Скрипты: `eval/exp12_*.py`; артефакты (тексты, JSON) — `/home/ubuntu/books/gu-zhenren/diag/` (ВНЕ git). Дефолт-конфиг НЕ менялся (D26.2).
### 2.1. Контроль рига (A1 ≈ A1) — ПРОЙДЕН
A1 (glm-5-моно моим ригом) vs A1 (store, боевой): similarity слов **0.9841.0** по 6 чанкам. Rig воспроизводит боевого редактора → контрасты A1↔A2 и др. не конфаундятся источником блока. Реконструкция инъекции верна.
### 2.2. МЕХАНИЧЕСКАЯ находка — редактор в моно-режиме ПАССИВЕН (ядро диагноза)
Активность редактора = `1 similarity(final, draft)` (выше = больше переписал), медиана по 6 чанкам глав 5/7/14:
| Арм | Активность | Чтение |
|---|---|---|
| A3 grok-4.3 моно | **0.001** | буквальный **no-op** (3/6 чанков char-identical черновику) |
| A6 grok-4.3 моно без констрейнтов | 0.004 | no-op (снятие констрейнтов почти ничего не меняет) |
| A4 grok-4.3 **билингв** | 0.006 | **всё ещё no-op даже с исходником** |
| A1 glm-5 моно (= этап A) | 0.013 | почти пассивен (12 правки на абзац, остальное копирует) |
| A2 glm-5 билингв | 0.137 | активен (~14% переписал, сильнее на нарративе) |
| A5 gemini-3.1-pro билингв | **0.343** | реально перерабатывает |
**Два жёстких вывода (мех., до чтения владельца):**
1. **Монолингвальный режим (оспоренный D1) → почти нулевая редактура** у обеих моделей (glm 0.013, grok 0.001). На всём срезе этапа A (54 чистых чанка) медиана draft→final similarity = **0.978**, редактор снял **0** канцелярит-маркеров. То, что владелец назвал «нечитаемо, слабейшее звено редактура» = **редактор почти не редактирует**; нечитаемость унаследована от черновика и не спасена.
2. **grok-4.3 при `reasoning_effort:"none"` (боевой конфиг!) инертен в ОБОИХ режимах** (0.001 / 0.006). exp04-ранг-1 grok был на ДЕФОЛТНОМ reasoning (low) и БИЛИНГВАЛЬНО; боевой reasoning-off превращает его в no-op-редактор. Это ровно «quality-transfer риск» exp08/D26, подтверждённый числами. **Grok как редактор жизнеспособен только с reasoning-ON** (аддитивный биллинг — заблокирован до резерва в EstimateUSD, D6.2/F3; отдельная проба — follow-up).
3. Реально редактируют только **gemini-биллингв (0.34)** и **glm-биллингв (0.14)**оба видят исходник. Активность ≠ качество (может плодить кальки — исходный страх D1); вердикт «лучше ли» — за чтением владельца (§2.4 primary) + fidelity-судьёй (§2.3, «гладко-неверное»).
### 2.3. LLM-судьи (вторично) — билингв побеждает, fidelity-гейт ПРОЙДЕН
Панель gemini-3.1-pro + grok-4.3 + gpt-5-mini (кросс-семейно, self-family exclusion соблюдён: gemini не судил A5, grok не судил A3/A4/A6), 3 повтора со свапом позиций, слепые метки. Парс **26/27 (96%)**, **$1.31** (кап $3). Стиль = медиана нормализ. ранга (0=лучший…1=худший), верность = медиана 15 билингвально против zh.
| Арм | Стиль (0=лучш) | Верность | Читается как |
|---|---|---|---|
| **A2 glm-5 билингв** | **0.056** | **5.0** | **#1 у ВСЕХ трёх судей** (gemini 0.0 / grok 0.0 / gpt5 0.17) |
| A5 gemini-3.1-pro билингв | 0.167 | 4.5 | #2 |
| A1 glm-5 моно (= этап A) | 0.456 | 4.33 | середина |
| A4 grok-4.3 билингв | 0.633 | 4.0 | низ |
| A0 черновик | 0.656 | 4.0 | низ |
| A6 grok моно без констрейнтов | 0.716 | 4.5 | низ |
| A3 grok-4.3 моно | 0.817 | 4.25 | **худший** |
**Выводы судей (вторично):** (1) **билингв ≫ моно** — A2/A5 наверху, моно/grok-off внизу; консенсус на A2 единогласный. (2) **Fidelity-гейт «гладко-неверное» ПРОЙДЕН:** победитель стиля A2 одновременно **максимум верности (5.0)** — билингв НЕ купил гладкость ценой смысла (исходный страх кальки D1 на этом срезе не подтвердился). (3) grok-off-армы (A3 худший, A6 предпоследний) — ниже даже сырого черновика: корроборирует no-op §2.2.
**⚠ Оговорка (важно):** судьи сравнивают ПОЛИРОВКУ прозы при ОДИНАКОВОЙ построчной вёрстке (все армы её сохраняют — §2.5-S) → они **недооценивают** структурный дефект, который доминировал в вердикте владельца. Судьи валидируют выбор **модель×режим** (билингв-glm), но НЕ фикс вёрстки (§2.6) — его подтверждают демо + чтение владельца. Плюс верность сжата (gpt-5-mini всем 5.0 — слабый дискриминатор); дискриминативнее ось стиля. A1 (моно, что читал владелец) у судей 3/7 — мягче человеческого вердикта ровно потому, что вёрстку они не штрафуют.
### 2.4. Слепое чтение владельца (ГЛАВНЫЙ сигнал)
*(pending — пакеты `diag/reading/глава_{5,7,14}.md`, 7 слепых вариантов/глава; ключ `_КЛЮЧ_НЕ_ОТКРЫВАТЬ.json` держать закрытым до ответов)*
### 2.5. Root-cause — разбор мест владельца (второй трек, D26.2)
Владелец прислал ~8 кейсов из прочитанных 25 глав (все — пролог гл.1, сцена смерти 第一节, editor=glm-5, чанки редактировались — не сырой черновик). Классификация по факту (исходник zh сверен):
| # | Место (RU, ≤12 слов) | Исходник zh | Диагноз | Класс |
|---|---|---|---|---|
| **S** | каждое предложение — отдельный абзац | 一句一行 (кит. вебновелла) | оба промпта велят «Сохраняй разбивку на абзацы» → пайплайн ВЕРНО копирует кит. построчную вёрстку, для ru — деструктивно | **оформление/дизайн промпта (ВСЕ главы)** |
| 1 | «три шичэна» | 三个时辰 | 时辰 (≈2 ч) не переведён/не глоссирован; транслит непоследователен («шичэна»/«шичэня») | черновик-realia; редактор не спас; глоссарий-пробел |
| 2 | «фракции праведного пути» | 正道各大派 | 派 = секта/школа (конвенция сянься); «фракции» — генерик-калька | черновик-жанр; редактор не спас |
| 3 | «затем ударил головой» | 磕头 | 磕头 = земной поклон; «ударил головой» — калька (磕 бить + 头 голова) | черновик-калька жеста; редактор не спас |
| 4 | «чему ты смеёшься» | 你笑什么 | грамматично, но неживо в реплике; «чего/над чем/почему» естественнее | стиль; редактор не сгладил |
| 5 | «отнял мою невинность» | 夺走了我的清白之身 | 清白之身 = целомудрие → «невинность» ВЕРНА; «обесчестил» читается натуральнее | стиль (НЕ ошибка верности; «гладко-неверное» не подтвердилось) |
| 6 | «ради совершенствования цикады» | 为了练成春秋蝉 | 练成 (выпестовать гу) → «совершенствование цикады» криво; «взрастить/вскормить» точнее | черновик-слововыбор; редактор не спас |
| 7 | «Весенне-осеннюю цикаду» | 春秋蝉 | approved-dst сида; владелец хочет «Цикада Весны и Осени»; констрейнт ЛОЧИТ редактора на текущей форме | курация глоссария (зона полигона) |
**Распределение:** оформление 1 (но бьёт по всем главам, №1 по воспринимаемости) · ошибки черновика, не спасённые редактором 4 (№1,2,3,6) · стиль 12 (№4, частично №5) · курация глоссария 12 (№7, №1) · ошибок верности (smooth-wrong) — 0 подтверждённых (№5 проверен — верен).
**Ключевой вывод:** почти все кейсы — ошибки ЧЕРНОВИКА (deepseek), а не редактора. Но **монолингвальный редактор структурно НЕ МОЖЕТ их починить**: видя в черновике «ударил головой», без исходника он не знает, что там 磕头 = земной поклон. То есть моно-редактор (D1) ограничен вдвойне — он и пассивен, и слеп к искажениям смысла. Чинит такие места только билингв-редактор (видит 磕头/派/时辰) или лучший черновик.
### 2.6. Экслораторная демонстрация фикса (пост-фидбек, ВНЕ слепого бейк-оффа)
Один вызов gemini-3.1-pro билингв на сцене смерти (пролог гл.1) с промптом, ЯВНО разрешающим переверстать абзацы по-европейски + чинить конвенции (派→секты, 磕头→земной поклон, 时辰→перевод). Результат (`diag/reflow_demo/reflow_gemini.txt`):
- **абзацы 49 → 25** строк (нарратив собран в логические абзацы, реплики — отдельными строками с тире);
- «фракции» → «**школы** Праведного пути»; «ударил головой» → устранено; «шичэн» → переведён; «отнял невинность» → «**обесчестил меня**» (предпочтение владельца);
- бонус: идиомы, сплющенные черновиком, восстановлены (罄竹难书, 诛九族).
Один вызов чинит ПРАКТИЧЕСКИ ВСЕ кейсы владельца сразу → фикс лежит в связке **билингв-редактор + разрешение реверстать абзацы + жанровые правила в бриф**, а не в одной смене модели.
---
## 3. РЕКОМЕНДАЦИЯ КОНФИГА (provisional — до чтения владельца; смену дефолта ратифицирует оркестратор, D-блоком)
Диагноз читаемости — НЕ «плохая одна ллмка», а связка трёх ортогональных дефектов, каждый со своим фиксом:
1. **Оформление (самый дешёвый, самый заметный):** снять из брифа переводчика И редактора «Сохраняй разбивку на абзацы» для zh→ru; дать редактору ЯВНОЕ разрешение/мандат собирать предложения в логические европейские абзацы (реплики — отдельно). Демо §2.6 подтверждает эффект. Правит промпты → `prompt_version` bump → resnapshot.
2. **Режим редактора (ядро):** **билингв, не моно** — моно-режим даёт почти нулевую редактуру у всех моделей и структурно слеп к искажениям черновика (§2.2, §2.5). Это прямой флип оспоренного **D1/D17** — ратификация оркестратора обязательна. Fidelity-судья (§2.3) должен подтвердить, что билингв не плодит кальки/«гладко-неверное» (исходный страх D1).
3. **Модель редактора:** кандидат по value — **glm-5 билингв****единогласный #1 у судей по стилю И максимум верности 5.0** (§2.3, fidelity-гейт пройден), активен 0.14, **$0.42/25 гл** editor-стадия, ~$0.019/глава конфиг целиком; премиум-потолок — **gemini-3.1-pro билингв** (#2 у судей, активен 0.34, чинит конвенции в демо, но **$5.6/25 гл** ≈ ×13 → селективно, не дефолт). **grok-4.3 reasoning-off — снять с рассмотрения как редактор** (инертен §2.2, худший у судей §2.3); при желании грока — отдельная проба reasoning-ON (упирается в D6.2-резерв).
4. **Черновик (корень большинства кейсов):** ошибки realia/калек/слововыбора рождаются на стадии draft (deepseek). Билингв-редактор их частично чинит; полное лечение — сильнее черновик и/или глоссарий realia. → вход в пилот Ф2.5 (draft-модель — часть ядра).
5. **Глоссарий (зона полигона):** пере-курация с владельцем — 春秋蝉 «Цикада Весны и Осени»? · seed 派→секта/школа · 时辰→«стража»/глосса · 磕头 в realia. Правит memory content-hash → тот же resnapshot.
**Дорожка:** ратификация оркестратором (режим+оформление = D-блок, флип D1) → пере-прогон 25 глав кандидатом (glm-5 билингв + реверстка + правки глоссария, ~$0.5) → чтение владельца → читаемо → этап B. Чтение владельца (§2.4) — арбитр между glm-биллингв и gemini-премиум.
**Что НЕ делаю (дисциплина):** дефолт-конфиг сам не меняю (D26.2); пре-регистрацию §1 не трогаю; §2.6 и grok-ON — помечены экслораторными, вне слепого бейк-оффа.
---
## 4. ВТОРОЙ РАУНД — чтение владельцем + монитор + глоссарий как первоклассная проблема
Владелец прочитал слепые пакеты и дал развёрнутый фидбек. Вердикт: **ни один из 7 вариантов не дотягивает до чтения** — «машинный перевод, модели не следят за смыслом/сюжетом». Слепой ранг (пост-фактум расшифрован): в гл.5 лучший — gemini-билингв (A5), в гл.7 gemini (A5) **испортил** себя утечкой преамбулы. Новые находки:
1. **⚠ gemini-3.1-pro (A5) ТЕЧЁТ служебной преамбулой** в выход: «Вот отредактированный перевод… с соблюдением всех терминов из глоссария…» перед текстом (wire-verified в 7_0, 7_1, 14_0, 14_1). glm/grok — чисто. Следствия: (а) реальный дефект gemini как продакшн-редактора (нужна пост-очистка вывода или он засорит книгу); (б) моя метрика активности A5 (0.34) частично **раздута** этой преамбулой; (в) судьи видели её (A5 всё равно #2 — но с оговоркой). **gemini понижается**: премиум-прозу даёт, но не follow-instructions-надёжен.
2. **Глоссарий — первоклассная, сквозная проблема (владелец прав).** Все констрейнт-армы делят ОДИН реконструированный сид → все наследуют его dst. Разбор терминов владельца (сверено с сидом):
| src | текущий dst (сид) | тип | владелец хочет | заметка полигона |
|---|---|---|---|---|
| 修炼 | *(НЕ в сиде → «совершенствование» = выбор модели)* | GAP | **культивация** | согласен, жанр-стандарт; засидить |
| 人祖 | *(НЕ в сиде → «Первопредок» = выбор модели)* | GAP | Рен Зу / имя | 人祖=Жэнь Цзу, конкретный Праотец; имя+глосса ИЛИ «Праотец»; на решение владельца |
| 蛊师 | гу-мастер | lock | Мастер Гу | ⚠ 蛊真人 (титул ГГ) ≠ 蛊师 (профессия); «Мастер Гу» под профессию спорно; обсудить |
| 蛊虫 | гу-тварь | lock | Черви Гу | 虫=червь; «гу-червь»/«черви гу» ок |
| 甲乙丙丁等 | разряд А/Б/В/Г | lock | ранг | ⚠ это ГРАДАЦИЯ ТАЛАНТА, НЕ ранг гу-мастера (转=ранг 19); «ранг» уже занят; вариант «класс/сорт таланта A» |
| 花酒行者 | Винный Странник | lock | Монах Цветочного Вина | 花酒=цветочное вино, 行者=странник/паломник-монах; «Странник Цветочного Вина»? обсудить |
| 真元 | истинная энергия | lock | (?) | владелец засомневался; вариант «изначальная ци»/оставить |
**пере-курация сида с владельцем (моя зона, после утверждения каждого термина).** GAP-термины (修炼/人祖) — засидить; lock-термины — переголосовать. Меняет memory content-hash → resnapshot.
3. **Идея «модель гуглит термины» (владелец спросил «мы это где-то упоминаем?») — В ДОКАХ НЕТ.** Ближайшее: D25.5 (веб = **недоверенные ДАННЫЕ**, webfetch за action-security-гейтом = блокер **Ф3**) и детерминированный Terminologist/глоссарий (не веб-лукап). → Автолукап жанр-терминов = архитектурно Ф3+ (нужен action-gate), НЕ Ф1. Практический near-term эквивалент без веба = **засидить глоссарий из устоявшихся фан-конвенций** (п.2). **Пинг оркестратору:** зафиксировать это как owner-raised предложение в архитектуру (моя зона доков — только exp12).
4. **Реверстка абзацев в слепых армах ОТСУТСТВУЕТ by design** (все 7 несут «Сохраняй разбивку на абзацы» — §2.5-S; фикс §2.6 в пакеты НЕ входил). Владелец справедливо заметил «редактуры по абзацам нет». Кандидат-фикс §4.5 её включает.
5. **Кандидат-фикс (glm-5 билингв + реверстка + жанр-конвенции, `diag/arms/CAND/`):** применяет культивация/секты/земной поклон, чище прозой, БЕЗ утечки; но **реверстку абзацев делает НЕПОСЛЕДОВАТЕЛЬНО** (гл.7 — да, гл.5 — частично). Вывод: glm-5 — приемлемый, но не идеальный исполнитель реверстки; layout-часть надёжнее возьмёт (а) более сильная модель, (б) детерминированный reflow-пасс, или (в) gemini с пост-очисткой преамбулы.
6. **Монитор для владельца (СЛЕПОЙ):** `diag/reading/monitor.html` (локальный, вне git/хостинга — текст копирайтный, в отличие от PD-фрагментов exp04) — оригинал zh + все 8 вариантов (7 армов + кандидат) по 3 главам под нейтральными метками «Вариант 1…8», **имена моделей скрыты, перестановка своя в каждой главе** (D13.5), ключ в отдельном `_МОНИТОР_КЛЮЧ.json` (держать закрытым). Кнопки «копировать оригинал+перевод» для флагман-сверки (Fable 5/5.6, слепо — имя модели не копируется), слот под любительский перевод. **Утечка преамбулы A5 снята в мониторе** для честного сравнения прозы (сам дефект — п.1).
**Обновление рекомендации (provisional):** к трём фиксам §3 добавляется **пере-курация глоссария (4-й равноправный фикс)** — сквозная, бьёт по всем вариантам; и **layout-часть надёжнее делать отдельным шагом** (детерминированный reflow ИЛИ сильная модель), т.к. glm-5 реверстывает нестабильно. gemini — понижен (утечка преамбулы). Ядро выбора glm-билингв-vs-премиум — по-прежнему за флагман-сверкой владельца через монитор.
---
## 5. ТРЕТИЙ РАУНД — слепая флагман-сверка (GPT + Claude), un-blind по ключу монитора
Владелец прогнал монитор через ДВА фронтир-флагмана (GPT-класс + Claude-класс) слепо + любительский релейный перевод (`translate.txt`) как контроль. Полный разбор Claude — `diag/reading/ОЦЕНКА_ФЛАГМАНА.md`; un-blind — `diag/reading/_РАСШИФРОВКА_ФЛАГМАНОВ.md`.
**Корректность (проверено):** оба флагмана читали `monitor.html` (8 вариантов — столько ТОЛЬКО у монитора; у пакетов `глава_N.md` 7) + `translate.txt`; ключи не открывали. Конфаундинга меток монитор↔пакеты НЕТ (оба дали чистые 8-вариантные ранги). «Какие варианты — переработки» флагманы вывели ЧТЕНИЕМ, и это un-blind'ится ровно в {A5, A2, CAND} — доказательство слепоты. Единственный de-blind-артефакт — **gemini (A5) сам себя палит утечкой** (`A5/5_1` хвостовой блок «Основные правки для справки»; `7_*/14_*` — ведущее «Вот отредактированный перевод») — реальный дефект, не ошибка оценки. (Мой стриппер снял ведущую форму, но пропустил хвостовую в 5_1 — учесть.)
**Конфиг-уровень (агрегат 3 глав, оба флагмана СОГЛАСНЫ):**
| Конфиг | GPT «строго» (сред.) | Claude ср. ранг (1=лучш) | Итог |
|---|---|---|---|
| **gemini-3.1-pro билингв (A5)** | **7.7** | **1.0** (#1 во всех главах) | лучшее ЯДРО, но ⚠ течёт → не прод-надёжен |
| **glm-5 билингв (A2)** | 7.3 | 2.7 | практич. лидер (без утечек) |
| **КАНДИДАТ (glm-билингв+реверстка+конвенции)** | 7.2 | 2.3 | сильный, без утечек |
| glm-5 **МОНО (= этап A, что читал владелец)** | 6.2 | 6.0 | **НИЗ — подтверждает «нечитаемо»** |
| черновик / grok-моно / grok-билингв / grok-безглосс | ~4.85.4 | ~57 | no-op семья, дно |
**Тройная триангуляция:** механика активности (§2.2) + LLM-судьи (§2.3) + ДВА флагмана слепо — **все** дают одно: **билингв-переработка ≫ моно/пассив**; текущий glm-моно — в нижнем тире; практич. выбор — **glm-билингв или КАНДИДАТ** (gemini лучшее ядро, но утечки).
**Ключевая находка флагманов (структура выборки):** 8 вариантов — это НЕ 8 переводов, а **~5 почти идентичных копий черновика** (A0/A1/A3/A4/A6 — косметика поверх draft) + **3 настоящие переработки** (A5/A2/CAND). Внутри «семьи» различия = шумовые порчи правки. → сравнивать надо «глубокая переработка vs лёгкая правка», и глубокая выигрывает во всех главах.
**Что флагманы добавили СВЕРХ моей диагностики (важно):**
1. **Никто не publishable** — лучшие = «крепкий сетевой перевод», не издательская проза. Инкрементальные фиксы дают «победить фан», НЕ «издательский уровень».
2. **Три вещи, чтобы победить фан-перевод** (у фана они есть, у нас нет): (а) **сквозной принудительный глоссарий**, (б) **европейская абзацная вёрстка + тире для речи**, (в) **сноски на аллюзии** (Су Ши, 采花=эвфемизм изнасилования — фан дал сноску, мы нет).
3. **Нужен output-санитайзер** (дёшево, ловит «мгновенную нечитаемость»): утёкшие заметки редактора, непереведённые слова («cultivation»), латиница в кириллице («vалуне»), диакритики («источа́ло»), битые словоформы («клёцок», «Первок предок», «Силённый»). Это класс дефектов ВНЕ моих гейтов — новая задача.
4. **Сквозные ошибки ЧЕРНОВИКА (в ВСЕХ вариантах, deepseek-корень):** 族长 глава клана ≠ 家老 старейшина (рушит иерархию); 本命蛊 = жизненный гу (не вид «гу Жизни»); 长生 = бессмертие (не «долголетие» — тема книги, 5/8 вариантов провалили); 失神 ≠ потерял сознание; 花酒行者 теряет 花 (= блуд, злодей-насильник); 青 = зелёный (не синий); 供奉/供养 инверсии. → лечит билингв-редактор ИЛИ более сильный черновик.
5. **Экспертный глоссарий:** оба флагмана дали детальные таблицы «оригинал→рекомендация→альтернативы» (в `ОЦЕНКА_ФЛАГМАНА.md` + чат) — прямой материал для пере-курации сида с владельцем. Точки согласия: врата→**апертура**, 真元 не «истинная энергия», 本命蛊→жизненный гу, 长生→бессмертие, 族长/家老 развести, 花酒行者 сохранить 花. Спорное (Первопредок vs Жэнь Цзу; культивация vs совершенствование; гу-мастер vs Мастер Гу) — решает владелец.
**Стратегический вывод (честно):** это сценарий **A+B** из strategic-развилки. Инкрементальные фиксы (билингв + глоссарий + вёрстка + санитайзер + сноски) реально сдвигают к «лучше фана / хороший сетевой перевод». Но «издательская проза» ими не достигается — для неё нужен более сильный базовый переводчик/подход (вопрос ЯДРА, пилот Ф2.5). Диагностика exp12 своё сделала: локализовала потерю и дала конфиг-кандидата; «потолок читаемости» — теперь вопрос выбора ядра, который надо поднимать в приоритете.
---
## 6. Стратегический разбор (по запросу владельца «какой план, это перебор моделей?») — escalation оркестратору
Мультиагентный разбор (4 угла: механизм/экономика/рынок/архитектура + синтез + адверсариальный ред-тим), заземлён на доки. Полный текст + ред-тим: `diag/reading/СТРАТЕГИЧЕСКИЙ_МЕМО.md`; перспективы: `СТРАТЕГИЯ_перспективы.md` (ВНЕ git). Это **продуктовая стратегия — зона оркестратора/владельца, не полигона**; фиксирую как escalation, решение не моё.
**Ядро:** «перебор моделей» — ложная рамка. Наибольший скачок дал режим-флип на ОДНОЙ модели + почти-бесплатные детерминированные механизмы (вёрстка/санитайзер/глоссарий/сноски) закрывают ~8090% разрыва «нечитаемо→лучше фана» при COGS ~$12/ранобэ. Наука: специализированная LitMT-8B ≈ фронтир на литпереводе (research/03) → боттлнек = пайплайн, не размер модели. Издательский уровень чистым LLM — фронтир-предел индустрии, не дефект стека; достижимо «в целом неотличимо, проигрывает в деталях» (и это уже продаётся).
**⚠ Ред-тим (честные оговорки, важно):** (1) эмпирика exp12 — ПРЕДВАРИТЕЛЬНА (одна книга, ~8 кейсов, LLM-судьи = циркулярность, прокси-метрики) → флип D1 и веса рычагов ГЕЙТИТЬ на реальном вебновелл-срезе, не D-блоком сейчас; (2) число COGS не сходится: $0.69→$1.52 это +117190%, а не «+1520% от флипа D1» — в тезис негласно вложены память+эскалация (уже Опция B); чинить арифметику до подписи; (3) глоссарий/память-состояния = САМЫЙ дорогой, человеко-кураторский, наименее доказанный слой (реестр рисков памяти) — не продавать как «дешёвый ров»; (4) рыночное окно = supply gap, НЕ доказанный WTP → нужна проба спроса (Rulate vs бесплатный фан) + оценка ToS/юр 18+.
**Рекомендация (мемо+ред-тим):** механизмы 25 (reflow, санитайзер, глоссарий-энфорс, сноски) катить как фикс пола читаемости НЕМЕДЛЕННО (правильно в любом сценарии); **флип D1 по умолчанию, ре-тезис COGS, ставку на серию-консистентность — за гейтом вебновелл-среза + пробы спроса**; Опция C (издательский) — premium-апсайд/B2B, НЕ блокер запуска.
**Владельцу на решение:** планка запуска (лучше-фана vs издательский) · готовность к честному COGS (~$1.52, верифицировать) · go/no-go на микро-пробу спроса. **Оркестратору:** ратификация (гейтированная) флипа D1, output-санитайзер в скоуп Ф1, разморозка reflow-вёрстки — D-блоком; полигон дефолт не трогает.

165
eval/exp12_arms.py Normal file
View file

@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""exp12 — генерация редакторских армов A1,A2,A3,A4,A5,A6 (платно, кап $5).
Черновики (A0) и финал glm-5-моно (A1) из store, $0 (не тут). Здесь ре-редактируем
ТЕ ЖЕ черновики разными редакторами/режимами. Промпты ЗАМОРОЖЕНЫ (пре-регистрация §1.2).
Faithful render: system(editor.md) system(инъекция-блок) user(черновик[+исходник]).
Layout повторяет render.go MessagesWithInjection.
Запуск: eval/.venv/bin/python eval/exp12_arms.py [--dry]
Выход: diag/arms/<ARM>/<ch>_<chunk>.txt + diag/arms/results.json
"""
from __future__ import annotations
import json, os, sys, time, urllib.request, urllib.error
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import load_env_file
load_env_file()
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
ARMS = DIAG / "arms"
EDITOR_MD = Path("/home/ubuntu/projects/textmachine/backend/prompts/editor.md")
CAP_USD = 5.0
# --- render vars (book.yaml) ---
VARS = {"genre": "вебновелла", "audience": "взрослые читатели вебновелл",
"title": "蛊真人", "source_lang": "zh", "honorifics": "keep", "venuti": "0.60"}
PRICES = { # per 1M tokens (models.yaml, prices_checked 2026-07-10)
"glm-5": (1.0, 3.2), "grok-4.3": (1.25, 2.50),
"gemini-3.1-pro-preview": (2.0, 12.0),
}
# --- frozen arm specs ---
BILINGUAL_LINE = ("Тебе также дан ИСХОДНЫЙ ТЕКСТ: сверяйся с ним по смыслу и исправляй "
"искажения черновика, сохраняя полноту.")
ARMS_SPEC = {
"A1p": dict(model="glm-5", base="https://api.z.ai/api/paas/v4", keyenv="ZAI_API_KEY",
mode="mono", constraints=True, extra={"thinking": {"type": "disabled"}}, max_tokens=8000),
"A2": dict(model="glm-5", base="https://api.z.ai/api/paas/v4", keyenv="ZAI_API_KEY",
mode="biling", constraints=True, extra={"thinking": {"type": "disabled"}}, max_tokens=8000),
"A3": dict(model="grok-4.3", base="https://api.x.ai/v1", keyenv="XAI_API_KEY",
mode="mono", constraints=True, extra={"reasoning_effort": "none"}, max_tokens=8000),
"A4": dict(model="grok-4.3", base="https://api.x.ai/v1", keyenv="XAI_API_KEY",
mode="biling", constraints=True, extra={"reasoning_effort": "none"}, max_tokens=8000),
"A5": dict(model="gemini-3.1-pro-preview",
base="https://generativelanguage.googleapis.com/v1beta/openai", keyenv="GEMINI_API_KEY",
mode="biling", constraints=True, extra={}, max_tokens=16000),
"A6": dict(model="grok-4.3", base="https://api.x.ai/v1", keyenv="XAI_API_KEY",
mode="mono", constraints=False, extra={"reasoning_effort": "none"}, max_tokens=8000),
}
def render(tpl: str) -> str:
for k, v in VARS.items():
tpl = tpl.replace("{{%s}}" % k, v)
return tpl
def editor_system() -> str:
raw = EDITOR_MD.read_text(encoding="utf-8")
sys_part = raw.split("---USER---")[0].rstrip()
return render(sys_part)
def build_messages(spec: dict, source: str, draft: str, blk: dict) -> list[dict]:
sys1 = editor_system()
if spec["mode"] == "biling":
sys1 = sys1 + "\n" + BILINGUAL_LINE
msgs = [{"role": "system", "content": sys1}]
if spec["constraints"]:
inj = blk["bilingual_glossary"] if spec["mode"] == "biling" else blk["editor_constraint"]
if inj.strip():
msgs.append({"role": "system", "content": inj})
if spec["mode"] == "biling":
user = f"ИСХОДНЫЙ ТЕКСТ (zh):\n\n{source}\n\n---\n\nЧерновик перевода для редактуры:\n\n{draft}"
else:
user = f"Черновик перевода для редактуры:\n\n{draft}"
msgs.append({"role": "user", "content": user})
return msgs
def call_messages(spec: dict, messages: list[dict], timeout: int = 300):
key = os.environ.get(spec["keyenv"], "")
if not key:
return None, f"no key {spec['keyenv']}", {}
body = {"model": spec["model"], "messages": messages,
"temperature": 0.4, "max_tokens": spec["max_tokens"], **spec["extra"]}
req = urllib.request.Request(spec["base"].rstrip("/") + "/chat/completions",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
return None, f"HTTP {e.code}: {e.read()[:200].decode('utf-8','replace')}", {}
except Exception as e:
return None, f"ERR {str(e)[:160]}", {}
ch = data["choices"][0]
text = (ch.get("message", {}) or {}).get("content") or ""
usage = data.get("usage", {}) or {}
usage["finish_reason"] = ch.get("finish_reason", "")
return text.strip(), (None if text.strip() else f"empty|finish={ch.get('finish_reason')}"), usage
def cost_of(model: str, usage: dict) -> float:
inp = usage.get("prompt_tokens", 0) or 0
comp = usage.get("completion_tokens", 0) or 0
total = usage.get("total_tokens", 0) or 0
reason = usage.get("reasoning_tokens", 0) or 0
pin, pout = PRICES.get(model, (0, 0))
if model == "gemini-3.1-pro-preview": # additive_total: thinking only in total_tokens
reason = max(0, total - inp - comp)
return (inp * pin + (comp + reason) * pout) / 1_000_000
def main() -> None:
dry = "--dry" in sys.argv
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
blocks = json.loads((DIAG / "blocks.json").read_text(encoding="utf-8"))
targets = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)]
ARMS.mkdir(parents=True, exist_ok=True)
results = []
spent = 0.0
for arm, spec in ARMS_SPEC.items():
adir = ARMS / arm
adir.mkdir(exist_ok=True)
for ch, ci in targets:
key = f"{ch}/{ci}"
src = chunks[key]["source"]
draft = chunks[key]["draft"]
blk = blocks[key]
outp = adir / f"{ch}_{ci}.txt"
if outp.exists() and outp.read_text(encoding="utf-8").strip():
print(f" [{arm} {key}] cached"); continue
msgs = build_messages(spec, src, draft, blk)
if dry:
approx_in = sum(len(m["content"]) for m in msgs) // 3
print(f" [{arm} {key}] mode={spec['mode']} constr={spec['constraints']} "
f"msgs={len(msgs)} ~in={approx_in}tok draft_chars={len(draft)}")
continue
if spent > CAP_USD * 0.8:
print(f"!! near cap (${spent:.3f}) — stop"); break
t0 = time.time()
text, err, usage = call_messages(spec, msgs)
c = cost_of(spec["model"], usage)
spent += c
if not text:
print(f" [{arm} {key}] ERROR: {err} (${spent:.3f})")
results.append({"arm": arm, "chunk": key, "error": err, "cost": c})
continue
outp.write_text(text, encoding="utf-8")
results.append({"arm": arm, "chunk": key, "chars": len(text),
"finish": usage.get("finish_reason"), "usage": usage, "cost": round(c, 5)})
print(f" [{arm} {key}] {time.time()-t0:.0f}s {len(text)}ch ${c:.4f} cum=${spent:.3f} "
f"finish={usage.get('finish_reason')}")
if not dry:
(ARMS / "results.json").write_text(json.dumps(results, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"\nTOTAL arm spend: ${spent:.4f} (cap ${CAP_USD})")
if __name__ == "__main__":
main()

104
eval/exp12_blocks.py Normal file
View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""exp12 — реконструкция per-chunk инъекционных блоков глоссария из приёмочного store.
$0. Воспроизводит ровно то, что боевой редактор A1 (glm-5 mono) видел:
- editorConstraintBlock (renderEditorConstraintBlock, memory.go): CONFIRMED dst-формы,
строки «- «dst»», дедуп по dst. Заголовок editorConstraintHeader.
- bilingualGlossaryBlock (renderGlossaryBlock, memory.go): «src dst» (+ проверить
для не-CONFIRMED). Заголовок glossaryBlockHeader. Для билингв-армов (редактор видит src).
Активные ключи чанка = injected_ids(chunk) injected_ids(chunk0) для chunk>0 (аппрокс sticky
A5 внутри главы; выбранные главы 2-чанковые, sticky несёт ровно активные chunk0).
Ключ injected_id = «src\x1fsense\x1fsince\x1funtil» строка glossary по (src,sense,since,until).
CONFIRMED status='approved' и окно since/until валидно для главы.
Выход: diag/blocks.json {(ch/chunk): {editor_constraint, bilingual_glossary, keys[]}}
"""
from __future__ import annotations
import json, sqlite3
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
DB = DIAG / "store-copy.db"
SEL = [5, 7, 14] # пре-регистрированный выбор (exp12_extract.py)
EDITOR_HEADER = ("КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике эти сущности должны быть "
"переданы ИМЕННО этими формами — приводи к ним любые расхождения, склоняя по "
"контексту; не вводи иных вариантов и не меняй ничего другого):")
GLOSS_HEADER = ("ГЛОССАРИЙ (используй эти утверждённые переводы имён и терминов последовательно; "
"строки с пометкой ⟨проверить⟩ — неподтверждённые кандидаты):")
def main() -> None:
con = sqlite3.connect(DB)
con.row_factory = sqlite3.Row
# glossary lookup by (src,sense,since,until)
gl = {}
for r in con.execute("SELECT src,sense,since_ch,until_ch,dst,status FROM glossary"):
gl[(r["src"], r["sense"], r["since_ch"], r["until_ch"])] = (r["dst"], r["status"])
# injected_ids per selected chunk
inj = {}
for r in con.execute(
"SELECT chapter,chunk_idx,injected_ids FROM retrieval_state WHERE chapter IN (%s)"
% ",".join(map(str, SEL))
):
inj[(r["chapter"], r["chunk_idx"])] = json.loads(r["injected_ids"] or "[]")
con.close()
def keydec(k: str):
src, sense, since, until = k.split("\x1f")
return (src, sense, int(since), int(until))
out = {}
for ch in SEL:
chunks = sorted(c for (cc, c) in inj if cc == ch)
chunk0 = inj.get((ch, 0), [])
for c in chunks:
active = list(inj.get((ch, c), []))
if c > 0: # sticky: carry chunk0's exact matches (A5 scene inertia within chapter)
for k in chunk0:
if k not in active:
active.append(k)
# map to records
recs = []
for k in active:
src, sense, since, until = keydec(k)
dst, status = gl.get((src, sense, since, until), ("", "auto"))
if not dst.strip():
continue
confirmed = (status == "approved") # windows all 0/0 here → spoiler-valid
recs.append({"src": src, "dst": dst.strip(), "confirmed": confirmed})
# editor constraint block: CONFIRMED only, dedup by dst
seen, elines = set(), []
for r in recs:
if not r["confirmed"] or r["dst"] in seen:
continue
seen.add(r["dst"]); elines.append(f"- «{r['dst']}»")
editor_block = (EDITOR_HEADER + "\n" + "\n".join(elines)) if elines else ""
# bilingual glossary block: src → dst (+ ⟨проверить⟩ if not confirmed)
glines = []
for r in recs:
line = f"{r['src']}{r['dst']}"
if not r["confirmed"]:
line += " ⟨проверить⟩"
glines.append(line)
gloss_block = (GLOSS_HEADER + "\n" + "\n".join(glines)) if glines else ""
out[f"{ch}/{c}"] = {
"editor_constraint": editor_block,
"bilingual_glossary": gloss_block,
"n_constraint_terms": len(elines),
"n_gloss_terms": len(glines),
}
(DIAG / "blocks.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
for k, v in out.items():
print(f"\n=== chunk {k}: {v['n_constraint_terms']} constraint / {v['n_gloss_terms']} gloss terms ===")
print(v["editor_constraint"][:400])
if __name__ == "__main__":
main()

72
eval/exp12_candidate.py Normal file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""exp12 — «кандидат-фикс» (пост-фидбек владельца, ВНЕ слепого бейк-оффа).
Демонстрация направления фикса на 3 тех же главах: glm-5 БИЛИНГВ (судейский #1, не течёт
преамбулой как gemini) + разрешение переверстать абзацы + жанр-конвенции (культивация,
секты/школы, земной поклон). Спорные термины (人祖, 花酒行者) НЕ хардкожу оставляю модели
с исходником + пометкой «на утверждение глоссария». НЕ арм пре-регистрации.
Выход: diag/arms/CAND/<ch>_<ci>.txt
"""
from __future__ import annotations
import json, os, sys, urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import load_env_file
load_env_file()
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
OUT = DIAG / "arms" / "CAND"
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
blocks = json.loads((DIAG / "blocks.json").read_text(encoding="utf-8"))
SYS = (
"Ты — литературный редактор художественного перевода китайской вебновеллы-культиваторки "
"(сянься/уся) на русский. Тебе дан ИСХОДНИК (китайский) и его ЧЕРНОВОЙ перевод. Сделай из "
"черновика ЖИВУЮ, читаемую русскую прозу издательского уровня:\n"
"1. АБЗАЦЫ: собирай предложения в логические ЕВРОПЕЙСКИЕ абзацы. Китайская манера "
"«предложение = отдельная строка» для русской прозы деструктивна — группируй повествование "
"по смыслу. Реплики прямой речи — отдельными строками с тире.\n"
"2. СМЫСЛ И СЮЖЕТ: сверяйся с ИСХОДНИКОМ, следи за тем, ЧТО происходит по сюжету, и правь так, "
"чтобы читатель понимал сцену. Исправляй искажения и машинные кальки черновика; сохраняй полноту.\n"
"3. ЖАНР-КОНВЕНЦИИ русского фан-перевода культиваторок: 修炼 = «культивация» (не «совершенствование»); "
"门派/派 = «секты»/«школы» (не «фракции»); 磕头 = «земной поклон»/«коснулся лбом пола» (не «ударил головой»); "
"меры времени (时辰≈2 часа) переводи или глоссируй. Прочую терминологию имён/титулов держи "
"последовательно (в приложенном глоссарии, если есть).\n"
"4. Убери канцелярит и кальки, оживи синтаксис и ритм (школа Норы Галь). Не пересочиняй сцены, "
"не добавляй отсебятины.\n"
"Выведи ТОЛЬКО отредактированный русский текст, без преамбул, комментариев и пояснений."
)
SPEC = dict(model="glm-5", base="https://api.z.ai/api/paas/v4", keyenv="ZAI_API_KEY",
extra={"thinking": {"type": "disabled"}}, max_tokens=8000)
def call(msgs):
key = os.environ.get(SPEC["keyenv"], "")
body = {"model": SPEC["model"], "temperature": 0.5, "max_tokens": SPEC["max_tokens"],
"messages": msgs, **SPEC["extra"]}
req = urllib.request.Request(SPEC["base"].rstrip("/") + "/chat/completions",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
with urllib.request.urlopen(req, timeout=300) as r:
return json.loads(r.read())["choices"][0]["message"]["content"].strip()
def main():
OUT.mkdir(parents=True, exist_ok=True)
for ch, ci in [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)]:
key = f"{ch}/{ci}"
src, draft, gl = chunks[key]["source"], chunks[key]["draft"], blocks[key]["bilingual_glossary"]
msgs = [{"role": "system", "content": SYS}]
if gl.strip():
msgs.append({"role": "system", "content": gl})
msgs.append({"role": "user",
"content": f"ИСХОДНИК (zh):\n\n{src}\n\n---\n\nЧерновик для редактуры:\n\n{draft}"})
out = call(msgs)
(OUT / f"{ch}_{ci}.txt").write_text(out, encoding="utf-8")
print(f" CAND {key}: {len(out)} ch")
if __name__ == "__main__":
main()

206
eval/exp12_extract.py Normal file
View file

@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""exp12 — извлечение per-chunk (source/draft/final) из приёмочного store + выбор 3 чистых глав.
$0. Никаких вызовов. Источники (ВНЕ git):
- store копия: /home/ubuntu/books/gu-zhenren/diag/store-copy.db (draft=deepseek, edit=glm-5)
- параллель: /home/ubuntu/books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt ([ОРИГ]/[ПЕРЕВОД])
Выход:
- diag/chunks.json per (chapter,chunk): source, draft(A0), final(A1), dispositions
- diag/selection.json метрики выбора глав + 3 выбранные главы (пре-регистрированное правило)
Пре-регистрированное правило выбора (frozen, см. 12-quality-diagnosis.md §Пре-регистрация):
frame = ЧИСТЫЕ главы (все чанки disposition=ok на draft И edit, flag_reason пуст).
метрика на ИСХОДНИКЕ zh: D = доля символов внутри кавычек (U+201CU+201D) от не-пробельных.
выбор: high=argmax D, low=argmin D, mid=ближайшая к медиане D. Тай-брейк меньший номер главы.
"""
from __future__ import annotations
import json, re, sqlite3, sys
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
DB = DIAG / "store-copy.db"
PARALLEL = Path("/home/ubuntu/books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt")
CH_HDR = re.compile(r"\s*Глава\s+(\d+)\s*】")
CHUNK_HDR = re.compile(r"^---\s*чанк\s+(\d+)\s*\[(.+?)\]\s*---")
LQUOTE, RQUOTE = "", "" # “ ”
def parse_parallel(path: Path) -> dict:
"""{(ch,chunk): {'source': str, 'final_parallel': str, 'flag': str}}"""
out: dict = {}
ch = None
chunk = None
mode = None # 'orig' | 'per'
buf_src: list[str] = []
buf_per: list[str] = []
def flush():
if ch is not None and chunk is not None:
out[(ch, chunk)] = {
"source": "\n".join(buf_src).strip(),
"final_parallel": "\n".join(buf_per).strip(),
"flag": flag,
}
flag = ""
for line in path.read_text(encoding="utf-8").splitlines():
m = CH_HDR.search(line)
if m:
flush(); ch = int(m.group(1)); chunk = None; mode = None
buf_src, buf_per = [], []
continue
m = CHUNK_HDR.match(line.strip())
if m:
flush()
chunk = int(m.group(1)); flag = m.group(2).strip()
mode = None; buf_src, buf_per = [], []
continue
s = line.strip()
if s == "[ОРИГ]":
mode = "orig"; continue
if s == "[ПЕРЕВОД]":
mode = "per"; continue
if s.startswith("[⚠") or s.startswith("=====") or not s and mode is None:
continue
if mode == "orig":
buf_src.append(line)
elif mode == "per":
buf_per.append(line)
flush()
return out
def load_store(db: Path) -> dict:
"""{(ch,chunk): {'draft':..,'edit':..,'draft_disp':..,'edit_disp':..,'draft_flag':..,'edit_flag':..}}"""
con = sqlite3.connect(db)
con.row_factory = sqlite3.Row
rows = con.execute("""
SELECT cs.chapter, cs.chunk_idx, cs.stage, cs.disposition, cs.flag_reason,
cp.model_actual, cp.response_text
FROM chunk_status cs
LEFT JOIN checkpoints cp ON cp.request_hash = cs.final_hash
ORDER BY cs.chapter, cs.chunk_idx, cs.stage
""").fetchall()
con.close()
out: dict = {}
for r in rows:
key = (r["chapter"], r["chunk_idx"])
d = out.setdefault(key, {})
st = r["stage"]
d[st] = r["response_text"] or ""
d[f"{st}_disp"] = r["disposition"]
d[f"{st}_flag"] = r["flag_reason"]
d[f"{st}_model"] = r["model_actual"]
return out
def dialogue_density(src: str) -> float:
nonspace = re.sub(r"\s", "", src)
if not nonspace:
return 0.0
inside = 0
depth = 0
for c in src:
if c == LQUOTE:
depth += 1; continue
if c == RQUOTE:
if depth > 0:
depth -= 1
continue
if depth > 0 and not c.isspace():
inside += 1
return inside / len(nonspace)
def main() -> None:
par = parse_parallel(PARALLEL)
store = load_store(DB)
# merge per chunk
chunks = {}
keys = sorted(set(par) | set(store))
for k in keys:
ch, ci = k
p = par.get(k, {})
s = store.get(k, {})
chunks[f"{ch}/{ci}"] = {
"chapter": ch, "chunk": ci,
"source": p.get("source", ""),
"draft": s.get("draft", ""), # A0
"final": s.get("edit", ""), # A1 (glm-5 mono)
"final_parallel": p.get("final_parallel", ""),
"draft_disp": s.get("draft_disp", "?"),
"edit_disp": s.get("edit_disp", "?"),
"draft_flag": s.get("draft_flag", ""),
"edit_flag": s.get("edit_flag", ""),
}
# sanity: store edit vs parallel final match
mism = [k for k, v in chunks.items()
if v["final"] and v["final_parallel"]
and v["final"].strip()[:120] != v["final_parallel"].strip()[:120]]
# per-chapter clean classification + dialogue density
chap = {}
for k, v in chunks.items():
c = v["chapter"]
d = chap.setdefault(c, {"chunks": 0, "clean": True, "src": [], "flags": []})
d["chunks"] += 1
d["src"].append(v["source"])
if v["draft_disp"] != "ok" or v["edit_disp"] != "ok":
d["clean"] = False
d["flags"].append(f"{v['draft_flag']}|{v['edit_flag']}")
metrics = {}
for c, d in chap.items():
full_src = "\n".join(d["src"])
metrics[c] = {
"clean": d["clean"],
"chunks": d["chunks"],
"src_chars": len(re.sub(r"\s", "", full_src)),
"dialogue_density": round(dialogue_density(full_src), 4),
}
clean = {c: m for c, m in metrics.items() if m["clean"]}
ds = sorted((m["dialogue_density"], c) for c, m in clean.items())
dens = [x[0] for x in ds]
median = dens[len(dens) // 2] if len(dens) % 2 else (dens[len(dens)//2 - 1] + dens[len(dens)//2]) / 2
high = max(clean, key=lambda c: (clean[c]["dialogue_density"], -c))
low = min(clean, key=lambda c: (clean[c]["dialogue_density"], c))
mid = min((c for c in clean if c not in (high, low)),
key=lambda c: (abs(clean[c]["dialogue_density"] - median), c))
selection = {
"rule": "clean-frame; D=share of chars in “…”; high=argmax, low=argmin, mid=nearest-median; tie=min chapter#",
"clean_chapters": sorted(clean),
"median_D": round(median, 4),
"selected": {
"high_dialogue": {"chapter": high, **clean[high]},
"mid": {"chapter": mid, **clean[mid]},
"low_exposition": {"chapter": low, **clean[low]},
},
"all_clean_metrics": {str(c): clean[c] for c in sorted(clean)},
}
DIAG.mkdir(exist_ok=True)
(DIAG / "chunks.json").write_text(json.dumps(chunks, ensure_ascii=False, indent=1), encoding="utf-8")
(DIAG / "selection.json").write_text(json.dumps(selection, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"chunks extracted: {len(chunks)} | store-parallel final mismatches (first120): {len(mism)}")
if mism:
print(" mism keys:", mism[:10])
print(f"clean chapters ({len(clean)}): {sorted(clean)}")
print(f"median D = {median:.4f}")
print("\nDialogue density (clean, sorted):")
for dval, c in ds:
star = " <-HIGH" if c == high else " <-LOW" if c == low else " <-MID" if c == mid else ""
print(f" ch{c:>2}: D={dval:.4f} chunks={clean[c]['chunks']} src_chars={clean[c]['src_chars']}{star}")
print(f"\nSELECTED: high=ch{high} (D={clean[high]['dialogue_density']}), "
f"mid=ch{mid} (D={clean[mid]['dialogue_density']}), low=ch{low} (D={clean[low]['dialogue_density']})")
if __name__ == "__main__":
main()

141
eval/exp12_glossary_v2.py Normal file
View file

@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""exp12 — пере-генерация глоссария (v2) по консенсусу двух флагманов + мои каллы.
Владелец делегировал «перегенерируй без меня». Источник рекомендаций: разборы GPT+Claude
(diag/reading/ОЦЕНКА_ФЛАГМАНА.md + вставка владельца). Высокая уверенность где ОБА флагмана
сошлись; контестные (gu-род, культивация/совершенствование, Первопредок/Жэнь-Цзу) ПОМЕЧЕНЫ
для ратификации владельцем/оркестратором НЕ финал.
Вход: guzhenren-seed.yaml (v1, 49 терминов). Выход: guzhenren-seed-v2.yaml + changelog.
НИЧЕГО не идёт в прод без ратификации + resnapshot. Оба файла ВНЕ git.
"""
from __future__ import annotations
import yaml
from pathlib import Path
BOOK = Path("/home/ubuntu/books/gu-zhenren")
SRC = BOOK / "guzhenren-seed.yaml"
OUT = BOOK / "guzhenren-seed-v2.yaml"
LOG = BOOK / "diag" / "glossary_v2_changelog.md"
# --- изменения существующих терминов: src -> (new_dst, forms|None, status|None, rationale, conf) ---
CHANGES = {
"空窍": ("апертура", ["апертуры","апертуре","апертуру","апертурой","апертуре"], "approved",
"оба флагмана — топ-проблема: «врата» ломают грамматику («одни врата»); «апертура» = фан-вики жанра", "HIGH"),
"开窍": ("открытие апертуры", ["открытия апертуры","открытию апертуры","открытием апертуры","открытии апертуры"], None,
"согласовано с 空窍→апертура; 开窍大典 = Церемония пробуждения/обряд открытия апертуры", "HIGH"),
"真元": ("истинная ци", ["истинной ци","истинной ци","истинную ци","истинной ци","истинной ци"], None,
"оба: НЕ «истинная энергия» (физикализм); отделить от 元气 (изначальная ци)", "HIGH"),
"元海": ("море истинной ци", ["моря истинной ци","морю истинной ци","морем истинной ци","море истинной ци"], None,
"согласовано с 真元→истинная ци", "HIGH"),
"本命蛊": ("жизненный гу", ["жизненного гу","жизненному гу","жизненного гу","жизненным гу","жизненном гу"], "approved",
"оба: НЕ «гу Жизни» (читается как ВИД, как «гу Надежды»); это СТАТУС. ⚑ род «гу» — см. флаг", "HIGH"),
"花酒行者": ("Странник Цветов и Вина", ["Странника Цветов и Вина","Страннику Цветов и Вина","Странника Цветов и Вина","Странником Цветов и Вина","Страннике Цветов и Вина"], "approved",
"оба: сохранить 花 (=блуд; злодей — насильник 采花大盗); «Винный Странник» терял половину смысла", "HIGH"),
"蛊虫": ("гу-червь", ["гу-червя","гу-червю","гу-червя","гу-червём","гу-черве"], None,
"«гу-тварь» пейоративно; «гу-червь» или просто «гу». ⚑ род «гу»", "MED"),
"元石": ("изначальный камень", ["изначального камня","изначальному камню","изначальный камень","изначальным камнем","изначальном камне"], None,
"«первокамень» → «двадцать первокамень» читается как «двадцать первый»; «изначальный» согласован с рядом 元", "MED"),
"古月山寨": ("селение Гуюэ", ["селения Гуюэ","селению Гуюэ","селением Гуюэ","селении Гуюэ"], None,
"«стан» = воинский лагерь; 山寨 здесь — укреплённое селение/деревня", "MED"),
"白家寨": ("селение Бай", ["селения Бай","селению Бай","селением Бай","селении Бай"], None, "согласовано", "MED"),
"熊家寨": ("селение Сюн", ["селения Сюн","селению Сюн","селением Сюн","селении Сюн"], None, "согласовано", "MED"),
}
# --- новые термины (были GAP → модель угадывала) ---
ADDITIONS = [
dict(src="长生", dst="бессмертие", type="term", forms=["бессмертия","бессмертию","бессмертие","бессмертием","бессмертии"],
note="chángshēng; вечная жизнь/бессмертие — СКВОЗНАЯ тема книги; НЕ «долголетие» (провалили 5/8 вариантов)", conf="HIGH"),
dict(src="修炼", dst="культивация", type="term", forms=["культивации","культивации","культивацию","культивацией","культивации"],
note="xiūliàn; культивация/культивировать. ⚑ лит.альтернатива «совершенствование» (флагманы для издания) — владельцу", conf="MED⚑"),
dict(src="修行", dst="культивация", type="term", forms=["культивации","культивации","культивацию","культивацией","культивации"],
note="xiūxíng; практика культивации; согласовано с 修炼. ⚑ см. флаг", conf="MED⚑"),
dict(src="人祖", dst="Первопредок", type="name", forms=["Первопредка","Первопредку","Первопредка","Первопредком","Первопредке"],
note="Rénzǔ; говорящее имя (Праотец человечества). НЕ «Рен Зу» (англ.релей-ошибка романизации). ⚑ альт-имя «Жэнь Цзу» (Палладица) — владельцу", conf="MED⚑"),
dict(src="族长", dst="глава клана", type="title", forms=["главы клана","главе клана","главу клана","главой клана","главе клана"],
note="zúzhǎng; глава клана — НАД старейшинами; частая ошибка черновика «старейшина» рушит иерархию сцены", conf="HIGH"),
dict(src="家老", dst="старейшина", type="title", forms=["старейшины","старейшине","старейшину","старейшиной","старейшине"],
note="jiālǎo; старейшина клана; 学堂家老 = старейшина академии. Развести с 族长", conf="HIGH"),
dict(src="困境", dst="Беды", type="term", forms=["Бед","Бедам","Беды","Бедами","Бедах"],
note="kùnjìng; стая зверей в легенде о Первопредке; «Беды»/«Невзгоды» живее абстрактных «Трудностей» как имя стаи", conf="MED"),
dict(src="炼化", dst="очистить и подчинить", type="term", invariant=True,
note="liànhuà; очистить-и-подчинить живого гу; НЕ «переплавить» (слишком металлично). Кратко «очистить»", conf="MED"),
]
# ⚑ ТОП-ФЛАГ (не применяю молча): род «гу».
GU_GENDER_FLAG = ("⚑ РОД «гу»: сид держит НЕСКЛОНЯЕМЫЙ СРЕДНИЙ; оба флагмана рекомендуют МУЖСКОЙ "
"(«этот гу», как «жук»; согласуется с «винный червь — он»). Это меняет согласование во ВСЕХ "
"гу-именах — НЕ флипаю молча. В v2 «жизненный гу»/«гу-червь» даны в МУЖСКОМ как рекомендация; "
"решение по проекту — за владельцем/оркестратором.")
KEEP_FLAGS = [
"蛊师 → «гу-мастер» ОСТАВЛЕН (владелец хотел «Мастер Гу», но это конфликтует с титулом 蛊真人/«Мастер Гу»/«Преподобный Гу») — флаг владельцу.",
"甲乙丙丁 → «разряд А/Б/В/Г» ОСТАВЛЕН (Claude одобрил русификацию; лучше латиницы AD).",
"采花大盗 → рекомендация «похититель цветов» + СНОСКА при 1-м употреблении (эвфемизм изнасилования) — но сноски = отдельная задача бэкенда, не в сиде.",
]
def main():
data = yaml.safe_load(SRC.read_text(encoding="utf-8"))
terms = data["terms"]
changed, added = [], []
for t in terms:
s = t.get("src")
if s in CHANGES:
new_dst, forms, status, rat, conf = CHANGES[s]
old = t.get("dst")
t["dst"] = new_dst
if forms is not None:
t["decl"] = {"invariant": False, "forms": forms}
if status:
t["status"] = status
t["note"] = f"[v2:{conf}] {rat}. (было: «{old}»). " + t.get("note", "")
changed.append((s, old, new_dst, rat, conf))
for a in ADDITIONS:
entry = {"src": a["src"], "dst": a["dst"], "type": a["type"],
"decl": {"invariant": a.get("invariant", False), "forms": a.get("forms", [])},
"since_ch": 0, "status": "approved",
"note": f"[v2-NEW:{a['conf']}] {a['note']}"}
terms.append(entry)
added.append((a["src"], a["dst"], a["note"], a["conf"]))
OUT.write_text(yaml.dump(data, allow_unicode=True, sort_keys=False, width=200), encoding="utf-8")
# changelog
lg = ["# Глоссарий 蛊真人 — v2 (пере-курация по флагманам, exp12)", "",
f"Изменено: {len(changed)} · Добавлено: {len(added)} · Всего терминов: {len(terms)}",
"Источник: консенсус GPT+Claude (diag/reading/ОЦЕНКА_ФЛАГМАНА.md). ⚑ = контестно, за владельцем.",
"**НЕ финал, НЕ в прод без ратификации + resnapshot.**", "",
"## Изменённые термины", "",
"| src | было | стало | conf | почему |", "|---|---|---|---|---|"]
for s, old, new, rat, conf in changed:
lg.append(f"| {s} | {old} | **{new}** | {conf} | {rat} |")
lg += ["", "## Новые термины (были GAP — модель угадывала)", "",
"| src | dst | conf | note |", "|---|---|---|---|"]
for s, dst, note, conf in added:
lg.append(f"| {s} | **{dst}** | {conf} | {note} |")
lg += ["", "## Оставлено намеренно / флаги", ""]
lg.append(f"- {GU_GENDER_FLAG}")
for k in KEEP_FLAGS:
lg.append(f"- {k}")
lg += ["", "## Контестные каллы (⚑ — за владельцем)",
"- 修炼/修行 → **культивация** (твоё предпочтение) vs «совершенствование» (флагманы для издания).",
"- 人祖 → **Первопредок** (смысл, оба флагмана как минимум допускают) vs имя «Жэнь Цзу» (НЕ «Рен Зу»).",
"- род «гу»: **мужской** (рекоменд. флагманов) vs средний (текущий сид).",
"- 蛊师 «гу-мастер» vs «Мастер Гу» (конфликт с титулом книги)."]
LOG.write_text("\n".join(lg), encoding="utf-8")
print(f"v2 seed → {OUT}")
print(f"changelog → {LOG}")
print(f"изменено {len(changed)}, добавлено {len(added)}, всего {len(terms)}")
print("\nИзменения:")
for s, old, new, rat, conf in changed:
print(f" {s}: «{old}» → «{new}» [{conf}]")
print("Новые:")
for s, dst, note, conf in added:
print(f" {s} → «{dst}» [{conf}]")
if __name__ == "__main__":
main()

181
eval/exp12_judges.py Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""exp12 — LLM-судьи (слой 2, ВТОРИЧНО). Кросс-семейные, семейство не судит свои выходы.
Судьи: gemini-3.1-pro-preview + grok-4.3 (кросс-семейные, D13.3г) + gpt-5-mini (нейтральный).
Self-family exclusion: gemini НЕ судит A5; grok НЕ судит A3/A4/A6; gpt-5-mini судит ВСЕ.
Одна комбинированная реплика на (судья, глава, повтор): слепые «Текст N» (перешаффл каждый
повтор = свап позиций), судья возвращает JSON {ranking:[Nbestworst], fidelity:{N:1-5}}.
3 повтора медиана ранга (стиль) и медиана 1-5 (верность, БИЛИНГВАЛЬНО против zh).
Запуск: eval/.venv/bin/python eval/exp12_judges.py
Выход: diag/judges.json (+ summary).
"""
from __future__ import annotations
import json, re, sys, random, time
from pathlib import Path
from statistics import median
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import call_provider, load_env_file
from exp12_pack import arm_chapter_text, POOL, CHAPTERS
load_env_file()
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
CAP_USD = 3.0
REPEATS = 3
PRICES = {"gemini-3.1-pro-preview": (2.0, 12.0), "grok-4.3": (1.25, 2.50), "gpt-5-mini": (0.25, 2.0)}
JUDGES = {
"gemini": dict(name="gemini", base_url="https://generativelanguage.googleapis.com/v1beta/openai",
model="gemini-3.1-pro-preview", api_key_env="GEMINI_API_KEY",
temperature=0.2, max_tokens=8000, exclude={"A5"}),
"grok": dict(name="grok", base_url="https://api.x.ai/v1", model="grok-4.3",
api_key_env="XAI_API_KEY", temperature=0.2, max_tokens=4000,
extra_body={"reasoning_effort": "none"}, exclude={"A3", "A4", "A6"}),
"gpt5mini": dict(name="gpt5mini", base_url="https://api.openai.com/v1", model="gpt-5-mini",
api_key_env="OPENAI_API_KEY", max_tokens=6000, reasoning_params=True,
extra_body={"reasoning_effort": "minimal"}, exclude=set()),
}
SYS = ("Ты — литературный критик и билингвальный редактор (китайский→русский). "
"Тебе дают ИСХОДНИК на китайском и несколько вариантов его русского перевода одной сцены. "
"Оцени два РАЗДЕЛЬНЫХ измерения:\n"
"1) СТИЛЬ — качество русского как ХУДОЖЕСТВЕННОЙ прозы (живость, естественность, "
"отсутствие канцелярита и калек с китайского), без оглядки на точность.\n"
"2) ВЕРНОСТЬ — насколько точно передан смысл ИСХОДНИКА (без потерь, отсебятины, искажений).\n"
"Верни СТРОГО JSON без пояснений, комментариев и текста вне JSON. Значения fidelity — "
"ТОЛЬКО целое число 1..5, без скобок и пояснений. Формат ровно такой:\n"
"{\"ranking\":[3,1,2],\"fidelity\":{\"1\":4,\"2\":5,\"3\":3}}\n"
"ranking — номера всех текстов от лучшего к худшему по СТИЛЮ; fidelity — оценка верности "
"каждого текста 1..5 (5=безупречно точно).")
def cost_of(model, usage):
inp = usage.get("prompt_tokens", 0) or 0
comp = usage.get("completion_tokens", 0) or 0
total = usage.get("total_tokens", 0) or 0
reason = usage.get("reasoning_tokens", 0) or 0
pin, pout = PRICES.get(model, (0, 0))
if model == "gemini-3.1-pro-preview":
reason = max(0, total - inp - comp)
return (inp * pin + (comp + reason) * pout) / 1_000_000
def parse_json(text):
"""Lenient: prefer strict JSON, else regex-extract ranking[] + integer fidelity even amid prose."""
if not text:
return None
m = re.search(r"\{.*\}", text, re.S)
if m:
try:
obj = json.loads(m.group(0))
if "ranking" in obj:
return obj
except Exception:
pass
out = {}
rm = re.search(r'"?ranking"?\s*:\s*\[([0-9,\s]+)\]', text, re.S)
if rm:
out["ranking"] = [int(x) for x in re.findall(r"\d+", rm.group(1))]
fm = re.search(r'"?fidelity"?\s*:\s*\{(.*)\}', text, re.S)
if fm:
fid = {}
for k, v in re.findall(r'"?(\d+)"?\s*:\s*(\d+)', fm.group(1)):
fid[k] = int(v)
if fid:
out["fidelity"] = fid
return out if "ranking" in out else None
def main():
chunks_json = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
# chapter texts per arm
ch_texts = {ch: {arm: arm_chapter_text(arm, ch, chunks_json) for arm in POOL} for ch in CHAPTERS}
sources = {ch: "\n\n".join(chunks_json[f"{ch}/{ci}"]["source"] for ci in (0, 1)) for ch in CHAPTERS}
raw = []
spent = 0.0
stopped = False
for jkey, jspec in JUDGES.items():
if stopped:
break
for ch in CHAPTERS:
arms = [a for a in POOL if a not in jspec["exclude"] and ch_texts[ch][a]]
for rep in range(REPEATS):
if spent > CAP_USD * 0.85:
print(f"!! judges near cap ${spent:.3f} — stop"); stopped = True; break
order = arms[:]
random.Random(f"{jkey}-{ch}-{rep}").shuffle(order) # position swap
labels = {i + 1: arm for i, arm in enumerate(order)}
parts = [f"ИСХОДНИК (zh):\n{sources[ch]}", ""]
for i, arm in enumerate(order):
parts.append(f"=== Текст {i+1} ===\n{ch_texts[ch][arm]}\n")
user = "\n".join(parts)
t0 = time.time()
text, err, usage = call_provider(jspec, SYS, user, timeout=240)
c = cost_of(jspec["model"], usage); spent += c
obj = parse_json(text or "")
ok = bool(obj and "ranking" in obj)
print(f" [{jkey} ch{ch} r{rep}] {time.time()-t0:.0f}s ${c:.4f} cum=${spent:.3f} "
f"{'OK' if ok else 'PARSE-FAIL:'+str(err or (text or '')[:60])}")
raw.append({"judge": jkey, "chapter": ch, "rep": rep, "labels": labels,
"result": obj, "ok": ok, "cost": round(c, 5),
"raw": (text or "")[:1200], "err": err})
# aggregate
# style: per (judge,chapter) collect rank of each arm across repeats; normalize rank/N; median
agg = {}
for r in raw:
if not r["ok"]:
continue
j, ch, labels, res = r["judge"], r["chapter"], r["labels"], r["result"]
ranking = res.get("ranking", [])
n = len(ranking)
for pos, num in enumerate(ranking):
arm = labels.get(int(num)) if str(num).isdigit() else labels.get(num)
if not arm:
continue
agg.setdefault((j, arm), {"style_norm": [], "fid": []})
agg[(j, arm)]["style_norm"].append(pos / max(1, n - 1)) # 0=best..1=worst
fid = res.get("fidelity", {}) or {}
for num, sc in fid.items():
arm = labels.get(int(num)) if str(num).isdigit() else labels.get(num)
if arm and isinstance(sc, (int, float)):
agg.setdefault((j, arm), {"style_norm": [], "fid": []})
agg[(j, arm)]["fid"].append(float(sc))
summary = {}
for arm in POOL:
row = {"arm": arm}
for j in JUDGES:
a = agg.get((j, arm))
if a and a["style_norm"]:
row[f"{j}_style"] = round(median(a["style_norm"]), 3)
if a and a["fid"]:
row[f"{j}_fid"] = round(median(a["fid"]), 2)
# combined style = mean of available judges' normalized median (lower=better)
sv = [row[f"{j}_style"] for j in JUDGES if f"{j}_style" in row]
fv = [row[f"{j}_fid"] for j in JUDGES if f"{j}_fid" in row]
if sv:
row["style_combined_norm"] = round(sum(sv) / len(sv), 3)
if fv:
row["fid_combined"] = round(sum(fv) / len(fv), 2)
summary[arm] = row
(DIAG / "judges.json").write_text(json.dumps(
{"raw": raw, "summary": summary, "total_cost": round(spent, 4), "repeats": REPEATS},
ensure_ascii=False, indent=1), encoding="utf-8")
print(f"\n=== JUDGE SUMMARY (style_norm: 0=best..1=worst; fid 1..5) total ${spent:.4f} ===")
order = sorted(summary.values(), key=lambda r: r.get("style_combined_norm", 9))
for r in order:
print(f" {r['arm']}: style={r.get('style_combined_norm','')} "
f"fid={r.get('fid_combined','')} "
f"(gemini s={r.get('gemini_style','')}/f={r.get('gemini_fid','')}, "
f"grok s={r.get('grok_style','')}/f={r.get('grok_fid','')}, "
f"gpt5 s={r.get('gpt5mini_style','')}/f={r.get('gpt5mini_fid','')})")
if __name__ == "__main__":
main()

96
eval/exp12_pack.py Normal file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""exp12 — сборка СЛЕПЫХ пакетов чтения владельца (слой 1, главный сигнал).
Per глава: 7 вариантов (A0,A1,A2,A3,A4,A5,A6), каждый = склейка чанков арма в полную главу.
Метки V1..V7 ПЕРЕ-РАНДОМИЗИРОВАНЫ ПО ФРАГМЕНТАМ (своя перестановка на главу D13.5), ключ
соответствий в ОТДЕЛЬНОМ файле ВНЕ пакета (diag/reading_KEY.json). A1 (QC) в пакет НЕ идёт.
Выход:
diag/reading_pack/глава_<ch>.md 7 слепых вариантов + бланк ответа
diag/reading_pack/КАК_ЧИТАТЬ.md
diag/reading_KEY.json меткаарм (ВНЕ пакета; не открывать до вынесения оценки)
"""
from __future__ import annotations
import json, random
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
ARMS = DIAG / "arms"
PACK = DIAG / "reading" # D27-аддендум: пакеты чтения владельца сюда (вне git)
SALT = "exp12-blind-v1"
CHAPTERS = [5, 7, 14]
CHUNKS = {5: [0, 1], 7: [0, 1], 14: [0, 1]}
POOL = ["A0", "A1", "A2", "A3", "A4", "A5", "A6"]
REGISTER = {5: "смешанная (нарратив+диалог)", 7: "диалого-плотная", 14: "экспозиция/описание"}
def arm_chapter_text(arm: str, ch: int, chunks_json: dict) -> str | None:
parts = []
for ci in CHUNKS[ch]:
key = f"{ch}/{ci}"
if arm == "A0":
t = chunks_json[key]["draft"]
elif arm == "A1":
t = chunks_json[key]["final"]
else:
p = ARMS / arm / f"{ch}_{ci}.txt"
if not p.exists() or not p.read_text(encoding="utf-8").strip():
return None
t = p.read_text(encoding="utf-8").strip()
parts.append(t.strip())
return "\n\n".join(parts)
def main() -> None:
chunks_json = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
PACK.mkdir(parents=True, exist_ok=True)
key_map = {}
for ch in CHAPTERS:
texts = {arm: arm_chapter_text(arm, ch, chunks_json) for arm in POOL}
missing = [a for a, t in texts.items() if t is None]
if missing:
print(f"!! глава {ch}: НЕ ГОТОВЫ армы {missing} — пропуск (перезапусти после arms)")
continue
order = POOL[:]
random.Random(f"{SALT}-{ch}").shuffle(order) # per-chapter permutation
labels = [f"V{i+1}" for i in range(len(order))]
key_map[str(ch)] = {lab: arm for lab, arm in zip(labels, order)}
out = [f"# Глава {ch} — слепое чтение ({REGISTER[ch]})", "",
"Ниже 7 вариантов ОДНОЙ главы (разные редактуры одного черновика). "
"Прочитай каждый, оцени как ЧИТАТЕЛЬ (естественность русского, живость прозы, "
"отсутствие канцелярита/калек). Форма ответа — в конце файла.", "",
"---", ""]
for lab, arm in zip(labels, order):
out += [f"## Вариант {lab}", "", texts[arm], "", "---", ""]
out += ["## БЛАНК ОТВЕТА (глава %d)" % ch, "",
"1. **Ранжирование** (лучший→худший): V… > V… > V… > V… > V… > V… > V…",
"2. **Читаемо ли** (да/нет по каждому): V1=? V2=? V3=? V4=? V5=? V6=? V7=?",
"3. **Свободные пометки** (что бесит / что хорошо; можно указывать V-метки и абзацы):", ""]
(PACK / f"глава_{ch}.md").write_text("\n".join(out), encoding="utf-8")
print(f"глава {ch}: пакет собран (7 вариантов, перестановка {[key_map[str(ch)][l] for l in labels]})")
if key_map:
(PACK / "_КЛЮЧ_НЕ_ОТКРЫВАТЬ.json").write_text(
json.dumps({"salt": SALT, "map": key_map,
"arms": {"A0": "черновик deepseek (без редактуры)",
"A1": "glm-5 моно (= финал этапа A)",
"A2": "glm-5 билингв", "A3": "grok-4.3 моно",
"A4": "grok-4.3 билингв", "A5": "gemini-3.1-pro билингв",
"A6": "grok-4.3 моно БЕЗ глоссарных констрейнтов"}},
ensure_ascii=False, indent=1), encoding="utf-8")
howto = [
"# Как читать (exp12, слепое)", "",
"- В `diag/reading/` — 3 файла `глава_5.md`, `глава_7.md`, `глава_14.md`. В каждом 7 вариантов V1..V7 ОДНОЙ главы.",
"- Метки V1..V7 **перемешаны СВОЕЙ перестановкой в каждой главе** — «V3» в гл.5 и «V3» в гл.7 это РАЗНЫЕ конфиги. Не пытайся угадать модель — оценивай текст.",
"- Заполни БЛАНК в конце каждого файла: ранжирование + читаемо(да/нет) + пометки. ~12 часа на все три.",
"- Файл `_КЛЮЧ_НЕ_ОТКРЫВАТЬ.json` лежит рядом — **не открывай до сдачи ответов** (слепота свята, урок exp04).",
"- Root-cause (1020 худших мест из 25 глав) уже прислан — обрабатывается отдельным треком.",
]
(PACK / "КАК_ЧИТАТЬ.md").write_text("\n".join(howto), encoding="utf-8")
print(f"\nКлюч → {PACK/'_КЛЮЧ_НЕ_ОТКРЫВАТЬ.json'} (в пакете, держать закрытым до ответов)")
if __name__ == "__main__":
main()

66
eval/exp12_reflow_demo.py Normal file
View file

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""exp12 — ЭКСПЛОРАТОРНАЯ демонстрация (пост-фидбек владельца, ВНЕ слепого бейк-оффа).
Отвечает на прямые замечания владельца: (1) построчные абзацы (кит. «предложение=абзац»);
(2) «фракции» вместо «секты»; (3) 磕头«ударил головой» вместо «земной поклон»;
(4) 时辰«шичэн» без перевода. Один вызов билингв-редактора с ЯВНЫМ разрешением
переверстать абзацы и чинить жанровые конвенции. НЕ арм пре-регистрации иллюстрация фикса.
Материал: сцена смерти (пролог гл.1, 第一节) ровно откуда кейсы владельца.
"""
from __future__ import annotations
import json, os, sys, urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import load_env_file
load_env_file()
DEMO = Path("/home/ubuntu/books/gu-zhenren/diag/reflow_demo")
SRC = (DEMO / "source.txt").read_text(encoding="utf-8").strip()
DRAFT = (DEMO / "draft.txt").read_text(encoding="utf-8").strip()
SYS = (
"Ты — литературный редактор художественного перевода китайской вебновеллы (сянься/культиваторка) на русский. "
"Тебе дан ИСХОДНИК (китайский) и его ЧЕРНОВОЙ перевод. Отредактируй черновик в живую издательскую прозу:\n"
"1. АБЗАЦЫ: собери предложения в логические ЕВРОПЕЙСКИЕ абзацы. Китайская манера «одно предложение — "
"один абзац» для русской прозы деструктивна: группируй повествование по смыслу. Реплики прямой речи "
"оставляй отдельными строками с тире.\n"
"2. СТИЛЬ: убери кальки с китайского и канцелярит, оживи синтаксис и ритм (школа Норы Галь).\n"
"3. СМЫСЛ: сверяйся с ИСХОДНИКОМ и исправляй искажения черновика; сохрани полноту, ничего не выбрасывай "
"и не пересочиняй.\n"
"4. РЕАЛИИ/КОНВЕНЦИИ: культиваторские 门派/派 — «секты» или «школы» (НЕ «фракции»); земной поклон 磕头 — "
"«отвесил земной поклон»/«коснулся лбом пола» (НЕ «ударил головой»); меры времени (时辰 = ~2 часа) — "
"переводи или глоссируй, не транслитерируй вслепую.\n"
"Выведи ТОЛЬКО отредактированный русский текст."
)
USER = f"ИСХОДНИК (zh):\n\n{SRC}\n\n---\n\nЧерновик перевода для редактуры:\n\n{DRAFT}"
SPEC = dict(model="gemini-3.1-pro-preview",
base="https://generativelanguage.googleapis.com/v1beta/openai",
keyenv="GEMINI_API_KEY", max_tokens=16000)
def main():
key = os.environ.get(SPEC["keyenv"], "")
body = {"model": SPEC["model"], "temperature": 0.4, "max_tokens": SPEC["max_tokens"],
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": USER}]}
req = urllib.request.Request(SPEC["base"].rstrip("/") + "/chat/completions",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
with urllib.request.urlopen(req, timeout=300) as r:
data = json.loads(r.read())
text = data["choices"][0]["message"]["content"].strip()
(DEMO / "reflow_gemini.txt").write_text(text, encoding="utf-8")
def paras(t):
return len([l for l in t.split("\n") if l.strip()])
print(f"draft para-lines: {paras(DRAFT)} → reflow para-lines: {paras(text)}")
for probe, good in [("фракци", "секты/школы"), ("ударил головой", "земной поклон"),
("шичэн", "перевод времени"), ("невинност", "清白之身")]:
d = "фракци" in DRAFT.lower() if probe == "фракци" else probe in DRAFT
print(f" '{probe}': draft={probe in DRAFT} reflow={probe in text} (ждём: {good})")
print("\n=== REFLOW head (first 900 chars) ===\n" + text[:900])
if __name__ == "__main__":
main()

89
eval/exp12_rootcause.py Normal file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""exp12 — предварительный АВТО-разбор draft→final по всем 25 главам (затравка root-cause).
Вопрос: редактор glm-5 «ухудшил», «не спас» или «пассивен»? Меряем:
- similarity(draft, final): насколько редактор ВООБЩЕ менял черновик (difflib word-ratio).
~1.0 = редактор почти ничего не сделал (пассивен) нечитаемость унаследована от черновика.
- len_ratio = chars(final)/chars(draft).
- translationese-маркеры (канцелярит/кальки) в draft и final снизил ли редактор.
- дефис-диалоги: доля реплик, начатых '-' (дефис) вместо '' (em-dash) Розенталь §47.
- CJK-остаток в final.
$0. Вход: diag/chunks.json. Выход: diag/rootcause_auto.json + печать сводки.
"""
from __future__ import annotations
import json, re
from difflib import SequenceMatcher
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
CJK = re.compile("[一-鿿぀-ヿ]")
# грубые маркеры translationese/канцелярита (RU) — не приговор, а сигнал тренда
CLERICAL = [r"\bявлял?ся\b", r"\bявляется\b", r"\bявляются\b", r"\bосуществля", r"\анн(ый|ая|ое|ые|ого|ому)\b",
r"\bв связи с\b", r"\bпо причине\b", r"\bв случае\b", r"\bпредставля(ет|л) собой\b",
r"\bбыл(а|о|и)? (?:вынужден|способ)", r"\bимел(а|о|и)? место\b", r"\bс целью\b",
r"\bв результате\b", r"\bпосредством\b", r"\есмотря на то,? что\b"]
CLERICAL_RE = re.compile("|".join(CLERICAL), re.I)
# начало прямой речи дефисом вместо тире
DASH_HYPHEN = re.compile(r"(?m)^\s*-\s+[А-ЯЁA-Z]")
DASH_EM = re.compile(r"(?m)^\s*—\s+[А-ЯЁA-Z]")
def words(t: str) -> list[str]:
return re.findall(r"\w+", t.lower())
def marker_count(t: str) -> int:
return len(CLERICAL_RE.findall(t))
def main() -> None:
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
rows = []
for key, v in chunks.items():
d, f = v["draft"], v["final"]
if not d.strip() or not f.strip():
continue # flagged/skipped edit
sim = SequenceMatcher(None, words(d), words(f)).ratio()
rows.append({
"chunk": key, "chapter": v["chapter"],
"sim": round(sim, 3),
"len_ratio": round(len(f) / max(1, len(d)), 3),
"cler_draft": marker_count(d), "cler_final": marker_count(f),
"hyphen_dlg_final": len(DASH_HYPHEN.findall(f)),
"em_dlg_final": len(DASH_EM.findall(f)),
"cjk_final": len(CJK.findall(f)),
})
n = len(rows)
avg = lambda k: round(sum(r[k] for r in rows) / n, 3)
tot = lambda k: sum(r[k] for r in rows)
summary = {
"n_chunks": n,
"avg_similarity_draft_final": avg("sim"),
"median_similarity": sorted(r["sim"] for r in rows)[n // 2],
"chunks_sim_ge_0.85 (editor near-passive)": sum(1 for r in rows if r["sim"] >= 0.85),
"chunks_sim_ge_0.95 (editor barely touched)": sum(1 for r in rows if r["sim"] >= 0.95),
"avg_len_ratio_final_draft": avg("len_ratio"),
"clerical_markers_draft_total": tot("cler_draft"),
"clerical_markers_final_total": tot("cler_final"),
"clerical_reduced_by_editor": tot("cler_draft") - tot("cler_final"),
"hyphen_dialogue_final_total": tot("hyphen_dlg_final"),
"em_dialogue_final_total": tot("em_dlg_final"),
"cjk_residue_final_total": tot("cjk_final"),
}
(DIAG / "rootcause_auto.json").write_text(
json.dumps({"summary": summary, "per_chunk": rows}, ensure_ascii=False, indent=1), encoding="utf-8")
print("=== AUTO root-cause (draft→final glm-5, all clean chunks) ===")
for k, val in summary.items():
print(f" {k}: {val}")
print("\n По similarity (насколько редактор менял черновик), топ-пассивные:")
for r in sorted(rows, key=lambda x: -x["sim"])[:6]:
print(f" {r['chunk']}: sim={r['sim']} len={r['len_ratio']} cler {r['cler_draft']}{r['cler_final']}")
print(" Топ-активные (редактор много менял):")
for r in sorted(rows, key=lambda x: x["sim"])[:6]:
print(f" {r['chunk']}: sim={r['sim']} len={r['len_ratio']} cler {r['cler_draft']}{r['cler_final']}")
if __name__ == "__main__":
main()