File size: 10,178 Bytes
2c87024 | 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | """Portable input plans and shared operations for independent corpus parts."""
import json
import sqlite3
import sys
import tomllib
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT / "src"))
from vimeml.data.build import validate, file_sha, sha, display_path, SCHEMA, dump_line, dump_json
from vimeml.data.clean import clean_block
from vimeml.data.segment import restore_linebreaks, split_sentences
from vimeml.data.annotations import load_annotations, document_annotations, match_fragment
PART_SCHEMA_VERSION = 1
AUDITS = ("review_blocks", "dropped_blocks", "fragments")
def text_sha(path):
# Git may check out CRLF on Windows and LF on Mac. These text files have
# identical semantics; hash normalized UTF-8 text rather than platform bytes.
return sha(path.read_text(encoding="utf-8-sig"))
def load_plan(config_path):
config = tomllib.loads(config_path.read_text(encoding="utf-8-sig"))
validate(config)
paths = config["inputs"]["fineweb_paths"]
if not isinstance(paths, list) or not paths or any(not isinstance(p, str) for p in paths):
raise ValueError("Partition builds require an explicit fineweb_paths list.")
inventory = [(p, "fineweb", config["build"]["fineweb_documents_per_shard"]) for p in sorted(paths)]
inventory.append((config["inputs"]["tatoeba_path"], "tatoeba", config["build"]["tatoeba_documents"]))
if len({p for p, _, _ in inventory}) != len(inventory):
raise ValueError("Input paths must be unique.")
for p, _, _ in inventory:
if Path(p).is_absolute() or not (ROOT / p).resolve().is_relative_to(ROOT):
raise ValueError("Input paths must be portable paths inside the repository.")
policy_paths = {}
if config["build"]["quality_mode"] == "rules_with_approved_annotations":
policy_paths["annotations"] = ROOT / config["review"]["annotations"]
policy_paths["calibration_ids"] = ROOT / config["inputs"]["calibration_ids"]
calibration_ids = json.loads(policy_paths["calibration_ids"].read_text(encoding="utf-8-sig"))
if not isinstance(calibration_ids, list) or any(not isinstance(p, str) for p in calibration_ids):
raise ValueError("calibration_ids must contain a JSON list of document IDs.")
policy_hashes = {key: text_sha(path) for key, path in policy_paths.items()}
names = ("build", "readers", "clean", "segment", "annotations", "corpus_parts", "corpus_export", "preprocess_part", "merge_parts")
code_hashes = {f"src/vimeml/data/{name}.py": text_sha(ROOT / f"src/vimeml/data/{name}.py") for name in names}
relevant = {key: value for key, value in config["build"].items() if key not in {"output_dir", "commit_every", "progress_every", "batch_size"}}
signature = sha(json.dumps({"schema": PART_SCHEMA_VERSION, "inventory": inventory,
"build": relevant, "split": config["split"], "review": config.get("review", {}),
"policy_sha256": policy_hashes, "code_sha256": code_hashes}, sort_keys=True))
return config, inventory, policy_paths, set(calibration_ids), policy_hashes, code_hashes, signature
def parse_part(value):
try:
index, total = map(int, value.split("/"))
except (ValueError, AttributeError):
raise ValueError("Use --part INDEX/COUNT, e.g. 0/2 or 1/2.") from None
if not 1 <= total <= 128 or not 0 <= index < total:
raise ValueError("Part index is zero-based and must be less than the part count.")
return index, total
def create_database(output, annotation_path):
if output.exists() and (not output.is_dir() or any(output.iterdir())):
raise FileExistsError(f"Output must be absent or empty: {output}")
output.mkdir(parents=True, exist_ok=True)
db = sqlite3.connect(output / "index.sqlite", uri=True)
try:
db.row_factory = sqlite3.Row
db.execute("PRAGMA temp_store=FILE")
db.executescript(SCHEMA)
db.execute("CREATE TABLE document_metrics(doc_hash TEXT PRIMARY KEY, payload TEXT NOT NULL)")
annotation_count = load_annotations(db, annotation_path)
return db, annotation_count
except BaseException:
db.close()
raise
def aggregate_metrics(db):
totals = {key: Counter() for key in ("counts", "rule_reasons", "fragment_flags", "effective_blocks")}
for row in db.execute("SELECT payload FROM document_metrics ORDER BY doc_hash"):
metrics = json.loads(row[0])
for key in totals:
totals[key].update(metrics[key])
return totals
def process_content(db, document, normalized, doc_hash, settings, audits):
counts, rule_reasons, fragment_flags, effective_blocks = (Counter() for _ in range(4))
counts["unique_documents"] += 1
blocks = []
for index, original in enumerate(normalized.split("\n")):
result = clean_block(original)
block_id = f"b{index:03d}"
counts["input_blocks"] += 1
counts[result["action"] + "_blocks"] += 1
rule_reasons[result["reason"]] += 1
blocks.append({
"id": block_id, "line_index": index,
"text": result["text"], "action": result["action"],
})
if result["action"] != "keep":
destination = (
"dropped_blocks" if result["action"] == "drop"
else "review_blocks"
)
dump_line(audits[destination], {
"source": document.source,
"doc_id": document.doc_id, "doc_hash": doc_hash,
"block_id": block_id, "original_text": original,
**result,
})
boundaries, fragment_labels, block_labels = document_annotations(db, doc_hash, blocks)
counts["approved_block_labels"] += len(block_labels)
effective_blocks.update(block["action"] for block in blocks)
for block_id, label_id in block_labels.items():
block = next(item for item in blocks if item["id"] == block_id)
destination = "dropped_blocks" if block["action"] == "drop" else "review_blocks"
dump_line(audits[destination], {
"source": document.source, "doc_id": document.doc_id,
"doc_hash": doc_hash, "block_id": block_id,
"text": block["text"], "action": block["action"],
"reason": "approved_local_annotation", "annotation_id": label_id,
})
paragraphs = restore_linebreaks(
blocks, max_chars=settings["max_join_chars"],
boundary_decisions={pair: label["action"] for pair, label in boundaries.items()},
)
counts["paragraphs"] += len(paragraphs)
for pi, paragraph in enumerate(paragraphs):
counts["join_events"] += len(paragraph["joins"])
counts["approved_join_events"] += sum(join["reason"] == "approved_annotation" for join in paragraph["joins"])
paragraph_label_ids = {
block_labels[part["block_id"]] for part in paragraph["parts"]
if part["block_id"] in block_labels
}
paragraph_label_ids.update(
boundaries[(join["left_block"], join["right_block"])]["id"]
for join in paragraph["joins"] if join["reason"] == "approved_annotation"
)
for si, sentence in enumerate(split_sentences(paragraph["text"])):
flags = sorted(set(
paragraph["flags"] + sentence["flags"]
))
spans = []
for part in paragraph["parts"]:
left = max(sentence["start"], part["start"])
right = min(sentence["end"], part["end"])
if left < right:
spans.append({
"block_id": part["block_id"],
"start": left - part["start"],
"end": right - part["start"],
})
fragment_label = match_fragment(db, fragment_labels, sentence["text"], spans)
explicitly_kept = fragment_label is not None and fragment_label["action"] == "keep"
explicitly_excluded = fragment_label is not None and fragment_label["action"] in {"drop", "review"}
if explicitly_kept and flags:
raise ValueError(f"Annotation {fragment_label['id']}: flagged fragments cannot be rescued.")
if (
(not sentence["terminated"] and not explicitly_kept) or flags or explicitly_excluded
or not any(c.isalnum() for c in sentence["text"])
):
counts["fragments"] += 1
fragment_flags.update(flags)
dump_line(audits["fragments"], {
"source": document.source,
"doc_id": document.doc_id, "doc_hash": doc_hash,
"paragraph_index": pi, "sentence_index": si,
"text": sentence["text"],
"terminated": sentence["terminated"],
"flags": flags, "cleaned_block_spans": spans,
"paragraph_joins": paragraph["joins"],
"approved_annotation_id": fragment_label["id"] if fragment_label else None,
})
continue
text = sentence["text"]
label_ids = set(paragraph_label_ids)
if fragment_label:
label_ids.add(fragment_label["id"])
counts["approved_fragment_recoveries"] += int(not sentence["terminated"])
db.execute(
"INSERT INTO units VALUES (?, ?, ?, ?, ?, ?, ?)",
(
doc_hash, sha(text), text, pi, si,
json.dumps(spans, ensure_ascii=False),
json.dumps(sorted(label_ids)),
),
)
counts["sentence_occurrences"] += 1
return {'counts': dict(counts), 'rule_reasons': dict(rule_reasons),
'fragment_flags': dict(fragment_flags), 'effective_blocks': dict(effective_blocks)}
|