206 lines
8 KiB
Python
206 lines
8 KiB
Python
#!/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()
|