use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImplicitSignal {
pub user_id: Uuid,
pub content_id: Uuid,
pub signal_type: SignalType,
pub strength: f32,
pub timestamp: i64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SignalType {
View,
Completion,
RepeatView,
Skip,
FastForward,
Rewind,
Pause,
Share,
Bookmark,
}
impl ImplicitSignal {
#[must_use]
pub fn new(user_id: Uuid, content_id: Uuid, signal_type: SignalType, strength: f32) -> Self {
Self {
user_id,
content_id,
signal_type,
strength: strength.clamp(0.0, 1.0),
timestamp: chrono::Utc::now().timestamp(),
}
}
#[must_use]
pub fn to_rating(&self) -> f32 {
let base = match self.signal_type {
SignalType::View => 2.0,
SignalType::Completion => 4.0,
SignalType::RepeatView => 4.5,
SignalType::Skip => 1.0,
SignalType::FastForward => 1.5,
SignalType::Rewind => 3.0,
SignalType::Pause => 2.5,
SignalType::Share => 5.0,
SignalType::Bookmark => 4.5,
};
(base * self.strength).min(5.0)
}
}
pub struct ImplicitSignalProcessor {
weights: SignalWeights,
}
#[derive(Debug, Clone)]
pub struct SignalWeights {
pub view: f32,
pub completion: f32,
pub repeat_view: f32,
pub skip: f32,
pub share: f32,
}
impl Default for SignalWeights {
fn default() -> Self {
Self {
view: 0.5,
completion: 1.0,
repeat_view: 0.9,
skip: -0.3,
share: 1.2,
}
}
}
impl ImplicitSignalProcessor {
#[must_use]
pub fn new() -> Self {
Self {
weights: SignalWeights::default(),
}
}
#[must_use]
pub fn process_signals(&self, signals: &[ImplicitSignal]) -> f32 {
if signals.is_empty() {
return 0.0;
}
let mut weighted_sum = 0.0;
let mut total_weight = 0.0;
for signal in signals {
let weight = self.get_signal_weight(&signal.signal_type);
weighted_sum += signal.to_rating() * weight;
total_weight += weight.abs();
}
if total_weight > 0.0 {
(weighted_sum / total_weight).clamp(0.0, 5.0)
} else {
0.0
}
}
fn get_signal_weight(&self, signal_type: &SignalType) -> f32 {
match signal_type {
SignalType::View => self.weights.view,
SignalType::Completion => self.weights.completion,
SignalType::RepeatView => self.weights.repeat_view,
SignalType::Skip => self.weights.skip,
SignalType::Share => self.weights.share,
SignalType::FastForward => 0.3,
SignalType::Rewind => 0.4,
SignalType::Pause => 0.2,
SignalType::Bookmark => 0.8,
}
}
}
impl Default for ImplicitSignalProcessor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_implicit_signal_creation() {
let signal = ImplicitSignal::new(Uuid::new_v4(), Uuid::new_v4(), SignalType::View, 0.8);
assert_eq!(signal.strength, 0.8);
}
#[test]
fn test_signal_to_rating() {
let signal =
ImplicitSignal::new(Uuid::new_v4(), Uuid::new_v4(), SignalType::Completion, 1.0);
let rating = signal.to_rating();
assert!((rating - 4.0).abs() < f32::EPSILON);
}
#[test]
fn test_process_signals() {
let processor = ImplicitSignalProcessor::new();
let user_id = Uuid::new_v4();
let content_id = Uuid::new_v4();
let signals = vec![
ImplicitSignal::new(user_id, content_id, SignalType::View, 1.0),
ImplicitSignal::new(user_id, content_id, SignalType::Completion, 1.0),
];
let rating = processor.process_signals(&signals);
assert!(rating > 0.0 && rating <= 5.0);
}
}