#pragma once /// TRACES: AR-010 | SR-002 /// /// SceneBoundaryAnnotatorFunc — the join of the decode butterfly. /// /// `source` fans out to two branches: dense frames to TransNetV2, sampled frames /// to face detection. A boundary found on the first has to reach the second, and /// cannot ride along in the frame because the branches run in parallel. /// /// This node sits on the sampled branch and stamps `Frame::is_scene_boundary` /// from the detector's published verdict. /// /// **It only works because the sampled branch lags.** TransNetV2 buffers /// `kWindow` frames before it can score any of them, so this node must not reach /// a frame before the detector has an opinion about it. Channel depth creates /// that lag: with backpressure (AR-004) the fanout blocks on the slower branch, /// so a deep channel here lets the detector run ahead by its window instead of /// anything being dropped. /// /// When the lag is insufficient the node **counts it** rather than guessing. /// Annotating an unscored frame as boundary-free is indistinguishable from a /// genuine "no boundary here", and that is the failure that makes a downstream /// test pass while verifying nothing. #include "scene_boundaries.hpp" #include "types.hpp" #include #include #include struct SceneBoundaryAnnotatorFunc { static constexpr std::string_view label() { return "scene_annotate"; } /// `tol` is half a sample interval. The branches sample at different rates, /// so a boundary found on a dense frame rarely lands exactly on a sampled /// one; half an interval attributes it to the nearest sampled frame and no /// further. SceneBoundaryAnnotatorFunc(std::shared_ptr b, double tol) : bounds_(std::move(b)), tol_(tol) {} Frame operator()(Frame f) { if (f.eof || !bounds_) return f; // Wait for the detector's verdict to cover this frame. Channel depth // alone cannot provide the lag: it holds frames back only when the // consumer is slower, and this branch is orders of magnitude faster per // frame than TransNetV2. Blocking here is what makes the join real. // // Safe under backpressure because the branches are independent: this // node stalling does not stop the detector consuming dense frames, and // the fanout keeps feeding it. if (!bounds_->wait_until_scored(f.timestamp_sec)) { // The detector finished without covering this frame — the tail after // its last full window. Unknown, not negative; counted so it cannot // pass for "no boundary here". bounds_->note_outran(); return f; } f.is_scene_boundary = bounds_->is_boundary(f.timestamp_sec, tol_); return f; } private: std::shared_ptr bounds_; double tol_{0.0}; };