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