#![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::unused_self)] #![allow(clippy::must_use_candidate)] #![allow(clippy::doc_markdown)] #![allow(clippy::unnecessary_wraps)] #![allow(clippy::float_cmp)] #![allow(clippy::match_same_arms)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::too_many_lines)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::similar_names)] #![allow(clippy::unused_async)] #![allow(clippy::needless_range_loop)] #![allow(clippy::uninlined_format_args)] #![allow(clippy::manual_clamp)] #![allow(clippy::return_self_not_must_use)] #![allow(clippy::cast_possible_wrap)] #![allow(clippy::cast_lossless)] #![allow(clippy::wildcard_imports)] #![allow(clippy::format_push_string)] #![allow(clippy::redundant_closure_for_method_calls)] #![allow(clippy::too_many_arguments)] #![allow(clippy::field_reassign_with_default)] #![allow(clippy::trivially_copy_pass_by_ref)] #![allow(clippy::await_holding_lock)]
use crate::cli_types::{CliAudioFormat, CliQualityLevel};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use voirs_sdk::config::{AppConfig, PipelineConfig};
use voirs_sdk::{AudioFormat, QualityLevel, Result, VoirsPipeline};
pub mod audio;
pub mod cli_types;
pub mod cloud;
pub mod commands;
pub mod completion;
pub mod config;
pub mod error;
pub mod help;
pub mod lsp;
pub mod model_types;
pub mod output;
pub mod packaging;
pub mod performance;
pub mod platform;
pub mod plugins;
pub mod progress;
pub mod ssml;
pub mod synthesis;
pub mod telemetry;
pub mod validation;
pub mod workflow;
#[derive(Parser)]
#[command(name = "voirs")]
#[command(about = "A pure Rust text-to-speech synthesis framework")]
#[command(version = env!("CARGO_PKG_VERSION"))]
pub struct CliApp {
#[command(flatten)]
pub global: GlobalOptions,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Parser)]
pub struct GlobalOptions {
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(short, long)]
pub quiet: bool,
#[arg(long)]
pub format: Option<CliAudioFormat>,
#[arg(long)]
pub voice: Option<String>,
#[arg(long)]
pub gpu: bool,
#[arg(long)]
pub threads: Option<usize>,
}
#[derive(Subcommand)]
pub enum CloudCommands {
Sync {
#[arg(long)]
force: bool,
#[arg(long)]
directory: Option<PathBuf>,
#[arg(long)]
dry_run: bool,
},
AddToSync {
local_path: PathBuf,
remote_path: String,
#[arg(long, default_value = "bidirectional")]
direction: String,
},
StorageStats,
CleanupCache {
#[arg(long, default_value = "30")]
max_age_days: u32,
#[arg(long)]
dry_run: bool,
},
Translate {
text: String,
#[arg(long)]
from: String,
#[arg(long)]
to: String,
#[arg(long, default_value = "balanced")]
quality: String,
},
AnalyzeContent {
text: String,
#[arg(long, default_value = "sentiment,entities")]
analysis_types: String,
#[arg(long)]
language: Option<String>,
},
AssessQuality {
audio_file: PathBuf,
text: String,
#[arg(long, default_value = "naturalness,intelligibility,overall")]
metrics: String,
},
HealthCheck,
Configure {
#[arg(long)]
show: bool,
#[arg(long)]
storage_provider: Option<String>,
#[arg(long)]
api_url: Option<String>,
#[arg(long)]
enable_service: Option<String>,
#[arg(long)]
init: bool,
},
}
#[derive(Subcommand)]
pub enum DatasetCommands {
Validate {
path: PathBuf,
#[arg(long)]
dataset_type: Option<String>,
#[arg(long)]
detailed: bool,
},
Convert {
input: PathBuf,
output: PathBuf,
#[arg(long)]
from: String,
#[arg(long)]
to: String,
},
Split {
path: PathBuf,
#[arg(long, default_value = "0.8")]
train_ratio: f32,
#[arg(long, default_value = "0.1")]
val_ratio: f32,
#[arg(long)]
test_ratio: Option<f32>,
#[arg(long)]
seed: Option<u64>,
},
Preprocess {
input: PathBuf,
output: PathBuf,
#[arg(long, default_value = "22050")]
sample_rate: u32,
#[arg(long)]
normalize: bool,
#[arg(long)]
filter: bool,
},
Analyze {
path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
detailed: bool,
},
}
#[derive(Subcommand)]
pub enum Commands {
Synthesize {
text: String,
output: Option<PathBuf>,
#[arg(long, default_value = "1.0")]
rate: f32,
#[arg(long, default_value = "0.0")]
pitch: f32,
#[arg(long, default_value = "0.0")]
volume: f32,
#[arg(long, default_value = "high")]
quality: CliQualityLevel,
#[arg(long)]
enhance: bool,
#[arg(short, long)]
play: bool,
#[arg(long)]
auto_detect: bool,
},
SynthesizeFile {
input: PathBuf,
#[arg(short, long)]
output_dir: Option<PathBuf>,
#[arg(long, default_value = "1.0")]
rate: f32,
#[arg(long, default_value = "high")]
quality: CliQualityLevel,
},
ListVoices {
#[arg(long)]
language: Option<String>,
#[arg(long)]
detailed: bool,
},
VoiceInfo {
voice_id: String,
},
DownloadVoice {
voice_id: String,
#[arg(long)]
force: bool,
},
PreviewVoice {
voice_id: String,
#[arg(long)]
text: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
no_play: bool,
},
CompareVoices {
voice_ids: Vec<String>,
},
Test {
#[arg(default_value = "Hello, this is a test of VoiRS speech synthesis.")]
text: String,
#[arg(long)]
play: bool,
},
CrossLangTest {
#[arg(long, default_value = "json")]
format: String,
#[arg(long)]
save_report: bool,
},
TestApi {
server_url: String,
#[arg(long)]
api_key: Option<String>,
#[arg(long)]
concurrent: Option<usize>,
#[arg(long)]
report: Option<String>,
#[arg(long)]
verbose: bool,
},
Config {
#[arg(long)]
show: bool,
#[arg(long)]
init: bool,
#[arg(long)]
path: Option<PathBuf>,
},
ListModels {
#[arg(long)]
backend: Option<String>,
#[arg(long)]
detailed: bool,
},
DownloadModel {
model_id: String,
#[arg(long)]
force: bool,
},
BenchmarkModels {
model_ids: Vec<String>,
#[arg(short, long, default_value = "3")]
iterations: u32,
#[arg(long)]
accuracy: bool,
},
OptimizeModel {
model_id: String,
#[arg(short, long)]
output: Option<String>,
#[arg(long, default_value = "balanced")]
strategy: String,
},
Batch {
input: PathBuf,
#[arg(short, long)]
output_dir: Option<PathBuf>,
#[arg(short, long)]
workers: Option<usize>,
#[arg(long, default_value = "1.0")]
rate: f32,
#[arg(long, default_value = "0.0")]
pitch: f32,
#[arg(long, default_value = "0.0")]
volume: f32,
#[arg(long, default_value = "high")]
quality: CliQualityLevel,
#[arg(long)]
resume: bool,
},
Server {
#[arg(short, long, default_value = "8080")]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
},
Interactive {
#[arg(short, long)]
voice: Option<String>,
#[arg(long)]
no_audio: bool,
#[arg(long)]
debug: bool,
#[arg(long)]
load_session: Option<PathBuf>,
#[arg(long)]
auto_save: bool,
},
Guide {
command: Option<String>,
#[arg(long)]
getting_started: bool,
#[arg(long)]
examples: bool,
},
GenerateCompletion {
#[arg(value_enum)]
shell: clap_complete::Shell,
#[arg(short, long)]
output: Option<std::path::PathBuf>,
#[arg(long)]
install_help: bool,
#[arg(long)]
install_script: bool,
#[arg(long)]
status: bool,
},
Dataset {
#[command(subcommand)]
command: DatasetCommands,
},
Dashboard {
#[arg(short, long, default_value = "500")]
interval: u64,
},
Cloud {
#[command(subcommand)]
command: CloudCommands,
},
Telemetry {
#[command(subcommand)]
command: commands::telemetry::TelemetryCommands,
},
Lsp {
#[arg(long)]
verbose: bool,
},
#[cfg(feature = "onnx")]
Kokoro {
#[command(subcommand)]
command: commands::kokoro::KokoroCommands,
},
#[cfg(feature = "onnx")]
Onnx {
#[command(subcommand)]
command: commands::onnx_tools::OnnxCommand,
},
Accuracy {
#[command(flatten)]
command: commands::accuracy::AccuracyCommand,
},
Performance {
#[command(flatten)]
command: commands::performance::PerformanceCommand,
},
#[cfg(feature = "emotion")]
Emotion {
#[command(subcommand)]
command: commands::emotion::EmotionCommand,
},
#[cfg(feature = "cloning")]
Clone {
#[command(subcommand)]
command: commands::cloning::CloningCommand,
},
#[cfg(feature = "conversion")]
Convert {
#[command(subcommand)]
command: commands::conversion::ConversionCommand,
},
#[cfg(feature = "singing")]
Sing {
#[command(subcommand)]
command: commands::singing::SingingCommand,
},
#[cfg(feature = "spatial")]
Spatial {
#[command(subcommand)]
command: commands::spatial::SpatialCommand,
},
Capabilities {
#[command(subcommand)]
command: commands::capabilities::CapabilitiesCommand,
},
Checkpoint {
#[command(subcommand)]
command: commands::checkpoint::CheckpointCommands,
},
Monitor {
#[command(subcommand)]
command: commands::monitoring::MonitoringCommand,
},
Train {
#[command(subcommand)]
command: commands::train::TrainCommands,
},
ConvertModel {
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long)]
from: Option<String>,
#[arg(long)]
model_type: String,
#[arg(long)]
verify: bool,
},
VocoderInfer {
checkpoint: PathBuf,
#[arg(long)]
mel: Option<PathBuf>,
#[arg(short, long, default_value = "vocoder_output.wav")]
output: PathBuf,
#[arg(long, default_value = "50")]
steps: usize,
#[arg(long)]
quality: Option<String>,
#[arg(long)]
batch_input: Option<PathBuf>,
#[arg(long)]
batch_output: Option<PathBuf>,
#[arg(long)]
metrics: bool,
},
Stream {
text: Option<String>,
#[arg(long, default_value = "100")]
latency: u64,
#[arg(long, default_value = "512")]
chunk_size: usize,
#[arg(long, default_value = "4")]
buffer_chunks: usize,
#[arg(long)]
play: bool,
},
ModelInspect {
model: PathBuf,
#[arg(long)]
detailed: bool,
#[arg(long)]
export: Option<PathBuf>,
#[arg(long)]
verify: bool,
},
Export {
#[arg(long)]
export_type: String,
source: String,
#[arg(short, long)]
output: PathBuf,
#[arg(long)]
include_weights: bool,
},
Import {
input: PathBuf,
#[arg(long)]
name: Option<String>,
#[arg(long)]
force: bool,
#[arg(long, default_value = "true")]
validate: bool,
},
History {
#[arg(short = 'n', long, default_value = "20")]
limit: usize,
#[arg(long)]
stats: bool,
#[arg(long)]
suggest: bool,
#[arg(long)]
clear: bool,
},
Workflow {
#[command(subcommand)]
command: commands::workflow::WorkflowCommands,
},
Alias {
#[command(subcommand)]
command: AliasCommand,
},
}
#[derive(Subcommand)]
pub enum AliasCommand {
Add {
name: String,
command: String,
#[arg(short, long)]
description: Option<String>,
},
Remove {
name: String,
},
List,
Show {
name: String,
},
Clear,
}
impl CliApp {
pub async fn run() -> Result<()> {
let app = Self::parse();
app.init_logging()?;
let config = app.load_config().await?;
app.execute_command(config).await
}
fn init_logging(&self) -> Result<()> {
let level = if self.global.quiet {
tracing::Level::ERROR
} else {
match self.global.verbose {
0 => tracing::Level::INFO,
1 => tracing::Level::DEBUG,
_ => tracing::Level::TRACE,
}
};
tracing_subscriber::fmt()
.with_max_level(level)
.with_target(false)
.with_writer(std::io::stderr) .init();
Ok(())
}
async fn load_config(&self) -> Result<AppConfig> {
let mut config = if let Some(config_path) = &self.global.config {
tracing::info!("Loading configuration from {:?}", config_path);
self.load_config_from_file(config_path).await?
} else {
self.load_config_from_default_locations().await?
};
self.apply_cli_overrides(&mut config);
Ok(config)
}
async fn load_config_from_file(&self, config_path: &std::path::Path) -> Result<AppConfig> {
if !config_path.exists() {
tracing::warn!(
"Configuration file not found: {}, using defaults",
config_path.display()
);
return Ok(AppConfig::default());
}
let content =
std::fs::read_to_string(config_path).map_err(|e| voirs_sdk::VoirsError::IoError {
path: config_path.to_path_buf(),
operation: voirs_sdk::error::IoOperation::Read,
source: e,
})?;
let config = match config_path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => {
toml::from_str(&content).or_else(|_| {
self.parse_config_auto_detect(&content)
})?
}
Some("json") => {
serde_json::from_str(&content).or_else(|_| {
self.parse_config_auto_detect(&content)
})?
}
Some("yaml") | Some("yml") => {
serde_yaml::from_str(&content).or_else(|_| {
self.parse_config_auto_detect(&content)
})?
}
_ => {
self.parse_config_auto_detect(&content)?
}
};
tracing::info!(
"Successfully loaded configuration from {}",
config_path.display()
);
Ok(config)
}
async fn load_config_from_default_locations(&self) -> Result<AppConfig> {
let possible_paths = get_default_config_paths();
for path in possible_paths {
if path.exists() {
tracing::info!("Found configuration file at: {}", path.display());
return self.load_config_from_file(&path).await;
}
}
tracing::info!("No configuration file found, using defaults");
Ok(AppConfig::default())
}
fn parse_config_auto_detect(&self, content: &str) -> Result<AppConfig> {
let trimmed = content.trim_start();
if trimmed.starts_with('{') {
serde_json::from_str(content).map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Failed to parse JSON configuration: {}",
e
))
})
} else if trimmed.contains("---") || content.contains(": ") {
serde_yaml::from_str(content).or_else(|yaml_err| {
toml::from_str(content).map_err(|toml_err| {
voirs_sdk::VoirsError::config_error(format!(
"Failed to parse configuration. YAML error: {}, TOML error: {}",
yaml_err, toml_err
))
})
})
} else {
toml::from_str(content)
.or_else(|_| serde_json::from_str(content))
.or_else(|_| serde_yaml::from_str(content))
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Unable to parse configuration file. Supported formats: TOML, JSON, YAML. Last error: {}", e
))
})
}
}
fn apply_cli_overrides(&self, config: &mut AppConfig) {
if self.global.gpu {
config.pipeline.use_gpu = true;
}
if let Some(threads) = self.global.threads {
config.pipeline.num_threads = Some(threads);
}
if let Some(ref voice) = self.global.voice {
config.cli.default_voice = Some(voice.clone());
}
if let Some(ref format) = self.global.format {
config.cli.default_format = (*format).into();
config.pipeline.default_synthesis.output_format = (*format).into();
}
}
async fn execute_command(&self, config: AppConfig) -> Result<()> {
match &self.command {
Commands::Synthesize {
text,
output,
rate,
pitch,
volume,
quality,
enhance,
play,
auto_detect,
} => {
let args = commands::synthesize::SynthesizeArgs {
text,
output: output.as_deref(),
rate: *rate,
pitch: *pitch,
volume: *volume,
quality: (*quality).into(),
enhance: *enhance,
play: *play,
auto_detect: *auto_detect,
};
commands::synthesize::run_synthesize(args, &config, &self.global).await
}
Commands::SynthesizeFile {
input,
output_dir,
rate,
quality,
} => {
commands::synthesize::run_synthesize_file(
input,
output_dir.as_deref(),
*rate,
(*quality).into(),
&config,
&self.global,
)
.await
}
Commands::ListVoices { language, detailed } => {
commands::voices::run_list_voices(language.as_deref(), *detailed, &config).await
}
Commands::VoiceInfo { voice_id } => {
commands::voices::run_voice_info(voice_id, &config).await
}
Commands::DownloadVoice { voice_id, force } => {
commands::voices::run_download_voice(voice_id, *force, &config).await
}
Commands::PreviewVoice {
voice_id,
text,
output,
no_play,
} => {
commands::voices::run_preview_voice(
voice_id,
text.as_deref(),
output.as_ref(),
*no_play,
&config,
&self.global,
)
.await
}
Commands::CompareVoices { voice_ids } => {
commands::voices::run_compare_voices(voice_ids.clone(), &config).await
}
Commands::Test { text, play } => {
commands::test::run_test(text, *play, &config, &self.global).await
}
Commands::CrossLangTest {
format,
save_report,
} => {
commands::cross_lang_test::run_cross_lang_tests(
format,
*save_report,
&config,
&self.global,
)
.await
}
Commands::TestApi {
server_url,
api_key,
concurrent,
report,
verbose,
} => commands::test_api::run_api_tests(
server_url.clone(),
api_key.clone(),
*concurrent,
report.clone(),
*verbose,
)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "API Tester".to_string(),
message: e.to_string(),
}),
Commands::Config { show, init, path } => {
commands::config::run_config(*show, *init, path.as_deref(), &config).await
}
Commands::ListModels { backend, detailed } => {
commands::models::run_list_models(
backend.as_deref(),
*detailed,
&config,
&self.global,
)
.await
}
Commands::DownloadModel { model_id, force } => {
commands::models::run_download_model(model_id, *force, &config, &self.global).await
}
Commands::BenchmarkModels {
model_ids,
iterations,
accuracy,
} => {
commands::models::run_benchmark_models(
model_ids,
*iterations,
*accuracy,
&config,
&self.global,
)
.await
}
Commands::OptimizeModel {
model_id,
output,
strategy,
} => {
commands::models::run_optimize_model(
model_id,
output.as_deref(),
Some(strategy),
&config,
&self.global,
)
.await
}
Commands::Batch {
input,
output_dir,
workers,
rate,
pitch,
volume,
quality,
resume,
} => {
commands::batch::run_batch_process(
commands::batch::BatchProcessArgs {
input,
output_dir: output_dir.as_deref(),
workers: *workers,
quality: (*quality).into(),
rate: *rate,
pitch: *pitch,
volume: *volume,
resume: *resume,
},
&config,
&self.global,
)
.await
}
Commands::Server { port, host } => {
commands::server::run_server(host, *port, &config).await
}
Commands::Interactive {
voice,
no_audio,
debug,
load_session,
auto_save,
} => {
let options = commands::interactive::InteractiveOptions {
voice: voice.clone(),
no_audio: *no_audio,
debug: *debug,
load_session: load_session.clone(),
auto_save: *auto_save,
};
commands::interactive::run_interactive(options)
.await
.map_err(Into::into)
}
Commands::Guide {
command,
getting_started,
examples,
} => {
let help_system = help::HelpSystem::new();
if *getting_started {
println!("{}", help::display_getting_started());
} else if *examples {
println!("{}", help_system.display_command_overview());
} else if let Some(cmd) = command {
println!("{}", help_system.display_command_help(cmd));
} else {
println!("{}", help_system.display_command_overview());
}
Ok(())
}
Commands::GenerateCompletion {
shell,
output,
install_help,
install_script,
status,
} => {
if *status {
println!("{}", completion::display_completion_status());
} else if *install_script {
println!("{}", completion::generate_install_script());
} else if *install_help {
println!("{}", completion::get_installation_instructions(*shell));
} else if let Some(output_path) = output {
completion::generate_completion_to_file(*shell, output_path).map_err(|e| {
voirs_sdk::VoirsError::IoError {
path: output_path.clone(),
operation: voirs_sdk::error::IoOperation::Write,
source: e,
}
})?;
println!("Completion script generated: {}", output_path.display());
} else {
completion::generate_completion_to_stdout(*shell).map_err(|e| {
voirs_sdk::VoirsError::IoError {
path: std::env::current_dir().unwrap_or_default(),
operation: voirs_sdk::error::IoOperation::Write,
source: e,
}
})?;
}
Ok(())
}
Commands::Dataset { command } => {
commands::dataset::execute_dataset_command(command, &config, &self.global).await
}
Commands::Dashboard { interval } => commands::dashboard::run_dashboard(*interval)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Dashboard".to_string(),
message: e.to_string(),
}),
Commands::Cloud { command } => {
commands::cloud::execute_cloud_command(command, &config, &self.global).await
}
Commands::Telemetry { command } => commands::telemetry::execute(command.clone())
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!("Telemetry command failed: {}", e))
}),
Commands::Lsp { verbose } => {
if *verbose {
eprintln!("Starting VoiRS LSP server in verbose mode...");
}
let server = crate::lsp::LspServer::new();
server.start().await.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!("LSP server failed: {}", e))
})
}
#[cfg(feature = "onnx")]
Commands::Kokoro { command } => {
commands::kokoro::execute_kokoro_command(command, &config, &self.global).await
}
#[cfg(feature = "onnx")]
Commands::Onnx { command } => commands::onnx_tools::handle_onnx_command(command)
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!("ONNX tool failed: {}", e))
}),
Commands::Accuracy { command } => {
commands::accuracy::execute_accuracy_command(command.clone())
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Accuracy command failed: {}",
e
))
})
}
Commands::Performance { command } => {
commands::performance::execute_performance_command(command.clone())
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Performance command failed: {}",
e
))
})
}
#[cfg(feature = "emotion")]
Commands::Emotion { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::emotion::execute_emotion_command(command.clone(), &output_formatter)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Emotion command failed: {}",
e
))
})
}
#[cfg(feature = "cloning")]
Commands::Clone { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::cloning::execute_cloning_command(command.clone(), &output_formatter)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Cloning command failed: {}",
e
))
})
}
#[cfg(feature = "conversion")]
Commands::Convert { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::conversion::execute_conversion_command(command.clone(), &output_formatter)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Conversion command failed: {}",
e
))
})
}
#[cfg(feature = "singing")]
Commands::Sing { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::singing::execute_singing_command(command.clone(), &output_formatter)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Singing command failed: {}",
e
))
})
}
#[cfg(feature = "spatial")]
Commands::Spatial { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::spatial::execute_spatial_command(command.clone(), &output_formatter)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Spatial command failed: {}",
e
))
})
}
Commands::Capabilities { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::capabilities::execute_capabilities_command(
command.clone(),
&output_formatter,
&config,
)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Capabilities command failed: {}",
e
))
})
}
Commands::Checkpoint { command } => {
commands::checkpoint::execute_checkpoint_command(command.clone(), &self.global)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Checkpoint command failed: {}",
e
))
})
}
Commands::Monitor { command } => {
use crate::output::OutputFormatter;
let output_formatter = OutputFormatter::new(!self.global.quiet, false);
commands::monitoring::execute_monitoring_command(
command.clone(),
&output_formatter,
&config,
)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!("Monitoring command failed: {}", e))
})
}
Commands::Train { command } => {
commands::train::execute_train_command(command.clone(), &self.global)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!("Train command failed: {}", e))
})
}
Commands::ConvertModel {
input,
output,
from,
model_type,
verify,
} => commands::convert_model::run_convert_model(
input.clone(),
output.clone(),
from.clone(),
model_type.clone(),
*verify,
&self.global,
)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!("Model conversion failed: {}", e))
}),
Commands::VocoderInfer {
checkpoint,
mel,
output,
steps,
quality,
batch_input,
batch_output,
metrics,
} => {
let config = commands::vocoder_inference::VocoderInferenceConfig {
checkpoint: checkpoint.as_path(),
mel_path: mel.as_deref(),
output: output.as_path(),
steps: *steps,
quality: quality.as_deref(),
batch_input: batch_input.as_ref(),
batch_output: batch_output.as_ref(),
show_metrics: *metrics,
};
commands::vocoder_inference::run_vocoder_inference(config, &self.global)
.await
.map_err(|e| {
voirs_sdk::VoirsError::config_error(format!(
"Vocoder inference failed: {}",
e
))
})
}
Commands::Stream {
text,
latency,
chunk_size,
buffer_chunks,
play,
} => {
commands::streaming::run_streaming_synthesis(
text.as_deref(),
*latency,
*chunk_size,
*buffer_chunks,
*play,
&config,
&self.global,
)
.await
}
Commands::ModelInspect {
model,
detailed,
export,
verify,
} => {
commands::model_inspect::run_model_inspect(
model,
*detailed,
export.as_ref(),
*verify,
&self.global,
)
.await
}
Commands::Export {
export_type,
source,
output,
include_weights,
} => {
commands::export_import::run_export(
export_type,
source,
output,
*include_weights,
&config,
&self.global,
)
.await
}
Commands::Import {
input,
name,
force,
validate,
} => {
commands::export_import::run_import(
input,
name.as_deref(),
*force,
*validate,
&config,
&self.global,
)
.await
}
Commands::History {
limit,
stats,
suggest,
clear,
} => commands::history::run_history(*limit, *stats, *suggest, *clear).await,
Commands::Workflow { command } => {
use commands::workflow::WorkflowCommands;
match command {
WorkflowCommands::Execute {
workflow_file,
variables,
max_parallel,
resume,
state_dir,
} => commands::workflow::run_workflow_execute(
workflow_file.clone(),
variables.clone(),
*max_parallel,
*resume,
state_dir.clone(),
)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Workflow".to_string(),
message: e.to_string(),
}),
WorkflowCommands::Validate {
workflow_file,
detailed,
format,
} => commands::workflow::run_workflow_validate(
workflow_file.clone(),
*detailed,
format.clone(),
)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Workflow".to_string(),
message: e.to_string(),
}),
WorkflowCommands::List {
registry_dir,
detailed,
} => commands::workflow::run_workflow_list(registry_dir.clone(), *detailed)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Workflow".to_string(),
message: e.to_string(),
}),
WorkflowCommands::Status {
workflow_name,
state_dir,
format,
} => commands::workflow::run_workflow_status(
workflow_name.clone(),
state_dir.clone(),
format.clone(),
)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Workflow".to_string(),
message: e.to_string(),
}),
WorkflowCommands::Resume {
workflow_name,
state_dir,
max_parallel,
} => commands::workflow::run_workflow_resume(
workflow_name.clone(),
state_dir.clone(),
*max_parallel,
)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Workflow".to_string(),
message: e.to_string(),
}),
WorkflowCommands::Stop {
workflow_name,
state_dir,
force,
} => commands::workflow::run_workflow_stop(
workflow_name.clone(),
state_dir.clone(),
*force,
)
.await
.map_err(|e| voirs_sdk::VoirsError::InternalError {
component: "Workflow".to_string(),
message: e.to_string(),
}),
}
}
Commands::Alias { command } => {
use commands::alias::AliasSubcommand;
let subcommand = match command {
AliasCommand::Add {
name,
command,
description,
} => AliasSubcommand::Add {
name: name.clone(),
command: command.clone(),
description: description.clone(),
},
AliasCommand::Remove { name } => AliasSubcommand::Remove { name: name.clone() },
AliasCommand::List => AliasSubcommand::List,
AliasCommand::Show { name } => AliasSubcommand::Show { name: name.clone() },
AliasCommand::Clear => AliasSubcommand::Clear,
};
commands::alias::run_alias(subcommand).await
}
}
}
}
pub mod utils {
use crate::cli_types::CliAudioFormat;
use std::path::Path;
use voirs_sdk::AudioFormat;
pub fn format_from_extension(path: &Path) -> Option<AudioFormat> {
path.extension()
.and_then(|ext| ext.to_str())
.and_then(|ext| match ext.to_lowercase().as_str() {
"wav" => Some(AudioFormat::Wav),
"flac" => Some(AudioFormat::Flac),
"mp3" => Some(AudioFormat::Mp3),
"opus" => Some(AudioFormat::Opus),
"ogg" => Some(AudioFormat::Ogg),
_ => None,
})
}
pub fn generate_output_filename(text: &str, format: AudioFormat) -> String {
let safe_text = text
.chars()
.take(30)
.filter(|c| c.is_alphanumeric() || c.is_whitespace())
.collect::<String>()
.replace(' ', "_")
.to_lowercase();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("SystemTime should be after UNIX_EPOCH")
.as_secs();
format!("voirs_{}_{}.{}", safe_text, timestamp, format.extension())
}
}
fn get_default_config_paths() -> Vec<std::path::PathBuf> {
let mut paths = Vec::new();
paths.push(
std::env::current_dir()
.unwrap_or_default()
.join("voirs.toml"),
);
paths.push(
std::env::current_dir()
.unwrap_or_default()
.join("voirs.json"),
);
paths.push(
std::env::current_dir()
.unwrap_or_default()
.join("voirs.yaml"),
);
if let Some(config_dir) = dirs::config_dir() {
let voirs_config_dir = config_dir.join("voirs");
paths.push(voirs_config_dir.join("config.toml"));
paths.push(voirs_config_dir.join("config.json"));
paths.push(voirs_config_dir.join("config.yaml"));
paths.push(voirs_config_dir.join("voirs.toml"));
paths.push(voirs_config_dir.join("voirs.json"));
paths.push(voirs_config_dir.join("voirs.yaml"));
}
if let Some(home_dir) = dirs::home_dir() {
paths.push(home_dir.join(".voirs.toml"));
paths.push(home_dir.join(".voirs.json"));
paths.push(home_dir.join(".voirs.yaml"));
paths.push(home_dir.join(".voirsrc"));
paths.push(home_dir.join(".config").join("voirs").join("config.toml"));
}
paths
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_from_extension() {
use std::path::Path;
assert_eq!(
utils::format_from_extension(Path::new("test.wav")),
Some(AudioFormat::Wav)
);
assert_eq!(
utils::format_from_extension(Path::new("test.flac")),
Some(AudioFormat::Flac)
);
assert_eq!(
utils::format_from_extension(Path::new("test.unknown")),
None
);
}
#[test]
fn test_generate_output_filename() {
let filename = utils::generate_output_filename("Hello World", AudioFormat::Wav);
assert!(filename.starts_with("voirs_hello_world_"));
assert!(filename.ends_with(".wav"));
}
}