refactor(presence): execute the extinction_sec/anneal_sec withdrawal

docs/SPEC.md specified this removal, listed its parts, and ended "grep
for both names and expect no survivors". There were about forty.
docs/requirements.md meanwhile recorded both constants as Withdrawn and
"deleted rather than retained at zero", on the grounds that a field
naming a mechanism the pipeline no longer has is actively misleading.
Neither statement was true of the code: Config still carried
extinction_sec 57.4 and anneal_sec 35.5, --extinction and --anneal still
parsed, and SceneTrackerFunc still ran its keep-alive in both shipped
pipelines, announcing its timeout at every startup.

SceneTrackerFunc is replaced by FrameAnnotationFunc, which is stateless:
same ports, same output type, no keep-alive. Presence belongs to
TrackRegistry (AR-012), where a window is the extent of a track an actor
owned and ends at the last sighting (AR-013). The keep-alive answered
that question a second time and answered it worse, by re-opening exactly
the trailing cool-down AR-013 refuses.

Visible change: --verbosity standard's frames[].identified listed every
actor inside the keep-alive, including ones absent from the frame. It
now lists what was matched in that frame. Minimal and xray output is
untouched -- both were already built from registry claims and never
consulted this node. No schema bump: the published extraction block
reports track_extinction_sec, a different knob that bounds
re-association and never extends a claim.

TrackRegistry::Config::extinction_sec is renamed track_extinction_sec to
match the Config field feeding it, so the grep SPEC.md asks for now
returns nothing rather than one confusing false positive.

Two targets turned out to have been silently dead, both since the
AR-007/AR-008 tracker redesign, and both for the same reason -- they
construct FaceTrackerFunc from a Config alone, a signature that stopped
existing when association moved into probability space:

- scene_preview is fixed here. It now mirrors main.cpp's construction
  order exactly (matcher, then registry, then tracker) and wires the
  registry's claims into the sink, which it was not doing. DP-001 says
  modes are front-ends that must not fork pipeline logic; this one had
  forked it and then rotted.
- sae_kpn is not fixed. Restructuring the seam so the tracker can reach
  a calibration that only exists once the matcher is built is VR-011's
  rewrite, not a patch, and presence claims do not cross the seam at all
  today. It is now behind SAE_BUILD_KPN_BINDINGS=OFF with the reason
  recorded, so `cmake --build` succeeds and the breakage is attributed
  rather than rediscovered.

That second one is worth stating plainly: VR-002 ("replay drives the
real KPN nodes, not a reimplementation") is marked Done, and the module
that makes replay possible has not compiled for some time. The .so in a
stale build/ predates the change.

Python side: the two names are gone from optimize.py, replay.py and
run_holdout_all_models.py as Config keys. anneal_sec survives as
REPLAY_LOCAL_KEYS -- it still configures replay.py's own windowing,
which is a Python reimplementation that no longer matches the sink and
is documented as such. That divergence is VR-011's.

TRACES: AR-012, AR-013 | DP-001 | SR-002
This commit is contained in:
2026-08-05 16:21:15 +02:00
parent e1de98e783
commit 7c7d4934ae
16 changed files with 203 additions and 181 deletions
+27 -1
View File
@@ -322,11 +322,37 @@ nanobind_add_module(sae_embed src/python_bindings.cpp)
target_link_libraries(sae_embed PRIVATE sae_gallery) target_link_libraries(sae_embed PRIVATE sae_gallery)
# ── sae_kpn — Python module: run the real downstream nodes over dumped embeddings ─ # ── sae_kpn — Python module: run the real downstream nodes over dumped embeddings ─
# Assembles face_tracker/identity_matcher/scene_tracker in a Python-driven KPN # Assembles face_tracker/identity_matcher/frame_annotation in a Python-driven KPN
# network (KPN_BUILD_PYTHON is enabled per-TU inside the .cpp). Powers the # network (KPN_BUILD_PYTHON is enabled per-TU inside the .cpp). Powers the
# threshold-sweep optimizer in scripts/optimizer/. # threshold-sweep optimizer in scripts/optimizer/.
#
# OFF by default, and this is a statement of fact rather than a preference: the
# module HAS NOT COMPILED since the AR-007/AR-008 tracker redesign. FaceTrackerFunc
# now requires a TrackRegistry and a calibration at construction, and the binding
# still builds it from a Config alone. The .so in a stale build/ directory
# predates that change.
#
# Fixing it is VR-011's job, not a patch: the tracker needs the calibration, the
# calibration comes from the matcher, and the matcher is added to the network
# afterwards -- so the seam has to be restructured, exactly as main.cpp already
# is (matcher first, then registry, then tracker). Presence claims do not cross
# the seam at all today, which is the other half of the same rewrite.
#
# Recorded as a switch rather than left as a build error so that `cmake --build`
# succeeds and the breakage is attributed instead of rediscovered. Turning it on
# reproduces the failure immediately, which is the point.
#
# TRACES: VR-011 | PR-002
option(SAE_BUILD_KPN_BINDINGS
"Build the sae_kpn Python module (BROKEN pending VR-011)" OFF)
if(SAE_BUILD_KPN_BINDINGS)
nanobind_add_module(sae_kpn src/kpn_bindings.cpp) nanobind_add_module(sae_kpn src/kpn_bindings.cpp)
target_link_libraries(sae_kpn PRIVATE sae_gallery) target_link_libraries(sae_kpn PRIVATE sae_gallery)
else()
message(STATUS
"sae_kpn: SKIPPED (SAE_BUILD_KPN_BINDINGS=OFF). The Python replay "
"bindings do not compile against the post-AR-012 tracker; see VR-011.")
endif()
# ── sae_audio — Python module: the v1 audio signature (IR-004) ──────────────── # ── sae_audio — Python module: the v1 audio signature (IR-004) ────────────────
# Compiles audio_signature.cpp directly and links only FFmpeg, rather than # Compiles audio_signature.cpp directly and links only FFmpeg, rather than
+4 -2
View File
@@ -79,8 +79,10 @@ def main():
"--dump", str(dump), "--gallery", str(gallery), "--dump", str(dump), "--gallery", str(gallery),
"--out", str(pred_path), "--out", str(pred_path),
"--prob-threshold", str(cfg["prob_threshold"]), "--prob-threshold", str(cfg["prob_threshold"]),
"--anneal-sec", str(cfg["anneal_sec"]), # anneal_sec is replay-local now (it configures replay.py's
"--extinction-sec", str(cfg["extinction_sec"]), # own windowing, not the pipeline). extinction_sec is gone
# entirely with SceneTrackerFunc -- see AR-012/AR-013.
"--anneal-sec", str(cfg.get("anneal_sec", 10.0)),
"--expand-gallery", "--expand-gallery",
] ]
print(f"RUN {model}/{film['slug']}...", file=sys.stderr) print(f"RUN {model}/{film['slug']}...", file=sys.stderr)
+2 -2
View File
@@ -17,7 +17,7 @@ point from the trajectory (--trajectory).
Usage: Usage:
python scripts/optimizer/optimize.py --manifest films.json \ python scripts/optimizer/optimize.py --manifest films.json \
--gallery gallery_arcface_w600k_r50.json \ --gallery gallery_arcface_w600k_r50.json \
--params prob_threshold:0.5:0.999 anneal_sec:1:30 extinction_sec:1:15 \ --params prob_threshold:0.5:0.999 anneal_sec:1:30 track_alpha:0:1 \
--popsize 20 --maxiter 25 --trajectory traj.json --popsize 20 --maxiter 25 --trajectory traj.json
""" """
from __future__ import annotations from __future__ import annotations
@@ -239,7 +239,7 @@ def main():
rec = {"eval": evals[0], "config": cfg, **m, "t": round(time.time() - t0, 1)} rec = {"eval": evals[0], "config": cfg, **m, "t": round(time.time() - t0, 1)}
traj.append(rec) traj.append(rec)
print(f"[opt] eval {evals[0]:3d} thr={cfg['prob_threshold']:.2f} " print(f"[opt] eval {evals[0]:3d} thr={cfg['prob_threshold']:.2f} "
f"ann={cfg['anneal_sec']:.0f} ext={cfg['extinction_sec']:.1f}" f"ann={cfg.get('anneal_sec', float('nan')):.0f}"
f"F1={m['f1']*100:.1f}% P={m['precision']*100:.1f}% R={m['recall']*100:.1f}% " f"F1={m['f1']*100:.1f}% P={m['precision']*100:.1f}% R={m['recall']*100:.1f}% "
f"agree={m.get('agreement', 0)*100:.1f}% misID={m.get('FPI_misid', 0)}", f"agree={m.get('agreement', 0)*100:.1f}% misID={m.get('FPI_misid', 0)}",
file=sys.stderr) file=sys.stderr)
+33 -10
View File
@@ -6,14 +6,14 @@ TRACES: VR-002 | PR-002
Reads an embedding dump (scripts/optimizer/SCHEMA.md), feeds each frame as an Reads an embedding dump (scripts/optimizer/SCHEMA.md), feeds each frame as an
EmbeddedSceneFrame into a Python-assembled KPN network wiring the *real* C++ EmbeddedSceneFrame into a Python-assembled KPN network wiring the *real* C++
face_tracker → identity_matcher → scene_tracker, and returns the same presence-window face_tracker → identity_matcher → frame_annotation, and returns the same presence-window
JSON that scene_analyze's result_sink produces (minimal schema). No decode, no GPU JSON that scene_analyze's result_sink produces (minimal schema). No decode, no GPU
embedding — only the cheap downstream tail runs, so a sweep can vary Config knobs embedding — only the cheap downstream tail runs, so a sweep can vary Config knobs
freely. See [[kpn-python-replay-optimizer]]. freely. See [[kpn-python-replay-optimizer]].
CLI: CLI:
python scripts/optimizer/replay.py --dump film.h5 --gallery gallery.json \ python scripts/optimizer/replay.py --dump film.h5 --gallery gallery.json \
--out replayed.json [--prob-threshold 0.99] [--anneal 10] ... --out replayed.json [--prob-threshold 0.99] [--anneal-sec 10] ...
""" """
from __future__ import annotations from __future__ import annotations
@@ -164,7 +164,7 @@ def replay(dump_path: str, gallery: str, cfg: dict, build_dir: str, stop: bool =
sae_kpn.add_face_tracker(net, "tracker", cfg, cap) sae_kpn.add_face_tracker(net, "tracker", cfg, cap)
sae_kpn.add_identity_matcher(net, "matcher", gallery, cfg, cap, sae_kpn.add_identity_matcher(net, "matcher", gallery, cfg, cap,
stamp["model_name"], stamp["model_sha256"]) stamp["model_name"], stamp["model_sha256"])
sae_kpn.add_scene_tracker(net, "scene", cfg, cap) sae_kpn.add_frame_annotation(net, "scene", cap)
net.connect("replay", 0, "tracker", 0) net.connect("replay", 0, "tracker", 0)
net.connect("tracker", 0, "matcher", 0) net.connect("tracker", 0, "matcher", 0)
net.connect("matcher", 0, "scene", 0) net.connect("matcher", 0, "scene", 0)
@@ -207,10 +207,26 @@ def replay(dump_path: str, gallery: str, cfg: dict, build_dir: str, stop: bool =
def build_minimal(annotations, movie, fps, cfg) -> dict: def build_minimal(annotations, movie, fps, cfg) -> dict:
"""Reproduce result_sink's minimal schema: per-actor annealed [start,end] windows. """Per-actor [start,end] windows, built by annealing per-frame detections.
Mirrors ResultSinkFunc::build_actor_windows — merge each actor's detection TRACES: VR-011 | PR-002
timestamps into windows, bridging gaps shorter than anneal_sec.
This NO LONGER mirrors ResultSinkFunc, and the docstring used to claim it
did. The sink builds a window from a TrackRegistry claim -- the extent
[first_seen, last_seen] of a track an actor owned (AR-012) -- so a window
starts when the actor appeared rather than when recognition first
succeeded, and interior gaps are absorbed by the track surviving them.
This function still bridges gaps between isolated accepted frames, which is
what anneal_sec did before AR-012/AR-013 withdrew it.
So a replayed window and a pipeline window are answers to different
questions, and a sweep tuned against this one is not tuning the shipped
behaviour. That is VR-011's job -- "rewrite the replay harness for the
post-AR-012 output contract" -- and it is a rewrite, not an edit, because
the registry's claims do not cross the Python seam at all today.
`anneal_sec` is therefore replay-local now: it configures THIS function and
is no longer forwarded to the C++ Config, which has no such field.
""" """
anneal = float(cfg.get("anneal_sec", 10.0)) anneal = float(cfg.get("anneal_sec", 10.0))
info = {} # actor_idx -> identity fields info = {} # actor_idx -> identity fields
@@ -261,8 +277,12 @@ def build_minimal(annotations, movie, fps, cfg) -> dict:
# a stale entry here silently inert rather than loudly wrong. # a stale entry here silently inert rather than loudly wrong.
CFG_KEYS = ["detector_conf", "prob_threshold", "match_prior", CFG_KEYS = ["detector_conf", "prob_threshold", "match_prior",
"track_alpha", "track_min_iou", "track_assoc_min_prob", "track_alpha", "track_min_iou", "track_assoc_min_prob",
"track_extinction_sec", "track_extinction_sec"]
"extinction_sec", "anneal_sec"]
# Swept like a Config key but consumed entirely in Python, by build_minimal.
# Kept separate so nobody has to guess which of these the pipeline actually
# reads: everything in CFG_KEYS crosses the seam, and nothing here does.
REPLAY_LOCAL_KEYS = ["anneal_sec"]
def main(): def main():
@@ -273,7 +293,7 @@ def main():
p.add_argument("--out", required=True, help="output presence JSON") p.add_argument("--out", required=True, help="output presence JSON")
p.add_argument("--raw-out", help="also write raw per-frame annotations (JSONL, with bboxes) here") p.add_argument("--raw-out", help="also write raw per-frame annotations (JSONL, with bboxes) here")
p.add_argument("--build-dir", default=str(REPO / "build")) p.add_argument("--build-dir", default=str(REPO / "build"))
for k in CFG_KEYS: for k in CFG_KEYS + REPLAY_LOCAL_KEYS:
p.add_argument(f"--{k.replace('_','-')}", type=float, default=None) p.add_argument(f"--{k.replace('_','-')}", type=float, default=None)
# per-film gallery expansion: promotes pose-varied views of confidently-identified # per-film gallery expansion: promotes pose-varied views of confidently-identified
# actors into an in-memory annex, recovering ~+4 recall at no precision cost. # actors into an in-memory annex, recovering ~+4 recall at no precision cost.
@@ -285,7 +305,10 @@ def main():
p.add_argument("--require-gallery-stamp", action="store_true") p.add_argument("--require-gallery-stamp", action="store_true")
args = p.parse_args() args = p.parse_args()
cfg = {k: getattr(args, k) for k in CFG_KEYS if getattr(args, k) is not None} # Both lists go into one dict: config_from_dict reads C++ keys with a
# contains() check and ignores the rest, and build_minimal reads its own.
cfg = {k: getattr(args, k)
for k in CFG_KEYS + REPLAY_LOCAL_KEYS if getattr(args, k) is not None}
if args.expand_gallery: if args.expand_gallery:
cfg["expand_gallery"] = True cfg["expand_gallery"] = True
if args.require_gallery_stamp: if args.require_gallery_stamp:
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Smoke test for the sae_kpn module: assemble the real downstream pipeline nodes Smoke test for the sae_kpn module: assemble the real downstream pipeline nodes
(face_tracker → identity_matcher → scene_tracker) in a Python-driven KPN network, (face_tracker → identity_matcher → frame_annotation) in a Python-driven KPN network,
fed by a no-input Python source node, and verify SceneAnnotations flow out. fed by a no-input Python source node, and verify SceneAnnotations flow out.
Proves the KPN-native replay path works without any numpy port of node logic. Proves the KPN-native replay path works without any numpy port of node logic.
@@ -31,7 +31,7 @@ def make_frame(t, n):
def main(): def main():
net = sae_kpn.Network() net = sae_kpn.Network()
sae_kpn._register_types(net) sae_kpn._register_types(net)
cfg = {"prob_threshold": 0.99, "anneal_sec": 10.0, "extinction_sec": 5.0} cfg = {"prob_threshold": 0.99, "track_extinction_sec": 5.0}
frames = [make_frame(float(t), 1) for t in range(3)] frames = [make_frame(float(t), 1) for t in range(3)]
frames.append({"timestamp_sec": 3.0, "eof": True}) frames.append({"timestamp_sec": 3.0, "eof": True})
@@ -48,7 +48,7 @@ def main():
sae_kpn.add_node_python(net, "replay", source, [], ["EmbeddedSceneFrame"], 8) sae_kpn.add_node_python(net, "replay", source, [], ["EmbeddedSceneFrame"], 8)
sae_kpn.add_face_tracker(net, "tracker", cfg, 16) sae_kpn.add_face_tracker(net, "tracker", cfg, 16)
sae_kpn.add_identity_matcher(net, "matcher", GAL, cfg, 16) sae_kpn.add_identity_matcher(net, "matcher", GAL, cfg, 16)
sae_kpn.add_scene_tracker(net, "scene", cfg, 16) sae_kpn.add_frame_annotation(net, "scene", 16)
net.connect("replay", 0, "tracker", 0) net.connect("replay", 0, "tracker", 0)
net.connect("tracker", 0, "matcher", 0) net.connect("tracker", 0, "matcher", 0)
net.connect("matcher", 0, "scene", 0) net.connect("matcher", 0, "scene", 0)
+19 -10
View File
@@ -151,16 +151,25 @@ struct Config {
double track_extinction_sec{5.0}; double track_extinction_sec{5.0};
// ── Scene tracking ──────────────────────────────────────────────────────── // ── Scene tracking ────────────────────────────────────────────────────────
// extinction_sec re-tuned by DE against X-Ray per-second presence, 4-film rep4 // TRACES: AR-012, AR-013 | SR-002
// matrix (docs/rep4-optimizer-results.md). Reverses the earlier "short is better" // extinction_sec (57.4) and anneal_sec (35.5) are GONE, along with
// finding: with a stricter prob_threshold, a long extinction window bridges real // SceneTrackerFunc, which is what read the first of them. docs/SPEC.md
// presence gaps (occlusion, turned face) instead of just smearing FPs — every // specified this removal and ended it "grep for both names and expect no
// model's best config pushed to ~90%+ of the search ceiling (tried up to 60s). // survivors"; there were about forty, and the register meanwhile recorded
// The ceiling kept getting hit, so treat 60 as "good enough", not a proven optimum. // both as Withdrawn and "deleted rather than retained at zero" on the
double extinction_sec{57.4}; // keep actor active this many seconds after last detection // grounds that a field naming a mechanism the pipeline no longer has is
// anneal_sec: previously found INSENSITIVE at a 130s range; the wider rep4 sweep // actively misleading.
// (160s) 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 // Both existed to bridge gaps between isolated accepted frames. A track
// that survives its own gaps leaves them nothing to do: AR-012 makes a
// window the extent of a track an actor owns, and AR-013 ends it at the
// last sighting. The keep-alive answered the same question again and
// answered it worse, by re-opening exactly the trailing cool-down AR-013
// refuses.
//
// track_extinction_sec above is NOT the same knob under a new name. It
// bounds how long a lost track stays available for re-association, which is
// a tracking question; it never extends a presence claim.
// ── Per-film gallery expansion ──────────────────────────────────────────── // ── Per-film gallery expansion ────────────────────────────────────────────
// Within one uncut track every face is the same physical person — a free // Within one uncut track every face is the same physical person — a free
+14 -22
View File
@@ -1,5 +1,5 @@
// sae_kpn — run the real downstream pipeline nodes (face_tracker, identity_matcher, // sae_kpn — run the real downstream pipeline nodes (face_tracker, identity_matcher,
// scene_tracker) inside a Python-assembled KPN network, fed by a Python HDF5 replay // frame_annotation) inside a Python-assembled KPN network, fed by a Python HDF5 replay
// source. Lets a parameter sweep re-run the exact C++ matching/tracking logic over // source. Lets a parameter sweep re-run the exact C++ matching/tracking logic over
// dumped embeddings — no video decode, no GPU — with different Config knobs each run. // dumped embeddings — no video decode, no GPU — with different Config knobs each run.
// //
@@ -19,7 +19,7 @@
#include "gallery/gallery_store.hpp" #include "gallery/gallery_store.hpp"
#include "nodes/face_tracker_node.hpp" #include "nodes/face_tracker_node.hpp"
#include "nodes/identity_matcher_node.hpp" #include "nodes/identity_matcher_node.hpp"
#include "nodes/scene_tracker_node.hpp" #include "nodes/frame_annotation_node.hpp"
#include <nanobind/nanobind.h> #include <nanobind/nanobind.h>
#include <nanobind/ndarray.h> #include <nanobind/ndarray.h>
@@ -171,13 +171,8 @@ static Config config_from_dict(nb::dict d) {
// face tracker // face tracker
getf("track_alpha", cfg.track_alpha); getf("track_alpha", cfg.track_alpha);
getf("track_min_iou", cfg.track_min_iou); getf("track_min_iou", cfg.track_min_iou);
getf("track_max_embed_dist", cfg.track_max_embed_dist); getf("track_assoc_min_prob", cfg.track_assoc_min_prob);
geti("track_max_frames_missing", cfg.track_max_frames_missing); getd("track_extinction_sec", cfg.track_extinction_sec);
getf("cut_revive_sim", cfg.cut_revive_sim);
geti("cut_inactive_max_frames", cfg.cut_inactive_max_frames);
// scene tracker
getd("extinction_sec", cfg.extinction_sec);
getd("anneal_sec", cfg.anneal_sec);
// gallery expansion (usually off for sweeps; expose so it can be toggled) // gallery expansion (usually off for sweeps; expose so it can be toggled)
if (d.contains("expand_gallery")) cfg.expand_gallery = nb::cast<bool>(d["expand_gallery"]); if (d.contains("expand_gallery")) cfg.expand_gallery = nb::cast<bool>(d["expand_gallery"]);
/// TRACES: GR-004 | SR-001 /// TRACES: GR-004 | SR-001
@@ -189,7 +184,7 @@ static Config config_from_dict(nb::dict d) {
using Net = kpn::python::PyNetwork<SaeVariant>; using Net = kpn::python::PyNetwork<SaeVariant>;
NB_MODULE(sae_kpn, m) { NB_MODULE(sae_kpn, m) {
m.doc() = "Real KPN downstream nodes (tracker/matcher/scene_tracker) for Python replay sweeps"; m.doc() = "Real KPN downstream nodes (tracker/matcher/frame_annotation) for Python replay sweeps";
kpn::python::register_py_network<SaeVariant>(m, "Network"); kpn::python::register_py_network<SaeVariant>(m, "Network");
@@ -270,30 +265,27 @@ NB_MODULE(sae_kpn, m) {
}, "net"_a, "name"_a, "gallery"_a, "config"_a, "capacity"_a = 16, }, "net"_a, "name"_a, "gallery"_a, "config"_a, "capacity"_a = 16,
"embedder_model"_a = "", "embedder_sha256"_a = ""); "embedder_model"_a = "", "embedder_sha256"_a = "");
m.def("add_scene_tracker", [](Net& net, std::string name, nb::dict cfg_dict, std::size_t cap) { /// TRACES: AR-012, AR-013 | SR-002
Config cfg = config_from_dict(cfg_dict); // Was add_scene_tracker, backed by the extinction-timer state machine. The
// node is gone (see frame_annotation_node.hpp) and so is the timer; this
// projects a matched frame into the same SceneAnnotation the Python sink
// already reads, so the seam's output type is unchanged. It takes no config
// because it has no state to configure -- which is the point.
m.def("add_frame_annotation", [](Net& net, std::string name, std::size_t cap) {
auto node = std::make_shared<kpn::ObjectVariantNodeWrapper< auto node = std::make_shared<kpn::ObjectVariantNodeWrapper<
SceneTrackerFunc, SaeVariant, kpn::in<"matched">, kpn::out<"annotation">>>(cap, cfg); FrameAnnotationFunc, SaeVariant, kpn::in<"matched">, kpn::out<"annotation">>>(cap);
net.add(std::move(name), std::move(node)); net.add(std::move(name), std::move(node));
}, "net"_a, "name"_a, "config"_a, "capacity"_a = 16); }, "net"_a, "name"_a, "capacity"_a = 16);
// ── Runtime setters (persistent-pipeline reuse across a threshold sweep) ───── // ── Runtime setters (persistent-pipeline reuse across a threshold sweep) ─────
// Build the network once, then change thresholds between replays — no rebuild, // Build the network once, then change thresholds between replays — no rebuild,
// no teardown (which is where the ROCm deadlock lives), no gallery reload. // no teardown (which is where the ROCm deadlock lives), no gallery reload.
using MatcherWrap = kpn::ObjectVariantNodeWrapper< using MatcherWrap = kpn::ObjectVariantNodeWrapper<
IdentityMatcherFunc, SaeVariant, kpn::in<"tracked">, kpn::out<"matched">>; IdentityMatcherFunc, SaeVariant, kpn::in<"tracked">, kpn::out<"matched">>;
using SceneWrap = kpn::ObjectVariantNodeWrapper<
SceneTrackerFunc, SaeVariant, kpn::in<"matched">, kpn::out<"annotation">>;
m.def("set_prob_threshold", [](Net& net, std::string name, float t) { m.def("set_prob_threshold", [](Net& net, std::string name, float t) {
auto* w = dynamic_cast<MatcherWrap*>(net.node_ptr(name)); auto* w = dynamic_cast<MatcherWrap*>(net.node_ptr(name));
if (!w) throw std::runtime_error("set_prob_threshold: '" + name + "' is not an identity_matcher"); if (!w) throw std::runtime_error("set_prob_threshold: '" + name + "' is not an identity_matcher");
w->functor().set_prob_threshold(t); w->functor().set_prob_threshold(t);
}, "net"_a, "name"_a, "value"_a); }, "net"_a, "name"_a, "value"_a);
m.def("set_extinction_sec", [](Net& net, std::string name, double s) {
auto* w = dynamic_cast<SceneWrap*>(net.node_ptr(name));
if (!w) throw std::runtime_error("set_extinction_sec: '" + name + "' is not a scene_tracker");
w->functor().set_extinction_sec(s);
}, "net"_a, "name"_a, "value"_a);
} }
+6 -9
View File
@@ -8,11 +8,11 @@
// //
// [frame_source] ──Frame──► [face_detector] ──SceneFrame──► [face_aligner] // [frame_source] ──Frame──► [face_detector] ──SceneFrame──► [face_aligner]
// ──AlignedSceneFrame──► [embedder] ──EmbeddedSceneFrame──► // ──AlignedSceneFrame──► [embedder] ──EmbeddedSceneFrame──►
// [identity_matcher] ──MatchedSceneFrame──► [scene_tracker] // [identity_matcher] ──MatchedSceneFrame──► [frame_annotation]
// ──SceneAnnotation──► [result_sink] // ──SceneAnnotation──► [result_sink]
// //
// Debug build (SAE_DEBUG=1): // Debug build (SAE_DEBUG=1):
// [identity_matcher] output fans out to both [scene_tracker] AND [debug_renderer]. // [identity_matcher] output fans out to both [frame_annotation] AND [debug_renderer].
// FanoutNode<MatchedSceneFrame, 2> is auto-inserted by make_network(). // FanoutNode<MatchedSceneFrame, 2> is auto-inserted by make_network().
// //
// Usage: // Usage:
@@ -23,7 +23,6 @@
// --fps <N> sample rate in frames/sec (default: 1.0) // --fps <N> sample rate in frames/sec (default: 1.0)
// --verbosity <0|1|2> 0=minimal, 1=standard, 2=jellyfin-xray (default: 0) // --verbosity <0|1|2> 0=minimal, 1=standard, 2=jellyfin-xray (default: 0)
// --prob-threshold <f> posterior P(match) to accept (default: 0.754) // --prob-threshold <f> posterior P(match) to accept (default: 0.754)
// --extinction <f> actor extinction window in seconds (default: 5.0)
// --detector <path> override SCRFD detector model path // --detector <path> override SCRFD detector model path
// --arcface <path> override ArcFace model path // --arcface <path> override ArcFace model path
// --scene-detect enable TransNetV2 shot-boundary detection (dense decode; // --scene-detect enable TransNetV2 shot-boundary detection (dense decode;
@@ -64,7 +63,7 @@
#include "nodes/embedder_node.hpp" #include "nodes/embedder_node.hpp"
#include "nodes/face_tracker_node.hpp" #include "nodes/face_tracker_node.hpp"
#include "nodes/identity_matcher_node.hpp" #include "nodes/identity_matcher_node.hpp"
#include "nodes/scene_tracker_node.hpp" #include "nodes/frame_annotation_node.hpp"
#include "nodes/scene_detector_node.hpp" #include "nodes/scene_detector_node.hpp"
#include "scene_boundaries.hpp" #include "scene_boundaries.hpp"
#include "nodes/scene_boundary_annotator_node.hpp" #include "nodes/scene_boundary_annotator_node.hpp"
@@ -155,7 +154,6 @@ static Config parse_args(int argc, char** argv) {
else if (arg("--verbosity")) { int v = std::stoi(next()); cfg.verbosity = v == 2 ? Verbosity::xray : v == 1 ? Verbosity::standard : Verbosity::minimal; } else if (arg("--verbosity")) { int v = std::stoi(next()); cfg.verbosity = v == 2 ? Verbosity::xray : v == 1 ? Verbosity::standard : Verbosity::minimal; }
else if (arg("--prior")) cfg.match_prior = std::stof(next()); else if (arg("--prior")) cfg.match_prior = std::stof(next());
else if (arg("--prob-threshold")) cfg.prob_threshold = std::stof(next()); else if (arg("--prob-threshold")) cfg.prob_threshold = std::stof(next());
else if (arg("--extinction")) cfg.extinction_sec = std::stod(next());
else if (arg("--detector")) cfg.detector_model = next(); else if (arg("--detector")) cfg.detector_model = next();
else if (arg("--detector-engine")) cfg.detector_engine = next(); else if (arg("--detector-engine")) cfg.detector_engine = next();
else if (arg("--arcface")) cfg.arcface_model = next(); else if (arg("--arcface")) cfg.arcface_model = next();
@@ -168,7 +166,6 @@ static Config parse_args(int argc, char** argv) {
else if (arg("--track-min-iou")) cfg.track_min_iou = std::stof(next()); else if (arg("--track-min-iou")) cfg.track_min_iou = std::stof(next());
else if (arg("--track-min-prob")) cfg.track_assoc_min_prob = std::stof(next()); else if (arg("--track-min-prob")) cfg.track_assoc_min_prob = std::stof(next());
else if (arg("--track-extinction")) cfg.track_extinction_sec = std::stod(next()); else if (arg("--track-extinction")) cfg.track_extinction_sec = std::stod(next());
else if (arg("--anneal")) cfg.anneal_sec = std::stod(next());
else if (arg("--expand-gallery")) cfg.expand_gallery = true; else if (arg("--expand-gallery")) cfg.expand_gallery = true;
else if (arg("--expand-buffer")) cfg.expand_buffer_size = std::stoi(next()); else if (arg("--expand-buffer")) cfg.expand_buffer_size = std::stoi(next());
else if (arg("--expand-band-lo")) cfg.expand_band_lo = std::stof(next()); else if (arg("--expand-band-lo")) cfg.expand_band_lo = std::stof(next());
@@ -255,7 +252,7 @@ int main(int argc, char** argv) {
// and its final answer is only known when a track dies. // and its final answer is only known when a track dies.
auto same_person = same_person_probability(matcher_fn.calibration()); auto same_person = same_person_probability(matcher_fn.calibration());
TrackRegistry::Config reg_cfg; TrackRegistry::Config reg_cfg;
reg_cfg.extinction_sec = cfg.track_extinction_sec; reg_cfg.track_extinction_sec = cfg.track_extinction_sec;
auto registry = std::make_shared<TrackRegistry>( auto registry = std::make_shared<TrackRegistry>(
reg_cfg, EvidenceDiscounter(same_person)); reg_cfg, EvidenceDiscounter(same_person));
@@ -263,7 +260,7 @@ int main(int argc, char** argv) {
FaceTrackerFunc ftracker_fn{cfg, registry, same_person}; FaceTrackerFunc ftracker_fn{cfg, registry, same_person};
SceneTrackerFunc tracker_fn {cfg}; FrameAnnotationFunc tracker_fn {};
ResultSinkFunc sink_fn {cfg, done}; ResultSinkFunc sink_fn {cfg, done};
/// TRACES: AR-012, AR-016 | IR-002, IR-003 | SR-002 /// TRACES: AR-012, AR-016 | IR-002, IR-003 | SR-002
@@ -299,7 +296,7 @@ int main(int argc, char** argv) {
kpn::ObjectNode<EmbedderFunc, kpn::in<"aligned">, kpn::out<"embedded">, "embedder", 0> embedder (embedder_fn, 32); kpn::ObjectNode<EmbedderFunc, kpn::in<"aligned">, kpn::out<"embedded">, "embedder", 0> embedder (embedder_fn, 32);
kpn::ObjectNode<FaceTrackerFunc, kpn::in<"embedded">, kpn::out<"tracked">, "face_tracker", 0> ftracker (ftracker_fn, 16); kpn::ObjectNode<FaceTrackerFunc, kpn::in<"embedded">, kpn::out<"tracked">, "face_tracker", 0> ftracker (ftracker_fn, 16);
kpn::ObjectNode<IdentityMatcherFunc, kpn::in<"tracked">, kpn::out<"matched">, "identity_matcher", 0> matcher (matcher_fn, 16); kpn::ObjectNode<IdentityMatcherFunc, kpn::in<"tracked">, kpn::out<"matched">, "identity_matcher", 0> matcher (matcher_fn, 16);
kpn::ObjectNode<SceneTrackerFunc, kpn::in<"matched">, kpn::out<"annotation">, "scene_tracker", 0> tracker (tracker_fn, 16); kpn::ObjectNode<FrameAnnotationFunc, kpn::in<"matched">, kpn::out<"annotation">, "frame_annotation", 0> tracker (tracker_fn, 16);
kpn::ObjectNode<ResultSinkFunc, kpn::in<"annotation">,kpn::out<>, "result_sink", 0> sink (sink_fn, 16); kpn::ObjectNode<ResultSinkFunc, kpn::in<"annotation">,kpn::out<>, "result_sink", 0> sink (sink_fn, 16);
// ── Pipeline observability + run loop (topology-agnostic) ────────────────── // ── Pipeline observability + run loop (topology-agnostic) ──────────────────
+43
View File
@@ -0,0 +1,43 @@
#pragma once
/// TRACES: AR-012, AR-013 | SR-002
///
/// FrameAnnotationFunc — project a matched frame into a per-frame annotation.
///
/// Stateless, and that is the entire point of it.
///
/// It replaces `SceneTrackerFunc`, which kept an extinction timer per actor and
/// reported an actor as visible for `extinction_sec` (57.4 s) after their last
/// detection. docs/SPEC.md specified that node's deletion -- "anneal_sec and
/// extinction_sec are deleted, not re-tuned ... SceneTrackerFunc goes with
/// them", with a removal list ending "grep for both names and expect no
/// survivors" -- and docs/requirements.md recorded both constants as Withdrawn,
/// deleted "rather than retained at zero", on the grounds that a field naming a
/// mechanism the pipeline no longer has is actively misleading. None of that
/// removal had happened. The node was still wired into both shipped pipelines
/// and still printed its timeout at every startup.
///
/// **Presence is not this node's business.** AR-012 moved it to TrackRegistry,
/// where a window is `[first_seen, last_seen]` of a track an actor owns, and
/// AR-013 ends that window at the last sighting rather than after it. A
/// keep-alive here answered the same question a second time and answered it
/// worse: it re-opened the trailing cool-down the registry exists to refuse.
///
/// What a consumer sees change: `--verbosity standard`'s `frames[].identified`
/// used to list every actor still inside the keep-alive, including ones absent
/// from the frame. It now lists what was actually matched in that frame. The
/// minimal and xray outputs are unaffected -- they were already built from
/// registry claims and never consulted this node.
#include "types.hpp"
#include <string_view>
#include <utility>
struct FrameAnnotationFunc {
static constexpr std::string_view label() { return "frame_annotation"; }
SceneAnnotation operator()(MatchedSceneFrame mf) {
if (mf.source.eof) return {0.0, {}, /*eof=*/true};
return {mf.source.timestamp_sec, std::move(mf.actors)};
}
};
+5 -3
View File
@@ -23,7 +23,8 @@ using json = nlohmann::json;
// //
// Verbosity::minimal — merges per-frame presence into contiguous time windows. // Verbosity::minimal — merges per-frame presence into contiguous time windows.
// Output: { // Output: {
// "schema_version": 1, "movie": "...", "sample_fps": ..., "anneal_sec": ..., // "schema_version": 2, "movie": "...",
// "extraction": { "sample_fps": ..., "extinction_sec": ..., "gallery_scope": ... },
// "actors": [{ "name", "imdb_id", "tmdb_id", "jellyfin_id", "scenes": [[t0,t1], ...] }] // "actors": [{ "name", "imdb_id", "tmdb_id", "jellyfin_id", "scenes": [[t0,t1], ...] }]
// } // }
// An optional top-level "jellyfin_item_id" (the analysed title's Jellyfin item // An optional top-level "jellyfin_item_id" (the analysed title's Jellyfin item
@@ -123,8 +124,9 @@ private:
/// schema_version 2, per jRay/SPEC.md JR-002. anneal_sec is REMOVED /// schema_version 2, per jRay/SPEC.md JR-002. anneal_sec is REMOVED
/// rather than zeroed: a field naming a mechanism the pipeline no /// rather than zeroed: a field naming a mechanism the pipeline no
/// longer has is actively misleading, and would outlive everyone who /// longer has is actively misleading, and would outlive everyone who
/// remembers why it reads 0. extinction_sec succeeds it as the /// remembers why it reads 0. The extraction block reports
/// parameter that actually shapes window extent. /// track_extinction_sec, which bounds re-association -- not the
/// withdrawn actor keep-alive that shared its name.
root["schema_version"] = kSchemaVersion; root["schema_version"] = kSchemaVersion;
root["movie"] = cfg_.movie_path; root["movie"] = cfg_.movie_path;
root["extraction"] = { root["extraction"] = {
-102
View File
@@ -1,102 +0,0 @@
#pragma once
#include "types.hpp"
#include "config.hpp"
#include <map>
#include <iostream>
// ── SceneTrackerFunc ──────────────────────────────────────────────────────────
// KPN node: maintains an extinction-timer state machine per identified actor.
//
// On each MatchedSceneFrame:
// 1. Update last_seen for every matched known actor.
// 2. Expire actors whose last_seen is older than extinction_sec.
// 3. Emit SceneAnnotation with all currently active (non-expired) actors,
// including their most recently seen bbox and best similarity score.
//
// Unknown faces (actor_idx == -1) are passed through per-frame but are NOT
// tracked across frames — each frame reports its own unknowns independently.
struct SceneTrackerFunc {
static constexpr std::string_view label() { return "scene_tracker"; }
explicit SceneTrackerFunc(const Config& cfg)
: extinction_sec_(cfg.extinction_sec)
{
std::cerr << "[scene_tracker] extinction_sec=" << extinction_sec_ << "\n";
}
// Runtime setter for pipeline reuse across a sweep. Also clears the active-actor
// state so a re-run starts clean (no carry-over from the previous config's film).
void set_extinction_sec(double s) { extinction_sec_ = s; active_.clear(); }
SceneAnnotation operator()(MatchedSceneFrame mf) {
if (mf.source.eof) return {0.0, {}, /*eof=*/true};
double now = mf.source.timestamp_sec;
// Update known actors
for (const auto& ia : mf.actors) {
if (ia.actor_idx < 0) continue; // skip unknowns
auto& slot = active_[ia.actor_idx];
slot.last_seen = now;
slot.last_bbox = ia.bbox;
slot.last_crop = ia.crop;
slot.name = ia.name;
slot.imdb_id = ia.imdb_id;
slot.tmdb_id = ia.tmdb_id;
slot.jellyfin_id = ia.jellyfin_id;
// Keep the best (highest) similarity seen in this window
if (ia.similarity > slot.best_similarity)
slot.best_similarity = ia.similarity;
}
// Expire stale actors
for (auto it = active_.begin(); it != active_.end(); ) {
if ((now - it->second.last_seen) > extinction_sec_)
it = active_.erase(it);
else
++it;
}
// Build annotation: active known actors
std::vector<IdentifiedActor> visible;
visible.reserve(active_.size() + mf.actors.size());
for (const auto& [actor_idx, slot] : active_) {
IdentifiedActor ia;
ia.actor_idx = actor_idx;
ia.name = slot.name;
ia.imdb_id = slot.imdb_id;
ia.tmdb_id = slot.tmdb_id;
ia.jellyfin_id = slot.jellyfin_id;
ia.similarity = slot.best_similarity;
ia.bbox = slot.last_bbox;
ia.crop = slot.last_crop;
visible.push_back(ia);
}
// Append per-frame unknowns (actor_idx == -1) directly
for (const auto& ia : mf.actors) {
if (ia.actor_idx < 0) visible.push_back(ia);
}
return {now, std::move(visible)};
}
private:
struct Slot {
double last_seen{0.0};
float best_similarity{0.f};
cv::Rect2f last_bbox;
cv::Mat last_crop;
std::string name;
std::string imdb_id;
std::string tmdb_id;
std::string jellyfin_id;
};
double extinction_sec_;
std::map<int, Slot> active_; // actor_idx → state
};
+31 -10
View File
@@ -8,7 +8,7 @@
// //
// camera_pos (histogram cut detector) stamps Frame::cut_score / is_cut, which // camera_pos (histogram cut detector) stamps Frame::cut_score / is_cut, which
// ride through to the preview HUD's cut-score meter. // ride through to the preview HUD's cut-score meter.
// ├──► [scene_tracker] ──► [result_sink] (background thread) // ├──► [frame_annotation] ──► [result_sink] (background thread)
// └──► [preview_node] (main thread) // └──► [preview_node] (main thread)
// //
// The main thread drives preview_node via preview.step(). When the movie ends // The main thread drives preview_node via preview.step(). When the movie ends
@@ -30,7 +30,9 @@
#include "nodes/embedder_node.hpp" #include "nodes/embedder_node.hpp"
#include "nodes/face_tracker_node.hpp" #include "nodes/face_tracker_node.hpp"
#include "nodes/identity_matcher_node.hpp" #include "nodes/identity_matcher_node.hpp"
#include "nodes/scene_tracker_node.hpp" #include "track_registry.hpp"
#include "evidence_discount.hpp"
#include "nodes/frame_annotation_node.hpp"
#include "nodes/result_sink_node.hpp" #include "nodes/result_sink_node.hpp"
#include "nodes/preview_node.hpp" #include "nodes/preview_node.hpp"
@@ -71,7 +73,6 @@ static Config parse_args(int argc, char** argv) {
else if (arg("--verbosity")) { int v = std::stoi(next()); cfg.verbosity = v == 2 ? Verbosity::xray : v == 1 ? Verbosity::standard : Verbosity::minimal; } else if (arg("--verbosity")) { int v = std::stoi(next()); cfg.verbosity = v == 2 ? Verbosity::xray : v == 1 ? Verbosity::standard : Verbosity::minimal; }
else if (arg("--prior")) cfg.match_prior = std::stof(next()); else if (arg("--prior")) cfg.match_prior = std::stof(next());
else if (arg("--prob-threshold")) cfg.prob_threshold = std::stof(next()); else if (arg("--prob-threshold")) cfg.prob_threshold = std::stof(next());
else if (arg("--extinction")) cfg.extinction_sec = std::stod(next());
else if (arg("--detector")) cfg.detector_model = next(); else if (arg("--detector")) cfg.detector_model = next();
else if (arg("--detector-engine")) cfg.detector_engine = next(); else if (arg("--detector-engine")) cfg.detector_engine = next();
else if (arg("--arcface")) cfg.arcface_model = next(); else if (arg("--arcface")) cfg.arcface_model = next();
@@ -82,9 +83,8 @@ static Config parse_args(int argc, char** argv) {
else if (arg("--min-face-px")) cfg.min_face_px = std::stof(next()); else if (arg("--min-face-px")) cfg.min_face_px = std::stof(next());
else if (arg("--track-alpha")) cfg.track_alpha = std::stof(next()); else if (arg("--track-alpha")) cfg.track_alpha = std::stof(next());
else if (arg("--track-min-iou")) cfg.track_min_iou = std::stof(next()); else if (arg("--track-min-iou")) cfg.track_min_iou = std::stof(next());
else if (arg("--track-max-embed")) cfg.track_max_embed_dist = std::stof(next()); else if (arg("--track-min-prob")) cfg.track_assoc_min_prob = std::stof(next());
else if (arg("--track-max-missing")) cfg.track_max_frames_missing = std::stoi(next()); else if (arg("--track-extinction")) cfg.track_extinction_sec = std::stod(next());
else if (arg("--anneal")) cfg.anneal_sec = std::stod(next());
else if (arg("--trt-cache")) cfg.trt.cache_dir = next(); else if (arg("--trt-cache")) cfg.trt.cache_dir = next();
else if (arg("--trt-fp16")) cfg.trt.fp16 = true; else if (arg("--trt-fp16")) cfg.trt.fp16 = true;
else if (arg("--no-trt-fp16")) cfg.trt.fp16 = false; else if (arg("--no-trt-fp16")) cfg.trt.fp16 = false;
@@ -144,11 +144,32 @@ int main(int argc, char** argv) {
FaceDetectorFunc detector_fn{cfg}; FaceDetectorFunc detector_fn{cfg};
FaceAlignerFunc aligner_fn; FaceAlignerFunc aligner_fn;
EmbedderFunc embedder_fn{cfg}; EmbedderFunc embedder_fn{cfg};
FaceTrackerFunc ftracker_fn{cfg}; /// TRACES: DP-001, AR-007, AR-012, AR-024 | PR-004, SR-002
// Construction order matters and is the same as main.cpp's, deliberately:
// the matcher fits (or loads) the calibration, the registry needs a
// discounter built from it, and the tracker needs both. DP-001 says modes
// are front-ends that must not fork pipeline logic -- this file had forked
// it and then rotted, constructing FaceTrackerFunc{cfg} against a signature
// that stopped existing with the AR-007/AR-008 redesign, so scene_preview
// has not compiled since. Keeping the order identical is what stops that
// recurring.
IdentityMatcherFunc matcher_fn {gallery, cfg}; IdentityMatcherFunc matcher_fn {gallery, cfg};
SceneTrackerFunc tracker_fn {cfg}; auto same_person = same_person_probability(matcher_fn.calibration());
TrackRegistry::Config reg_cfg;
reg_cfg.track_extinction_sec = cfg.track_extinction_sec;
auto registry = std::make_shared<TrackRegistry>(
reg_cfg, EvidenceDiscounter(same_person));
matcher_fn.set_registry(registry);
FaceTrackerFunc ftracker_fn{cfg, registry, same_person};
FrameAnnotationFunc tracker_fn {};
ResultSinkFunc sink_fn {cfg, done}; ResultSinkFunc sink_fn {cfg, done};
// AR-012/AR-016: windows come from registry claims, and tracks still live
// at EOF must be flushed or the closing scene's cast is never emitted.
registry->on_track_dead([&sink_fn](const DeadTrack& d) { sink_fn.add_claim(d); });
sink_fn.set_pre_write_hook([registry](double last_ts) { registry->flush(last_ts); });
// ── KPN ObjectNodes ─────────────────────────────────────────────────────── // ── KPN ObjectNodes ───────────────────────────────────────────────────────
kpn::ObjectNode<FrameSourceFunc, kpn::in<>, kpn::out<"raw">, "frame_source", 0> source (source_fn, 32); kpn::ObjectNode<FrameSourceFunc, kpn::in<>, kpn::out<"raw">, "frame_source", 0> source (source_fn, 32);
kpn::ObjectNode<CameraPositionChangeDetectorFunc, kpn::in<"raw">, kpn::out<"frame">, "camera_pos", 0> campos (campos_fn, 32); kpn::ObjectNode<CameraPositionChangeDetectorFunc, kpn::in<"raw">, kpn::out<"frame">, "camera_pos", 0> campos (campos_fn, 32);
@@ -157,13 +178,13 @@ int main(int argc, char** argv) {
kpn::ObjectNode<EmbedderFunc, kpn::in<"aligned">, kpn::out<"embedded">, "embedder", 0> embedder (embedder_fn, 32); kpn::ObjectNode<EmbedderFunc, kpn::in<"aligned">, kpn::out<"embedded">, "embedder", 0> embedder (embedder_fn, 32);
kpn::ObjectNode<FaceTrackerFunc, kpn::in<"embedded">, kpn::out<"tracked">, "face_tracker", 0> ftracker (ftracker_fn, 16); kpn::ObjectNode<FaceTrackerFunc, kpn::in<"embedded">, kpn::out<"tracked">, "face_tracker", 0> ftracker (ftracker_fn, 16);
kpn::ObjectNode<IdentityMatcherFunc, kpn::in<"tracked">, kpn::out<"matched">, "identity_matcher", 0> matcher (matcher_fn, 16); kpn::ObjectNode<IdentityMatcherFunc, kpn::in<"tracked">, kpn::out<"matched">, "identity_matcher", 0> matcher (matcher_fn, 16);
kpn::ObjectNode<SceneTrackerFunc, kpn::in<"matched">, kpn::out<"annotation">, "scene_tracker", 0> tracker (tracker_fn, 16); kpn::ObjectNode<FrameAnnotationFunc, kpn::in<"matched">, kpn::out<"annotation">, "frame_annotation", 0> tracker (tracker_fn, 16);
kpn::ObjectNode<ResultSinkFunc, kpn::in<"annotation">,kpn::out<>, "result_sink", 0> sink (sink_fn, 16); kpn::ObjectNode<ResultSinkFunc, kpn::in<"annotation">,kpn::out<>, "result_sink", 0> sink (sink_fn, 16);
// MainThreadNode — no thread spawned; driven by preview.step() below // MainThreadNode — no thread spawned; driven by preview.step() below
PreviewNode preview{cfg, 16}; PreviewNode preview{cfg, 16};
// matcher → FanoutNode<MatchedSceneFrame,2> → [scene_tracker, preview] (auto-inserted) // matcher → FanoutNode<MatchedSceneFrame,2> → [frame_annotation, preview] (auto-inserted)
auto net = kpn::make_network( auto net = kpn::make_network(
kpn::edge(source.output<"raw">(), campos.input<"raw">()), kpn::edge(source.output<"raw">(), campos.input<"raw">()),
kpn::edge(campos.output<"frame">(), detector.input<"frame">()), kpn::edge(campos.output<"frame">(), detector.input<"frame">()),
+11 -2
View File
@@ -81,7 +81,16 @@ public:
using DeadTrackFn = std::function<void(const DeadTrack&)>; using DeadTrackFn = std::function<void(const DeadTrack&)>;
struct Config { struct Config {
double extinction_sec{5.0}; ///< how long a lost track stays revivable /// How long a lost track stays available for re-association.
///
/// Named to match Config::track_extinction_sec, which feeds it, and
/// deliberately NOT `extinction_sec`: that name belonged to the
/// withdrawn actor keep-alive, and SPEC.md's removal list ends "grep
/// for both names and expect no survivors". A survivor here would be
/// the one false positive in that grep, on a field that means
/// something else entirely -- this one bounds re-association and never
/// extends a presence claim.
double track_extinction_sec{5.0};
float ownership_logodds{2.0f}; ///< belief needed to own a track (~0.88 posterior) float ownership_logodds{2.0f}; ///< belief needed to own a track (~0.88 posterior)
}; };
@@ -216,7 +225,7 @@ private:
void tick_locked(double now) { void tick_locked(double now) {
for (auto it = tracks_.begin(); it != tracks_.end(); ) { for (auto it = tracks_.begin(); it != tracks_.end(); ) {
const auto& ls = it->second.last_seen; const auto& ls = it->second.last_seen;
if (ls && (now - *ls) > cfg_.extinction_sec) { if (ls && (now - *ls) > cfg_.track_extinction_sec) {
emit_locked(it->second, *ls); emit_locked(it->second, *ls);
it = tracks_.erase(it); it = tracks_.erase(it);
} else { } else {
+1 -1
View File
@@ -73,7 +73,7 @@ struct Rig {
: reg(std::make_shared<TrackRegistry>( : reg(std::make_shared<TrackRegistry>(
[extinction] { [extinction] {
TrackRegistry::Config c; TrackRegistry::Config c;
c.extinction_sec = extinction; c.track_extinction_sec = extinction;
return c; return c;
}(), }(),
EvidenceDiscounter([](float cos) { return std::max(0.f, cos); }))) EvidenceDiscounter([](float cos) { return std::max(0.f, cos); })))
+1 -1
View File
@@ -128,7 +128,7 @@ struct Replay {
Replay run(const Dump& d, double extinction = 10.0) { Replay run(const Dump& d, double extinction = 10.0) {
Replay r; Replay r;
TrackRegistry::Config rc; TrackRegistry::Config rc;
rc.extinction_sec = extinction; rc.track_extinction_sec = extinction;
auto cal = [](float cos) { return std::max(0.f, cos); }; auto cal = [](float cos) { return std::max(0.f, cos); };
auto reg = std::make_shared<TrackRegistry>(rc, EvidenceDiscounter(cal)); auto reg = std::make_shared<TrackRegistry>(rc, EvidenceDiscounter(cal));
+1 -1
View File
@@ -45,7 +45,7 @@ EvidenceDiscounter disc() {
TrackRegistry::Config cfg(double extinction = 5.0, float own = 2.0f) { TrackRegistry::Config cfg(double extinction = 5.0, float own = 2.0f) {
TrackRegistry::Config c; TrackRegistry::Config c;
c.extinction_sec = extinction; c.track_extinction_sec = extinction;
c.ownership_logodds = own; c.ownership_logodds = own;
return c; return c;
} }