chore(semantics): add v6 release gate pipeline
- Add reproducible v6 boundary, blessing, and consensus-adjudication corpora, plus the tiny-transformer trainer and v6 release-gate evaluator that gate every candidate on the deployed baselines. - Wire consensus-label merging, product-policy anchor evaluation, and sealed blessing benchmark review with their pytest coverage. - Refresh open-training corpus generation, iterative retraining runner, and random-holdout evaluation so v6 candidates can be benchmarked end-to-end.
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a provenance-preserving clipboard-semantics corpus registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
INTENT_FIELDS = (
|
||||
"task",
|
||||
"question",
|
||||
"invitation",
|
||||
"complaint",
|
||||
"scheduleNegotiation",
|
||||
"confirmationDecision",
|
||||
"followUpReminder",
|
||||
"blessing",
|
||||
"replyableMessage",
|
||||
"assistantCommand",
|
||||
"informationQuery",
|
||||
"systemNotification",
|
||||
)
|
||||
DOMAINS = {
|
||||
"finance",
|
||||
"travel",
|
||||
"calendar",
|
||||
"communication",
|
||||
"media",
|
||||
"smartHome",
|
||||
"shopping",
|
||||
"dining",
|
||||
"health",
|
||||
"weather",
|
||||
"accountService",
|
||||
"generalKnowledge",
|
||||
}
|
||||
SPLIT_USE = {
|
||||
"train": "train",
|
||||
"silverTrain": "train",
|
||||
"validation": "calibration-only",
|
||||
"silverCalibration": "calibration-only",
|
||||
"test": "evaluation-only",
|
||||
"golden": "evaluation-only",
|
||||
"silverAcceptance": "evaluation-only",
|
||||
"calibration": "calibration-only",
|
||||
}
|
||||
USE_PRIORITY = {
|
||||
"train": 0,
|
||||
"calibration-only": 1,
|
||||
"evaluation-only": 2,
|
||||
"research-only": 3,
|
||||
}
|
||||
TRAIN_LICENSES = {
|
||||
"Apache-2.0",
|
||||
"CC0-1.0 source / Apache-2.0 mirror",
|
||||
"CC-BY-3.0",
|
||||
"CC-BY-4.0",
|
||||
"CDLA-Permissive-1.0",
|
||||
"MIT",
|
||||
"OSGKeyboard project license",
|
||||
"CC0-1.0",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Source:
|
||||
identifier: str
|
||||
path: Path
|
||||
license: str
|
||||
default_use: str
|
||||
source_type: str
|
||||
all_intents_known: bool
|
||||
known_intent_labels: frozenset[str] | None
|
||||
sentiment_known: bool
|
||||
required: bool
|
||||
sensitive: bool
|
||||
weight: float
|
||||
|
||||
|
||||
def normalize_text(value: str) -> str:
|
||||
return " ".join(
|
||||
unicodedata.normalize("NFKC", value)
|
||||
.replace("\u0000", " ")
|
||||
.casefold()
|
||||
.split()
|
||||
).strip()
|
||||
|
||||
|
||||
def text_hash(value: str) -> str:
|
||||
return hashlib.sha256(normalize_text(value).encode()).hexdigest()
|
||||
|
||||
|
||||
def cluster_signature(text: str, language: str, family: str) -> str:
|
||||
normalized = normalize_text(text)
|
||||
normalized = re.sub(r"https?://\S+|www\.\S+", "<url>", normalized)
|
||||
normalized = re.sub(r"[\w.+-]+@[\w.-]+\.[a-z]{2,}", "<email>", normalized)
|
||||
normalized = re.sub(r"\d+(?:[./:-]\d+)*", "<n>", normalized)
|
||||
normalized = re.sub(r"[^\w\u3400-\u9fff<>]+", " ", normalized)
|
||||
tokens = normalized.split()
|
||||
skeleton = tokens[:12] + (["|"] + tokens[-8:] if len(tokens) > 20 else [])
|
||||
value = f"{language}|{family}|{' '.join(skeleton)}"
|
||||
return hashlib.sha256(value.encode()).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def iter_records(path: Path) -> Iterator[dict]:
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
first = ""
|
||||
while not first:
|
||||
first = handle.readline()
|
||||
if not first:
|
||||
return
|
||||
first = first.strip()
|
||||
if first.startswith("["):
|
||||
payload = json.loads(first + handle.read())
|
||||
if not isinstance(payload, list):
|
||||
raise TypeError(f"Expected JSON array in {path}")
|
||||
yield from payload
|
||||
return
|
||||
yield json.loads(first)
|
||||
for line in handle:
|
||||
if line.strip():
|
||||
yield json.loads(line)
|
||||
|
||||
|
||||
def resolve_path(raw_path: str, repository_root: Path) -> Path:
|
||||
expanded = raw_path.replace("${REPO_ROOT}", str(repository_root))
|
||||
path = Path(expanded).expanduser()
|
||||
return path if path.is_absolute() else repository_root / path
|
||||
|
||||
|
||||
def load_sources(manifest_path: Path, repository_root: Path) -> tuple[list[Source], dict]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
sources = []
|
||||
for raw in manifest["sources"]:
|
||||
if not raw.get("enabled", True):
|
||||
continue
|
||||
weight = float(raw.get("weight", 1.0))
|
||||
if not 0 < weight <= 1:
|
||||
raise ValueError(f"Invalid source weight for {raw['id']}: {weight}")
|
||||
known_labels = raw.get("knownIntentLabels")
|
||||
if known_labels is not None:
|
||||
unknown = set(known_labels) - set(INTENT_FIELDS)
|
||||
if unknown:
|
||||
raise ValueError(
|
||||
f"Unknown intent labels for {raw['id']}: {sorted(unknown)}"
|
||||
)
|
||||
sources.append(
|
||||
Source(
|
||||
identifier=raw["id"],
|
||||
path=resolve_path(raw["path"], repository_root),
|
||||
license=raw["license"],
|
||||
default_use=raw["defaultUse"],
|
||||
source_type=raw["sourceType"],
|
||||
all_intents_known=raw.get("allIntentLabelsKnown", False),
|
||||
known_intent_labels=(
|
||||
frozenset(known_labels) if known_labels is not None else None
|
||||
),
|
||||
sentiment_known=raw.get("sentimentKnown", False),
|
||||
required=raw.get("required", False),
|
||||
sensitive=raw.get("sensitive", False),
|
||||
weight=weight,
|
||||
)
|
||||
)
|
||||
return sources, manifest
|
||||
|
||||
|
||||
def source_use(source: Source, record: dict) -> str:
|
||||
if source.default_use != "by-split":
|
||||
return source.default_use
|
||||
return SPLIT_USE.get(
|
||||
record.get("split") or record.get("sourceSplit") or "",
|
||||
"evaluation-only",
|
||||
)
|
||||
|
||||
|
||||
def known_intents(source: Source, record: dict) -> set[str]:
|
||||
if source.known_intent_labels is not None:
|
||||
values = set(source.known_intent_labels)
|
||||
record_values = set(record.get("knownLabels") or [])
|
||||
if "replyable" in record_values:
|
||||
record_values.add("replyableMessage")
|
||||
return values | (record_values & set(INTENT_FIELDS))
|
||||
if source.all_intents_known:
|
||||
return set(INTENT_FIELDS)
|
||||
values = set(record.get("knownLabels") or [])
|
||||
if "replyable" in values:
|
||||
values.add("replyableMessage")
|
||||
return values & set(INTENT_FIELDS)
|
||||
|
||||
|
||||
def record_value(record: dict, label: str) -> bool:
|
||||
source_field = "replyable" if label == "replyableMessage" else label
|
||||
return bool(record.get(source_field))
|
||||
|
||||
|
||||
def effective_license(source: Source, record: dict) -> str:
|
||||
return record.get("sourceLicense") or source.license
|
||||
|
||||
|
||||
def effective_weight(source: Source, record: dict) -> float:
|
||||
record_weight = float(record.get("sampleWeight", 1.0))
|
||||
if not 0 < record_weight <= 1:
|
||||
raise ValueError(
|
||||
f"Invalid record sampleWeight for {record.get('id')}: {record_weight}"
|
||||
)
|
||||
return round(source.weight * record_weight, 6)
|
||||
|
||||
|
||||
def add_record(
|
||||
registry: dict[str, dict],
|
||||
source: Source,
|
||||
record: dict,
|
||||
source_path: Path,
|
||||
) -> str:
|
||||
text = str(record.get("text") or "").strip()
|
||||
language = str(record.get("language") or "").strip()
|
||||
if not text or language not in {"en", "zh-Hans"}:
|
||||
return "invalid"
|
||||
digest = text_hash(text)
|
||||
family = str(record.get("family") or "unknown")
|
||||
use = source_use(source, record)
|
||||
license_name = effective_license(source, record)
|
||||
if use == "train" and license_name not in TRAIN_LICENSES:
|
||||
return "unsafe-license"
|
||||
known_labels = set(record.get("knownLabels") or [])
|
||||
if "domain" in known_labels and record.get("domain") not in DOMAINS:
|
||||
return "invalid-domain"
|
||||
sample_weight = effective_weight(source, record)
|
||||
canonical = registry.setdefault(
|
||||
digest,
|
||||
{
|
||||
"id": f"corpus-{digest[:20]}",
|
||||
"text": text,
|
||||
"normalizedTextSHA256": digest,
|
||||
"language": language,
|
||||
"clusterSignature": cluster_signature(text, language, family),
|
||||
"families": set(),
|
||||
"uses": set(),
|
||||
"provenance": [],
|
||||
"intentEvidence": defaultdict(list),
|
||||
"sentimentEvidence": [],
|
||||
"domainEvidence": [],
|
||||
},
|
||||
)
|
||||
canonical["families"].add(family)
|
||||
canonical["uses"].add(use)
|
||||
known = known_intents(source, record)
|
||||
for label in known:
|
||||
canonical["intentEvidence"][label].append(
|
||||
{
|
||||
"source": source.identifier,
|
||||
"value": record_value(record, label),
|
||||
}
|
||||
)
|
||||
sentiment_is_known = source.sentiment_known or "sentiment" in set(
|
||||
record.get("knownLabels") or []
|
||||
)
|
||||
if sentiment_is_known and record.get("sentiment") in {
|
||||
"negative",
|
||||
"neutral",
|
||||
"positive",
|
||||
}:
|
||||
canonical["sentimentEvidence"].append(
|
||||
{
|
||||
"source": source.identifier,
|
||||
"value": record["sentiment"],
|
||||
}
|
||||
)
|
||||
if "domain" in known_labels:
|
||||
domain = record.get("domain")
|
||||
canonical["domainEvidence"].append(
|
||||
{
|
||||
"source": source.identifier,
|
||||
"value": domain,
|
||||
}
|
||||
)
|
||||
canonical["provenance"].append(
|
||||
{
|
||||
"source": source.identifier,
|
||||
"sourcePath": str(source_path),
|
||||
"sourceRecordID": record.get("id"),
|
||||
"sourceDataset": record.get("sourceDataset") or source.identifier,
|
||||
"sourceRevision": record.get("sourceRevision"),
|
||||
"sourceURL": record.get("sourceURL"),
|
||||
"split": record.get("split") or record.get("sourceSplit"),
|
||||
"allowedUse": use,
|
||||
"license": license_name,
|
||||
"sourceType": source.source_type,
|
||||
"sensitive": source.sensitive,
|
||||
"sampleWeight": sample_weight,
|
||||
}
|
||||
)
|
||||
return "accepted"
|
||||
|
||||
|
||||
def resolve_state(evidence: list[dict]) -> tuple[str, bool]:
|
||||
values = {item["value"] for item in evidence}
|
||||
if len(values) != 1:
|
||||
return "unknown", len(values) > 1
|
||||
return ("true" if values.pop() is True else "false"), False
|
||||
|
||||
|
||||
def finalize_record(raw: dict) -> dict:
|
||||
states = {}
|
||||
conflicts = []
|
||||
evidence = {}
|
||||
for label in INTENT_FIELDS:
|
||||
values = raw["intentEvidence"].get(label, [])
|
||||
state, conflict = resolve_state(values)
|
||||
states[label] = state
|
||||
if values:
|
||||
evidence[label] = values
|
||||
if conflict:
|
||||
conflicts.append(label)
|
||||
sentiment_values = {
|
||||
item["value"] for item in raw["sentimentEvidence"]
|
||||
}
|
||||
sentiment = (
|
||||
next(iter(sentiment_values)) if len(sentiment_values) == 1 else "unknown"
|
||||
)
|
||||
if len(sentiment_values) > 1:
|
||||
conflicts.append("sentiment")
|
||||
domain_values = {item["value"] for item in raw["domainEvidence"]}
|
||||
domain = next(iter(domain_values)) if len(domain_values) == 1 else "unknown"
|
||||
if len(domain_values) > 1:
|
||||
conflicts.append("domain")
|
||||
allowed_use = max(raw["uses"], key=USE_PRIORITY.__getitem__)
|
||||
training_weights = [
|
||||
value["sampleWeight"]
|
||||
for value in raw["provenance"]
|
||||
if value["allowedUse"] == "train"
|
||||
]
|
||||
training_datasets = sorted(
|
||||
{
|
||||
value["sourceDataset"]
|
||||
for value in raw["provenance"]
|
||||
if value["allowedUse"] == "train"
|
||||
}
|
||||
)
|
||||
known_labels = [
|
||||
label for label, state in states.items() if state != "unknown"
|
||||
]
|
||||
if sentiment != "unknown":
|
||||
known_labels.append("sentiment")
|
||||
if domain != "unknown":
|
||||
known_labels.append("domain")
|
||||
flattened_labels = {
|
||||
("replyable" if label == "replyableMessage" else label): state == "true"
|
||||
for label, state in states.items()
|
||||
}
|
||||
return {
|
||||
"id": raw["id"],
|
||||
"text": raw["text"],
|
||||
"normalizedTextSHA256": raw["normalizedTextSHA256"],
|
||||
"language": raw["language"],
|
||||
"sourceDataset": training_datasets[0] if training_datasets else None,
|
||||
"clusterSignature": raw["clusterSignature"],
|
||||
"families": sorted(raw["families"]),
|
||||
"family": sorted(raw["families"])[0],
|
||||
"observedUses": sorted(raw["uses"], key=USE_PRIORITY.__getitem__),
|
||||
"allowedUse": allowed_use,
|
||||
"split": (
|
||||
"train"
|
||||
if allowed_use == "train"
|
||||
else "validation"
|
||||
if allowed_use == "calibration-only"
|
||||
else "test"
|
||||
),
|
||||
**flattened_labels,
|
||||
"labels": states,
|
||||
"sentiment": sentiment if sentiment != "unknown" else "neutral",
|
||||
"domain": domain if domain != "unknown" else None,
|
||||
"knownLabels": sorted(known_labels),
|
||||
"sampleWeight": max(training_weights, default=1.0),
|
||||
"labelConflicts": sorted(conflicts),
|
||||
"sourceEvidence": evidence,
|
||||
"sentimentEvidence": raw["sentimentEvidence"],
|
||||
"domainEvidence": raw["domainEvidence"],
|
||||
"provenance": raw["provenance"],
|
||||
}
|
||||
|
||||
|
||||
def write_json_lines(path: Path, records: list[dict]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
for record in records:
|
||||
handle.write(
|
||||
json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n"
|
||||
)
|
||||
|
||||
|
||||
def select_pilot(records: list[dict], count: int) -> list[dict]:
|
||||
grouped: dict[tuple[str, str, str], list[dict]] = defaultdict(list)
|
||||
for record in records:
|
||||
source = sorted(
|
||||
{
|
||||
value["sourceDataset"]
|
||||
for value in record["provenance"]
|
||||
if value["allowedUse"] == "train"
|
||||
}
|
||||
)[0]
|
||||
family = record["families"][0]
|
||||
grouped[(record["language"], source, family)].append(record)
|
||||
for values in grouped.values():
|
||||
values.sort(
|
||||
key=lambda item: (
|
||||
not item["labelConflicts"],
|
||||
item["clusterSignature"],
|
||||
item["id"],
|
||||
)
|
||||
)
|
||||
keys = sorted(grouped)
|
||||
selected = []
|
||||
seen_clusters = set()
|
||||
offsets = defaultdict(int)
|
||||
while len(selected) < count:
|
||||
added = False
|
||||
for key in keys:
|
||||
values = grouped[key]
|
||||
while offsets[key] < len(values):
|
||||
candidate = values[offsets[key]]
|
||||
offsets[key] += 1
|
||||
if candidate["clusterSignature"] in seen_clusters:
|
||||
continue
|
||||
selected.append(
|
||||
{
|
||||
"id": candidate["id"],
|
||||
"text": candidate["text"],
|
||||
"language": candidate["language"],
|
||||
}
|
||||
)
|
||||
seen_clusters.add(candidate["clusterSignature"])
|
||||
added = True
|
||||
break
|
||||
if len(selected) >= count:
|
||||
break
|
||||
if not added:
|
||||
break
|
||||
return sorted(selected, key=lambda item: item["id"])
|
||||
|
||||
|
||||
def build(arguments: argparse.Namespace) -> dict:
|
||||
repository_root = arguments.repository_root.resolve()
|
||||
sources, manifest = load_sources(arguments.source_manifest, repository_root)
|
||||
raw_registry: dict[str, dict] = {}
|
||||
source_reports = []
|
||||
missing_sources = []
|
||||
excluded_counts = Counter()
|
||||
for source in sources:
|
||||
if not source.path.exists():
|
||||
if source.required:
|
||||
raise FileNotFoundError(f"Required corpus is missing: {source.path}")
|
||||
missing_sources.append(source.identifier)
|
||||
continue
|
||||
statuses = Counter()
|
||||
for record in iter_records(source.path):
|
||||
statuses[add_record(raw_registry, source, record, source.path)] += 1
|
||||
source_reports.append(
|
||||
{
|
||||
"id": source.identifier,
|
||||
"path": str(source.path),
|
||||
"sha256": sha256_file(source.path),
|
||||
"records": sum(statuses.values()),
|
||||
"statuses": dict(sorted(statuses.items())),
|
||||
}
|
||||
)
|
||||
excluded_counts.update(
|
||||
{
|
||||
key: value
|
||||
for key, value in statuses.items()
|
||||
if key != "accepted"
|
||||
}
|
||||
)
|
||||
records = sorted(
|
||||
(finalize_record(value) for value in raw_registry.values()),
|
||||
key=lambda item: item["id"],
|
||||
)
|
||||
train_candidates = [
|
||||
record
|
||||
for record in records
|
||||
if record["allowedUse"] == "train"
|
||||
and not record["labelConflicts"]
|
||||
and (
|
||||
any(value != "unknown" for value in record["labels"].values())
|
||||
or record["sentiment"] != "unknown"
|
||||
or record["domain"] != "unknown"
|
||||
)
|
||||
]
|
||||
conflicts = [
|
||||
{
|
||||
"id": record["id"],
|
||||
"text": record["text"],
|
||||
"language": record["language"],
|
||||
"conflicts": record["labelConflicts"],
|
||||
"sourceEvidence": record["sourceEvidence"],
|
||||
"sentimentEvidence": record["sentimentEvidence"],
|
||||
"domainEvidence": record["domainEvidence"],
|
||||
"resolution": None,
|
||||
"reviewer": None,
|
||||
}
|
||||
for record in records
|
||||
if record["labelConflicts"]
|
||||
]
|
||||
pilot_was_preserved = (
|
||||
getattr(arguments, "preserve_pilot", False) and arguments.pilot.exists()
|
||||
)
|
||||
if pilot_was_preserved:
|
||||
pilot = list(iter_records(arguments.pilot))
|
||||
if len(pilot) != arguments.pilot_count:
|
||||
raise ValueError(
|
||||
f"Preserved pilot has {len(pilot)} records; "
|
||||
f"expected {arguments.pilot_count}"
|
||||
)
|
||||
else:
|
||||
pilot = select_pilot(train_candidates, arguments.pilot_count)
|
||||
write_json_lines(arguments.registry, records)
|
||||
write_json_lines(arguments.train_candidates, train_candidates)
|
||||
if not pilot_was_preserved:
|
||||
write_json_lines(arguments.pilot, pilot)
|
||||
write_json_lines(arguments.human_review, conflicts)
|
||||
usage_counts = Counter(record["allowedUse"] for record in records)
|
||||
language_counts = Counter(record["language"] for record in records)
|
||||
train_barred_by_evaluation = sum(
|
||||
"train" in record["observedUses"]
|
||||
and "evaluation-only" in record["observedUses"]
|
||||
for record in records
|
||||
)
|
||||
train_barred_by_calibration = sum(
|
||||
"train" in record["observedUses"]
|
||||
and "calibration-only" in record["observedUses"]
|
||||
for record in records
|
||||
)
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"sourceManifest": str(arguments.source_manifest),
|
||||
"sourceManifestSHA256": sha256_file(arguments.source_manifest),
|
||||
"sourceReports": source_reports,
|
||||
"excludedSources": manifest.get("excludedSources", []),
|
||||
"missingOptionalSources": missing_sources,
|
||||
"inputRecordCount": sum(
|
||||
item["records"] for item in source_reports
|
||||
),
|
||||
"canonicalRecordCount": len(records),
|
||||
"exactDuplicateCount": sum(
|
||||
max(len(record["provenance"]) - 1, 0) for record in records
|
||||
),
|
||||
"trainCandidateCount": len(train_candidates),
|
||||
"trainBarredByEvaluationCount": train_barred_by_evaluation,
|
||||
"trainBarredByCalibrationCount": train_barred_by_calibration,
|
||||
"labelConflictCount": len(conflicts),
|
||||
"pilotCount": len(pilot),
|
||||
"pilotPreserved": pilot_was_preserved,
|
||||
"pilotSHA256": sha256_file(arguments.pilot),
|
||||
"usageCounts": dict(sorted(usage_counts.items())),
|
||||
"languageCounts": dict(sorted(language_counts.items())),
|
||||
"excludedRecordCounts": dict(sorted(excluded_counts.items())),
|
||||
"outputs": {
|
||||
"registry": str(arguments.registry),
|
||||
"trainCandidates": str(arguments.train_candidates),
|
||||
"pilot": str(arguments.pilot),
|
||||
"humanReview": str(arguments.human_review),
|
||||
},
|
||||
}
|
||||
arguments.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
arguments.report.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
root = argparse.ArgumentParser()
|
||||
root.add_argument("--repository-root", type=Path, default=Path.cwd())
|
||||
root.add_argument(
|
||||
"--source-manifest",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"ModelTraining/ClipboardSemantics/corpus-registry-sources.json"
|
||||
),
|
||||
)
|
||||
root.add_argument(
|
||||
"--registry",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"ModelTraining/ClipboardSemantics/CorpusRegistry/registry.jsonl"
|
||||
),
|
||||
)
|
||||
root.add_argument(
|
||||
"--train-candidates",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"ModelTraining/ClipboardSemantics/CorpusRegistry/"
|
||||
"train-candidates.jsonl"
|
||||
),
|
||||
)
|
||||
root.add_argument(
|
||||
"--pilot",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"ModelTraining/ClipboardSemantics/CorpusRegistry/"
|
||||
"labeling-pilot.jsonl"
|
||||
),
|
||||
)
|
||||
root.add_argument(
|
||||
"--human-review",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"ModelTraining/ClipboardSemantics/CorpusRegistry/"
|
||||
"source-conflicts-human-review.jsonl"
|
||||
),
|
||||
)
|
||||
root.add_argument(
|
||||
"--report",
|
||||
type=Path,
|
||||
default=Path(
|
||||
"ModelTraining/ClipboardSemantics/CorpusRegistry/registry-report.json"
|
||||
),
|
||||
)
|
||||
root.add_argument("--pilot-count", type=int, default=1000)
|
||||
root.add_argument("--preserve-pilot", action="store_true")
|
||||
return root
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = parser().parse_args()
|
||||
report = build(arguments)
|
||||
print(
|
||||
"CORPUS_REGISTRY_DONE "
|
||||
f"canonical={report['canonicalRecordCount']} "
|
||||
f"train={report['trainCandidateCount']} "
|
||||
f"pilot={report['pilotCount']} "
|
||||
f"conflicts={report['labelConflictCount']}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user