Skip to main content

talk_core/
settle.rs

1#[derive(Clone, Debug, PartialEq, Eq)]
2pub struct Block {
3    pub clean: String,
4    /// Raw is retained so `u` (raw⇄clean toggle) and recovery work.
5    pub raw: String,
6}
7
8/// Three-phase model per spec §7, so Plan 2's renderer and Plan 3's async swap
9/// EXTEND this type instead of rewriting it:
10/// - `live`       — the jittering partial hypothesis (not committed)
11/// - `committing` — the most recent phrase, shown bright with deterministic-Light,
12///   still inside the decode-lag / async-swap window (may be upgraded or take a
13///   late STT revision)
14/// - `settled`    — finalized, immutable blocks (never move again)
15#[derive(Default)]
16pub struct Settle {
17    settled: Vec<Block>,
18    committing: Option<Block>,
19    live: String,
20    committing_revised: bool,
21}
22
23impl Settle {
24    pub fn new() -> Self { Self::default() }
25
26    /// A new/revised partial hypothesis for the live edge.
27    pub fn on_partial(&mut self, partial: &str) {
28        self.live = partial.to_string();
29    }
30
31    /// Endpoint boundary: the live edge becomes the committing block (`clean` is
32    /// the deterministic-Light result). Any prior committing block's window has
33    /// closed, so it is finalized into `settled` first.
34    pub fn commit(&mut self, raw: &str, clean: &str) {
35        self.finalize();
36        self.committing = Some(Block { clean: clean.to_string(), raw: raw.to_string() });
37        self.committing_revised = false;
38        self.live.clear();
39    }
40
41    /// Promote the committing block to settled (its lag/swap window elapsed).
42    pub fn finalize(&mut self) {
43        if let Some(b) = self.committing.take() {
44            self.settled.push(b);
45        }
46    }
47
48    /// Plan 3 async LLM swap: replace the committing block's clean text while
49    /// still inside its window. No-op (returns false) once finalized — the
50    /// settled rule wins.
51    pub fn upgrade_committing(&mut self, clean: &str) -> bool {
52        match self.committing.as_mut() {
53            Some(b) => {
54                b.clean = clean.to_string();
55                self.committing_revised = true;
56                true
57            }
58            None => false,
59        }
60    }
61
62    /// Plan 5 second-pass swap: replace BOTH raw and clean of the committing
63    /// block (a better transcription of the same audio, not a reformat). No-op
64    /// once finalized — the settled rule wins, exactly like `upgrade_committing`.
65    pub fn revise_committing(&mut self, raw: &str, clean: &str) -> bool {
66        match self.committing.as_mut() {
67            Some(b) => {
68                b.raw = raw.to_string();
69                b.clean = clean.to_string();
70                self.committing_revised = true;
71                true
72            }
73            None => false,
74        }
75    }
76
77    /// A late STT revision targeting already-settled text is DROPPED (returns
78    /// false so the caller can log it to the raw layer).
79    pub fn try_late_revision_settled(&mut self, _index: usize, _new_text: &str) -> bool {
80        false
81    }
82
83    pub fn settled(&self) -> &[Block] { &self.settled }
84    pub fn committing(&self) -> Option<&Block> { self.committing.as_ref() }
85    pub fn live(&self) -> &str { &self.live }
86
87    /// True once the committing block has been revised/upgraded (its text is
88    /// final-quality). Drives the bright-is-final rendering: dim until revised.
89    pub fn committing_revised(&self) -> bool { self.committing_revised }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn partial_updates_only_the_live_edge() {
98        let mut s = Settle::new();
99        s.on_partial("um so the thing");
100        assert_eq!(s.live(), "um so the thing");
101        assert!(s.settled().is_empty() && s.committing().is_none());
102    }
103
104    #[test]
105    fn commit_moves_live_into_committing_not_settled() {
106        let mut s = Settle::new();
107        s.on_partial("um so the thing is");
108        s.commit("um so the thing is", "The thing is.");
109        assert_eq!(s.committing().unwrap().clean, "The thing is.");
110        assert_eq!(s.live(), "");
111        assert!(s.settled().is_empty());
112    }
113
114    #[test]
115    fn second_commit_finalizes_the_first() {
116        let mut s = Settle::new();
117        s.commit("a", "A.");
118        s.commit("b", "B.");
119        assert_eq!(s.settled().len(), 1);
120        assert_eq!(s.settled()[0].clean, "A.");
121        assert_eq!(s.committing().unwrap().clean, "B.");
122    }
123
124    #[test]
125    fn upgrade_changes_only_the_committing_block() {
126        let mut s = Settle::new();
127        s.commit("a", "A.");
128        s.commit("b raw", "B.");
129        let settled0 = s.settled()[0].clone();
130        assert!(s.upgrade_committing("B, refined."));
131        assert_eq!(s.settled()[0], settled0); // settled is immutable
132        assert_eq!(s.committing().unwrap().clean, "B, refined.");
133    }
134
135    #[test]
136    fn upgrade_is_noop_after_finalize() {
137        let mut s = Settle::new();
138        s.commit("a", "A.");
139        s.finalize();
140        assert!(!s.upgrade_committing("A!"));
141        assert_eq!(s.settled()[0].clean, "A.");
142    }
143
144    #[test]
145    fn revise_replaces_both_raw_and_clean_of_the_committing_block() {
146        let mut s = Settle::new();
147        s.commit("a", "A.");
148        s.commit("live hypothesis", "Live hypothesis.");
149        assert!(s.revise_committing("better raw", "Better raw."));
150        assert_eq!(s.settled()[0].clean, "A."); // settled untouched
151        let c = s.committing().unwrap();
152        assert_eq!(c.raw, "better raw");
153        assert_eq!(c.clean, "Better raw.");
154    }
155
156    #[test]
157    fn revise_is_noop_after_finalize() {
158        let mut s = Settle::new();
159        s.commit("a", "A.");
160        s.finalize();
161        assert!(!s.revise_committing("x", "X."));
162        assert_eq!(s.settled()[0].raw, "a");
163    }
164
165    #[test]
166    fn committing_revised_flag_tracks_commit_then_revise_then_recommit() {
167        let mut s = Settle::new();
168        s.commit("a", "A.");
169        assert!(!s.committing_revised());
170        assert!(s.revise_committing("a2", "A2."));
171        assert!(s.committing_revised());
172        s.commit("b", "B.");
173        assert!(!s.committing_revised());
174    }
175
176    #[test]
177    fn upgrade_committing_sets_the_revised_flag() {
178        let mut s = Settle::new();
179        s.commit("a", "A.");
180        assert!(!s.committing_revised());
181        assert!(s.upgrade_committing("A, refined."));
182        assert!(s.committing_revised());
183    }
184
185    #[test]
186    fn late_revision_never_mutates_settled() {
187        let mut s = Settle::new();
188        s.commit("first", "First.");
189        s.finalize();
190        let before = s.settled().to_vec();
191        assert!(!s.try_late_revision_settled(0, "FIRST!!"));
192        assert_eq!(s.settled(), &before[..]);
193    }
194}