subx_cli/config/
mod.rs

1// src/config/mod.rs
2#![allow(deprecated)]
3//! Configuration management module for SubX.
4//!
5//! This module provides the complete configuration service system with
6//! dependency injection support and comprehensive type definitions.
7//!
8//! # Key Components
9//!
10//! - [`Config`] - Main configuration structure containing all settings
11//! - [`ConfigService`] - Service interface for configuration management
12//! - [`ProductionConfigService`] - Production implementation with file I/O
13//! - [`TestConfigService`] - Test implementation with controlled behavior
14//! - [`TestConfigBuilder`] - Builder pattern for test configurations
15//!
16//! # Examples
17//!
18//! ```rust
19//! use subx_cli::config::{Config, ConfigService, ProductionConfigService};
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! // Create a production configuration service
23//! let config_service = ProductionConfigService::new()?;
24//!
25//! // Load configuration
26//! let config = config_service.get_config()?;
27//! println!("AI Provider: {}", config.ai.provider);
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! # Architecture
33//!
34//! The configuration system uses dependency injection to provide testable
35//! and maintainable configuration management. All configuration access
36//! should go through the [`ConfigService`] trait.
37
38use serde::{Deserialize, Serialize};
39use std::path::PathBuf;
40
41// Configuration service system
42pub mod builder;
43pub mod environment;
44pub mod service;
45pub mod test_macros;
46pub mod test_service;
47pub mod validation;
48pub mod validator;
49
50// ============================================================================
51// Configuration Type Definitions
52// ============================================================================
53
54/// Full application configuration for SubX.
55///
56/// This struct aggregates all settings for AI integration, subtitle format
57/// conversion, synchronization, general options, and parallel execution.
58///
59/// # Examples
60///
61/// ```rust
62/// use subx_cli::config::Config;
63///
64/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
65/// let config = Config::default();
66/// assert_eq!(config.ai.provider, "openai");
67/// assert_eq!(config.formats.default_output, "srt");
68/// # Ok(())
69/// # }
70/// ```
71///
72/// # Serialization
73///
74/// This struct can be serialized to/from TOML format for configuration files.
75///
76/// ```rust
77/// use subx_cli::config::Config;
78///
79/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
80/// let config = Config::default();
81/// let toml_str = toml::to_string(&config)?;
82/// assert!(toml_str.contains("[ai]"));
83/// # Ok(())
84/// # }
85/// ```
86#[derive(Debug, Serialize, Deserialize, Clone, Default)]
87pub struct Config {
88    /// AI service configuration parameters.
89    pub ai: AIConfig,
90    /// Subtitle format conversion settings.
91    pub formats: FormatsConfig,
92    /// Audio-subtitle synchronization options.
93    pub sync: SyncConfig,
94    /// General runtime options (e.g., backup enabled, job limits).
95    pub general: GeneralConfig,
96    /// Parallel processing parameters.
97    pub parallel: ParallelConfig,
98    /// Optional file path from which the configuration was loaded.
99    pub loaded_from: Option<PathBuf>,
100}
101
102/// AI service configuration parameters.
103///
104/// This structure defines all configuration options for AI providers,
105/// including authentication, model parameters, retry behavior, and timeouts.
106///
107/// # Examples
108///
109/// Creating a default configuration:
110/// ```rust
111/// use subx_cli::config::AIConfig;
112///
113/// let ai_config = AIConfig::default();
114/// assert_eq!(ai_config.provider, "openai");
115/// assert_eq!(ai_config.model, "gpt-4.1-mini");
116/// assert_eq!(ai_config.temperature, 0.3);
117/// ```
118#[derive(Debug, Serialize, Deserialize, Clone)]
119pub struct AIConfig {
120    /// AI provider name (e.g. "openai", "anthropic").
121    pub provider: String,
122    /// API key for authentication.
123    pub api_key: Option<String>,
124    /// AI model name to use.
125    pub model: String,
126    /// API base URL.
127    pub base_url: String,
128    /// Maximum sample length per request.
129    pub max_sample_length: usize,
130    /// AI generation creativity parameter (0.0-1.0).
131    pub temperature: f32,
132    /// Maximum tokens in response.
133    pub max_tokens: u32,
134    /// Number of retries on request failure.
135    pub retry_attempts: u32,
136    /// Retry interval in milliseconds.
137    pub retry_delay_ms: u64,
138    /// HTTP request timeout in seconds.
139    /// This controls how long to wait for a response from the AI service.
140    /// For slow networks or complex requests, you may need to increase this value.
141    pub request_timeout_seconds: u64,
142}
143
144impl Default for AIConfig {
145    fn default() -> Self {
146        Self {
147            provider: "openai".to_string(),
148            api_key: None,
149            model: "gpt-4.1-mini".to_string(),
150            base_url: "https://api.openai.com/v1".to_string(),
151            max_sample_length: 3000,
152            temperature: 0.3,
153            max_tokens: 10000,
154            retry_attempts: 3,
155            retry_delay_ms: 1000,
156            // Set to 120 seconds to handle slow networks and complex AI requests
157            // This is especially important for users with high-latency connections
158            request_timeout_seconds: 120,
159        }
160    }
161}
162
163/// Subtitle format related configuration.
164///
165/// Controls how subtitle files are processed, including format conversion,
166/// encoding detection, and style preservation.
167///
168/// # Examples
169///
170/// ```rust
171/// use subx_cli::config::FormatsConfig;
172///
173/// let formats = FormatsConfig::default();
174/// assert_eq!(formats.default_output, "srt");
175/// assert_eq!(formats.default_encoding, "utf-8");
176/// assert!(!formats.preserve_styling);
177/// ```
178#[derive(Debug, Serialize, Deserialize, Clone)]
179pub struct FormatsConfig {
180    /// Default output format (e.g. "srt", "ass", "vtt").
181    pub default_output: String,
182    /// Whether to preserve style information during format conversion.
183    pub preserve_styling: bool,
184    /// Default character encoding (e.g. "utf-8", "gbk").
185    pub default_encoding: String,
186    /// Encoding detection confidence threshold (0.0-1.0).
187    pub encoding_detection_confidence: f32,
188}
189
190impl Default for FormatsConfig {
191    fn default() -> Self {
192        Self {
193            default_output: "srt".to_string(),
194            preserve_styling: false,
195            default_encoding: "utf-8".to_string(),
196            encoding_detection_confidence: 0.8,
197        }
198    }
199}
200
201/// Audio synchronization configuration supporting VAD speech detection.
202///
203/// This configuration struct defines settings for subtitle-audio synchronization,
204/// including method selection, timing constraints, and VAD-specific parameters.
205#[derive(Debug, Serialize, Deserialize, Clone)]
206pub struct SyncConfig {
207    /// Default synchronization method ("vad", "auto")
208    pub default_method: String,
209    /// Maximum allowed time offset in seconds
210    pub max_offset_seconds: f32,
211    /// Local VAD related settings
212    pub vad: VadConfig,
213
214    // Deprecated legacy fields, preserved for backward compatibility
215    /// Deprecated: correlation threshold for audio analysis
216    #[deprecated]
217    #[serde(skip)]
218    pub correlation_threshold: f32,
219    /// Deprecated: dialogue detection threshold
220    #[deprecated]
221    #[serde(skip)]
222    pub dialogue_detection_threshold: f32,
223    /// Deprecated: minimum dialogue duration in milliseconds
224    #[deprecated]
225    #[serde(skip)]
226    pub min_dialogue_duration_ms: u32,
227    /// Deprecated: dialogue merge gap in milliseconds
228    #[deprecated]
229    #[serde(skip)]
230    pub dialogue_merge_gap_ms: u32,
231    /// Deprecated: enable dialogue detection flag
232    #[deprecated]
233    #[serde(skip)]
234    pub enable_dialogue_detection: bool,
235    /// Deprecated: audio sample rate
236    #[deprecated]
237    #[serde(skip)]
238    pub audio_sample_rate: u32,
239    /// Deprecated: auto-detect sample rate flag  
240    #[deprecated]
241    #[serde(skip)]
242    pub auto_detect_sample_rate: bool,
243}
244
245/// Local Voice Activity Detection configuration.
246///
247/// Defines parameters for local VAD processing, including sensitivity settings,
248/// audio processing parameters, and speech detection behavior.
249#[derive(Debug, Serialize, Deserialize, Clone)]
250pub struct VadConfig {
251    /// Whether to enable local VAD method
252    pub enabled: bool,
253    /// Speech detection sensitivity (0.0-1.0)
254    pub sensitivity: f32,
255    /// Audio chunk size in samples
256    pub chunk_size: usize,
257    /// Processing sample rate in Hz
258    pub sample_rate: u32,
259    /// Padding chunks before and after speech detection
260    pub padding_chunks: u32,
261    /// Minimum speech duration in milliseconds
262    pub min_speech_duration_ms: u32,
263    /// Speech segment merge gap in milliseconds
264    pub speech_merge_gap_ms: u32,
265}
266
267#[allow(deprecated)]
268impl Default for SyncConfig {
269    fn default() -> Self {
270        Self {
271            default_method: "auto".to_string(),
272            max_offset_seconds: 60.0,
273            vad: VadConfig::default(),
274            correlation_threshold: 0.8,
275            dialogue_detection_threshold: 0.6,
276            min_dialogue_duration_ms: 500,
277            dialogue_merge_gap_ms: 200,
278            enable_dialogue_detection: true,
279            audio_sample_rate: 44100,
280            auto_detect_sample_rate: true,
281        }
282    }
283}
284
285impl Default for VadConfig {
286    fn default() -> Self {
287        Self {
288            enabled: true,
289            sensitivity: 0.75,
290            chunk_size: 512,
291            sample_rate: 16000,
292            padding_chunks: 3,
293            min_speech_duration_ms: 100,
294            speech_merge_gap_ms: 200,
295        }
296    }
297}
298
299/// General configuration settings for the SubX CLI tool.
300///
301/// This struct contains general settings that control the overall behavior
302/// of the application, including backup policies, processing limits, and
303/// user interface preferences.
304///
305/// # Examples
306///
307/// ```rust
308/// use subx_cli::config::GeneralConfig;
309///
310/// let config = GeneralConfig::default();
311/// assert_eq!(config.max_concurrent_jobs, 4);
312/// assert!(!config.backup_enabled);
313/// ```
314#[derive(Debug, Serialize, Deserialize, Clone)]
315pub struct GeneralConfig {
316    /// Enable automatic backup of original files.
317    pub backup_enabled: bool,
318    /// Maximum number of concurrent processing jobs.
319    pub max_concurrent_jobs: usize,
320    /// Task timeout in seconds.
321    pub task_timeout_seconds: u64,
322    /// Enable progress bar display.
323    pub enable_progress_bar: bool,
324    /// Worker idle timeout in seconds.
325    pub worker_idle_timeout_seconds: u64,
326}
327
328impl Default for GeneralConfig {
329    fn default() -> Self {
330        Self {
331            backup_enabled: false,
332            max_concurrent_jobs: 4,
333            task_timeout_seconds: 300,
334            enable_progress_bar: true,
335            worker_idle_timeout_seconds: 60,
336        }
337    }
338}
339
340/// Parallel processing configuration.
341///
342/// Controls how parallel processing is performed, including worker
343/// management, task distribution, and overflow handling strategies.
344///
345/// # Examples
346///
347/// ```rust
348/// use subx_cli::config::{ParallelConfig, OverflowStrategy};
349///
350/// let parallel = ParallelConfig::default();
351/// assert!(parallel.max_workers > 0);
352/// assert_eq!(parallel.overflow_strategy, OverflowStrategy::Block);
353/// ```
354#[derive(Debug, Serialize, Deserialize, Clone)]
355pub struct ParallelConfig {
356    /// Maximum number of worker threads.
357    pub max_workers: usize,
358    /// Strategy for handling task overflow when queues are full.
359    ///
360    /// Determines the behavior when the task queue reaches capacity.
361    /// - [`OverflowStrategy::Block`] - Block until space is available
362    /// - [`OverflowStrategy::Drop`] - Drop new tasks when full
363    /// - [`OverflowStrategy::Expand`] - Dynamically expand queue size
364    pub overflow_strategy: OverflowStrategy,
365    /// Task queue size.
366    pub task_queue_size: usize,
367    /// Enable task priorities.
368    pub enable_task_priorities: bool,
369    /// Auto-balance workers.
370    pub auto_balance_workers: bool,
371}
372
373impl Default for ParallelConfig {
374    fn default() -> Self {
375        Self {
376            max_workers: num_cpus::get(),
377            overflow_strategy: OverflowStrategy::Block,
378            task_queue_size: 1000,
379            enable_task_priorities: false,
380            auto_balance_workers: true,
381        }
382    }
383}
384
385/// Strategy for handling overflow when all workers are busy.
386///
387/// This enum defines different strategies for handling situations where
388/// all worker threads are occupied and new tasks arrive.
389///
390/// # Examples
391///
392/// ```rust
393/// use subx_cli::config::OverflowStrategy;
394///
395/// let strategy = OverflowStrategy::Block;
396/// assert_eq!(strategy, OverflowStrategy::Block);
397///
398/// // Comparison and serialization
399/// let strategies = vec![
400///     OverflowStrategy::Block,
401///     OverflowStrategy::Drop,
402///     OverflowStrategy::Expand,
403/// ];
404/// assert_eq!(strategies.len(), 3);
405/// ```
406#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
407pub enum OverflowStrategy {
408    /// Block until a worker becomes available.
409    ///
410    /// This is the safest option as it ensures all tasks are processed,
411    /// but may cause the application to become unresponsive.
412    Block,
413    /// Drop new tasks when all workers are busy.
414    ///
415    /// Use this when task loss is acceptable and responsiveness is critical.
416    Drop,
417    /// Create additional temporary workers.
418    ///
419    /// This can help with load spikes but may consume excessive resources.
420    Expand,
421    /// Drop oldest tasks in queue.
422    ///
423    /// Prioritizes recent tasks over older ones in the queue.
424    DropOldest,
425    /// Reject new tasks.
426    ///
427    /// Similar to Drop but may provide error feedback to the caller.
428    Reject,
429}
430
431// ============================================================================
432// Configuration Tests
433// ============================================================================
434
435#[cfg(test)]
436mod config_tests {
437    use super::*;
438
439    #[test]
440    fn test_default_config_creation() {
441        let config = Config::default();
442        assert_eq!(config.ai.provider, "openai");
443        assert_eq!(config.ai.model, "gpt-4.1-mini");
444        assert_eq!(config.formats.default_output, "srt");
445        assert!(!config.general.backup_enabled);
446        assert_eq!(config.general.max_concurrent_jobs, 4);
447    }
448
449    #[test]
450    fn test_ai_config_defaults() {
451        let ai_config = AIConfig::default();
452        assert_eq!(ai_config.provider, "openai");
453        assert_eq!(ai_config.model, "gpt-4.1-mini");
454        assert_eq!(ai_config.temperature, 0.3);
455        assert_eq!(ai_config.max_sample_length, 3000);
456        assert_eq!(ai_config.max_tokens, 10000);
457    }
458
459    #[test]
460    fn test_ai_config_max_tokens_configuration() {
461        let mut ai_config = AIConfig::default();
462        ai_config.max_tokens = 5000;
463        assert_eq!(ai_config.max_tokens, 5000);
464
465        // Test with different value
466        ai_config.max_tokens = 20000;
467        assert_eq!(ai_config.max_tokens, 20000);
468    }
469
470    #[test]
471    fn test_new_sync_config_defaults() {
472        let sync = SyncConfig::default();
473        assert_eq!(sync.default_method, "auto");
474        assert_eq!(sync.max_offset_seconds, 60.0);
475        assert!(sync.vad.enabled);
476    }
477
478    #[test]
479    fn test_sync_config_validation() {
480        let mut sync = SyncConfig::default();
481
482        // Valid configuration should pass validation
483        assert!(sync.validate().is_ok());
484
485        // Invalid default_method
486        sync.default_method = "invalid".to_string();
487        assert!(sync.validate().is_err());
488
489        // Reset and test other invalid values
490        sync = SyncConfig::default();
491        sync.max_offset_seconds = -1.0;
492        assert!(sync.validate().is_err());
493    }
494
495    #[test]
496    fn test_vad_config_validation() {
497        let mut vad = VadConfig::default();
498
499        // Valid configuration
500        assert!(vad.validate().is_ok());
501
502        // Invalid sensitivity
503        vad.sensitivity = 1.5;
504        assert!(vad.validate().is_err());
505
506        // Invalid chunk_size (not power of 2)
507        vad = VadConfig::default();
508        vad.chunk_size = 500;
509        assert!(vad.validate().is_err());
510
511        // Invalid sample rate (unsupported rate)
512        vad = VadConfig::default();
513        vad.sample_rate = 12345;
514        assert!(vad.validate().is_err());
515    }
516
517    #[test]
518    fn test_config_serialization_with_new_sync() {
519        let config = Config::default();
520        let toml_str = toml::to_string(&config).unwrap();
521
522        // Ensure new configuration structure exists in serialized output
523        assert!(toml_str.contains("[sync]"));
524        assert!(toml_str.contains("[sync.vad]"));
525        assert!(toml_str.contains("default_method"));
526        // Whisper-related fields removed, should not appear in serialized output
527        assert!(!toml_str.contains("[sync.whisper]"));
528        assert!(!toml_str.contains("analysis_window_seconds"));
529    }
530}
531
532// ============================================================================
533// Public API Re-exports
534// ============================================================================
535
536// Re-export the configuration service system
537pub use builder::TestConfigBuilder;
538pub use environment::{EnvironmentProvider, SystemEnvironmentProvider, TestEnvironmentProvider};
539pub use service::{ConfigService, ProductionConfigService};
540pub use test_service::TestConfigService;