#pragma once /// TRACES: AR-028 | VR-001, VR-010 | PR-002 #include "types.hpp" #include "config.hpp" #include "gallery/embedder_stamp.hpp" #include #include // cv::calcHist for the per-frame RGB histogram #include #include #include #include #include #include #include // ── DumpProvenance ──────────────────────────────────────────────────────────── /// TRACES: VR-010 | PR-002 // Everything that determined a dump's *content*, read back tolerantly. // // Two dumps of the same film with different detector thresholds, a different // `dense_scale`, or scene detection on versus off are different measurements of // different things — but they are byte-shaped identically, so a consumer that // mixes them gets a plausible number from an incoherent input. GR-004 closed the // worst case (a cross-model replay, where every cosine is meaningless); this // closes the rest. // // Every field is optional because dumps written before VR-010 lack the // attributes. A missing field reads as *unknown*, never as a default — a // silently-defaulted `detector_conf` is exactly the fabricated provenance the // requirement exists to prevent ("a fixture whose provenance is unknown is worse // than no fixture, because it will be trusted"). struct DumpProvenance { // Model identity std::optional embedder_model; // GR-004 std::optional embedder_sha256; // GR-004 std::optional detector_model; // Sampling std::optional movie; std::optional sample_fps; std::optional start_sec; std::optional end_sec; // -1 = to end of file // Detection — what the run admitted into the dump std::optional detector_conf; std::optional detector_nms; std::optional min_face_px; std::optional max_faces; // 0 = uncapped (AR-003) // Frame geometry std::optional dense_scale; std::optional bbox_upscale; // faces/bbox × this = original-resolution px std::optional cut_threshold; // Scene detection. The reason this flag exists: `is_scene_boundary` is // all-zero both when TransNetV2 found no boundaries and when it never ran, // and no amount of staring at the array distinguishes them. std::optional scene_detect; // Downstream knob that shaped nothing in the dump but everything a replay is // compared against — recorded so a sweep can be told apart from the baseline. std::optional track_assoc_min_prob; }; // Read whatever provenance a dump carries. Never throws on a missing attribute; // an old dump simply yields a DumpProvenance full of empty optionals. inline DumpProvenance read_dump_provenance(const H5::H5File& f) { DumpProvenance p; auto str = [&](const char* n, std::optional& out) { if (!f.attrExists(n)) return; // Written as a variable-length string, so the read must name the same // type explicitly — the default would truncate to a fixed length. H5::StrType vlen(H5::PredType::C_S1, H5T_VARIABLE); std::string v; f.openAttribute(n).read(vlen, v); out = v; }; auto num = [&](const char* n, const H5::PredType& dt, auto& out) { if (!f.attrExists(n)) return; typename std::decay_t::value_type v{}; f.openAttribute(n).read(dt, &v); out = v; }; str("embedder_model", p.embedder_model); str("embedder_sha256", p.embedder_sha256); str("detector_model", p.detector_model); str("movie", p.movie); num("sample_fps", H5::PredType::NATIVE_FLOAT, p.sample_fps); num("start_sec", H5::PredType::NATIVE_DOUBLE, p.start_sec); num("end_sec", H5::PredType::NATIVE_DOUBLE, p.end_sec); num("detector_conf", H5::PredType::NATIVE_FLOAT, p.detector_conf); num("detector_nms", H5::PredType::NATIVE_FLOAT, p.detector_nms); num("min_face_px", H5::PredType::NATIVE_FLOAT, p.min_face_px); num("max_faces", H5::PredType::NATIVE_INT, p.max_faces); num("dense_scale", H5::PredType::NATIVE_FLOAT, p.dense_scale); num("bbox_upscale", H5::PredType::NATIVE_FLOAT, p.bbox_upscale); num("cut_threshold", H5::PredType::NATIVE_FLOAT, p.cut_threshold); num("track_assoc_min_prob", H5::PredType::NATIVE_FLOAT, p.track_assoc_min_prob); if (f.attrExists("scene_detect")) { uint8_t v = 0; f.openAttribute("scene_detect").read(H5::PredType::NATIVE_UINT8, &v); p.scene_detect = (v != 0); } return p; } // ── EmbeddingDumpFunc ───────────────────────────────────────────────────────── // KPN sink that taps the EmbeddedSceneFrame channel and writes the per-frame face // metadata + embeddings to one HDF5 file (schema: scripts/optimizer/SCHEMA.md). // The dump is the expensive, parameter-independent half of the pipeline // (decode→detect→align→embed); replaying it lets a threshold sweep re-run the cheap // downstream nodes thousands of times with no GPU. See sae_kpn / scripts/optimizer. // // Accumulates in flat/ragged arrays and writes once on EOF. struct EmbeddingDumpFunc { static constexpr std::string_view label() { return "embedding_dump"; } EmbeddingDumpFunc(const Config& cfg, std::atomic& done) : path_(cfg.dump_embeddings_path), movie_(cfg.movie_path), sample_fps_(cfg.sample_fps), done_(done) { /// TRACES: GR-004 | SR-001 // A dump is a bag of embeddings with no model attached, replayed against a // gallery hours or weeks later — the same silent cross-model hazard as the // gallery itself, so it carries the same stamp. stamp_ = make_embedder_stamp(cfg.arcface_model); /// TRACES: VR-010 | PR-002 // The rest of what determined this file's content. Captured from the live // Config at construction, so it describes the run that is being written // rather than whatever config happens to be lying around at read time. prov_.detector_model = basename_of(cfg.detector_model); prov_.detector_conf = cfg.detector_conf; prov_.detector_nms = cfg.detector_nms; prov_.min_face_px = cfg.min_face_px; prov_.max_faces = cfg.max_faces; prov_.cut_threshold = cfg.cut_threshold; prov_.dense_scale = cfg.dense_scale; prov_.start_sec = cfg.start_sec; prov_.end_sec = cfg.end_sec; prov_.scene_detect = cfg.scene_detect; prov_.track_assoc_min_prob = cfg.track_assoc_min_prob; std::cerr << "[embedding_dump] writing " << path_ << " embedder: " << stamp_.describe() << " detector: " << *prov_.detector_model << " @conf " << cfg.detector_conf << " scene_detect=" << (cfg.scene_detect ? "on" : "off") << "\n"; } void operator()(EmbeddedSceneFrame ef) { if (ef.source.eof) { flush(); return; } /// TRACES: VR-010 | PR-002 // Taken from the frames themselves, not recomputed from dense_scale — the // factor the source actually stamped on them is the one that maps // faces/bbox back to original resolution, whatever rule produced it. if (!prov_.bbox_upscale) prov_.bbox_upscale = ef.source.bbox_upscale; const int32_t n = static_cast(ef.faces.size()); ts_.push_back(ef.source.timestamp_sec); fidx_.push_back(ef.source.frame_idx); is_cut_.push_back(ef.source.is_cut ? 1 : 0); is_bnd_.push_back(ef.source.is_scene_boundary ? 1 : 0); // Per-frame normalised RGB histogram (kHistBins per channel), for offline // training of a learned scene-boundary detector against X-Ray scene // boundaries — the grayscale-correlation cut detector is blind on // low-contrast grades (Scarface: 1 cut in 10k frames). Cheap and the frame // is already decoded here; empty frame → zeros. append_rgb_hist(ef.source.image); face_off_.push_back(static_cast(conf_.size())); face_cnt_.push_back(n); for (int i = 0; i < n; ++i) { const auto& f = ef.faces[i]; bbox_.insert(bbox_.end(), {f.bbox.x, f.bbox.y, f.bbox.width, f.bbox.height}); for (int k = 0; k < 5; ++k) { lmk_.push_back(f.landmarks[k].x); lmk_.push_back(f.landmarks[k].y); } conf_.push_back(f.confidence); /// TRACES: AR-028 | SR-002 // The quality vector, carried rather than consumed: written beside // the embedding it describes so VR-012 can locate its knees against // recorded data instead of by re-running video. Size is the third // axis and is already here as bbox + the bbox_upscale attribute. // Both are -1 only if a face reached the dump unscored, which the // aligner does not allow — the sentinel is preserved rather than // clamped so that a future path which did would be visible. sharp_.push_back(f.sharpness); resid_.push_back(f.alignment_residual); const auto& e = ef.embeddings[i]; emb_.insert(emb_.end(), e.begin(), e.end()); } } void flush() { if (written_.exchange(true)) return; try { write_hdf5(); } catch (const H5::Exception& e) { std::cerr << "[embedding_dump] HDF5 error: " << e.getDetailMsg() << "\n"; } done_.store(true, std::memory_order_release); } private: // Root attributes are additive: schema_version stayed 1 across VR-010, because // every reader takes attributes by name with a default (replay.py) or an // existence check (read_dump_provenance), so an old dump loses nothing and a // new dump breaks nothing. A bump is for a change to the *datasets*. // // v2 is that change: AR-028 adds faces/sharpness and faces/alignment_residual. // The bump is not about readers — those check for the datasets by name, and a // v1 dump still replays. It is so a *consumer of the quality vector* can tell // "this film's faces were never scored" from "this film's faces scored zero", // which is the same distinction scene_detect exists to make and is likewise // not recoverable from the arrays. A v1 dump reports the vector as unknown; // re-dump to acquire it, since nobody can assert after the fact how sharp a // face was. static constexpr int kSchemaVersion = 2; static constexpr int kEmbedDim = 512; static std::string basename_of(const std::string& path) { const auto slash = path.find_last_of("/\\"); return slash == std::string::npos ? path : path.substr(slash + 1); } template void write_vec(H5::Group& g, const char* name, const std::vector& v, const H5::PredType& dtype, hsize_t cols = 0) { hsize_t rows = cols ? v.size() / cols : v.size(); std::vector dims = cols ? std::vector{rows, cols} : std::vector{rows}; H5::DataSpace space(static_cast(dims.size()), dims.data()); auto ds = g.createDataSet(name, dtype, space); if (!v.empty()) ds.write(v.data(), dtype); } static void attr_str(H5::H5File& f, const char* name, const std::string& v) { H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE); f.createAttribute(name, str, H5::DataSpace(H5S_SCALAR)).write(str, v); } template static void attr_num(H5::H5File& f, const char* name, const H5::PredType& dt, T v) { f.createAttribute(name, dt, H5::DataSpace(H5S_SCALAR)).write(dt, &v); } void write_hdf5() { H5::H5File file(path_, H5F_ACC_TRUNC); // root attrs attr_num(file, "schema_version", H5::PredType::NATIVE_INT, kSchemaVersion); attr_num(file, "embed_dim", H5::PredType::NATIVE_INT, kEmbedDim); attr_num(file, "sample_fps", H5::PredType::NATIVE_FLOAT, sample_fps_); attr_str(file, "movie", movie_); /// TRACES: GR-004 | SR-001 attr_str(file, "embedder_model", stamp_.model_name); attr_str(file, "embedder_sha256", stamp_.model_sha256); /// TRACES: VR-010 | PR-002 attr_str(file, "detector_model", prov_.detector_model.value_or("")); attr_num(file, "detector_conf", H5::PredType::NATIVE_FLOAT, *prov_.detector_conf); attr_num(file, "detector_nms", H5::PredType::NATIVE_FLOAT, *prov_.detector_nms); attr_num(file, "min_face_px", H5::PredType::NATIVE_FLOAT, *prov_.min_face_px); attr_num(file, "max_faces", H5::PredType::NATIVE_INT, *prov_.max_faces); attr_num(file, "cut_threshold", H5::PredType::NATIVE_FLOAT, *prov_.cut_threshold); attr_num(file, "dense_scale", H5::PredType::NATIVE_FLOAT, *prov_.dense_scale); // Recorded, NOT applied — faces/bbox stays in the detector's own frame // space so a replay feeds the tracker exactly what the live run fed it. attr_num(file, "bbox_upscale", H5::PredType::NATIVE_FLOAT, prov_.bbox_upscale.value_or(1.f)); attr_num(file, "start_sec", H5::PredType::NATIVE_DOUBLE, *prov_.start_sec); attr_num(file, "end_sec", H5::PredType::NATIVE_DOUBLE, *prov_.end_sec); attr_num(file, "track_assoc_min_prob", H5::PredType::NATIVE_FLOAT, *prov_.track_assoc_min_prob); // 0/1, matching the uint8 booleans in frames/. Tells "TransNetV2 found no // boundaries" apart from "TransNetV2 never ran", which is/was the same // all-zero is_scene_boundary array either way. attr_num(file, "scene_detect", H5::PredType::NATIVE_UINT8, static_cast(*prov_.scene_detect ? 1 : 0)); H5::Group frames = file.createGroup("frames"); write_vec(frames, "timestamp_sec", ts_, H5::PredType::NATIVE_DOUBLE); write_vec(frames, "frame_idx", fidx_, H5::PredType::NATIVE_INT64); write_vec(frames, "is_cut", is_cut_, H5::PredType::NATIVE_UINT8); write_vec(frames, "is_scene_boundary", is_bnd_, H5::PredType::NATIVE_UINT8); write_vec(frames, "face_offset", face_off_, H5::PredType::NATIVE_INT64); write_vec(frames, "face_count", face_cnt_, H5::PredType::NATIVE_INT32); // Per-frame normalised RGB histogram, kHistBins per channel laid out // [R(kHistBins) G(kHistBins) B(kHistBins)] per row. Feeds the learned // scene-boundary detector (see scripts/scene_detector/). write_vec(frames, "rgb_hist", rgb_hist_, H5::PredType::NATIVE_FLOAT, kHistBins * 3); H5::Group faces = file.createGroup("faces"); write_vec(faces, "embedding", emb_, H5::PredType::NATIVE_FLOAT, kEmbedDim); write_vec(faces, "bbox", bbox_, H5::PredType::NATIVE_FLOAT, 4); write_vec(faces, "landmarks", lmk_, H5::PredType::NATIVE_FLOAT, 10); write_vec(faces, "confidence", conf_, H5::PredType::NATIVE_FLOAT); /// TRACES: AR-028 | SR-002 write_vec(faces, "sharpness", sharp_, H5::PredType::NATIVE_FLOAT); write_vec(faces, "alignment_residual", resid_, H5::PredType::NATIVE_FLOAT); std::cerr << "[embedding_dump] wrote " << ts_.size() << " frames, " << conf_.size() << " faces → " << path_ << "\n"; } // Per-channel bin count for the RGB histogram. 32 → a 96-float row per frame, // ~40 KB per 10k-frame film: negligible next to the embeddings. static constexpr int kHistBins = 32; // Append the frame's normalised per-channel RGB histogram (R,G,B blocks). An // empty frame (EOF sentinels never reach here) yields a zero row so the array // stays parallel to ts_. void append_rgb_hist(const cv::Mat& img) { const size_t base = rgb_hist_.size(); rgb_hist_.resize(base + kHistBins * 3, 0.f); if (img.empty() || img.channels() != 3) return; float range[] = {0.f, 256.f}; const float* ranges[] = {range}; int bins = kHistBins; for (int c = 0; c < 3; ++c) { // OpenCV is BGR; store as B,G,R blocks cv::Mat h; cv::calcHist(&img, 1, &c, cv::Mat(), h, 1, &bins, ranges); cv::normalize(h, h, 1.0, 0.0, cv::NORM_L1); for (int b = 0; b < kHistBins; ++b) rgb_hist_[base + c * kHistBins + b] = h.at(b); } } std::string path_, movie_; EmbedderStamp stamp_; DumpProvenance prov_; float sample_fps_; std::atomic& done_; std::atomic written_{false}; std::vector ts_; std::vector fidx_; std::vector is_cut_, is_bnd_; std::vector face_off_; std::vector face_cnt_; std::vector emb_, bbox_, lmk_, conf_; std::vector sharp_, resid_; // AR-028 quality vector, parallel to conf_ std::vector rgb_hist_; // kHistBins*3 per frame, parallel to ts_ };