#pragma once #include "config.hpp" #include "inference/face_embedder.hpp" #include #include #include #include // ── EmbedderFunc ────────────────────────────────────────────────────────────── // KPN node: runs ArcFace on every 112×112 crop in an AlignedSceneFrame, // producing one L2-normalised 512-dim embedding per face. // // The inference backend (ONNX Runtime or raw TensorRT) is selected at compile // time; this node talks only to IFaceEmbedder via make_face_embedder(cfg). // // All crops in one frame are batched into a single forward pass (capped at // embed_batch_size). The backend serialises itself; we only call it from the // single embedder thread. // /// TRACES: AR-006 | SR-002 struct EmbedderFunc { static constexpr std::string_view label() { return "embedder"; } explicit EmbedderFunc(const Config& cfg) : embedder_(make_face_embedder(cfg)) , batch_size_(std::max(1, cfg.embed_batch_size)) { if (embedder_->max_batch() < static_cast(batch_size_)) throw std::runtime_error( "embed_batch_size " + std::to_string(batch_size_) + " exceeds backend max_batch " + std::to_string(embedder_->max_batch()) + " — rebuild the engine with EMBED_BATCH=" + std::to_string(batch_size_)); } EmbeddedSceneFrame operator()(AlignedSceneFrame af) { if (af.source.eof || af.crops.empty()) return {std::move(af.source), {}, {}, {}}; const auto& crops = af.crops; std::vector embeddings; embeddings.reserve(crops.size()); for (size_t i = 0; i < crops.size(); i += batch_size_) { const size_t end = std::min(i + batch_size_, crops.size()); std::vector chunk_crops(crops.begin() + i, crops.begin() + end); auto chunk = embedder_->embed(chunk_crops); embeddings.insert(embeddings.end(), chunk.begin(), chunk.end()); } return {std::move(af.source), std::move(af.faces), std::move(af.crops), std::move(embeddings)}; } private: std::unique_ptr embedder_; size_t batch_size_; };