textmachine/eval/exp12/exp12_extract.py

206 lines
8 KiB
Python
Raw Permalink 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
"""exp12 — извлечение per-chunk (source/draft/final) из приёмочного store + выбор 3 чистых глав.
$0. Никаких вызовов. Источники (ВНЕ git):
- store копия: /home/ubuntu/books/gu-zhenren/diag/store-copy.db (draft=deepseek, edit=glm-5)
- параллель: /home/ubuntu/books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt ([ОРИГ]/[ПЕРЕВОД])
Выход:
- diag/chunks.json — per (chapter,chunk): source, draft(A0), final(A1), dispositions
- diag/selection.json — метрики выбора глав + 3 выбранные главы (пре-регистрированное правило)
Пре-регистрированное правило выбора (frozen, см. 12-quality-diagnosis.md §Пре-регистрация):
frame = ЧИСТЫЕ главы (все чанки disposition=ok на draft И edit, flag_reason пуст).
метрика на ИСХОДНИКЕ zh: D = доля символов внутри кавычек “…” (U+201C…U+201D) от не-пробельных.
выбор: high=argmax D, low=argmin D, mid=ближайшая к медиане D. Тай-брейк — меньший номер главы.
"""
from __future__ import annotations
import json, re, sqlite3, sys
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
DB = DIAG / "store-copy.db"
PARALLEL = Path("/home/ubuntu/books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt")
CH_HDR = re.compile(r"\s*Глава\s+(\d+)\s*】")
CHUNK_HDR = re.compile(r"^---\s*чанк\s+(\d+)\s*\[(.+?)\]\s*---")
LQUOTE, RQUOTE = "", "" # “ ”
def parse_parallel(path: Path) -> dict:
"""{(ch,chunk): {'source': str, 'final_parallel': str, 'flag': str}}"""
out: dict = {}
ch = None
chunk = None
mode = None # 'orig' | 'per'
buf_src: list[str] = []
buf_per: list[str] = []
def flush():
if ch is not None and chunk is not None:
out[(ch, chunk)] = {
"source": "\n".join(buf_src).strip(),
"final_parallel": "\n".join(buf_per).strip(),
"flag": flag,
}
flag = ""
for line in path.read_text(encoding="utf-8").splitlines():
m = CH_HDR.search(line)
if m:
flush(); ch = int(m.group(1)); chunk = None; mode = None
buf_src, buf_per = [], []
continue
m = CHUNK_HDR.match(line.strip())
if m:
flush()
chunk = int(m.group(1)); flag = m.group(2).strip()
mode = None; buf_src, buf_per = [], []
continue
s = line.strip()
if s == "[ОРИГ]":
mode = "orig"; continue
if s == "[ПЕРЕВОД]":
mode = "per"; continue
if s.startswith("[⚠") or s.startswith("=====") or not s and mode is None:
continue
if mode == "orig":
buf_src.append(line)
elif mode == "per":
buf_per.append(line)
flush()
return out
def load_store(db: Path) -> dict:
"""{(ch,chunk): {'draft':..,'edit':..,'draft_disp':..,'edit_disp':..,'draft_flag':..,'edit_flag':..}}"""
con = sqlite3.connect(db)
con.row_factory = sqlite3.Row
rows = con.execute("""
SELECT cs.chapter, cs.chunk_idx, cs.stage, cs.disposition, cs.flag_reason,
cp.model_actual, cp.response_text
FROM chunk_status cs
LEFT JOIN checkpoints cp ON cp.request_hash = cs.final_hash
ORDER BY cs.chapter, cs.chunk_idx, cs.stage
""").fetchall()
con.close()
out: dict = {}
for r in rows:
key = (r["chapter"], r["chunk_idx"])
d = out.setdefault(key, {})
st = r["stage"]
d[st] = r["response_text"] or ""
d[f"{st}_disp"] = r["disposition"]
d[f"{st}_flag"] = r["flag_reason"]
d[f"{st}_model"] = r["model_actual"]
return out
def dialogue_density(src: str) -> float:
nonspace = re.sub(r"\s", "", src)
if not nonspace:
return 0.0
inside = 0
depth = 0
for c in src:
if c == LQUOTE:
depth += 1; continue
if c == RQUOTE:
if depth > 0:
depth -= 1
continue
if depth > 0 and not c.isspace():
inside += 1
return inside / len(nonspace)
def main() -> None:
par = parse_parallel(PARALLEL)
store = load_store(DB)
# merge per chunk
chunks = {}
keys = sorted(set(par) | set(store))
for k in keys:
ch, ci = k
p = par.get(k, {})
s = store.get(k, {})
chunks[f"{ch}/{ci}"] = {
"chapter": ch, "chunk": ci,
"source": p.get("source", ""),
"draft": s.get("draft", ""), # A0
"final": s.get("edit", ""), # A1 (glm-5 mono)
"final_parallel": p.get("final_parallel", ""),
"draft_disp": s.get("draft_disp", "?"),
"edit_disp": s.get("edit_disp", "?"),
"draft_flag": s.get("draft_flag", ""),
"edit_flag": s.get("edit_flag", ""),
}
# sanity: store edit vs parallel final match
mism = [k for k, v in chunks.items()
if v["final"] and v["final_parallel"]
and v["final"].strip()[:120] != v["final_parallel"].strip()[:120]]
# per-chapter clean classification + dialogue density
chap = {}
for k, v in chunks.items():
c = v["chapter"]
d = chap.setdefault(c, {"chunks": 0, "clean": True, "src": [], "flags": []})
d["chunks"] += 1
d["src"].append(v["source"])
if v["draft_disp"] != "ok" or v["edit_disp"] != "ok":
d["clean"] = False
d["flags"].append(f"{v['draft_flag']}|{v['edit_flag']}")
metrics = {}
for c, d in chap.items():
full_src = "\n".join(d["src"])
metrics[c] = {
"clean": d["clean"],
"chunks": d["chunks"],
"src_chars": len(re.sub(r"\s", "", full_src)),
"dialogue_density": round(dialogue_density(full_src), 4),
}
clean = {c: m for c, m in metrics.items() if m["clean"]}
ds = sorted((m["dialogue_density"], c) for c, m in clean.items())
dens = [x[0] for x in ds]
median = dens[len(dens) // 2] if len(dens) % 2 else (dens[len(dens)//2 - 1] + dens[len(dens)//2]) / 2
high = max(clean, key=lambda c: (clean[c]["dialogue_density"], -c))
low = min(clean, key=lambda c: (clean[c]["dialogue_density"], c))
mid = min((c for c in clean if c not in (high, low)),
key=lambda c: (abs(clean[c]["dialogue_density"] - median), c))
selection = {
"rule": "clean-frame; D=share of chars in “…”; high=argmax, low=argmin, mid=nearest-median; tie=min chapter#",
"clean_chapters": sorted(clean),
"median_D": round(median, 4),
"selected": {
"high_dialogue": {"chapter": high, **clean[high]},
"mid": {"chapter": mid, **clean[mid]},
"low_exposition": {"chapter": low, **clean[low]},
},
"all_clean_metrics": {str(c): clean[c] for c in sorted(clean)},
}
DIAG.mkdir(exist_ok=True)
(DIAG / "chunks.json").write_text(json.dumps(chunks, ensure_ascii=False, indent=1), encoding="utf-8")
(DIAG / "selection.json").write_text(json.dumps(selection, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"chunks extracted: {len(chunks)} | store-parallel final mismatches (first120): {len(mism)}")
if mism:
print(" mism keys:", mism[:10])
print(f"clean chapters ({len(clean)}): {sorted(clean)}")
print(f"median D = {median:.4f}")
print("\nDialogue density (clean, sorted):")
for dval, c in ds:
star = " <-HIGH" if c == high else " <-LOW" if c == low else " <-MID" if c == mid else ""
print(f" ch{c:>2}: D={dval:.4f} chunks={clean[c]['chunks']} src_chars={clean[c]['src_chars']}{star}")
print(f"\nSELECTED: high=ch{high} (D={clean[high]['dialogue_density']}), "
f"mid=ch{mid} (D={clean[mid]['dialogue_density']}), low=ch{low} (D={clean[low]['dialogue_density']})")
if __name__ == "__main__":
main()