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;