"""Validate the fixed face-restoration benchmark before model evaluation. This deliberately separates benchmark curation from training. It has a ``--dry-run`` mode so the evaluation set can be checked without loading a model or processing private images. """ from __future__ import annotations import argparse import csv from dataclasses import dataclass from pathlib import Path from typing import Iterable REQUIRED_COLUMNS = {"id", "input_path", "reference_path", "category", "notes"} IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"} @dataclass(frozen=True) class BenchmarkSample: sample_id: str input_path: Path reference_path: Path | None category: str notes: str def _resolve_path(manifest_path: Path, raw_path: str) -> Path | None: raw_path = raw_path.strip() if not raw_path: return None path = Path(raw_path) return path if path.is_absolute() else manifest_path.parent / path def load_manifest(manifest_path: Path) -> list[BenchmarkSample]: """Load and validate a benchmark manifest without opening its images.""" if not manifest_path.is_file(): raise ValueError(f"Benchmark manifest was not found: {manifest_path}") with manifest_path.open("r", encoding="utf-8", newline="") as handle: reader = csv.DictReader(handle) columns = set(reader.fieldnames or []) missing = REQUIRED_COLUMNS - columns if missing: raise ValueError(f"Manifest is missing columns: {', '.join(sorted(missing))}") samples: list[BenchmarkSample] = [] seen_ids: set[str] = set() for row_number, row in enumerate(reader, start=2): sample_id = (row.get("id") or "").strip() category = (row.get("category") or "").strip() input_path = _resolve_path(manifest_path, row.get("input_path") or "") reference_path = _resolve_path(manifest_path, row.get("reference_path") or "") if not sample_id or not category or input_path is None: raise ValueError(f"Manifest row {row_number} needs id, input_path and category.") if sample_id in seen_ids: raise ValueError(f"Duplicate benchmark id: {sample_id}") if input_path.suffix.lower() not in IMAGE_SUFFIXES: raise ValueError(f"Unsupported input image type for {sample_id}: {input_path.suffix}") if reference_path and reference_path.suffix.lower() not in IMAGE_SUFFIXES: raise ValueError(f"Unsupported reference image type for {sample_id}: {reference_path.suffix}") seen_ids.add(sample_id) samples.append(BenchmarkSample(sample_id, input_path, reference_path, category, row.get("notes") or "")) if not samples: raise ValueError("Benchmark manifest has no samples.") return samples def validate_files(samples: Iterable[BenchmarkSample]) -> tuple[int, int]: """Return input/reference counts, failing closed for missing benchmark data.""" input_count = 0 reference_count = 0 for sample in samples: if not sample.input_path.is_file(): raise ValueError(f"Missing input for {sample.sample_id}: {sample.input_path}") input_count += 1 if sample.reference_path: if not sample.reference_path.is_file(): raise ValueError(f"Missing reference for {sample.sample_id}: {sample.reference_path}") reference_count += 1 return input_count, reference_count def main() -> None: parser = argparse.ArgumentParser(description="Validate the fixed restoration benchmark manifest.") parser.add_argument("--manifest", type=Path, required=True, help="CSV manifest for held-out benchmark pairs.") parser.add_argument("--dry-run", action="store_true", help="Validate only; required until the evaluator is configured.") args = parser.parse_args() if not args.dry_run: parser.error("Only --dry-run is enabled until a reviewed benchmark is available.") samples = load_manifest(args.manifest) input_count, reference_count = validate_files(samples) categories = sorted({sample.category for sample in samples}) print(f"Benchmark valid: {input_count} inputs, {reference_count} references") print(f"Categories: {', '.join(categories)}") if __name__ == "__main__": main()