| """Validate local assets needed for an Orienter smoke or reproduction run.""" |
|
|
| import argparse |
| import hashlib |
| import json |
| import sys |
| from pathlib import Path |
|
|
|
|
| if __package__ in {None, ""}: |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
|
|
| from approach.app_metadata import load_app_metadata_cache |
| from approach.pipeline_utils import parse_orienter_image_name, resolve_image_path |
|
|
|
|
| CHECKPOINT_RELATIVE_PATH = Path("approach/ovod/APE/ape_d_model_final.pth") |
| CHECKPOINT_SHA256 = "3548f41a3238148180e08fd4b16c71f4abc3ac3caf9c8434444462d1bdb7f965" |
| CHECKPOINT_SIZE = 5_956_547_279 |
|
|
|
|
| def sha256_file(path: Path): |
| digest = hashlib.sha256() |
| with path.open("rb") as file: |
| for chunk in iter(lambda: file.read(1024 * 1024), b""): |
| digest.update(chunk) |
| return digest.hexdigest() |
|
|
|
|
| def load_jsonl(path: Path): |
| with path.open(encoding="utf-8") as file: |
| return [json.loads(line) for line in file if line.strip()] |
|
|
|
|
| def check_checkpoint(repo_root: Path, hash_checkpoint: bool): |
| checkpoint = repo_root / CHECKPOINT_RELATIVE_PATH |
| if not checkpoint.is_file(): |
| raise FileNotFoundError(f"Missing APE checkpoint: {checkpoint}") |
| size = checkpoint.stat().st_size |
| if size != CHECKPOINT_SIZE: |
| raise ValueError(f"Unexpected checkpoint size: {size}") |
| result = {"path": str(CHECKPOINT_RELATIVE_PATH), "size_bytes": size} |
| if hash_checkpoint: |
| digest = sha256_file(checkpoint) |
| if digest != CHECKPOINT_SHA256: |
| raise ValueError(f"Unexpected checkpoint sha256: {digest}") |
| result["sha256"] = digest |
| return result |
|
|
|
|
| def check_questions(questions_path: Path, images_dir: Path): |
| records = load_jsonl(questions_path) |
| if not records: |
| raise ValueError(f"Question manifest is empty: {questions_path}") |
| seen_question_ids = set() |
| seen_image_ids = set() |
| app_ids = set() |
| image_ids = [] |
| for record in records: |
| question_id = record["question_id"] |
| if question_id in seen_question_ids: |
| raise ValueError(f"Duplicate question_id: {question_id}") |
| seen_question_ids.add(question_id) |
| image_name = record["image"] |
| image_path = resolve_image_path(images_dir, image_name) |
| if not image_path.is_file(): |
| raise FileNotFoundError(f"Question references a missing image: {image_path}") |
| app_id, _, image_id = parse_orienter_image_name(Path(image_name).name) |
| if image_id in seen_image_ids: |
| raise ValueError(f"Duplicate image_id in question manifest: {image_id}") |
| seen_image_ids.add(image_id) |
| if "image_id" in record and int(record["image_id"]) != image_id: |
| raise ValueError(f"image_id mismatch for {image_name}: {record['image_id']} != {image_id}") |
| app_ids.add(app_id) |
| image_ids.append(image_id) |
| return { |
| "questions": len(records), |
| "unique_question_ids": len(seen_question_ids), |
| "unique_images": len(set(image_ids)), |
| "app_ids": sorted(app_ids), |
| } |
|
|
|
|
| def check_metadata_cache(cache_path: Path, app_ids): |
| cache = load_app_metadata_cache(cache_path) |
| missing = [app_id for app_id in app_ids if str(app_id) not in cache] |
| if missing: |
| raise KeyError(f"Metadata cache is missing app IDs: {', '.join(missing)}") |
| return {"metadata_cache": str(cache_path), "covered_app_ids": len(app_ids)} |
|
|
|
|
| def check_embedding_cache(cache_path: Path, manifest_path: Path): |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| expected_size = int(manifest["size_bytes"]) |
| expected_sha256 = manifest["sha256"] |
| actual_size = cache_path.stat().st_size |
| if actual_size != expected_size: |
| raise ValueError( |
| f"Unexpected embedding cache size: {actual_size}; expected {expected_size}" |
| ) |
| actual_sha256 = sha256_file(cache_path) |
| if actual_sha256 != expected_sha256: |
| raise ValueError( |
| f"Unexpected embedding cache sha256: {actual_sha256}; expected {expected_sha256}" |
| ) |
| return { |
| "artifact": manifest.get("artifact", cache_path.name), |
| "size_bytes": actual_size, |
| "sha256": actual_sha256, |
| "entry_count": manifest.get("entry_count"), |
| "embedding_dimension": manifest.get("embedding_dimension"), |
| } |
|
|
|
|
| def build_parser(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--repo-root", type=Path, default=Path.cwd()) |
| parser.add_argument("--questions", type=Path) |
| parser.add_argument("--images-dir", type=Path) |
| parser.add_argument("--app-metadata-cache", type=Path) |
| parser.add_argument( |
| "--embedding-cache", |
| type=Path, |
| help="external frozen semantic cache to verify against evaluation/cache_manifest.json", |
| ) |
| parser.add_argument( |
| "--embedding-cache-manifest", |
| type=Path, |
| help="override the semantic cache manifest (defaults to the repository manifest)", |
| ) |
| parser.add_argument("--hash-checkpoint", action="store_true") |
| parser.add_argument("--skip-checkpoint", action="store_true") |
| return parser |
|
|
|
|
| def main(argv=None): |
| args = build_parser().parse_args(argv) |
| report = {} |
| if not args.skip_checkpoint: |
| report["checkpoint"] = check_checkpoint(args.repo_root, args.hash_checkpoint) |
| if args.embedding_cache: |
| manifest_path = args.embedding_cache_manifest or ( |
| args.repo_root / "evaluation" / "cache_manifest.json" |
| ) |
| report["embedding_cache"] = check_embedding_cache( |
| args.embedding_cache, |
| manifest_path, |
| ) |
| elif args.embedding_cache_manifest: |
| raise SystemExit("--embedding-cache-manifest requires --embedding-cache") |
| if args.questions or args.images_dir: |
| if not args.questions or not args.images_dir: |
| raise SystemExit("--questions and --images-dir must be supplied together") |
| report["questions"] = check_questions(args.questions, args.images_dir) |
| if args.app_metadata_cache: |
| report["metadata"] = check_metadata_cache( |
| args.app_metadata_cache, |
| report["questions"]["app_ids"], |
| ) |
| elif args.app_metadata_cache: |
| raise SystemExit("--app-metadata-cache requires --questions and --images-dir") |
| print(json.dumps(report, indent=2, sort_keys=True)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|