textmachine/eval/editor_contract/offblock.py

120 lines
5.9 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
"""Метрика «дрейф ПОДПИСАННЫХ сущностей ВНЕ инъецированного блока» — класс 花酒行者.
Найдена пробой 18 (`docs/experiments/18-editor-wire-probe.md`): инъекция селективна и ограничена
бюджетом 800 токенов, поэтому блок несёт лишь то, что сработало в чанке. Всё остальное подписанное
не защищено ничем — а редактор его переписывает. Здесь это считается числом.
Знаменатель — ПОДПИСАННЫЕ (approved, `gold.jsonl` in_bank) строки, чей src встречается в исходнике
окна и которых НЕТ в тест-блоке пробы 18. Скорится присутствие канонической формы в выходе.
$0: работает по уже персистированному сырью.
"""
from __future__ import annotations
import json
import re
import sys
import unicodedata
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
from inject_probe import pick_windows, TERMS # noqa: E402
GOLD = {r["bank_src"]: r["gold_dst"]
for r in (json.loads(l) for l in open(REPO / "eval/bank_arbitration/gold/gold.jsonl", encoding="utf-8"))
if r.get("in_bank")}
# Стем-регексы канонических форм. КОНСЕРВАТИВНО: скорим только строки, у которых форма
# опознаётся устойчивым стемом. Отброшенные строки печатаются — знаменатель честный.
STEM = {
"方源": r"фан\s*юан",
"方正": r"фан\s*чжэн",
"古月": r"гуюэ", # «Гу Юэ» раздельно НЕ матчится — это и есть дрейф
"古月漠北": r"гуюэ\s*мобэй",
"蛊虫": r"гу-черв",
"本命蛊": r"гу\s*жизни",
"希望蛊": r"гу\s*надежды",
"酒虫": r"винн\w*\s*черв",
"家老": r"старейшин",
"花酒行者": r"монах\s*цветочного\s*вина",
"青茅山": r"гор\w*\s*цинмао",
"凡人": r"смертн",
"一转": r"перв\w*\s*ранг",
"三转": r"трет\w*\s*ранг",
"甲等": r"класс\w*\s+а\b",
"乙等": r"класс\w*\s+б\b",
"丙等": r"класс\w*\s+в\b",
"丁等": r"класс\w*\s+г\b",
}
# Не скорятся: 转 «ранг» (слишком общая форма — ложные срабатывания внутри «первый ранг»),
# 炼化 «очистить и подчинить» (глагольная перифраза, устойчивого стема нет).
DROPPED = {"", "炼化"}
def norm(s: str) -> str:
return " ".join(unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е").split())
def offblock_cells(block_terms: set[str] | None = None):
"""[(окно, src, gold_dst, regex)] — подписанные строки окна вне блока."""
block = set(TERMS) if block_terms is None else block_terms
out = []
for k, (_, buf, _) in enumerate(pick_windows()):
for src, dst in GOLD.items():
if src in block or src in DROPPED or src not in STEM:
continue
if src in buf:
out.append((k, src, dst, STEM[src]))
return out
def score(tags: dict[str, str], label_of=lambda k: None):
"""tags: {метка арма: шаблон тега с {k}}. Печатает долю канонических форм вне блока."""
cells = offblock_cells()
print(f"знаменатель: {len(cells)} клеток (подписанные строки в исходнике окна, ВНЕ блока), "
f"{len({c[1] for c in cells})} уникальных сущностей")
print(f"не скорятся (нет устойчивого стема): {', '.join(DROPPED)}\n")
rows = {}
for arm, pat in tags.items():
hit = tot = 0
misses = []
for k, src, dst, rx in cells:
f = RAW / (pat.format(k=k) + ".json")
if not f.exists():
continue
out = norm(json.load(open(f, encoding="utf-8"))["content"])
tot += 1
if re.search(rx, out):
hit += 1
else:
misses.append(f"w{k}:{src}«{dst}»")
rows[arm] = (hit, tot, misses)
pct = f"{100*hit/tot:.0f}%" if tot else ""
print(f" {arm:34s} {hit:3d}/{tot:<3d} ({pct})")
return rows
if __name__ == "__main__":
print("=== ЧЕРНОВИК (что редактор получил на вход) ===")
cells = offblock_cells()
hit = 0
dmiss = []
for k, src, dst, rx in cells:
d = norm(json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))["content"])
if re.search(rx, d):
hit += 1
else:
dmiss.append(f"w{k}:{src}«{dst}»")
print(f" канон в черновике: {hit}/{len(cells)}")
print(f" чего в черновике не было: {', '.join(dmiss)}\n")
print("=== ВЫХОД РЕДАКТОРА ===")
rows = score({
"A0 full-regen БЕЗ блока": "edp-A0-w{k}",
"A5 full-regen БЕЗ блока (2-й розыгр.)": "edp-A5-w{k}",
"A1 full-regen + закон-блок": "edp-A1-w{k}",
"A2 full-regen + блок (неверная 元海)": "edp-A2-w{k}",
})
print("\n=== что именно потеряно (арм A1, блок есть, но эти строки в него не попали) ===")
for m in rows["A1 full-regen + закон-блок"][2]:
print(" ", m)