File size: 5,461 Bytes
5952424 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | #!/usr/bin/env python3
"""Stage 6b (documentary-clean variant): materialize ONE train corpus that is
clean of all documentary text and every textual trace of it.
Output record set = pristine + repaired + bronze, where:
- records with source ddbdp/dclp (papyri, both tiers) are DROPPED entirely
- (inscriptions tier is simply never read -- it is excluded by construction)
- every sentence whose stage-5b mask is nonzero (i.e. matches ANY PHI
inscription or papyrus by exact skeleton / bag / shared word-8-gram) is
EXCISED; maximal clean runs are re-stitched into id#segN segments of
>= MIN_SEG_CHARS chars (same rule as the 10-fold train sets)
- NO literary-bucket excision: all 10 literary buckets are trainable here
(this corpus is fold-free; it is held out only against documentary text)
Output: <DOC_OUTDIR>/train.jsonl.zst (records {id, tier, source, text})
Default DOC_OUTDIR: $STOICHEIA_DATA
"""
import glob
import json
import os
import sys
from collections import defaultdict
from concurrent.futures import ProcessPoolExecutor
import orjson
import pyarrow.parquet as pq
import zstandard as zstd
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common import MIN_SEG_CHARS
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "work", "doc_clean")
OUTDIR = os.path.expandvars(os.environ.get("DOC_OUTDIR",
"$STOICHEIA_DATA"))
PAPYRI_SOURCES = {"ddbdp", "dclp"}
def segments(text, starts, ends, masks, min_chars):
"""(pieces, cut_chars, was_cut) after excising sentences with nonzero mask."""
if not starts:
return [], 0, 0
keep = [not m for m in masks]
if all(keep):
return [text], 0, 0
pieces = []
cut_chars = 0
i = 0
n = len(starts)
while i < n:
if not keep[i]:
cut_chars += ends[i] - starts[i]
i += 1
continue
j = i
while j + 1 < n and keep[j + 1]:
j += 1
piece = text[starts[i]:ends[j]].strip()
if len(piece) >= min_chars:
pieces.append(piece)
else:
cut_chars += len(piece)
i = j + 1
return pieces, cut_chars, 1
def process_shard(args):
tier, spath, mpath = args
tag = tier + "-" + os.path.basename(spath).split(".")[0]
st = pq.read_table(spath, columns=["rid", "source", "text", "starts", "ends"])
mt = pq.read_table(mpath, columns=["rid", "masks"])
assert st["rid"].to_pylist() == mt["rid"].to_pylist(), "shard misalignment"
pd = os.path.join(OUTDIR, "parts")
os.makedirs(pd, exist_ok=True)
f = open(os.path.join(pd, "train-%s.jsonl.zst" % tag), "wb")
w = zstd.ZstdCompressor(level=6).stream_writer(f)
stats = defaultdict(lambda: [0, 0, 0, 0]) # tier -> recs,chars,cut,dropped
for rid, source, text, starts, ends, masks in zip(
st["rid"].to_pylist(), st["source"].to_pylist(),
st["text"].to_pylist(), st["starts"].to_pylist(),
st["ends"].to_pylist(), mt["masks"].to_pylist()):
s = stats[tier]
if source in PAPYRI_SOURCES:
s[3] += 1
continue
pieces, cut, was_cut = segments(text, starts, ends, masks, MIN_SEG_CHARS)
if not pieces:
s[3] += 1
s[2] += cut
continue
if len(pieces) == 1 and not was_cut:
w.write(orjson.dumps({"id": rid, "tier": tier, "source": source,
"text": pieces[0]}) + b"\n")
else:
for pi, piece in enumerate(pieces):
w.write(orjson.dumps({"id": "%s#seg%d" % (rid, pi), "tier": tier,
"source": source, "text": piece}) + b"\n")
s[0] += len(pieces)
s[1] += sum(len(p) for p in pieces)
s[2] += cut
w.close()
f.close()
return dict(stats)
def main():
os.makedirs(OUTDIR, exist_ok=True)
tasks = []
for tier in ("pristine", "repaired", "bronze"):
for spath in sorted(glob.glob(os.path.join(
ROOT, "work", "sentences", tier, "shard_*.parquet"))):
mpath = os.path.join(OUT, "masks", tier, os.path.basename(spath))
tasks.append((tier, spath, mpath))
print("%d shards" % len(tasks), flush=True)
agg = defaultdict(lambda: [0, 0, 0, 0])
workers = max(4, min(16, (os.cpu_count() or 12) - 8))
with ProcessPoolExecutor(max_workers=workers) as ex:
for res in ex.map(process_shard, tasks, chunksize=1):
for key, v in res.items():
a = agg[key]
for j in range(4):
a[j] += v[j]
with open(os.path.join(OUT, "stage6b_stats.json"), "w") as f:
json.dump(agg, f, indent=2)
print(json.dumps(agg, indent=2), flush=True)
# concatenate parts (zstd frames concatenate losslessly)
pd = os.path.join(OUTDIR, "parts")
parts = sorted(glob.glob(os.path.join(pd, "train-*.jsonl.zst")))
outp = os.path.join(OUTDIR, "train.jsonl.zst")
with open(outp, "wb") as out:
for p in parts:
with open(p, "rb") as src:
while True:
chunk = src.read(1 << 24)
if not chunk:
break
out.write(chunk)
for p in parts:
os.remove(p)
os.rmdir(pd)
print("assembled %s" % outp, flush=True)
if __name__ == "__main__":
main()
|