subx_cli/config/
partial.rs

1//! Partial configuration structures and merging logic.
2
3use crate::config::OverflowStrategy;
4use serde::{Deserialize, Serialize};
5
6/// Partial configuration for all sections.
7#[derive(Debug, Default, Serialize, Deserialize)]
8#[serde(default)]
9pub struct PartialConfig {
10    pub ai: PartialAIConfig,
11    pub formats: PartialFormatsConfig,
12    pub sync: PartialSyncConfig,
13    pub general: PartialGeneralConfig,
14    pub parallel: PartialParallelConfig,
15}
16
17#[cfg(test)]
18mod tests {
19    use super::*;
20    use crate::config::Config;
21
22    #[test]
23    fn test_partial_ai_config_merge_and_to_complete_base_url() {
24        // 初始部分配置,含預設 base_url
25        let mut base = PartialConfig::default();
26        // 覆蓋 base_url 欄位
27        let mut override_cfg = PartialConfig::default();
28        override_cfg.ai.base_url = Some("https://override.example.com/v1".to_string());
29
30        base.merge(override_cfg).unwrap();
31        let complete = base.to_complete_config().unwrap();
32        assert_eq!(complete.ai.base_url, "https://override.example.com/v1");
33    }
34
35    #[test]
36    fn test_partial_ai_config_to_complete_default_base_url() {
37        let base = PartialConfig::default();
38        let complete = base.to_complete_config().unwrap();
39        assert_eq!(complete.ai.base_url, Config::default().ai.base_url);
40    }
41}
42
43/// Partial AI configuration.
44#[derive(Debug, Default, Serialize, Deserialize)]
45pub struct PartialAIConfig {
46    pub provider: Option<String>,
47    pub api_key: Option<String>,
48    pub model: Option<String>,
49    pub base_url: Option<String>,
50    pub max_sample_length: Option<usize>,
51    pub temperature: Option<f32>,
52    pub retry_attempts: Option<u32>,
53    pub retry_delay_ms: Option<u64>,
54}
55
56/// Partial formats configuration.
57#[derive(Debug, Default, Serialize, Deserialize)]
58pub struct PartialFormatsConfig {
59    pub default_output: Option<String>,
60    pub preserve_styling: Option<bool>,
61    pub default_encoding: Option<String>,
62    /// 編碼檢測信心度閾值(0.0-1.0)
63    pub encoding_detection_confidence: Option<f32>,
64}
65
66/// Partial sync configuration.
67#[derive(Debug, Default, Serialize, Deserialize)]
68pub struct PartialSyncConfig {
69    pub max_offset_seconds: Option<f32>,
70    pub audio_sample_rate: Option<u32>,
71    pub correlation_threshold: Option<f32>,
72    pub dialogue_detection_threshold: Option<f32>,
73    pub min_dialogue_duration_ms: Option<u64>,
74    /// 對話片段合併間隔(毫秒)
75    pub dialogue_merge_gap_ms: Option<u64>,
76    /// 是否啟用對話檢測
77    pub enable_dialogue_detection: Option<bool>,
78    /// 是否自動檢測原始採樣率
79    pub auto_detect_sample_rate: Option<bool>,
80}
81
82/// Partial general configuration.
83#[derive(Debug, Default, Serialize, Deserialize)]
84pub struct PartialGeneralConfig {
85    pub backup_enabled: Option<bool>,
86    pub max_concurrent_jobs: Option<usize>,
87    pub task_timeout_seconds: Option<u64>,
88    pub enable_progress_bar: Option<bool>,
89    pub worker_idle_timeout_seconds: Option<u64>,
90}
91
92/// Partial parallel processing configuration
93#[derive(Debug, Default, Serialize, Deserialize)]
94pub struct PartialParallelConfig {
95    pub task_queue_size: Option<usize>,
96    pub enable_task_priorities: Option<bool>,
97    pub auto_balance_workers: Option<bool>,
98    /// Strategy to apply when the task queue reaches its maximum size.
99    pub queue_overflow_strategy: Option<OverflowStrategy>,
100}
101
102impl PartialConfig {
103    /// Merge another partial configuration, overriding present fields.
104    pub fn merge(
105        &mut self,
106        other: PartialConfig,
107    ) -> Result<(), crate::config::manager::ConfigError> {
108        if let Some(v) = other.ai.provider {
109            self.ai.provider = Some(v);
110        }
111        if let Some(v) = other.ai.api_key {
112            self.ai.api_key = Some(v);
113        }
114        if let Some(v) = other.ai.model {
115            self.ai.model = Some(v);
116        }
117        if let Some(v) = other.ai.base_url {
118            self.ai.base_url = Some(v);
119        }
120        if let Some(v) = other.ai.max_sample_length {
121            self.ai.max_sample_length = Some(v);
122        }
123        if let Some(v) = other.ai.temperature {
124            self.ai.temperature = Some(v);
125        }
126        if let Some(v) = other.ai.retry_attempts {
127            self.ai.retry_attempts = Some(v);
128        }
129        if let Some(v) = other.ai.retry_delay_ms {
130            self.ai.retry_delay_ms = Some(v);
131        }
132        if let Some(v) = other.formats.default_output {
133            self.formats.default_output = Some(v);
134        }
135        if let Some(v) = other.formats.preserve_styling {
136            self.formats.preserve_styling = Some(v);
137        }
138        if let Some(v) = other.formats.default_encoding {
139            self.formats.default_encoding = Some(v);
140        }
141        if let Some(v) = other.sync.max_offset_seconds {
142            self.sync.max_offset_seconds = Some(v);
143        }
144        if let Some(v) = other.sync.audio_sample_rate {
145            self.sync.audio_sample_rate = Some(v);
146        }
147        if let Some(v) = other.sync.correlation_threshold {
148            self.sync.correlation_threshold = Some(v);
149        }
150        if let Some(v) = other.sync.dialogue_detection_threshold {
151            self.sync.dialogue_detection_threshold = Some(v);
152        }
153        if let Some(v) = other.sync.min_dialogue_duration_ms {
154            self.sync.min_dialogue_duration_ms = Some(v);
155        }
156        if let Some(v) = other.sync.auto_detect_sample_rate {
157            self.sync.auto_detect_sample_rate = Some(v);
158        }
159        if let Some(v) = other.general.backup_enabled {
160            self.general.backup_enabled = Some(v);
161        }
162        if let Some(v) = other.general.max_concurrent_jobs {
163            self.general.max_concurrent_jobs = Some(v);
164        }
165        if let Some(v) = other.general.task_timeout_seconds {
166            self.general.task_timeout_seconds = Some(v);
167        }
168        if let Some(v) = other.general.enable_progress_bar {
169            self.general.enable_progress_bar = Some(v);
170        }
171        if let Some(v) = other.general.worker_idle_timeout_seconds {
172            self.general.worker_idle_timeout_seconds = Some(v);
173        }
174        if let Some(v) = other.parallel.task_queue_size {
175            self.parallel.task_queue_size = Some(v);
176        }
177        if let Some(v) = other.parallel.enable_task_priorities {
178            self.parallel.enable_task_priorities = Some(v);
179        }
180        if let Some(v) = other.parallel.auto_balance_workers {
181            self.parallel.auto_balance_workers = Some(v);
182        }
183        if let Some(v) = other.parallel.queue_overflow_strategy {
184            self.parallel.queue_overflow_strategy = Some(v);
185        }
186        Ok(())
187    }
188}
189
190impl PartialConfig {
191    /// 轉換為完整配置,使用預設值填充缺少的欄位
192    pub fn to_complete_config(
193        &self,
194    ) -> Result<crate::config::Config, crate::config::manager::ConfigError> {
195        use crate::config::{
196            AIConfig, Config, FormatsConfig, GeneralConfig, ParallelConfig, SyncConfig,
197        };
198        let default = Config::default();
199
200        let ai = AIConfig {
201            provider: self.ai.provider.clone().unwrap_or(default.ai.provider),
202            api_key: self.ai.api_key.clone().or(default.ai.api_key),
203            model: self.ai.model.clone().unwrap_or(default.ai.model),
204            base_url: self.ai.base_url.clone().unwrap_or(default.ai.base_url),
205            max_sample_length: self
206                .ai
207                .max_sample_length
208                .unwrap_or(default.ai.max_sample_length),
209            temperature: self.ai.temperature.unwrap_or(default.ai.temperature),
210            retry_attempts: self.ai.retry_attempts.unwrap_or(default.ai.retry_attempts),
211            retry_delay_ms: self.ai.retry_delay_ms.unwrap_or(default.ai.retry_delay_ms),
212        };
213
214        let formats = FormatsConfig {
215            default_output: self
216                .formats
217                .default_output
218                .clone()
219                .unwrap_or(default.formats.default_output),
220            preserve_styling: self
221                .formats
222                .preserve_styling
223                .unwrap_or(default.formats.preserve_styling),
224            default_encoding: self
225                .formats
226                .default_encoding
227                .clone()
228                .unwrap_or(default.formats.default_encoding.clone()),
229            encoding_detection_confidence: self
230                .formats
231                .encoding_detection_confidence
232                .unwrap_or(default.formats.encoding_detection_confidence),
233        };
234
235        let sync = SyncConfig {
236            max_offset_seconds: self
237                .sync
238                .max_offset_seconds
239                .unwrap_or(default.sync.max_offset_seconds),
240            audio_sample_rate: self
241                .sync
242                .audio_sample_rate
243                .unwrap_or(default.sync.audio_sample_rate),
244            correlation_threshold: self
245                .sync
246                .correlation_threshold
247                .unwrap_or(default.sync.correlation_threshold),
248            dialogue_detection_threshold: self
249                .sync
250                .dialogue_detection_threshold
251                .unwrap_or(default.sync.dialogue_detection_threshold),
252            min_dialogue_duration_ms: self
253                .sync
254                .min_dialogue_duration_ms
255                .unwrap_or(default.sync.min_dialogue_duration_ms),
256            dialogue_merge_gap_ms: self
257                .sync
258                .dialogue_merge_gap_ms
259                .unwrap_or(default.sync.dialogue_merge_gap_ms),
260            enable_dialogue_detection: self
261                .sync
262                .enable_dialogue_detection
263                .unwrap_or(default.sync.enable_dialogue_detection),
264            auto_detect_sample_rate: self
265                .sync
266                .auto_detect_sample_rate
267                .unwrap_or(default.sync.auto_detect_sample_rate),
268        };
269
270        let general = GeneralConfig {
271            backup_enabled: self
272                .general
273                .backup_enabled
274                .unwrap_or(default.general.backup_enabled),
275            max_concurrent_jobs: self
276                .general
277                .max_concurrent_jobs
278                .unwrap_or(default.general.max_concurrent_jobs),
279            task_timeout_seconds: self
280                .general
281                .task_timeout_seconds
282                .unwrap_or(default.general.task_timeout_seconds),
283            enable_progress_bar: self
284                .general
285                .enable_progress_bar
286                .unwrap_or(default.general.enable_progress_bar),
287            worker_idle_timeout_seconds: self
288                .general
289                .worker_idle_timeout_seconds
290                .unwrap_or(default.general.worker_idle_timeout_seconds),
291        };
292
293        let parallel = ParallelConfig {
294            task_queue_size: self
295                .parallel
296                .task_queue_size
297                .unwrap_or(default.parallel.task_queue_size),
298            enable_task_priorities: self
299                .parallel
300                .enable_task_priorities
301                .unwrap_or(default.parallel.enable_task_priorities),
302            auto_balance_workers: self
303                .parallel
304                .auto_balance_workers
305                .unwrap_or(default.parallel.auto_balance_workers),
306            queue_overflow_strategy: self
307                .parallel
308                .queue_overflow_strategy
309                .unwrap_or(default.parallel.queue_overflow_strategy),
310        };
311        Ok(Config {
312            ai,
313            formats,
314            sync,
315            general,
316            parallel,
317            loaded_from: None,
318        })
319    }
320}