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;