textmachine/eval/exp12/exp12_monitor.py

151 lines
9.4 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
"""exp12 — «монитор»: локальная HTML-страница ОРИГИНАЛ + все варианты перевода, СЛЕПО.
По запросу владельца: удобный просмотр для сравнения и для загрузки (оригинал+вариант) во
флагманские модели (Fable 5/5.6) + место под любительский перевод. ЛОКАЛЬНЫЙ файл (текст книги
копирайтный → держим на машине владельца, ВНЕ git/хостинга).
СЛЕПОТА: имена моделей в мониторе НЕ показаны; варианты под нейтральными метками, перемешаны
СВОЕЙ перестановкой в каждой главе (D13.5). Ключ соответствий — в ОТДЕЛЬНОМ файле
`diag/reading/_МОНИТОР_КЛЮЧ.json` (не открывать до вынесения оценки).
Выход: /home/ubuntu/books/gu-zhenren/diag/reading/monitor.html
"""
from __future__ import annotations
import html, json, random
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
ARMS = DIAG / "arms"
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 = "exp12-monitor-blind-v1"
POOL = ["A0", "A1", "A2", "A5", "CAND", "A3", "A4", "A6"] # порядок неважен — тасуется
LEGEND = {
"A0": "черновик deepseek (без редактуры)",
"A1": "glm-5 монолингвальный (= финал этапа A, что читал владелец)",
"A2": "glm-5 билингвальный",
"A5": "gemini-3.1-pro билингвальный (⚠ течёт преамбулой)",
"CAND": "КАНДИДАТ: glm-5 билингв + реверстка + жанр-конвенции",
"A3": "grok-4.3 моно (reasoning-off, ≈ черновик)",
"A4": "grok-4.3 билингв (reasoning-off, ≈ черновик)",
"A6": "grok-4.3 моно без глоссария (reasoning-off, ≈ черновик)",
}
def strip_preamble(t):
"""gemini (A5) течёт служебной преамбулой «Вот отредактированный перевод…: ***». Снимаем
для ЧЕСТНОГО сравнения ПРОЗЫ (сам дефект утечки задокументирован в отчёте §4.1)."""
first = t.lstrip().split("\n", 1)[0]
if first.startswith("Вот ") and "перевод" in first.lower():
if "***" in t:
t = t.split("***", 1)[1]
else: # преамбула = первая строка, дальше текст
t = t.split("\n", 1)[1] if "\n" in t else t
return t.strip()
def arm_text(arm, ch):
parts = []
for ci in CHUNKS[ch]:
key = f"{ch}/{ci}"
if arm == "A0":
t = chunks[key]["draft"]
elif arm == "A1":
t = chunks[key]["final"]
else:
p = ARMS / arm / f"{ch}_{ci}.txt"
t = p.read_text(encoding="utf-8").strip() if p.exists() else "(нет)"
if arm == "A5":
t = strip_preamble(t)
parts.append(t.strip())
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:#7a5c3e}
@media(prefers-color-scheme:dark){:root{--bg:#17150f;--fg:#e8e3d8;--card:#211e17;--bd:#38332a;--mut:#9c9384;--acc:#c9a678}}
*{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;cursor:pointer;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}
"""
JS = """
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 flash(b){var o=b.textContent;b.textContent='✓ скопировано';setTimeout(function(){b.textContent=o},1200)}
"""
def main():
key_map = {}
out = ['<div class="wrap">']
out.append('<h1>蛊真人 — слепой монитор переводов (exp12)</h1>')
out.append('<p class="sub">Оригинал + варианты редактуры по 3 главам, СЛЕПО: имена моделей скрыты, '
'варианты перемешаны по-своему в каждой главе («V3» в гл.5 и гл.7 — РАЗНЫЕ конфиги). '
'Кнопка «копировать оригинал+перевод» кладёт пару в буфер для загрузки во флагман (Fable 5/5.6) — '
'имя модели не копируется, флагман судит слепо. Любительский перевод добавляй сам. '
'Ключ соответствий — в файле <code>_МОНИТОР_КЛЮЧ.json</code> рядом, НЕ открывай до вынесения оценки.</p>')
for ch, reg in CH:
order = POOL[:]
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}"
out.append(f'<div class="src"><div class="lbl">Оригинал (zh)</div>'
f'<div class="toolbar"><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">Имена моделей и соответствия меток намеренно скрыты — чтобы восприятие '
'(твоё и флагманской модели) не искажалось ярлыком. Расшифровка — в закрытом ключе рядом.</div>')
out.append('</div>')
out.append(f'<style>{CSS}</style><script>{JS}</script>')
Path(DIAG / "reading" / "monitor.html").write_text("\n".join(out), encoding="utf-8")
(DIAG / "reading" / "_МОНИТОР_КЛЮЧ.json").write_text(
json.dumps({"salt": SALT, "map": key_map, "arms": LEGEND}, ensure_ascii=False, indent=1),
encoding="utf-8")
print("monitor (blind) → diag/reading/monitor.html")
print("key (held) → diag/reading/_МОНИТОР_КЛЮЧ.json")
for ch in [5, 7, 14]:
print(f" гл.{ch} перестановка: {[key_map[str(ch)][f'V{i+1}'] for i in range(len(POOL))]}")
if __name__ == "__main__":
main()