The parser reads a tag up to end of line, so `# TRACES: GR-004 | SR-001 — prose` swallowed the prose into the tag and the row went unmatched. Splitting the comment leaves the tag greppable by the same pattern as the code tags and the commit trailers, which is the point of the house format. Mechanical throughout; no logic touched. The regenerated report reflects this session's new tags: 137 -> 148 found, and one more tagged-but-unexecuted, which is the SuperHero accuracy assertion that is documented but not yet a test.
217 lines
8.8 KiB
Python
Executable File
217 lines
8.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""make_gallery.py — fetch actor images for a movie and build gallery.h5.
|
|
|
|
Fetches the cast from TMDB, downloads actor profile images, embeds them via
|
|
the sae_embed module (SCRFD + ArcFace, same models as scene_analyze, loaded
|
|
once), then writes gallery.h5.
|
|
|
|
Requirements:
|
|
pip install requests Pillow
|
|
|
|
Usage:
|
|
# By IMDB movie ID (most natural — resolves to TMDB automatically):
|
|
python scripts/make_gallery.py \\
|
|
--tmdb-key YOUR_KEY \\
|
|
--imdb-id tt0137523 \\
|
|
--output gallery.h5
|
|
|
|
# Or directly with a TMDB movie ID:
|
|
python scripts/make_gallery.py \\
|
|
--tmdb-key YOUR_KEY \\
|
|
--movie-id 550 \\
|
|
--output gallery.h5
|
|
|
|
# Additional options:
|
|
# --build-dir build/ build dir containing sae_embed module
|
|
# --models-dir models/ directory with ONNX models
|
|
# --images-per-actor 3 profile images to download per actor
|
|
# --image-dir /tmp/gallery_imgs where to cache downloaded images
|
|
|
|
Get a free TMDB API key at: https://www.themoviedb.org/settings/api
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from sae_embed_loader import load_embedder, resolve_arcface
|
|
from sae_gallery import (download_images, embedder_stamp, save_gallery,
|
|
wikidata_image_urls)
|
|
from sae_tmdb import TMDB_IMG, tmdb_get, tmdb_id_from_imdb
|
|
|
|
|
|
def fetch_cast(movie_id: int, key: str) -> list[dict]:
|
|
"""Return list of {id, name, imdb_id, profile_images: [...url...]}."""
|
|
credits = tmdb_get(f"/movie/{movie_id}/credits", key)
|
|
cast = credits.get("cast", [])
|
|
|
|
actors = []
|
|
for member in cast:
|
|
person_id = member["id"]
|
|
|
|
# Get IMDB ID for this person
|
|
ext = tmdb_get(f"/person/{person_id}/external_ids", key)
|
|
imdb_id = ext.get("imdb_id") or ""
|
|
|
|
# Get profile images (sorted by vote_average desc by TMDB)
|
|
images_data = tmdb_get(f"/person/{person_id}/images", key)
|
|
profiles = images_data.get("profiles", [])
|
|
image_urls = [TMDB_IMG + p["file_path"] for p in profiles if p.get("file_path")]
|
|
|
|
if not image_urls:
|
|
image_urls = wikidata_image_urls(imdb_id)
|
|
if image_urls:
|
|
print(f" [info] no TMDB images for {member['name']}, "
|
|
f"found {len(image_urls)} via Wikidata", file=sys.stderr)
|
|
|
|
if not image_urls:
|
|
print(f" [warn] no images for {member['name']}", file=sys.stderr)
|
|
|
|
actors.append({
|
|
"id": person_id,
|
|
"name": member["name"],
|
|
"imdb_id": imdb_id,
|
|
"tmdb_id": str(person_id),
|
|
"profile_images": image_urls,
|
|
})
|
|
time.sleep(0.05) # be polite to TMDB
|
|
|
|
return actors
|
|
|
|
|
|
# ── Gallery assembly ─────────────────────────────────────────────────────────
|
|
|
|
def build_gallery(movie_id: int, key: str, embedder,
|
|
images_per_actor: int,
|
|
image_root: Path) -> tuple[dict, list[dict]]:
|
|
"""Fetch cast, download images, embed, return (gallery dict, actors needing more images)."""
|
|
print(f"Fetching cast for TMDB movie {movie_id}…", file=sys.stderr)
|
|
actors = fetch_cast(movie_id, key)
|
|
print(f"Found {len(actors)} cast member(s)", file=sys.stderr)
|
|
|
|
gallery_actors = []
|
|
missing = []
|
|
|
|
for actor in actors:
|
|
safe_name = actor["name"].replace(" ", "_")
|
|
dir_id = actor["imdb_id"] or f"tmdb_{actor['tmdb_id']}"
|
|
actor_dir = image_root / f"{dir_id}_{safe_name}"
|
|
|
|
print(f"\n{actor['name']} ({dir_id})", file=sys.stderr)
|
|
|
|
if not actor["profile_images"]:
|
|
print(" no images found, skipping", file=sys.stderr)
|
|
missing.append({"name": actor["name"], "imdb_id": actor["imdb_id"],
|
|
"tmdb_id": actor["tmdb_id"], "reason": "no images found"})
|
|
continue
|
|
|
|
image_paths = download_images(actor["profile_images"], actor_dir, images_per_actor)
|
|
if not image_paths:
|
|
print(" no images downloaded, skipping", file=sys.stderr)
|
|
missing.append({"name": actor["name"], "imdb_id": actor["imdb_id"],
|
|
"tmdb_id": actor["tmdb_id"], "reason": "download failed"})
|
|
continue
|
|
|
|
print(f" embedding {len(image_paths)} image(s)…", file=sys.stderr)
|
|
|
|
embeddings = []
|
|
source_images = []
|
|
for path in image_paths:
|
|
res = embedder.embed(str(path))
|
|
if not res.ok:
|
|
print(f" [skip] {path.name}: {res.error}", file=sys.stderr)
|
|
continue
|
|
embeddings.append(res.embedding)
|
|
source_images.append(path.name)
|
|
print(f" [ok] {path.name} conf={res.confidence:.2f}",
|
|
file=sys.stderr)
|
|
|
|
if not embeddings:
|
|
print(" no valid embeddings, skipping actor", file=sys.stderr)
|
|
missing.append({"name": actor["name"], "imdb_id": actor["imdb_id"],
|
|
"tmdb_id": actor["tmdb_id"], "reason": "no valid embeddings"})
|
|
continue
|
|
|
|
gallery_actors.append({
|
|
"imdb_id": actor["imdb_id"],
|
|
"tmdb_id": actor["tmdb_id"],
|
|
"jellyfin_id": "",
|
|
"name": actor["name"],
|
|
"source_images": source_images,
|
|
"embeddings": embeddings,
|
|
})
|
|
print(f" → {len(embeddings)} embedding(s) stored", file=sys.stderr)
|
|
|
|
return {"actors": gallery_actors}, missing
|
|
|
|
|
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Fetch TMDB cast images and build gallery.h5 via sae_embed")
|
|
parser.add_argument("--tmdb-key", required=True,
|
|
help="TMDB Bearer token (API Read Access Token from themoviedb.org/settings/api)")
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
group.add_argument("--imdb-id",
|
|
help="IMDB movie ID, e.g. tt0137523 — looked up via TMDB automatically")
|
|
group.add_argument("--movie-id", type=int,
|
|
help="TMDB movie ID (alternative to --imdb-id)")
|
|
parser.add_argument("--output", required=True, help="Output gallery.h5 path")
|
|
parser.add_argument("--build-dir", default="build",
|
|
help="Build directory containing the sae_embed module (default: build)")
|
|
parser.add_argument("--models-dir", default="models",
|
|
help="Directory containing ONNX models (default: models/)")
|
|
parser.add_argument("--arcface", default=None,
|
|
help="Path to ArcFace ONNX model (overrides --models-dir selection)")
|
|
parser.add_argument("--images-per-actor",type=int, default=3,
|
|
help="Profile images to download per actor (default: 3)")
|
|
parser.add_argument("--image-dir", default=None,
|
|
help="Where to store downloaded images (default: <output_dir>/images)")
|
|
parser.add_argument("--keep-images", action="store_true",
|
|
help="Do not delete downloaded images after embedding")
|
|
args = parser.parse_args()
|
|
|
|
# Resolve paths
|
|
output = Path(args.output)
|
|
image_root = Path(args.image_dir) if args.image_dir else output.parent / "images"
|
|
|
|
# TRACES: GR-004 | SR-001
|
|
# stamp with the model actually loaded, resolved
|
|
# through the same helper load_embedder uses so the two cannot diverge.
|
|
arcface_path = resolve_arcface(args.models_dir, args.arcface)
|
|
embedder = load_embedder(args.build_dir, args.models_dir, args.arcface)
|
|
stamp = embedder_stamp(arcface_path)
|
|
|
|
# Resolve movie ID
|
|
movie_id = args.movie_id
|
|
if movie_id is None:
|
|
print(f"Resolving IMDB ID {args.imdb_id} → TMDB…", file=sys.stderr)
|
|
movie_id = tmdb_id_from_imdb(args.imdb_id, args.tmdb_key)
|
|
print(f"TMDB movie ID: {movie_id}", file=sys.stderr)
|
|
|
|
# Build gallery
|
|
gallery, missing = build_gallery(
|
|
movie_id = movie_id,
|
|
key = args.tmdb_key,
|
|
embedder = embedder,
|
|
images_per_actor = args.images_per_actor,
|
|
image_root = image_root,
|
|
)
|
|
|
|
n_actors = len(gallery["actors"])
|
|
n_embeddings = sum(len(a["embeddings"]) for a in gallery["actors"])
|
|
print(f"\nGallery: {n_actors} actors, {n_embeddings} total embeddings",
|
|
file=sys.stderr)
|
|
|
|
if n_actors == 0:
|
|
sys.exit("No actors could be processed — check models and images.")
|
|
|
|
save_gallery(gallery, missing, output, embedder=stamp)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|