1use serde::Deserialize;
2use std::collections::HashMap;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use std::path::Path;
6
7#[derive(Debug, Clone, Deserialize)]
9pub struct SceneRecord {
10 pub scene: String,
11 pub metrics: HashMap<String, f64>,
12 pub timestamp: f64,
13}
14
15pub fn read_scenes(path: &Path) -> Vec<SceneRecord> {
17 let file = match File::open(path) {
18 Ok(f) => f,
19 Err(_) => return Vec::new(),
20 };
21
22 let reader = BufReader::new(file);
23 let mut records = Vec::new();
24
25 for line in reader.lines() {
26 let line = match line {
27 Ok(l) => l,
28 Err(_) => continue,
29 };
30
31 let trimmed = line.trim();
32 if trimmed.is_empty() {
33 continue;
34 }
35
36 match serde_json::from_str::<SceneRecord>(trimmed) {
37 Ok(record) => records.push(record),
38 Err(e) => {
39 eprintln!("[nuviz] Warning: skipping malformed scene line: {e}");
40 }
41 }
42 }
43
44 records
45}
46
47pub fn scenes_by_name(records: &[SceneRecord]) -> HashMap<String, &SceneRecord> {
49 let mut map: HashMap<String, &SceneRecord> = HashMap::new();
50 for record in records {
51 map.entry(record.scene.clone())
52 .and_modify(|existing| {
53 if record.timestamp > existing.timestamp {
54 *existing = record;
55 }
56 })
57 .or_insert(record);
58 }
59 map
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use std::io::Write;
66 use tempfile::NamedTempFile;
67
68 fn write_jsonl(lines: &[&str]) -> NamedTempFile {
69 let mut f = NamedTempFile::new().unwrap();
70 for line in lines {
71 writeln!(f, "{line}").unwrap();
72 }
73 f
74 }
75
76 #[test]
77 fn test_read_valid_scenes() {
78 let f = write_jsonl(&[
79 r#"{"scene":"garden","metrics":{"psnr":27.41,"ssim":0.945},"timestamp":1.0}"#,
80 r#"{"scene":"bicycle","metrics":{"psnr":25.12,"ssim":0.912},"timestamp":2.0}"#,
81 ]);
82 let records = read_scenes(f.path());
83 assert_eq!(records.len(), 2);
84 assert_eq!(records[0].scene, "garden");
85 assert!((records[0].metrics["psnr"] - 27.41).abs() < f64::EPSILON);
86 }
87
88 #[test]
89 fn test_skip_malformed() {
90 let f = write_jsonl(&[
91 r#"{"scene":"garden","metrics":{"psnr":27.0},"timestamp":1.0}"#,
92 "invalid json",
93 r#"{"scene":"stump","metrics":{"psnr":26.0},"timestamp":3.0}"#,
94 ]);
95 let records = read_scenes(f.path());
96 assert_eq!(records.len(), 2);
97 }
98
99 #[test]
100 fn test_missing_file() {
101 let records = read_scenes(Path::new("/nonexistent/scenes.jsonl"));
102 assert!(records.is_empty());
103 }
104
105 #[test]
106 fn test_scenes_by_name() {
107 let records = vec![
108 SceneRecord {
109 scene: "garden".into(),
110 metrics: HashMap::from([("psnr".into(), 25.0)]),
111 timestamp: 1.0,
112 },
113 SceneRecord {
114 scene: "garden".into(),
115 metrics: HashMap::from([("psnr".into(), 27.0)]),
116 timestamp: 2.0,
117 },
118 SceneRecord {
119 scene: "bicycle".into(),
120 metrics: HashMap::from([("psnr".into(), 24.0)]),
121 timestamp: 1.0,
122 },
123 ];
124 let by_name = scenes_by_name(&records);
125 assert_eq!(by_name.len(), 2);
126 assert!((by_name["garden"].metrics["psnr"] - 27.0).abs() < f64::EPSILON);
128 }
129}