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]>
60 lines
2.4 KiB
Rust
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);
|
|
}
|