sensorlm/data/captioning/
semantic.rs1use rand::{seq::SliceRandom, Rng};
21
22use crate::data::captioning::templates::{ACTIVITY_TEMPLATES, MOOD_TEMPLATES};
23
24#[derive(Debug, Clone)]
30pub struct ActivityEvent {
31 pub name: String,
33 pub start_minute: usize,
35 pub end_minute: usize,
37}
38
39impl ActivityEvent {
40 pub fn duration(&self) -> usize {
42 self.end_minute.saturating_sub(self.start_minute) + 1
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct SleepEvent {
49 pub start_minute: usize,
51 pub end_minute: usize,
53}
54
55#[derive(Debug, Clone)]
57pub struct MoodEvent {
58 pub mood: String,
60 pub minute: usize,
62}
63
64pub fn generate_semantic_caption<R: Rng>(
86 activities: &[ActivityEvent],
87 sleep: &[SleepEvent],
88 moods: &[MoodEvent],
89 top_k_activity: usize,
90 top_k_sleep: usize,
91 min_activity_duration: usize,
92 rng: &mut R,
93) -> String {
94 let mut parts = Vec::<String>::new();
95
96 let mut valid_acts: Vec<&ActivityEvent> = activities
100 .iter()
101 .filter(|a| a.duration() >= min_activity_duration)
102 .collect();
103
104 valid_acts.sort_by_key(|a| a.duration());
106
107 let selected_acts: Vec<&&ActivityEvent> = valid_acts
109 .iter()
110 .rev()
111 .take(top_k_activity)
112 .collect();
113
114 let mut act_sentences = Vec::new();
115 for &&act in &selected_acts {
116 act_sentences.push(describe_activity(&act.name, act.start_minute, act.end_minute, rng));
117 }
118 if !act_sentences.is_empty() {
119 parts.push(act_sentences.join(" "));
120 }
121
122 let mut sleep_sorted: Vec<&SleepEvent> = sleep.iter().collect();
126 sleep_sorted.sort_by_key(|s| s.end_minute.saturating_sub(s.start_minute));
127
128 let mut sleep_sentences = Vec::new();
129 for &s in sleep_sorted.iter().rev().take(top_k_sleep) {
130 sleep_sentences.push(describe_activity(
131 "Sleep",
132 s.start_minute,
133 s.end_minute,
134 rng,
135 ));
136 }
137 if !sleep_sentences.is_empty() {
138 parts.push(sleep_sentences.join(" "));
139 }
140
141 for m in moods {
145 parts.push(describe_mood(&m.mood, m.minute, rng));
146 }
147
148 parts.join("\n")
149}
150
151fn describe_activity<R: Rng>(
156 activity: &str,
157 start_minute: usize,
158 end_minute: usize,
159 rng: &mut R,
160) -> String {
161 let tmpl = ACTIVITY_TEMPLATES
162 .choose(rng)
163 .copied()
164 .unwrap_or(ACTIVITY_TEMPLATES[0]);
165 tmpl.replace("{activity}", activity)
166 .replace("{start_minute}", &start_minute.to_string())
167 .replace("{end_minute}", &end_minute.to_string())
168}
169
170fn describe_mood<R: Rng>(mood: &str, minute: usize, rng: &mut R) -> String {
171 let tmpl = MOOD_TEMPLATES
172 .choose(rng)
173 .copied()
174 .unwrap_or(MOOD_TEMPLATES[0]);
175 tmpl.replace("{mood}", mood)
176 .replace("{time}", &minute.to_string())
177}
178
179pub fn parse_minutes(time_str: &str) -> Option<usize> {
183 let parts: Vec<&str> = time_str.trim_start_matches(|c: char| !c.is_ascii_digit())
185 .split(':')
186 .collect();
187 if parts.len() >= 2 {
188 let h: usize = parts[0].parse().ok()?;
189 let m: usize = parts[1].parse().ok()?;
190 return Some(h * 60 + m);
191 }
192 None
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use rand::{rngs::StdRng, SeedableRng};
199
200 #[test]
201 fn test_semantic_caption_basic() {
202 let mut rng = StdRng::seed_from_u64(1);
203 let activities = vec![
204 ActivityEvent { name: "Walking".into(), start_minute: 480, end_minute: 540 },
205 ActivityEvent { name: "Running".into(), start_minute: 600, end_minute: 660 },
206 ];
207 let sleep = vec![SleepEvent { start_minute: 0, end_minute: 440 }];
208 let moods = vec![MoodEvent { mood: "calm".into(), minute: 300 }];
209 let cap = generate_semantic_caption(&activities, &sleep, &moods, 8, 2, 20, &mut rng);
210 assert!(cap.contains("Walking") || cap.contains("Running"),
211 "Caption should mention activities: {cap}");
212 assert!(cap.contains("Sleep"), "Caption should mention sleep: {cap}");
213 assert!(cap.contains("calm"), "Caption should mention mood: {cap}");
214 }
215
216 #[test]
217 fn test_min_duration_filter() {
218 let mut rng = StdRng::seed_from_u64(2);
219 let activities = vec![
220 ActivityEvent { name: "Short".into(), start_minute: 0, end_minute: 10 }, ActivityEvent { name: "Long".into(), start_minute: 0, end_minute: 60 }, ];
223 let cap = generate_semantic_caption(&activities, &[], &[], 8, 2, 20, &mut rng);
224 assert!(!cap.contains("Short"), "Short activity should be filtered");
225 assert!(cap.contains("Long"), "Long activity should appear");
226 }
227
228 #[test]
229 fn test_parse_minutes() {
230 assert_eq!(parse_minutes("08:30:00"), Some(510));
231 assert_eq!(parse_minutes("00:00:00"), Some(0));
232 }
233}