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