// TRACES: AR-028 | VR-001 | UT-139, UT-140, UT-141 | SR-002 // // The other half of AR-028: the quality vector has to *survive into the dump*. // Measuring it at inference and then leaving it in a struct that dies at the // EmbeddedSceneFrame channel would satisfy the letter of "assessed" and none of // the point — VR-012 sets its knees from recorded data, and what the dump does // not carry cannot be re-litigated without re-running video on a GPU. // // Tier T2, but cheap: EmbeddingDumpFunc is a sink, so it can be driven directly // with hand-built frames. No model, no video, no gallery — the embedder stamp // tolerates an unset model path (GR-004 records it as unverifiable). #include #include #include "config.hpp" #include "nodes/embedding_dump_node.hpp" #include "types.hpp" #include #include #include #include #include #include using Catch::Matchers::WithinAbs; namespace { namespace fs = std::filesystem; // Removes the file on scope exit so a failing assertion cannot leave the next // run reading a stale dump. struct TempDump { fs::path path; explicit TempDump(const char* stem) : path(fs::temp_directory_path() / (std::string("sae_") + stem + ".h5")) { std::remove(path.c_str()); } ~TempDump() { std::error_code ec; fs::remove(path, ec); } }; EmbeddedSceneFrame frame_with(double ts, const std::vector>& quality) { EmbeddedSceneFrame ef; ef.source.timestamp_sec = ts; ef.source.frame_idx = static_cast(ts * 5.0); for (const auto& [sharpness, residual] : quality) { DetectedFace f; f.bbox = cv::Rect2f(10.f, 20.f, 60.f, 60.f); f.confidence = 0.8f; f.sharpness = sharpness; f.alignment_residual = residual; ef.faces.push_back(f); Embedding e{}; e[0] = 1.f; ef.embeddings.push_back(e); } return ef; } EmbeddedSceneFrame eof_frame() { EmbeddedSceneFrame ef; ef.source.eof = true; return ef; } std::vector read_face_col(const H5::H5File& f, const char* name) { H5::DataSet ds = f.openDataSet(std::string("faces/") + name); hsize_t n = 0; ds.getSpace().getSimpleExtentDims(&n, nullptr); std::vector out(n); if (n) ds.read(out.data(), H5::PredType::NATIVE_FLOAT); return out; } int read_schema_version(const H5::H5File& f) { int v = 0; f.openAttribute("schema_version").read(H5::PredType::NATIVE_INT, &v); return v; } } // namespace TEST_CASE("the quality vector survives into the dump", "[dump][AR-028][UT-139]") { TempDump tmp("quality_roundtrip"); Config cfg; cfg.dump_embeddings_path = tmp.path.string(); cfg.movie_path = "synthetic"; cfg.sample_fps = 5.f; std::atomic done{false}; { EmbeddingDumpFunc dump(cfg, done); dump(frame_with(0.0, {{3.25f, 0.75f}, {0.5f, 4.5f}})); dump(frame_with(0.2, {})); // a frame with no faces dump(frame_with(0.4, {{12.0f, 0.0f}})); dump(eof_frame()); } REQUIRE(done.load()); REQUIRE(fs::exists(tmp.path)); H5::H5File f(tmp.path.string(), H5F_ACC_RDONLY); const std::vector sharp = read_face_col(f, "sharpness"); const std::vector resid = read_face_col(f, "alignment_residual"); const std::vector conf = read_face_col(f, "confidence"); // Parallel to every other per-face array, so a consumer can index the // quality of face i with the same slice it uses for the embedding. REQUIRE(sharp.size() == conf.size()); REQUIRE(resid.size() == conf.size()); REQUIRE(sharp.size() == 3); CHECK_THAT(sharp[0], WithinAbs(3.25f, 1e-6f)); CHECK_THAT(sharp[1], WithinAbs(0.50f, 1e-6f)); CHECK_THAT(sharp[2], WithinAbs(12.0f, 1e-6f)); CHECK_THAT(resid[0], WithinAbs(0.75f, 1e-6f)); CHECK_THAT(resid[1], WithinAbs(4.50f, 1e-6f)); CHECK_THAT(resid[2], WithinAbs(0.00f, 1e-6f)); } TEST_CASE("a dump carrying the quality vector announces itself as v2", "[dump][AR-028][UT-140]") { // The bump is not for readers — they check for the datasets by name, and a // v1 dump still replays. It is so a consumer of the vector can tell "these // faces were never scored" from "these faces scored zero", which is not // recoverable from the arrays. Same reason scene_detect is an attribute. TempDump tmp("quality_version"); Config cfg; cfg.dump_embeddings_path = tmp.path.string(); cfg.movie_path = "synthetic"; std::atomic done{false}; { EmbeddingDumpFunc dump(cfg, done); dump(frame_with(0.0, {{1.f, 1.f}})); dump(eof_frame()); } H5::H5File f(tmp.path.string(), H5F_ACC_RDONLY); CHECK(read_schema_version(f) == 2); } TEST_CASE("an unscored face keeps its sentinel through the dump", "[dump][AR-028][UT-141]") { // The aligner admits no unscored face, so this state should be unreachable. // The dump still must not clamp it: -1 is how a future path that skipped // scoring would be caught, and rewriting it to 0 would hide that path behind // a legitimate-looking "featureless crop" reading. TempDump tmp("quality_sentinel"); Config cfg; cfg.dump_embeddings_path = tmp.path.string(); cfg.movie_path = "synthetic"; std::atomic done{false}; { EmbeddingDumpFunc dump(cfg, done); dump(frame_with(0.0, {{-1.f, -1.f}})); dump(eof_frame()); } H5::H5File f(tmp.path.string(), H5F_ACC_RDONLY); CHECK(read_face_col(f, "sharpness")[0] < 0.f); CHECK(read_face_col(f, "alignment_residual")[0] < 0.f); }