"""Curated before/after pair registry for the ``/curated`` demo mode (PRD ยง10.1). A curated *pair* is two co-registered RGB tiles of the same place at two times. On the real Space these are LEVIR-CD test samples (plus a couple of well-known change events); the registry reads a ``manifest.json`` describing each pair and serves the images from disk. ``scripts/gen_sample_pairs`` can synthesize placeholder pairs so the pipeline is demonstrable before the real tiles are staged. """ from __future__ import annotations import json from pathlib import Path from typing import Any from PIL import Image class CuratedRegistry: """Reads ``/manifest.json`` -> pairs, each with ``before.png`` / ``after.png``.""" def __init__(self, data_dir: str | Path) -> None: self.data_dir = Path(data_dir) self.pairs: dict[str, dict[str, Any]] = {} self.reload() def reload(self) -> None: self.pairs.clear() manifest = self.data_dir / "manifest.json" if not manifest.exists(): return for entry in json.loads(manifest.read_text()).get("pairs", []): pid = str(entry["id"]) if (self.data_dir / pid / "before.png").exists(): self.pairs[pid] = entry def list(self) -> list[dict[str, Any]]: out = [] for pid, entry in self.pairs.items(): before = self.data_dir / pid / "before.png" with Image.open(before) as im: w, h = im.size out.append( { "id": pid, "title": entry.get("title", pid), "description": entry.get("description", ""), "source": entry.get("source", ""), "width": w, "height": h, } ) return out def image_path(self, pair_id: str, which: str) -> Path: if pair_id not in self.pairs: raise KeyError(pair_id) if which not in ("before", "after"): raise ValueError(which) return self.data_dir / pair_id / f"{which}.png" def get_pair(self, pair_id: str) -> tuple[Image.Image, Image.Image]: before = Image.open(self.image_path(pair_id, "before")).convert("RGB") after = Image.open(self.image_path(pair_id, "after")).convert("RGB") return before, after