textmachine/eval/role_topology/blind_read.py

144 lines
9.1 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
"""ТРЕТИЙ КОНТУР — слепое ЧТЕНИЕ выходов. $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__)