Add polygon eval scripts for local-model, editor and MoE benchmarks plus ollama launcher and provider config
This commit is contained in:
parent
1985c16d23
commit
ce71a1a752
6 changed files with 460 additions and 6 deletions
202
eval/build_editor_artifact.py
Normal file
202
eval/build_editor_artifact.py
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
#!/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()
|
||||||
164
eval/editor_bench.py
Normal file
164
eval/editor_bench.py
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Эксперимент 04: бейк-офф редакторов ru-стиля (задача из PROGRESS, вопрос бэкенда №2).
|
||||||
|
|
||||||
|
Пайплайн: DeepSeek делает ЧЕРНОВИК перевода → каждый редактор-кандидат полирует его
|
||||||
|
в живой литературный русский (чек-лист Норы Галь: убрать канцелярит, кальки, оживить
|
||||||
|
синтаксис), НЕ меняя смысл и не теряя предложений. Выходы сохраняются для сравнения
|
||||||
|
(человеческая оценка владельца — золотой стандарт для худ. качества, LAIT).
|
||||||
|
|
||||||
|
Запуск: eval/.venv/bin/python eval/editor_bench.py
|
||||||
|
Выход: eval/data/editor_bench/<fragment>/{source,draft,<editor>}.txt + results.json
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CJK_RE = re.compile("[一-鿿-ヿ]")
|
||||||
|
|
||||||
|
|
||||||
|
def cjk_share(text: str) -> float:
|
||||||
|
return len(CJK_RE.findall(text)) / max(1, len(text))
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from refusal_bench import call_provider, load_env_file # noqa: E402
|
||||||
|
|
||||||
|
load_env_file()
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
# Черновик — реальный черновой контур пайплайна
|
||||||
|
DRAFT = {"name": "deepseek", "base_url": "https://api.deepseek.com/v1",
|
||||||
|
"model": "deepseek-chat", "api_key_env": "DEEPSEEK_API_KEY",
|
||||||
|
"temperature": 0.3, "max_tokens": 3000}
|
||||||
|
# Фолбэк-черновик, если основной «эхнул» оригинал (deepseek-chat так делает на плотной zh-прозе)
|
||||||
|
DRAFT_FALLBACK = {"name": "glm-draft", "base_url": "https://api.z.ai/api/paas/v4",
|
||||||
|
"model": "glm-4.6", "api_key_env": "ZAI_API_KEY", "temperature": 0.3,
|
||||||
|
"max_tokens": 4000, "extra_body": {"thinking": {"type": "disabled"}}}
|
||||||
|
|
||||||
|
# Редакторы-кандидаты (дорогой тир; имена моделей сверены с /models 2026-07-04)
|
||||||
|
EDITORS = [
|
||||||
|
{"name": "glm-4.6", "base_url": "https://api.z.ai/api/paas/v4", "model": "glm-4.6",
|
||||||
|
"api_key_env": "ZAI_API_KEY", "extra_body": {"thinking": {"type": "disabled"}}},
|
||||||
|
{"name": "kimi-k2.6", "base_url": "https://api.moonshot.ai/v1", "model": "kimi-k2.6",
|
||||||
|
"api_key_env": "KIMI_API_KEY", "temperature": 1, "max_tokens": 16000},
|
||||||
|
# Kimi K2.6: только temp=1; ОЧЕНЬ многословная думалка (~11k reasoning-токенов на абзац!) →
|
||||||
|
# бюджет ≥16000, иначе content пуст (finish=length). Дорогой редактор: reasoning биллится как выход
|
||||||
|
{"name": "gemini-3.1-pro", "base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||||
|
"model": "gemini-3.1-pro-preview", "api_key_env": "GEMINI_API_KEY",
|
||||||
|
"max_tokens": 16000}, # обязательно-думающая: thinking не отключить; на длинном черновике 8k обрезал ответ
|
||||||
|
{"name": "grok-4.3", "base_url": "https://api.x.ai/v1", "model": "grok-4.3",
|
||||||
|
"api_key_env": "XAI_API_KEY"},
|
||||||
|
]
|
||||||
|
for e in EDITORS:
|
||||||
|
e.setdefault("temperature", 0.4)
|
||||||
|
e.setdefault("max_tokens", 4000)
|
||||||
|
|
||||||
|
# Фрагменты: (id, lang, файл, срез) — разные исходники и стили.
|
||||||
|
# wells — en→ru: владелец читает английский, поэтому оценит и стиль, И верность на 100%.
|
||||||
|
FRAGMENTS = [
|
||||||
|
("rashomon", "японского", "ja/akutagawa-rashomon.txt", 1400),
|
||||||
|
("ahq", "китайского", "zh/luxun-ah-q-ch1-4.txt", 1300), # нарративнее zhufu (тот deepseek эхает)
|
||||||
|
("merosu", "японского", "ja/dazai-hashire-merosu.txt", 1400),
|
||||||
|
("wells", "английского", "en/wells-time-machine-ch1-2.txt", 1400),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Английский эталон-подстрочник для ja/zh — читаемый владельцем якорь смысла
|
||||||
|
# (сверить верность русских версий). Для en-исходника не нужен (сам оригинал английский).
|
||||||
|
ENREF_SYS = ("Translate this fiction fragment from {src} into faithful, literal English. "
|
||||||
|
"Preserve every sentence and all proper nouns/cultural terms. Output only the translation.")
|
||||||
|
|
||||||
|
DRAFT_SYS = ("Ты переводчик. Переведи фрагмент художественной прозы с {src} на русский "
|
||||||
|
"буквально и точно, сохрани все предложения. Выведи только перевод.")
|
||||||
|
|
||||||
|
EDITOR_SYS = (
|
||||||
|
"Ты — литературный редактор русского художественного перевода уровня издательства. "
|
||||||
|
"Тебе дают ОРИГИНАЛ (на {src}) и его ЧЕРНОВОЙ подстрочный перевод. Отредактируй "
|
||||||
|
"русский текст в живую художественную прозу: убери канцелярит и кальки с оригинала, "
|
||||||
|
"разнообразь синтаксис, добавь естественную русскую идиоматику и ритм — по духу школы "
|
||||||
|
"Норы Галь. СТРОГО сохрани смысл, все предложения, реалии и имена собственные из черновика. "
|
||||||
|
"Не добавляй отсебятины. Выведи ТОЛЬКО отредактированный русский текст, без комментариев."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_frag(rel: str, n: int) -> str:
|
||||||
|
return (ROOT / "data" / "samples" / rel).read_text()[:n]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
out_dir = ROOT / "data" / "editor_bench"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for fid, src_name, rel, n in FRAGMENTS:
|
||||||
|
fdir = out_dir / fid
|
||||||
|
fdir.mkdir(exist_ok=True)
|
||||||
|
source = read_frag(rel, n)
|
||||||
|
(fdir / "source.txt").write_text(source)
|
||||||
|
print(f"\n=== {fid} ({src_name}) ===")
|
||||||
|
|
||||||
|
# 0. английский эталон смысла (для ja/zh; владелец сверяет верность по нему)
|
||||||
|
enref_path = fdir / "english_ref.txt"
|
||||||
|
if src_name != "английского" and not (enref_path.exists() and enref_path.read_text().strip()):
|
||||||
|
en_src = {"src": src_name.replace("японского", "Japanese").replace("китайского", "Chinese")}
|
||||||
|
text, err, _ = call_provider(DRAFT, ENREF_SYS.format(**en_src), source, timeout=180)
|
||||||
|
if text:
|
||||||
|
enref_path.write_text(text.strip())
|
||||||
|
print(f" english_ref: {len(text)} симв")
|
||||||
|
|
||||||
|
# 1. черновик DeepSeek
|
||||||
|
draft_path = fdir / "draft.txt"
|
||||||
|
if draft_path.exists() and draft_path.read_text().strip():
|
||||||
|
draft = draft_path.read_text()
|
||||||
|
print(" draft: из кэша")
|
||||||
|
else:
|
||||||
|
draft = None
|
||||||
|
for attempt in range(3): # ретрай на «эхо» (модель вернула оригинал вместо перевода)
|
||||||
|
t0 = time.time()
|
||||||
|
cand, err, usage = call_provider(
|
||||||
|
DRAFT, DRAFT_SYS.format(src=src_name), source, timeout=180)
|
||||||
|
if not cand:
|
||||||
|
print(f" draft ОШИБКА: {err[:120]}"); break
|
||||||
|
if cjk_share(cand) > 0.15:
|
||||||
|
print(f" draft эхо ({cjk_share(cand):.0%} CJK), ретрай {attempt+1}")
|
||||||
|
continue
|
||||||
|
draft = cand
|
||||||
|
draft_path.write_text(draft)
|
||||||
|
print(f" draft ({DRAFT['name']}): {time.time()-t0:.0f}с, {len(draft)} симв")
|
||||||
|
break
|
||||||
|
if not draft: # основной эхает стабильно → фолбэк-черновик (GLM)
|
||||||
|
cand, err, _ = call_provider(
|
||||||
|
DRAFT_FALLBACK, DRAFT_SYS.format(src=src_name), source, timeout=180)
|
||||||
|
if cand and cjk_share(cand) <= 0.15:
|
||||||
|
draft = cand
|
||||||
|
draft_path.write_text(draft)
|
||||||
|
print(f" draft (фолбэк {DRAFT_FALLBACK['name']}): {len(draft)} симв")
|
||||||
|
else:
|
||||||
|
print(" draft не удался даже фолбэком — фрагмент пропущен"); continue
|
||||||
|
|
||||||
|
# 2. каждый редактор полирует черновик
|
||||||
|
user = f"ОРИГИНАЛ ({src_name}):\n{source}\n\nЧЕРНОВОЙ ПЕРЕВОД:\n{draft}"
|
||||||
|
for ed in EDITORS:
|
||||||
|
ep = fdir / f"{ed['name']}.txt"
|
||||||
|
if ep.exists() and ep.read_text().strip():
|
||||||
|
print(f" {ed['name']}: из кэша"); continue
|
||||||
|
t0 = time.time()
|
||||||
|
text, err, usage = call_provider(ed, EDITOR_SYS.format(src=src_name), user, timeout=300)
|
||||||
|
if not text:
|
||||||
|
print(f" {ed['name']}: ОШИБКА/пусто — {err or 'empty'}")
|
||||||
|
results.append({"fragment": fid, "editor": ed["name"], "error": err or "empty"})
|
||||||
|
continue
|
||||||
|
ep.write_text(text.strip())
|
||||||
|
el = time.time() - t0
|
||||||
|
results.append({"fragment": fid, "editor": ed["name"], "elapsed_s": round(el, 1),
|
||||||
|
"chars": len(text), "usage": usage})
|
||||||
|
print(f" {ed['name']}: {el:.0f}с, {len(text)} симв")
|
||||||
|
|
||||||
|
(out_dir / "results.json").write_text(json.dumps(results, ensure_ascii=False, indent=1))
|
||||||
|
print(f"\nГотово. Выходы: {out_dir}/<fragment>/*.txt")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
43
eval/llama_moe_bench.sh
Normal file
43
eval/llama_moe_bench.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Замер 30b-a3b MoE через llama-server с офлоадом экспертов на CPU (--n-cpu-moe / --cpu-moe).
|
||||||
|
# Сравнение с ollama (дал ~10 tok/s). Использование: bash eval/llama_moe_bench.sh [NCMOE]
|
||||||
|
# NCMOE пусто → --cpu-moe (все эксперты на CPU); число → --n-cpu-moe N (первые N слоёв).
|
||||||
|
set -u
|
||||||
|
BIN=/home/ubuntu/llama.cpp/build/bin/llama-server
|
||||||
|
GGUF=/home/ubuntu/.ollama/models/blobs/sha256-11b3b371aedb6c43d438ec010e66f9dfaa92127d64c22499891d9013ab7a6ee6
|
||||||
|
PORT=8080
|
||||||
|
LOG=/home/ubuntu/llama-moe.log
|
||||||
|
export NO_PROXY="localhost,127.0.0.1" no_proxy="localhost,127.0.0.1"
|
||||||
|
NCMOE_ARG="--cpu-moe"
|
||||||
|
[ $# -ge 1 ] && NCMOE_ARG="--n-cpu-moe $1"
|
||||||
|
|
||||||
|
pkill -f "llama-server" 2>/dev/null || true; sleep 2
|
||||||
|
echo "== старт llama-server: -ngl 99 $NCMOE_ARG (лог $LOG) =="
|
||||||
|
setsid nohup "$BIN" -m "$GGUF" -ngl 99 $NCMOE_ARG -c 8192 -fa on \
|
||||||
|
--host 127.0.0.1 --port $PORT --no-webui >"$LOG" 2>&1 < /dev/null &
|
||||||
|
|
||||||
|
# ждём готовности (загрузка 18GB весов — до ~90с)
|
||||||
|
for _ in $(seq 1 180); do
|
||||||
|
curl -s --noproxy '*' --max-time 1 "http://127.0.0.1:$PORT/health" 2>/dev/null | grep -q '"ok"' && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if ! curl -s --noproxy '*' --max-time 2 "http://127.0.0.1:$PORT/health" | grep -q '"ok"'; then
|
||||||
|
echo "СЕРВЕР НЕ ПОДНЯЛСЯ:"; tail -15 "$LOG"; exit 1
|
||||||
|
fi
|
||||||
|
echo "== VRAM после загрузки =="; nvidia-smi --query-gpu=memory.used --format=csv,noheader
|
||||||
|
|
||||||
|
# Нарезка ПОСИМВОЛЬНО (не head -c — тот режет по байтам и рубит иероглиф → битый JSON)
|
||||||
|
BODY=$(/home/ubuntu/projects/textmachine/eval/.venv/bin/python -c '
|
||||||
|
import json
|
||||||
|
sys_p = "Ты профессиональный литературный переводчик с китайского на русский. Переведи фрагмент, сохрани все предложения. Выведи только перевод."
|
||||||
|
frag = open("/home/ubuntu/projects/textmachine/eval/data/samples/zh/luxun-zhufu.txt", encoding="utf-8").read()[:1500]
|
||||||
|
print(json.dumps({"model":"local","temperature":0.3,"max_tokens":2000,
|
||||||
|
"messages":[{"role":"system","content":sys_p},{"role":"user","content":frag}]}))')
|
||||||
|
|
||||||
|
echo "== генерация (zh→ru, 1500 знаков) =="
|
||||||
|
curl -s --noproxy '*' --max-time 600 "http://127.0.0.1:$PORT/v1/chat/completions" \
|
||||||
|
-H "Content-Type: application/json" -d "$BODY" > /home/ubuntu/llama-moe-resp.json
|
||||||
|
echo "-- tok/s из лога llama-server --"; grep -iE "prompt eval time|eval time|tokens per second" "$LOG" | tail -6
|
||||||
|
echo "-- первые 400 симв. перевода --"
|
||||||
|
/home/ubuntu/projects/textmachine/eval/.venv/bin/python -c "import json; print(json.load(open('/home/ubuntu/llama-moe-resp.json'))['choices'][0]['message']['content'][:400])"
|
||||||
|
pkill -f "llama-server" 2>/dev/null || true
|
||||||
|
|
@ -19,6 +19,7 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -95,16 +96,34 @@ class PeakSampler(threading.Thread):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
def ollama_generate(model: str, prompt: str, num_ctx: int, timeout: int = 1800) -> dict:
|
def ollama_generate(model: str, prompt: str, num_ctx: int, timeout: int = 1800,
|
||||||
|
think: bool = False) -> dict:
|
||||||
|
# think=False (дефолт): у моделей 3.5/GLM thinking включён по умолчанию и съедает
|
||||||
|
# бюджет вывода на рассуждения (перевод обрезается/пустой) — для роли переводчика
|
||||||
|
# отключаем. think=True: явно включаем + большой num_predict, чтобы модель успела
|
||||||
|
# и порассуждать, и выдать перевод (замер влияния reasoning на качество).
|
||||||
|
# num_predict — потолок вывода, чтобы модель не убегала.
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{OLLAMA}/api/generate",
|
f"{OLLAMA}/api/generate",
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
"model": model, "prompt": prompt, "stream": False,
|
"model": model, "prompt": prompt, "stream": False, "think": think,
|
||||||
"options": {"num_ctx": num_ctx, "temperature": 0.3},
|
"options": {"num_ctx": num_ctx, "temperature": 0.3,
|
||||||
|
"num_predict": 6000 if think else 2048},
|
||||||
}).encode(),
|
}).encode(),
|
||||||
headers={"Content-Type": "application/json"})
|
headers={"Content-Type": "application/json"})
|
||||||
with _NO_PROXY_OPENER.open(req, timeout=timeout) as resp:
|
try:
|
||||||
return json.loads(resp.read())
|
with _NO_PROXY_OPENER.open(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
# не все модели принимают think=False (нет thinking-способности) — повтор без него
|
||||||
|
if e.code == 400:
|
||||||
|
body = json.loads(req.data)
|
||||||
|
body.pop("think", None)
|
||||||
|
req2 = urllib.request.Request(f"{OLLAMA}/api/generate", data=json.dumps(body).encode(),
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
with _NO_PROXY_OPENER.open(req2, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def strip_think(text: str) -> str:
|
def strip_think(text: str) -> str:
|
||||||
|
|
@ -138,6 +157,8 @@ def main() -> None:
|
||||||
ap.add_argument("--models", help="через запятую; по умолчанию все установленные")
|
ap.add_argument("--models", help="через запятую; по умолчанию все установленные")
|
||||||
ap.add_argument("--num-ctx", type=int, default=8192)
|
ap.add_argument("--num-ctx", type=int, default=8192)
|
||||||
ap.add_argument("--out", default=str(ROOT / "data" / "local_bench"))
|
ap.add_argument("--out", default=str(ROOT / "data" / "local_bench"))
|
||||||
|
ap.add_argument("--think", action="store_true",
|
||||||
|
help="включить thinking-режим (reasoning) + большой num_predict — замер влияния на качество")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
models = args.models.split(",") if args.models else installed_models()
|
models = args.models.split(",") if args.models else installed_models()
|
||||||
|
|
@ -160,7 +181,7 @@ def main() -> None:
|
||||||
sampler.start()
|
sampler.start()
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
try:
|
try:
|
||||||
r = ollama_generate(model, prompt, args.num_ctx)
|
r = ollama_generate(model, prompt, args.num_ctx, think=args.think)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sampler.stop_flag = True
|
sampler.stop_flag = True
|
||||||
print(f" {fname}: ОШИБКА {type(e).__name__}: {str(e)[:200]}")
|
print(f" {fname}: ОШИБКА {type(e).__name__}: {str(e)[:200]}")
|
||||||
|
|
|
||||||
16
eval/ollama-up.sh
Normal file
16
eval/ollama-up.sh
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Запуск ollama в боевом конфиге стенда (как в ~/vojo-local-up.sh, без туннеля/релея).
|
||||||
|
# Флаги: держать модель в VRAM, flash attention, q8 KV-кэш. Прокси для localhost — в обход.
|
||||||
|
# Использование: bash eval/ollama-up.sh
|
||||||
|
set -u
|
||||||
|
LOG="${OLLAMA_LOG:-$HOME/ollama.log}"
|
||||||
|
pkill -f "ollama serve" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
export OLLAMA_HOST=0.0.0.0:11434 OLLAMA_KEEP_ALIVE=-1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0
|
||||||
|
export NO_PROXY="localhost,127.0.0.1,0.0.0.0,::1" no_proxy="localhost,127.0.0.1,0.0.0.0,::1"
|
||||||
|
setsid nohup ollama serve >"$LOG" 2>&1 < /dev/null &
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
curl -s --noproxy '*' --max-time 1 http://127.0.0.1:11434/api/version >/dev/null 2>&1 && break
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
curl -s --noproxy '*' http://127.0.0.1:11434/api/version && echo " <- ollama up (FA=1, KV=q8_0, keep_alive=-1); log: $LOG"
|
||||||
|
|
@ -33,6 +33,14 @@
|
||||||
"extra_body": { "thinking": { "type": "disabled" } },
|
"extra_body": { "thinking": { "type": "disabled" } },
|
||||||
"_comment": "thinking у GLM-4.6 включён по умолчанию и не влезал в 180с — для перевода отключаем"
|
"_comment": "thinking у GLM-4.6 включён по умолчанию и не влезал в 180с — для перевода отключаем"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "kimi",
|
||||||
|
"base_url": "https://api.moonshot.ai/v1",
|
||||||
|
"model": "kimi-k2.6",
|
||||||
|
"api_key_env": "KIMI_API_KEY",
|
||||||
|
"rps_delay": 0.5,
|
||||||
|
"_comment": "Moonshot Kimi K2.6 — кандидат в редакторы zh-исходников (Р4); интернациональный endpoint api.moonshot.ai (не .cn)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "gemini",
|
"name": "gemini",
|
||||||
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
|
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue