AR-018 — an embedding joins a track's store only if its similarity to something already there falls inside a band, rather than merely being far from the gallery. Above the upper bound it is redundant: another look at a pose the store already covers, teaching the annex nothing while costing a slot a novel view could have used. Below the lower bound it is suspect: within one track every face is the same person by construction, so an embedding unlike everything else on the track is evidence that construction failed — a track-ID collision or a bad detection. Admitting it is exactly how an actor's annex gets poisoned with someone else's face. The old gate had only the upper half of that idea, expressed as a raw cosine against the gallery. Both bounds are now calibrated probabilities (AR-024), so the same number means the same thing here as in association and evidence weighting rather than three different things. This catches track-ID collisions EARLIER than the spread gate did — at the door rather than at promotion — so the buffer never becomes two-person in the first place. The spread gate stays as a second line for a track that drifts gradually instead of jumping. The existing test was asserting the mechanism rather than the outcome, so it was rewritten to assert what actually matters: whichever gate fires, the outsider must not reach the annex. Rejections are counted. A store that admits nothing is as broken as one that admits everything, and neither is visible otherwise. Band defaults 0.90-0.95 are working values pending VR-007; the two bounds fail in opposite directions and must be swept separately. Suite: 92 cases, 6133 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-018, AR-024 | SR-005
181 lines
13 KiB
C++
181 lines
13 KiB
C++
#pragma once
|
||
#include "inference/backend_config.hpp"
|
||
#include <string>
|
||
|
||
inline const std::string kDefaultDetectorModel = std::string(SAE_MODELS_DIR) + "/scrfd_500m_bnkps.onnx";
|
||
inline const std::string kDefaultArcfaceModel = std::string(SAE_MODELS_DIR) + "/LVFace-B_Glint360K.onnx";
|
||
inline const std::string kDefaultSceneModel = std::string(SAE_MODELS_DIR) + "/transnetv2.onnx";
|
||
|
||
enum class Verbosity {
|
||
minimal, // actor names + merged time windows only
|
||
standard, // per-frame detail: bbox, similarity, unknowns logged
|
||
xray, // Jellyfin-Xray format: {"second": ["Actor", ...], ...}
|
||
};
|
||
// debug verbosity = compile with -DSAE_DEBUG → scene_analyze_debug binary
|
||
|
||
struct Config {
|
||
// ── Input ─────────────────────────────────────────────────────────────────
|
||
std::string movie_path;
|
||
std::string gallery_path;
|
||
// TRACES: IR-002 | SR-003
|
||
// "global" (matched against the whole library) or "limited" (this title's
|
||
// credited cast only). The strongest single quality signal when two
|
||
// manifests compete for the same cut: identical gallery_size can mean very
|
||
// different recall depending on which was used.
|
||
std::string gallery_scope{"global"}; // gallery.json produced by build_gallery
|
||
|
||
// ── Output ───────────────────────────────────────────────────────────────
|
||
std::string output_path; // annotations.json
|
||
Verbosity verbosity{Verbosity::minimal};
|
||
|
||
// When set, tee the embedder output to an HDF5 dump (schema:
|
||
// scripts/optimizer/SCHEMA.md) for offline threshold-sweep replay via sae_kpn.
|
||
std::string dump_embeddings_path;
|
||
|
||
// ── Sampling ─────────────────────────────────────────────────────────────
|
||
float sample_fps{1.0f}; // frames to analyse per second of movie
|
||
float max_decode_fps{0.f}; // wall-clock cap on source decode rate (0 = uncapped)
|
||
double start_sec{0.0}; // seek to this timestamp before sampling
|
||
double end_sec{-1.0}; // stop at this timestamp (-1 = end of file)
|
||
|
||
// ── Detection (SCRFD-500MF via cv::dnn::Net) ──────────────────────────────
|
||
std::string detector_model;
|
||
std::string detector_engine; // optional path to pre-built TRT engine; bypasses ORT
|
||
// TRACES: AR-003 | SR-002
|
||
// 0 = no cap, the default. A fixed cap discards the SMALLEST faces first,
|
||
// which are exactly the background cast X-Ray still credits with scene
|
||
// membership. Per-frame cost is contained by backpressure (AR-004) rather
|
||
// than by throwing work away. Set >0 only to bound a pathological source.
|
||
int max_faces{0};
|
||
float min_face_px{40.f}; // discard detections narrower or shorter than this
|
||
float detector_conf{0.5f};
|
||
float detector_nms{0.4f};
|
||
|
||
/// TRACES: GR-004 | SR-001
|
||
// Gallery ↔ embedder binding. A gallery built with a different model than the
|
||
// one loaded here is a hard error, always. This flag additionally promotes
|
||
// "cannot prove they match" (unstamped legacy gallery, or a name-only match
|
||
// because the ONNX could not be hashed) from a loud warning to a hard error.
|
||
// Also settable via SAE_REQUIRE_GALLERY_STAMP=1. Measurement runs want it on.
|
||
bool require_gallery_stamp{false}; // --require-gallery-stamp
|
||
|
||
// ── Recognition (ArcFace ONNX) ────────────────────────────────────────────
|
||
std::string arcface_model;
|
||
std::string arcface_engine; // optional path to a pre-built TRT engine; bypasses ORT
|
||
int embed_batch_size{4}; // max faces per ORT Run() call — bounds per-call latency
|
||
float match_prior{0.5f}; // base-rate prior; 0.5 = use calibrated sigmoid directly
|
||
// prob_threshold tuned by Differential Evolution against Amazon X-Ray per-scene
|
||
// presence over 4 films, per-second metric (see docs/rep4-optimizer-results.md).
|
||
// Best model+mode: LVFace-B_Glint360K, full gallery, expansion on. Supersedes the
|
||
// earlier 9-film scene-union-metric tuning (0.76) — that metric is now known to
|
||
// have hidden out-of-cast false positives (see docs/optimizer-experiments.md).
|
||
float prob_threshold{0.754f}; // posterior P(match | sim, prior) threshold
|
||
float match_threshold{0.45f}; // cosine distance hard ceiling fallback (no calibration)
|
||
float match_ratio{0.80f}; // ratio test fallback: accept if best/second < ratio
|
||
float match_ratio_ceil{0.65f}; // ratio test only fires below this absolute distance
|
||
|
||
// ── Cut detection ────────────────────────────────────────────────────────
|
||
float cut_threshold{0.70f}; // grayscale histogram correlation below this → hard cut
|
||
|
||
// ── Scene detection (TransNetV2, opt-in) ─────────────────────────────────
|
||
// When enabled, the source decodes densely (native FPS) and a decimator
|
||
// splits the stream: full-res 1-FPS frames to the face pipeline, and a
|
||
// downscaled dense stream to the TransNetV2 scene detector. Shot boundaries
|
||
// it finds are surfaced as Frame::is_scene_boundary. This is separate from
|
||
// the always-on histogram cut, which flags intra-scene camera-angle changes.
|
||
bool scene_detect{false}; // master switch (--scene-detect)
|
||
std::string scene_model; // TransNetV2 .onnx (default set in main)
|
||
std::string scene_engine; // optional pre-built TRT .engine; bypasses ORT
|
||
float scene_threshold{0.60f}; // sigmoid boundary prob above this → boundary
|
||
// (this export's non-boundary baseline sits
|
||
// at ~0.50; real boundaries spike to ~0.7+)
|
||
int scene_stride{50}; // frames advanced between windows (≤ kWindow)
|
||
|
||
// Dense-decode throughput knobs (only active with scene_detect). Dense decode
|
||
// of every native-rate frame is the pipeline's cost driver; these trade a
|
||
// little boundary precision for a large speedup.
|
||
// scene_decode_fps: rate the source decodes at in dense mode. Lower =
|
||
// fewer frames decoded. TransNetV2 tolerates ~12fps; boundary timestamps
|
||
// stay correct (keyed off each frame's real timestamp). 0 = native fps.
|
||
// dense_scale: downscale factor applied to decoded frames in dense mode
|
||
// (0<f≤1; e.g. 0.5 = half size). Cheaper sws_scale + smaller frames
|
||
// through the fanout. NOTE: also shrinks what the face detector sees —
|
||
// keep ≥0.5 on 1080p sources so SCRFD still resolves small faces. 1 = off.
|
||
float scene_decode_fps{12.0f}; // dense decode rate (0 = native)
|
||
float dense_scale{1.0f}; // dense-mode frame downscale (1 = off)
|
||
|
||
// ── Face tracking (frame-to-frame) ───────────────────────────────────────
|
||
/// TRACES: AR-007, AR-008, AR-024 | SR-002
|
||
// track_alpha is the *base* weight, used on ordinary frames. It is
|
||
// frame-dependent (AR-007): on is_cut / is_scene_boundary, and for any track
|
||
// that is no longer on screen, it drops to 0 (embedding only), because
|
||
// position carries no information across a viewpoint change or a gap.
|
||
float track_alpha{0.4f}; // base cost weight: 0=embedding only, 1=spatial only
|
||
float track_min_iou{0.1f}; // IoU below which spatial link alone is rejected
|
||
// Minimum P(same person) for an association to be admissible on appearance
|
||
// alone. This replaces track_max_embed_dist (a raw cosine distance, AR-024).
|
||
// 0.5 is not a tuned constant: it is the decision boundary. Below it the pair
|
||
// is more likely two people than one, and no amount of IoU makes that a link
|
||
// worth asserting on identity grounds.
|
||
float track_assoc_min_prob{0.5f};
|
||
// How long a track that has gone off screen stays available for association
|
||
// before the registry reaps it and emits its presence claim (AR-013).
|
||
// Replaces track_max_frames_missing: a frame count silently changed meaning
|
||
// with sample_fps, and the same number had to be guessed twice (once for an
|
||
// ordinary miss, once for a cut). Seconds mean one thing at any sample rate.
|
||
double track_extinction_sec{5.0};
|
||
|
||
// ── Scene tracking ────────────────────────────────────────────────────────
|
||
// extinction_sec re-tuned by DE against X-Ray per-second presence, 4-film rep4
|
||
// matrix (docs/rep4-optimizer-results.md). Reverses the earlier "short is better"
|
||
// finding: with a stricter prob_threshold, a long extinction window bridges real
|
||
// presence gaps (occlusion, turned face) instead of just smearing FPs — every
|
||
// model's best config pushed to ~90%+ of the search ceiling (tried up to 60s).
|
||
// The ceiling kept getting hit, so treat 60 as "good enough", not a proven optimum.
|
||
double extinction_sec{57.4}; // keep actor active this many seconds after last detection
|
||
// anneal_sec: previously found INSENSITIVE at a 1–30s range; the wider rep4 sweep
|
||
// (1–60s) also pushed this to the ceiling alongside extinction_sec (see above).
|
||
double anneal_sec{35.5}; // merge actor windows separated by less than this into one epoch
|
||
|
||
// ── Per-film gallery expansion ────────────────────────────────────────────
|
||
// Within one uncut track every face is the same physical person — a free
|
||
// same-identity label the baked gallery lacks. When a track is confidently
|
||
// owned by an actor, its gallery-far (pose-varied) embeddings are validated
|
||
// new reference views; they are promoted into a per-film, in-memory annex so
|
||
// later frames/tracks of that actor at similar poses recognise. See
|
||
// gallery/track_gallery.hpp.
|
||
// Default ON: rep4 matrix (docs/rep4-optimizer-results.md) found expansion helps
|
||
// recall on the full (unrestricted) gallery for the winning model/mode — the
|
||
// opposite of the earlier assumption that it only helps restricted galleries.
|
||
bool expand_gallery{true}; // master switch
|
||
int expand_buffer_size{20}; // per-track diversity buffer capacity
|
||
// TRACES: AR-018, AR-024 | SR-005
|
||
// Banded admission for the per-subject store, in PROBABILITY space. An
|
||
// embedding joins only if P(same person) against something already stored
|
||
// lands inside [lo, hi]: above hi it is redundant, below lo it is evidence
|
||
// the track is not one person. Replaces expand_novelty_sim, a raw cosine.
|
||
// Working values pending VR-007; sweep both bounds, they fail in opposite
|
||
// directions.
|
||
float expand_band_lo{0.90f};
|
||
float expand_band_hi{0.95f};
|
||
float expand_novelty_sim{0.55f}; // promote only embeddings whose best sim to the
|
||
// actor's refs is below this (gallery-far / novel)
|
||
float expand_track_spread_max{0.60f}; // reject promotion if the retained buffer's
|
||
// internal spread (1 - min pairwise sim) exceeds
|
||
// this — guards track-ID collisions / two people
|
||
int expand_min_anchor_frames{3}; // require ≥N accepted frames naming the actor before
|
||
// the track is confirmed and its buffer promoted
|
||
std::string expand_debug_dir; // if set, dump promoted mugshots + embeddings here
|
||
|
||
// ── Inference backend tuning ────────────────────────────────────────────
|
||
// Consumed by the compiled-in inference backend (ORT or TRT).
|
||
// INT8 is unsafe for ArcFace without a calibration table.
|
||
BackendConfig trt{}; // fp16=true, int8=false, cache_dir="./trt_cache"
|
||
|
||
// ── Debug output (only used when SAE_DEBUG is defined) ───────────────────
|
||
#ifdef SAE_DEBUG
|
||
std::string debug_dir{"debug_frames"};
|
||
float crop_context{1.5f}; // bbox expansion factor for context crop
|
||
#endif
|
||
};
|