120 lines
5.9 KiB
Python
120 lines
5.9 KiB
Python
#!/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)
|