Skip to main content

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