oxihuman_export/
srt_export.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone)]
9pub struct SrtEntry {
10 pub index: u32,
12 pub start_ms: u64,
14 pub end_ms: u64,
16 pub text: Vec<String>,
18}
19
20#[derive(Debug, Clone, Default)]
22pub struct SrtDocument {
23 pub entries: Vec<SrtEntry>,
24}
25
26impl SrtDocument {
27 pub fn add_entry(&mut self, start_ms: u64, end_ms: u64, text: impl Into<String>) {
29 let index = self.entries.len() as u32 + 1;
30 self.entries.push(SrtEntry {
31 index,
32 start_ms,
33 end_ms,
34 text: vec![text.into()],
35 });
36 }
37
38 pub fn entry_count(&self) -> usize {
40 self.entries.len()
41 }
42}
43
44pub fn ms_to_srt_time(ms: u64) -> String {
46 let h = ms / 3_600_000;
47 let m = (ms % 3_600_000) / 60_000;
48 let s = (ms % 60_000) / 1_000;
49 let ms = ms % 1_000;
50 format!("{h:02}:{m:02}:{s:02},{ms:03}")
51}
52
53pub fn render_srt(doc: &SrtDocument) -> String {
55 let mut out = String::new();
56 for entry in &doc.entries {
57 out.push_str(&format!("{}\n", entry.index));
58 out.push_str(&format!(
59 "{} --> {}\n",
60 ms_to_srt_time(entry.start_ms),
61 ms_to_srt_time(entry.end_ms)
62 ));
63 for line in &entry.text {
64 out.push_str(line);
65 out.push('\n');
66 }
67 out.push('\n');
68 }
69 out
70}
71
72pub fn validate_srt(doc: &SrtDocument) -> bool {
74 doc.entries
75 .iter()
76 .all(|e| e.start_ms < e.end_ms && !e.text.is_empty())
77}
78
79pub fn total_duration_ms(doc: &SrtDocument) -> u64 {
81 doc.entries.iter().map(|e| e.end_ms).max().unwrap_or(0)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn sample_doc() -> SrtDocument {
89 let mut doc = SrtDocument::default();
90 doc.add_entry(0, 2000, "Hello world");
91 doc.add_entry(3000, 5000, "Second line");
92 doc
93 }
94
95 #[test]
96 fn entry_count_correct() {
97 assert_eq!(sample_doc().entry_count(), 2);
99 }
100
101 #[test]
102 fn indices_start_at_one() {
103 assert_eq!(sample_doc().entries[0].index, 1);
105 }
106
107 #[test]
108 fn ms_to_srt_time_format() {
109 assert_eq!(ms_to_srt_time(3_723_456), "01:02:03,456");
111 }
112
113 #[test]
114 fn ms_to_srt_zero() {
115 assert_eq!(ms_to_srt_time(0), "00:00:00,000");
116 }
117
118 #[test]
119 fn render_contains_arrow() {
120 let s = render_srt(&sample_doc());
122 assert!(s.contains("-->"));
123 }
124
125 #[test]
126 fn render_contains_text() {
127 let s = render_srt(&sample_doc());
128 assert!(s.contains("Hello world"));
129 }
130
131 #[test]
132 fn validate_ok() {
133 assert!(validate_srt(&sample_doc()));
134 }
135
136 #[test]
137 fn validate_bad_timestamps() {
138 let mut doc = SrtDocument::default();
140 doc.entries.push(SrtEntry {
141 index: 1,
142 start_ms: 5000,
143 end_ms: 3000,
144 text: vec!["bad".to_string()],
145 });
146 assert!(!validate_srt(&doc));
147 }
148
149 #[test]
150 fn total_duration_correct() {
151 assert_eq!(total_duration_ms(&sample_doc()), 5000);
152 }
153
154 #[test]
155 fn empty_doc_duration_zero() {
156 assert_eq!(total_duration_ms(&SrtDocument::default()), 0);
157 }
158}