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;