use rand::{seq::SliceRandom, Rng};
use crate::data::captioning::templates::{ACTIVITY_TEMPLATES, MOOD_TEMPLATES};
#[derive(Debug, Clone)]
pub struct ActivityEvent {
pub name: String,
pub start_minute: usize,
pub end_minute: usize,
}
impl ActivityEvent {
pub fn duration(&self) -> usize {
self.end_minute.saturating_sub(self.start_minute) + 1
}
}
#[derive(Debug, Clone)]
pub struct SleepEvent {
pub start_minute: usize,
pub end_minute: usize,
}
#[derive(Debug, Clone)]
pub struct MoodEvent {
pub mood: String,
pub minute: usize,
}
pub fn generate_semantic_caption<R: Rng>(
activities: &[ActivityEvent],
sleep: &[SleepEvent],
moods: &[MoodEvent],
top_k_activity: usize,
top_k_sleep: usize,
min_activity_duration: usize,
rng: &mut R,
) -> String {
let mut parts = Vec::<String>::new();
let mut valid_acts: Vec<&ActivityEvent> = activities
.iter()
.filter(|a| a.duration() >= min_activity_duration)
.collect();
valid_acts.sort_by_key(|a| a.duration());
let selected_acts: Vec<&&ActivityEvent> = valid_acts
.iter()
.rev()
.take(top_k_activity)
.collect();
let mut act_sentences = Vec::new();
for &&act in &selected_acts {
act_sentences.push(describe_activity(&act.name, act.start_minute, act.end_minute, rng));
}
if !act_sentences.is_empty() {
parts.push(act_sentences.join(" "));
}
let mut sleep_sorted: Vec<&SleepEvent> = sleep.iter().collect();
sleep_sorted.sort_by_key(|s| s.end_minute.saturating_sub(s.start_minute));
let mut sleep_sentences = Vec::new();
for &s in sleep_sorted.iter().rev().take(top_k_sleep) {
sleep_sentences.push(describe_activity(
"Sleep",
s.start_minute,
s.end_minute,
rng,
));
}
if !sleep_sentences.is_empty() {
parts.push(sleep_sentences.join(" "));
}
for m in moods {
parts.push(describe_mood(&m.mood, m.minute, rng));
}
parts.join("\n")
}
fn describe_activity<R: Rng>(
activity: &str,
start_minute: usize,
end_minute: usize,
rng: &mut R,
) -> String {
let tmpl = ACTIVITY_TEMPLATES
.choose(rng)
.copied()
.unwrap_or(ACTIVITY_TEMPLATES[0]);
tmpl.replace("{activity}", activity)
.replace("{start_minute}", &start_minute.to_string())
.replace("{end_minute}", &end_minute.to_string())
}
fn describe_mood<R: Rng>(mood: &str, minute: usize, rng: &mut R) -> String {
let tmpl = MOOD_TEMPLATES
.choose(rng)
.copied()
.unwrap_or(MOOD_TEMPLATES[0]);
tmpl.replace("{mood}", mood)
.replace("{time}", &minute.to_string())
}
pub fn parse_minutes(time_str: &str) -> Option<usize> {
let parts: Vec<&str> = time_str.trim_start_matches(|c: char| !c.is_ascii_digit())
.split(':')
.collect();
if parts.len() >= 2 {
let h: usize = parts[0].parse().ok()?;
let m: usize = parts[1].parse().ok()?;
return Some(h * 60 + m);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{rngs::StdRng, SeedableRng};
#[test]
fn test_semantic_caption_basic() {
let mut rng = StdRng::seed_from_u64(1);
let activities = vec![
ActivityEvent { name: "Walking".into(), start_minute: 480, end_minute: 540 },
ActivityEvent { name: "Running".into(), start_minute: 600, end_minute: 660 },
];
let sleep = vec![SleepEvent { start_minute: 0, end_minute: 440 }];
let moods = vec![MoodEvent { mood: "calm".into(), minute: 300 }];
let cap = generate_semantic_caption(&activities, &sleep, &moods, 8, 2, 20, &mut rng);
assert!(cap.contains("Walking") || cap.contains("Running"),
"Caption should mention activities: {cap}");
assert!(cap.contains("Sleep"), "Caption should mention sleep: {cap}");
assert!(cap.contains("calm"), "Caption should mention mood: {cap}");
}
#[test]
fn test_min_duration_filter() {
let mut rng = StdRng::seed_from_u64(2);
let activities = vec![
ActivityEvent { name: "Short".into(), start_minute: 0, end_minute: 10 }, ActivityEvent { name: "Long".into(), start_minute: 0, end_minute: 60 }, ];
let cap = generate_semantic_caption(&activities, &[], &[], 8, 2, 20, &mut rng);
assert!(!cap.contains("Short"), "Short activity should be filtered");
assert!(cap.contains("Long"), "Long activity should appear");
}
#[test]
fn test_parse_minutes() {
assert_eq!(parse_minutes("08:30:00"), Some(510));
assert_eq!(parse_minutes("00:00:00"), Some(0));
}
}