Skip to main content

voirs_cli/
config.rs

1//! CLI-specific configuration utilities.
2
3pub mod profiles;
4
5use crate::error::{CliError, Result};
6use serde::{Deserialize, Serialize};
7use std::env;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::time::{Duration, Instant};
11use voirs_sdk::config::AppConfig;
12
13/// CLI-specific configuration
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CliConfig {
16    /// Core VoiRS configuration
17    #[serde(flatten)]
18    pub core: AppConfig,
19
20    /// CLI-specific settings
21    pub cli: CliSettings,
22}
23
24/// Alias for compatibility with interactive modules
25pub type Config = CliConfig;
26
27/// CLI-specific settings
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct CliSettings {
30    /// Default output format
31    pub default_output_format: String,
32
33    /// Default voice
34    pub default_voice: Option<String>,
35
36    /// Default quality level
37    pub default_quality: String,
38
39    /// Enable colored output
40    pub colored_output: bool,
41
42    /// Show progress bars
43    pub show_progress: bool,
44
45    /// Auto-play synthesized audio
46    pub auto_play: bool,
47
48    /// Preferred output directory
49    pub output_directory: Option<PathBuf>,
50
51    /// SSML validation level
52    pub ssml_validation: SsmlValidationLevel,
53
54    /// Recent files history size
55    pub history_size: usize,
56
57    /// Voice download preferences
58    pub download: DownloadSettings,
59}
60
61/// SSML validation levels
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum SsmlValidationLevel {
64    /// No validation
65    None,
66    /// Warn on issues
67    Warn,
68    /// Error on issues
69    Strict,
70}
71
72/// Download settings
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct DownloadSettings {
75    /// Parallel downloads
76    pub parallel_downloads: usize,
77
78    /// Retry attempts
79    pub retry_attempts: usize,
80
81    /// Auto-verify checksums
82    pub verify_checksums: bool,
83
84    /// Preferred download mirrors
85    pub preferred_mirrors: Vec<String>,
86}
87
88impl Default for CliConfig {
89    fn default() -> Self {
90        Self {
91            core: AppConfig::default(),
92            cli: CliSettings::default(),
93        }
94    }
95}
96
97impl Default for CliSettings {
98    fn default() -> Self {
99        Self {
100            default_output_format: "wav".to_string(),
101            default_voice: None,
102            default_quality: "high".to_string(),
103            colored_output: true,
104            show_progress: true,
105            auto_play: false,
106            output_directory: None,
107            ssml_validation: SsmlValidationLevel::Warn,
108            history_size: 100,
109            download: DownloadSettings::default(),
110        }
111    }
112}
113
114impl Default for DownloadSettings {
115    fn default() -> Self {
116        Self {
117            parallel_downloads: 3,
118            retry_attempts: 3,
119            verify_checksums: true,
120            preferred_mirrors: vec![
121                "https://huggingface.co".to_string(),
122                "https://github.com".to_string(),
123            ],
124        }
125    }
126}
127
128/// Configuration manager for the CLI
129pub struct ConfigManager {
130    config_path: PathBuf,
131    config: CliConfig,
132}
133
134impl ConfigManager {
135    /// Create a new configuration manager
136    pub fn new() -> Result<Self> {
137        let config_path = Self::find_config_file().unwrap_or_else(Self::default_config_path);
138
139        let config = if config_path.exists() {
140            Self::load_from_file(&config_path)?
141        } else {
142            CliConfig::default()
143        };
144
145        Ok(Self {
146            config_path,
147            config,
148        })
149    }
150
151    /// Create configuration manager with specific path
152    pub fn with_path<P: AsRef<Path>>(path: P) -> Result<Self> {
153        let config_path = path.as_ref().to_path_buf();
154
155        let config = if config_path.exists() {
156            Self::load_from_file(&config_path)?
157        } else {
158            CliConfig::default()
159        };
160
161        Ok(Self {
162            config_path,
163            config,
164        })
165    }
166
167    /// Get the current configuration
168    pub fn config(&self) -> &CliConfig {
169        &self.config
170    }
171
172    /// Get mutable reference to configuration
173    pub fn config_mut(&mut self) -> &mut CliConfig {
174        &mut self.config
175    }
176
177    /// Save configuration to file
178    pub fn save(&self) -> Result<()> {
179        // Create parent directory if it doesn't exist
180        if let Some(parent) = self.config_path.parent() {
181            fs::create_dir_all(parent).map_err(|e| {
182                CliError::file_operation("create directory", &parent.display().to_string(), e)
183            })?;
184        }
185
186        let content = toml::to_string_pretty(&self.config).map_err(CliError::from)?;
187
188        fs::write(&self.config_path, content).map_err(|e| {
189            CliError::file_operation("write", &self.config_path.display().to_string(), e)
190        })?;
191
192        Ok(())
193    }
194
195    /// Update configuration value
196    pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
197        match key {
198            "default_output_format" => {
199                self.config.cli.default_output_format = value.to_string();
200            }
201            "default_voice" => {
202                self.config.cli.default_voice = if value.is_empty() {
203                    None
204                } else {
205                    Some(value.to_string())
206                };
207            }
208            "default_quality" => {
209                if ["low", "medium", "high", "ultra"].contains(&value) {
210                    self.config.cli.default_quality = value.to_string();
211                } else {
212                    return Err(CliError::invalid_parameter(
213                        key,
214                        "must be one of: low, medium, high, ultra",
215                    ));
216                }
217            }
218            "colored_output" => {
219                self.config.cli.colored_output = value
220                    .parse()
221                    .map_err(|_| CliError::invalid_parameter(key, "must be true or false"))?;
222            }
223            "show_progress" => {
224                self.config.cli.show_progress = value
225                    .parse()
226                    .map_err(|_| CliError::invalid_parameter(key, "must be true or false"))?;
227            }
228            "auto_play" => {
229                self.config.cli.auto_play = value
230                    .parse()
231                    .map_err(|_| CliError::invalid_parameter(key, "must be true or false"))?;
232            }
233            "output_directory" => {
234                self.config.cli.output_directory = if value.is_empty() {
235                    None
236                } else {
237                    Some(PathBuf::from(value))
238                };
239            }
240            _ => {
241                return Err(CliError::invalid_parameter(
242                    key,
243                    "unknown configuration key",
244                ));
245            }
246        }
247
248        Ok(())
249    }
250
251    /// Get configuration value as string
252    pub fn get_value(&self, key: &str) -> Option<String> {
253        match key {
254            "default_output_format" => Some(self.config.cli.default_output_format.clone()),
255            "default_voice" => self.config.cli.default_voice.clone(),
256            "default_quality" => Some(self.config.cli.default_quality.clone()),
257            "colored_output" => Some(self.config.cli.colored_output.to_string()),
258            "show_progress" => Some(self.config.cli.show_progress.to_string()),
259            "auto_play" => Some(self.config.cli.auto_play.to_string()),
260            "output_directory" => self
261                .config
262                .cli
263                .output_directory
264                .as_ref()
265                .map(|p| p.display().to_string()),
266            _ => None,
267        }
268    }
269
270    /// Apply environment variable overrides
271    pub fn apply_env_overrides(&mut self) {
272        if let Ok(format) = env::var("VOIRS_OUTPUT_FORMAT") {
273            self.config.cli.default_output_format = format;
274        }
275
276        if let Ok(voice) = env::var("VOIRS_DEFAULT_VOICE") {
277            self.config.cli.default_voice = Some(voice);
278        }
279
280        if let Ok(quality) = env::var("VOIRS_QUALITY") {
281            if ["low", "medium", "high", "ultra"].contains(&quality.as_str()) {
282                self.config.cli.default_quality = quality;
283            }
284        }
285
286        if let Ok(colored) = env::var("VOIRS_COLORED_OUTPUT") {
287            if let Ok(value) = colored.parse() {
288                self.config.cli.colored_output = value;
289            }
290        }
291
292        if let Ok(progress) = env::var("VOIRS_SHOW_PROGRESS") {
293            if let Ok(value) = progress.parse() {
294                self.config.cli.show_progress = value;
295            }
296        }
297
298        if let Ok(output_dir) = env::var("VOIRS_OUTPUT_DIR") {
299            self.config.cli.output_directory = Some(PathBuf::from(output_dir));
300        }
301    }
302
303    /// Validate configuration
304    pub fn validate(&self) -> Result<Vec<String>> {
305        let mut warnings = Vec::new();
306
307        // Check if default voice exists
308        if let Some(ref voice) = self.config.cli.default_voice {
309            // Validate voice existence by checking if it can be resolved
310            match self.validate_voice_exists(voice) {
311                Ok(true) => {
312                    // Voice exists, no warning needed
313                }
314                Ok(false) => {
315                    warnings.push(format!(
316                        "Default voice '{}' does not exist. Use 'voirs voices list' to see available voices.",
317                        voice
318                    ));
319                }
320                Err(_) => {
321                    // Could not validate (e.g., voice system not initialized)
322                    // This is not critical, just note it as a warning
323                    warnings.push(format!(
324                        "Could not verify existence of default voice '{}'. Voice system may not be initialized.",
325                        voice
326                    ));
327                }
328            }
329        }
330
331        // Check output directory
332        if let Some(ref output_dir) = self.config.cli.output_directory {
333            if !output_dir.exists() {
334                warnings.push(format!(
335                    "Output directory '{}' does not exist",
336                    output_dir.display()
337                ));
338            } else if !output_dir.is_dir() {
339                return Err(CliError::config(format!(
340                    "Output directory '{}' is not a directory",
341                    output_dir.display()
342                )));
343            }
344        }
345
346        // Validate download settings
347        if self.config.cli.download.parallel_downloads == 0 {
348            return Err(CliError::config(
349                "parallel_downloads must be greater than 0",
350            ));
351        }
352
353        if self.config.cli.download.parallel_downloads > 10 {
354            warnings.push("parallel_downloads > 10 may cause server rate limiting".to_string());
355        }
356
357        Ok(warnings)
358    }
359
360    /// Validate if a voice exists in the system
361    fn validate_voice_exists(&self, voice_id: &str) -> Result<bool> {
362        // Try to check if voice exists by looking in standard voice directories
363        // This is a lightweight check that doesn't require initializing the full pipeline
364
365        // Get potential voice directories
366        let voice_dirs = self.get_voice_directories();
367
368        for voice_dir in voice_dirs {
369            let voice_config_path = voice_dir.join(voice_id).join("voice.json");
370            if voice_config_path.exists() {
371                // Found the voice config file
372                return Ok(true);
373            }
374        }
375
376        // Voice not found in standard directories
377        Ok(false)
378    }
379
380    /// Get list of directories where voices might be stored
381    fn get_voice_directories(&self) -> Vec<PathBuf> {
382        let mut dirs = Vec::new();
383
384        // 1. Check default data directory (~/.voirs/voices)
385        if let Some(home) = dirs::home_dir() {
386            dirs.push(home.join(".voirs").join("voices"));
387        }
388
389        // 3. Check XDG data directory on Linux
390        #[cfg(target_os = "linux")]
391        {
392            if let Ok(xdg_data_home) = std::env::var("XDG_DATA_HOME") {
393                dirs.push(PathBuf::from(xdg_data_home).join("voirs").join("voices"));
394            } else if let Some(home) = dirs::home_dir() {
395                dirs.push(
396                    home.join(".local")
397                        .join("share")
398                        .join("voirs")
399                        .join("voices"),
400                );
401            }
402        }
403
404        // 4. Check Library directory on macOS
405        #[cfg(target_os = "macos")]
406        {
407            if let Some(home) = dirs::home_dir() {
408                dirs.push(
409                    home.join("Library")
410                        .join("Application Support")
411                        .join("voirs")
412                        .join("voices"),
413                );
414            }
415        }
416
417        // 5. Check AppData on Windows
418        #[cfg(target_os = "windows")]
419        {
420            if let Ok(appdata) = std::env::var("APPDATA") {
421                dirs.push(PathBuf::from(appdata).join("voirs").join("voices"));
422            }
423        }
424
425        // 6. Check current directory (for development/testing)
426        dirs.push(PathBuf::from("./voices"));
427
428        dirs
429    }
430
431    /// Get configuration path
432    pub fn config_path(&self) -> &Path {
433        &self.config_path
434    }
435
436    /// Load configuration from file
437    fn load_from_file<P: AsRef<Path>>(path: P) -> Result<CliConfig> {
438        let content = fs::read_to_string(path.as_ref()).map_err(|e| {
439            CliError::file_operation("read", &path.as_ref().display().to_string(), e)
440        })?;
441
442        // Try TOML first, then JSON for backward compatibility
443        if let Ok(config) = toml::from_str::<CliConfig>(&content) {
444            Ok(config)
445        } else {
446            serde_json::from_str::<CliConfig>(&content)
447                .map_err(|e| CliError::config(format!("Invalid configuration format: {}", e)))
448        }
449    }
450
451    /// Find configuration file in standard locations
452    fn find_config_file() -> Option<PathBuf> {
453        let possible_paths = [
454            env::current_dir().ok().map(|d| d.join("voirs.toml")),
455            env::current_dir().ok().map(|d| d.join("voirs.json")),
456            Self::config_dir().map(|d| d.join("voirs.toml")),
457            Self::config_dir().map(|d| d.join("voirs.json")),
458            env::var("VOIRS_CONFIG").ok().map(PathBuf::from),
459        ];
460
461        possible_paths
462            .into_iter()
463            .flatten()
464            .find(|path| path.exists())
465    }
466
467    /// Get default configuration path
468    fn default_config_path() -> PathBuf {
469        Self::config_dir()
470            .unwrap_or_else(|| env::current_dir().unwrap())
471            .join("voirs.toml")
472    }
473
474    /// Get configuration directory
475    fn config_dir() -> Option<PathBuf> {
476        if let Some(config_dir) = env::var_os("XDG_CONFIG_HOME") {
477            Some(PathBuf::from(config_dir).join("voirs"))
478        } else if let Some(home_dir) = env::var_os("HOME") {
479            Some(PathBuf::from(home_dir).join(".config").join("voirs"))
480        } else {
481            env::var_os("APPDATA").map(|app_data| PathBuf::from(app_data).join("voirs"))
482        }
483    }
484}
485
486/// Configuration utilities
487pub mod utils {
488    use super::*;
489
490    /// Create a default configuration file
491    pub fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
492        let config = CliConfig::default();
493        let content = toml::to_string_pretty(&config).map_err(CliError::from)?;
494
495        if let Some(parent) = path.as_ref().parent() {
496            fs::create_dir_all(parent).map_err(|e| {
497                CliError::file_operation("create directory", &parent.display().to_string(), e)
498            })?;
499        }
500
501        fs::write(path.as_ref(), content).map_err(|e| {
502            CliError::file_operation("write", &path.as_ref().display().to_string(), e)
503        })?;
504
505        Ok(())
506    }
507
508    /// Migrate old configuration format to new format
509    pub fn migrate_config<P: AsRef<Path>>(old_path: P, new_path: P) -> Result<()> {
510        let old_content = fs::read_to_string(old_path.as_ref()).map_err(|e| {
511            CliError::file_operation("read", &old_path.as_ref().display().to_string(), e)
512        })?;
513
514        // Try to parse as old format (assuming it was JSON)
515        let old_config: serde_json::Value = serde_json::from_str(&old_content)
516            .map_err(|e| CliError::config(format!("Cannot parse old config: {}", e)))?;
517
518        // Create new config with migrated values
519        let mut new_config = CliConfig::default();
520
521        // Migrate known fields (this is a simplified example)
522        if let Some(output_format) = old_config.get("output_format") {
523            if let Some(format_str) = output_format.as_str() {
524                new_config.cli.default_output_format = format_str.to_string();
525            }
526        }
527
528        // Save migrated config
529        let content = toml::to_string_pretty(&new_config).map_err(CliError::from)?;
530
531        fs::write(new_path.as_ref(), content).map_err(|e| {
532            CliError::file_operation("write", &new_path.as_ref().display().to_string(), e)
533        })?;
534
535        Ok(())
536    }
537
538    /// Export configuration for sharing
539    pub fn export_config<P: AsRef<Path>>(
540        config: &CliConfig,
541        path: P,
542        format: ConfigFormat,
543    ) -> Result<()> {
544        let content = match format {
545            ConfigFormat::Toml => toml::to_string_pretty(config)?,
546            ConfigFormat::Json => serde_json::to_string_pretty(config)?,
547            ConfigFormat::Yaml => serde_yaml::to_string(config)
548                .map_err(|e| CliError::config(format!("YAML serialization error: {}", e)))?,
549        };
550
551        fs::write(path.as_ref(), content).map_err(|e| {
552            CliError::file_operation("write", &path.as_ref().display().to_string(), e)
553        })?;
554
555        Ok(())
556    }
557}
558
559/// Enhanced configuration loader with performance optimizations
560pub struct EnhancedConfigLoader {
561    cache: Option<(PathBuf, std::time::SystemTime, CliConfig)>,
562}
563
564impl EnhancedConfigLoader {
565    /// Create a new enhanced configuration loader
566    pub fn new() -> Self {
567        Self { cache: None }
568    }
569
570    /// Load configuration with caching and smart format detection
571    pub fn load_config<P: AsRef<Path>>(&mut self, path: P) -> Result<CliConfig> {
572        let path = path.as_ref();
573        let start_time = Instant::now();
574
575        // Check cache validity
576        if let Some((cached_path, cached_time, ref cached_config)) = &self.cache {
577            if cached_path == path {
578                if let Ok(metadata) = fs::metadata(path) {
579                    if let Ok(modified) = metadata.modified() {
580                        if modified <= *cached_time {
581                            // Cache hit - return cached config
582                            return Ok(cached_config.clone());
583                        }
584                    }
585                }
586            }
587        }
588
589        // Cache miss - load from file
590        let content = fs::read_to_string(path)
591            .map_err(|e| CliError::file_operation("read", &path.display().to_string(), e))?;
592
593        let config = self.parse_config_content(&content, path)?;
594
595        // Update cache
596        if let Ok(metadata) = fs::metadata(path) {
597            if let Ok(modified) = metadata.modified() {
598                self.cache = Some((path.to_path_buf(), modified, config.clone()));
599            }
600        }
601
602        let load_time = start_time.elapsed();
603        if load_time > Duration::from_millis(100) {
604            eprintln!("Warning: Configuration loading took {:?}", load_time);
605        }
606
607        Ok(config)
608    }
609
610    /// Parse configuration content with smart format detection
611    fn parse_config_content<P: AsRef<Path>>(&self, content: &str, path: P) -> Result<CliConfig> {
612        let extension = path.as_ref().extension().and_then(|ext| ext.to_str());
613
614        // Try format detection based on file extension first
615        match extension {
616            Some("toml") => {
617                match toml::from_str::<CliConfig>(content) {
618                    Ok(config) => return Ok(config),
619                    Err(e) => {
620                        // If TOML parsing fails, try fallback formats
621                        eprintln!("TOML parsing failed: {}, trying fallback formats", e);
622                    }
623                }
624            }
625            Some("json") => match serde_json::from_str::<CliConfig>(content) {
626                Ok(config) => return Ok(config),
627                Err(e) => {
628                    eprintln!("JSON parsing failed: {}, trying fallback formats", e);
629                }
630            },
631            Some("yaml") | Some("yml") => match serde_yaml::from_str::<CliConfig>(content) {
632                Ok(config) => return Ok(config),
633                Err(e) => {
634                    eprintln!("YAML parsing failed: {}, trying fallback formats", e);
635                }
636            },
637            _ => {
638                // No extension or unknown extension - try content-based detection
639            }
640        }
641
642        // Content-based format detection with smart heuristics
643        let trimmed = content.trim();
644
645        // Try TOML first (most common format)
646        if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
647            if let Ok(config) = toml::from_str::<CliConfig>(content) {
648                return Ok(config);
649            }
650        }
651
652        // Try JSON if content looks like JSON
653        if trimmed.starts_with('{') && trimmed.ends_with('}') {
654            if let Ok(config) = serde_json::from_str::<CliConfig>(content) {
655                return Ok(config);
656            }
657        }
658
659        // Try YAML as last resort
660        if let Ok(config) = serde_yaml::from_str::<CliConfig>(content) {
661            return Ok(config);
662        }
663
664        Err(CliError::config(format!(
665            "Unable to parse configuration file '{}' - tried TOML, JSON, and YAML formats",
666            path.as_ref().display()
667        )))
668    }
669
670    /// Clear the configuration cache
671    pub fn clear_cache(&mut self) {
672        self.cache = None;
673    }
674
675    /// Check if configuration is cached
676    pub fn is_cached<P: AsRef<Path>>(&self, path: P) -> bool {
677        if let Some((cached_path, _, _)) = &self.cache {
678            cached_path == path.as_ref()
679        } else {
680            false
681        }
682    }
683
684    /// Get cache statistics
685    pub fn cache_stats(&self) -> Option<(PathBuf, std::time::SystemTime)> {
686        self.cache
687            .as_ref()
688            .map(|(path, time, _)| (path.clone(), *time))
689    }
690}
691
692impl Default for EnhancedConfigLoader {
693    fn default() -> Self {
694        Self::new()
695    }
696}
697
698/// Configuration validation utilities
699pub mod validation {
700    use super::*;
701    use std::time::{Duration, Instant};
702
703    /// Validate configuration with detailed reporting
704    pub fn validate_config_detailed(config: &CliConfig) -> Result<ValidationReport> {
705        let mut report = ValidationReport::new();
706        let start_time = Instant::now();
707
708        // Validate CLI settings
709        validate_cli_settings(&config.cli, &mut report)?;
710
711        // Validate core configuration
712        validate_core_config(&config.core, &mut report)?;
713
714        // Performance check
715        let validation_time = start_time.elapsed();
716        if validation_time > Duration::from_millis(50) {
717            report.add_warning(format!(
718                "Configuration validation took {:?} - consider optimizing",
719                validation_time
720            ));
721        }
722
723        Ok(report)
724    }
725
726    /// Validate CLI-specific settings
727    fn validate_cli_settings(settings: &CliSettings, report: &mut ValidationReport) -> Result<()> {
728        // Validate output format
729        let valid_formats = ["wav", "mp3", "flac", "ogg", "m4a"];
730        if !valid_formats.contains(&settings.default_output_format.as_str()) {
731            report.add_error(format!(
732                "Invalid default output format '{}'. Valid formats: {}",
733                settings.default_output_format,
734                valid_formats.join(", ")
735            ));
736        }
737
738        // Validate quality level
739        let valid_qualities = ["low", "medium", "high", "ultra"];
740        if !valid_qualities.contains(&settings.default_quality.as_str()) {
741            report.add_error(format!(
742                "Invalid default quality '{}'. Valid qualities: {}",
743                settings.default_quality,
744                valid_qualities.join(", ")
745            ));
746        }
747
748        // Validate output directory
749        if let Some(ref output_dir) = settings.output_directory {
750            if !output_dir.exists() {
751                report.add_warning(format!(
752                    "Output directory '{}' does not exist",
753                    output_dir.display()
754                ));
755            } else if !output_dir.is_dir() {
756                report.add_error(format!(
757                    "Output directory '{}' is not a directory",
758                    output_dir.display()
759                ));
760            }
761        }
762
763        // Validate download settings
764        if settings.download.parallel_downloads == 0 {
765            report.add_error("parallel_downloads must be greater than 0".to_string());
766        } else if settings.download.parallel_downloads > 20 {
767            report.add_warning(format!(
768                "parallel_downloads ({}) is very high and may cause issues",
769                settings.download.parallel_downloads
770            ));
771        }
772
773        if settings.download.retry_attempts > 10 {
774            report.add_warning(format!(
775                "retry_attempts ({}) is very high and may cause long delays",
776                settings.download.retry_attempts
777            ));
778        }
779
780        Ok(())
781    }
782
783    /// Validate core configuration
784    fn validate_core_config(config: &AppConfig, report: &mut ValidationReport) -> Result<()> {
785        // Validate device configuration
786        match config.pipeline.device.as_str() {
787            "cpu" => {
788                report.add_info("Using CPU device - synthesis will be slower than GPU".to_string());
789            }
790            "gpu" | "cuda" => {
791                report.add_info("GPU acceleration enabled - ensure CUDA is available".to_string());
792                #[cfg(not(feature = "cuda"))]
793                report.add_warning(
794                    "GPU device specified but CUDA feature not enabled in build".to_string(),
795                );
796            }
797            "metal" => {
798                report.add_info("Metal acceleration enabled - macOS only".to_string());
799                #[cfg(not(target_os = "macos"))]
800                report.add_error("Metal device is only available on macOS".to_string());
801                #[cfg(not(feature = "metal"))]
802                report.add_warning(
803                    "Metal device specified but metal feature not enabled in build".to_string(),
804                );
805            }
806            other => {
807                report.add_error(format!(
808                    "Invalid device '{}' - must be 'cpu', 'gpu', 'cuda', or 'metal'",
809                    other
810                ));
811            }
812        }
813
814        // Validate threads configuration
815        if let Some(threads) = config.pipeline.num_threads {
816            if threads == 0 {
817                report.add_error("num_threads must be greater than 0".to_string());
818            } else if threads > num_cpus::get() * 2 {
819                report.add_warning(format!(
820                    "num_threads ({}) exceeds 2x CPU count ({}) - may cause overhead",
821                    threads,
822                    num_cpus::get()
823                ));
824            }
825        }
826
827        // Validate sample rate from default synthesis config
828        let sample_rate = config.pipeline.default_synthesis.sample_rate;
829        match sample_rate {
830            8000 | 16000 | 22050 | 24000 | 32000 | 44100 | 48000 => {
831                // Standard sample rates are ok
832            }
833            rate if rate < 8000 => {
834                report.add_error(format!("sample_rate {} is too low - minimum 8000 Hz", rate));
835            }
836            rate if rate > 48000 => {
837                report.add_warning(format!(
838                    "sample_rate {} is very high - may increase processing time",
839                    rate
840                ));
841            }
842            rate => {
843                report.add_warning(format!(
844                    "non-standard sample_rate {} - common rates: 16000, 22050, 44100, 48000",
845                    rate
846                ));
847            }
848        }
849
850        // Check cache directory if specified
851        if let Some(cache_dir) = &config.pipeline.cache_dir {
852            if !cache_dir.exists() {
853                report.add_warning(format!(
854                    "cache directory does not exist: {}",
855                    cache_dir.display()
856                ));
857            } else if !cache_dir.is_dir() {
858                report.add_error(format!(
859                    "cache path exists but is not a directory: {}",
860                    cache_dir.display()
861                ));
862            }
863        }
864
865        // Validate cache size
866        let max_cache_size_mb = config.pipeline.max_cache_size_mb;
867        if max_cache_size_mb == 0 {
868            report.add_warning("cache disabled (max_cache_size_mb = 0)".to_string());
869        } else if max_cache_size_mb > 10240 {
870            report.add_warning(format!(
871                "very large cache size ({} MB) may consume excessive memory",
872                max_cache_size_mb
873            ));
874        }
875
876        // Validate GPU usage consistency
877        if config.pipeline.use_gpu && config.pipeline.device == "cpu" {
878            report.add_warning(
879                "use_gpu is true but device is set to 'cpu' - inconsistent configuration"
880                    .to_string(),
881            );
882        }
883
884        Ok(())
885    }
886
887    /// Configuration validation report
888    #[derive(Debug, Clone)]
889    pub struct ValidationReport {
890        pub errors: Vec<String>,
891        pub warnings: Vec<String>,
892        pub info: Vec<String>,
893    }
894
895    impl ValidationReport {
896        pub fn new() -> Self {
897            Self {
898                errors: Vec::new(),
899                warnings: Vec::new(),
900                info: Vec::new(),
901            }
902        }
903
904        pub fn add_error(&mut self, error: String) {
905            self.errors.push(error);
906        }
907
908        pub fn add_warning(&mut self, warning: String) {
909            self.warnings.push(warning);
910        }
911
912        pub fn add_info(&mut self, info: String) {
913            self.info.push(info);
914        }
915
916        pub fn is_valid(&self) -> bool {
917            self.errors.is_empty()
918        }
919
920        pub fn has_warnings(&self) -> bool {
921            !self.warnings.is_empty()
922        }
923
924        pub fn summary(&self) -> String {
925            format!(
926                "Validation complete: {} errors, {} warnings, {} info messages",
927                self.errors.len(),
928                self.warnings.len(),
929                self.info.len()
930            )
931        }
932    }
933
934    impl Default for ValidationReport {
935        fn default() -> Self {
936            Self::new()
937        }
938    }
939}
940
941/// Configuration export formats
942pub enum ConfigFormat {
943    Toml,
944    Json,
945    Yaml,
946}