textmachine/eval/build_editor_artifact.py

202 lines
12 KiB
Python
Raw 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
"""Генератор страницы слепого сравнения редакторов (эксперимент 04).
Читает eval/data/editor_bench/<fragment>/*.txt, применяет слепую разметку A/B/C/D,
эмитит самодостаточный HTML (контент для Artifact-обёртки). Запуск:
eval/.venv/bin/python eval/build_editor_artifact.py <out.html>
"""
import html
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
BENCH = ROOT / "data" / "editor_bench"
# Слепая разметка: фиксированный перемешанный порядок (одинаков на всех фрагментах)
BLIND = [("A", "grok-4.3"), ("B", "glm-4.6"), ("C", "gemini-3.1-pro"), ("D", "kimi-k2.6")]
FRAGMENTS = [
("rashomon", "японский → русский", "Акутагава Рюноскэ, «Расёмон»", False),
("ahq", "китайский → русский", "Лу Синь, «Подлинная история А-Кью»", False),
("merosu", "японский → русский", "Дадзай Осаму, «Беги, Мелос!»", False),
("wells", "английский → русский", "Г. Уэллс, «Машина времени»", True),
]
# стоимость/латентность из results.json (последний прогон) — для секции-расшифровки
COST = {"grok-4.3": "~13 с, без «размышлений» — самый быстрый",
"glm-4.6": "~105 с (thinking off)",
"gemini-3.1-pro": "~6086 с, обязательный thinking",
"kimi-k2.6": "~150 с, ~11 000 токенов «размышлений» на абзац — самый дорогой"}
def paras(text: str) -> str:
text = text.strip()
blocks = re.split(r"\n\s*\n", text) or [text]
if len(blocks) == 1:
blocks = [b for b in text.split("\n") if b.strip()]
return "".join(f"<p>{html.escape(b.strip())}</p>" for b in blocks if b.strip())
def read(frag: str, name: str) -> str:
f = BENCH / frag / f"{name}.txt"
return f.read_text() if f.exists() else ""
def main() -> None:
out = Path(sys.argv[1])
S = []
S.append("<title>Сравнение редакторов ru-стиля · Эксперимент 04</title>")
S.append(f"<style>{CSS}</style>")
S.append('<div class="wrap">')
S.append('<header class="intro">')
S.append('<p class="eyebrow">TextMachine · полигон · эксперимент 04</p>')
S.append("<h1>Кто лучше правит русский стиль</h1>")
S.append('<p class="lede">Четыре модели-редактора получили один и тот же черновой перевод и '
'задание: превратить его в живую художественную прозу, ничего не переврав. '
'Версии обезличены — <strong>A, B, C, D</strong>. Оцени по двум осям:</p>')
S.append('<ul class="criteria">'
'<li><span class="k k-style">стиль</span> — какая русская проза читается живее, без канцелярита и калек;</li>'
'<li><span class="k k-fidel">верность</span> — кто не потерял смысл и не добавил отсебятины (сверяй со «смысловым якорем»).</li>'
'</ul>')
S.append('<p class="note">Названия моделей и цена — внизу, под ключом-расшифровкой, чтобы не влияли на оценку. '
'Фрагмент Уэллса — англ.→рус.: оригинал перед тобой, судишь и стиль, и верность напрямую.</p>')
S.append("</header>")
for i, (frag, direction, work, is_en) in enumerate(FRAGMENTS, 1):
anchor = read(frag, "source") if is_en else read(frag, "english_ref")
anchor_label = "Оригинал (английский)" if is_en else "Смысловой якорь — дословный английский перевод"
draft = read(frag, "draft")
S.append('<section class="frag">')
S.append(f'<div class="frag-head"><span class="fnum">{i:02d}</span>'
f'<div><p class="eyebrow">{html.escape(direction)}</p>'
f'<h2>{html.escape(work)}</h2></div>'
+ ('<span class="badge">твой полный контроль</span>' if is_en else '') +
'</div>')
S.append('<details class="context"><summary>Смысловой якорь и черновик</summary>'
'<div class="context-grid">')
S.append(f'<div class="ctx"><p class="ctx-label">{html.escape(anchor_label)}</p>'
f'<div class="prose muted">{paras(anchor)}</div></div>')
S.append(f'<div class="ctx"><p class="ctx-label">Черновой перевод (вход редактора)</p>'
f'<div class="prose muted">{paras(draft)}</div></div>')
S.append("</div></details>")
S.append('<div class="versions">')
for letter, model in BLIND:
txt = read(frag, model)
S.append(f'<article class="ver"><header class="ver-head"><span class="chip">{letter}</span></header>'
f'<div class="prose">{paras(txt)}</div></article>')
S.append("</div>")
S.append("</section>")
# ключ-расшифровка + стоимость (свёрнуто)
S.append('<details class="reveal"><summary>Показать расшифровку и стоимость</summary>')
S.append('<table><thead><tr><th>Метка</th><th>Модель</th><th>Латентность / цена редактуры</th></tr></thead><tbody>')
for letter, model in BLIND:
S.append(f'<tr><td class="chip-cell"><span class="chip sm">{letter}</span></td>'
f'<td class="mono">{html.escape(model)}</td><td>{html.escape(COST.get(model, ""))}</td></tr>')
S.append("</tbody></table>")
S.append('<p class="note">Черновик — DeepSeek (на китайском А-Кью DeepSeek «эхал» оригинал, '
'поэтому там черновик от GLM — деталь для бэкенда, на сравнение редакторов не влияет). '
'Латентность — на коротком фрагменте; в продакшене всё идёт батчем.</p>')
S.append("</details>")
S.append('<footer class="foot"><p>Выбор дефолтного редактора для Фазы 1. '
'Твоя оценка — золотой стандарт: LLM-судьи на художественном качестве расходятся с людьми.</p></footer>')
S.append("</div>")
out.write_text("\n".join(S), encoding="utf-8")
print("written", out, out.stat().st_size, "bytes")
CSS = r"""
:root{
--paper:#f7f6f3; --surface:#fffffe; --ink:#1c1b19; --muted:#6f6b64;
--hair:#e4e0d8; --accent:#9e2b25; --accent-soft:#f0e2df;
--style:#3f6f52; --fidel:#9e2b25;
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
--sans:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
--measure:64ch;
}
@media (prefers-color-scheme:dark){
:root{--paper:#17181a; --surface:#1f2124; --ink:#e9e7e2; --muted:#9a978f;
--hair:#31343a; --accent:#d67a72; --accent-soft:#33231f;
--style:#7db38f; --fidel:#d67a72;}
}
:root[data-theme="light"]{--paper:#f7f6f3; --surface:#fffffe; --ink:#1c1b19; --muted:#6f6b64;
--hair:#e4e0d8; --accent:#9e2b25; --accent-soft:#f0e2df; --style:#3f6f52; --fidel:#9e2b25;}
:root[data-theme="dark"]{--paper:#17181a; --surface:#1f2124; --ink:#e9e7e2; --muted:#9a978f;
--hair:#31343a; --accent:#d67a72; --accent-soft:#33231f; --style:#7db38f; --fidel:#d67a72;}
*{box-sizing:border-box;}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
line-height:1.5;-webkit-font-smoothing:antialiased;}
.wrap{max-width:1080px;margin:0 auto;padding:clamp(1.2rem,4vw,3.5rem) clamp(1rem,4vw,2rem) 4rem;}
.eyebrow{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
font-weight:600;margin:0 0 .5rem;}
h1{font-family:var(--serif);font-weight:600;font-size:clamp(1.9rem,5vw,3rem);line-height:1.08;
margin:.1rem 0 .8rem;text-wrap:balance;letter-spacing:-.01em;}
.lede{font-size:1.08rem;max-width:var(--measure);color:var(--ink);margin:0 0 1rem;}
.criteria{list-style:none;padding:0;margin:0 0 1.2rem;max-width:var(--measure);
display:flex;flex-direction:column;gap:.4rem;}
.criteria li{padding-left:.2rem;color:var(--muted);}
.k{display:inline-block;font-family:var(--sans);font-size:.72rem;font-weight:700;text-transform:uppercase;
letter-spacing:.08em;padding:.12em .5em;border-radius:3px;margin-right:.4em;color:#fff;}
.k-style{background:var(--style);} .k-fidel{background:var(--fidel);}
.note{font-size:.9rem;color:var(--muted);max-width:var(--measure);}
.intro{border-bottom:1px solid var(--hair);padding-bottom:2rem;margin-bottom:2.5rem;}
.frag{margin:0 0 3.5rem;}
.frag-head{display:flex;align-items:baseline;gap:1rem;margin-bottom:1.1rem;flex-wrap:wrap;}
.fnum{font-family:var(--serif);font-size:1.5rem;color:var(--accent);font-weight:600;
font-variant-numeric:tabular-nums;line-height:1;}
.frag-head h2{font-family:var(--serif);font-weight:600;font-size:1.4rem;margin:.1rem 0 0;letter-spacing:-.01em;}
.badge{margin-left:auto;align-self:center;font-size:.7rem;font-weight:600;text-transform:uppercase;
letter-spacing:.08em;color:var(--accent);background:var(--accent-soft);
padding:.28em .7em;border-radius:999px;}
.context{margin:0 0 1.3rem;border:1px solid var(--hair);border-radius:10px;background:var(--surface);}
.context>summary{cursor:pointer;padding:.75rem 1rem;font-size:.82rem;font-weight:600;
text-transform:uppercase;letter-spacing:.06em;color:var(--muted);list-style:none;}
.context>summary::-webkit-details-marker{display:none;}
.context>summary::before{content:"";color:var(--accent);}
.context[open]>summary::before{content:"";}
.context-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:0 1rem 1.1rem;}
.ctx-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.07em;color:var(--accent);
font-weight:600;margin:0 0 .4rem;}
@media(max-width:680px){.context-grid{grid-template-columns:1fr;}}
.versions{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem;}
@media(max-width:760px){.versions{grid-template-columns:1fr;}}
.ver{background:var(--surface);border:1px solid var(--hair);border-radius:12px;
padding:1.1rem 1.3rem 1.3rem;position:relative;}
.ver-head{margin:0 0 .5rem;}
.chip{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;
font-family:var(--serif);font-weight:700;font-size:1.05rem;color:#fff;background:var(--accent);
border-radius:50%;}
.chip.sm{width:1.6rem;height:1.6rem;font-size:.85rem;}
.prose{font-family:var(--serif);font-size:1.02rem;line-height:1.62;max-width:var(--measure);}
.prose p{margin:0 0 .75em;}
.prose p:last-child{margin-bottom:0;}
.prose.muted{color:var(--muted);font-size:.94rem;line-height:1.55;}
.reveal{margin:2rem 0 0;border:1px solid var(--hair);border-radius:10px;background:var(--surface);}
.reveal>summary{cursor:pointer;padding:.9rem 1.1rem;font-weight:600;color:var(--accent);list-style:none;}
.reveal>summary::-webkit-details-marker{display:none;}
.reveal>summary::before{content:"🔑 ";}
.reveal table{width:100%;border-collapse:collapse;margin:0 0 .5rem;}
.reveal th,.reveal td{text-align:left;padding:.55rem 1.1rem;border-top:1px solid var(--hair);font-size:.92rem;}
.reveal th{font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;}
.chip-cell{width:3rem;} .mono{font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:.88rem;}
.reveal .note{padding:0 1.1rem 1rem;}
.foot{margin-top:2.5rem;padding-top:1.5rem;border-top:1px solid var(--hair);color:var(--muted);font-size:.9rem;}
a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px;}
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;}}
"""
if __name__ == "__main__":
main()