// Replay tests — the real tracker and registry driven from committed fixtures. // // TRACES: AR-002, AR-004, AR-012, AR-013 | VR-001, VR-002 | IT-001 // // Tier T2: composition, not units. The registry tests construct awkward states // directly; these check that the pieces behave when wired together and fed real // footage — 480x360 public-domain clips at 5 fps, with the cuts, gaps and // crowded frames that actual film produces and synthetic input does not. // // No GPU and no model: the fixtures are HDF5 dumps taken after embedding, so // everything here is CPU maths. That is what lets this run on the CI host at // all (see docs/requirements.md, "CI never calls a model"). // // Driving the node functors directly rather than through a KPN network is // deliberate: functors are plain objects, so there are no threads, no channels // and no scheduling — the same input gives the same output every time, which is // exactly what a fixture-based test needs. #include #include "config.hpp" #include "evidence_discount.hpp" #include "nodes/face_tracker_node.hpp" #include "track_registry.hpp" #include "types.hpp" #include #include #include #include #include #include #include namespace { // ── Fixture reader ─────────────────────────────────────────────────────────── // The flat/ragged layout of scripts/optimizer/SCHEMA.md: per-face arrays // concatenated, with a per-frame index table pointing into them. struct Dump { std::vector ts; std::vector is_cut; std::vector face_offset; std::vector face_count; std::vector emb; std::vector bbox; // 4 per face std::string embedder; float min_face_px{0.f}; // AR-002, as the run was configured float bbox_upscale{1.f}; // bbox × this = original resolution std::size_t frames() const { return ts.size(); } std::size_t faces() const { return emb.size(); } }; template std::vector read1d(H5::Group& g, const char* name, const H5::DataType& dt) { H5::DataSet ds = g.openDataSet(name); hsize_t n = 0; ds.getSpace().getSimpleExtentDims(&n, nullptr); std::vector out(n); if (n) ds.read(out.data(), dt); return out; } Dump load(const std::string& path) { H5::H5File f(path, H5F_ACC_RDONLY); H5::Group frames = f.openGroup("frames"); H5::Group faces = f.openGroup("faces"); Dump d; d.ts = read1d(frames, "timestamp_sec", H5::PredType::NATIVE_DOUBLE); d.is_cut = read1d(frames, "is_cut", H5::PredType::NATIVE_UINT8); d.face_offset = read1d(frames, "face_offset", H5::PredType::NATIVE_INT64); d.face_count = read1d(frames, "face_count", H5::PredType::NATIVE_INT32); H5::DataSet e = faces.openDataSet("embedding"); hsize_t dims[2]{0, 0}; e.getSpace().getSimpleExtentDims(dims, nullptr); std::vector flat(dims[0] * dims[1]); if (!flat.empty()) e.read(flat.data(), H5::PredType::NATIVE_FLOAT); d.emb.resize(dims[0]); for (hsize_t i = 0; i < dims[0]; ++i) std::copy_n(flat.begin() + i * dims[1], 512, d.emb[i].begin()); // bbox is 2-D [N,4]; reading it with the 1-D helper would size the buffer // from the first extent only and then read four times that many floats. { H5::DataSet bs = faces.openDataSet("bbox"); hsize_t bd[2]{0, 0}; bs.getSpace().getSimpleExtentDims(bd, nullptr); d.bbox.resize(bd[0] * bd[1]); if (!d.bbox.empty()) bs.read(d.bbox.data(), H5::PredType::NATIVE_FLOAT); } // GR-004: the dump records which embedder produced it, so a replay cannot // be silently scored against a gallery from a different model. if (f.attrExists("embedder_model")) { // Written as a variable-length string (embedding_dump_node.hpp:99), so // the read must name the same type explicitly. H5::StrType vlen(H5::PredType::C_S1, H5T_VARIABLE); f.openAttribute("embedder_model").read(vlen, d.embedder); } // AR-002: the threshold the run was configured with, and the scale its boxes // are in. Read from the dump rather than assumed, so the check is against // what this fixture was actually generated with — the hero clips predate the // move to 40 px and were dumped at 32. if (f.attrExists("min_face_px")) f.openAttribute("min_face_px").read(H5::PredType::NATIVE_FLOAT, &d.min_face_px); if (f.attrExists("bbox_upscale")) f.openAttribute("bbox_upscale").read(H5::PredType::NATIVE_FLOAT, &d.bbox_upscale); return d; } std::string fixture(const char* name) { return std::string(SAE_TEST_FIXTURES_DIR) + "/dumps/" + name; } // ── Harness ────────────────────────────────────────────────────────────────── struct Replay { std::vector claims; std::vector track_ids; // per face, in fixture order std::size_t faces_seen{0}; }; Replay run(const Dump& d, double extinction = 10.0) { Replay r; TrackRegistry::Config rc; rc.extinction_sec = extinction; auto cal = [](float cos) { return std::max(0.f, cos); }; auto reg = std::make_shared(rc, EvidenceDiscounter(cal)); reg->on_track_dead([&r](const DeadTrack& t) { r.claims.push_back(t); }); Config cfg; cfg.track_assoc_min_prob = 0.5f; FaceTrackerFunc ft(cfg, reg, cal); for (std::size_t i = 0; i < d.frames(); ++i) { EmbeddedSceneFrame ef; ef.source.timestamp_sec = d.ts[i]; ef.source.is_cut = d.is_cut[i] != 0; const int64_t off = d.face_offset[i]; const int32_t n = d.face_count[i]; for (int32_t k = 0; k < n; ++k) { DetectedFace face; const float* b = &d.bbox[(off + k) * 4]; face.bbox = cv::Rect2f(b[0], b[1], b[2], b[3]); face.confidence = 1.0f; ef.faces.push_back(face); ef.crops.push_back(cv::Mat()); ef.embeddings.push_back(d.emb[off + k]); } r.faces_seen += static_cast(n); auto out = ft(std::move(ef)); for (int id : out.track_ids) r.track_ids.push_back(id); } reg->flush(d.ts.empty() ? 0.0 : d.ts.back()); return r; } } // namespace // ── AR-004 / VR-001 — the fixtures are intact and self-describing ──────────── TEST_CASE("superhero fixture is complete", "[replay][VR-001]") { Dump d = load(fixture("superhero.h5")); CHECK(d.frames() == 5128); CHECK(d.faces() == 4307); CHECK(d.embedder == "LVFace-B_Glint360K.onnx"); int64_t running = 0; for (std::size_t i = 0; i < d.frames(); ++i) { REQUIRE(d.face_offset[i] == running); running += d.face_count[i]; } CHECK(static_cast(running) == d.faces()); } // ── AR-002 — the size filter held, all the way to the dump ─────────────────── // The T1 arithmetic is in test_face_detector_node.cpp. This is the other half: // that the rule was applied on real footage and nothing downstream of it let an // undersized face back in. TEST_CASE("no dumped face is below the configured minimum size", "[replay][AR-002]") { Dump d = load(fixture("superhero.h5")); // A dump that did not record its threshold cannot be checked against one. REQUIRE(d.min_face_px > 0.f); REQUIRE(d.bbox_upscale > 0.f); float smallest_side = std::numeric_limits::max(); for (std::size_t i = 0; i < d.faces(); ++i) { const float w = d.bbox[i * 4 + 2] * d.bbox_upscale; // original resolution const float h = d.bbox[i * 4 + 3] * d.bbox_upscale; REQUIRE(w >= d.min_face_px); REQUIRE(h >= d.min_face_px); smallest_side = std::min({smallest_side, w, h}); } // And the filter was binding, not vacuously satisfied. 480x360 footage puts // faces right on the cutoff, which is what makes this fixture worth checking: // if the threshold stopped being applied the assertions above would still // pass on a corpus of close-ups. CHECK(smallest_side < d.min_face_px * 1.05f); } TEST_CASE("replaying the superhero fixture twice gives identical tracks", "[replay][VR-002]") { Dump d = load(fixture("superhero.h5")); Replay a = run(d); Replay b = run(d); REQUIRE(a.track_ids.size() == b.track_ids.size()); CHECK(a.track_ids == b.track_ids); REQUIRE(a.claims.size() == b.claims.size()); } TEST_CASE("every face is assigned a track and every track closes", "[replay][AR-012]") { Dump d = load(fixture("superhero.h5")); Replay r = run(d); CHECK(r.track_ids.size() == r.faces_seen); for (int id : r.track_ids) CHECK(id >= 0); // nothing silently unassigned // flush() must leave nothing behind: a track still open at EOF would be a // window that never reaches the output. CHECK(r.claims.size() > 0); } TEST_CASE("windows are well-formed and inside the film", "[replay][AR-013]") { for (const char* f : {"superhero.h5", "superhero.h5", "superhero.h5", "superhero.h5", "superhero.h5"}) { INFO(f); Dump d = load(fixture(f)); Replay r = run(d); const double t0 = d.ts.front(), t1 = d.ts.back(); for (const auto& c : r.claims) { // A window ends at the last sighting, never after it — so it can // never extend past the footage that produced it. CHECK(c.first_seen <= c.last_seen); CHECK(c.first_seen >= t0); CHECK(c.last_seen <= t1); } } }