181 lines
8.2 KiB
Python
181 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
||
"""exp15 — extract A0/A0' from the real backend DBs+logs and compute:
|
||
- per-chunk SOURCE (recovered from draft user-msg heads), DRAFT, FINAL (editor) texts
|
||
- greedy cut offsets (A0 boundaries — the Q1 common-denominator boundaries) + est_out distribution
|
||
- deterministic KPI per whole-anchor (mean_spp, 1-sent-para frac, dialogue-dash frac, CJK-leak,
|
||
glossary/term consistency vs seed)
|
||
- A0<->A0' deterministic divergence (chunk count, boundary offsets, KPI deltas, term consistency,
|
||
char-level similarity) = PRELIMINARY noise floor (the judged trap floor is measured with the judges)
|
||
|
||
Chunk SOURCE recovery reuses validate_injection.recover_chunk_sources (draft user-msg head -> offset in
|
||
S2'.txt). FINAL text = the edit-stage response_text per chunk (the authoritative editor output).
|
||
"""
|
||
from __future__ import annotations
|
||
import json
|
||
import re
|
||
import sqlite3
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from validate_injection import load_exchanges, recover_chunk_sources
|
||
from chunker import est_out, est_tokens
|
||
import regex
|
||
|
||
S2 = Path("/home/ubuntu/books/gu-zhenren/exp15/S2prime.txt")
|
||
_RE_HAN = regex.compile(r"\p{Han}")
|
||
_RE_CYR = regex.compile(r"[А-Яа-яЁё]")
|
||
|
||
|
||
def final_texts(db):
|
||
"""chunk_idx -> editor (final) response_text, latest attempt. S2' is one chapter."""
|
||
con = sqlite3.connect(db)
|
||
rows = con.execute(
|
||
"SELECT chunk_idx, attempt, response_text FROM checkpoints WHERE stage='edit' ORDER BY chunk_idx, attempt"
|
||
).fetchall()
|
||
con.close()
|
||
out = {}
|
||
for ck, att, txt in rows:
|
||
out[ck] = txt # later attempt overwrites
|
||
return out
|
||
|
||
|
||
def draft_texts(db):
|
||
con = sqlite3.connect(db)
|
||
rows = con.execute(
|
||
"SELECT chunk_idx, attempt, response_text FROM checkpoints WHERE stage='draft' ORDER BY chunk_idx, attempt"
|
||
).fetchall()
|
||
con.close()
|
||
out = {}
|
||
for ck, att, txt in rows:
|
||
out[ck] = txt
|
||
return out
|
||
|
||
|
||
def load_anchor(db, log):
|
||
src_text = S2.read_text(encoding="utf-8")
|
||
exchanges = load_exchanges(Path(log))
|
||
srcs = recover_chunk_sources(exchanges, src_text, whole_file=False) # {(chap,ck): source}
|
||
finals = final_texts(db)
|
||
drafts = draft_texts(db)
|
||
chunks = []
|
||
for (chap, ck) in sorted(srcs.keys()):
|
||
src = srcs[(chap, ck)]
|
||
chunks.append({"chapter": chap, "chunk": ck, "source": src,
|
||
"draft": drafts.get(ck), "final": finals.get(ck),
|
||
"src_est_out": est_out(src) if src else None,
|
||
"src_est_tokens": est_tokens(src) if src else None})
|
||
return chunks, src_text
|
||
|
||
|
||
def cut_offsets(chunks):
|
||
"""Rune offsets in S2' where each greedy cut falls (chunk starts, excluding 0)."""
|
||
offs, cur = [], 0
|
||
for c in chunks:
|
||
if c["source"] is None:
|
||
return None
|
||
if cur > 0:
|
||
offs.append(cur)
|
||
cur += len(c["source"])
|
||
return offs
|
||
|
||
|
||
# --- deterministic KPI ------------------------------------------------------------------------------
|
||
def paragraphs(text):
|
||
return [p for p in re.split(r"\n\s*\n|\n", text) if p.strip()]
|
||
|
||
|
||
def sentences_ru(text):
|
||
return [s for s in re.split(r"(?<=[.!?…])\s+", text) if s.strip()]
|
||
|
||
|
||
def kpi(final_text):
|
||
paras = paragraphs(final_text)
|
||
spp = [len(sentences_ru(p)) for p in paras] or [0]
|
||
mean_spp = sum(spp) / len(spp)
|
||
one_sent = sum(1 for x in spp if x == 1) / len(spp) if spp else 0
|
||
dash_paras = sum(1 for p in paras if p.lstrip().startswith(("—", "–", "-")))
|
||
dash_frac = dash_paras / len(paras) if paras else 0
|
||
# CJK leak: Han runes in the RU output NOT immediately after a Cyrillic word in parens gloss
|
||
# (owner decision #3: «Кулак Ло Ханя (羅漢拳)» balanced Han-in-parens is NOT a leak). Crude: count
|
||
# Han runes that are inside parentheses -> whitelisted; Han outside parens -> leak.
|
||
han_total = len(_RE_HAN.findall(final_text))
|
||
han_in_parens = 0
|
||
for m in re.finditer(r"[((]([^))]*)[))]", final_text):
|
||
han_in_parens += len(_RE_HAN.findall(m.group(1)))
|
||
han_leak = han_total - han_in_parens
|
||
return {"n_paras": len(paras), "mean_spp": round(mean_spp, 2), "one_sent_para_frac": round(one_sent, 3),
|
||
"dialogue_dash_frac": round(dash_frac, 3), "han_total": han_total,
|
||
"han_in_parens": han_in_parens, "han_leak_outside_parens": han_leak,
|
||
"ru_chars": len(_RE_CYR.findall(final_text))}
|
||
|
||
|
||
def term_consistency(final_text, seed_path):
|
||
"""LTCR-style: for each approved seed term present in S2', is its canonical dst (or a decl form)
|
||
present in the final? Reports coverage and any missing (a drift/omission signal, deterministic)."""
|
||
import yaml
|
||
d = yaml.safe_load(Path(seed_path).read_text(encoding="utf-8"))
|
||
src_text = S2.read_text(encoding="utf-8")
|
||
hits, miss = [], []
|
||
for t in d["terms"]:
|
||
src = t.get("src"); dst = (t.get("dst") or "").strip()
|
||
if not src or not dst or (t.get("status") or "approved") != "approved":
|
||
continue
|
||
variants = [src] + [a.get("alias") for a in (t.get("aliases") or []) if a.get("alias")]
|
||
if not any(v in src_text for v in variants):
|
||
continue
|
||
forms = set([dst] + list((t.get("decl") or {}).get("forms") or []))
|
||
# match the distinctive first content token of the dst (case-insensitive)
|
||
present = any(f.lower() in final_text.lower() for f in forms)
|
||
if not present:
|
||
# fallback: distinctive stem (first word >=4 chars)
|
||
toks = [w for w in re.findall(r"[А-Яа-яЁё]+", dst) if len(w) >= 4]
|
||
present = any(w.lower()[:5] in final_text.lower() for w in toks)
|
||
(hits if present else miss).append(dst)
|
||
return {"n_terms_in_source": len(hits) + len(miss), "present": len(hits),
|
||
"missing": miss, "coverage": round(len(hits) / (len(hits) + len(miss)), 3) if (hits or miss) else None}
|
||
|
||
|
||
def main():
|
||
seed = "/home/ubuntu/books/gu-zhenren/guzhenren-seed-v2.yaml"
|
||
AH = "/home/ubuntu/books/gu-zhenren/exp15/anchors"
|
||
out = {}
|
||
for tag in ("flat_a0", "flat_a0prime"):
|
||
chunks, _ = load_anchor(f"{AH}/{tag}.db", f"{AH}/{tag}.log.json")
|
||
whole_final = "\n\n".join(c["final"] for c in chunks if c["final"])
|
||
offs = cut_offsets(chunks)
|
||
src_outs = [c["src_est_out"] for c in chunks if c["src_est_out"]]
|
||
out[tag] = {
|
||
"n_chunks": len(chunks),
|
||
"cut_offsets": offs,
|
||
"src_est_out": {"mean": round(sum(src_outs) / len(src_outs), 1), "min": round(min(src_outs), 1),
|
||
"max": round(max(src_outs), 1), "per_chunk": [round(x, 1) for x in src_outs]},
|
||
"kpi": kpi(whole_final),
|
||
"term_consistency": term_consistency(whole_final, seed),
|
||
"final_chars": len(whole_final),
|
||
}
|
||
Path(f"{AH}/{tag}.final.txt").write_text(whole_final, encoding="utf-8")
|
||
Path(f"{AH}/{tag}.chunks.json").write_text(
|
||
json.dumps([{k: c[k] for k in ("chunk", "source", "draft", "final", "src_est_out")} for c in chunks],
|
||
ensure_ascii=False, indent=2), encoding="utf-8")
|
||
# A0<->A0' deterministic floor
|
||
a, b = out["flat_a0"], out["flat_a0prime"]
|
||
floor = {
|
||
"same_chunk_count": a["n_chunks"] == b["n_chunks"],
|
||
"same_cut_offsets": a["cut_offsets"] == b["cut_offsets"],
|
||
"n_chunks": [a["n_chunks"], b["n_chunks"]],
|
||
"kpi_delta": {k: round(a["kpi"][k] - b["kpi"][k], 3) for k in ("mean_spp", "one_sent_para_frac",
|
||
"dialogue_dash_frac", "han_leak_outside_parens") if isinstance(a["kpi"][k], (int, float))},
|
||
"term_coverage": [a["term_consistency"]["coverage"], b["term_consistency"]["coverage"]],
|
||
"final_chars": [a["final_chars"], b["final_chars"]],
|
||
}
|
||
result = {"anchors": out, "deterministic_floor_A0_vs_A0prime": floor}
|
||
Path(f"{AH}/anchors_analysis.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
print(json.dumps({"a0_chunks": a["n_chunks"], "a0prime_chunks": b["n_chunks"],
|
||
"a0_cut_offsets": a["cut_offsets"], "same_boundaries": floor["same_cut_offsets"],
|
||
"a0_src_est_out_mean": a["src_est_out"]["mean"],
|
||
"floor": floor}, ensure_ascii=False, indent=2))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|