144 lines
9.1 KiB
Python
144 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
||
"""ТРЕТИЙ КОНТУР — слепое ЧТЕНИЕ выходов. $0 по ключам провайдеров.
|
||
|
||
Зачем нужен. Первая редакция пака мерила качество ровно одним инструментом — судьями по ключам
|
||
(`gpt-5.6-luna`, `grok-4.3`), и когда у этого инструмента нашлась позиционная фора +4.65 ошибки,
|
||
проверить его оказалось нечем: батарея говорит только о механических дефектах, а сам я выходы
|
||
систематически не читал. Один инструмент без второго мнения — это не замер, а доверие.
|
||
|
||
Чем этот контур ОТЛИЧАЕТСЯ от судей, а не дублирует их:
|
||
* другое семейство моделей (читатели — не из семей, населяющих армы, и не из семей судей),
|
||
поэтому общий слепой участок семейства не переносится;
|
||
* другая задача: не «посчитай ошибки по осям», а «прочитай как редактор издательства и назови
|
||
конкретные места цитатой». Счёт ошибок и чтение ловят разное;
|
||
* ранжирование ВСЕХ армов сразу, а не парно — позиционная фора парного протокола сюда
|
||
структурно не переносится, зато появляется своя (порядок в списке), и она гасится
|
||
перестановкой на каждой единице.
|
||
|
||
Слепота. Метки армов снимаются, варианты перемешиваются детерминированным ключом от uid, карта
|
||
раскладки лежит ОТДЕЛЬНЫМ файлом и читателю не выдаётся. Расшифровка — только при сведении.
|
||
|
||
Запуск: --emit разложить пакеты чтения (и карту) по файлам
|
||
--map показать карту раскладки (после чтения)
|
||
"""
|
||
from __future__ import annotations
|
||
import hashlib
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||
|
||
OUT = Path.home() / "books" / "role-topology"
|
||
PACKETS = OUT / "blind"
|
||
# ⚠ Карта раскладки лежит ВНЕ каталога пакетов (починка 07.08 по ревью). Прежде `zh-MAP.json`
|
||
# лежал рядом с 28 пакетами: читателю запрещали её открывать инструкцией, а не расположением.
|
||
# Запрет, который держится только на послушании читателя, слепотой не является.
|
||
KEYS = OUT / "blind-keys"
|
||
|
||
INSTRUCTION = """Ты — редактор русского издательства, готовящий книгу к печати.
|
||
|
||
Ниже ИСХОДНИК и несколько независимых переводов одного и того же фрагмента. Кто их сделал —
|
||
не сообщается намеренно.
|
||
|
||
Прочитай каждый вариант ЦЕЛИКОМ как читатель, а не как чекер. Затем по КАЖДОМУ варианту:
|
||
1. Назови конкретные места, которые не пойдут в печать — ЦИТАТОЙ из варианта (не пересказом).
|
||
Классы, которые важны: искажение смысла исходника; выдуманное или несуществующее русское
|
||
слово; translationese (калька синтаксиса, канцелярит, неестественный порядок слов);
|
||
потеря или подмена фактуры (числа, имена, полярность утверждения); сбой вёрстки и диалога.
|
||
2. Отметь, чем вариант ЛУЧШЕ остальных, если есть чем.
|
||
Затем расставь варианты в порядке пригодности к печати, от лучшего к худшему, и назови ОДИН
|
||
решающий довод за первое место — тоже цитатой.
|
||
|
||
Отвечай строго в таком виде:
|
||
ВАРИАНТ <буква>
|
||
ДЕФЕКТЫ: <класс> — «цитата» | <класс> — «цитата» | ... (или «нет»)
|
||
СИЛЬНОЕ: <одной строкой или «нет»>
|
||
(повторить для каждого варианта)
|
||
РАНГ: <буквы через > , например В > А > Б>
|
||
ДОВОД: <одна фраза с цитатой>
|
||
"""
|
||
|
||
|
||
def shuffle_key(uid: str, arms: list[str]) -> list[str]:
|
||
"""Детерминированная перестановка от uid: воспроизводима и не зависит от порядка армов."""
|
||
h = hashlib.sha1(uid.encode()).digest() # noqa: S324
|
||
return [a for _, a in sorted(zip(h[:len(arms)], sorted(arms)))]
|
||
|
||
|
||
def emit(pack: str) -> None:
|
||
if pack == "zh":
|
||
import bakeoff as M # noqa: PLC0415
|
||
us, arms, prefix = M.units(), ["F", *M.ARMS], "zh"
|
||
else:
|
||
import pair_en as M # noqa: PLC0415
|
||
us, arms, prefix = M.units(), ["F", *M.ARMS], "en"
|
||
PACKETS.mkdir(parents=True, exist_ok=True)
|
||
letters = "АБВГДЕЖЗ"
|
||
written, mapping = 0, {}
|
||
for u in us:
|
||
texts = {a: M.text_of(a, u) for a in arms}
|
||
have = [a for a in shuffle_key(u["uid"], arms) if texts[a].strip()]
|
||
if len(have) < 2:
|
||
continue
|
||
body = [INSTRUCTION, "\n=== ИСХОДНИК ===\n", u["source"]]
|
||
for i, a in enumerate(have):
|
||
body.append(f"\n\n=== ВАРИАНТ {letters[i]} ===\n{texts[a]}")
|
||
mapping.setdefault(u["uid"], {})[letters[i]] = a
|
||
(PACKETS / f"{prefix}-{u['uid']}.txt").write_text("".join(body), encoding="utf-8")
|
||
written += 1
|
||
KEYS.mkdir(parents=True, exist_ok=True)
|
||
(KEYS / f"{prefix}-MAP.json").write_text(json.dumps(mapping, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
print(f"пакетов {written} → {PACKETS}/{prefix}-*.txt · карта в {KEYS} (вне каталога пакетов)")
|
||
|
||
|
||
def score(pack: str) -> None:
|
||
"""Свод чтения по персистированным рангам. $0.
|
||
|
||
⚠ Заведён 07.08 по адверсариальному ревью второй редакции. Прежде модуль умел только
|
||
ВЫПУСКАТЬ пакеты, а ответы читателей нигде не оседали — ранги переносились в свод руками.
|
||
Это нарушение закона промта стр.32 («per-vote персист КАЖДОГО голоса»), рождённого
|
||
ревью-шапкой эксп-20: замер без персиста невоспроизводим. Ранги прошлого прогона
|
||
восстановлены в `READINGS.json`, полные разборы утрачены и требуют ре-рана читателей.
|
||
"""
|
||
import statistics # noqa: PLC0415
|
||
from collections import defaultdict # noqa: PLC0415
|
||
f = KEYS / "READINGS.json"
|
||
if not f.exists():
|
||
print("нет READINGS.json — чтение не персистировано")
|
||
return
|
||
d = json.loads(f.read_text(encoding="utf-8"))
|
||
m = json.loads((KEYS / f"{pack}-MAP.json").read_text(encoding="utf-8"))
|
||
ranks = d[f"ranks_{pack}"]
|
||
pos: dict[str, list[int]] = defaultdict(list)
|
||
wins: dict[str, int] = defaultdict(int)
|
||
for uid, order in ranks.items():
|
||
letters = order.split()
|
||
for i, letter in enumerate(letters):
|
||
pos[m[uid][letter]].append(i + 1)
|
||
wins[m[uid][letters[0]]] += 1
|
||
print(f"СЛЕПОЕ ЧТЕНИЕ {pack} · единиц {len(ranks)}\n")
|
||
print(f"{'арм':7s}{'ср.ранг':>9s}{'первых':>8s}{'медиана':>9s}")
|
||
for arm in sorted(pos, key=lambda a: statistics.mean(pos[a])):
|
||
print(f"{arm:7s}{statistics.mean(pos[arm]):9.2f}{wins[arm]:8d}"
|
||
f"{statistics.median(pos[arm]):9.1f}")
|
||
print(f"\n⚠ {d['provenance']}")
|
||
|
||
|
||
def show_map(pack: str) -> None:
|
||
f = KEYS / f"{pack}-MAP.json"
|
||
print(f.read_text(encoding="utf-8") if f.exists() else "карты нет")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
a = sys.argv[1:] or ["--emit", "zh"]
|
||
if a[0] == "--emit":
|
||
emit(a[1] if len(a) > 1 else "zh")
|
||
elif a[0] == "--score":
|
||
score(a[1] if len(a) > 1 else "zh")
|
||
elif a[0] == "--map":
|
||
show_map(a[1] if len(a) > 1 else "zh")
|
||
else:
|
||
print(__doc__)
|