textmachine/eval/pkg7/bank_profile.py

169 lines
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
"""Полигон, пакет-8 (замер 1, часть 2): профиль канала БАНКНОТЫ — то, чего recall против сида не видит.
Первая часть замера (`joint_recall.py`) упёрлась в пре-зарегистрированный стоп: банкнота гонялась с
непустым банком, свободных от инъекции термов сида осталось 2 из 51, и совместный recall против
подписи объявлен неизмеримым на существующих артефактах. Этот скрипт добывает то, что ИЗМЕРИМО и на
что цензура не действует:
1. ЦЕНЗУРА ПО ГЛАВАМ. Инъекция глоссария пер-чанковая, а не книжная: терм, инъектированный в главе 2,
в главе 7 модели не показывался. Значит пара (терм, глава) даёт куда больший знаменатель со
свободным шансом, чем терм целиком. Считаем именно пары.
2. ОБЪЕКТИВНЫЙ ПРОФИЛЬ эмиссии банкноты: частота и рассеяние по главам каждой предложенной
поверхности. Это заменяет разметку «годный/мусор» там, где разметка была бы моим вкусом:
пороги берём НЕ свои, а боевые (`emitMinFreq`=5, `runeLen`≥2 из miner_emit.go) — вопрос
ставится как «сколько предложений банкноты прошло бы фильтры, которые движок уже применяет к
майнеру», и ответ на него проверяем кем угодно.
$0. Читает только КОПИИ баз и срез.
"""
from __future__ import annotations
import argparse
import json
import sqlite3
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).parent))
from joint_recall import parse_banknote, split_banknote # noqa: E402 (одна реализация парсера, не две)
EMIT_MIN_FREQ = 5 # miner_emit.go:31 — боевой порог частоты эмиссии
EMIT_MIN_RUNES = 2 # miner_emit.go — боевой порог длины
def norm_keys(harness: Path, strings: list[str]) -> dict[str, str]:
uniq = sorted({s for s in strings if s.strip()})
p = subprocess.run([str(harness), "-mode", "norm"], input="\n".join(uniq),
capture_output=True, text=True, check=True)
out = {}
for line in p.stdout.split("\n"):
if line:
src, _, key = line.partition("\t")
out[src] = key
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--seed", required=True, type=Path)
ap.add_argument("--chunks", required=True, type=Path)
ap.add_argument("--db", required=True, type=Path, help="КОПИЯ базы прогона банкноты (основной)")
ap.add_argument("--harness", required=True, type=Path)
ap.add_argument("--out", type=Path)
a = ap.parse_args()
rows = [json.loads(l) for l in a.chunks.open(encoding="utf-8") if l.strip()]
by_chapter: dict[int, str] = defaultdict(str)
for r in rows:
by_chapter[r["chapter"]] += "\n" + r["source"]
text = "\n".join(by_chapter[c] for c in sorted(by_chapter))
con = sqlite3.connect(f"file:{a.db}?mode=ro", uri=True)
# инъекция по главам
injected_by_ch: dict[int, set[str]] = defaultdict(set)
for ch, blob in con.execute("select chapter, injected_ids from retrieval_state where injected_ids<>''"):
for ident in json.loads(blob):
injected_by_ch[ch].add(ident.split("\x1f")[0])
# банкнота по главам
bank_by_ch: dict[int, list[dict]] = defaultdict(list)
q = ("select j.chapter, cp.response_text, cp.finish_reason from checkpoints cp "
"join jobs j on j.id=cp.job_id where cp.role='translator' and cp.response_text like '%TM-BANK%'")
for ch, resp, finish in con.execute(q):
_, block, malformed = split_banknote(resp or "")
if malformed or not block or finish != "stop":
continue
ents, _ = parse_banknote(block)
bank_by_ch[ch].extend(ents)
con.close()
doc = yaml.safe_load(a.seed.read_text(encoding="utf-8")) or {}
terms = doc.get("terms") or []
strings = [t.get("src") or "" for t in terms]
for t in terms:
strings.extend(al.get("alias", "") for al in (t.get("aliases") or []))
for ents in bank_by_ch.values():
strings.extend(e["src"] for e in ents)
for s in injected_by_ch.values():
strings.extend(s)
nk = norm_keys(a.harness, strings)
bank_keys_by_ch = {ch: {nk.get(e["src"], e["src"]) for e in ents} for ch, ents in bank_by_ch.items()}
inj_keys_by_ch = {ch: {nk.get(s, s) for s in ss} for ch, ss in injected_by_ch.items()}
# --- 1. пары (терм, глава) --------------------------------------------------------------------
pairs_total = pairs_free = pairs_free_hit = pairs_cens = pairs_cens_hit = 0
free_rows = []
for t in terms:
src = t.get("src") or ""
surfaces = [src] + [al.get("alias", "") for al in (t.get("aliases") or []) if al.get("alias")]
keys = {nk.get(s, s) for s in surfaces if s}
for ch in sorted(by_chapter):
body = by_chapter[ch]
if not any(s and s in body for s in surfaces):
continue
pairs_total += 1
hit = bool(keys & bank_keys_by_ch.get(ch, set()))
if keys & inj_keys_by_ch.get(ch, set()):
pairs_cens += 1
pairs_cens_hit += int(hit)
else:
pairs_free += 1
pairs_free_hit += int(hit)
free_rows.append({"src": src, "type": t.get("type", "") or "term", "chapter": ch,
"hit": hit, "occ": sum(body.count(s) for s in surfaces if s)})
print("=== 1. Цензура по парам (терм × глава) ===")
print(f"пар «терм сида встречается в главе»: {pairs_total}")
print(f" инъектирован в этой главе (шанса не было): {pairs_cens}"
f" из них банкнота всё равно предложила: {pairs_cens_hit}")
print(f" НЕ инъектирован (свободный шанс): {pairs_free}"
f" банкнота предложила: {pairs_free_hit}"
+ (f" = {pairs_free_hit / pairs_free:.3f}" if pairs_free else ""))
if free_rows:
print(" свободные пары (топ-15 по вхождениям в главе):")
for r in sorted(free_rows, key=lambda r: -r["occ"])[:15]:
print(f" гл.{r['chapter']:<3d} {r['src']}\t{r['type']}\t{r['occ']}×\t{'НАШЛА' if r['hit'] else ''}")
# --- 2. объективный профиль эмиссии банкноты --------------------------------------------------
all_ents = [e for ents in bank_by_ch.values() for e in ents]
prof = {}
for e in all_ents:
s = e["src"]
if s in prof:
continue
freq = text.count(s)
chs = sum(1 for ch in by_chapter if s in by_chapter[ch])
prof[s] = {"type": e["type"], "freq": freq, "chapters": chs, "runes": len(s)}
n = len(prof)
passes = [s for s, p in prof.items() if p["freq"] >= EMIT_MIN_FREQ and p["runes"] >= EMIT_MIN_RUNES]
multi = [s for s, p in prof.items() if p["chapters"] >= 2]
once = [s for s, p in prof.items() if p["freq"] <= 1]
print("\n=== 2. Объективный профиль эмиссии банкноты (боевые пороги майнера) ===")
print(f"уникальных поверхностей: {n}")
print(f" прошли бы emitMinFreq≥{EMIT_MIN_FREQ} и runeLen≥{EMIT_MIN_RUNES}: {len(passes)} = {len(passes)/n:.3f}")
print(f" встречаются в ≥2 главах: {len(multi)} = {len(multi)/n:.3f}")
print(f" встречаются в срезе ≤1 раза (разовые): {len(once)} = {len(once)/n:.3f}")
print("\n прошедшие оба боевых порога:")
for s in sorted(passes, key=lambda s: -prof[s]["freq"]):
p = prof[s]
print(f" {s}\t{p['type']}\t{p['freq']}×\tглав:{p['chapters']}")
if a.out:
a.out.write_text(json.dumps({
"pairs": {"total": pairs_total, "censored": pairs_cens, "censored_hit": pairs_cens_hit,
"free": pairs_free, "free_hit": pairs_free_hit},
"free_rows": free_rows, "profile": prof,
}, ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())