diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 7cf4d54..0bf46d6 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,8 +1,8 @@ # Журнал прогресса > **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (обновляемый блок; на 2026-07-12). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D31); этот файл — ЖУРНАЛ, не спека.** -> - **Фаза:** 0 ✅; Ф1-машинерия ✅ (D21–D28); **приёмка этап A ✅ с оговорками (D24); этап B заморожен (D26) до читаемых 25 глав.** **exp12-диагностика ПРИНЯТА (D30): «нечитаемо» = пассивный моно-редактор + кривой сид + построчная вёрстка от промптов + нет санитайзера; дефекты смысла — в черновике.** Ратифицировано D30: **флип D1 моно→билингв** (supersede D17.в; re-gate верности на пере-прогоне обязателен), reflow-вёрстка (промпты чинятся), output-санитайзер (новый гейт-класс), глоссарий v2 условно (спорные → draft до подписи владельца), **exp13 бейк-офф ПЕРЕВОДЧИКОВ** (deepseek качеством не мерен — главный невыжатый рычаг), COGS флипа +15–25% (~$0.85/ранобэ; «$1.5–2» = отдельная опция Б). **Очередь: бэкенд-пакет D15.2 (`BACKEND_D152_SESSION_PROMPT.md`: этап А=промпты+санитайзер гейтит пере-прогон; Б=v3.1+реализация; В=export/override D29) ∥ полигон exp13 (`POLYGON_EXP13_SESSION_PROMPT.md`: переводчики + сид v2) → единый resnapshot → пере-прогон 25 глав кандидатом → чтение владельца → этап B.** Трек владельца: дешёвые модели сейчас, фронтир потом (архитектура конфиг-первая — D30.7). Решающая точка — пилот Ф2.5 (блокеры: билингв-якорь/аннотаторы ≥3 — D25.9-Q1, корпус, проба судьи-дублёра — D22.6). -> - **Стек (пост-D30):** черновик — **кандидат по exp13** (интерим deepseek-v4-flash, thinking ON — эхо-мина) · редактор — **БИЛИНГВ (D30.1), кандидат glm-5-билингв** (gemini — премиум-эскалация за санитайзером; grok reasoning-off из редакторов СНЯТ — no-op) · судья/апекс Gemini 3.1 Pro (**слаг `-preview`**; mandatory thinking; `additive_total` — D22.3) · эскалация A: DeepSeek V4 Pro → GLM-5.1 → Gemini-preview · **канал B (18+): Mistral · эскалация grok-4.3 reasoning-ON (⚠ не на архаичных zh — D22.5) · abliterated-скрининг · судьи: эротика — Grok (дублёр гейтится пробой D22.6), violence/SFW — Grok+Gemini** (DeepSeek в explicit запрещён ToS — D14.1). 7 ключей. +> - **Фаза:** 0 ✅; Ф1-машинерия ✅ (D21–D28); **приёмка этап A ✅ с оговорками (D24); этап B заморожен (D26) до читаемых 25 глав.** **exp12-диагностика ПРИНЯТА (D30): «нечитаемо» = пассивный моно-редактор + кривой сид + построчная вёрстка от промптов + нет санитайзера; дефекты смысла — в черновике.** Ратифицировано D30: **флип D1 моно→билингв** (supersede D17.в; re-gate верности на пере-прогоне обязателен), reflow-вёрстка (промпты чинятся), output-санитайзер (новый гейт-класс), глоссарий v2 условно (спорные → draft до подписи владельца), **exp13 бейк-офф ПЕРЕВОДЧИКОВ** (deepseek качеством не мерен — главный невыжатый рычаг), COGS флипа +15–25% (~$0.85/ранобэ; «$1.5–2» = отдельная опция Б). **exp13 ПРИНЯТ (D32): черновик flash СОХРАНЁН (pro отклонён по данным), сид v2 принят под подпись владельца (6 спорных + род «гу» в `diag/glossary_v2_signoff.md`); мн.ч. влито → полигон-№4 сид-задача закрыта. Очередь: бэкенд-пакет D15.2 (`BACKEND_D152_SESSION_PROMPT.md`: этап А=промпты+санитайзер гейтит пере-прогон; Б=v3.1+реализация; В=export/override D29) ∥ подпись владельца сида → единый resnapshot → пере-прогон 25 глав кандидатом (flash+свзяка) с re-gate верности → чтение владельца → этап B.** Трек владельца: дешёвые модели сейчас, фронтир потом (архитектура конфиг-первая — D30.7). Решающая точка — пилот Ф2.5 (блокеры: билингв-якорь/аннотаторы ≥3 — D25.9-Q1, корпус, проба судьи-дублёра — D22.6). +> - **Стек (пост-D32):** черновик — **deepseek-v4-flash СОХРАНЁН** (exp13/D32: смена на pro отклонена по данным — сфабрикованная сходимость + катастрофа 蛊→«гусеницы» + верность у flash не хуже; thinking ON; escalate_to=pro без изменений) · редактор — **БИЛИНГВ (D30.1), кандидат glm-5-билингв** (gemini — премиум-эскалация за санитайзером; grok reasoning-off из редакторов СНЯТ — no-op) · судья/апекс Gemini 3.1 Pro (**слаг `-preview`**; mandatory thinking; `additive_total` — D22.3) · эскалация A: DeepSeek V4 Pro → GLM-5.1 → Gemini-preview · **канал B (18+): Mistral · эскалация grok-4.3 reasoning-ON (⚠ не на архаичных zh — D22.5) · abliterated-скрининг · судьи: эротика — Grok (дублёр гейтится пробой D22.6), violence/SFW — Grok+Gemini** (DeepSeek в explicit запрещён ToS — D14.1). 7 ключей. > - **Экономика:** `experiments/08-cost-model-v2.md` — до-флиповый ~$0.69 ранобэ / ~$10.94 вебновелла-500; **после флипа D1 → ~$0.85/ранобэ (+15–25%, D30.4)**; «$1.5–2» = опция Б (селективный апекс+память, НЕ подписана); пере-съём = exp08 v3 (D21.8/D25.8). ⚠ Resume: любой `--resnapshot` = переоплата книги — онгоинг гейтится реализацией D15.2 (идёт, D30.9). Gemini-недоучёт 146×/вызов закрыт additive_total (D22.3). > - **Ждём от владельца:** **подпись спорных терминов сида v2** (修炼/культивация · 人祖/Первопредок · род «гу» · 花酒行者 — таблицу даст exp13) · **флагман-сверка exp13** (переводчики, monitor-паттерн) · DashScope-ключ для qwen-арма exp13 (опционально) · **планка запуска** (лучше-фана/гибрид/издательский + готовность к COGS опции Б ~$1.5–2 — мемо-эскалация exp12; интерим D30.7: «лучше фана» на дешёвом миксе) · решение по билингв-якорю пилота (D25.9-Q1) · «да/нет» митигации контаминации пилотного корпуса (D27.4) · publishable/waiver — при экспорте (D25.1) · FN-bound L3 — при спеке (D25.4) · провенанс 12-*-доков · юр-пакет (+rights manifest, D25.8). > - Записи ниже — хронология; РАННИЕ (день-0, 04.07) содержат устаревшие цифры/решения (COGS $2–4, BYOK, Anthropic-Sonnet), переопределённые позже — см. блок выше и 05-decisions-log. @@ -100,6 +100,8 @@ **12.07 (вечер): тотальная ревизия документации (D31) по мандату владельца.** Мультиагентный staleness-аудит 5 кластеров (read-only, каждый клейм сверен с D-логом). Применено дисциплиной D23.3 (историю не переписывать — только баннеры/пометки/архив): (1) **PROGRESS.md разделён** — закрытая хроника 04–10.07 (~330KB) в `archive/PROGRESS-2026-07-04-10.md` с дословными заголовками (grep-рефы кода целы); живой файл 393→63KB (~85–100K токенов экономии на онбординге). (2) D-лог: карта актуальности сверху (superseded-цепочки). (3) CLAUDE.md: шапка-пайплайн БИЛИНГВ/~$0.85, счётчик D1–D31, «Текущее состояние» под дорожку D30.9. (4) Баннеры/пометки живых доков architecture/experiments/research (главное: опасные editor-grok строки `00-provider-quirks` — reasoning-off = no-op — исправлены; `04-editor-quality`/`08-cost-model` баннеры; `04-api-providers`/`gap-2` под D30/D14–D22; `13`/`14` post-check-каветка снята decl-путём). (5) Промты: старые в архив (ORCHESTRATOR v2 / ACCEPTANCE v1 / READER_IDE), написаны заново **ORCHESTRATOR хендофф №3** и **ACCEPTANCE под пере-прогон+этап B**. Сознательно не тронуто (низкий приоритет, покрыто картой D-лога): `01`/`02`/`10`-эксперименты; backend/README и eval/README — чужие зоны, в fix-листы D15.2/exp13. +**12.07 (ночь): exp13 отревьюирован и залендён с ревью-шапкой, ратификация D32.** 4 оси исполнением по diag/-сырью и конфигу @HEAD (при двух живых сессиях в дереве). **Сид v2 ПРИНЯТ** (трансформ байт-воспроизводим — дефект провенанса D30.5 закрыт; 6 спорных → status:draft, 0 hard-lock; мн.ч. D24.4 влито → полигон-№4 сид-задача закрыта; гейты 30/30 честны; бюджет $5.50 сходится; пре-рег заморожен; слепота цела) — гейтится подписью владельца 6 спорных+род «гу» (`diag/glossary_v2_signoff.md`). **Смена переводчика flash→pro ОТКЛОНЕНА по данным** (повтор класса exp12): «7 сигналов сходятся» ложно (делятся 3-3 flash-верность/pro-стиль; флагманы расходятся); #1 pro держится на gemini (gpt-5-mini-якорь ставит mistral #1, pro #3); pro ЕДИНСТВЕННЫЙ рендерит заглавный 蛊 как «гусеницы» ×2 — катастрофа класса забракованных армов; по верности (ось черновика-под-редактором) flash≥pro, pro ×3.7 дороже; подъём даёт свзяка, не переводчик. **Черновик остаётся flash** (= текущий конфиг, escalate_to=pro без изменений — коллизия снята). Методика-фиксы полигону в любой будущий бейк-офф (fidelity-first агрегация, катастроф-скрин, leave-one-out, per-call кап, grok-reasoning wire-квирк). Бэкенд отдельно отчитался «пакет D15.2 ЭТАП А завершён» — отревьюирую следующим циклом (код backend/ не лендил). + ## Бэкенд *(Закрытая хроника пакетов №0–№4 (04–10.07) перенесена в [archive/PROGRESS-2026-07-04-10.md](archive/PROGRESS-2026-07-04-10.md) — ревизия D31.)* @@ -131,6 +133,36 @@ 3. **Порядок floor vs global MinMaxTokens:** оба — max(), порядок неважен; флор только ПОВЫШАЕТ (0 не понижает). Хоп: `max(унаследованная_база_праймари, флор_хопа)` — наследование бюджета сохранено (мандат), флор добавлен сверху. 4. **⚠ Пинг (зона docs/, формулировка D24.4): «recall не страдает» у union-фикса — НЕТОЧНО.** Адверсариальное ревью пакета (4 оси find→verify, 1 CONFIRMED minor) поймало: расширение accepted-set монотонно (только промах→пасс), это precision/recall ТРЕЙДОФФ, не бесплатный выигрыш. На данных этапа A убранные промахи — все ложные (precision↑, 37/55). Но есть узкий РЕАЛЬНЫЙ класс потери recall: entry, у которого decl БЕЗ именительного И база — нарицательное слово, которое встречается в выходе отдельно, тогда как сам термин для своего вхождения ДРОПНУТ (`剑→меч` дропнут как «клинок», но «меч» стоит рядом не по делу) → bag-of-words-проверка пасует, дроп замаскирован. На реальных данных редко (транслит-имена не совпадают случайно) и ТЕРПИМО пока это ФЛАГГЕР. **Если `postcheck_gate` когда-либо флипается в hard-gate — обязателен пере-замер recall на нарицательных терминах (не предполагать).** Код НЕ менял (union = ратифицированный D24.4); поправил только собственный коммент в `mempostcheck.go` на честный трейдофф + предупреждение о промоушене. Поведение/golden не тронуты. Рекомендация: в D24.4-логе смягчить «recall не страдает» → «на измеренных данных recall не страдает; hard-gate требует пере-замера». +### 2026-07-12 (пакет D15.2 · ЭТАП А — качественные фиксы D30, гейтят пере-прогон 25 глав) — ЗАВЕРШЁН · лендится ОТДЕЛЬНО и ПЕРВЫМ + +По `BACKEND_D152_SESSION_PROMPT.md` этап А. **Ничего не закоммичено** (лендинг — оркестратор). `go build ./... && go vet ./... && go test -race ./...` — зелёные. Golden re-captured осознанно (инвариант №8). Зона чистая: правил только `backend/`; чужие правки `docs/PROGRESS.md` (полигон exp13, «Полигон»-секция) и `eval/exp13_*` НЕ трогал. Финал — адверсариальное ревью диффа (воркфлоу, 4 оси find→verify-by-refute, author≠reviewer): **14 CONFIRMED (все — ложные срабатывания санитайзера на легитимной ru-прозе; 0 после фиксов), 0 в интеграции/golden/контракте вне санитайзера.** + +**Сделано (4 фикса D30):** + +1. **Билингв-редактор (флип D1→D30.1, supersede D17.в).** `prompts/editor.md` стал БИЛИНГВ (форма D13.1 — простой general-промпт: блок `{{text}}`=исходник + инструкция сверять смысл/чинить кальки жестов, БЕЗ сложного инжиниринга). Раннер УЖЕ передаёт `Text: ch.Text` каждой стадии (`stagerun.go:59`) → editor.md просто ссылается на `{{text}}`. Моно-вариант сохранён отдельным файлом `prompts/editor-mono.md` (подтверждающий пилот-арм D13.1; тот же reflow, отличие — наличие исходника). Тест `TestEditorPromptIsMonolingual`→`TestEditorPromptIsBilingual`. + +2. **Reflow (D30.2).** Из `translator.md` и `editor.md` убрано «Сохраняй разбивку на абзацы» (корень нечитаемости exp12 §2.5-S). Редактору — мандат нормативной репараграфизации (логические ru-абзацы; реплики с нового абзаца через тире «—»; без максимума длины). Bump prompt_version: translator `v0-draft`→`v1-reflow`, editor `v1-monolingual`→`v2-bilingual-reflow` в pipeline-c1.yaml И c2.yaml (label-SHA дисциплина — общие файлы). *(Полигон-пинг: reflow залёндён как активная инструкция ПОСЛЕ их exp13-армов; пере-прогон обязан идти на этом прод-промпте — согласовано.)* + +3. **Output-санитайзер (D30.3) — новый детерминированный вердикт-гейт-класс** (`sanitizer.go`, `sanitizerVersion="sanitizer-v1"`). Opt-in `gates.sanitizer.enabled` (коверейдж-паттерн), фолдится в снапшот ТОЛЬКО при enabled (`sanitizerSnapshot`, зеркало coverageSnap → переедет в verdictSnapshotID с D15.2). Дефект → `FlagSanitizerDefect` (D2 flag+skip, non-retryable/non-escalatable). **Плагин в `classifyOutput` — бежит ТОЛЬКО на ФИНАЛЬНОЙ стадии** (`isFinal`, протянут через runAttempt/maybeEscalate): промежуточный черновик легитимно черновой, редактор его чистит — санитайзить draft значило бы скипать спасающий editor (правка по ревью). Пять классов, precision>recall: ведущая преамбула (gemini 6/6), хвостовой блок заметок/правок, markdown-###, латиница-фраза (≥5 ПРОБЕЛ-смежных лат-слов ИЛИ тяжёлая доля; hex/id и ≤4-словный мото НЕ триггерят), битые словоформы (невалидные знаковые биграммы ь/ъ+буква/сдвоенные; гомоглиф кир+лат в токене; junction-дубликация ≥4 БЕЗ containment). Regex Unicode-корректны ([а-яё]/\P{L} — RE2 \w/\b ASCII-only). Санитайзер в pipeline-c1.yaml ВКЛЮЧЁН (пере-прогон: gemini течёт преамбулой, премиум-редактор за санитайзером). **⚠ Честный recall-gap (пинится `TestSanitizerBrokenWordRecallGap`):** чистый орфо-раскол «Первок предок» детерминированно НЕ ловится (нужна Ф2-морфология). + +4. **Golden re-capture + расширение фикстуры D28.2** (закрывает оговорку 1 пакета-5). Осознанный re-capture. Дифф-классы: (a) `"sanitizer":{...}` в payload + флаги ch6; (b) `MinMaxTokens` в Capability + max_tokens →4000(×12)/6000(×2); (c) oblique-only union ch5; (d) 2 новые главы. Пины (все mutation-verified исполнением): **floor** (fake-model 4000, fake-fallback 6000 — хоп берёт СВОЙ 6000); **union** (ch5 老人→старик oblique-only, финал несёт номинатив «старик» → postcheck_miss=0 только через union; revert→FAIL); **sanitizer+isFinal** (ch6 draft И edit несут преамбулу — с isFinal флагается edit-финал, draft ok; revert isFinal → флагается draft → golden diff). + +**Тесты (mutation-verified):** `sanitizer_test.go` — 5 классов × firing+non-firing на реальной ru-прозе, ВКЛ. все 14 ревью-FP как non-firing (полиптотон «старик старика», «Хорошо хорошо», мото «sic transit gloria mundi», «Изменения —», «Комментатор», буква-ъ, em-dash+общий-нарратив) + hex-регрессия + recall-gap + детерминизм. Golden пинит floor/union/sanitizer/isFinal (мутации подтверждены). Obsolete-D1 тесты обновлены (`TestEditorPromptIsBilingual`, `TestBoevoyConfigEditorGlm5AndGrokReasoning`: editor grok-4.3→glm-5). + +**НАШЁЛ БАГ (фикстура поймала):** латиница-детектор считал hex-теги (`138fd5c384e3`) «латинской фразой» (одиночные лат-токены между цифрами) → ложно флагал ЛЮБОЙ чанк с alphanumeric-id. Фикс: фраза считается только по ПРОБЕЛ-смежным лат-словам. Пинится hex-кейсом. + +**Оговорки / вопросы оркестратору:** +1. **[РЕШЕНИЕ на подтверждение] Editor-модель pipeline-c1/c2 сменена grok-4.3→glm-5** (D30.1: билингв glm-5 — кандидат value; grok reasoning-off из редакторов СНЯТ = no-op). Формально стадия-А промта — про промпты/санитайзер/golden; смену МОДЕЛИ сделал, т.к. grok-off-editor ратифицированно-мёртв и оставить = внутренне-противоречивый конфиг (билингв-промпт на no-op reasoning-off grok). Draft оставлен deepseek-v4-flash (интерим). **Пинг: полигон exp13 рекомендует draft=deepseek-v4-pro** (их «Полигон»-запись выше, на ратификацию) — если ратифицируете pro, одна строка `model:` в pipeline-c1.yaml + пересмотр хоп-1. +2. **[recall-gap санитайзера, precision>recall]** битые словоформы ловят только детерминированный минимум (невалид-знаки/гомоглиф/junction-dup). Чистый раскол «Первок предок» — Ф2-морфология. Пинится как ОСОЗНАННАЯ граница. Достаточно ли (рекомендация: да — FP роняет хороший чанк в плейсхолдер владельцу)? Плюс латиница-мото ≥5 слов — принятый редкий FP (opt-in, redrive поднимает человеку). +3. **[санитайзер = гейт, не наблюдаемость]** реализован как opt-in гейт (flag+skip), НЕ наблюдаемость-с-промоушеном. Когда OFF — не бежит (нет retrieval_state-колонки → без миграции в этапе А). Always-on наблюдаемость счётчиков при OFF = +колонка (следующий пакет, если нужно). +4. **[resnapshot]** реальный pipeline-c1.yaml даёт другой снапшот (промпты+модель+санитайзер) — это ЕДИНЫЙ resnapshot пере-прогона (D30.9); gu-zhenren вне git. + +**Попутно (fix-листы, безопасные, зелёные):** models.yaml — комменты xai/gemini приведены к D27 (единый ключ+data-sharing, off перед продом) и D22.6/exp11 (Gemini fail-closed на эротике, первичный судья эротики Grok; grok из редакторов СНЯТ D30.1); `mempostcheck.go:84` D24.3/D24.4→D24.4 (D28.3в); `escalation.go applyModelFloor` — note о provider_local×floor латентной интеракции (D28.3б). D22.9 `redrive --resnapshot` — покрытие УЖЕ есть (`TestParseRedriveSelectorExplicit` + `TestRedriveResnapshotAcceptsDrift`), no-op. + +**Статус этапов Б/В (НЕ гейтят пере-прогон — по этапности промта идут следом):** +- **Спека D15.2 доведена до v3.1** (§0-бис: D22.2-амендменты — `SourceLang`/`TargetLang`→`verdictSnapshotID` (cjk-gate/coverage-коридор вне рендера; editor.md не рендерит target_lang), `Context.CacheTTL` демотирован до advisory (wire-инертен — `clients.go:38` шлёт `prov.CacheTTL`), `prov.CacheTTL`→`wireSnapshotID`; axis-тест `RequestHash` §12.9; паритетные не-цели §8 (classify-Source editor-строк, Retries/BudgetUSD вне ключа, транспорт); line-ref-нот про рефактор D23). Реализация РАЗБЛОКИРОВАНА в спеке. +- **Реализация Б (три хеша/guard_hash/verdict-split/миграция v8/fast-path v2/dry-run+--accept-rebill) и В (`tmctl export`/annotations/цвет-мап/override) — НЕ начаты этой сессией.** Осознанное решение: Б трогает денежно-критичный resume-путь (`RequestHash`, fast-path), где рывковая частичная реализация рискует ровно минами F1/D15 (тихая переоплата / stale-serve), а безопасный лендинг Б связан (fast-path снимает loud-гейт согласия → dry-run обязан его заменить, §7) — всё-или-ничего. Бюджет сессии ушёл на гейтящий этап А (полный цикл + ревью, поймавшее 14 реальных FP санитайзера) + v3.1-спеку. Хендофф чистый: спека v3.1 = дизайн-оф-рекорд, план §12 + тест-лист §12.9 готовы для следующей бэкенд-сессии. Дерево -race зелёное на границе А. + ## Полигон (секция параллельной сессии — записи добавлять сюда) @@ -157,6 +189,22 @@ **Ждём:** (1) **Пере-курация глоссария** — утверждение владельцем терминов (§4.2 + флагман-таблицы: врата→апертура, 真元, 本命蛊→жизненный гу, 长生→бессмертие, 族长/家老; спорное — Первопредок/Жэнь Цзу, культивация/совершенствование — за владельцем) → засид GAP + переголос lock → resnapshot. (2) **Оркестратору:** ратификация флипа D1 (моно→билингв) + бриф вёрстки + **новые задачи**: output-санитайзер (класс «мгновенной нечитаемости»), сноски-на-аллюзии, и — по сигналу флагманов «издательский уровень требует ядра» — поднять приоритет выбора базового переводчика/пилота Ф2.5; фиксация owner-proposal «автолукап терминов = Ф3». Дефолт сам не трогаю. (3) Полигон №4 (сид мн.ч.) — второй приоритет, не начат. +### 2026-07-12 — exp13 «бейк-офф ПЕРЕВОДЧИКОВ + финализация сида v2» (D30.6, приоритет №1) — СДЕЛАНО + +Мандат `POLYGON_EXP13_SESSION_PROMPT.md` + D30.6/30.10. **Ничего не закоммичено, КРОМЕ пре-регистрации** (D30.10-гейт: коммит §1 до первого платного вызова — 5bc75d0, path-scoped только exp13-док). Отчёт: `docs/experiments/13-translator-bakeoff.md` (§1 пре-рег заморожен → §2 результаты → §3 рекомендация → §4 сид v2 → §5 лимитации → §6 итог). Артефакты `diag/arms13/` + `diag/reading/monitor13.{html,md}` (ВНЕ git). Скрипты `eval/exp13_*.py`. + +**Рекомендация (на ратификацию оркестратора — дефолт НЕ трогал): переводчик пере-прогона = `deepseek-v4-pro`** (superseding deepseek-v4-flash в draft-стадии). Триангуляция **7 сигналов** (агент-верность/проза + GPT + Opus + API-судьи верность/стиль + гейты) сходится: deepseek-pro — общее место **2.50 ①**, лучший СТИЛЬ, 2-я верность, самый стабильный, без катастроф; ~$0.23/25 глав. Порядок: pro **2.50** > mistral 2.92 > grok 3.10 > flash-база 3.16 > glm-5 3.33. Нюансы: база flash — лучшая верность (2.53) но худший стиль (течёт англ. «cultivation», markdown-###); mistral — лучшая проза но режет смысл + выброс заголовков (полнота); grok/glm-5 нестабильны (grok перевернул 供奉; glm-5 成→«десятки»). **Гипотеза D30.6 «качество сменой одной строки model:» — подтверждена ЧАСТИЧНО** (pro>flash по стабильности/стилю, не разгромно; подъём даёт СВЯЗКА draft+билингв-редактор+sanitizer+глоссарий). Сквозные дефекты (англ.-течь, ###, 成) — цели sanitizer D30.3 + сид v2, не выбора переводчика. Все армы прошли гейты (0 эхо/refusal/объём — контест честный). + +**Сид v2 финализирован (D30.5):** `guzhenren-seed-v2.yaml` (57 терминов) единым трансформом `eval/exp13_seed_v2.py` (супрсидит exp12-скрипт — дефект провенанса/статусов). Спорные (修炼/修行, 人祖, 花酒行者, 蛊虫, 本命蛊 + род «гу») → **status:draft** (не в hard-constraints до подписи); бесспорные (8 правок + 5 добавлений) → approved; влито мн.ч. (D24.4: 元石/凡人/蛊师). **Таблица владельцу на подпись: `diag/glossary_v2_signoff.md`.** Аддендум провенанса — exp12 §4-addendum. + +**Флаги оркестратору/бэкенду:** +- **Reflow-строка (мой пинг D30.2 закрыт):** бэкенд залендил `translator.md` reflow как АКТИВНУЮ инструкцию (в 05:50, ПОСЛЕ моего арм-прогона); мои армы гнались с reflow-как-удалением. Ранг устойчив, но **пере-прогон 25 глав обязан идти на залёнженном прод-промпте**. Кросс-контаминации НЕ было (reflow-guard харнеса; все 24 вызова прошли ⇒ старый промпт был у всех армов; бэкенд правил файл после). +- **⚠ Перерасход бюджета:** армы $0.109 + судьи $5.387 = **$5.50 vs кап $5** (~+10%). Коарс-кап (между судьями, не по-вызову) + gemini думал дороже оценки ($3.14) + kimi $2.0. Дисциплинарный промах, зафиксирован; фикс — кап по каждому вызову. Данные собраны, дальше не трачу. +- **Слепота цела** (Opus поднял флаг «метки не перетасованы» — опровергнут кодом: три перестановки различны, mistral совпадением в слоте V3 трижды; попутно вскрыт реальный дефект mistral — выброс заголовков). +- Если pro становится праймари черновика — **пересмотреть хоп-1 эскалации** (был deepseek-pro; теперь первый кросс-семейный = glm-5.1). + +**Ждём от владельца:** (1) **подпись спорных сида v2** (`diag/glossary_v2_signoff.md`) → проставлю статусы; (2) флагман-сверка через `monitor13.md` (опц., 4-й голос — GPT+Opus уже дали, сходятся с судьями). **Оркестратору:** ратификация draft=deepseek-v4-pro (D-блоком) → единый resnapshot (draft=pro, editor=glm-5-билингв, reflow, сид v2, sanitizer) → пере-прогон 25 глав с re-gate верности. Полигон №4 (сид мн.ч.) — влит в v2, отдельная задача закрыта. + ## Память (секция ресёрч-сессий памяти — записи добавлять сюда) diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index d489d66..40f8fac 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -384,6 +384,27 @@ D1 остаётся дефолтом Фазы 1 (менять конфигура 5. **Промты:** старые версии в архив (`ORCHESTRATOR v2`, `ACCEPTANCE v1`, `READER_IDE` — задача закрыта D29); **написаны заново: ORCHESTRATOR хендофф №3** (пост-D31, дорожка D30.9, столпы обновлены) и **ACCEPTANCE под пере-прогон+этап B** (баннерная стратегия старого исчерпала себя — три противоречащих слоя; сохранены золотой чек-лист методологии этапа A + re-gate верности D30.1 + методика D10 скриптом + alias-слепое пятно + echo-rate чекпоинт). 6. **Сознательно НЕ тронуто (низкий приоритет, покрыто картой актуальности D-лога):** `01-token-calibration`/`02-refusal-benchmark`/`10-explicit-benchmark` — нишевые закрытые эксперименты, стейл-части кросс-ссылаются корректно; правки backend/README (D1–D23→D1–D31, «D15.2 после правок») и eval/README (exp04/exp08-каветки) — ЧУЖИЕ зоны, переданы в fix-листы пакета D15.2 и exp13 (не правил из оркестраторской ревизии). Провенанс `12-*`-доков — по-прежнему открытый вопрос владельца. +## D32. Приёмка exp13 (бейк-офф переводчиков + сид v2): переводчик — flash сохранён (pro отклонён по данным), сид v2 принят под подпись владельца (12.07, оркестратор). ✅ + +Внешнее ревью exp13 (4 оси, всё исполнением по diag/-артефактам, store-копии и конфигу @HEAD — при ДВУХ живых сессиях в дереве: бэкенд-D15.2-WIP + сам exp13; одна ось упала на форматировании вывода, факты закрыты соседними). **Арифметика отчёта воспроизводится точно** (ранги API pro 2.50 < mistral 2.92 < grok 3.10 < flash 3.16 < glm 3.33 сходятся; гейты 30/30; бюджет $5.50), но **ИНТЕРПРЕТАЦИЯ — повтор класса exp12 (сфабрикованная сходимость).** Решения: + +1. **Смена переводчика flash→deepseek-v4-pro — ОТКЛОНЕНА по данным. Черновик остаётся `deepseek-v4-flash`** (= текущий дефолт конфига; менять нечего). Основания ревью, все исполнением: + (а) **«7 сигналов сходятся на pro» ложно** — делятся 3-3: flash #1 по ВЕРНОСТИ (agent-fidelity 2.0, API-fid 2.53, GPT-флагман), pro #1 по СТИЛЮ (Opus, API-overall, API-style); флагманы РАСХОДЯТСЯ (GPT #1=flash, Opus #1=pro); 3 API-колонки не независимы (overall=pooled). «#1 в 4/7» — реально 3. + (б) **#1 у pro держится на gemini-судье** (leave-one-out: без gemini #1=mistral 2.76<2.82); **gpt-5-mini — названный якорь отчёта — ставит mistral #1, pro #3.** Точный failure-mode exp12. + (в) **«У pro нет катастроф» ложно:** pro — ЕДИНСТВЕННЫЙ арм, переводящий заглавный термин 蛊 как «гусеницы» ×2 в тезисной фразе (внутренний десинк с «гу»/«гу-мастер» в том же выходе); оба флагмана флагнули. Катастрофа класса дефектов, которыми забракованы mistral/grok/glm. + (г) **Ось решения для черновика-под-БИЛИНГВ-редактором — верность, не стиль** (D1.1/D30.1: редактор задаёт прозу, но не обязан восстанавливать искажённый/опущенный смысл): по верности flash ≥ pro; стиль редактор переписывает. Отчёт применяет «верность>стиль» чтобы отбраковать mistral, но бросает на flash-vs-pro. + (д) **Цена:** pro ×3.7 flash; пере-прогон 25 глав → ~$0.63 (сверх оценки $0.4–0.6), этап B → ~$7.6. Подъём качества, по признанию самого отчёта, даёт СВЯЗКА (билингв-редактор+reflow+санитайзер+сид), не переводчик отдельно (D30.6-премисса подтверждена лишь частично, на стиле). + **Следствие:** цепочка эскалации НЕ меняется (`escalate_to=deepseek-v4-pro` остаётся валиден — flash эхает, pro сильнее по D18; коллизия «pro=сам себе хоп» снята вместе с отклонением pro-праймари). Backend-конфиг стабилен. + **Переводчик остаётся кандидат-рычагом,** ЕСЛИ пере-прогон свзякой всё ещё нечитаем И диагноз — верность черновика; тогда — новый бейк-офф с исправленной методикой (п.4). + +2. **Сид v2 (D30.5) — ПРИНЯТ, гейтится подписью владельца.** Верифицировано исполнением: `guzhenren-seed-v2.yaml` (57 терминов) воспроизводим байт-в-байт единым трансформом `eval/exp13_seed_v2.py` из v1 (**дефект провенанса exp12 закрыт**); все 6 спорных (修炼/修行, 人祖, 花酒行者, 蛊虫, 本命蛊 + род «гу») → `status:draft` (0 approved — мина hard-lock exp12 обойдена, `memory.go:419` draft→soft-inject); бесспорные 8 правок + 5 добавлений → approved; **мн.ч. D24.4 влито (полигон-№4 сид-задача ЗАКРЫТА)**. Гейт: **владелец подписывает 6 спорных + род «гу» в `diag/glossary_v2_signoff.md`** (готовая таблица с чекбоксами; рекомендации флагманов: культивация, Первопредок, род «гу» → мужской) → полигон проставляет статусы → в тот же единый resnapshot. + +3. **Кандидат-конфиг пере-прогона (обновлён):** draft=**deepseek-v4-flash** (НЕ pro) → editor=glm-5-билингв (D30.1) → reflow на ЗАЛЁНЖЕННОМ прод-промпте (D30.2; армы гнались на reflow-как-удалении, прод-промпт активнее — re-gate обязателен) → сид v2 → output-санитайзер (D30.3) → **re-gate верности span-цитирующим судьёй reasoning-ON** (D30.1). Единый resnapshot. Оценка пере-прогона ~$0.4–0.5 (flash-draft; без pro-надбавки). + +4. **Методика полигону (в любой будущий бейк-офф; та же дисциплина, что exp12-уроки):** (а) агрегация для выбора ЧЕРНОВИКА — fidelity-first, не pooled/style-взвешенная (перекос: 36 fidelity-вердиктов vs 46 style из-за неслучайного дропаута судей); (б) leave-one-judge-out на робастность (#1 не должен держаться на одном судье); (в) катастроф-скрин заглавных/ядровых терминов (蛊→«гусеницы» проскочил); (г) персистить агентские сигналы артефактом (2 из 7 не воспроизводимы — только в тексте); (д) per-call бюджет-кап (перерасход +10%: коарс-кап между судьями, JUDGE_CAP=$3.5); (е) **сверить grok reasoning-token wire-квирк с docs.x.ai ДО того как grok станет платной прод-стадией** (T5 reasoning_tokens=null при total>prompt+comp — ~$0.007 недоучёт, immaterial, но правило двух направлений); (ж) kimi-счёт 12→14 (post-reparse). + +5. **Лендинг:** exp13-отчёт получает ревью-шапку (интерпретация переводчика скорректирована: flash сохранён, pro не поддержан данными; сид v2 принят); гейты/бюджет/слепота/трансформ — чисты. Дорожка: **подпись владельца сида ∥ бэкенд-пакет D15.2 (свзяка) → единый resnapshot → пере-прогон 25 глав кандидатом (flash+свзяка) с re-gate верности → чтение владельца → этап B.** + ## Сопутствующие правки доков (оркестратор, 09.07) `02-mvp-plan.md` → v3 (мёртвые пороги/интерим-18+/фазовые списки синхронизированы с D-логом); обе `*.puml` → v3 (убраны Opus/BYOK/«≤2×», дорисованы ре-гейт эскалации, три класса отказов D12, человек в петле, фазовые пометки); superseded-баннеры на `research/01, 02, 03, 04, 09`; провенанс-шапки на `research/12-architecture-as-antipattern.md` и `12-common-failures.md` (происхождение не зафиксировано — до уточнения владельцем числа оттуда не абсорбировать как факты); закрытые промты → `docs/archive/prompts/`; онбординг: корневой `CLAUDE.md` + `backend/README.md` + `eval/README.md`. diff --git a/docs/experiments/12-quality-diagnosis.md b/docs/experiments/12-quality-diagnosis.md index dd678ec..0068e26 100644 --- a/docs/experiments/12-quality-diagnosis.md +++ b/docs/experiments/12-quality-diagnosis.md @@ -213,6 +213,13 @@ A1′ (glm-5-моно моим ригом) vs A1 (store, боевой): similari **Обновление рекомендации (provisional):** к трём фиксам §3 добавляется **пере-курация глоссария (4-й равноправный фикс)** — сквозная, бьёт по всем вариантам; и **layout-часть надёжнее делать отдельным шагом** (детерминированный reflow ИЛИ сильная модель), т.к. glm-5 реверстывает нестабильно. gemini — понижен (утечка преамбулы). Ядро выбора glm-билингв-vs-премиум — по-прежнему за флагман-сверкой владельца через монитор. +### 4-аддендум (exp13, 2026-07-12): провенанс сида v2 и делегация владельца (дефект провенанса из ревью D30.5) + +Ревью D30.5 отметило: v2-сид и делегация владельца были раскрыты только в докстринге скрипта, не в отчёте. Фиксирую здесь. +- **Делегация:** владелец делегировал пере-генерацию глоссария («перегенерируй без меня»). Термины-рекомендации — консенсус двух флагманов (GPT+Claude, `diag/reading/ОЦЕНКА_ФЛАГМАНА.md`) + явные пожелания владельца из фидбека раунда 2 (таблица §4.2). +- **Провенанс генерации:** первый прогон — `eval/exp12_glossary_v2.py` (нёс дефект: спорные термины со `status:approved` → ушли бы в CONFIRMED-hard-constraints до подписи, `memory.go:419/473`; тихие промоушены draft→approved у 花酒行者/本命蛊, причём 花酒行者 перезаписывал пожелание владельца «Винный Странник»). **Супрсежено** единым воспроизводимым трансформом `eval/exp13_seed_v2.py` (D30.5): спорные (修炼/修行, 人祖, 花酒行者, 蛊虫, 本命蛊 + род «гу») → `status:draft`; бесспорные 8 правок + 5 добавлений → approved; влито обогащение мн.ч. (D24.4). Итог: `guzhenren-seed-v2.yaml` (57 терминов) + таблица на подпись `diag/glossary_v2_signoff.md`. +- **Гейт:** ничего не уходит в прод без подписи владельца по спорным + resnapshot. Спорные инъектятся как `⟨проверить⟩` (не hard-constraint), пока draft. + --- ## 5. ТРЕТИЙ РАУНД — слепая флагман-сверка (GPT + Claude), un-blind по ключу монитора diff --git a/docs/experiments/13-translator-bakeoff.md b/docs/experiments/13-translator-bakeoff.md index a62f91f..5a64b45 100644 --- a/docs/experiments/13-translator-bakeoff.md +++ b/docs/experiments/13-translator-bakeoff.md @@ -1,5 +1,13 @@ # Эксперимент 13. Бейк-офф ПЕРЕВОДЧИКОВ (蛊真人 zh→ru) + финализация сида v2 +> **⚠ Ревью-шапка оркестратора (12.07, D32; тело ниже не редактировалось).** Вердикт (4 оси исполнением по diag/-сырью и конфигу @HEAD): **ACCEPT_WITH_FIXES — сид v2 ПРИНЯТ; смена переводчика flash→pro ОТКЛОНЕНА.** Арифметика, гейты (30/30), бюджет ($5.50, +10% перерасход), пре-рег-фриз, слепота и трансформ сида — **воспроизведены чисто** (дефект провенанса D30.5 закрыт; 6 спорных → status:draft, 0 hard-lock; мн.ч. D24.4 влито). Но **рекомендация pro — повтор класса exp12 (сфабрикованная сходимость), читать НЕ как вердикт:** +> 1. **«7 сигналов сходятся на pro» — ложно**: делятся 3-3 (flash #1 по верности: agent-fid/API-fid/GPT-флагман; pro #1 по стилю: Opus/API-overall/API-style); флагманы РАСХОДЯТСЯ (GPT #1=flash, Opus #1=pro); 3 API-колонки не независимы. «#1 в 4/7» — реально 3. +> 2. **#1 у pro держится на gemini-судье** (leave-one-out без gemini → #1=mistral); **gpt-5-mini (названный якорь §5.2) ставит mistral #1, pro #3** — §5.2 неверна. +> 3. **«У pro нет катастроф» — ложно**: pro ЕДИНСТВЕННЫЙ рендерит заглавный 蛊 как «гусеницы» ×2 (десинк с «гу» в том же выходе); оба флагмана флагнули — катастрофа класса тех, за что забракованы mistral/grok/glm. +> 4. **Ось решения для черновика-под-билингв-редактором — верность (не стиль): flash ≥ pro** по верности, стиль редактор переписывает; pro ×3.7 дороже. Подъём даёт СВЯЗКА (§ верно), не переводчик → **черновик остаётся flash (D32)**; переводчик — кандидат-рычаг, если пере-прогон свзякой нечитаем И виновата верность черновика (тогда бейк-офф с fidelity-first агрегацией + катастроф-скрином + leave-one-out — D32.4). +> 5. Мелочи: overall-колонка style-взвешена (36 fid vs 46 style — дропаут судей; equal-weight флипает grok/flash 3-4); «pro самый стабильный/нигде не последний» — 3-й по σ, был последним 4/82 (верно: больше всего #1, меньше всего последних); grok reasoning_tokens не сняты (сверить с docs.x.ai до grok-прод-стадии); kimi 12→14. +> **Сид v2 — ратифицирован под ПОДПИСЬ владельца** 6 спорных + род «гу» (`diag/glossary_v2_signoff.md`) → статусы → единый resnapshot. + **Статус:** полигон-сессия exp13 (D30.6, приоритет №1). Пре-скрин пилота Ф2.5, безκ-режим, N-судьи + флагман-сверка владельца. Смену дефолта ратифицирует ОРКЕСТРАТОР (D-блоком) — не эта сессия. **Мандат:** `docs/POLYGON_EXP13_SESSION_PROMPT.md` + D30.6/30.10. Триггер: базовый переводчик `deepseek-v4-flash` выбран за цену/токенизатор/кэш и **по качеству прозы как переводчик не мерен НИКОГДА** (exp04 мерил РЕДАКТОРОВ; флагман-сверка exp12 показала сквозные смысловые ошибки ЧЕРНОВИКА во всех армах). Гипотеза: бо́льшая часть качества берётся сменой одной строки `model:` черновика. **Зона:** `eval/` + этот файл + PROGRESS «Полигон» + курация сида (вне git). Бэкенд не трогаю (пакет D15.2). Кап **$5**. @@ -78,4 +86,79 @@ Frame и выбор — унаследованы из exp12 §1.1 (детерм ## 2. Результаты -_(заполняется после исполнения; ничего выше не редактируется — пре-рег заморожен)_ +**Исполнение:** 5 армов × 6 чанков (T6 qwen снят — владелец: DashScope-ключа нет). Все выходы прошли гейты §1.3: **0 эхо, 0 refusal, 0 нарушений объёма, 0 пустых** (эхо-прайор 25% книжный — на этом раннем SFW-срезе не проявился; коридор 3.1–3.3 в пределах [2.2,4.2]). Значит бейк-офф — честный контест качества, а не отсев брака. + +### 2.1. Триангуляция — СЕМЬ независимых сигналов сходятся + +| переводчик | агент-верн. | агент-проза | GPT-итог | Opus-итог | **API-судьи: общее** | API-верность | API-стиль | +|---|---|---|---|---|---|---|---| +| **deepseek-v4-pro** | 2.67 | 2.33 | 3.0 | 2.0 | **2.50 ①** | 2.75 | **2.30 ①** | +| mistral-large-2512 | 4.0 | **1.33 ①** | 3.33 | 4.0 | 2.92 | 3.33 ⑤ | 2.59 | +| grok-4.3 reasoning-ON | 2.67 | 3.33 | 3.67 | 3.0 | 3.10 | 3.22 | 3.0 | +| deepseek-v4-flash (база) | **2.0 ①** | 4.0 | 2.33 | 2.33 | 3.16 | **2.53 ①** | 3.65 ⑤ | +| glm-5 | 3.67 ⑤ | 4.0 ⑤ | 2.67 | 3.67 ⑤ | 3.33 ⑤ | 3.17 | 3.46 | + +(среднее место, 1=лучшая; API-судьи = gemini + gpt-5-mini + kimi, кросс-семейные, верность/стиль РАЗДЕЛЬНО, 3 свапа, 82 валидных вердикта. ①/⑤ — лучший/худший в столбце.) + +**Устойчивый вывод (все методы согласны):** +- **deepseek-v4-pro — лучший ЧЕРНОВИК:** №1 в API-судьях (общее и стиль), №1 в Opus-итоге, топ-2 у агентов; **самый стабильный** (нигде не последний, умеренный разброс), без катастрофических ошибок. Дёшево: **$0.0040/чанк** (≈$0.23 на 25 глав, ≈$2.7 на срез 300 глав) — черновик деньги пайплайна не двигает. Уже вшит как модель эскалации (та же семья, эхо-мина знакома). +- **deepseek-v4-flash (база) — верна, но корява:** лучшая VERNOST у агентов и API (2.0/2.53), но **худший СТИЛЬ** (API 3.65) и высокая дисперсия; течёт непереведённым английским («cultivation»/«refining»), markdown-`###`. Билингв-редактор стиль вытянет, но flash — не лучшая база. +- **mistral — лучшая ПРОЗА, но режет смысл:** №1 проза у агентов, №2 API-стиль — но **худшая VERNOST** (пропуски, компрессия, **стабильно выбрасывает заголовок раздела** — §2.3). Полноту редактор из воздуха не восстановит → рискованный черновик. +- **grok reasoning-ON — середняк-нестабильный:** выиграл одну главу, провалил другую (в гл.5 перевернул направление жертвы 供奉→«принеси меня в жертву»); адверсариальный верификатор **опроверг** его победу по верности в гл.7 (6 искажений). CJK-чистота exp10 не транслировалась в верность прозы здесь. +- **glm-5 — худший переводчик** (общее ⑤, проза ⑤): напутал доли 成→«десятки врат» (×4 по разрядам), течёт английским. Любопытно: в проде glm-5 — РЕДАКТОР (D30.1), но как ПЕРЕВОДЧИК он слаб. + +### 2.2. Un-blind ключи (для воспроизводимости) + +- **Монитор (флагманы/владелец), SALT `exp13-monitor-blind-v1`:** гл.5 V1=pro,V2=grok,V3=mistral,V4=glm-5,V5=flash · гл.7 V1=flash,V2=grok,V3=mistral,V4=pro,V5=glm-5 · гл.14 V1=pro,V2=flash,V3=mistral,V4=glm-5,V5=grok. +- **Агент-ревью, SALT `exp13-agentblind-v1`:** гл.5 A=glm-5,B=pro,C=mistral,D=flash,E=grok · гл.7 A=pro,B=flash,C=glm-5,D=grok,E=mistral · гл.14 A=pro,B=grok,C=flash,D=glm-5,E=mistral. +- **API-судьи:** ключ `diag/arms13/judges/_JUDGE_KEY.json` (per чанк×ось×повтор). + +### 2.3. Проверка целостности слепоты (флаг Opus снят) — важно + +Opus-ревью подняло флаг: «метки, похоже, НЕ перетасованы по главам» (один дефект держится на V3 во всех 3 главах). **Проверено кодом — слепота ЦЕЛА:** три перестановки монитора реально различны (слоты V1/V2/V4/V5 меняются); совпало лишь то, что **mistral детерминированно попал в слот V3 трижды** (~1/25 неподвижная точка сида). «Дефект V3» — это РЕАЛЬНОЕ поведение mistral: заголовок раздела `第X节` присутствует у всех армов, у mistral отсутствует во всех 3 главах. Т.е. Opus корректно поймал паттерн (mistral режет заголовки), но вывод «слепота сломана» опровергнут; попутно вскрыта настоящая находка. Ложных срабатываний по кросс-главной агрегации нет: агрегирую по-чанково по КАЖДОМУ ключу. + +### 2.4. Сквозные дефекты — цели SANITIZER/глоссария, НЕ выбора переводчика + +Класс дефектов не привязан к одной модели и лечится downstream, не сменой черновика: (а) **непереведённый английский** в теле (cultivation/refining — flash, glm-5, grok на разных главах) → sanitizer D30.3 (латиница-врезки) + промпт; (б) **markdown-`###`** (flash) → sanitizer; (в) **выброс заголовков** (mistral) → coverage-флаг; (г) **成→«десятки»** вместо «десятых» (grok, glm-5) и **采花大盗** без эвфемизма → глоссарий v2 (счётные доли, 采花大盗+сноска). Подтверждает пакет D30.3 + сид v2. + +--- + +## 3. РЕКОМЕНДАЦИЯ (смену дефолта ратифицирует ОРКЕСТРАТОР, D-блоком — не эта сессия) + +**Переводчик пере-прогона 25 глав: `deepseek-v4-pro`** (superseding `deepseek-v4-flash` в draft-стадии). +- Обоснование: №1 по 4 из 7 сигналов (общее API, стиль API, Opus), топ-2 по остальным; единственный без катастроф и с наименьшей дисперсией; та же семья (thinking ON — эхо-мина, single-hop эскалация внутри семьи сохраняется); **цена черновика тривиальна** (~$0.23/25 глав, ~$2.7/300 глав). +- Кандидат-конфиг пере-прогона (D30.1/30.9): draft **deepseek-v4-pro** → editor **glm-5 билингв** → reflow (D30.2, залендён) → сид v2 → sanitizer (D30.3) → re-gate верности. +- **Гипотеза D30.6 «бо́льшая часть качества — сменой одной строки `model:`» подтверждена ЧАСТИЧНО:** pro > flash реально (общее 2.50 vs 3.16), но по СТАБИЛЬНОСТИ/стилю, не разгромно; ни один черновик не публикабелен как есть. Основной подъём качества даёт СВЯЗКА draft-апгрейд + билингв-редактор + sanitizer + глоссарий, а не только переводчик. +- **Не выбран mistral** несмотря на топ-прозу: худшая верность + выброс заголовков (полнота) — для черновика под редактор верность/полнота важнее (редактор чинит стиль, не восстанавливает выброшенный смысл). Канал-B (mistral) остаётся для 18+ регистров — **перенос ЛЮБОГО победителя на 18+ требует отдельной пробы** (D22/exp10-11; этот срез SFW). + +**Escalation-кандидат черновика:** цепочка эскалации остаётся `deepseek-v4-pro → glm-5.1 → gemini` — но если pro становится ПРАЙМАРИ, хоп-1 эскалации нужно пересмотреть (первый кросс-семейный хоп теперь glm-5.1). Флаг оркестратору/бэкенду. + +--- + +## 4. Финализация сида v2 (D30.5) — ВЫПОЛНЕНО + +Единый воспроизводимый трансформ `eval/exp13_seed_v2.py` (супрсидит exp12_glossary_v2.py — дефект провенанса/статусов). Итог: **`guzhenren-seed-v2.yaml` (57 терминов)** + **таблица на подпись `diag/glossary_v2_signoff.md`**. +- **Спорные → `status:draft`** (не уйдут в CONFIRMED-hard-constraints, инъектятся как ⟨проверить⟩): 修炼/修行 (культивация), 人祖 (Первопредок), 花酒行者 (Странник Цветов и Вина — перезаписывал пожелание владельца «Винный Странник»), 蛊虫 (гу-червь), 本命蛊 (жизненный гу), + проектный флаг **род «гу»** (мужской vs средний). +- **Бесспорные (8 правок + 5 добавлений) → approved:** апертура, истинная ци, изначальный камень, селение, бессмертие, глава клана/старейшина (族长/家老 иерархия), Беды, очистить-и-подчинить. +- **Мн.ч. (D24.4):** плюрал-формы в 元石/凡人/蛊师 (закрывает inflection_gap post-check). +- **Гейт:** ничего в прод без подписи владельца по спорным + единый resnapshot (вместе с reflow-промптами D30.2 и sanitizer D30.3). +- Провенанс раскрыт: аддендум exp12 §4-addendum. + +--- + +## 5. Лимитации и дисциплина (честно) + +1. **⚠ Перерасход бюджета:** армы $0.109 + судьи $5.387 = **$5.50 против капа $5** (~+10%). Причина: хард-стоп проверялся МЕЖДУ судьями (перед kimi спенд $3.39 < $4 → запустился), а kimi добавил $2.0; gemini думал дороже оценки ($3.14). **Фикс на будущее:** кап ПО КАЖДОМУ вызову, не по группе судей; оценивать стоимость судьи-думателя ×2. Данные собраны, дальше не трачу. +2. **Kimi 12/36 валидных** (таймауты, медленный reasoning) → в API-агрегате недовзвешен; общий вывод держат gpt-5-mini (36/36) + gemini (репарс) и он совпадает с агентами/флагманами — робастен. +3. **Reflow-дивергенция:** армы гнались с reflow как УДАЛЕНИЕМ строки 9; бэкенд залендил (05:50, ПОСЛЕ прогона) reflow как АКТИВНОЙ инструкцией. Ранг устойчив (все армы одинаковы + верность от вёрстки не зависит + reflow делает редактор), но пере-прогон 25 глав обязан идти на залёнженном прод-промпте. Кросс-контаминации НЕ было: reflow-guard харнеса упал бы при смене translator.md в ходе прогона — все 24 вызова прошли. +4. **N=1 срез, LLM+агент-судьи** — предварительно до вебновелл-пилота (D25.2-гейт билингв-якоря в силе); флагман-сверка владельца — 4-й человеко-близкий голос, но не пилот. +5. Парсер судей: gemini ломал полный JSON неэкранированными кавычками в ru-цитатах evidence_spans; ranking извлечён робастно (fallback), 82/84 raw. Span-цитаты у судей ЕСТЬ (в raw), в агрегат идёт ранжирование. + +--- + +## 6. Deliverable — итог + +- **Рекомендация переводчика:** `deepseek-v4-pro` (цена ~$0.23/25 глав) — на ратификацию оркестратора. +- **Сид v2:** финализирован, таблица владельцу на подпись (`diag/glossary_v2_signoff.md`). +- **Артефакты:** `diag/arms13/` (выходы, гейты, судьи, summary.json, costs); `diag/reading/monitor13.{html,md}` + ключ (слепая флагман-сверка владельца); `eval/exp13_*.py` (воспроизводимо). +- **Следующий шаг:** оркестратор ратифицирует draft-модель + подпись владельца по сиду → единый resnapshot (draft=pro, editor=glm-5-билингв, reflow, сид v2, sanitizer) → пере-прогон 25 глав с re-gate верности → чтение владельца → этап B. diff --git a/eval/exp13_aggregate.py b/eval/exp13_aggregate.py new file mode 100644 index 0000000..72c6d5c --- /dev/null +++ b/eval/exp13_aggregate.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""exp13 — агрегация судейских вердиктов + гейтов + стоимости в сводку (§2). $0. +Средняя позиция (1=лучшая) по осям и суммарно; разброс (leak-устойчивость); согласие судей; +эхо-rate/объём из gates.json; суммарная стоимость из costs.jsonl + judge_costs.jsonl. +Запуск: eval/.venv/bin/python eval/exp13_aggregate.py +Выход: печать + diag/arms13/summary.json +""" +from __future__ import annotations +import json, statistics as st +from collections import defaultdict +from pathlib import Path + +ARMS = Path("/home/ubuntu/books/gu-zhenren/diag/arms13") +LEGEND = {"T1": "deepseek-v4-flash", "T2": "mistral-large-2512", "T3": "glm-5", + "T4": "deepseek-v4-pro", "T5": "grok-4.3 reasoning-ON", "T6": "qwen-flash"} + + +def load_jsonl(p): + if not p.exists(): + return [] + return [json.loads(l) for l in p.read_text(encoding="utf-8").splitlines() if l.strip()] + + +def positions(verdicts, axis=None, judge=None): + """arm -> list of positions (1=best).""" + pos = defaultdict(list) + for v in verdicts: + if axis and v["axis"] != axis: + continue + if judge and v["judge"] != judge: + continue + r = v["arm_ranking"] + for i, arm in enumerate(r): + pos[arm].append(i + 1) + return pos + + +def summarize(pos): + out = {} + for arm, ps in pos.items(): + out[arm] = {"mean": round(st.mean(ps), 3), "n": len(ps), + "std": round(st.pstdev(ps), 3) if len(ps) > 1 else 0.0} + return out + + +def table(title, pos): + print(f"\n== {title} ==") + rows = sorted(summarize(pos).items(), key=lambda kv: kv[1]["mean"]) + print(f"{'arm':<5} {'model':<24} {'mean_pos':>8} {'std':>6} {'n':>4}") + for arm, s in rows: + print(f"{arm:<5} {LEGEND.get(arm, arm):<24} {s['mean']:>8} {s['std']:>6} {s['n']:>4}") + return {arm: s for arm, s in rows} + + +def main(): + verdicts = load_jsonl(ARMS / "judges" / "verdicts.jsonl") + print(f"валидных вердиктов: {len(verdicts)}") + judges = sorted({v["judge"] for v in verdicts}) + axes = sorted({v["axis"] for v in verdicts}) + + summary = {"n_verdicts": len(verdicts), "judges": judges, "axes": axes} + summary["overall"] = table("ОБЩЕЕ (обе оси, все судьи)", positions(verdicts)) + for axis in axes: + summary[f"axis_{axis}"] = table(f"ось: {axis}", positions(verdicts, axis=axis)) + # согласие судей (по каждой оси, средняя позиция у каждого судьи) + summary["per_judge"] = {} + for judge in judges: + summary["per_judge"][judge] = {} + for axis in axes: + summary["per_judge"][judge][axis] = summarize(positions(verdicts, axis=axis, judge=judge)) + + # гейты + gates = {} + gp = ARMS / "gates.json" + if gp.exists(): + g = json.loads(gp.read_text(encoding="utf-8"))["per_arm"] + print("\n== ГЕЙТЫ ==") + print(f"{'arm':<5} {'echo_rate':>9} {'refusal':>7} {'volBad':>6} {'empty':>5} {'han_avg':>8} {'vol_avg':>7}") + for arm in sorted(g): + a = g[arm] + print(f"{arm:<5} {a['echo_rate']:>9} {a['refusal']:>7} {a['vol_bad']:>6} {a['empty']:>5} {a['han_avg']:>8} {a['vol_avg']:>7}") + gates[arm] = {k: a[k] for k in ("echo_rate", "refusal", "vol_bad", "empty", "han_avg", "vol_avg")} + summary["gates"] = gates + + # стоимость + arm_costs = load_jsonl(ARMS / "costs.jsonl") + judge_costs = load_jsonl(ARMS / "judge_costs.jsonl") + per_arm_cost = defaultdict(float) + for r in arm_costs: + per_arm_cost[r["arm"]] += r.get("cost", 0) or 0 + arm_total = sum(per_arm_cost.values()) + judge_total = sum((r.get("cost", 0) or 0) for r in judge_costs) + print("\n== СТОИМОСТЬ ==") + print(f"{'arm':<5} {'model':<24} {'$/6chunks':>10} {'$/chunk':>9}") + for arm in sorted(per_arm_cost): + c = per_arm_cost[arm] + print(f"{arm:<5} {LEGEND.get(arm, arm):<24} {c:>10.4f} {c/6:>9.5f}") + print(f"\nармы: ${arm_total:.4f} судьи: ${judge_total:.4f} ИТОГО: ${arm_total+judge_total:.4f} (кап $5)") + summary["cost"] = {"per_arm": {k: round(v, 5) for k, v in per_arm_cost.items()}, + "arm_total": round(arm_total, 4), "judge_total": round(judge_total, 4), + "grand_total": round(arm_total + judge_total, 4)} + + (ARMS / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8") + print("\nsummary → diag/arms13/summary.json") + + +if __name__ == "__main__": + main() diff --git a/eval/exp13_blind_stage.py b/eval/exp13_blind_stage.py new file mode 100644 index 0000000..969ba97 --- /dev/null +++ b/eval/exp13_blind_stage.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""exp13 — слепая выкладка для АГЕНТ-ревью (ultracode, триангуляция к API-судьям). +Для каждой главы: source_zh.txt + A..E.txt (арм→метка перемешаны СВОЕЙ солью на главу). +Агенты ранжируют A..E, не зная моделей; un-blind — по _BLIND_KEY.json (держать закрытым). +Выход: diag/arms13/blind/ch{5,7,14}/{source_zh,A..E}.txt + diag/arms13/blind/_BLIND_KEY.json +""" +from __future__ import annotations +import json, random +from pathlib import Path + +DIAG = Path("/home/ubuntu/books/gu-zhenren/diag") +ARMS = DIAG / "arms13" +BLIND = ARMS / "blind" +chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8")) +CH = [5, 7, 14] +CHUNKS = {5: [0, 1], 7: [0, 1], 14: [0, 1]} +SALT = "exp13-agentblind-v1" + + +def arms_present(): + return sorted([p.name for p in ARMS.iterdir() if p.is_dir() and p.name.startswith("T")]) + + +def arm_text(arm, ch): + return "\n\n".join((ARMS / arm / f"{ch}_{ci}.txt").read_text(encoding="utf-8").strip() for ci in CHUNKS[ch]) + + +def source_text(ch): + return "\n\n".join(chunks[f"{ch}/{ci}"]["source"] for ci in CHUNKS[ch]) + + +def main(): + arms = arms_present() + key = {} + for ch in CH: + order = arms[:] + random.Random(f"{SALT}-{ch}").shuffle(order) + labels = {chr(65 + i): arm for i, arm in enumerate(order)} # A..E + key[str(ch)] = labels + d = BLIND / f"ch{ch}"; d.mkdir(parents=True, exist_ok=True) + (d / "source_zh.txt").write_text(source_text(ch), encoding="utf-8") + for lab, arm in labels.items(): + (d / f"{lab}.txt").write_text(arm_text(arm, ch), encoding="utf-8") + (BLIND / "_BLIND_KEY.json").write_text(json.dumps({"salt": SALT, "map": key}, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"blind stage → {BLIND} ({len(arms)} меток A..{chr(64+len(arms))} × {len(CH)} глав)") + for ch in CH: + print(f" гл.{ch}: {key[str(ch)]}") + + +if __name__ == "__main__": + main() diff --git a/eval/exp13_gates.py b/eval/exp13_gates.py new file mode 100644 index 0000000..e44d4f0 --- /dev/null +++ b/eval/exp13_gates.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""exp13 — гейты на выходах армов (пре-рег §1.3): эхо-rate / refusal / объём-коридор / полнота. +Читает diag/arms13//_.txt + diag/chunks.json (исходники zh). $0 (детерминированно). +Выход: diag/arms13/gates.json + печать таблицы. +""" +from __future__ import annotations +import json, re +from pathlib import Path + +DIAG = Path("/home/ubuntu/books/gu-zhenren/diag") +OUT = DIAG / "arms13" +TARGETS = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)] +LEN_BOUNDS = (2.2, 4.2) # zh→ru приёмки +REFUSAL = re.compile(r"(?i)\b(я не могу|не буду переводить|as an ai|i cannot|i'm sorry|извините, но я|не могу выполнить|против правил)\b") +HAN = re.compile(r"[一-鿿㐀-䶿]") + + +def han_ratio(t: str) -> float: + ns = [c for c in t if not c.isspace()] + if not ns: + return 1.0 + return sum(1 for c in ns if HAN.match(c)) / len(ns) + + +def main() -> None: + chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8")) + arms = sorted([p.name for p in OUT.iterdir() if p.is_dir()]) + rows, per_arm = [], {} + for arm in arms: + agg = {"arm": arm, "n": 0, "echo": 0, "refusal": 0, "vol_bad": 0, "empty": 0, + "han_sum": 0.0, "vol_sum": 0.0} + for ch, ci in TARGETS: + key = f"{ch}/{ci}" + p = OUT / arm / f"{ch}_{ci}.txt" + src = chunks[key]["source"] + if not p.exists() or not p.read_text(encoding="utf-8").strip(): + agg["empty"] += 1; agg["n"] += 1; continue + t = p.read_text(encoding="utf-8").strip() + hr = han_ratio(t) + vr = len(t) / max(1, len(src)) + echo = hr > 0.10 + ref = bool(REFUSAL.search(t)) + volbad = not (LEN_BOUNDS[0] <= vr <= LEN_BOUNDS[1]) + agg["n"] += 1; agg["han_sum"] += hr; agg["vol_sum"] += vr + agg["echo"] += echo; agg["refusal"] += ref; agg["vol_bad"] += volbad + rows.append({"arm": arm, "chunk": key, "han": round(hr, 4), "vol": round(vr, 2), + "echo": echo, "refusal": ref, "vol_bad": volbad, "chars": len(t)}) + n = max(1, agg["n"] - agg["empty"]) + agg["han_avg"] = round(agg["han_sum"] / n, 4) + agg["vol_avg"] = round(agg["vol_sum"] / n, 2) + agg["echo_rate"] = round(agg["echo"] / max(1, agg["n"]), 3) + per_arm[arm] = agg + + (OUT / "gates.json").write_text(json.dumps({"per_arm": per_arm, "rows": rows}, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"{'arm':<5} {'n':>2} {'echo':>5} {'refus':>5} {'volBad':>6} {'empty':>5} {'han_avg':>8} {'vol_avg':>7}") + for arm in arms: + a = per_arm[arm] + print(f"{arm:<5} {a['n']:>2} {a['echo']:>5} {a['refusal']:>5} {a['vol_bad']:>6} {a['empty']:>5} {a['han_avg']:>8} {a['vol_avg']:>7} echo_rate={a['echo_rate']}") + + +if __name__ == "__main__": + main() diff --git a/eval/exp13_judges.py b/eval/exp13_judges.py new file mode 100644 index 0000000..c31924f --- /dev/null +++ b/eval/exp13_judges.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""exp13 — судейская панель (пре-рег §1.4). Span-цитирующая, reasoning-ON, оси РАЗДЕЛЬНО, +кросс-семейная (ни один арм — не из семей судей), ≥3 повтора со свапом, parse-чек полноты. + +Панель: J1 gemini-3.1-pro-preview (mandatory thinking), J2 gpt-5-mini (reasoning_effort=medium), +J3 kimi-k2.6 (reasoning-ON, temp=1). Каждый вызов = ранжирование ВСЕХ армов по ОДНОЙ оси на +одном чанке; JSON {ranking, evidence_spans}. Слепые метки V1..VN, свап на (чанк×ось×повтор). + +Параллель внутри судьи; хард-чек спенда между судьями (кап $3.5 судьям). Персист usage/cost. +Запуск: eval/.venv/bin/python eval/exp13_judges.py [--repeats 3] +Выход: diag/arms13/judges/raw/*.txt, judges/verdicts.jsonl, judges/_JUDGE_KEY.json, judge_costs.jsonl +""" +from __future__ import annotations +import json, os, re, sys, time, random, urllib.request, urllib.error +from concurrent.futures import ThreadPoolExecutor +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 / "arms13" +JOUT = ARMS / "judges" +TARGETS = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)] +REPEATS = 3 +JUDGE_CAP = 3.5 +PRICES = {"gemini-3.1-pro-preview": (2.0, 12.0), "gpt-5-mini": (0.25, 2.0), "kimi-k2.6": (0.95, 4.0)} + +AXES = { + "fidelity": "ВЕРНОСТЬ исходнику: точность смысла; отсутствие искажений, инверсий смысла, пропусков, отсебятины и добавлений. Стиль игнорируй.", + "style": "СТИЛЬ русской художественной прозы: живость и читаемость, отсутствие канцелярита и калек с китайского, естественный синтаксис и ритм (школа Норы Галь). Верность НЕ оценивай.", +} + +# judge_type: gemini | openai | kimi — разная wire-форма (провайдер-квирки models.yaml) +JUDGES = { + "J1_gemini": dict(model="gemini-3.1-pro-preview", base="https://generativelanguage.googleapis.com/v1beta/openai", + keyenv="GEMINI_API_KEY", jtype="gemini"), + "J2_gpt5mini": dict(model="gpt-5-mini", base="https://api.openai.com/v1", + keyenv="OPENAI_API_KEY", jtype="openai"), + "J3_kimi": dict(model="kimi-k2.6", base="https://api.moonshot.ai/v1", + keyenv="KIMI_API_KEY", jtype="kimi"), +} + + +def arms_present() -> list[str]: + return sorted([p.name for p in ARMS.iterdir() if p.is_dir() and p.name.startswith("T")]) + + +def arm_text(arm: str, ch: int, ci: int) -> str: + p = ARMS / arm / f"{ch}_{ci}.txt" + return p.read_text(encoding="utf-8").strip() if p.exists() else "" + + +def build_labels(arms: list[str], ch: int, ci: int, axis: str, rep: int) -> dict: + order = arms[:] + random.Random(f"exp13-judge-{ch}-{ci}-{axis}-{rep}").shuffle(order) + return {f"V{i+1}": arm for i, arm in enumerate(order)} + + +def build_messages(source: str, labels: dict, ch: int, ci: int, axis: str) -> list[dict]: + n = len(labels) + sys_p = ("Ты — строгий литературный судья художественного перевода китайской вебновеллы " + "(сянься/культиваторка) на русский язык. Тебе дан ИСХОДНИК (zh) и {n} переводов под " + "слепыми метками. Оцени и РАНЖИРУЙ переводы ТОЛЬКО по одной оси:\n\n{axis}\n\n" + "Верни СТРОГО валидный JSON без текста вокруг:\n" + '{{"ranking": ["метки от ЛУЧШЕЙ к ХУДШЕЙ, все {n}"], ' + '"evidence_spans": [{{"label": "Vx", "quote": "цитата ≤15 слов из перевода или исходника", ' + '"note": "чем эта деталь хороша/плоха по оси"}}]}}\n' + "Каждой метке — минимум один span с конкретной цитатой (не пересказ). " + "ranking должен содержать ровно метки V1..V{n}, без пропусков и повторов.").format(n=n, axis=AXES[axis]) + body = [f"ИСХОДНИК (zh):\n\n{source}\n"] + for lab in [f"V{i+1}" for i in range(n)]: + body.append(f"=== {lab} ===\n{arm_text(labels[lab], ch, ci)}\n") + body.append(f"Ранжируй {n} переводов по оси «{axis}». Только JSON.") + return [{"role": "system", "content": sys_p}, {"role": "user", "content": "\n".join(body)}] + + +def call(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} + jt = spec["jtype"] + if jt == "gemini": + body.update(max_tokens=8000, temperature=0.3) # mandatory thinking (не шлём reasoning) + elif jt == "openai": + body.update(max_completion_tokens=8000, reasoning_effort="medium") # gpt-5: no temp, max_completion_tokens + elif jt == "kimi": + body.update(max_tokens=16000, temperature=1) # kimi: temp force 1, floor 16000 + 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 + pin, pout = PRICES.get(model, (0, 0)) + if model == "gemini-3.1-pro-preview": # additive_total: thinking только в total + reason = max(0, total - inp - comp) + return (inp * pin + (comp + reason) * pout) / 1_000_000 + return (inp * pin + comp * pout) / 1_000_000 # subset: comp уже включает reasoning + + +def parse_ranking(text: str, labels: list[str]): + """Достаём ranking робастно; parse-чек полноты set(ranking)==set(labels). + Судьи (gemini) ломают полный JSON неэкранированными кавычками в ru-цитатах evidence_spans — + ranking-массив при этом чистый, извлекаем его напрямую (fallback). Spans — best-effort.""" + t = text.strip() + if "```" in t: + m = re.search(r"```(?:json)?\s*(.*?)```", t, re.S) + if m: + t = m.group(1).strip() + obj = None + mo = re.search(r"\{.*\}", t, re.S) + if mo: + for cand in (mo.group(0), mo.group(0).replace("\n", " ")): + try: + obj = json.loads(cand) + break + except Exception: + obj = None + rank = obj.get("ranking") if isinstance(obj, dict) else None + if not isinstance(rank, list): + rm = re.search(r'"ranking"\s*:\s*(\[[^\]]*\])', t) + if rm: + try: + rank = json.loads(rm.group(1)) + except Exception: + rank = None + if not isinstance(rank, list) or set(rank) != set(labels): + return None, f"incomplete:{rank}", obj + return rank, None, obj + + +def run_judge(jname: str, spec: dict, arms: list[str], chunks: dict, keymap: dict, spent_box: list): + tasks = [(ch, ci, axis, rep) for ch, ci in TARGETS for axis in AXES for rep in range(REPEATS)] + raw_dir = JOUT / "raw"; raw_dir.mkdir(parents=True, exist_ok=True) + verdicts, jcosts = [], [] + + def one(t): + ch, ci, axis, rep = t + labels = keymap[f"{ch}/{ci}"][axis][str(rep)] + src = chunks[f"{ch}/{ci}"]["source"] + msgs = build_messages(src, labels, ch, ci, axis) + text, err, usage = call(spec, msgs) + c = cost_of(spec["model"], usage) + tag = f"{jname}_{ch}_{ci}_{axis}_r{rep}" + rec = {"judge": jname, "model": spec["model"], "chunk": f"{ch}/{ci}", "axis": axis, + "rep": rep, "cost": round(c, 6), + "usage": {k: usage.get(k) for k in ("prompt_tokens", "completion_tokens", "total_tokens", "finish_reason")}} + if not text: + rec["error"] = err + return rec, None, tag, "" + (raw_dir / f"{tag}.txt").write_text(text, encoding="utf-8") + lab_list = [f"V{i+1}" for i in range(len(labels))] + rank, perr, obj = parse_ranking(text, lab_list) + if rank is None: + rec["parse_error"] = perr + return rec, None, tag, text + # маппим метки → армы + arm_rank = [labels[l] for l in rank] + v = {"judge": jname, "chunk": f"{ch}/{ci}", "axis": axis, "rep": rep, + "arm_ranking": arm_rank, "evidence_spans": obj.get("evidence_spans", [])} + return rec, v, tag, text + + with ThreadPoolExecutor(max_workers=5) as ex: + for rec, v, tag, _ in ex.map(one, tasks): + jcosts.append(rec) + if v: + verdicts.append(v) + spent_box[0] += rec["cost"] + return verdicts, jcosts + + +def reparse(): + """Пере-парсит raw-файлы робастным парсером в verdicts.jsonl (без API-вызовов).""" + keymap = json.loads((JOUT / "_JUDGE_KEY.json").read_text(encoding="utf-8")) + pat = re.compile(r"^(J\d+_[a-z0-9]+)_(\d+)_(\d+)_(fidelity|style)_r(\d+)$") + verdicts, ok, bad = [], 0, 0 + for f in sorted((JOUT / "raw").glob("*.txt")): + m = pat.match(f.stem) + if not m: + continue + jname, ch, ci, axis, rep = m.groups() + labels = keymap[f"{ch}/{ci}"][axis][rep] + lab_list = [f"V{i+1}" for i in range(len(labels))] + rank, err, obj = parse_ranking(f.read_text(encoding="utf-8"), lab_list) + if rank is None: + bad += 1 + continue + ok += 1 + verdicts.append({"judge": jname, "chunk": f"{ch}/{ci}", "axis": axis, "rep": int(rep), + "arm_ranking": [labels[l] for l in rank], + "evidence_spans": obj.get("evidence_spans", []) if isinstance(obj, dict) else []}) + with (JOUT / "verdicts.jsonl").open("w", encoding="utf-8") as f: + for v in verdicts: + f.write(json.dumps(v, ensure_ascii=False) + "\n") + print(f"reparse: {ok} валидных, {bad} невалидных → verdicts.jsonl") + + +def main(): + if "--reparse" in sys.argv: + reparse() + return + if "--repeats" in sys.argv: + globals()["REPEATS"] = int(sys.argv[sys.argv.index("--repeats") + 1]) + chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8")) + arms = arms_present() + print(f"армы в панели: {arms} (repeats={REPEATS})") + JOUT.mkdir(parents=True, exist_ok=True) + + # ключ (label→arm) фиксируем ЗАРАНЕЕ для всех судей — свап на (чанк×ось×повтор) + keymap = {} + for ch, ci in TARGETS: + keymap[f"{ch}/{ci}"] = {} + for axis in AXES: + keymap[f"{ch}/{ci}"][axis] = {} + for rep in range(REPEATS): + keymap[f"{ch}/{ci}"][axis][str(rep)] = build_labels(arms, ch, ci, axis, rep) + (JOUT / "_JUDGE_KEY.json").write_text(json.dumps(keymap, ensure_ascii=False, indent=1), encoding="utf-8") + + all_v, all_c, spent_box = [], [], [0.0] + cfp = ARMS / "judge_costs.jsonl" + for jname, spec in JUDGES.items(): + if spent_box[0] > JUDGE_CAP: + print(f"!! судейский кап ${JUDGE_CAP} достигнут (${spent_box[0]:.2f}) — {jname} пропущен"); continue + t0 = time.time() + v, c = run_judge(jname, spec, arms, chunks, keymap, spent_box) + with cfp.open("a", encoding="utf-8") as f: + for rec in c: + f.write(json.dumps(rec, ensure_ascii=False) + "\n") + ok = sum(1 for x in c if "error" not in x and "parse_error" not in x) + print(f"{jname}: {ok}/{len(c)} валидных, {time.time()-t0:.0f}s, спенд-накопл ${spent_box[0]:.3f}") + all_v += v; all_c += c + + with (JOUT / "verdicts.jsonl").open("w", encoding="utf-8") as f: + for v in all_v: + f.write(json.dumps(v, ensure_ascii=False) + "\n") + print(f"\nвсего валидных вердиктов: {len(all_v)} судейский спенд: ${spent_box[0]:.4f}") + + +if __name__ == "__main__": + main() diff --git a/eval/exp13_monitor.py b/eval/exp13_monitor.py new file mode 100644 index 0000000..3f3da81 --- /dev/null +++ b/eval/exp13_monitor.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""exp13 — слепой «монитор ПЕРЕВОДЧИКОВ»: оригинал zh + все армы-переводчики, СЛЕПО. + +Для флагман-сверки владельца (пре-рег §1.4): оригинал+вариант на копирование во Fable 5/5.6, ++ место под любительский перевод. Имена моделей скрыты; армы под нейтральными метками, +перемешаны СВОЕЙ перестановкой в каждой главе (свап). Ключ — отдельный файл +`_МОНИТОР13_КЛЮЧ.json`, не открывать до вынесения оценки. Локальный HTML (текст книги ВНЕ git/хостинга). + +Выход: diag/reading/monitor13.html + diag/reading/_МОНИТОР13_КЛЮЧ.json +""" +from __future__ import annotations +import html, json, random +from pathlib import Path + +DIAG = Path("/home/ubuntu/books/gu-zhenren/diag") +ARMS = DIAG / "arms13" +chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8")) +CH = [(5, "смешанная (нарратив+диалог)"), (7, "диалого-плотная"), (14, "экспозиция/описание")] +CHUNKS = {5: [0, 1], 7: [0, 1], 14: [0, 1]} +SALT = "exp13-monitor-blind-v1" +LEGEND = { + "T1": "deepseek-v4-flash (текущий базовый черновик)", + "T2": "mistral-large-2512", + "T3": "glm-5", + "T4": "deepseek-v4-pro", + "T5": "grok-4.3 reasoning-ON", + "T6": "qwen-flash", +} + + +MD_PROMPT = """# Слепая оценка ЧЕРНОВИКОВ перевода (蛊真人, zh→ru) — задача для модели + +Ты — эксперт по художественному переводу китайских вебновелл-культиваторок (сянься/уся) на русский. Ниже — 3 главы. В КАЖДОЙ: китайский ОРИГИНАЛ и 5 анонимных вариантов её русского ЧЕРНОВОГО перевода (метки V1…V5). Это черновики РАЗНЫХ моделей-переводчиков (редактура будет позже) — оценивай именно черновой перевод. Метки перемешаны В КАЖДОЙ ГЛАВЕ ПО-РАЗНОМУ: «V1» в главе 5 и в главе 7 — это РАЗНЫЕ модели, не связывай метки между главами. Не пытайся угадать, какая модель за какой меткой — суди только текст. + +Для КАЖДОЙ главы сравни варианты по трём осям, СТРОГО сверяясь с оригиналом: +1. **ВЕРНОСТЬ смыслу (главное):** точность; отсутствие искажений, ИНВЕРСИЙ смысла, пропусков, отсебятины, перепутанных субъектов. Особо отмечай «smooth-wrong» — гладко по-русски, но смысл искажён. +2. **КАЧЕСТВО русской прозы:** живость, читаемость, отсутствие канцелярита и калек, естественный синтаксис/ритм. +3. **ЖАНР/ТЕРМИНОЛОГИЯ:** корректность реалий культивации (культивация, секты, ранги, гу-термины), уместность имён/титулов. + +**Вывод по каждой главе:** (a) ранг V1…V5 от лучшего к худшему (приоритет — верность); (b) по каждому варианту 2–4 примера (цитата + что не так/хорошо относительно оригинала); (c) список smooth-wrong искажений (вариант → цитата → верный смысл). +**Сводно в конце:** какая метка в какой главе — лучший черновик; есть ли модель, стабильно лучшая во всех 3 главах; дотягивает ли хоть один черновик до чтения без спотыкания и какой ближе к публикабельному. + +--- +""" + + +def arms_present(): + return sorted([p.name for p in ARMS.iterdir() if p.is_dir() and p.name.startswith("T")]) + + +def build_markdown(arms, key_map): + out = [MD_PROMPT] + for ch, reg in CH: + out.append(f"\n# ГЛАВА {ch} — {reg}\n") + out.append("## Оригинал (zh)\n") + out.append(source_text(ch)) + for i in range(len(arms)): + lab = f"V{i+1}" + out.append(f"\n## Вариант {lab}\n") + out.append(arm_text(key_map[str(ch)][lab], ch)) + return "\n".join(out) + + +def arm_text(arm, ch): + parts = [] + for ci in CHUNKS[ch]: + p = ARMS / arm / f"{ch}_{ci}.txt" + parts.append(p.read_text(encoding="utf-8").strip() if p.exists() else "(нет)") + return "\n\n".join(parts) + + +def source_text(ch): + return "\n\n".join(chunks[f"{ch}/{ci}"]["source"] for ci in CHUNKS[ch]) + + +CSS = """ +:root{--bg:#faf9f7;--fg:#1c1a17;--card:#fff;--bd:#e3ddd3;--mut:#8a8177;--acc:#3e5c7a} +@media(prefers-color-scheme:dark){:root{--bg:#0f1317;--fg:#e8e3d8;--card:#171e25;--bd:#2a333b;--mut:#8494a0;--acc:#78a6c9}} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 Georgia,"Noto Serif",serif} +.wrap{max-width:1180px;margin:0 auto;padding:24px} +h1{font-size:24px;margin:0 0 6px}.sub{color:var(--mut);font-size:14px;margin:0 0 20px;line-height:1.5} +.chap{margin:34px 0 8px;font-size:20px;border-bottom:2px solid var(--acc);padding-bottom:6px} +.src{background:var(--card);border:1px solid var(--bd);border-radius:10px;padding:14px 16px;margin:12px 0 20px} +.src .lbl{color:var(--acc);font-weight:700;font-size:13px;letter-spacing:.03em;text-transform:uppercase} +.grid{display:grid;grid-template-columns:1fr;gap:14px} +.card{background:var(--card);border:1px solid var(--bd);border-radius:10px;overflow:hidden} +.hd{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid var(--bd)} +.hd .t{font-weight:700;font-size:15px} +.body{padding:6px 16px 14px;white-space:pre-wrap;font-size:15.5px} +.body.zh{font-family:"Noto Sans CJK SC","Microsoft YaHei",sans-serif;font-size:15px} +button{font:inherit;font-size:12.5px;background:var(--acc);color:#fff;border:0;border-radius:6px;padding:5px 11px;cursor:pointer} +button.ghost{background:transparent;color:var(--acc);border:1px solid var(--acc)} +.slot{border:2px dashed var(--bd);border-radius:10px;padding:16px;color:var(--mut);font-size:14px;margin-top:14px} +.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0 0} +.note{background:var(--card);border:1px solid var(--bd);border-left:3px solid var(--acc);border-radius:8px;padding:10px 14px;font-size:13.5px;color:var(--mut);margin:16px 0} +""" +FLAGSHIP_PROMPT = ( + "Ты — эксперт по художественному переводу китайских вебновелл-культиваторок (сянься/уся) на русский. " + "Ниже — КИТАЙСКИЙ ИСХОДНИК одной главы и несколько АНОНИМНЫХ вариантов её русского ЧЕРНОВОГО перевода " + "(метки V1, V2, …). Это черновики РАЗНЫХ моделей; редактура будет позже — оценивай именно черновой перевод. " + "Не пытайся угадать модель — суди только текст.\\n\\n" + "Сравни варианты по трём осям, СТРОГО сверяясь с исходником:\\n" + "1) ВЕРНОСТЬ смыслу (главное): точность; отсутствие искажений, ИНВЕРСИЙ смысла, пропусков, отсебятины, " + "перепутанных субъектов. Особо отмечай «smooth-wrong» — гладко по-русски, но смысл искажён.\\n" + "2) КАЧЕСТВО русской прозы: живость, читаемость, отсутствие канцелярита и калек, естественный синтаксис/ритм.\\n" + "3) ЖАНР/ТЕРМИНОЛОГИЯ: корректность реалий культивации, уместность имён/титулов.\\n\\n" + "Ответ: (a) РАНГ вариантов от лучшего к худшему (приоритет — верность); (b) по каждому 2–4 конкретных " + "примера (цитата + что не так/хорошо относительно zh); (c) отдельный список smooth-wrong (вариант → цитата → " + "верный смысл); (d) вердикт: дотягивает ли ХОТЬ ОДИН до чтения без спотыкания и какой ближе к публикабельному черновику." +) + +JS = """ +var PROMPT=%r; +function esc(id){var e=document.getElementById(id);return e?e.textContent:''} +function copyOne(vid){navigator.clipboard.writeText(esc(vid));flash(event.target)} +function copyPair(sid,vid){navigator.clipboard.writeText('=== ОРИГИНАЛ (zh) ===\\n'+esc(sid)+'\\n\\n=== ПЕРЕВОД ===\\n'+esc(vid));flash(event.target)} +function copyPrompt(){navigator.clipboard.writeText(PROMPT);flash(event.target)} +function copyChapter(sid,vids){ + var parts=[PROMPT,'\\n\\n===== ИСХОДНИК (zh) =====\\n'+esc(sid)]; + for(var i=0;i'] + out.append('

蛊真人 — слепой монитор ПЕРЕВОДЧИКОВ (exp13)

') + out.append('

Оригинал + варианты ЧЕРНОВОГО перевода (разные модели-переводчики) по 3 главам, ' + 'СЛЕПО: имена моделей скрыты, метки перемешаны по-своему в каждой главе («V3» в гл.5 и гл.7 — РАЗНЫЕ модели). ' + 'Это ЧЕРНОВИКИ (без редактуры) — сравнивай ВЕРНОСТЬ смыслу и качество русской прозы. ' + 'Кнопка «копировать оригинал+перевод» кладёт пару в буфер для флагмана (Fable 5/5.6) слепо. ' + 'Ключ — _МОНИТОР13_КЛЮЧ.json рядом, НЕ открывай до вынесения оценки.

') + out.append('
🎯 Как гонять флагмана (GPT/Claude), слепо: у каждой главы жми ' + '«📋 копировать ГЛАВУ целиком» — в буфер лягут ГОТОВЫЙ ПРОМТ + оригинал + все версии V1…V5. ' + 'Вставь одним куском в GPT/Claude и отправь. Флагман не увидит имён моделей. ' + '
') + + for ch, reg in CH: + order = arms[:] + random.Random(f"{SALT}-{ch}").shuffle(order) + labels = {f"V{i+1}": arm for i, arm in enumerate(order)} + key_map[str(ch)] = labels + out.append(f'

Глава {ch} — {html.escape(reg)}

') + sid = f"src{ch}" + vids_js = "[" + ",".join(f"'v{ch}_{i+1}'" for i in range(len(arms))) + "]" + out.append(f'
Оригинал (zh)
' + f'
' + f'' + f'
' + f'
{html.escape(source_text(ch))}
') + out.append('
') + for i, (lab, arm) in enumerate(labels.items(), 1): + vid = f"v{ch}_{i}" + out.append('
') + out.append(f'
{lab}
') + out.append(f'
' + f'' + f'
') + out.append(f'
{html.escape(arm_text(arm, ch))}
') + out.append('
') + out.append('
') + out.append('
➕ Любительский перевод этой главы: добавляй сам при загрузке во флагман.
') + + out.append('
Это ЧЕРНОВИКИ переводчиков (стадия draft), НЕ финал: downstream их чинит ' + 'билингвальный редактор (D30.1). Задача монитора — выбрать САМЫЙ ВЕРНЫЙ и живой ЧЕРНОВИК. ' + 'Имена моделей скрыты, чтобы ярлык не искажал восприятие. Расшифровка — в закрытом ключе.
') + out.append('') + out.append(f'') + + (DIAG / "reading").mkdir(parents=True, exist_ok=True) + (DIAG / "reading" / "monitor13.html").write_text("\n".join(out), encoding="utf-8") + (DIAG / "reading" / "monitor13.md").write_text(build_markdown(arms, key_map), encoding="utf-8") + (DIAG / "reading" / "_МОНИТОР13_КЛЮЧ.json").write_text( + json.dumps({"salt": SALT, "map": key_map, "arms": {a: LEGEND.get(a, a) for a in arms}}, ensure_ascii=False, indent=1), + encoding="utf-8") + print(f"монитор ({len(arms)} армов) → diag/reading/monitor13.html + monitor13.md (самодостаточный)") + for ch in [5, 7, 14]: + print(f" гл.{ch}: {[key_map[str(ch)][f'V{i+1}'] for i in range(len(arms))]}") + + +if __name__ == "__main__": + main() diff --git a/eval/exp13_seed_v2.py b/eval/exp13_seed_v2.py new file mode 100644 index 0000000..2cc00fa --- /dev/null +++ b/eval/exp13_seed_v2.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""exp13 — ФИНАЛИЗАЦИЯ сида v2 (D30.5). Единый воспроизводимый трансформ v1 → v2, +СУПЕРСИДИТ exp12_glossary_v2.py (дефект провенанса/статусов из ревью D30.5): + • спорные термины → status:DRAFT (не approved!): 修炼/修行 (культивация), 人祖 (Первопредок), + род «гу»-зависимые (本命蛊 жизненный гу, 蛊虫 гу-червь), 花酒行者 (перезаписывал пожелание владельца); + • бесспорные правки/добавления (D30.5: 11 правок + 8 добавлений минус спорные) → approved; + • откат тихих промоушенов draft→approved (花酒行者, 本命蛊 остаются draft); + • обогащение мн.ч. (D24.4/полигон-№4): плюрал-формы в decl.forms счётных 元石/凡人/蛊师; + • gu-gender: НЕ флипаю молча (рекоменд. флагманов «мужской») — проектное решение владельца, в sign-off. +Вход: guzhenren-seed.yaml. Выход: guzhenren-seed-v2.yaml + diag/glossary_v2_signoff.md. ВНЕ git. НЕ в прод без подписи+resnapshot. +""" +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" +SIGNOFF = BOOK / "diag" / "glossary_v2_signoff.md" + +DRAFT = "draft" # спорное — до подписи владельца +OK = "approved" + +# src -> (new_dst, forms|None, status, contested_reason|None, rationale) +CHANGES = { + "空窍": ("апертура", ["апертуры", "апертуре", "апертуру", "апертурой", "апертуре"], OK, None, + "оба флагмана — топ-проблема: «врата» ломают грамматику («одни врата»); «апертура» = фан-вики жанра"), + "开窍": ("открытие апертуры", ["открытия апертуры", "открытию апертуры", "открытием апертуры", "открытии апертуры"], OK, None, + "согласовано с 空窍→апертура; 开窍大典 = обряд открытия апертуры"), + "真元": ("истинная ци", ["истинной ци", "истинной ци", "истинную ци", "истинной ци", "истинной ци"], OK, None, + "оба: НЕ «истинная энергия» (физикализм); ци — жанровая норма; отделить от 元气"), + "元海": ("море истинной ци", ["моря истинной ци", "морю истинной ци", "морем истинной ци", "море истинной ци"], OK, None, + "согласовано с 真元→истинная ци"), + "元石": ("изначальный камень", ["изначального камня", "изначальному камню", "изначальный камень", "изначальным камнем", "изначальном камне"], OK, None, + "«первокамень» → «двадцать первокамень» читается как «двадцать первый»; согласован с рядом 元"), + "古月山寨": ("селение Гуюэ", ["селения Гуюэ", "селению Гуюэ", "селением Гуюэ", "селении Гуюэ"], OK, None, + "«стан» = воинский лагерь; 山寨 здесь — укреплённое селение/деревня"), + "白家寨": ("селение Бай", ["селения Бай", "селению Бай", "селением Бай", "селении Бай"], OK, None, "согласовано"), + "熊家寨": ("селение Сюн", ["селения Сюн", "селению Сюн", "селением Сюн", "селении Сюн"], OK, None, "согласовано"), + # --- СПОРНЫЕ → DRAFT --- + "本命蛊": ("жизненный гу", ["жизненного гу", "жизненному гу", "жизненного гу", "жизненным гу", "жизненном гу"], DRAFT, + "род «гу» + выбор рендеринга", "оба: НЕ «гу Жизни» (читается как ВИД). СТАТУС, не вид. Согласование зависит от рода «гу»"), + "花酒行者": ("Странник Цветов и Вина", ["Странника Цветов и Вина", "Страннику Цветов и Вина", "Странника Цветов и Вина", "Странником Цветов и Вина", "Страннике Цветов и Вина"], DRAFT, + "перезаписывает пожелание владельца «Винный Странник»", "флагманы: сохранить 花 (=блуд; 采花大盗). Владелец предпочёл «Винный Странник» — за ним"), + "蛊虫": ("гу-червь", ["гу-червя", "гу-червю", "гу-червя", "гу-червём", "гу-черве"], DRAFT, + "род «гу»", "«гу-тварь» пейоративно; «гу-червь»/«гу». Согласование зависит от рода «гу»"), +} + +# новые термины: dict(src,dst,type,forms|None,invariant,status,contested_reason|None,note) +ADDITIONS = [ + dict(src="长生", dst="бессмертие", type="term", forms=["бессмертия", "бессмертию", "бессмертие", "бессмертием", "бессмертии"], + status=OK, contested=None, note="chángshēng; вечная жизнь/бессмертие — сквозная тема; НЕ «долголетие»"), + dict(src="族长", dst="глава клана", type="title", forms=["главы клана", "главе клана", "главу клана", "главой клана", "главе клана"], + status=OK, contested=None, note="zúzhǎng; глава клана — НАД старейшинами; черновик путал со «старейшиной». ⚠ консистентность род/клан — см. sign-off"), + dict(src="家老", dst="старейшина", type="title", forms=["старейшины", "старейшине", "старейшину", "старейшиной", "старейшине"], + status=OK, contested=None, note="jiālǎo; старейшина клана; развести с 族长"), + dict(src="困境", dst="Беды", type="term", forms=["Бед", "Бедам", "Беды", "Бедами", "Бедах"], + status=OK, contested=None, note="kùnjìng; стая зверей в легенде о Первопредке; имя стаи"), + dict(src="炼化", dst="очистить и подчинить", type="term", forms=[], invariant=True, + status=OK, contested=None, note="liànhuà; очистить-и-подчинить живого гу; НЕ «переплавить». Кратко «очистить»"), + # --- СПОРНЫЕ → DRAFT --- + dict(src="修炼", dst="культивация", type="term", forms=["культивации", "культивации", "культивацию", "культивацией", "культивации"], + status=DRAFT, contested="лит.альтернатива «совершенствование» (флагманы для издания)", note="xiūliàn; культивация/культивировать"), + dict(src="修行", dst="культивация", type="term", forms=["культивации", "культивации", "культивацию", "культивацией", "культивации"], + status=DRAFT, contested="как 修炼", note="xiūxíng; практика культивации; согласовано с 修炼"), + dict(src="人祖", dst="Первопредок", type="name", forms=["Первопредка", "Первопредку", "Первопредка", "Первопредком", "Первопредке"], + status=DRAFT, contested="альт-имя «Жэнь Цзу» (Палладица)", note="Rénzǔ; говорящее имя (Праотец человечества). НЕ «Рен Зу» (англ.релей-ошибка)"), +] + +# обогащение мн.ч. (D24.4): src -> плюрал-формы (append к decl.forms), закрывает inflection_gap +PLURALS = { + "元石": ["изначальные камни", "изначальных камней", "изначальным камням", "изначальными камнями", "изначальных камнях"], + "凡人": ["смертные", "смертных", "смертным", "смертными"], + "蛊师": ["гу-мастеров", "гу-мастерам", "гу-мастерами", "гу-мастерах"], +} + +GU_FLAG = ("РОД «гу»: сид держит НЕСКЛОНЯЕМЫЙ СРЕДНИЙ; оба флагмана рекомендуют МУЖСКОЙ («этот гу», как «жук»; " + "согласуется с «винный червь — он»). Меняет согласование во ВСЕХ гу-именах — НЕ флипаю молча. " + "Жизненный гу/гу-червь даны в мужском как рекомендация под status:draft. Решение — за владельцем.") + + +def main(): + data = yaml.safe_load(SRC.read_text(encoding="utf-8")) + terms = data["terms"] + changed, added, plural_done, rollbacks = [], [], [], [] + + for t in terms: + s = t.get("src") + if s in CHANGES: + new_dst, forms, status, contested, rat = CHANGES[s] + old_dst, old_status = t.get("dst"), t.get("status") + t["dst"] = new_dst + if forms is not None: + t["decl"] = {"invariant": False, "forms": list(forms)} + t["status"] = status + tag = "DRAFT⚑" if status == DRAFT else "OK" + t["note"] = f"[v2:{tag}] {rat}. (было: «{old_dst}»). " + t.get("note", "") + changed.append((s, old_dst, new_dst, status, contested, rat)) + if old_status == DRAFT and status == DRAFT and contested: + rollbacks.append((s, "оставлен draft (тихий промоушен НЕ применён)")) + if s in PLURALS: + forms = t.setdefault("decl", {}).setdefault("forms", []) + for f in PLURALS[s]: + if f not in forms: + forms.append(f) + t["note"] = t.get("note", "") + " [v2: +мн.ч. D24.4]" + plural_done.append((s, t["dst"], PLURALS[s])) + + for a in ADDITIONS: + entry = {"src": a["src"], "dst": a["dst"], "type": a["type"], + "decl": {"invariant": a.get("invariant", False), "forms": list(a.get("forms", []))}, + "since_ch": 0, "status": a["status"], + "note": f"[v2-NEW:{'DRAFT⚑' if a['status']==DRAFT else 'OK'}] {a['note']}"} + terms.append(entry) + added.append((a["src"], a["dst"], a["status"], a.get("contested"), a["note"])) + + OUT.write_text(yaml.dump(data, allow_unicode=True, sort_keys=False, width=200), encoding="utf-8") + + # --- sign-off table для владельца --- + contested_changes = [(s, o, n, c) for s, o, n, st, c, _ in changed if st == DRAFT] + contested_adds = [(s, d, c) for s, d, st, c, _ in added if st == DRAFT] + lg = ["# Глоссарий 蛊真人 — v2 · таблица на ПОДПИСЬ владельца (D30.5)", "", + f"Изменено: {len(changed)} · Добавлено: {len(added)} · Обогащено мн.ч.: {len(plural_done)} · Всего терминов: {len(terms)}", + "Провенанс: единый трансформ `eval/exp13_seed_v2.py` из v1-сида; рекомендации — консенсус флагманов " + "(GPT+Claude, diag/reading/ОЦЕНКА_ФЛАГМАНА.md) + пожелания владельца. **НЕ в прод без подписи + resnapshot.**", + "Статусы: **approved** = применяю (бесспорное); **draft⚑** = НЕ уходит в CONFIRMED-hard-constraints до твоей подписи (memory.go:419/473).", "", + "## A. Спорные — на решение (сейчас status:draft, инъектятся как ⟨проверить⟩)", "", + "| src | вариант в v2 | альтернатива / причина спора | твоё решение |", "|---|---|---|---|"] + for s, o, n, c in contested_changes: + lg.append(f"| {s} | **{n}** (было «{o}») | {c} | ☐ принять / ☐ альтернатива |") + for s, d, c in contested_adds: + lg.append(f"| {s} | **{d}** | {c} | ☐ принять / ☐ альтернатива |") + lg += ["", f"- **Род «гу»** (проектное, влияет на согласование всех гу-имён): {GU_FLAG}", ""] + lg += ["## B. Бесспорные — применены (status:approved)", "", + "| src | было | стало | почему |", "|---|---|---|---|"] + for s, o, n, st, c, rat in changed: + if st == OK: + lg.append(f"| {s} | {o} | **{n}** | {rat} |") + for s, d, st, c, note in added: + if st == OK: + lg.append(f"| {s} | — (новый) | **{d}** | {note} |") + lg += ["", "## C. Обогащение мн.ч. (D24.4/полигон-№4 — закрывает inflection_gap post-check)", "", + "| src | dst | добавленные формы |", "|---|---|---|"] + for s, d, forms in plural_done: + lg.append(f"| {s} | {d} | {', '.join(forms)} |") + lg += ["", "## D. Прочие открытые флаги сида (уже draft в v1, не трогаю без тебя)", + "- 花酒行者/Винный Странник, 甲乙丙丁/разряд А–Г, 蛊虫/гу-червь, 本命蛊/жизненный гу, 南疆/Наньцзян,", + " 一气金光虫/Золотосветный червь, 青丝蛊/гу Чёрных волос, 留影存声蛊 (длинное имя), 沈嬷嬷/матушка Шэнь.", + "- ⚠ Консистентность **род/клан**: 族长→«глава клана» (новый) vs 四代族长→«Четвёртый глава рода» (v1). Унифицировать? ☐ клан ☐ род"] + SIGNOFF.write_text("\n".join(lg), encoding="utf-8") + + print(f"v2 seed → {OUT}") + print(f"sign-off → {SIGNOFF}") + print(f"изменено {len(changed)} (спорных→draft: {len(contested_changes)}), " + f"добавлено {len(added)} (спорных→draft: {len(contested_adds)}), мн.ч. {len(plural_done)}, всего {len(terms)}") + print("Спорные (draft):", [s for s, *_ in contested_changes] + [s for s, *_ in contested_adds]) + + +if __name__ == "__main__": + main() diff --git a/eval/exp13_translate.py b/eval/exp13_translate.py new file mode 100644 index 0000000..489fb7d --- /dev/null +++ b/eval/exp13_translate.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""exp13 — бейк-офф ПЕРЕВОДЧИКОВ (платно, кап $5). Пре-регистрация §1.2 заморожена. + +Faithful draft-стадия: swap ТОЛЬКО модели переводчика. Layout повторяет прод draft-стадию +(chunkrun.go: translatorInjection = renderGlossaryBlock; render.go MessagesWithInjection): + system(translator.md reflow-edition, brief-filled) → system(инъекция «src → dst») → user(перевод). +Инъекция и исходник — из приёмочного store (V1-сид), ИДЕНТИЧНЫ по чанку у всех армов. +translator.md строится в коде с УДАЛЕНИЕМ строки 9 «Сохраняй разбивку…» (ратифиц. D30.2; +пакет бэкенда ещё не залендён — прод-файл не трогаю). + +T1 deepseek-v4-flash — из store ($0). T6 qwen — только с DashScope-ключом (env QWEN_API_KEY|DASHSCOPE_API_KEY). +Запуск: eval/.venv/bin/python eval/exp13_translate.py [--dry] +Выход: diag/arms13//_.txt + diag/arms13/results.json + diag/arms13/costs.jsonl +""" +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") +OUT = DIAG / "arms13" +TRANSLATOR_MD = Path("/home/ubuntu/projects/textmachine/backend/prompts/translator.md") +CAP_USD = 5.0 +TARGETS = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)] + +# brief-vars из acceptance/book.yaml (D18) +VARS = {"source_lang": "zh", "target_lang": "ru", "genre": "вебновелла", + "audience": "взрослые читатели вебновелл", "title": "蛊真人", + "honorifics": "keep", "transcription": "palladius", "venuti": "0.6", + "footnotes": "minimal"} + +# строка D30.2-reflow: удаляем ровно её (ратифицированный снос «Сохраняй разбивку») +REFLOW_DROP = "Сохраняй разбивку на абзацы и прямую речь как в оригинале." + +PRICES = { # $/1M in/out (models.yaml, prices_checked 2026-07-10) + "deepseek-v4-flash": (0.14, 0.28), "deepseek-v4-pro": (0.435, 0.87), + "mistral-large-2512": (0.5, 1.5), "glm-5": (1.0, 3.2), "grok-4.3": (1.25, 2.50), +} + +ARMS = { + # T1 — из store, генерации нет (см. main); прайс для справки + "T2": dict(model="mistral-large-2512", base="https://api.mistral.ai/v1", + keyenv="MISTRAL_API_KEY", extra={}, max_tokens=8000), + "T3": dict(model="glm-5", base="https://api.z.ai/api/paas/v4", + keyenv="ZAI_API_KEY", extra={"thinking": {"type": "disabled"}}, max_tokens=8000), + "T4": dict(model="deepseek-v4-pro", base="https://api.deepseek.com/v1", + keyenv="DEEPSEEK_API_KEY", extra={}, max_tokens=8000), # thinking ON = default + "T5": dict(model="grok-4.3", base="https://api.x.ai/v1", + keyenv="XAI_API_KEY", extra={}, max_tokens=8000), # reasoning ON = default low +} +# T6 qwen — гейт на ключ; DashScope OpenAI-совместимый слой +QWEN = dict(model="qwen-flash", base="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + keyenv="DASHSCOPE_API_KEY", extra={}, max_tokens=8000) + + +def render(tpl: str) -> str: + for k, v in VARS.items(): + tpl = tpl.replace("{{%s}}" % k, v) + return tpl + + +def translator_system() -> str: + raw = TRANSLATOR_MD.read_text(encoding="utf-8") + sys_part = raw.split("---USER---")[0].rstrip() + lines = [ln for ln in sys_part.split("\n") if REFLOW_DROP not in ln] + dropped = sys_part.count(REFLOW_DROP) + if dropped != 1: + raise SystemExit(f"reflow-guard: ожидал 1 вхождение строки D30.2, нашёл {dropped} — translator.md изменился, сверься с оркестратором") + return render("\n".join(lines)) + + +def build_messages(source: str, inj: str) -> list[dict]: + msgs = [{"role": "system", "content": translator_system()}] + if inj.strip(): + msgs.append({"role": "system", "content": inj}) + msgs.append({"role": "user", "content": f"Переведи следующий фрагмент:\n\n{source}"}) + return msgs + + +def call(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.3, "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 + reason = usage.get("reasoning_tokens", 0) or 0 # grok additive шлёт отдельно; subset=0 + pin, pout = PRICES.get(model, (0, 0)) + 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")) + OUT.mkdir(parents=True, exist_ok=True) + costs_fp = OUT / "costs.jsonl" + results, spent = [], 0.0 + + arms = dict(ARMS) + qkey = os.environ.get("DASHSCOPE_API_KEY") or os.environ.get("QWEN_API_KEY") + if qkey: + os.environ["DASHSCOPE_API_KEY"] = qkey + arms["T6"] = QWEN + print("T6 qwen: DashScope-ключ найден — включаю") + else: + print("T6 qwen: ключа нет — пропускаю (не блокируюсь)") + + # T1 — из store (deepseek-v4-flash draft), $0 + t1 = OUT / "T1"; t1.mkdir(exist_ok=True) + for ch, ci in TARGETS: + (t1 / f"{ch}_{ci}.txt").write_text(chunks[f"{ch}/{ci}"]["draft"], encoding="utf-8") + print("T1 deepseek-v4-flash: 6 чанков из store ($0)") + + for arm, spec in arms.items(): + adir = OUT / arm; adir.mkdir(exist_ok=True) + for ch, ci in TARGETS: + key = f"{ch}/{ci}" + src = chunks[key]["source"] + inj = blocks[key]["bilingual_glossary"] + 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(src, inj) + if dry: + approx_in = sum(len(m["content"]) for m in msgs) // 3 + print(f" [{arm} {key}] model={spec['model']} msgs={len(msgs)} ~in={approx_in}tok src_chars={len(src)}") + continue + if spent > CAP_USD * 0.8: + print(f"!! near cap (${spent:.3f}) — stop"); break + t0 = time.time() + text, err, usage = call(spec, msgs) + c = cost_of(spec["model"], usage) + spent += c + rec = {"arm": arm, "model": spec["model"], "chunk": key, "cost": round(c, 6), + "usage": {k: usage.get(k) for k in ("prompt_tokens", "completion_tokens", "reasoning_tokens", "total_tokens", "finish_reason")}} + with costs_fp.open("a", encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False) + "\n") + if not text: + print(f" [{arm} {key}] ERROR: {err} (${spent:.3f})") + results.append({**rec, "error": err}); continue + outp.write_text(text, encoding="utf-8") + results.append({**rec, "chars": len(text)}) + print(f" [{arm} {key}] {time.time()-t0:.0f}s {len(text)}ch ${c:.4f} cum=${spent:.3f} finish={usage.get('finish_reason')}") + + if not dry: + (OUT / "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()