Skip to main content

forge_server/surface/
dictate.rs

1//! `dictate()`: models and preflight state.
2
3use std::path::PathBuf;
4
5use forge_primitives::SessionSlot;
6use forge_workspace::DictateSnapshot;
7
8use super::ViewSurface;
9
10/// Dictation's preflight state: what the preflight screen draws.
11pub struct DictateView {
12    /// Whether `[dictate] enabled` is set. **Carried rather than inferred
13    /// from an empty `models` list**: the list is empty for a switched-off
14    /// section AND for a `DictateSnapshot::default()`, so a reader asserting
15    /// the cause from the value reports a healthy configuration as switched
16    /// off - or a switched-off one as healthy - with nothing to say which.
17    pub enabled: bool,
18    /// Per-model fetch and load progress.
19    pub snapshot: DictateSnapshot,
20    /// Where the dictation models land. `None` when the platform has no
21    /// usable cache directory and none was configured.
22    pub models_dir: Option<PathBuf>,
23}
24
25impl ViewSurface {
26    /// The connection that was streaming a take for `key` has gone: drop
27    /// the take it started and free the seat for the next one.
28    ///
29    /// A connection's own lifecycle rather than a user action, which is why
30    /// it is a verb here and not a command: nothing a view dispatches means
31    /// "the client went away".
32    pub fn dictate_close(&self, key: &SessionSlot, initiator: u64) -> bool {
33        self.workspace.dictate_close(key, initiator)
34    }
35
36    /// The axes a client that captures starts on and resets to: the
37    /// `[dictate]` keys over the crate's own defaults.
38    pub fn dictate_axes(&self) -> forge_workspace::DictateAxes {
39        self.workspace.dictate_axes()
40    }
41
42    /// Push one frame of client-captured audio into a seat's live take,
43    /// answering whether the samples were kept. The take must belong to
44    /// `initiator`: a seat's live take can be another connection's, and its
45    /// audio is not the pushing connection's to feed.
46    ///
47    /// A data plane rather than a command: a client sends this ~50 times a
48    /// second for as long as it talks, and the command bus's dispatch is
49    /// for the actions a view takes on the user's behalf, not for the
50    /// audio itself.
51    pub fn dictate_push(&self, key: &SessionSlot, samples: &[f32], initiator: Option<u64>) -> bool {
52        self.workspace.dictate_push(key, samples, initiator)
53    }
54
55    /// [`Self::dictate_push`] for the read-aloud recording, which belongs to
56    /// no seat: the same data plane, kept only where the recording is the
57    /// pushing connection's.
58    pub fn dictate_read_aloud_push(&self, samples: &[f32], initiator: Option<u64>) -> bool {
59        self.workspace.read_aloud_push(samples, initiator)
60    }
61
62    pub fn dictate(&self) -> DictateView {
63        DictateView {
64            enabled: self.workspace.dictate_enabled(),
65            snapshot: self.workspace.dictate_snapshot(),
66            models_dir: self.workspace.dictate_models_dir(),
67        }
68    }
69
70    /// The models page's whole read: the pinned models, the catalogue
71    /// check, what the feed proposes, and every row it carries.
72    ///
73    /// Read when the page subscribes and again from each
74    /// [`forge_workspace::SessionUpdate::DictateModelsChanged`]; the
75    /// check it reports on is started by the core at boot and by
76    /// `Command::DictateCatalogueCheck`.
77    pub fn dictate_models(&self) -> forge_workspace::catalogue::DictateModelsSnapshot {
78        self.workspace.dictate_models()
79    }
80
81    /// The `[dictate] bind` key, as `forge.toml` declares it. A client
82    /// draws the keyboard the record carries rather than hardcoding one.
83    pub fn dictate_bind(&self) -> forge_workspace::DictateBind {
84        self.workspace.dictate_bind()
85    }
86
87    /// How a press maps onto a take, as `forge.toml` declares it.
88    pub fn dictate_mode(&self) -> forge_workspace::DictateMode {
89        self.workspace.dictate_mode()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use std::sync::Arc;
96
97    use super::*;
98
99    /// Catches either field being read from a source other than the call
100    /// it replaces - the models directory comes off the config rather
101    /// than the platform default, so bypassing it leaves the first-run
102    /// note naming the wrong path.
103    #[test]
104    fn dictate_agrees_with_the_calls_it_replaces() {
105        let (workspace, _dir) = crate::surface::testing::workspace();
106
107        let view = ViewSurface::new(Arc::clone(&workspace)).dictate();
108
109        assert_eq!(view.snapshot, workspace.dictate_snapshot(), "the snapshot is the call's own");
110        assert_eq!(view.snapshot.models.len(), 2, "dictation is on, so the snapshot is not empty");
111        assert_eq!(
112            view.models_dir,
113            workspace.dictate_models_dir(),
114            "the models directory is the config's answer",
115        );
116        assert_eq!(
117            view.models_dir,
118            Some(PathBuf::from("/tmp/forge-dictate-models")),
119            "the configured directory is the one reported, not the platform default",
120        );
121    }
122}