#pragma once #include "inference/backend_config.hpp" #include 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", ...], ...} }; // How a track's accepted frames become a reported presence window. enum class PresenceMode { // A claim IS its track's [first_seen, last_seen] (AR-012/AR-013). The // default and the only mode whose semantics the register validated. track_extent, // Flood-fill: snap each claim to the shot it sits in, so an actor seen once // anywhere in a scene is reported for the whole scene [prev_boundary, // next_boundary]. Trades precision for recall against X-Ray's per-scene cast // granularity. Snaps to TransNetV2 shot boundaries (is_scene_boundary) when a // scene detector populated them, else to the always-on histogram cuts // (is_cut). With no boundaries at all it degrades to track_extent per claim. flood, }; // 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; /// TRACES: VR-015 | PR-004 // When set, write a per-node timing and bottleneck report here (src/ // benchmark.hpp) and print it at shutdown. Costs one background thread // reading relaxed atomics on a timer, so it is safe to leave on, but a // measurement run should still be isolated (nothing else on the GPU). std::string benchmark_path; int benchmark_interval_ms{100}; // channel-occupancy sampling period // ── 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.433f}; // base-rate prior; 10-knob DE optimum (was 0.5) // Tuned by Differential Evolution against Amazon X-Ray per-second presence // over ALL 9 films (opencv5 build, LVFace-B_Glint360K, full gallery, // expansion on), a 10-parameter sweep — see docs/model-bakeoff.md. The // per-second misID-weighted macro-F1 optimum is 64.0% (P 79.0%, R 61.1%). // // This is a permissive operating point: the sweep discovered that with // flood-fill presence recovering recall, a LOW threshold pays off. It // supersedes the earlier 0.754, which came from a 4-film subset under the // now-withdrawn anneal/extinction windows and was never re-derived after a // scoring-bug fix. The full-9-film sweep at 0.485 beats it. // // Caveat, still true: the optimum generalises unevenly. It is strong on 7 of // 9 films (F1 62–80%) and weak on two — The Many Saints of Newark (an // ensemble of look-alikes; nearly all the run's misIDs land here) and // Scarface (sparse cuts, so flood-fill over-extends: R 95% / P 26%). Both // were the low outliers in every prior run too. Shipped because it wins on // average and on the misID-weighted objective; not a settled, film-agnostic // constant. float prob_threshold{0.485f}; // posterior P(match | sim, prior) threshold // TRACES: AR-024 | SR-002 // match_threshold (0.45), match_ratio (0.80) and match_ratio_ceil (0.65) are // RETIRED, joining track_max_embed_dist, cut_revive_sim, expand_novelty_sim // and expand_track_spread_max. All were raw cosine distances, and they were // the accept rule whenever the calibration fit failed — so the one situation // in which the pipeline knew its probabilities were untrustworthy was the // one in which it stopped using them. An unfitted sigmoid is now the // fallback everywhere, which is at least the same wrong number in every // stage. See identity_matcher_node.hpp. // ── Presence derivation ────────────────────────────────────────────────── // How accepted frames become a reported window. flood requires scene_detect. // Default flood: the 10-knob DE optimum uses it — snapping presence to the // shot recovers enough recall against X-Ray's scene-level cast to win the // misID-weighted F1, at a precision cost that is a net gain on 7 of 9 films. // Falls back to track_extent per claim when no boundaries exist. See // docs/model-bakeoff.md and PresenceMode above. PresenceMode presence_mode{PresenceMode::flood}; // Path to the learned XGBoost scene-boundary model. When set (build has // SAE_SCENE_XGB), the camera-position node stamps a per-frame RGB histogram // and the sink runs the detector post-EOF to supply flood-fill boundaries — // the measured best flood boundary source (presence F1 ~76% vs ~64% for the // always-on histogram cut). Empty → flood falls back to is_cut. std::string scene_xgb_model; // ── 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 knobs (only active with scene_detect). Dense decode of every // native-rate frame is the pipeline's cost driver, which is what made the // temporal shortcut below tempting. /// TRACES: AR-011 | SR-002 // scene_decode_fps: rate the source decodes at in dense mode. // **0 = native, and native is the only correct setting.** kWindow is 100 // frames: at native 25 fps that window spans ~4 s, which is what // TransNetV2 was trained on; at the 12 fps this used to default to it // spans ~8.3 s, so the model saw half-speed motion over twice its // temporal context. Boundary *timestamps* stay right either way — which // is exactly why the degradation was invisible, and why the compressed // separation it produced (~0.50 baseline against ~0.7+ peaks) was read // as a property of the export rather than of the input. Lowering this // buys decode time by running the model off-distribution; reach for // dense_scale or scene_stride instead, which do not. // dense_scale: downscale factor applied to decoded frames in dense mode // (0