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,no_run
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 /// Subtitle translation configuration parameters.
126 #[serde(default)]
127 pub translation: TranslationConfig,
128 /// Optional file path from which the configuration was loaded.
129 pub loaded_from: Option<PathBuf>,
130}
131
132/// AI service configuration parameters.
133///
134/// This structure defines all configuration options for AI providers,
135/// including authentication, model parameters, retry behavior, and timeouts.
136///
137/// # Examples
138///
139/// Creating a default configuration:
140/// ```rust
141/// use subx_cli::config::AIConfig;
142///
143/// let ai_config = AIConfig::default();
144/// assert_eq!(ai_config.provider, "openai");
145/// assert_eq!(ai_config.model, "gpt-4.1-mini");
146/// assert_eq!(ai_config.temperature, 0.3);
147/// ```
148#[derive(Serialize, Deserialize, Clone)]
149pub struct AIConfig {
150 /// AI provider name.
151 ///
152 /// Supported canonical values:
153 /// - `"openai"` — hosted OpenAI API
154 /// - `"openrouter"` — hosted OpenRouter API
155 /// - `"azure-openai"` — hosted Azure OpenAI deployments
156 /// - `"local"` — any OpenAI-compatible local, LAN, or VPN endpoint
157 /// (Ollama, LM Studio, llama.cpp `llama-server`, vLLM, etc.)
158 ///
159 /// The string `"ollama"` is accepted as an input alias and is
160 /// normalized to `"local"` by
161 /// [`crate::config::field_validator::normalize_ai_provider`]; the
162 /// persisted on-disk value is always the canonical form.
163 pub provider: String,
164 /// API key for authentication.
165 pub api_key: Option<String>,
166 /// AI model name to use.
167 pub model: String,
168 /// API base URL.
169 pub base_url: String,
170 /// Maximum sample length per request.
171 pub max_sample_length: usize,
172 /// AI generation creativity parameter (0.0-1.0).
173 pub temperature: f32,
174 /// Maximum tokens in response.
175 pub max_tokens: u32,
176 /// Number of retries on request failure.
177 pub retry_attempts: u32,
178 /// Retry interval in milliseconds.
179 pub retry_delay_ms: u64,
180 /// HTTP request timeout in seconds.
181 /// This controls how long to wait for a response from the AI service.
182 /// For slow networks or complex requests, you may need to increase this value.
183 pub request_timeout_seconds: u64,
184
185 /// Azure OpenAI API version (optional, defaults to latest)
186 #[serde(default)]
187 pub api_version: Option<String>,
188}
189
190impl fmt::Debug for AIConfig {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 f.debug_struct("AIConfig")
193 .field("provider", &self.provider)
194 .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
195 .field("model", &self.model)
196 .field("base_url", &self.base_url)
197 .field("max_sample_length", &self.max_sample_length)
198 .field("temperature", &self.temperature)
199 .field("max_tokens", &self.max_tokens)
200 .field("retry_attempts", &self.retry_attempts)
201 .field("retry_delay_ms", &self.retry_delay_ms)
202 .field("request_timeout_seconds", &self.request_timeout_seconds)
203 .field("api_version", &self.api_version)
204 .finish()
205 }
206}
207
208impl Default for AIConfig {
209 fn default() -> Self {
210 Self {
211 provider: "openai".to_string(),
212 api_key: None,
213 model: "gpt-4.1-mini".to_string(),
214 base_url: "https://api.openai.com/v1".to_string(),
215 max_sample_length: 3000,
216 temperature: 0.3,
217 max_tokens: 10000,
218 retry_attempts: 3,
219 retry_delay_ms: 1000,
220 // Set to 120 seconds to handle slow networks and complex AI requests
221 // This is especially important for users with high-latency connections
222 request_timeout_seconds: 120,
223 api_version: None,
224 }
225 }
226}
227
228/// Subtitle format related configuration.
229///
230/// Controls how subtitle files are processed, including format conversion,
231/// encoding detection, and style preservation.
232///
233/// # Examples
234///
235/// ```rust
236/// use subx_cli::config::FormatsConfig;
237///
238/// let formats = FormatsConfig::default();
239/// assert_eq!(formats.default_output, "srt");
240/// assert_eq!(formats.default_encoding, "utf-8");
241/// assert!(!formats.preserve_styling);
242/// ```
243#[derive(Debug, Serialize, Deserialize, Clone)]
244pub struct FormatsConfig {
245 /// Default output format (e.g. "srt", "ass", "vtt").
246 pub default_output: String,
247 /// Whether to preserve style information during format conversion.
248 pub preserve_styling: bool,
249 /// Default character encoding (e.g. "utf-8", "gbk").
250 pub default_encoding: String,
251 /// Encoding detection confidence threshold (0.0-1.0).
252 pub encoding_detection_confidence: f32,
253}
254
255impl Default for FormatsConfig {
256 fn default() -> Self {
257 Self {
258 default_output: "srt".to_string(),
259 preserve_styling: false,
260 default_encoding: "utf-8".to_string(),
261 encoding_detection_confidence: 0.8,
262 }
263 }
264}
265
266/// Audio synchronization configuration supporting VAD speech detection.
267///
268/// This configuration struct defines settings for subtitle-audio synchronization,
269/// including method selection, timing constraints, and VAD-specific parameters.
270#[derive(Debug, Serialize, Deserialize, Clone)]
271pub struct SyncConfig {
272 /// Default synchronization method ("vad", "auto")
273 pub default_method: String,
274 /// Maximum allowed time offset in seconds
275 pub max_offset_seconds: f32,
276 /// Local VAD related settings
277 pub vad: VadConfig,
278
279 // Deprecated legacy fields, preserved for backward compatibility
280 /// Deprecated: correlation threshold for audio analysis
281 #[deprecated]
282 #[serde(skip)]
283 pub correlation_threshold: f32,
284 /// Deprecated: dialogue detection threshold
285 #[deprecated]
286 #[serde(skip)]
287 pub dialogue_detection_threshold: f32,
288 /// Deprecated: minimum dialogue duration in milliseconds
289 #[deprecated]
290 #[serde(skip)]
291 pub min_dialogue_duration_ms: u32,
292 /// Deprecated: dialogue merge gap in milliseconds
293 #[deprecated]
294 #[serde(skip)]
295 pub dialogue_merge_gap_ms: u32,
296 /// Deprecated: enable dialogue detection flag
297 #[deprecated]
298 #[serde(skip)]
299 pub enable_dialogue_detection: bool,
300 /// Deprecated: audio sample rate
301 #[deprecated]
302 #[serde(skip)]
303 pub audio_sample_rate: u32,
304 /// Deprecated: auto-detect sample rate flag
305 #[deprecated]
306 #[serde(skip)]
307 pub auto_detect_sample_rate: bool,
308}
309
310/// Local Voice Activity Detection configuration.
311///
312/// This struct defines parameters for local VAD processing, including sensitivity,
313/// audio chunking, and speech segment filtering. Adjust these fields to control
314/// how strictly speech is detected and how short segments are filtered out.
315///
316/// # Fields
317///
318/// - `enabled`: Whether local VAD is enabled
319/// - `sensitivity`: Speech detection sensitivity (0.0-1.0). Lower values are stricter and less likely to classify audio as speech.
320/// - `padding_chunks`: Number of non-speech chunks to include before and after detected speech
321/// - `min_speech_duration_ms`: Minimum duration (ms) for a segment to be considered valid speech
322///
323/// # Examples
324///
325/// ```rust
326/// use subx_cli::config::VadConfig;
327///
328/// let vad = VadConfig::default();
329/// assert!(vad.enabled);
330/// assert_eq!(vad.sensitivity, 0.25);
331/// ```
332#[derive(Debug, Serialize, Deserialize, Clone)]
333pub struct VadConfig {
334 /// Whether to enable local VAD method.
335 pub enabled: bool,
336 /// Speech detection sensitivity (0.0-1.0).
337 ///
338 /// Lower values are stricter: a smaller value means the detector is less likely to classify a chunk as speech.
339 /// For example, 0.25 is more strict than 0.75.
340 pub sensitivity: f32,
341 /// Number of non-speech chunks to pad before and after detected speech.
342 pub padding_chunks: u32,
343 /// Minimum speech duration in milliseconds.
344 ///
345 /// Segments shorter than this value will be discarded as noise or non-speech.
346 pub min_speech_duration_ms: u32,
347}
348
349#[allow(deprecated)]
350impl Default for SyncConfig {
351 fn default() -> Self {
352 Self {
353 default_method: "auto".to_string(),
354 max_offset_seconds: 60.0,
355 vad: VadConfig::default(),
356 correlation_threshold: 0.8,
357 dialogue_detection_threshold: 0.6,
358 min_dialogue_duration_ms: 500,
359 dialogue_merge_gap_ms: 200,
360 enable_dialogue_detection: true,
361 audio_sample_rate: 44100,
362 auto_detect_sample_rate: true,
363 }
364 }
365}
366
367impl Default for VadConfig {
368 fn default() -> Self {
369 Self {
370 enabled: true,
371 sensitivity: 0.25, // Default changed to 0.25, the smaller the value, the stricter the detection
372 padding_chunks: 3,
373 min_speech_duration_ms: 300,
374 }
375 }
376}
377
378/// General configuration settings for the SubX CLI tool.
379///
380/// This struct contains general settings that control the overall behavior
381/// of the application, including backup policies, processing limits, and
382/// user interface preferences.
383///
384/// # Examples
385///
386/// ```rust
387/// use subx_cli::config::GeneralConfig;
388///
389/// let config = GeneralConfig::default();
390/// assert_eq!(config.max_concurrent_jobs, 4);
391/// assert!(!config.backup_enabled);
392/// ```
393#[derive(Debug, Serialize, Deserialize, Clone)]
394pub struct GeneralConfig {
395 /// Enable automatic backup of original files.
396 pub backup_enabled: bool,
397 /// Maximum number of concurrent processing jobs.
398 pub max_concurrent_jobs: usize,
399 /// Task timeout in seconds.
400 pub task_timeout_seconds: u64,
401 /// Workspace directory for CLI commands (override current working directory).
402 pub workspace: std::path::PathBuf,
403 /// Enable progress bar display.
404 pub enable_progress_bar: bool,
405 /// Worker idle timeout in seconds.
406 pub worker_idle_timeout_seconds: u64,
407 /// Maximum subtitle file size in bytes (default 50 MiB).
408 pub max_subtitle_bytes: u64,
409 /// Maximum audio file size in bytes (default 2 GiB).
410 pub max_audio_bytes: u64,
411}
412
413impl Default for GeneralConfig {
414 fn default() -> Self {
415 Self {
416 backup_enabled: false,
417 max_concurrent_jobs: 4,
418 task_timeout_seconds: 300,
419 // Default workspace is current directory
420 workspace: std::path::PathBuf::from("."),
421 enable_progress_bar: true,
422 worker_idle_timeout_seconds: 60,
423 max_subtitle_bytes: 52_428_800,
424 max_audio_bytes: 2_147_483_648,
425 }
426 }
427}
428
429/// Parallel processing configuration.
430///
431/// Controls how parallel processing is performed, including worker
432/// management, task distribution, and overflow handling strategies.
433///
434/// # Examples
435///
436/// ```rust
437/// use subx_cli::config::{ParallelConfig, OverflowStrategy};
438///
439/// let parallel = ParallelConfig::default();
440/// assert!(parallel.max_workers > 0);
441/// assert_eq!(parallel.overflow_strategy, OverflowStrategy::Block);
442/// ```
443#[derive(Debug, Serialize, Deserialize, Clone)]
444pub struct ParallelConfig {
445 /// Maximum number of worker threads.
446 pub max_workers: usize,
447 /// Strategy for handling task overflow when queues are full.
448 ///
449 /// Determines the behavior when the task queue reaches capacity.
450 /// - [`OverflowStrategy::Block`] - Block until space is available
451 /// - [`OverflowStrategy::Drop`] - Drop new tasks when full
452 /// - [`OverflowStrategy::Expand`] - Dynamically expand queue size
453 pub overflow_strategy: OverflowStrategy,
454 /// Task queue size.
455 pub task_queue_size: usize,
456 /// Enable task priorities.
457 pub enable_task_priorities: bool,
458 /// Auto-balance workers.
459 pub auto_balance_workers: bool,
460}
461
462impl Default for ParallelConfig {
463 fn default() -> Self {
464 Self {
465 max_workers: num_cpus::get(),
466 overflow_strategy: OverflowStrategy::Block,
467 task_queue_size: 1000,
468 enable_task_priorities: false,
469 auto_balance_workers: true,
470 }
471 }
472}
473
474/// Strategy for handling overflow when all workers are busy.
475///
476/// This enum defines different strategies for handling situations where
477/// all worker threads are occupied and new tasks arrive.
478///
479/// # Examples
480///
481/// ```rust
482/// use subx_cli::config::OverflowStrategy;
483///
484/// let strategy = OverflowStrategy::Block;
485/// assert_eq!(strategy, OverflowStrategy::Block);
486///
487/// // Comparison and serialization
488/// let strategies = vec![
489/// OverflowStrategy::Block,
490/// OverflowStrategy::Drop,
491/// OverflowStrategy::Expand,
492/// ];
493/// assert_eq!(strategies.len(), 3);
494/// ```
495#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
496pub enum OverflowStrategy {
497 /// Block until a worker becomes available.
498 ///
499 /// This is the safest option as it ensures all tasks are processed,
500 /// but may cause the application to become unresponsive.
501 Block,
502 /// Drop new tasks when all workers are busy.
503 ///
504 /// Use this when task loss is acceptable and responsiveness is critical.
505 Drop,
506 /// Create additional temporary workers.
507 ///
508 /// This can help with load spikes but may consume excessive resources.
509 Expand,
510 /// Drop oldest tasks in queue.
511 ///
512 /// Prioritizes recent tasks over older ones in the queue.
513 DropOldest,
514 /// Reject new tasks.
515 ///
516 /// Similar to Drop but may provide error feedback to the caller.
517 Reject,
518}
519
520/// Subtitle translation configuration parameters.
521///
522/// Controls translation behavior shared by the `translate` command, the
523/// translation engine, and AI prompt builders. Validation rules ensure
524/// translation requests do not silently degrade to nonsensical values.
525///
526/// # Examples
527///
528/// ```rust
529/// use subx_cli::config::TranslationConfig;
530///
531/// let cfg = TranslationConfig::default();
532/// assert!(cfg.batch_size > 0);
533/// assert!(cfg.default_target_language.is_none());
534/// ```
535#[derive(Debug, Serialize, Deserialize, Clone)]
536pub struct TranslationConfig {
537 /// Maximum number of subtitle cues per AI translation request.
538 ///
539 /// Larger batches reduce the number of round-trips but risk hitting
540 /// per-request token limits. Must be greater than zero.
541 pub batch_size: usize,
542
543 /// Optional default target language used when the user omits
544 /// `--target-language` on the CLI.
545 #[serde(default)]
546 pub default_target_language: Option<String>,
547}
548
549impl Default for TranslationConfig {
550 fn default() -> Self {
551 Self {
552 batch_size: 40,
553 default_target_language: None,
554 }
555 }
556}
557
558// ============================================================================
559// Configuration Tests
560// ============================================================================
561
562#[cfg(test)]
563mod config_tests {
564 use super::*;
565
566 #[test]
567 fn test_ai_config_debug_redacts_api_key() {
568 let ai = AIConfig {
569 api_key: Some("sk-topsecret-1234567890".to_string()),
570 ..AIConfig::default()
571 };
572 let rendered = format!("{:?}", ai);
573 assert!(
574 !rendered.contains("sk-topsecret-1234567890"),
575 "Debug output must not leak API key: {rendered}"
576 );
577 assert!(
578 rendered.contains("[REDACTED]"),
579 "Debug output should mark api_key as redacted: {rendered}"
580 );
581 }
582
583 #[test]
584 fn test_ai_config_debug_none_api_key_is_none() {
585 let ai = AIConfig::default();
586 let rendered = format!("{:?}", ai);
587 assert!(rendered.contains("api_key: None"), "got: {rendered}");
588 }
589
590 #[test]
591 fn test_default_config_creation() {
592 let config = Config::default();
593 assert_eq!(config.ai.provider, "openai");
594 assert_eq!(config.ai.model, "gpt-4.1-mini");
595 assert_eq!(config.formats.default_output, "srt");
596 assert!(!config.general.backup_enabled);
597 assert_eq!(config.general.max_concurrent_jobs, 4);
598 }
599
600 #[test]
601 fn test_ai_config_defaults() {
602 let ai_config = AIConfig::default();
603 assert_eq!(ai_config.provider, "openai");
604 assert_eq!(ai_config.model, "gpt-4.1-mini");
605 assert_eq!(ai_config.temperature, 0.3);
606 assert_eq!(ai_config.max_sample_length, 3000);
607 assert_eq!(ai_config.max_tokens, 10000);
608 }
609
610 #[test]
611 fn test_ai_config_max_tokens_configuration() {
612 let mut ai_config = AIConfig {
613 max_tokens: 5000,
614 ..Default::default()
615 };
616 assert_eq!(ai_config.max_tokens, 5000);
617
618 // Test with different value
619 ai_config.max_tokens = 20000;
620 assert_eq!(ai_config.max_tokens, 20000);
621 }
622
623 #[test]
624 fn test_new_sync_config_defaults() {
625 let sync = SyncConfig::default();
626 assert_eq!(sync.default_method, "auto");
627 assert_eq!(sync.max_offset_seconds, 60.0);
628 assert!(sync.vad.enabled);
629 }
630
631 #[test]
632 fn test_sync_config_validation() {
633 let mut sync = SyncConfig::default();
634
635 // Valid configuration should pass validation
636 assert!(sync.validate().is_ok());
637
638 // Invalid default_method
639 sync.default_method = "invalid".to_string();
640 assert!(sync.validate().is_err());
641
642 // Reset and test other invalid values
643 sync = SyncConfig::default();
644 sync.max_offset_seconds = -1.0;
645 assert!(sync.validate().is_err());
646 }
647
648 #[test]
649 fn test_vad_config_validation() {
650 let mut vad = VadConfig::default();
651
652 // Valid configuration
653 assert!(vad.validate().is_ok());
654
655 // Invalid sensitivity
656 vad.sensitivity = 1.5;
657 assert!(vad.validate().is_err());
658 }
659
660 #[test]
661 fn test_config_serialization_with_new_sync() {
662 let config = Config::default();
663 let toml_str = toml::to_string(&config).unwrap();
664
665 // Ensure new configuration structure exists in serialized output
666 assert!(toml_str.contains("[sync]"));
667 assert!(toml_str.contains("[sync.vad]"));
668 assert!(toml_str.contains("default_method"));
669 // Whisper-related fields removed, should not appear in serialized output
670 assert!(!toml_str.contains("[sync.whisper]"));
671 assert!(!toml_str.contains("analysis_window_seconds"));
672 }
673}
674
675// ============================================================================
676// Public API Re-exports
677// ============================================================================
678
679// Re-export the configuration service system
680pub use builder::TestConfigBuilder;
681pub use environment::{EnvironmentProvider, SystemEnvironmentProvider, TestEnvironmentProvider};
682pub use service::{ConfigService, ProductionConfigService};
683pub use test_service::TestConfigService;
684
685// Re-export commonly used validation functions
686pub use field_validator::validate_field;
687pub use validator::validate_config;