textmachine/eval/exp13/exp13_monitor.py

186 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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) по каждому варианту 24 примера (цитата + что не так/хорошо относительно оригинала); (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) по каждому 24 конкретных "
"примера (цитата + что не так/хорошо относительно 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<vids.length;i++){parts.push('\\n\\n===== V'+(i+1)+' =====\\n'+esc(vids[i]))}
navigator.clipboard.writeText(parts.join(''));flash(event.target)
}
function flash(b){var o=b.textContent;b.textContent='✓ скопировано';setTimeout(function(){b.textContent=o},1400)}
""" % FLAGSHIP_PROMPT
def main():
arms = arms_present()
key_map = {}
out = ['<div class="wrap">']
out.append('<h1>蛊真人 — слепой монитор ПЕРЕВОДЧИКОВ (exp13)</h1>')
out.append('<p class="sub">Оригинал + варианты ЧЕРНОВОГО перевода (разные модели-переводчики) по 3 главам, '
'СЛЕПО: имена моделей скрыты, метки перемешаны по-своему в каждой главе («V3» в гл.5 и гл.7 — РАЗНЫЕ модели). '
'Это ЧЕРНОВИКИ (без редактуры) — сравнивай ВЕРНОСТЬ смыслу и качество русской прозы. '
'Кнопка «копировать оригинал+перевод» кладёт пару в буфер для флагмана (Fable 5/5.6) слепо. '
'Ключ — <code>_МОНИТОР13_КЛЮЧ.json</code> рядом, НЕ открывай до вынесения оценки.</p>')
out.append('<div class="note">🎯 <b>Как гонять флагмана (GPT/Claude), слепо:</b> у каждой главы жми '
'«📋 копировать ГЛАВУ целиком» — в буфер лягут ГОТОВЫЙ ПРОМТ + оригинал + все версии V1…V5. '
'Вставь одним куском в GPT/Claude и отправь. Флагман не увидит имён моделей. '
'<button onclick="copyPrompt()">копировать только промт</button></div>')
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'<h2 class="chap">Глава {ch}{html.escape(reg)}</h2>')
sid = f"src{ch}"
vids_js = "[" + ",".join(f"'v{ch}_{i+1}'" for i in range(len(arms))) + "]"
out.append(f'<div class="src"><div class="lbl">Оригинал (zh)</div>'
f'<div class="toolbar">'
f'<button onclick="copyChapter(\'{sid}\',{vids_js})">📋 копировать ГЛАВУ целиком (промт + оригинал + все версии) — для флагмана</button>'
f'<button class="ghost" onclick="copyOne(\'{sid}\')">только оригинал</button></div>'
f'<div class="body zh" id="{sid}">{html.escape(source_text(ch))}</div></div>')
out.append('<div class="grid">')
for i, (lab, arm) in enumerate(labels.items(), 1):
vid = f"v{ch}_{i}"
out.append('<div class="card">')
out.append(f'<div class="hd"><span class="t">{lab}</span></div>')
out.append(f'<div class="toolbar" style="padding:8px 16px 0">'
f'<button onclick="copyPair(\'{sid}\',\'{vid}\')">копировать оригинал+перевод</button>'
f'<button class="ghost" onclick="copyOne(\'{vid}\')">только перевод</button></div>')
out.append(f'<div class="body" id="{vid}">{html.escape(arm_text(arm, ch))}</div>')
out.append('</div>')
out.append('</div>')
out.append('<div class="slot"> Любительский перевод этой главы: добавляй сам при загрузке во флагман.</div>')
out.append('<div class="note">Это ЧЕРНОВИКИ переводчиков (стадия draft), НЕ финал: downstream их чинит '
'билингвальный редактор (D30.1). Задача монитора — выбрать САМЫЙ ВЕРНЫЙ и живой ЧЕРНОВИК. '
'Имена моделей скрыты, чтобы ярлык не искажал восприятие. Расшифровка — в закрытом ключе.</div>')
out.append('</div>')
out.append(f'<style>{CSS}</style><script>{JS}</script>')
(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()