#!/usr/bin/env python3 """ tmdb_imdb_map.py — build & consult a cached tmdb_person_id → imdb_id crosswalk. The gallery (and pipeline output) key actors by TMDB person id but carry no IMDb `nm…` id, while the X-Ray / MovieNet ground truth keys on IMDb. Rather than join on fuzzy names, we resolve tmdb→imdb once via TMDB's authoritative `/person/{id}/external_ids` endpoint and cache the result to JSON. The eval loaders consult this table to add an exact `imdb:` key alongside each `tmdb:` key. Table format (JSON): { "": "nm0000123" | null, ... } A null means "looked up, TMDB has no IMDb id" — cached so we don't re-query. Build / refresh the table: python scripts/validation/tmdb_imdb_map.py \ --gallery gallery_arcface_w600k_r50.json \ --out scripts/validation/tmdb_imdb.json # TMDB_API_KEY read from env / .env (via sae_env) Consult it from code: from tmdb_imdb_map import CrosswalkTable tbl = CrosswalkTable.load("scripts/validation/tmdb_imdb.json") nm = tbl.imdb_for("35467") # -> "nm..." or None """ from __future__ import annotations import argparse import json import os import sys import time from pathlib import Path _HERE = Path(__file__).resolve().parent class CrosswalkTable: """Read-only view over the cached tmdb→imdb JSON. Missing file → empty table.""" def __init__(self, mapping: dict[str, str | None]): self._m = mapping @classmethod def load(cls, path: str | Path) -> "CrosswalkTable": p = Path(path) if not p.exists(): return cls({}) return cls(json.loads(p.read_text())) def imdb_for(self, tmdb_id) -> str | None: if tmdb_id is None: return None return self._m.get(str(tmdb_id)) def __len__(self) -> int: return sum(1 for v in self._m.values() if v) # ── builder ───────────────────────────────────────────────────────────────── def _external_ids(tmdb_get, tmdb_person_id: str, token: str) -> str | None: data = tmdb_get(f"/person/{tmdb_person_id}/external_ids", token) imdb = data.get("imdb_id") return imdb or None # normalize "" → None def build(gallery_path: str, out_path: str, token: str, sleep: float = 0.0) -> None: # import the existing TMDB helper (scripts/ is the parent dir) sys.path.insert(0, str(_HERE.parent)) from sae_tmdb import tmdb_get with open(gallery_path) as f: actors = json.load(f).get("actors", []) tmdb_ids = sorted({str(a["tmdb_id"]) for a in actors if a.get("tmdb_id")}) print(f"[map] gallery tmdb ids: {len(tmdb_ids)}", file=sys.stderr) out = Path(out_path) existing: dict[str, str | None] = {} if out.exists(): existing = json.loads(out.read_text()) print(f"[map] resuming from {len(existing)} cached entries", file=sys.stderr) todo = [t for t in tmdb_ids if t not in existing] print(f"[map] to resolve: {len(todo)}", file=sys.stderr) n_ok = n_none = n_err = 0 for i, tid in enumerate(todo, 1): try: nm = _external_ids(tmdb_get, tid, token) existing[tid] = nm n_ok += (nm is not None) n_none += (nm is None) except Exception as e: # network/rate-limit/404 — record nothing, keep going n_err += 1 print(f"\n[map] error on tmdb {tid}: {e}", file=sys.stderr) if i % 25 == 0 or i == len(todo): print(f"\r[map] {i}/{len(todo)} resolved " f"(imdb={n_ok} none={n_none} err={n_err})", end="", file=sys.stderr) out.write_text(json.dumps(existing, indent=2)) # checkpoint if sleep: time.sleep(sleep) out.write_text(json.dumps(existing, indent=2)) print(f"\n[map] wrote {out} — {sum(1 for v in existing.values() if v)} imdb ids", file=sys.stderr) def main(): # load .env → os.environ (same convention as the gallery builders) sys.path.insert(0, str(_HERE.parent)) try: import sae_env # noqa: F401 (side-effect import) except Exception: pass p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--gallery", required=True, help="gallery.json (source of tmdb ids)") p.add_argument("--out", default=str(_HERE / "tmdb_imdb.json"), help="output crosswalk JSON (default: scripts/validation/tmdb_imdb.json)") p.add_argument("--tmdb-key", default=os.environ.get("TMDB_API_KEY"), help="TMDB v3 API key or v4 read token. Env: TMDB_API_KEY") p.add_argument("--sleep", type=float, default=0.0, help="seconds between requests (TMDB has no hard limit; use if throttled)") args = p.parse_args() if not args.tmdb_key: sys.exit("[map] no TMDB key — set TMDB_API_KEY or pass --tmdb-key") build(args.gallery, args.out, args.tmdb_key, args.sleep) if __name__ == "__main__": main()