use anyhow::{Context, Result}; use clap::Parser; use std::path::PathBuf; use tracing::{error, info}; use voice_cli::{ cli::{Cli, Commands, ModelAction, ServerAction, TtsAction}, config::ServiceType, config_rs_integration::ConfigRsLoader, server, }; #[tokio::main] async fn main() { init_locale_from_env(); // Parse command line arguments let cli = Cli::parse(); // Don't initialize logging here - let each service initialize its own logging // based on configuration files // Generate CLI overrides from command line arguments let cli_overrides = match ConfigRsLoader::generate_cli_overrides_from_args(&cli) { Ok(overrides) => overrides, Err(e) => { error!("Failed to generate CLI overrides: {}", e); std::process::exit(1); } }; // Load configuration based on command type using config-rs with proper hierarchy let config = match &cli.command { // For init commands, we don't need to load existing config Commands::Server { action: ServerAction::Init { .. }, } => { // Use default config for init commands voice_cli::Config::default() } // For server commands, use server-specific config Commands::Server { action } => { let config_path = get_config_path_for_server_action(action, &cli.config); match ConfigRsLoader::load( config_path.as_ref(), &cli_overrides, Some(ServiceType::Server), ) { Ok(config) => config, Err(e) => { error!("Failed to load server configuration: {}", e); std::process::exit(1); } } } // For other commands, use default config loading _ => { let config_path = PathBuf::from(&cli.config); match ConfigRsLoader::load(Some(&config_path), &cli_overrides, None) { Ok(config) => config, Err(e) => { error!("Failed to load configuration: {}", e); std::process::exit(1); } } } }; // Log configuration summary if verbose if cli.verbose { info!("Configuration loaded successfully"); } // Route to appropriate handler let result = match cli.command { Commands::Server { action } => handle_server_command(action, &config).await, Commands::Model { action } => handle_model_command(action, &config).await, Commands::Tts { action } => handle_tts_command(action, &config).await, }; // Handle result match result { Ok(_) => { info!("Command completed successfully"); } Err(e) => { // Print error to stderr to ensure it's always visible eprintln!("❌ Error: {}", e); // Also print the error chain if available let mut current_error = e.source(); while let Some(err) = current_error { eprintln!(" Caused by: {}", err); current_error = err.source(); } // Also log the error error!("Command failed: {}", e); std::process::exit(1); } } } const AVAILABLE_LOCALES: &[&str] = &["en", "zh-CN", "zh-TW"]; const DEFAULT_LOCALE: &str = "en"; fn init_locale_from_env() { for env_key in ["DEFAULT_LOCALE", "LANG"] { let Ok(raw_locale) = std::env::var(env_key) else { continue; }; let normalized = if env_key == "LANG" { parse_lang_env(&raw_locale) } else { normalize_locale(&raw_locale) }; if AVAILABLE_LOCALES.contains(&normalized.as_str()) { rust_i18n::set_locale(&normalized); return; } } rust_i18n::set_locale(DEFAULT_LOCALE); } fn parse_lang_env(lang: &str) -> String { let lang = lang.split('.').next().unwrap_or(lang); let lang = lang.split('@').next().unwrap_or(lang); normalize_locale(lang) } fn normalize_locale(input: &str) -> String { let input = input.trim(); let input = input.split('.').next().unwrap_or(input); let input = input.split('@').next().unwrap_or(input); match input.to_lowercase().as_str() { "en" | "en_us" | "en-us" | "en_gb" | "en-gb" => "en".to_string(), "zh-cn" | "zh_cn" | "zh-hans" | "zh" => "zh-CN".to_string(), "zh-tw" | "zh_tw" | "zh-hant" => "zh-TW".to_string(), _ => input.to_string(), } } /// Handle server-related commands async fn handle_server_command(action: ServerAction, config: &voice_cli::Config) -> Result<()> { match action { ServerAction::Init { config: config_path, force, } => { info!("Initializing server configuration"); server::handle_server_init(config_path, force) .await .context("Failed to initialize server configuration") } ServerAction::Run { config: _ } => { info!("Running server in foreground mode"); server::handle_server_run(config) .await .context("Failed to run server") } } } /// Handle model-related commands async fn handle_model_command(action: ModelAction, config: &voice_cli::Config) -> Result<()> { use voice_cli::cli::model; match action { ModelAction::Download { model_name } => { info!("Downloading model: {}", model_name); model::handle_model_download(config, &model_name) .await .context("Failed to download model") } ModelAction::List => { info!("Listing models"); model::handle_model_list(config) .await .context("Failed to list models") } ModelAction::Validate => { info!("Validating models"); model::handle_model_validate(config) .await .context("Failed to validate models") } ModelAction::Remove { model_name } => { info!("Removing model: {}", model_name); model::handle_model_remove(config, &model_name) .await .context("Failed to remove model") } ModelAction::Diagnose { model_name } => { info!("Diagnosing model: {}", model_name); model::handle_model_diagnose(config, &model_name) .await .context("Failed to diagnose model") } } } /// Handle TTS-related commands async fn handle_tts_command(action: TtsAction, config: &voice_cli::Config) -> Result<()> { use voice_cli::cli::tts; match action { TtsAction::Init { force } => { info!("Initializing TTS environment"); tts::handle_tts_init(force) .await .context("Failed to initialize TTS environment") } TtsAction::Test { text, output, model, speed, pitch, volume, format, } => { info!("Testing TTS functionality"); tts::handle_tts_test(config, text, output, model, speed, pitch, volume, format) .await .context("Failed to test TTS functionality") } } } /// Extract config path from server action fn get_config_path_for_server_action( action: &ServerAction, _default_config: &str, ) -> Option { match action { ServerAction::Run { config } => config.clone(), _ => None, } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_cli_parsing() { use clap::Parser; // Test server run command let args = vec!["voice-cli", "server", "run"]; let cli = Cli::try_parse_from(args); assert!(cli.is_ok()); // Test model download command let args = vec!["voice-cli", "model", "download", "base"]; let cli = Cli::try_parse_from(args); assert!(cli.is_ok()); } }