#[derive(Debug, Clone)]
pub struct PacingMetrics {
pub avg_shot_duration_frames: f64,
pub shot_count: usize,
pub cut_rate_per_minute: f64,
pub fastest_shot: u64,
pub slowest_shot: u64,
}
impl PacingMetrics {
#[must_use]
pub fn pacing_score(&self) -> f64 {
((self.cut_rate_per_minute - 10.0) / 110.0).clamp(0.0, 1.0)
}
#[must_use]
pub fn is_fast_paced(&self) -> bool {
self.pacing_score() > 0.5
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PacingStyle {
Slow,
Moderate,
Fast,
HyperActive,
}
#[must_use]
pub fn classify_pacing(metrics: &PacingMetrics) -> PacingStyle {
let cpm = metrics.cut_rate_per_minute;
if cpm < 20.0 {
PacingStyle::Slow
} else if cpm < 40.0 {
PacingStyle::Moderate
} else if cpm < 80.0 {
PacingStyle::Fast
} else {
PacingStyle::HyperActive
}
}
#[derive(Debug, Clone)]
pub struct ShotList {
durations_frames: Vec<u64>,
fps: f64,
}
impl ShotList {
#[must_use]
pub fn new(fps: f64) -> Self {
Self {
durations_frames: Vec::new(),
fps,
}
}
pub fn add_shot(&mut self, duration: u64) {
self.durations_frames.push(duration);
}
#[must_use]
pub fn compute_metrics(&self) -> PacingMetrics {
let n = self.durations_frames.len();
if n == 0 {
return PacingMetrics {
avg_shot_duration_frames: 0.0,
shot_count: 0,
cut_rate_per_minute: 0.0,
fastest_shot: 0,
slowest_shot: 0,
};
}
let total: u64 = self.durations_frames.iter().sum();
let avg = total as f64 / n as f64;
let fastest = *self.durations_frames.iter().min().unwrap_or(&0);
let slowest = *self.durations_frames.iter().max().unwrap_or(&0);
let total_seconds = total as f64 / self.fps.max(f64::EPSILON);
let cut_rate = if total_seconds > 0.0 {
n as f64 / total_seconds * 60.0
} else {
0.0
};
PacingMetrics {
avg_shot_duration_frames: avg,
shot_count: n,
cut_rate_per_minute: cut_rate,
fastest_shot: fastest,
slowest_shot: slowest,
}
}
#[must_use]
pub fn rhythm_deviation(&self) -> f64 {
let n = self.durations_frames.len();
if n < 2 {
return 0.0;
}
let mean = self.durations_frames.iter().sum::<u64>() as f64 / n as f64;
if mean < f64::EPSILON {
return 0.0;
}
let variance = self
.durations_frames
.iter()
.map(|&d| {
let diff = d as f64 - mean;
diff * diff
})
.sum::<f64>()
/ n as f64;
variance.sqrt() / mean
}
}
#[must_use]
pub fn detect_pacing_changes(
shots: &[u64],
window_size: usize,
fps: f64,
) -> Vec<(usize, PacingStyle)> {
if shots.is_empty() || window_size == 0 {
return Vec::new();
}
let step = window_size.max(1);
let mut results = Vec::new();
let mut i = 0;
while i < shots.len() {
let end = (i + step).min(shots.len());
let window = &shots[i..end];
let mut list = ShotList::new(fps);
for &d in window {
list.add_shot(d);
}
let metrics = list.compute_metrics();
let style = classify_pacing(&metrics);
results.push((i, style));
i += step;
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pacing_metrics_score_slow() {
let m = PacingMetrics {
avg_shot_duration_frames: 120.0,
shot_count: 10,
cut_rate_per_minute: 5.0,
fastest_shot: 100,
slowest_shot: 150,
};
assert!((m.pacing_score() - 0.0).abs() < f64::EPSILON);
assert!(!m.is_fast_paced());
}
#[test]
fn test_pacing_metrics_score_fast() {
let m = PacingMetrics {
avg_shot_duration_frames: 10.0,
shot_count: 100,
cut_rate_per_minute: 120.0,
fastest_shot: 5,
slowest_shot: 20,
};
assert!((m.pacing_score() - 1.0).abs() < f64::EPSILON);
assert!(m.is_fast_paced());
}
#[test]
fn test_pacing_metrics_is_fast_paced_boundary() {
let m = PacingMetrics {
avg_shot_duration_frames: 30.0,
shot_count: 20,
cut_rate_per_minute: 65.0,
fastest_shot: 20,
slowest_shot: 50,
};
assert!(!m.is_fast_paced());
}
#[test]
fn test_classify_pacing_slow() {
let m = PacingMetrics {
avg_shot_duration_frames: 200.0,
shot_count: 5,
cut_rate_per_minute: 10.0,
fastest_shot: 180,
slowest_shot: 220,
};
assert_eq!(classify_pacing(&m), PacingStyle::Slow);
}
#[test]
fn test_classify_pacing_moderate() {
let m = PacingMetrics {
avg_shot_duration_frames: 80.0,
shot_count: 10,
cut_rate_per_minute: 30.0,
fastest_shot: 60,
slowest_shot: 100,
};
assert_eq!(classify_pacing(&m), PacingStyle::Moderate);
}
#[test]
fn test_classify_pacing_fast() {
let m = PacingMetrics {
avg_shot_duration_frames: 30.0,
shot_count: 30,
cut_rate_per_minute: 60.0,
fastest_shot: 20,
slowest_shot: 50,
};
assert_eq!(classify_pacing(&m), PacingStyle::Fast);
}
#[test]
fn test_classify_pacing_hyperactive() {
let m = PacingMetrics {
avg_shot_duration_frames: 10.0,
shot_count: 100,
cut_rate_per_minute: 100.0,
fastest_shot: 5,
slowest_shot: 20,
};
assert_eq!(classify_pacing(&m), PacingStyle::HyperActive);
}
#[test]
fn test_shot_list_empty_metrics() {
let list = ShotList::new(24.0);
let m = list.compute_metrics();
assert_eq!(m.shot_count, 0);
assert!((m.avg_shot_duration_frames - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_shot_list_metrics_basic() {
let mut list = ShotList::new(24.0);
list.add_shot(48); list.add_shot(24); let m = list.compute_metrics();
assert_eq!(m.shot_count, 2);
assert!((m.avg_shot_duration_frames - 36.0).abs() < f64::EPSILON);
assert_eq!(m.fastest_shot, 24);
assert_eq!(m.slowest_shot, 48);
}
#[test]
fn test_shot_list_cut_rate() {
let mut list = ShotList::new(24.0);
for _ in 0..60 {
list.add_shot(24);
}
let m = list.compute_metrics();
assert!((m.cut_rate_per_minute - 60.0).abs() < 0.01);
}
#[test]
fn test_rhythm_deviation_uniform() {
let mut list = ShotList::new(24.0);
for _ in 0..5 {
list.add_shot(48);
}
assert!((list.rhythm_deviation() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_rhythm_deviation_nonzero() {
let mut list = ShotList::new(24.0);
list.add_shot(24);
list.add_shot(96);
assert!(list.rhythm_deviation() > 0.0);
}
#[test]
fn test_detect_pacing_changes_empty() {
let result = detect_pacing_changes(&[], 3, 24.0);
assert!(result.is_empty());
}
#[test]
fn test_detect_pacing_changes_single_window() {
let shots = vec![48u64; 10];
let result = detect_pacing_changes(&shots, 10, 24.0);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, 0);
}
#[test]
fn test_detect_pacing_changes_multiple_windows() {
let shots = vec![48u64; 6];
let result = detect_pacing_changes(&shots, 3, 24.0);
assert_eq!(result.len(), 2);
assert_eq!(result[0].0, 0);
assert_eq!(result[1].0, 3);
}
}