#!/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 = ['
'] out.append('

蛊真人 — слепой монитор переводов (exp12)

') out.append('

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

') 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'

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

') sid = f"src{ch}" out.append(f'
Оригинал (zh)
' 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('
Имена моделей и соответствия меток намеренно скрыты — чтобы восприятие ' '(твоё и флагманской модели) не искажалось ярлыком. Расшифровка — в закрытом ключе рядом.
') out.append('
') out.append(f'') 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()