202 lines
12 KiB
Python
202 lines
12 KiB
Python
#!/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": "~60–86 с, обязательный 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()
|