super-stt-daemon/src/daemon/settings_handlers.rs
Line | Count | Source |
1 | | // SPDX-License-Identifier: GPL-3.0-only |
2 | | |
3 | | use crate::config::DaemonConfig; |
4 | | use crate::daemon::types::SuperSTTDaemon; |
5 | | use log::{info, warn}; |
6 | | use super_stt_shared::models::protocol::DaemonResponse; |
7 | | use super_stt_shared::models::recording_stop_mode::RecordingStopMode; |
8 | | use super_stt_shared::models::write_method::WriteMethod; |
9 | | |
10 | | impl SuperSTTDaemon { |
11 | | /// Mutate the config under the write lock, then persist it. Centralizes the |
12 | | /// lock → mutate → persist sequence so a settings handler can't hand-roll it |
13 | | /// and forget the persist (see Tier 1 #3). Returns the persist outcome so the |
14 | | /// caller can fold a save failure into its response. |
15 | 0 | async fn set_config_field<F>(&self, mutate: F) -> anyhow::Result<()> |
16 | 0 | where |
17 | 0 | F: FnOnce(&mut DaemonConfig), |
18 | 0 | { |
19 | | { |
20 | 0 | let mut config = self.config.write().await; |
21 | 0 | mutate(&mut config); |
22 | | } |
23 | 0 | self.persist_config().await |
24 | 0 | } |
25 | | |
26 | | /// Fold a persist outcome into a settings response. `base` already carries |
27 | | /// the setting-specific `.with_*` field and `message` is the success text. |
28 | | /// A save failure keeps the (already-applied) in-memory change — the daemon |
29 | | /// stays authoritative for the process lifetime — and appends the error to |
30 | | /// the message, logging a warning. |
31 | 0 | fn settings_saved( |
32 | 0 | base: DaemonResponse, |
33 | 0 | message: String, |
34 | 0 | persist: anyhow::Result<()>, |
35 | 0 | ) -> DaemonResponse { |
36 | 0 | match persist { |
37 | 0 | Ok(()) => base.with_message(message), |
38 | 0 | Err(e) => { |
39 | 0 | warn!("Setting changed but config save failed: {e}"); |
40 | 0 | base.with_message(format!("{message} (save failed: {e})")) |
41 | | } |
42 | | } |
43 | 0 | } |
44 | | /// Handle set preview typing command - enable or disable preview typing |
45 | | #[must_use] |
46 | 0 | pub async fn handle_set_preview_typing(&self, enabled: bool) -> DaemonResponse { |
47 | | // Update the in-memory setting. |
48 | 0 | self.preview_typing_enabled |
49 | 0 | .store(enabled, std::sync::atomic::Ordering::Relaxed); |
50 | | |
51 | 0 | let persist = self |
52 | 0 | .set_config_field(|c| c.transcription.preview_typing_enabled = enabled) |
53 | 0 | .await; |
54 | | |
55 | 0 | let state = if enabled { "enabled" } else { "disabled" }; |
56 | 0 | info!("Preview typing {state}"); |
57 | 0 | Self::settings_saved( |
58 | 0 | DaemonResponse::success().with_preview_typing_enabled(enabled), |
59 | 0 | format!("Preview typing {state}"), |
60 | 0 | persist, |
61 | | ) |
62 | 0 | } |
63 | | |
64 | | /// Handle get preview typing command - return current preview typing setting |
65 | | #[must_use] |
66 | 0 | pub fn handle_get_preview_typing(&self) -> DaemonResponse { |
67 | 0 | let enabled = self |
68 | 0 | .preview_typing_enabled |
69 | 0 | .load(std::sync::atomic::Ordering::Relaxed); |
70 | | |
71 | 0 | DaemonResponse::success() |
72 | 0 | .with_preview_typing_enabled(enabled) |
73 | 0 | .with_message("Preview typing setting retrieved successfully".to_string()) |
74 | 0 | } |
75 | | |
76 | | /// Handle set recording stop mode command |
77 | 0 | pub async fn handle_set_recording_stop_mode(&self, mode: RecordingStopMode) -> DaemonResponse { |
78 | 0 | let persist = self |
79 | 0 | .set_config_field(|c| c.transcription.recording_stop_mode = mode) |
80 | 0 | .await; |
81 | | |
82 | 0 | info!("Recording stop mode set to {mode}"); |
83 | 0 | Self::settings_saved( |
84 | 0 | DaemonResponse::success().with_recording_stop_mode(mode.to_string()), |
85 | 0 | format!("Recording stop mode set to {mode}"), |
86 | 0 | persist, |
87 | | ) |
88 | 0 | } |
89 | | |
90 | | /// Handle get recording stop mode command |
91 | 0 | pub async fn handle_get_recording_stop_mode(&self) -> DaemonResponse { |
92 | 0 | let config = self.config.read().await; |
93 | 0 | let mode = config.transcription.recording_stop_mode; |
94 | 0 | DaemonResponse::success().with_recording_stop_mode(mode.to_string()) |
95 | 0 | } |
96 | | |
97 | | /// Handle set write method command |
98 | 0 | pub async fn handle_set_write_method(&self, method: WriteMethod) -> DaemonResponse { |
99 | 0 | let persist = self |
100 | 0 | .set_config_field(|c| c.transcription.write_method = method) |
101 | 0 | .await; |
102 | | // Invalidate the cached simulator so the next recording creates a fresh one. |
103 | 0 | *self.simulator.write().await = None; |
104 | | |
105 | 0 | info!("Write method set to {method}"); |
106 | 0 | Self::settings_saved( |
107 | 0 | DaemonResponse::success().with_write_method(method.to_string()), |
108 | 0 | format!("Write method set to {method}"), |
109 | 0 | persist, |
110 | | ) |
111 | 0 | } |
112 | | |
113 | | /// Handle get write method command |
114 | 0 | pub async fn handle_get_write_method(&self) -> DaemonResponse { |
115 | 0 | let config = self.config.read().await; |
116 | 0 | let method = config.transcription.write_method; |
117 | 0 | DaemonResponse::success().with_write_method(method.to_string()) |
118 | 0 | } |
119 | | |
120 | | /// Handle set allow online models command |
121 | 8 | pub async fn handle_set_allow_online_models(&self, enabled: bool) -> DaemonResponse { |
122 | | { |
123 | 8 | let mut config_guard = self.config.write().await; |
124 | 8 | config_guard.online.allow_online_models = enabled; |
125 | | } |
126 | | |
127 | | // If disabling online models and the current model is online, revert to a |
128 | | // local one. Track why the revert didn't leave a usable local model so the |
129 | | // response doesn't falsely claim "all transcription is local". |
130 | 8 | let mut revert_warning: Option<String> = None; |
131 | 8 | if !enabled { |
132 | 4 | let current_is_online = { |
133 | 4 | let guard = self.model.read().await; |
134 | 4 | guard |
135 | 4 | .as_ref() |
136 | 4 | .is_some_and(|loaded| loaded.definition2 .is_online2 ()) |
137 | | }; |
138 | 4 | if current_is_online { |
139 | 2 | info!("Online models disabled; reverting to a local model"); |
140 | 2 | if let Some((name0 , provider0 , source0 )) = self.first_local_model().await { |
141 | 0 | let resp = self.handle_set_model(name, provider, source).await; |
142 | 0 | if resp.status != "success" { |
143 | 0 | warn!("Reverting to a local model failed; unloading the online model"); |
144 | 0 | *self.model.write().await = None; |
145 | 0 | revert_warning = Some(format!( |
146 | | "could not load a local model: {}", |
147 | 0 | resp.message.unwrap_or_else(|| "unknown error".to_string()) |
148 | | )); |
149 | 0 | } |
150 | | } else { |
151 | 2 | warn!("No local backend installed to revert to; unloading current model"); |
152 | 2 | *self.model.write().await = None; |
153 | 2 | revert_warning = Some("no local backend installed to fall back to".to_string()); |
154 | | } |
155 | 2 | } |
156 | 4 | } |
157 | | |
158 | 8 | let persist_result = self.persist_config().await; |
159 | | |
160 | 8 | match persist_result { |
161 | | Ok(()) => { |
162 | 8 | info!( |
163 | | "Online models {}", |
164 | 0 | if enabled { "enabled" } else { "disabled" } |
165 | | ); |
166 | 8 | let message = if enabled { |
167 | 4 | "Online models enabled — audio may be sent to external APIs".to_string() |
168 | | } else { |
169 | 4 | match &revert_warning { |
170 | 2 | Some(w) => format!("Online models disabled, but {w}"), |
171 | 2 | None => "Online models disabled — all transcription is local".to_string(), |
172 | | } |
173 | | }; |
174 | 8 | DaemonResponse::success() |
175 | 8 | .with_allow_online_models(enabled) |
176 | 8 | .with_message(message) |
177 | | } |
178 | 0 | Err(e) => { |
179 | 0 | warn!("Online models setting changed but config save failed: {e}"); |
180 | 0 | let mut message = format!( |
181 | | "Online models {} (config save failed: {e})", |
182 | 0 | if enabled { "enabled" } else { "disabled" } |
183 | | ); |
184 | 0 | if let Some(w) = &revert_warning { |
185 | 0 | message = format!("{message}; {w}"); |
186 | 0 | } |
187 | 0 | DaemonResponse::success() |
188 | 0 | .with_allow_online_models(enabled) |
189 | 0 | .with_message(message) |
190 | | } |
191 | | } |
192 | 8 | } |
193 | | |
194 | | /// Handle get allow online models command |
195 | 2 | pub async fn handle_get_allow_online_models(&self) -> DaemonResponse { |
196 | 2 | let config = self.config.read().await; |
197 | 2 | let enabled = config.online.allow_online_models; |
198 | 2 | DaemonResponse::success() |
199 | 2 | .with_allow_online_models(enabled) |
200 | 2 | .with_message("Allow online models setting retrieved".to_string()) |
201 | 2 | } |
202 | | |
203 | | /// Handle get custom models directory command |
204 | 0 | pub async fn handle_get_custom_models_dir(&self) -> DaemonResponse { |
205 | 0 | let path = self |
206 | 0 | .config |
207 | 0 | .read() |
208 | 0 | .await |
209 | | .transcription |
210 | | .custom_models_dir |
211 | 0 | .clone(); |
212 | 0 | DaemonResponse::success().with_custom_models_dir(path) |
213 | 0 | } |
214 | | |
215 | | /// Handle set custom models directory command |
216 | 0 | pub async fn handle_set_custom_models_dir(&self, path: Option<String>) -> DaemonResponse { |
217 | 0 | let path_display = path.as_deref().unwrap_or("none").to_string(); |
218 | | |
219 | 0 | let persist = self |
220 | 0 | .set_config_field(|c| c.transcription.custom_models_dir = path) |
221 | 0 | .await; |
222 | | |
223 | 0 | info!("Custom models directory set to {path_display}"); |
224 | 0 | Self::settings_saved( |
225 | 0 | DaemonResponse::success(), |
226 | 0 | format!("Custom models directory set to {path_display}"), |
227 | 0 | persist, |
228 | | ) |
229 | 0 | } |
230 | | } |