// AR-011 — the boundary dedup window is derived from the stream's cadence, not // assumed. // // TRACES: AR-011 | SR-002 | UT-003 // // Tier T1: the derivation is arithmetic on frame timestamps, so it is checked // against synthetic cadences at 24, 25 and 30 fps rather than against a decode. // The number this replaced was 0.04 s — one frame at 25 fps, correct for exactly // one of those three and quietly wrong for the other two. // // SceneDetectorFunc is never constructed: its constructor loads TransNetV2. Only // the static rule is called, so make_scene_detector() is never odr-used. #include #include "nodes/scene_detector_node.hpp" #include namespace { // The intervals the node accumulates from a steady stream at `fps`. std::vector cadence(double fps, int n = 200) { return std::vector(static_cast(n), 1.0 / fps); } double window(double fps) { return SceneDetectorFunc::dedup_window_sec(cadence(fps)); } } // namespace // ── The property that has to hold at every rate ────────────────────────────── // The window has exactly one job: tell "one frame scored twice by two // overlapping windows" (a gap of zero) from "two adjacent frames, both of them // real cuts" (a gap of one frame interval). It has to sit strictly between. TEST_CASE("the dedup window separates a duplicate from an adjacent frame", "[scene][AR-011]") { for (double fps : {24.0, 25.0, 30.0, 23.976, 29.97, 50.0, 60.0}) { INFO("source at " << fps << " fps"); const double frame = 1.0 / fps; const double w = window(fps); CHECK(w > 0.0); // a duplicate (gap 0) is still merged CHECK(w < frame); // two consecutive frames both survive } } // The concrete failure the hardcoded constant caused: at 30 fps a frame is // 0.0333 s, so a 0.04 s window swallowed a cut on the very next frame. Nothing in // the output showed it — the file just had fewer boundaries. TEST_CASE("cuts on consecutive frames survive at 30 fps", "[scene][AR-011]") { const double frame = 1.0 / 30.0; CHECK(window(30.0) < frame); CHECK(0.04 > frame); // the constant that was there, for the record } TEST_CASE("the window tracks the rate rather than a constant", "[scene][AR-011]") { // If it were still assumed, these would be equal. CHECK(window(24.0) > window(30.0)); CHECK(window(30.0) > window(60.0)); CHECK(window(25.0) == 0.5 / 25.0); } // ── Robustness of the estimate ─────────────────────────────────────────────── TEST_CASE("a seek or a dropped frame does not move the derived cadence", "[scene][AR-011]") { auto intervals = cadence(25.0); intervals[0] = 3.5; // a seek at the start intervals[97] = 0.4; // a gap where the decoder lost frames // Median, not mean: two long intervals out of 200 cannot shift it at all. CHECK(SceneDetectorFunc::dedup_window_sec(intervals) == 0.5 / 25.0); } TEST_CASE("too few frames to have a cadence yields an inert window", "[scene][AR-011]") { // Under two frames there is no interval to measure — and also no second // boundary to merge with, so a window of 0 changes nothing. Guessing a rate // here would be the mistake this requirement is about. CHECK(SceneDetectorFunc::dedup_window_sec({}) == 0.0); } TEST_CASE("a single observed interval is enough", "[scene][AR-011]") { CHECK(SceneDetectorFunc::dedup_window_sec({1.0 / 24.0}) == 0.5 / 24.0); }