#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundaryType {
HardCut,
Dissolve,
Wipe,
Fade,
}
impl BoundaryType {
#[must_use]
pub fn is_hard_cut(self) -> bool {
self == Self::HardCut
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::HardCut => "hard_cut",
Self::Dissolve => "dissolve",
Self::Wipe => "wipe",
Self::Fade => "fade",
}
}
}
#[derive(Debug, Clone)]
pub struct SceneBoundary {
pub start_frame: u64,
pub end_frame: u64,
pub boundary_type: BoundaryType,
pub confidence: f32,
}
impl SceneBoundary {
#[must_use]
pub fn new(
start_frame: u64,
end_frame: u64,
boundary_type: BoundaryType,
confidence: f32,
) -> Self {
Self {
start_frame,
end_frame,
boundary_type,
confidence: confidence.clamp(0.0, 1.0),
}
}
#[must_use]
pub fn duration_frames(&self) -> u64 {
self.end_frame.saturating_sub(self.start_frame)
}
#[must_use]
pub fn is_confident(&self, threshold: f32) -> bool {
self.confidence >= threshold
}
}
#[derive(Debug, Clone)]
struct FrameEntry {
index: u64,
diff: f32,
}
pub struct BoundaryDetector {
pub cut_threshold: f32,
pub gradual_threshold: f32,
pub gradual_min_frames: usize,
frames: Vec<FrameEntry>,
}
impl BoundaryDetector {
#[must_use]
pub fn new(cut_threshold: f32, gradual_threshold: f32, gradual_min_frames: usize) -> Self {
Self {
cut_threshold,
gradual_threshold,
gradual_min_frames: gradual_min_frames.max(2),
frames: Vec::new(),
}
}
pub fn add_frame(&mut self, frame_index: u64, diff: f32) {
self.frames.push(FrameEntry {
index: frame_index,
diff: diff.clamp(0.0, 1.0),
});
}
#[must_use]
pub fn detect_boundaries(&self) -> Vec<SceneBoundary> {
let mut boundaries = Vec::new();
let n = self.frames.len();
if n == 0 {
return boundaries;
}
let mut i = 0;
while i < n {
let entry = &self.frames[i];
if entry.diff >= self.cut_threshold {
boundaries.push(SceneBoundary::new(
entry.index,
entry.index,
BoundaryType::HardCut,
(entry.diff / self.cut_threshold).min(1.0),
));
i += 1;
continue;
}
if entry.diff >= self.gradual_threshold {
let start = i;
while i < n && self.frames[i].diff >= self.gradual_threshold {
i += 1;
}
let run_len = i - start;
if run_len >= self.gradual_min_frames {
let start_frame = self.frames[start].index;
let end_frame = self.frames[i - 1].index;
let mean_diff = self.frames[start..i]
.iter()
.map(|e| e.diff as f64)
.sum::<f64>()
/ run_len as f64;
boundaries.push(SceneBoundary::new(
start_frame,
end_frame,
BoundaryType::Dissolve,
mean_diff as f32,
));
}
continue;
}
i += 1;
}
boundaries.sort_by_key(|b| b.start_frame);
boundaries
}
pub fn reset(&mut self) {
self.frames.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hard_cut_is_hard_cut() {
assert!(BoundaryType::HardCut.is_hard_cut());
}
#[test]
fn test_dissolve_is_not_hard_cut() {
assert!(!BoundaryType::Dissolve.is_hard_cut());
}
#[test]
fn test_boundary_type_names() {
assert_eq!(BoundaryType::HardCut.name(), "hard_cut");
assert_eq!(BoundaryType::Dissolve.name(), "dissolve");
assert_eq!(BoundaryType::Wipe.name(), "wipe");
assert_eq!(BoundaryType::Fade.name(), "fade");
}
#[test]
fn test_duration_hard_cut_zero() {
let b = SceneBoundary::new(10, 10, BoundaryType::HardCut, 0.9);
assert_eq!(b.duration_frames(), 0);
}
#[test]
fn test_duration_dissolve() {
let b = SceneBoundary::new(20, 35, BoundaryType::Dissolve, 0.7);
assert_eq!(b.duration_frames(), 15);
}
#[test]
fn test_confidence_clamped() {
let b = SceneBoundary::new(0, 0, BoundaryType::HardCut, 1.5);
assert!((b.confidence - 1.0).abs() < f32::EPSILON);
let b2 = SceneBoundary::new(0, 0, BoundaryType::HardCut, -0.5);
assert!((b2.confidence - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_is_confident() {
let b = SceneBoundary::new(0, 0, BoundaryType::HardCut, 0.8);
assert!(b.is_confident(0.7));
assert!(!b.is_confident(0.9));
}
#[test]
fn test_empty_detector() {
let det = BoundaryDetector::new(0.5, 0.2, 3);
assert!(det.detect_boundaries().is_empty());
}
#[test]
fn test_detect_single_hard_cut() {
let mut det = BoundaryDetector::new(0.5, 0.2, 3);
for i in 0..5u64 {
det.add_frame(i, 0.1);
}
det.add_frame(5, 0.9); for i in 6..10u64 {
det.add_frame(i, 0.1);
}
let bounds = det.detect_boundaries();
assert_eq!(bounds.len(), 1);
assert_eq!(bounds[0].boundary_type, BoundaryType::HardCut);
assert_eq!(bounds[0].start_frame, 5);
}
#[test]
fn test_detect_dissolve() {
let mut det = BoundaryDetector::new(0.6, 0.25, 3);
for i in 0..5u64 {
det.add_frame(i, 0.05);
}
for i in 5..9u64 {
det.add_frame(i, 0.4);
}
for i in 9..15u64 {
det.add_frame(i, 0.05);
}
let bounds = det.detect_boundaries();
assert!(!bounds.is_empty(), "expected dissolve boundary");
assert_eq!(bounds[0].boundary_type, BoundaryType::Dissolve);
}
#[test]
fn test_gradual_run_too_short_ignored() {
let mut det = BoundaryDetector::new(0.6, 0.25, 5);
det.add_frame(0, 0.4);
det.add_frame(1, 0.4);
det.add_frame(2, 0.05);
let bounds = det.detect_boundaries();
assert!(bounds.is_empty(), "short run should be ignored");
}
#[test]
fn test_reset_clears_frames() {
let mut det = BoundaryDetector::new(0.5, 0.2, 3);
det.add_frame(0, 0.9);
det.reset();
assert!(det.detect_boundaries().is_empty());
}
#[test]
fn test_multiple_hard_cuts() {
let mut det = BoundaryDetector::new(0.5, 0.2, 3);
det.add_frame(0, 0.05);
det.add_frame(1, 0.8);
det.add_frame(2, 0.05);
det.add_frame(3, 0.9);
det.add_frame(4, 0.05);
let bounds = det.detect_boundaries();
assert_eq!(bounds.len(), 2);
assert_eq!(bounds[0].start_frame, 1);
assert_eq!(bounds[1].start_frame, 3);
}
#[test]
fn test_sorted_output() {
let mut det = BoundaryDetector::new(0.5, 0.2, 3);
det.add_frame(10, 0.8);
det.add_frame(2, 0.8);
let bounds = det.detect_boundaries();
assert_eq!(bounds[0].start_frame, 2);
assert_eq!(bounds[1].start_frame, 10);
}
}