Skip to main content

voice_bird_cli/session/
writer.rs

1use std::fs::{File, OpenOptions};
2use std::io::{BufWriter, Write};
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct WrittenSegment {
9    pub t_start_ms: u64,
10    pub t_end_ms: u64,
11    pub text: String,
12}
13
14pub struct SegmentWriter {
15    file: BufWriter<File>,
16}
17
18impl SegmentWriter {
19    pub fn open(path: &Path) -> anyhow::Result<Self> {
20        if let Some(parent) = path.parent() {
21            std::fs::create_dir_all(parent)?;
22        }
23        let file = OpenOptions::new().create(true).append(true).open(path)?;
24        Ok(Self {
25            file: BufWriter::new(file),
26        })
27    }
28
29    pub fn append(&mut self, seg: &WrittenSegment) -> anyhow::Result<()> {
30        serde_json::to_writer(&mut self.file, seg)?;
31        self.file.write_all(b"\n")?;
32        self.file.flush()?;
33        self.file.get_ref().sync_data()?; // fsync per segment
34        Ok(())
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use std::time::Duration;
42    use tempfile::TempDir;
43
44    fn seg(t0: f64, t1: f64, text: &str) -> WrittenSegment {
45        WrittenSegment {
46            t_start_ms: (t0 * 1000.0) as u64,
47            t_end_ms: (t1 * 1000.0) as u64,
48            text: text.into(),
49        }
50    }
51
52    #[test]
53    fn appends_one_line_per_segment() {
54        let dir = TempDir::new().unwrap();
55        let path = dir.path().join("transcript.jsonl");
56        let mut w = SegmentWriter::open(&path).unwrap();
57        w.append(&seg(0.0, 1.5, "hello")).unwrap();
58        w.append(&seg(1.5, 3.0, "world")).unwrap();
59        drop(w);
60
61        let s = std::fs::read_to_string(&path).unwrap();
62        let lines: Vec<_> = s.lines().collect();
63        assert_eq!(lines.len(), 2);
64        assert!(lines[0].contains("\"text\":\"hello\""));
65        assert!(lines[1].contains("\"text\":\"world\""));
66    }
67
68    #[test]
69    fn survives_reopen_and_appends() {
70        let dir = TempDir::new().unwrap();
71        let path = dir.path().join("transcript.jsonl");
72        {
73            let mut w = SegmentWriter::open(&path).unwrap();
74            w.append(&seg(0.0, 1.0, "first")).unwrap();
75        }
76        {
77            let mut w = SegmentWriter::open(&path).unwrap();
78            w.append(&seg(1.0, 2.0, "second")).unwrap();
79        }
80        let s = std::fs::read_to_string(&path).unwrap();
81        assert_eq!(s.lines().count(), 2);
82    }
83}