fix(pipeline): finish three changes that had only been half applied

Each of these was recorded as done and was done in one place out of two.

AR-011 -- the TransNetV2 dedup window. The derived window
(dedup_window_sec, median observed interval halved) reached scenes.json
and nothing else. SceneBoundaries, the path that actually feeds
is_scene_boundary to the tracker, kept the literal 0.04 s under a
comment claiming it "matches the dedup scenes.json applies, so the two
views agree". They did not agree. 0.04 is one frame at 25 fps and wider
than a frame at 30, so two cuts on consecutive frames merged into one
and the loss was invisible: the pipeline simply saw fewer boundaries.
The detector now supplies the window it derived.

AR-019 -- ownership. The register says ownership "comes from the
registry, not a second local tally". Both existed: promotion fired on a
local accepted-frame count and fell back to a local per-actor plurality
when the registry had not yet claimed the track. That fallback was
reachable in the live pipeline, not just in tests -- three accepted
frames arrive well before a posterior crosses the ownership threshold --
so in practice the plurality usually decided, and it could not see the
AR-025 correlation discounting it was meant to defer to. The tally is
gone; promotion now requires the registry's verdict, with the accepted
-frame count demoted to an explicit evidence floor.

AR-017 -- the route. DeadTrack carried belief but no route, and the sink
wrote the literal string "live", so a field the schema publishes could
not distinguish anything. AR-017's own verification asks for "deferred
and pooled routes distinguishable". Route is now an enum on the claim.
Only `live` occurs today; `deferred` exists so AR-020's pass has
somewhere to write instead of a serialisation change to make.

Also: TrackGallery::forget had no callers, under a comment asserting the
matcher called it "on a cut or track disappearance". The cut half was
true by another route; the disappearance half was not, so a track that
died quietly kept its diversity buffer until the next cut cleared
everything. Replaced with prune_dead against the registry's own
liveness, the same shape as the tracker's prune_boxes -- a second
opinion about which tracks exist is a second thing that can be wrong.

Removes dead logistic/logit helpers and fixes five TRACES tags that used
a comma where a pipe separates requirement types, which the gate had
been reporting as diagnostics.

TRACES: AR-011, AR-017, AR-019 | IR-002 | SR-002, SR-005
This commit is contained in:
2026-08-05 17:33:12 +02:00
parent 7c7d4934ae
commit 88c42573a5
9 changed files with 163 additions and 64 deletions
+3 -3
View File
@@ -173,7 +173,7 @@ struct NodeCost {
double queue_wait_ms{0.0}; double queue_wait_ms{0.0};
bool is_bottleneck{false}; bool is_bottleneck{false};
/// TRACES: VR-015, AR-004 | PR-004 /// TRACES: VR-015 | AR-004 | PR-004
/// Live scheduling state, so a wedged run says *why* it is wedged rather /// Live scheduling state, so a wedged run says *why* it is wedged rather
/// than only that it is. With `queued=0, wake=1` a wake was recorded and /// than only that it is. With `queued=0, wake=1` a wake was recorded and
/// never consumed; with `queued=0, wake=0` and a full input, no wake was /// never consumed; with `queued=0, wake=0` and a full input, no wake was
@@ -469,7 +469,7 @@ public:
print_impl(os, final_, film_sec); print_impl(os, final_, film_sec);
} }
/// TRACES: VR-015, AR-004 | PR-004 /// TRACES: VR-015 | AR-004 | PR-004
/// Dump the report from a LIVE snapshot, mid-run, without stopping anything. /// Dump the report from a LIVE snapshot, mid-run, without stopping anything.
/// ///
/// A report that only exists at shutdown is no use against the failure this /// A report that only exists at shutdown is no use against the failure this
@@ -519,7 +519,7 @@ private:
<< "\n"; << "\n";
} }
/// TRACES: VR-015, AR-004 | PR-004 /// TRACES: VR-015 | AR-004 | PR-004
// Fires only when the scheduling state is actually wrong, so a healthy // Fires only when the scheduling state is actually wrong, so a healthy
// run stays quiet and a wedged one names the fault — instead of leaving // run stays quiet and a wedged one names the fault — instead of leaving
// it to be reconstructed under a debugger that suppresses the bug. // it to be reconstructed under a debugger that suppresses the bug.
+42 -31
View File
@@ -133,25 +133,52 @@ struct TrackGallery {
TrackState& ts = tracks_[track_id]; TrackState& ts = tracks_[track_id];
// Vote toward ownership: only accepted frames name an actor, and a track /// TRACES: AR-019 | SR-005
// that flip-flops between actors is ambiguous, so we tally per actor and // accepted_frames is an EVIDENCE FLOOR, not an identity decision: it
// pick the plurality winner at confirmation time. // asks "has this track been recognised often enough to be worth
if (accepted && best_actor >= 0) { // promoting", never "who is it". Who it is comes from the registry.
ts.actor_votes[best_actor]++; //
ts.accepted_frames++; // There used to be a per-actor tally here too, and promote() fell back
} // to its plurality winner. That made two answers to "who is this track"
// able to coexist, and the local one ignored the Bayesian accumulation
// entirely -- weighting thirty near-identical looks the same as thirty
// distinct ones, which is exactly what AR-025's discounting exists to
// stop. Since promotion only fired on the local count, the fallback was
// reachable in the live pipeline and not merely in tests: three
// accepted frames arrive well before a posterior crosses ownership.
if (accepted && best_actor >= 0) ts.accepted_frames++;
insert_into_buffer(ts, emb, best_gal_sim, crop); insert_into_buffer(ts, emb, best_gal_sim, crop);
// Confirm and promote as soon as the anchor threshold is met, once. // Confirm and promote once BOTH hold: the registry owns this track, and
if (!ts.promoted && ts.accepted_frames >= min_anchor_frames_) // enough frames have been accepted to be worth the slots. Ownership is
// the necessary one -- without it there is no actor to promote into.
if (!ts.promoted && ts.registry_owner >= 0 &&
ts.accepted_frames >= min_anchor_frames_)
promote(track_id, ts); promote(track_id, ts);
} }
// Drop a track's buffer when the face_tracker expires it or on a scene cut, /// TRACES: AR-019 | SR-005
// so stale/cross-cut embeddings can never be promoted later. Called by the /// Drop the buffers of tracks the registry no longer has.
// matcher when it observes a cut or track disappearance. ///
void forget(int track_id) { tracks_.erase(track_id); } /// `alive` is the registry's own liveness test, so this annotates the track
/// pool rather than duplicating it — the same shape as FaceTrackerFunc's
/// prune_boxes, and for the same reason: a second opinion about which
/// tracks exist is a second thing that can be wrong.
///
/// This replaces a `forget(int)` that had NO callers, under a comment
/// asserting "called by the matcher when it observes a cut or track
/// disappearance". The cut half was true by another route (clear_tracks);
/// the disappearance half was not, so a track that died quietly kept its
/// buffer until the next cut cleared everything.
template <typename AlivePredicate>
void prune_dead(const AlivePredicate& alive) {
if (!enabled_) return;
for (auto it = tracks_.begin(); it != tracks_.end(); ) {
if (alive(it->first)) ++it;
else it = tracks_.erase(it);
}
}
/// TRACES: AR-019 | SR-005 /// TRACES: AR-019 | SR-005
/// The registry's verdict on who this track is. Authoritative: it comes from /// The registry's verdict on who this track is. Authoritative: it comes from
@@ -198,7 +225,6 @@ private:
struct TrackState { struct TrackState {
std::vector<BufEntry> buf; std::vector<BufEntry> buf;
std::map<int, int> actor_votes; // actor_idx → accepted-frame count
int accepted_frames{0}; int accepted_frames{0};
bool promoted{false}; bool promoted{false};
int registry_owner{-1}; ///< AR-019: authoritative int registry_owner{-1}; ///< AR-019: authoritative
@@ -271,8 +297,8 @@ private:
void promote(int track_id, TrackState& ts) { void promote(int track_id, TrackState& ts) {
ts.promoted = true; // idempotent: never promote a track twice ts.promoted = true; // idempotent: never promote a track twice
int actor = owning_actor(ts); const int actor = ts.registry_owner;
if (actor < 0) return; if (actor < 0) return; // unreachable: observe() gates on this
// ── Safety gate: the band's lower bound, across the whole store ────── // ── Safety gate: the band's lower bound, across the whole store ──────
float worst = store_coherence(ts.buf); float worst = store_coherence(ts.buf);
@@ -302,21 +328,6 @@ private:
<< " views; annex now " << annex_size() << "\n"; << " views; annex now " << annex_size() << "\n";
} }
/// Prefer the registry's verdict; fall back to the local tally only when no
/// registry is attached (unit tests, replay harness).
static int owning_actor(const TrackState& ts) {
if (ts.registry_owner >= 0) return ts.registry_owner;
return plurality_actor(ts);
}
static int plurality_actor(const TrackState& ts) {
int best = -1, best_votes = 0;
for (const auto& [ai, v] : ts.actor_votes) {
if (v > best_votes) { best_votes = v; best = ai; }
}
return best;
}
/// TRACES: AR-018, AR-024 | SR-005 /// TRACES: AR-018, AR-024 | SR-005
/// The store's weakest pairwise P(same person) — the band's lower bound /// The store's weakest pairwise P(same person) — the band's lower bound
/// asked of every pair, not just of the best match at the door. /// asked of every pair, not just of the best match at the door.
+2 -2
View File
@@ -105,7 +105,7 @@ static constexpr std::size_t kSceneJoinDepth = 256;
/// actually worked. /// actually worked.
static std::shared_ptr<SceneBoundaries> scene_stats; static std::shared_ptr<SceneBoundaries> scene_stats;
/// TRACES: VR-015, AR-004 | PR-004 /// TRACES: VR-015 | AR-004 | PR-004
/// Set by SIGUSR1, serviced by the wait loop. `kill -USR1 <pid>` on a running /// Set by SIGUSR1, serviced by the wait loop. `kill -USR1 <pid>` on a running
/// or WEDGED run prints the benchmark table immediately — channel occupancy /// or WEDGED run prints the benchmark table immediately — channel occupancy
/// names the stalled node (full input, empty output) without a debug build or a /// names the stalled node (full input, empty output) without a debug build or a
@@ -371,7 +371,7 @@ int main(int argc, char** argv) {
!scene_done.load(std::memory_order_acquire)) && !scene_done.load(std::memory_order_acquire)) &&
!node_crashed.load(std::memory_order_acquire)) { !node_crashed.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::this_thread::sleep_for(std::chrono::milliseconds(100));
/// TRACES: VR-015, AR-004 | PR-004 /// TRACES: VR-015 | AR-004 | PR-004
if (g_dump_request.exchange(false, std::memory_order_relaxed)) if (g_dump_request.exchange(false, std::memory_order_relaxed))
bench.dump_live(std::cerr, film_sec.load(std::memory_order_acquire)); bench.dump_live(std::cerr, film_sec.load(std::memory_order_acquire));
} }
+10
View File
@@ -311,6 +311,16 @@ struct IdentityMatcherFunc {
absorb_promotions(); absorb_promotions();
/// TRACES: AR-019 | SR-005
// Drop buffers for tracks the registry has reaped. Without this a track
// that simply went off screen kept its diversity buffer until the next
// cut, so the store grew with the film rather than with what is on
// screen — and a buffer that outlives its track is evidence about a
// person nobody is looking at any more.
if (registry_)
track_gallery_.prune_dead(
[this](int id) { return registry_->is_live(id); });
return {std::move(tf.source), std::move(actors)}; return {std::move(tf.source), std::move(actors)};
} }
+3 -2
View File
@@ -152,6 +152,7 @@ private:
double start{0.0}; double start{0.0};
double end{0.0}; double end{0.0};
float belief{0.f}; ///< the posterior that justified the claim (AR-017) float belief{0.f}; ///< the posterior that justified the claim (AR-017)
Route route{Route::live}; ///< how it was identified (AR-017)
}; };
struct ActorWindow { struct ActorWindow {
std::string name, imdb_id, tmdb_id, jellyfin_id; std::string name, imdb_id, tmdb_id, jellyfin_id;
@@ -180,7 +181,7 @@ private:
aw.jellyfin_id = it->second.jellyfin_id; aw.jellyfin_id = it->second.jellyfin_id;
} }
} }
aw.scenes.push_back({c.first_seen, c.last_seen, c.belief}); aw.scenes.push_back({c.first_seen, c.last_seen, c.belief, c.route});
} }
std::vector<ActorWindow> result; std::vector<ActorWindow> result;
@@ -204,7 +205,7 @@ private:
windows.push_back({{"start", w.start}, windows.push_back({{"start", w.start},
{"end", w.end}, {"end", w.end},
{"belief", w.belief}, {"belief", w.belief},
{"route", "live"}}); {"route", route_name(w.route)}});
json ja; json ja;
ja["name"] = aw.name; ja["name"] = aw.name;
ja["imdb_id"] = aw.imdb_id; ja["imdb_id"] = aw.imdb_id;
+13 -3
View File
@@ -149,8 +149,15 @@ private:
// final verdict. The face branch consults this for frames it has not // final verdict. The face branch consults this for frames it has not
// reached yet, and the watermark is what lets it tell "no boundary // reached yet, and the watermark is what lets it tell "no boundary
// here" from "not scored yet". // here" from "not scored yet".
if (shared_ && hi > lo) /// TRACES: AR-011 | SR-002
shared_->publish(fresh, times_[hi - 1]); // Hand the join the same dedup window scenes.json uses, derived from the
// observed cadence rather than assumed. Set on every window because the
// median refines as intervals accumulate; it converges within the first
// window and costs a double assignment thereafter.
if (shared_) {
shared_->set_merge_window(dedup_window_sec(intervals_));
if (hi > lo) shared_->publish(fresh, times_[hi - 1]);
}
} }
// At EOF the tail (< kWindow frames) never formed a full window. Pad it out // At EOF the tail (< kWindow frames) never formed a full window. Pad it out
@@ -183,7 +190,10 @@ private:
// the last full window — reach the join with no verdict and are treated // the last full window — reach the join with no verdict and are treated
// as boundary-free without evidence, which is precisely the ambiguity // as boundary-free without evidence, which is precisely the ambiguity
// the watermark exists to prevent. // the watermark exists to prevent.
if (shared_ && n > 0) shared_->publish(fresh, times_[n - 1]); if (shared_ && n > 0) {
shared_->set_merge_window(dedup_window_sec(intervals_));
shared_->publish(fresh, times_[n - 1]);
}
} }
void write_output() { void write_output() {
+25 -4
View File
@@ -27,15 +27,35 @@
class SceneBoundaries { class SceneBoundaries {
public: public:
/// Peaks closer than this are one boundary. Matches the dedup scenes.json /// TRACES: AR-011 | SR-002
/// applies, so the two views agree. /// Peaks closer than this are one boundary.
static constexpr double kMergeSec = 0.04; ///
/// Supplied by the detector, derived from the cadence it was actually fed
/// (SceneDetectorFunc::dedup_window_sec), NOT assumed. It used to be a hard
/// 0.04 here, and AR-011 is recorded as having replaced that literal --
/// which it did, but only for scenes.json. This path, the one that feeds
/// is_scene_boundary into the tracker, kept the constant while the comment
/// above it claimed "matches the dedup scenes.json applies, so the two
/// views agree". They did not agree. 0.04 s is one frame at 25 fps and
/// wider than a frame at 30, so two cuts on consecutive frames merged into
/// one and the loss was invisible: the pipeline simply saw fewer
/// boundaries.
///
/// Zero until the detector sets it, which makes the pre-cadence state a
/// no-op dedup rather than a wrong one -- adjacent peaks stay separate
/// until there is evidence about how far apart frames are, and is_boundary
/// absorbs duplicates in its tolerance anyway.
void set_merge_window(double sec) {
std::lock_guard<std::mutex> g(mu_);
merge_sec_ = sec;
}
/// Called by the scene detector as each window is scored. `through` is the /// Called by the scene detector as each window is scored. `through` is the
/// timestamp up to which its verdict is now final. /// timestamp up to which its verdict is now final.
void publish(const std::vector<double>& ts, double through) { void publish(const std::vector<double>& ts, double through) {
{ {
std::lock_guard<std::mutex> g(mu_); std::lock_guard<std::mutex> g(mu_);
const double merge = merge_sec_;
// Dedup on insert, matching what scenes.json does at write time. A run // Dedup on insert, matching what scenes.json does at write time. A run
// of adjacent high-scoring frames is one boundary, not several, and // of adjacent high-scoring frames is one boundary, not several, and
// leaving them raw made this view report 357 where the file said 13 — // leaving them raw made this view report 357 where the file said 13 —
@@ -45,7 +65,7 @@ public:
bounds_.insert(bounds_.end(), ts.begin(), ts.end()); bounds_.insert(bounds_.end(), ts.begin(), ts.end());
std::sort(bounds_.begin(), bounds_.end()); std::sort(bounds_.begin(), bounds_.end());
bounds_.erase(std::unique(bounds_.begin(), bounds_.end(), bounds_.erase(std::unique(bounds_.begin(), bounds_.end(),
[](double a, double b) { return b - a < kMergeSec; }), [merge](double a, double b) { return b - a < merge; }),
bounds_.end()); bounds_.end());
scored_through_ = std::max(scored_through_, through); scored_through_ = std::max(scored_through_, through);
} }
@@ -120,6 +140,7 @@ private:
mutable std::mutex mu_; mutable std::mutex mu_;
mutable std::condition_variable cv_; mutable std::condition_variable cv_;
bool finished_{false}; bool finished_{false};
double merge_sec_{0.0}; ///< set by the detector; see set_merge_window
std::vector<double> bounds_; std::vector<double> bounds_;
double scored_through_{-1.0}; double scored_through_{-1.0};
mutable std::size_t outran_{0}; mutable std::size_t outran_{0};
+36 -11
View File
@@ -44,12 +44,39 @@
// A finished presence claim, emitted exactly once when a track is reaped or // A finished presence claim, emitted exactly once when a track is reaped or
// flushed. Immutable by construction: it carries everything needed to justify // flushed. Immutable by construction: it carries everything needed to justify
// itself (AR-017), with no back-reference into registry state. // itself (AR-017), with no back-reference into registry state.
/// TRACES: AR-017 | IR-002 | SR-002, SR-003
/// How an actor came to be attached to a track.
///
/// AR-017 requires every presence claim to carry its identification route, and
/// IR-002 publishes it per window. Until now the sink wrote the string "live"
/// unconditionally, so the field existed but could not distinguish anything --
/// and AR-017's own verification asks for "deferred and pooled routes
/// distinguishable".
///
/// Only `live` occurs today. `deferred` is what AR-020's pass will set when it
/// resolves a track that failed during streaming and was identified against the
/// final expanded gallery; the value exists now so that pass has somewhere to
/// write rather than a serialisation change to make.
enum class Route {
live, ///< identified while streaming, from accumulated per-frame evidence
deferred, ///< resolved after EOF against the expanded gallery (AR-020)
};
inline const char* route_name(Route r) {
switch (r) {
case Route::deferred: return "deferred";
case Route::live: break;
}
return "live";
}
struct DeadTrack { struct DeadTrack {
int track_id{-1}; int track_id{-1};
double first_seen{0.0}; double first_seen{0.0};
double last_seen{0.0}; ///< always the last sighting, never the death time double last_seen{0.0}; ///< always the last sighting, never the death time
int actor_idx{-1}; ///< -1 when the track was never owned int actor_idx{-1}; ///< -1 when the track was never owned
float belief{0.0f}; ///< accumulated posterior for actor_idx float belief{0.0f}; ///< accumulated posterior for actor_idx
Route route{Route::live}; ///< how the actor was attached (AR-017)
int observations{0}; ///< evidence updates that landed on this track int observations{0}; ///< evidence updates that landed on this track
float effective_obs{0.f}; ///< sum of discounted weights — the evidence that counted float effective_obs{0.f}; ///< sum of discounted weights — the evidence that counted
}; };
@@ -215,6 +242,15 @@ public:
// ── Diagnostics ────────────────────────────────────────────────────────── // ── Diagnostics ──────────────────────────────────────────────────────────
// These measure how often tracking is silently wrong, which nothing in the // These measure how often tracking is silently wrong, which nothing in the
// pipeline currently reveals. // pipeline currently reveals.
/// Whether the registry still holds this track. The authority on which
/// tracks exist, so annotating structures elsewhere (spatial boxes in the
/// tracker, diversity buffers in the expansion store) can prune against it
/// rather than keeping a second opinion.
bool is_live(int track_id) const {
std::lock_guard g(mu_);
return tracks_.count(track_id) != 0;
}
int dropped_votes() const { std::lock_guard g(mu_); return dropped_votes_; } int dropped_votes() const { std::lock_guard g(mu_); return dropped_votes_; }
int belief_swaps() const { std::lock_guard g(mu_); return belief_swaps_; } int belief_swaps() const { std::lock_guard g(mu_); return belief_swaps_; }
int actor_conflicts() const { std::lock_guard g(mu_); return actor_conflicts_; } int actor_conflicts() const { std::lock_guard g(mu_); return actor_conflicts_; }
@@ -340,17 +376,6 @@ private:
for (int i = 0; i < 512; ++i) t.mean[i] = static_cast<float>(t.mean[i] / norm); for (int i = 0; i < 512; ++i) t.mean[i] = static_cast<float>(t.mean[i] / norm);
} }
static float logistic(float z) {
return z >= 0 ? 1.f / (1.f + std::exp(-z))
: std::exp(z) / (1.f + std::exp(z));
}
static float logit(float p) {
const float eps = 1e-6f;
p = std::min(1.f - eps, std::max(eps, p));
return std::log(p / (1.f - p));
}
Config cfg_; Config cfg_;
EvidenceDiscounter discounter_; EvidenceDiscounter discounter_;
mutable std::mutex mu_; mutable std::mutex mu_;
+29 -8
View File
@@ -210,6 +210,9 @@ TEST_CASE("band thresholds probability, not cosine", "[track_gallery][AR-018][AR
TEST_CASE("a two-person track never poisons the annex", "[track_gallery][AR-018]") { TEST_CASE("a two-person track never poisons the annex", "[track_gallery][AR-018]") {
TrackGallery tg(expand_cfg()); TrackGallery tg(expand_cfg());
tg.set_calibration(identity_cal); tg.set_calibration(identity_cal);
// AR-019: promotion needs the registry's verdict; the local
// accepted-frame plurality that used to supply it is gone.
tg.set_owner(3, 0);
// Two orthogonal identities under one track ID — a track-ID collision. // Two orthogonal identities under one track ID — a track-ID collision.
// The band refuses the outsider at the door, so the store never becomes // The band refuses the outsider at the door, so the store never becomes
// two-person in the first place. // two-person in the first place.
@@ -246,6 +249,9 @@ TEST_CASE("a track that drifts through the band is refused at promotion",
TEST_CASE("confirmed track promotes its store", "[track_gallery][AR-019]") { TEST_CASE("confirmed track promotes its store", "[track_gallery][AR-019]") {
TrackGallery tg(expand_cfg()); TrackGallery tg(expand_cfg());
tg.set_calibration(identity_cal); tg.set_calibration(identity_cal);
// AR-019: promotion needs the registry's verdict; the local
// accepted-frame plurality that used to supply it is gone.
tg.set_owner(7, 0);
REQUIRE(tg.enabled()); REQUIRE(tg.enabled());
// A track owned by actor 0: every frame accepted, every view mutually // A track owned by actor 0: every frame accepted, every view mutually
@@ -281,21 +287,29 @@ TEST_CASE("unconfirmed track (too few accepts) does not promote", "[track_galler
CHECK(tg.annex_size() == 0); CHECK(tg.annex_size() == 0);
} }
TEST_CASE("plurality actor wins a mixed-vote track", "[track_gallery][AR-019]") { TEST_CASE("an unowned track never promotes, however many frames it accepts",
"[track_gallery][AR-019]") {
TrackGallery tg(expand_cfg()); TrackGallery tg(expand_cfg());
tg.set_calibration(identity_cal); tg.set_calibration(identity_cal);
// No registry attached (unit-test path): actor 5 accepted twice, actor 6 // This case used to assert the opposite: it drove a mixed-vote track with
// once → plurality is 5. // no registry owner and expected the local plurality winner (actor 5) to
tg.observe(8, spoke(1, kSpokeCos), 5, 0.30f, true, kNoCrop); // take the promotion. That fallback is gone. AR-019 says ownership comes
tg.observe(8, spoke(2, kSpokeCos), 5, 0.30f, true, kNoCrop); // from the registry and not from a second local tally, and the tally could
tg.observe(8, spoke(3, kSpokeCos), 6, 0.30f, true, kNoCrop); // not see the AR-025 discounting -- so it weighted thirty near-identical
REQUIRE(tg.annex_size() > 0); // looks like thirty distinct ones.
for (int actor : tg.annex_actors()) CHECK(actor == 5); //
// Ten accepted frames, no set_owner, nothing promoted.
for (int k = 1; k <= 10; ++k)
tg.observe(8, spoke(k, kSpokeCos), 5, 0.30f, true, kNoCrop);
CHECK(tg.annex_size() == 0);
} }
TEST_CASE("promotion is idempotent across a long track", "[track_gallery][AR-019]") { TEST_CASE("promotion is idempotent across a long track", "[track_gallery][AR-019]") {
TrackGallery tg(expand_cfg()); TrackGallery tg(expand_cfg());
tg.set_calibration(identity_cal); tg.set_calibration(identity_cal);
// AR-019: promotion needs the registry's verdict; the local
// accepted-frame plurality that used to supply it is gone.
tg.set_owner(9, 0);
for (int k = 1; k <= 3; ++k) for (int k = 1; k <= 3; ++k)
tg.observe(9, spoke(k, kSpokeCos), 0, 0.30f, true, kNoCrop); tg.observe(9, spoke(k, kSpokeCos), 0, 0.30f, true, kNoCrop);
const int after_confirm = tg.annex_size(); const int after_confirm = tg.annex_size();
@@ -328,6 +342,9 @@ TEST_CASE("eviction keeps the gallery-far views", "[track_gallery][AR-018]") {
cfg.expand_min_anchor_frames = 4; cfg.expand_min_anchor_frames = 4;
TrackGallery tg(cfg); TrackGallery tg(cfg);
tg.set_calibration(identity_cal); tg.set_calibration(identity_cal);
// AR-019: promotion needs the registry's verdict; the local
// accepted-frame plurality that used to supply it is gone.
tg.set_owner(6, 0);
tg.observe(6, spoke(1, kSpokeCos), 0, 0.80f, true, kNoCrop); // well recognised tg.observe(6, spoke(1, kSpokeCos), 0, 0.80f, true, kNoCrop); // well recognised
tg.observe(6, spoke(2, kSpokeCos), 0, 0.40f, true, kNoCrop); tg.observe(6, spoke(2, kSpokeCos), 0, 0.40f, true, kNoCrop);
@@ -354,6 +371,10 @@ TEST_CASE("promotions drain exactly once, in matrix order",
"[track_gallery][AR-026]") { "[track_gallery][AR-026]") {
TrackGallery tg(expand_cfg()); TrackGallery tg(expand_cfg());
tg.set_calibration(identity_cal); tg.set_calibration(identity_cal);
// AR-019: promotion needs the registry's verdict; the local
// accepted-frame plurality that used to supply it is gone.
tg.set_owner(7, 0);
tg.set_owner(8, 4);
std::vector<float> emb; std::vector<float> emb;
std::vector<int> actor; std::vector<int> actor;