#![allow(deprecated)]
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
pub mod builder;
pub mod environment;
pub mod field_validator;
pub mod masking;
pub mod service;
pub mod test_macros;
pub mod test_service;
pub mod validation;
pub mod validator;
pub use masking::mask_sensitive_value;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Config {
pub ai: AIConfig,
pub formats: FormatsConfig,
pub sync: SyncConfig,
pub general: GeneralConfig,
pub parallel: ParallelConfig,
pub loaded_from: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct AIConfig {
pub provider: String,
pub api_key: Option<String>,
pub model: String,
pub base_url: String,
pub max_sample_length: usize,
pub temperature: f32,
pub max_tokens: u32,
pub retry_attempts: u32,
pub retry_delay_ms: u64,
pub request_timeout_seconds: u64,
#[serde(default)]
pub api_version: Option<String>,
}
impl fmt::Debug for AIConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AIConfig")
.field("provider", &self.provider)
.field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
.field("model", &self.model)
.field("base_url", &self.base_url)
.field("max_sample_length", &self.max_sample_length)
.field("temperature", &self.temperature)
.field("max_tokens", &self.max_tokens)
.field("retry_attempts", &self.retry_attempts)
.field("retry_delay_ms", &self.retry_delay_ms)
.field("request_timeout_seconds", &self.request_timeout_seconds)
.field("api_version", &self.api_version)
.finish()
}
}
impl Default for AIConfig {
fn default() -> Self {
Self {
provider: "openai".to_string(),
api_key: None,
model: "gpt-4.1-mini".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
max_sample_length: 3000,
temperature: 0.3,
max_tokens: 10000,
retry_attempts: 3,
retry_delay_ms: 1000,
request_timeout_seconds: 120,
api_version: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FormatsConfig {
pub default_output: String,
pub preserve_styling: bool,
pub default_encoding: String,
pub encoding_detection_confidence: f32,
}
impl Default for FormatsConfig {
fn default() -> Self {
Self {
default_output: "srt".to_string(),
preserve_styling: false,
default_encoding: "utf-8".to_string(),
encoding_detection_confidence: 0.8,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SyncConfig {
pub default_method: String,
pub max_offset_seconds: f32,
pub vad: VadConfig,
#[deprecated]
#[serde(skip)]
pub correlation_threshold: f32,
#[deprecated]
#[serde(skip)]
pub dialogue_detection_threshold: f32,
#[deprecated]
#[serde(skip)]
pub min_dialogue_duration_ms: u32,
#[deprecated]
#[serde(skip)]
pub dialogue_merge_gap_ms: u32,
#[deprecated]
#[serde(skip)]
pub enable_dialogue_detection: bool,
#[deprecated]
#[serde(skip)]
pub audio_sample_rate: u32,
#[deprecated]
#[serde(skip)]
pub auto_detect_sample_rate: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VadConfig {
pub enabled: bool,
pub sensitivity: f32,
pub padding_chunks: u32,
pub min_speech_duration_ms: u32,
}
#[allow(deprecated)]
impl Default for SyncConfig {
fn default() -> Self {
Self {
default_method: "auto".to_string(),
max_offset_seconds: 60.0,
vad: VadConfig::default(),
correlation_threshold: 0.8,
dialogue_detection_threshold: 0.6,
min_dialogue_duration_ms: 500,
dialogue_merge_gap_ms: 200,
enable_dialogue_detection: true,
audio_sample_rate: 44100,
auto_detect_sample_rate: true,
}
}
}
impl Default for VadConfig {
fn default() -> Self {
Self {
enabled: true,
sensitivity: 0.25, padding_chunks: 3,
min_speech_duration_ms: 300,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GeneralConfig {
pub backup_enabled: bool,
pub max_concurrent_jobs: usize,
pub task_timeout_seconds: u64,
pub workspace: std::path::PathBuf,
pub enable_progress_bar: bool,
pub worker_idle_timeout_seconds: u64,
pub max_subtitle_bytes: u64,
pub max_audio_bytes: u64,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
backup_enabled: false,
max_concurrent_jobs: 4,
task_timeout_seconds: 300,
workspace: std::path::PathBuf::from("."),
enable_progress_bar: true,
worker_idle_timeout_seconds: 60,
max_subtitle_bytes: 52_428_800,
max_audio_bytes: 2_147_483_648,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ParallelConfig {
pub max_workers: usize,
pub overflow_strategy: OverflowStrategy,
pub task_queue_size: usize,
pub enable_task_priorities: bool,
pub auto_balance_workers: bool,
}
impl Default for ParallelConfig {
fn default() -> Self {
Self {
max_workers: num_cpus::get(),
overflow_strategy: OverflowStrategy::Block,
task_queue_size: 1000,
enable_task_priorities: false,
auto_balance_workers: true,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum OverflowStrategy {
Block,
Drop,
Expand,
DropOldest,
Reject,
}
#[cfg(test)]
mod config_tests {
use super::*;
#[test]
fn test_ai_config_debug_redacts_api_key() {
let ai = AIConfig {
api_key: Some("sk-topsecret-1234567890".to_string()),
..AIConfig::default()
};
let rendered = format!("{:?}", ai);
assert!(
!rendered.contains("sk-topsecret-1234567890"),
"Debug output must not leak API key: {rendered}"
);
assert!(
rendered.contains("[REDACTED]"),
"Debug output should mark api_key as redacted: {rendered}"
);
}
#[test]
fn test_ai_config_debug_none_api_key_is_none() {
let ai = AIConfig::default();
let rendered = format!("{:?}", ai);
assert!(rendered.contains("api_key: None"), "got: {rendered}");
}
#[test]
fn test_default_config_creation() {
let config = Config::default();
assert_eq!(config.ai.provider, "openai");
assert_eq!(config.ai.model, "gpt-4.1-mini");
assert_eq!(config.formats.default_output, "srt");
assert!(!config.general.backup_enabled);
assert_eq!(config.general.max_concurrent_jobs, 4);
}
#[test]
fn test_ai_config_defaults() {
let ai_config = AIConfig::default();
assert_eq!(ai_config.provider, "openai");
assert_eq!(ai_config.model, "gpt-4.1-mini");
assert_eq!(ai_config.temperature, 0.3);
assert_eq!(ai_config.max_sample_length, 3000);
assert_eq!(ai_config.max_tokens, 10000);
}
#[test]
fn test_ai_config_max_tokens_configuration() {
let mut ai_config = AIConfig {
max_tokens: 5000,
..Default::default()
};
assert_eq!(ai_config.max_tokens, 5000);
ai_config.max_tokens = 20000;
assert_eq!(ai_config.max_tokens, 20000);
}
#[test]
fn test_new_sync_config_defaults() {
let sync = SyncConfig::default();
assert_eq!(sync.default_method, "auto");
assert_eq!(sync.max_offset_seconds, 60.0);
assert!(sync.vad.enabled);
}
#[test]
fn test_sync_config_validation() {
let mut sync = SyncConfig::default();
assert!(sync.validate().is_ok());
sync.default_method = "invalid".to_string();
assert!(sync.validate().is_err());
sync = SyncConfig::default();
sync.max_offset_seconds = -1.0;
assert!(sync.validate().is_err());
}
#[test]
fn test_vad_config_validation() {
let mut vad = VadConfig::default();
assert!(vad.validate().is_ok());
vad.sensitivity = 1.5;
assert!(vad.validate().is_err());
}
#[test]
fn test_config_serialization_with_new_sync() {
let config = Config::default();
let toml_str = toml::to_string(&config).unwrap();
assert!(toml_str.contains("[sync]"));
assert!(toml_str.contains("[sync.vad]"));
assert!(toml_str.contains("default_method"));
assert!(!toml_str.contains("[sync.whisper]"));
assert!(!toml_str.contains("analysis_window_seconds"));
}
}
pub use builder::TestConfigBuilder;
pub use environment::{EnvironmentProvider, SystemEnvironmentProvider, TestEnvironmentProvider};
pub use service::{ConfigService, ProductionConfigService};
pub use test_service::TestConfigService;
pub use field_validator::validate_field;
pub use validator::validate_config;