Skip to main content

scenecut_probe/
scenecut_probe.rs

1// Dev/test target, not shipped: a panic here IS the diagnostic (that is what an
2// assertion is). The workspace's unwrap/expect/panic denials exist to keep them
3// off the decoder's untrusted-input path, so they are relaxed for this file.
4#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
5// Dev tools also accumulate fields and helpers kept for the NEXT investigation;
6// dead code here is a scratchpad, not a defect.
7#![allow(dead_code, unused)]
8#![allow(clippy::unnecessary_unwrap, clippy::zombie_processes)]
9//! Scene-cut calibration probe (keyint campaign): prints each clip's MAX
10//! frame-pair inter/intra ratio and the count of pairs at or above the cut
11//! threshold — the false-positive evidence for the corpus, and the fire
12//! evidence for spliced content. Uses the SHIPPING detector via the encoder's
13//! own segmentation entry points (same arithmetic, no probe fork).
14//!
15//!   cargo run --release -p rusty_h264-encoder --example scenecut_probe -- <clip.y4m> [frames]
16
17use rusty_h264_encoder::EncoderConfig;
18
19fn read_y4m(path: &str, max_frames: usize) -> (usize, usize, Vec<rusty_h264_common::types::YuvFrame>) {
20    let raw = std::fs::read(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
21    let hdr_end = raw.iter().position(|&b| b == b'\n').expect("y4m header");
22    let hdr = std::str::from_utf8(&raw[..hdr_end]).expect("utf8 header");
23    let (mut w, mut h) = (0usize, 0usize);
24    for tok in hdr.split_whitespace() {
25        match tok.as_bytes().first() {
26            Some(b'W') => w = tok[1..].parse().expect("width"),
27            Some(b'H') => h = tok[1..].parse().expect("height"),
28            _ => {}
29        }
30    }
31    let (ys, cs) = (w * h, (w / 2) * (h / 2));
32    let mut frames = Vec::new();
33    let mut p = hdr_end + 1;
34    while frames.len() < max_frames {
35        let Some(rel) = raw[p..].iter().position(|&b| b == b'\n') else { break };
36        p += rel + 1;
37        if p + ys + 2 * cs > raw.len() {
38            break;
39        }
40        frames.push(rusty_h264_common::types::YuvFrame {
41            width: w,
42            height: h,
43            y: raw[p..p + ys].to_vec(),
44            u: raw[p + ys..p + ys + cs].to_vec(),
45            v: raw[p + ys + cs..p + ys + 2 * cs].to_vec(),
46        });
47        p += ys + 2 * cs;
48    }
49    (w, h, frames)
50}
51
52fn main() {
53    let mut args = std::env::args().skip(1);
54    let path = args.next().expect("clip");
55    let nframes: usize = args.next().and_then(|s| s.parse().ok()).unwrap_or(60);
56    let (w, h, frames) = read_y4m(&path, nframes);
57    let name = std::path::Path::new(&path).file_stem().unwrap().to_string_lossy().to_string();
58    let cfg = EncoderConfig::new(w, h); // defaults: scenecut 40 → threshold 0.60
59    let ratios = rusty_h264_encoder::scene_cut_ratios(&cfg, &frames);
60    let max = ratios.iter().cloned().fold(0.0f64, f64::max);
61    let thresh = 1.0 - cfg.scenecut as f64 / 100.0;
62    let flat = ratios.iter().filter(|&&r| r >= thresh).count();
63    // v2 spike rule: high AND a jump over the recent baseline (min of the two
64    // previous pair ratios) — a cut is a discontinuity, chaos is a plateau.
65    let mut spike = 0usize;
66    for i in 0..ratios.len() {
67        let base = match i {
68            0 => 1.0,
69            1 => ratios[0],
70            _ => ratios[i - 1].min(ratios[i - 2]),
71        };
72        if ratios[i] >= thresh && ratios[i] >= base + 0.25 {
73            spike += 1;
74        }
75    }
76    println!(
77        "{name:<24} pairs={} max_ratio={max:.3} flat@{thresh:.2}={flat} spike={spike}",
78        ratios.len()
79    );
80}