Optimizer (scripts/optimizer/): replay.py runs the real C++ tracker/matcher/ scene_tracker chain over a dumped-embeddings HDF5 via sae_kpn, so a threshold sweep never re-decodes video or re-embeds faces. optimize.py drives scipy's differential_evolution over the knob space, with DE-level parallelism (multiple population candidates evaluated concurrently via a ThreadPoolExecutor) on top of per-film replay parallelism. second_score.py is the per-second X-Ray scoring metric (TPI/FPI/FN, out-of-cast misID weighted 10x, fair recall masked to gallery-known cast) that superseded an earlier scene-union metric. dump_error_frames.py / dump_scene_montage.py extract annotated video frames (bounding boxes, TPI/FPI/FN captions, onscreen-vs-offscreen split) for visual review of a replay against ground truth. Gallery utilities: cast_restrict.py, gallery_membership.py, fetch_missing_actors.py, reembed_gallery.py. scripts/validation/: X-Ray ground-truth loading and provider-agnostic identity matching (identity.py's keys_for — an actor is the union of every id we can derive, since pipeline output and ground truth don't share one id space). scripts/artifacts/: push/pull scripts for the Gitea generic package registry — galleries, montage frames, and experiment data (manifests/trajectories/results) are pushed there instead of committed, since none are needed to run the app, only benchmarks. Versioned by git short-SHA. scripts/docs/: MkDocs site build (build_site.sh) and the calibration-curve comparison chart (calibration_chart.py, matplotlib, reads each gallery's embedded calibration). Gallery-building scripts (make_jellyfin_gallery.py, make_gallery.py, filter_gallery.py, run_from_jellyfin.py, movienet_eval.py, movienet_prep.py, sae_gallery.py) updated to read/write HDF5 galleries exclusively, matching the engine-side format switch. run_from_jellyfin.py and the optimizer no longer carry movie source paths in shared manifests (some source filenames include scene-release tags) — resolved locally via a gitignored file-lut.json instead.
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
cast_restrict.py — produce a per-film gallery restricted to its credited cast.
|
|
|
|
Benchmark arm: instead of matching a face against the WHOLE gallery (2418 actors,
|
|
risking cross-film misIDs like naming Archie Yates in a film he's not in), restrict
|
|
the matcher's candidate set to the title's credited cast (from Jellyfin — the top
|
|
~15 billed actors, exactly what run_from_jellyfin.py does in production).
|
|
|
|
Filters a gallery to actors whose jellyfin_id is in the film's cast set, writing a
|
|
small gallery JSON the replay can load. Actors are kept if their jellyfin_id (or, as
|
|
a fallback, normalized name) matches the cast.
|
|
|
|
Used by the full-vs-restricted bake-off. Cached per (gallery, film) so a DE sweep
|
|
reuses the restricted gallery.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent.parent
|
|
sys.path.insert(0, str(REPO / "scripts" / "validation"))
|
|
from identity import norm_name # noqa: E402
|
|
|
|
_CACHE: dict = {}
|
|
|
|
|
|
def restricted_gallery_path(gallery_path: str, cast_jellyfin_ids: set[str],
|
|
cast_names: set[str] | None = None) -> str:
|
|
"""Write (once, cached) a gallery filtered to the film's credited cast; return path.
|
|
|
|
Matches gallery actors to the cast by jellyfin_id first, then normalized name."""
|
|
key = (gallery_path, frozenset(cast_jellyfin_ids))
|
|
if key in _CACHE:
|
|
return _CACHE[key]
|
|
|
|
gal = json.loads(Path(gallery_path).read_text())
|
|
names = {norm_name(n) for n in (cast_names or set())}
|
|
kept = []
|
|
for a in gal["actors"]:
|
|
jid = a.get("jellyfin_id", "")
|
|
if (jid and jid in cast_jellyfin_ids) or (names and norm_name(a["name"]) in names):
|
|
kept.append(a)
|
|
|
|
tf = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False,
|
|
prefix="castgal_")
|
|
json.dump({"actors": kept}, tf)
|
|
tf.close()
|
|
_CACHE[key] = tf.name
|
|
return tf.name
|
|
|
|
|
|
def load_casts(casts_json: str) -> dict[str, list[str]]:
|
|
"""film name → [jellyfin person id, ...] from jellyfin_casts.json."""
|
|
return json.loads(Path(casts_json).read_text())
|