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:
parent
4952673b22
commit
75db9e144f
11 changed files with 1382 additions and 1 deletions
|
|
@ -823,7 +823,9 @@ keep-alive (Ф1–2), инъекция глоссария (`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) ЗАВЕРШЕНА
|
||||
|
||||
|
|
|
|||
259
docs/experiments/12-quality-diagnosis.md
Normal file
259
docs/experiments/12-quality-diagnosis.md
Normal 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.31–0.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.26–2.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 = +15–25% (~$0.83–0.86/ранобэ; замерено: вход редактора +60% токенов, книга +19%)**; «$1.5–2» = опция Б (селективный апекс 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 главы** {2–9, 11–25}.
|
||||
**Метрика на ИСХОДНИКЕ 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 попала ровно на медиану). Оговорка честно: **чистого «экшена»/боя в главах 2–25 нет** (ранняя арка деревни Цинмао — экспозиция и диалог; бои гу — позже), поэтому реализованная ось разнообразия = **диалого-плотность (high/mid/low)**, спан регистра, который реально присутствует в срезе. Гл. 18 (известный alias-дрейф 古月方源, D24.2) правилом НЕ выбрана — конфаунд исключён естественно.
|
||||
|
||||
**Наблюдение (уже видно из инъекции):** гл. 7 несёт **13–17** констрейнт-терминов/чанк (плотная терминосистема рангов), гл. 5 — 3, гл. 14 — 6–9. Гл. 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 chunk0∪chunk1.
|
||||
|
||||
| Арм | Редактор | Режим | Констрейнты | Источник | Стоимость |
|
||||
|---|---|---|---|---|---|
|
||||
| **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.5–4k токенов/вызов. Прайсы (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.6–1.1**. Судьи (см. 1.5): ~2 семейства × 3 главы × (rank-свапы + fidelity) ≈ **$0.5–1.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** + бинарный гейт **«читаемо да/нет»** по каждому + свободные пометки. Целевое время: **~1–2 часа** на все 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-исходника!):** каждый судья оценивает каждый арм 1–5 против исходника, ≥3 повтора, медиана. Раздельно от стиля.
|
||||
- **Гейт «гладко-неверное» (D1.1):** арм, выигравший ЧТЕНИЕ владельца, обязателен к билингв-сверке смысла — если он в низах верности, это фиксируется явно, «гладко-неверное» не побеждает молча.
|
||||
- **Слой 3 (третичный, ultracode):** слепая читательская панель Claude-агентов (Anthropic — вне армов, кросс-семейно; свободна от grok/gemini self-judging) — доп. ранжирование по стилю на тех же слепых пакетах. Информативно, не замена слоям 1–2.
|
||||
|
||||
### 1.6. Root-cause разбор текущих 25 глав
|
||||
|
||||
Владелец даёт **10–20 худших мест** своего чтения → каждое классифицирую по фактам (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.984–1.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 | почти пассивен (1–2 правки на абзац, остальное копирует) |
|
||||
| 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=худший), верность = медиана 1–5 билингвально против 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) · стиль 1–2 (№4, частично №5) · курация глоссария 1–2 (№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 | ранг | ⚠ это ГРАДАЦИЯ ТАЛАНТА, НЕ ранг гу-мастера (转=ранг 1–9); «ранг» уже занят; вариант «класс/сорт таланта 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.8–5.4 | ~5–7 | 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, решение не моё.
|
||||
|
||||
**Ядро:** «перебор моделей» — ложная рамка. Наибольший скачок дал режим-флип на ОДНОЙ модели + почти-бесплатные детерминированные механизмы (вёрстка/санитайзер/глоссарий/сноски) закрывают ~80–90% разрыва «нечитаемо→лучше фана» при COGS ~$1–2/ранобэ. Наука: специализированная LitMT-8B ≈ фронтир на литпереводе (research/03) → боттлнек = пайплайн, не размер модели. Издательский уровень чистым LLM — фронтир-предел индустрии, не дефект стека; достижимо «в целом неотличимо, проигрывает в деталях» (и это уже продаётся).
|
||||
|
||||
**⚠ Ред-тим (честные оговорки, важно):** (1) эмпирика exp12 — ПРЕДВАРИТЕЛЬНА (одна книга, ~8 кейсов, LLM-судьи = циркулярность, прокси-метрики) → флип D1 и веса рычагов ГЕЙТИТЬ на реальном вебновелл-срезе, не D-блоком сейчас; (2) число COGS не сходится: $0.69→$1.5–2 это +117–190%, а не «+15–20% от флипа D1» — в тезис негласно вложены память+эскалация (уже Опция B); чинить арифметику до подписи; (3) глоссарий/память-состояния = САМЫЙ дорогой, человеко-кураторский, наименее доказанный слой (реестр рисков памяти) — не продавать как «дешёвый ров»; (4) рыночное окно = supply gap, НЕ доказанный WTP → нужна проба спроса (Rulate vs бесплатный фан) + оценка ToS/юр 18+.
|
||||
|
||||
**Рекомендация (мемо+ред-тим):** механизмы 2–5 (reflow, санитайзер, глоссарий-энфорс, сноски) катить как фикс пола читаемости НЕМЕДЛЕННО (правильно в любом сценарии); **флип D1 по умолчанию, ре-тезис COGS, ставку на серию-консистентность — за гейтом вебновелл-среза + пробы спроса**; Опция C (издательский) — premium-апсайд/B2B, НЕ блокер запуска.
|
||||
|
||||
**Владельцу на решение:** планка запуска (лучше-фана vs издательский) · готовность к честному COGS (~$1.5–2, верифицировать) · go/no-go на микро-пробу спроса. **Оркестратору:** ратификация (гейтированная) флипа D1, output-санитайзер в скоуп Ф1, разморозка reflow-вёрстки — D-блоком; полигон дефолт не трогает.
|
||||
165
eval/exp12_arms.py
Normal file
165
eval/exp12_arms.py
Normal 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
104
eval/exp12_blocks.py
Normal 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
72
eval/exp12_candidate.py
Normal 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
206
eval/exp12_extract.py
Normal 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+201C…U+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
141
eval/exp12_glossary_v2.py
Normal 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 одобрил русификацию; лучше латиницы A–D).",
|
||||
"采花大盗 → рекомендация «похититель цветов» + СНОСКА при 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
181
eval/exp12_judges.py
Normal 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:[N…best→worst], 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
96
eval/exp12_pack.py
Normal 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 это РАЗНЫЕ конфиги. Не пытайся угадать модель — оценивай текст.",
|
||||
"- Заполни БЛАНК в конце каждого файла: ранжирование + читаемо(да/нет) + пометки. ~1–2 часа на все три.",
|
||||
"- Файл `_КЛЮЧ_НЕ_ОТКРЫВАТЬ.json` лежит рядом — **не открывай до сдачи ответов** (слепота свята, урок exp04).",
|
||||
"- Root-cause (10–20 худших мест из 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
66
eval/exp12_reflow_demo.py
Normal 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
89
eval/exp12_rootcause.py
Normal 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данн(ый|ая|ое|ые|ого|ому)\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несмотря на то,? что\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()
|
||||
Loading…
Add table
Reference in a new issue