1use std::fmt::Write as _;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::Result;
11
12pub const VERSION: u32 = 1;
14
15#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
16pub struct Index {
17 pub version: u32,
18 pub root: PathBuf,
20 pub clips: Vec<Clip>,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
25pub struct Span {
26 pub start: f64,
27 pub end: f64,
28}
29
30impl Span {
31 pub fn len(self) -> f64 {
32 self.end - self.start
33 }
34 pub fn contains(self, t: f64) -> bool {
35 self.start <= t && t < self.end
36 }
37}
38
39#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
40pub struct Clip {
41 pub id: String,
43 pub source: PathBuf,
45 pub analysed: PathBuf,
47 pub size: u64,
49 pub mtime: i64,
50 pub duration: f64,
52 pub fps: f64,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub width: Option<u32>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub height: Option<u32>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub recorded: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub utc: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub take: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub strip: Option<String>,
71 #[serde(default)]
73 pub strip_times: Vec<f64>,
74 #[serde(default)]
76 pub cuts: Vec<f64>,
77 #[serde(default)]
79 pub activity: Vec<f32>,
80 #[serde(default)]
81 pub luma: Vec<f32>,
82 #[serde(default)]
84 pub has_audio: bool,
85 #[serde(default)]
87 pub speech: Vec<Span>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub transcript: Option<Transcript>,
90 #[serde(default)]
91 pub flags: Vec<Flag>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub location: Option<Location>,
94}
95
96impl Clip {
97 pub fn speech_seconds(&self) -> f64 {
98 self.speech.iter().map(|s| s.len()).sum()
99 }
100}
101
102#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
103pub struct Transcript {
104 pub language: String,
106 pub model: String,
108 pub segments: Vec<Segment>,
109}
110
111impl Transcript {
112 pub fn words(&self) -> impl Iterator<Item = &Word> {
113 self.segments.iter().flat_map(|s| &s.words)
114 }
115 pub fn text(&self) -> String {
116 let mut s = String::new();
117 for seg in &self.segments {
118 if !s.is_empty() {
119 s.push(' ');
120 }
121 s.push_str(seg.text.trim());
122 }
123 s
124 }
125}
126
127#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
128pub struct Segment {
129 pub start: f64,
130 pub end: f64,
131 pub text: String,
132 #[serde(default)]
133 pub words: Vec<Word>,
134}
135
136#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
137pub struct Word {
138 pub start: f64,
139 pub end: f64,
140 pub text: String,
142 #[serde(default)]
143 pub prob: f32,
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct Flag {
148 pub kind: FlagKind,
149 pub detail: String,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub span: Option<Span>,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub enum FlagKind {
158 Runaway,
160 Dead,
162 Dark,
164 Blip,
166 NoAudio,
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct Location {
171 pub lat: f64,
172 pub lon: f64,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub name: Option<String>,
176 pub source: String,
178}
179
180impl Index {
181 pub fn load(path: &Path) -> Result<Self> {
182 let s = std::fs::read_to_string(path)?;
183 Ok(serde_json::from_str(&s)?)
184 }
185
186 pub fn save(&self, path: &Path) -> Result<()> {
187 write_json(path, self)
188 }
189
190 pub fn clip(&self, id: &str) -> Option<&Clip> {
191 self.clips.iter().find(|c| c.id == id)
192 }
193}
194
195pub(crate) fn write_json<T: Serialize>(path: &Path, v: &T) -> Result<()> {
197 let tmp = path.with_extension("json.tmp");
198 std::fs::write(&tmp, serde_json::to_vec(v)?)?;
199 std::fs::rename(&tmp, path)?;
200 Ok(())
201}
202
203pub fn timecode(t: f64) -> String {
205 let t = t.max(0.0);
206 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
207 let whole = t as u64;
208 let frac = t - whole as f64;
209 let (h, m, s) = (whole / 3600, whole / 60 % 60, whole % 60);
210 let mut out = String::new();
211 if h > 0 {
212 let _ = write!(out, "{h}:{m:02}:");
213 } else {
214 let _ = write!(out, "{m}:");
215 }
216 let _ = write!(out, "{:05.2}", s as f64 + frac);
217 out
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn timecodes() {
226 assert_eq!(timecode(0.0), "0:00.00");
227 assert_eq!(timecode(61.5), "1:01.50");
228 assert_eq!(timecode(16_200.25), "4:30:00.25");
229 }
230
231 #[test]
232 fn unknown_fields_are_ignored() {
233 let j = r#"{"version":1,"root":"/x","clips":[{"id":"a","source":"a.mp4","analysed":"a.lrf",
234 "size":1,"mtime":2,"duration":3.0,"fps":24.0,"future":true}]}"#;
235 let ix: Index = serde_json::from_str(j).expect("parse");
236 assert_eq!(ix.clips[0].id, "a");
237 assert_eq!(ix.clips[0].speech, []);
238 }
239}