open-standards/wai/wai-pack/tests/scenecut.rs
Transaction Science fb61bb69bd wai-pack: the policy, scene-cut segmentation and their corpus
A new crate beside wai-rs, with its own workspace and lock: the reference
packager of the packaging extension, built so far as far as a plan.

- policy.rs: wai.pack.policy/1 read in three stages (value kinds a canonical
  JSON cannot carry; the schema with every default written in; the
  constraints across members), each refusal named. The effective policy's
  canonical JSON goes through wai::jcs, and its digest is
  BLAKE3("wai:pack-policy\x01" || JCS).
- source/: Y4M to RGB8 (BT.709 limited range, integer), WAV PCM s16le.
- scenecut.rs: wai.pack.scenecut/1, one streaming segmenter with a bounded
  lookahead and the return and revisit clauses, so live and on-demand
  packing cut alike at equal lookahead; plan digest.
- classify.rs: static, synthetic, high motion, natural.
- fixture.rs: integer-generated Y4M fixtures (cuts-01, kinds-01).
- conformance.rs and wai-pack conformance regen|check.

pack-conformance/ holds the policy digests and equal-spelling pairs, a
refusal case per reason, the unit and colour conversions, and the scene-cut
vectors; pack_verify.py recomputes every one of them from the rules, with
its own TOML schema, JCS, readers, fixture generator and segmenter. It
found one defect while it was written: a millisecond bound converted to an
extreme timebase wrapped past 2^64; both conversions now saturate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 07:33:52 -04:00

60 lines
2.4 KiB
Rust

//! `wai.pack.scenecut/1` on the corpus fixtures, stated as the cuts a reader can check
//! against the fixture's definition (`src/fixture.rs`), beside the corpus's pinned
//! vectors.
use wai_pack::classify::classify;
use wai_pack::policy::Policy;
use wai_pack::scenecut::{segment, SegParams, FLAG_FORCED, FLAG_SCENE_CUT};
use wai_pack::{fixture, source::y4m};
const POLICY: &str = include_str!("../../pack-conformance/scenecut/policy.toml");
fn run(name: &str, lookahead: u64) -> Vec<(u64, u8, &'static str)> {
let p = Policy::parse(POLICY).unwrap();
let mut sp = SegParams::from_policy(&p.segmentation, p.component("v").unwrap().timebase);
sp.lookahead = lookahead;
let (hd, frames) = y4m::read_all(&fixture::generate(name).unwrap()).unwrap();
let (pieces, _) = segment(sp, hd.w, hd.h, frames.iter().map(|f| &f.rgb));
pieces.iter().map(|x| (x.start, x.flags, classify(&x.stats, &p.classify).label())).collect()
}
fn starts(v: &[(u64, u8, &str)]) -> Vec<u64> {
v.iter().map(|x| x.0).collect()
}
/// cuts-01: hard cuts at 60, 140 and 230, and at every switch between E and F (260,
/// 290, 320), each a return after more than the lookahead. The flash at 90..96 lasts
/// exactly the lookahead: neither its first frame (the return clause) nor the frame
/// that returns to B (the revisit clause) is a cut. The fade from C to D (160..200)
/// is not cut.
#[test]
fn cuts_01_cuts_where_its_shots_change() {
let got = run("cuts-01", 6);
assert_eq!(starts(&got), vec![0, 60, 140, 230, 260, 290, 320]);
assert!(got[1..].iter().all(|x| x.1 == FLAG_SCENE_CUT));
}
/// With a lookahead one frame shorter than the flash, its first frame is a cut: live
/// and on-demand packing agree only at equal lookahead.
#[test]
fn a_shorter_lookahead_cuts_the_flash() {
let got = starts(&run("cuts-01", 5));
assert!(got.contains(&90), "{got:?}");
assert!(!starts(&run("cuts-01", 6)).contains(&90));
}
/// kinds-01: one shot of each kind, then a static shot longer than `max` (100 frames
/// at 25 per second), forced at 260.
#[test]
fn kinds_01_classifies_each_shot() {
let got = run("kinds-01", 6);
let want = [
(0, 0, "static"),
(40, FLAG_SCENE_CUT, "synthetic"),
(80, FLAG_SCENE_CUT, "motion_high"),
(120, FLAG_SCENE_CUT, "natural"),
(160, FLAG_SCENE_CUT, "static"),
(260, FLAG_FORCED, "static"),
];
assert_eq!(got, want);
}