175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Check or finalize the deterministic human label-and-difficulty review."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Sequence
|
|
|
|
from prepare_data import (
|
|
DATASET_VERSION,
|
|
DEFAULT_CURATION_REVIEW,
|
|
DEFAULT_FIXTURES,
|
|
default_source_paths,
|
|
load_reviewed_semantic_exclusions,
|
|
)
|
|
from purpose_data import (
|
|
DataError,
|
|
SourceRecord,
|
|
curate_records,
|
|
exclude_reviewed_duplicates,
|
|
load_classifiable_fixtures,
|
|
load_sources,
|
|
)
|
|
from review_contract import (
|
|
DEFAULT_SAMPLE_FRACTION,
|
|
DEFAULT_SAMPLE_SEED,
|
|
finalize_human_review,
|
|
inspect_review_csv,
|
|
stratified_review_sample,
|
|
write_review_csv,
|
|
)
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
REPOSITORY_ROOT = SCRIPT_DIR.parent.parent
|
|
DEFAULT_REVIEW_CSV = SCRIPT_DIR / ".artifacts" / "human-review-v1.csv"
|
|
DEFAULT_OUTPUT = SCRIPT_DIR / "data" / "human-review-v1.json"
|
|
|
|
|
|
def _relative(path: Path) -> str:
|
|
try:
|
|
return str(path.resolve().relative_to(REPOSITORY_ROOT))
|
|
except ValueError:
|
|
return str(path.resolve())
|
|
|
|
|
|
def reviewed_population(args: argparse.Namespace) -> list[SourceRecord]:
|
|
sources = (
|
|
[path.resolve() for path in args.source]
|
|
if args.source
|
|
else default_source_paths()
|
|
)
|
|
fixtures = load_classifiable_fixtures(args.fixtures.resolve())
|
|
lexical = curate_records(
|
|
load_sources(sources),
|
|
fixtures,
|
|
near_duplicate_threshold=args.near_duplicate_threshold,
|
|
)
|
|
return exclude_reviewed_duplicates(
|
|
lexical.records,
|
|
load_reviewed_semantic_exclusions(args.curation_review.resolve()),
|
|
).records
|
|
|
|
|
|
def review_policy_status(path: Path) -> str:
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
status = value["humanLabelAndDifficultyReview"]["status"]
|
|
except (OSError, UnicodeError, json.JSONDecodeError, KeyError, TypeError) as exc:
|
|
raise DataError(f"{path}: cannot read human-review policy: {exc}") from exc
|
|
if not isinstance(status, str) or not status:
|
|
raise DataError(f"{path}: invalid human-review policy status")
|
|
return status
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--source", action="append", type=Path)
|
|
parser.add_argument("--fixtures", type=Path, default=DEFAULT_FIXTURES)
|
|
parser.add_argument(
|
|
"--curation-review",
|
|
type=Path,
|
|
default=DEFAULT_CURATION_REVIEW,
|
|
help="completed semantic duplicate review used to reconstruct the sample",
|
|
)
|
|
parser.add_argument("--review-csv", type=Path, default=DEFAULT_REVIEW_CSV)
|
|
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
|
parser.add_argument("--review-fraction", type=float, default=DEFAULT_SAMPLE_FRACTION)
|
|
parser.add_argument("--review-seed", type=int, default=DEFAULT_SAMPLE_SEED)
|
|
parser.add_argument("--near-duplicate-threshold", type=float, default=0.92)
|
|
action = parser.add_mutually_exclusive_group()
|
|
action.add_argument(
|
|
"--finalize",
|
|
action="store_true",
|
|
help="write the versionable review ledger; fails unless every row is complete",
|
|
)
|
|
action.add_argument(
|
|
"--regenerate",
|
|
action="store_true",
|
|
help="replace the blank review CSV from the deterministic current sample",
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
try:
|
|
population = reviewed_population(args)
|
|
policy_status = review_policy_status(args.curation_review.resolve())
|
|
sample = stratified_review_sample(
|
|
population,
|
|
fraction=args.review_fraction,
|
|
seed=args.review_seed,
|
|
)
|
|
if args.regenerate:
|
|
if args.review_csv.exists():
|
|
existing = inspect_review_csv(
|
|
args.review_csv.resolve(),
|
|
sample,
|
|
source_formatter=_relative,
|
|
)
|
|
if existing.completed:
|
|
raise DataError(
|
|
f"{args.review_csv}: refusing to replace "
|
|
f"{existing.completed} completed review rows"
|
|
)
|
|
write_review_csv(
|
|
args.review_csv.resolve(),
|
|
sample,
|
|
source_formatter=_relative,
|
|
)
|
|
print(
|
|
f"Regenerated {len(sample)} blank human-review rows at "
|
|
f"{_relative(args.review_csv)}."
|
|
)
|
|
return 0
|
|
progress = inspect_review_csv(
|
|
args.review_csv.resolve(),
|
|
sample,
|
|
source_formatter=_relative,
|
|
)
|
|
if args.finalize:
|
|
artifact = finalize_human_review(
|
|
args.review_csv.resolve(),
|
|
args.output.resolve(),
|
|
population,
|
|
dataset_version=DATASET_VERSION,
|
|
fraction=args.review_fraction,
|
|
seed=args.review_seed,
|
|
source_formatter=_relative,
|
|
)
|
|
print(
|
|
f"Finalized {artifact['sampleRecords']} human-review decisions at "
|
|
f"{_relative(args.output)}."
|
|
)
|
|
return 0
|
|
except (DataError, OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(
|
|
f"Human review: {progress.completed}/{progress.records} complete "
|
|
f"({progress.accepted} accept, {progress.relabeled} relabel, "
|
|
f"{progress.rejected} reject, {progress.incomplete} remaining). "
|
|
f"Dataset policy: {policy_status}."
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|