Skip to main content

forge_workspace/store/
dictate_models.rs

1//! What this machine has downloaded from the dictation feed, and which
2//! model each role was last set to run.
3//!
4//! App-level rather than per-project: dictation is one process-wide engine,
5//! so a model is on the machine or it is not.
6
7use redb::{ReadableTable, TableDefinition};
8
9use crate::dictate::DictateRole;
10use crate::install::{ActiveChoice, InstalledModel};
11
12use super::Db;
13
14/// One row per installed file, keyed by file name.
15const INSTALLED: TableDefinition<&str, &[u8]> = TableDefinition::new("dictate_installed");
16
17/// One row per role, keyed by [`role_key`]: the user's last runtime pick.
18const ACTIVE: TableDefinition<&str, &[u8]> = TableDefinition::new("dictate_active");
19
20/// The store's own spelling of a role, stable across releases: the key a
21/// pick is filed under.
22pub fn role_key(role: DictateRole) -> &'static str {
23    match role {
24        DictateRole::Transcribing => "transcribing",
25        DictateRole::Normalization => "normalization",
26        DictateRole::Speaking => "speaking",
27    }
28}
29
30/// Every installed model, oldest first.
31pub fn installed(db: &Db) -> anyhow::Result<Vec<InstalledModel>> {
32    let txn = db.database().begin_read()?;
33    let table = match txn.open_table(INSTALLED) {
34        Ok(table) => table,
35        // A store that never installed anything has no table, which is not
36        // an error: the boot reads cleanly either way.
37        Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
38        Err(error) => return Err(error.into()),
39    };
40    let mut rows = Vec::new();
41    for entry in table.iter()? {
42        let (_, value) = entry?;
43        rows.push(serde_json::from_slice(value.value())?);
44    }
45    rows.sort_by(|a: &InstalledModel, b: &InstalledModel| a.at.cmp(&b.at));
46    Ok(rows)
47}
48
49/// Record one installed model, replacing any row for the same file.
50pub fn record_installed(db: &Db, model: &InstalledModel) -> anyhow::Result<()> {
51    let txn = db.database().begin_write()?;
52    {
53        let mut table = txn.open_table(INSTALLED)?;
54        table.insert(model.file.as_str(), serde_json::to_vec(model)?.as_slice())?;
55    }
56    txn.commit()?;
57    Ok(())
58}
59
60/// Drop one installed record, by the file it was filed under. A file with no
61/// row is not an error: the record the caller wanted gone is already gone.
62pub fn remove_installed(db: &Db, file: &str) -> anyhow::Result<()> {
63    let txn = db.database().begin_write()?;
64    {
65        let mut table = match txn.open_table(INSTALLED) {
66            Ok(table) => table,
67            Err(redb::TableError::TableDoesNotExist(_)) => return Ok(()),
68            Err(error) => return Err(error.into()),
69        };
70        table.remove(file)?;
71    }
72    txn.commit()?;
73    Ok(())
74}
75
76/// The runtime pick recorded for one role, when there is one.
77pub fn active(db: &Db, role: DictateRole) -> anyhow::Result<Option<ActiveChoice>> {
78    let txn = db.database().begin_read()?;
79    let table = match txn.open_table(ACTIVE) {
80        Ok(table) => table,
81        Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
82        Err(error) => return Err(error.into()),
83    };
84    let Some(value) = table.get(role_key(role))? else {
85        return Ok(None);
86    };
87    Ok(Some(serde_json::from_slice(value.value())?))
88}
89
90/// Record the runtime pick for one role, replacing any row for it.
91pub fn record_active(db: &Db, role: DictateRole, choice: &ActiveChoice) -> anyhow::Result<()> {
92    let txn = db.database().begin_write()?;
93    {
94        let mut table = txn.open_table(ACTIVE)?;
95        table.insert(role_key(role), serde_json::to_vec(choice)?.as_slice())?;
96    }
97    txn.commit()?;
98    Ok(())
99}
100
101/// Drop one role's runtime pick, so the role answers its config key or its
102/// compiled pin again.
103pub fn clear_active(db: &Db, role: DictateRole) -> anyhow::Result<()> {
104    let txn = db.database().begin_write()?;
105    {
106        let mut table = txn.open_table(ACTIVE)?;
107        table.remove(role_key(role))?;
108    }
109    txn.commit()?;
110    Ok(())
111}