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:
@@ -133,25 +133,52 @@ struct TrackGallery {
|
||||
|
||||
TrackState& ts = tracks_[track_id];
|
||||
|
||||
// Vote toward ownership: only accepted frames name an actor, and a track
|
||||
// that flip-flops between actors is ambiguous, so we tally per actor and
|
||||
// pick the plurality winner at confirmation time.
|
||||
if (accepted && best_actor >= 0) {
|
||||
ts.actor_votes[best_actor]++;
|
||||
ts.accepted_frames++;
|
||||
}
|
||||
/// TRACES: AR-019 | SR-005
|
||||
// accepted_frames is an EVIDENCE FLOOR, not an identity decision: it
|
||||
// asks "has this track been recognised often enough to be worth
|
||||
// promoting", never "who is it". Who it is comes from the registry.
|
||||
//
|
||||
// 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);
|
||||
|
||||
// Confirm and promote as soon as the anchor threshold is met, once.
|
||||
if (!ts.promoted && ts.accepted_frames >= min_anchor_frames_)
|
||||
// Confirm and promote once BOTH hold: the registry owns this track, and
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Drop a track's buffer when the face_tracker expires it or on a scene cut,
|
||||
// so stale/cross-cut embeddings can never be promoted later. Called by the
|
||||
// matcher when it observes a cut or track disappearance.
|
||||
void forget(int track_id) { tracks_.erase(track_id); }
|
||||
/// TRACES: AR-019 | SR-005
|
||||
/// Drop the buffers of tracks the registry no longer has.
|
||||
///
|
||||
/// `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
|
||||
/// The registry's verdict on who this track is. Authoritative: it comes from
|
||||
@@ -198,7 +225,6 @@ private:
|
||||
|
||||
struct TrackState {
|
||||
std::vector<BufEntry> buf;
|
||||
std::map<int, int> actor_votes; // actor_idx → accepted-frame count
|
||||
int accepted_frames{0};
|
||||
bool promoted{false};
|
||||
int registry_owner{-1}; ///< AR-019: authoritative
|
||||
@@ -271,8 +297,8 @@ private:
|
||||
void promote(int track_id, TrackState& ts) {
|
||||
ts.promoted = true; // idempotent: never promote a track twice
|
||||
|
||||
int actor = owning_actor(ts);
|
||||
if (actor < 0) return;
|
||||
const int actor = ts.registry_owner;
|
||||
if (actor < 0) return; // unreachable: observe() gates on this
|
||||
|
||||
// ── Safety gate: the band's lower bound, across the whole store ──────
|
||||
float worst = store_coherence(ts.buf);
|
||||
@@ -302,21 +328,6 @@ private:
|
||||
<< " 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
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user