Skip to main content

forge_workspace/store/
speaking_results.rs

1//! What this machine's speaking benches measured, keyed by what they ran.
2//!
3//! App-level, like the words bench's results: the corpus is the machine's,
4//! and a result for a different corpus is not a comparison. **The key is
5//! `(model file, corpus sha)`** - overwriting the row for the same corpus is
6//! what keeps a re-run from stacking a second copy of the same number.
7
8use redb::{ReadableTable, TableDefinition};
9
10use crate::bench::SpeakingBenchResult;
11
12use super::Db;
13
14/// One row per (model file, corpus), keyed by the two of them.
15const RESULTS: TableDefinition<&str, &[u8]> = TableDefinition::new("speaking_bench_results");
16
17/// The key one result is filed under.
18pub fn key(result: &SpeakingBenchResult) -> String {
19    format!("{}|{}", result.file, result.corpus.sha256)
20}
21
22/// Every saved result, newest first.
23pub fn results(db: &Db) -> anyhow::Result<Vec<SpeakingBenchResult>> {
24    let txn = db.database().begin_read()?;
25    let table = match txn.open_table(RESULTS) {
26        Ok(table) => table,
27        Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
28        Err(error) => return Err(error.into()),
29    };
30    let mut rows = Vec::new();
31    for entry in table.iter()? {
32        let (_, value) = entry?;
33        rows.push(serde_json::from_slice(value.value())?);
34    }
35    rows.sort_by(|a: &SpeakingBenchResult, b: &SpeakingBenchResult| b.at.cmp(&a.at));
36    Ok(rows)
37}
38
39/// Save one result, replacing any row for the same (model file, corpus).
40pub fn save(db: &Db, result: &SpeakingBenchResult) -> anyhow::Result<()> {
41    let txn = db.database().begin_write()?;
42    {
43        let mut table = txn.open_table(RESULTS)?;
44        table.insert(key(result).as_str(), serde_json::to_vec(result)?.as_slice())?;
45    }
46    txn.commit()?;
47    Ok(())
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use forge_dictate::bench::CorpusId;
54    use forge_tts::bench::TtsBenchResult;
55
56    fn result(file: &str, sha: &str, at: &str) -> SpeakingBenchResult {
57        SpeakingBenchResult {
58            file: file.to_owned(),
59            metrics: TtsBenchResult {
60                clips: 3,
61                round_trip_wer: 0.125,
62                first_audio_ms: 3_400,
63                realtime_factor: 0.3,
64                peak_rss_bytes: 417 << 20,
65            },
66            at: at.to_owned(),
67            corpus: CorpusId { clips: 3, audio_seconds: 9, sha256: sha.to_owned() },
68        }
69    }
70
71    fn db(dir: &std::path::Path) -> Db {
72        Db::open(&dir.join("speaking.redb")).expect("a store")
73    }
74
75    #[test]
76    fn a_result_round_trips_and_a_rerun_over_the_same_corpus_replaces_its_row() {
77        let dir = tempfile::tempdir().unwrap();
78        let store = db(dir.path());
79
80        save(&store, &result("qwen.gguf", "aaaa", "2026-11-01T10:00:00Z")).unwrap();
81        save(&store, &result("qwen.gguf", "bbbb", "2026-11-01T11:00:00Z")).unwrap();
82        let rows = results(&store).unwrap();
83        assert_eq!(rows.len(), 2, "two corpora are two results");
84        assert_eq!(rows[0].at, "2026-11-01T11:00:00Z", "newest first");
85        assert_eq!(rows[0].metrics.round_trip_wer, 0.125, "the figures cross whole");
86
87        save(&store, &result("qwen.gguf", "aaaa", "2026-11-01T12:00:00Z")).unwrap();
88        let rows = results(&store).unwrap();
89        assert_eq!(rows.len(), 2, "a re-run replaces its own row");
90        assert_eq!(rows.iter().filter(|row| row.corpus.sha256 == "aaaa").count(), 1);
91    }
92}