textmachine/eval/exp15/build_s2prime.py

113 lines
4.8 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
"""exp15 — S2' synthetic scene-marked slice via guzhenren chapter concatenation (owner decision 2026-07-17).
S2' = concatenate the FIRST N consecutive 节 of 蛊真人 (opening arc), STRIP the 第X节 headers at the seams,
so each seam is an IMPLICIT scene boundary (no explicit marker). Every seam is recorded as a ground-truth
boundary (rune-offset in the concatenated text + the two adjacent 节 ids). The seam previews (tail of 节_i +
head of 节_{i+1}) are emitted so each seam can be MANUALLY classified scene-shift vs continuation-cliffhanger
(§1.14b): continuation seams are negative-boundaries ("must NOT cut here"), separate strata for Q1.
Deterministic, $0, pure function of the book bytes. Outputs OUTSIDE git.
Run: eval/.venv/bin/python eval/exp15/build_s2prime.py [N] # default N=12 节
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
BOOK = Path("/home/ubuntu/books/gu-zhenren/guzhenren-utf8.txt")
OUT_DIR = Path("/home/ubuntu/books/gu-zhenren/exp15")
OUT_TXT = OUT_DIR / "S2prime.txt"
OUT_SEAMS = OUT_DIR / "S2prime_seams.json"
OUT_PREVIEW = OUT_DIR / "S2prime_seam_previews.md"
SEC_RE = re.compile(r"^第[0-9一二三四五六七八九十百千零两]{1,10}[节節][: ]?.*$", re.M)
PREVIEW_TAIL, PREVIEW_HEAD = 220, 220
def load_text() -> str:
raw = BOOK.read_bytes()
try:
return raw.decode("utf-8")
except UnicodeDecodeError:
return raw.decode("gb18030", errors="replace")
def sections(text: str):
"""Return list of (header_line, body_text) for each 节 in file order (body = up to next 节)."""
marks = list(SEC_RE.finditer(text))
out = []
for k, m in enumerate(marks):
header = m.group(0).strip()
body_start = m.end()
body_end = marks[k + 1].start() if k + 1 < len(marks) else len(text)
body = text[body_start:body_end].strip("\n")
out.append((k + 1, header, body)) # sec index n = k+1 (running, per gu_corpus_build convention)
return out
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 12
OUT_DIR.mkdir(parents=True, exist_ok=True)
text = load_text()
secs = sections(text)
if len(secs) < n:
print(f"only {len(secs)} 节 found; using all", file=sys.stderr)
n = len(secs)
chosen = secs[:n]
# concatenate bodies (headers STRIPPED at seams); single "\n" join = the implicit seam
parts, seams, cursor = [], [], 0
JOIN = "\n"
for i, (idx, header, body) in enumerate(chosen):
if i > 0:
cursor += len(JOIN) # account the join separator (rune count)
seam_offset = cursor # rune offset where this section's body begins
if i > 0:
seams.append({
"seam_id": i,
"rune_offset": seam_offset, # boundary between prev body and this body
"prev_sec": chosen[i - 1][0],
"next_sec": idx,
"prev_header": chosen[i - 1][1],
"next_header": header,
"class": "UNCLASSIFIED", # -> scene-shift | continuation (manual, §1.14b)
"rationale": "",
})
parts.append(body)
cursor += len(body)
joined = JOIN.join(parts)
OUT_TXT.write_text(joined, encoding="utf-8")
OUT_SEAMS.write_text(json.dumps({
"book": "guzhenren", "arc": "opening", "n_sections": n,
"section_ids": [c[0] for c in chosen],
"total_runes": len(joined),
"headers_stripped_at_seams": True,
"seam_marker": "implicit (single \\n; no 第X节 header)",
"seams": seams,
}, ensure_ascii=False, indent=2), encoding="utf-8")
# seam-context previews for MANUAL classification (read tail_i + head_{i+1})
pv = [f"# S2' seam previews — classify each: scene-shift | continuation-cliffhanger (§1.14b)",
f"# {n} 节 concatenated, headers stripped; {len(seams)} seams; total {len(joined):,} runes", ""]
for s in seams:
off = s["rune_offset"]
tail = joined[max(0, off - len(JOIN) - PREVIEW_TAIL):off - len(JOIN)]
head = joined[off:off + PREVIEW_HEAD]
pv += [f"## seam {s['seam_id']} (节{s['prev_sec']} → 节{s['next_sec']}, offset {off})",
f"**{s['prev_header']}** → **{s['next_header']}**",
f"…tail of 节{s['prev_sec']}: …{tail}",
f"head of 节{s['next_sec']}: {head}", ""]
OUT_PREVIEW.write_text("\n".join(pv), encoding="utf-8")
print(f"S2' built: {n} 节, {len(seams)} seams, {len(joined):,} runes -> {OUT_TXT}")
print(f"section ids: {[c[0] for c in chosen]}")
print(f"seams (offset, 节i->节j): " + ", ".join(f"{s['rune_offset']}({s['prev_sec']}->{s['next_sec']})" for s in seams))
print(f"previews for manual classification -> {OUT_PREVIEW}")
if __name__ == "__main__":
main()