A track recognised on 318 of 385 frames was owned on none, so the truth file named nobody while the matcher was accepting almost continuously. The correlation discount was an annihilator rather than an attenuator. Weight was 1 - P(same view), so once a track had one stored view every later frame of that same face scored ~0.01 and the belief stopped moving. One observation just over the accept threshold is logit(0.78) ~ 1.27, under the ownership bar — hence recognised always, owned never. Two changes, in the order they were found. Correlated evidence is now attenuated by effective sample size, n_eff = n / (1 + (n-1)·rho), each frame contributing the marginal gain. That has the right shape at both ends: uncorrelated evidence accumulates linearly, and a held pose converges on 1/rho rather than growing without bound. A constant floor was tried first and rejected — it grows linearly forever, so a long shot could out-argue genuinely varied evidence purely by lasting longer. Combination is now weighted lazy-OR: P = 1 - (1-P_old)·(1-p)^w, stored as log(1-P) so the update is additive and precision stays where it matters as P approaches 1. Each frame is new evidence that this track is that actor, and the belief is the probability that at least one sighting was right. It converges faster than summing log-odds at the same effective count — 2.98 vs 2.53 after two observations at p=0.78 — which is what a real clip needs. Note that summing log-odds was already a correct sequential Bayesian update: the matcher fits with prior 0.5, so logit(p) IS the per-frame log-likelihood ratio and the running sum carries the prior forward. It was not wrong, it was slow. What blocked ownership was the discount, not the combination rule. Also fixes a real correctness bug: the observation count lived on the discounter, which is shared by every track, so tracks pooled into one effective sample and each was discounted by how many others happened to be on screen. It is now a per-track parameter. The registry's frame scope holds its lock for its lifetime and the mutex is not recursive, so calling observe() inside a scope self-deadlocks. The pipeline never does — separate nodes — but the test did, and hung rather than failing. Documented at the call site. Verified end to end: the same clip that produced zero actors now identifies Bing Crosby and Dorothy Lamour with belief 0.97. Suite: 96 cases, 6142 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-025 | SR-002
50 lines
2.4 KiB
C++
50 lines
2.4 KiB
C++
#pragma once
|
|
#include "types.hpp"
|
|
#include <string>
|
|
|
|
// Load/save the actor gallery. HDF5 (.h5/.hdf5) is the only format written;
|
|
// legacy gallery.json files are still readable for backward compatibility but
|
|
// save_gallery always writes HDF5 regardless of the requested extension.
|
|
//
|
|
// GR-005 is preserved here by absence: this is the only path that serialises a
|
|
// gallery, and it reads and writes the local filesystem only. There is no
|
|
// upload, no client, and no encoder that could put an embedding on a wire — the
|
|
// public server refuses to carry one (SR-004/UR-012), and the prohibition holds
|
|
// on this side by there being nothing that would try.
|
|
//
|
|
/// TRACES: GR-005 | SR-005
|
|
//
|
|
// HDF5 layout:
|
|
// /embeddings float32 [N, 512] all actors' refs concatenated, row-major
|
|
// /offset int64 [A] first row of actor a in /embeddings
|
|
// /count int32 [A] number of refs for actor a
|
|
// /imdb_id /tmdb_id /jellyfin_id /name : variable-length string [A]
|
|
// /source_images : variable-length string [N], parallel to /embeddings rows
|
|
// /embedder/model_name : scalar var-len string attr — embedder file basename
|
|
// /embedder/model_sha256 : scalar var-len string attr — SHA-256 of that file
|
|
// /embedder/embed_dim : scalar int32 attr
|
|
// The GR-004 model binding. Absent group == unstamped
|
|
// (pre-GR-004 file); see gallery/embedder_stamp.hpp.
|
|
// /calibration/a, /b : scalar float32 attrs — Platt-sigmoid P(match|sim) fit
|
|
// /calibration/valid : scalar int8 attr (0/1)
|
|
// /calibration/hash : scalar uint64 attr — hash of the embeddings the fit
|
|
// was computed from; a mismatch means "recompute"
|
|
//
|
|
// Legacy JSON format (read-only):
|
|
// {
|
|
// "embedder": {"model_name": "...", "model_sha256": "...", "embed_dim": 512},
|
|
// "actors": [
|
|
// {
|
|
// "imdb_id": "nm0000093", // optional, "" if unknown
|
|
// "tmdb_id": "287", // optional, "" if unknown
|
|
// "jellyfin_id": "abc123-guid", // optional, "" unless from make_jellyfin_gallery.py
|
|
// "name": "Brad Pitt",
|
|
// "source_images": ["img1.jpg", "img2.jpg"],
|
|
// "embeddings": [[0.012, -0.034, ...], ...] // one 512-float array per image
|
|
// }
|
|
// ]
|
|
// }
|
|
|
|
ActorGallery load_gallery(const std::string& path);
|
|
void save_gallery(const std::string& path, const ActorGallery& gallery);
|