#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScoreCategory {
VisualQuality,
Aesthetic,
Motion,
FacePresence,
Relevance,
AudioClarity,
Engagement,
}
impl ScoreCategory {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::VisualQuality => "visual_quality",
Self::Aesthetic => "aesthetic",
Self::Motion => "motion",
Self::FacePresence => "face_presence",
Self::Relevance => "relevance",
Self::AudioClarity => "audio_clarity",
Self::Engagement => "engagement",
}
}
#[must_use]
pub fn is_video_category(self) -> bool {
!matches!(self, Self::AudioClarity)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CategoryScore {
pub category: ScoreCategory,
pub score: f32,
pub weight: f32,
}
impl CategoryScore {
#[must_use]
pub fn new(category: ScoreCategory, score: f32, weight: f32) -> Self {
Self {
category,
score: score.clamp(0.0, 1.0),
weight: weight.max(0.0),
}
}
#[must_use]
pub fn unit(category: ScoreCategory, score: f32) -> Self {
Self::new(category, score, 1.0)
}
#[must_use]
pub fn passes(&self, threshold: f32) -> bool {
self.score >= threshold
}
#[must_use]
pub fn weighted_value(self) -> f32 {
self.score * self.weight
}
}
#[derive(Debug, Clone, Default)]
pub struct SceneScore {
scores: Vec<CategoryScore>,
}
impl SceneScore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, score: CategoryScore) {
self.scores.push(score);
}
#[must_use]
pub fn len(&self) -> usize {
self.scores.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.scores.is_empty()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn aggregate(&self) -> f32 {
let total_weight: f32 = self.scores.iter().map(|s| s.weight).sum();
if total_weight == 0.0 {
return 0.0;
}
let weighted_sum: f32 = self.scores.iter().map(|s| s.weighted_value()).sum();
weighted_sum / total_weight
}
#[must_use]
pub fn get(&self, category: ScoreCategory) -> Option<&CategoryScore> {
self.scores.iter().find(|s| s.category == category)
}
#[must_use]
pub fn max_score(&self) -> Option<f32> {
self.scores.iter().map(|s| s.score).reduce(f32::max)
}
#[must_use]
pub fn min_score(&self) -> Option<f32> {
self.scores.iter().map(|s| s.score).reduce(f32::min)
}
#[must_use]
pub fn all_pass(&self, threshold: f32) -> bool {
self.scores.iter().all(|s| s.passes(threshold))
}
#[must_use]
pub fn any_pass(&self, threshold: f32) -> bool {
self.scores.iter().any(|s| s.passes(threshold))
}
#[must_use]
pub fn pass_count(&self, threshold: f32) -> usize {
self.scores.iter().filter(|s| s.passes(threshold)).count()
}
pub fn into_scores(self) -> Vec<CategoryScore> {
self.scores
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_labels_non_empty() {
for cat in [
ScoreCategory::VisualQuality,
ScoreCategory::Aesthetic,
ScoreCategory::Motion,
ScoreCategory::FacePresence,
ScoreCategory::Relevance,
ScoreCategory::AudioClarity,
ScoreCategory::Engagement,
] {
assert!(!cat.label().is_empty(), "label empty for {cat:?}");
}
}
#[test]
fn test_is_video_category() {
assert!(ScoreCategory::VisualQuality.is_video_category());
assert!(ScoreCategory::Aesthetic.is_video_category());
assert!(!ScoreCategory::AudioClarity.is_video_category());
}
#[test]
fn test_score_clamped_high() {
let cs = CategoryScore::new(ScoreCategory::Aesthetic, 1.5, 1.0);
assert!((cs.score - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_score_clamped_low() {
let cs = CategoryScore::new(ScoreCategory::Motion, -0.5, 1.0);
assert!((cs.score - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_weight_clamped_negative() {
let cs = CategoryScore::new(ScoreCategory::Relevance, 0.5, -1.0);
assert!(cs.weight >= 0.0);
}
#[test]
fn test_unit_weight_one() {
let cs = CategoryScore::unit(ScoreCategory::Engagement, 0.7);
assert!((cs.weight - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_passes() {
let cs = CategoryScore::unit(ScoreCategory::VisualQuality, 0.8);
assert!(cs.passes(0.5));
assert!(!cs.passes(0.9));
}
#[test]
fn test_weighted_value() {
let cs = CategoryScore::new(ScoreCategory::Aesthetic, 0.5, 2.0);
assert!((cs.weighted_value() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_empty_aggregate_is_zero() {
let ss = SceneScore::new();
assert!((ss.aggregate() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_single_score_aggregate() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::VisualQuality, 0.6));
assert!((ss.aggregate() - 0.6).abs() < 1e-5);
}
#[test]
fn test_uniform_weight_average() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::VisualQuality, 0.4));
ss.add(CategoryScore::unit(ScoreCategory::Aesthetic, 0.6));
assert!((ss.aggregate() - 0.5).abs() < 1e-5);
}
#[test]
fn test_weighted_average() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::new(ScoreCategory::VisualQuality, 1.0, 1.0));
ss.add(CategoryScore::new(ScoreCategory::Aesthetic, 0.0, 3.0));
assert!((ss.aggregate() - 0.25).abs() < 1e-5);
}
#[test]
fn test_get_category_found() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::Motion, 0.7));
let cs = ss
.get(ScoreCategory::Motion)
.expect("should succeed in test");
assert!((cs.score - 0.7).abs() < f32::EPSILON);
}
#[test]
fn test_get_category_not_found() {
let ss = SceneScore::new();
assert!(ss.get(ScoreCategory::FacePresence).is_none());
}
#[test]
fn test_max_min_scores() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::VisualQuality, 0.9));
ss.add(CategoryScore::unit(ScoreCategory::Motion, 0.2));
assert!((ss.max_score().expect("should succeed in test") - 0.9).abs() < f32::EPSILON);
assert!((ss.min_score().expect("should succeed in test") - 0.2).abs() < f32::EPSILON);
}
#[test]
fn test_all_pass() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::VisualQuality, 0.8));
ss.add(CategoryScore::unit(ScoreCategory::Aesthetic, 0.9));
assert!(ss.all_pass(0.7));
assert!(!ss.all_pass(0.85));
}
#[test]
fn test_any_pass() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::VisualQuality, 0.3));
ss.add(CategoryScore::unit(ScoreCategory::Aesthetic, 0.9));
assert!(ss.any_pass(0.8));
assert!(!ss.any_pass(0.95));
}
#[test]
fn test_pass_count() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::VisualQuality, 0.9));
ss.add(CategoryScore::unit(ScoreCategory::Motion, 0.4));
ss.add(CategoryScore::unit(ScoreCategory::Engagement, 0.8));
assert_eq!(ss.pass_count(0.7), 2);
}
#[test]
fn test_len_and_is_empty() {
let mut ss = SceneScore::new();
assert!(ss.is_empty());
ss.add(CategoryScore::unit(ScoreCategory::Relevance, 0.5));
assert_eq!(ss.len(), 1);
assert!(!ss.is_empty());
}
#[test]
fn test_into_scores() {
let mut ss = SceneScore::new();
ss.add(CategoryScore::unit(ScoreCategory::Aesthetic, 0.5));
ss.add(CategoryScore::unit(ScoreCategory::Engagement, 0.6));
let scores = ss.into_scores();
assert_eq!(scores.len(), 2);
}
}