1#![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};
39use clap::{Parser, Subcommand};
40use std::path::PathBuf;
41use voirs_sdk::config::{AppConfig, PipelineConfig};
42use voirs_sdk::{AudioFormat, QualityLevel, Result, VoirsPipeline};
43
44pub mod audio;
45pub mod cli_types;
46pub mod cloud;
47pub mod commands;
48pub mod completion;
49pub mod config;
50pub mod error;
51pub mod help;
52pub mod lsp;
53pub mod model_types;
54pub mod output;
55pub mod packaging;
56pub mod performance;
57pub mod platform;
58pub mod plugins;
59pub mod progress;
60pub mod ssml;
61pub mod synthesis;
62pub mod telemetry;
63pub mod validation;
64pub mod workflow;
65
66#[derive(Parser)]
70#[command(name = "voirs")]
71#[command(about = "A pure Rust text-to-speech synthesis framework")]
72#[command(version = env!("CARGO_PKG_VERSION"))]
73pub struct CliApp {
74 #[command(flatten)]
76 pub global: GlobalOptions,
77
78 #[command(subcommand)]
80 pub command: Commands,
81}
82
83#[derive(Parser)]
85pub struct GlobalOptions {
86 #[arg(short, long)]
88 pub config: Option<PathBuf>,
89
90 #[arg(short, long, action = clap::ArgAction::Count)]
92 pub verbose: u8,
93
94 #[arg(short, long)]
96 pub quiet: bool,
97
98 #[arg(long)]
100 pub format: Option<CliAudioFormat>,
101
102 #[arg(long)]
104 pub voice: Option<String>,
105
106 #[arg(long)]
108 pub gpu: bool,
109
110 #[arg(long)]
112 pub threads: Option<usize>,
113}
114
115#[derive(Subcommand)]
117pub enum CloudCommands {
118 Sync {
120 #[arg(long)]
122 force: bool,
123
124 #[arg(long)]
126 directory: Option<PathBuf>,
127
128 #[arg(long)]
130 dry_run: bool,
131 },
132
133 AddToSync {
135 local_path: PathBuf,
137
138 remote_path: String,
140
141 #[arg(long, default_value = "bidirectional")]
143 direction: String,
144 },
145
146 StorageStats,
148
149 CleanupCache {
151 #[arg(long, default_value = "30")]
153 max_age_days: u32,
154
155 #[arg(long)]
157 dry_run: bool,
158 },
159
160 Translate {
162 text: String,
164
165 #[arg(long)]
167 from: String,
168
169 #[arg(long)]
171 to: String,
172
173 #[arg(long, default_value = "balanced")]
175 quality: String,
176 },
177
178 AnalyzeContent {
180 text: String,
182
183 #[arg(long, default_value = "sentiment,entities")]
185 analysis_types: String,
186
187 #[arg(long)]
189 language: Option<String>,
190 },
191
192 AssessQuality {
194 audio_file: PathBuf,
196
197 text: String,
199
200 #[arg(long, default_value = "naturalness,intelligibility,overall")]
202 metrics: String,
203 },
204
205 HealthCheck,
207
208 Configure {
210 #[arg(long)]
212 show: bool,
213
214 #[arg(long)]
216 storage_provider: Option<String>,
217
218 #[arg(long)]
220 api_url: Option<String>,
221
222 #[arg(long)]
224 enable_service: Option<String>,
225
226 #[arg(long)]
228 init: bool,
229 },
230}
231
232#[derive(Subcommand)]
234pub enum DatasetCommands {
235 Validate {
237 path: PathBuf,
239
240 #[arg(long)]
242 dataset_type: Option<String>,
243
244 #[arg(long)]
246 detailed: bool,
247 },
248
249 Convert {
251 input: PathBuf,
253
254 output: PathBuf,
256
257 #[arg(long)]
259 from: String,
260
261 #[arg(long)]
263 to: String,
264 },
265
266 Split {
268 path: PathBuf,
270
271 #[arg(long, default_value = "0.8")]
273 train_ratio: f32,
274
275 #[arg(long, default_value = "0.1")]
277 val_ratio: f32,
278
279 #[arg(long)]
281 test_ratio: Option<f32>,
282
283 #[arg(long)]
285 seed: Option<u64>,
286 },
287
288 Preprocess {
290 input: PathBuf,
292
293 output: PathBuf,
295
296 #[arg(long, default_value = "22050")]
298 sample_rate: u32,
299
300 #[arg(long)]
302 normalize: bool,
303
304 #[arg(long)]
306 filter: bool,
307 },
308
309 Analyze {
311 path: PathBuf,
313
314 #[arg(short, long)]
316 output: Option<PathBuf>,
317
318 #[arg(long)]
320 detailed: bool,
321 },
322}
323
324#[derive(Subcommand)]
326pub enum Commands {
327 Synthesize {
329 text: String,
331
332 output: Option<PathBuf>,
334
335 #[arg(long, default_value = "1.0")]
337 rate: f32,
338
339 #[arg(long, default_value = "0.0")]
341 pitch: f32,
342
343 #[arg(long, default_value = "0.0")]
345 volume: f32,
346
347 #[arg(long, default_value = "high")]
349 quality: CliQualityLevel,
350
351 #[arg(long)]
353 enhance: bool,
354
355 #[arg(short, long)]
357 play: bool,
358
359 #[arg(long)]
361 auto_detect: bool,
362 },
363
364 SynthesizeFile {
366 input: PathBuf,
368
369 #[arg(short, long)]
371 output_dir: Option<PathBuf>,
372
373 #[arg(long, default_value = "1.0")]
375 rate: f32,
376
377 #[arg(long, default_value = "high")]
379 quality: CliQualityLevel,
380 },
381
382 ListVoices {
384 #[arg(long)]
386 language: Option<String>,
387
388 #[arg(long)]
390 detailed: bool,
391 },
392
393 VoiceInfo {
395 voice_id: String,
397 },
398
399 DownloadVoice {
401 voice_id: String,
403
404 #[arg(long)]
406 force: bool,
407 },
408
409 PreviewVoice {
411 voice_id: String,
413
414 #[arg(long)]
416 text: Option<String>,
417
418 #[arg(short, long)]
420 output: Option<PathBuf>,
421
422 #[arg(long)]
424 no_play: bool,
425 },
426
427 CompareVoices {
429 voice_ids: Vec<String>,
431 },
432
433 Test {
435 #[arg(default_value = "Hello, this is a test of VoiRS speech synthesis.")]
437 text: String,
438
439 #[arg(long)]
441 play: bool,
442 },
443
444 CrossLangTest {
446 #[arg(long, default_value = "json")]
448 format: String,
449
450 #[arg(long)]
452 save_report: bool,
453 },
454
455 TestApi {
457 server_url: String,
459
460 #[arg(long)]
462 api_key: Option<String>,
463
464 #[arg(long)]
466 concurrent: Option<usize>,
467
468 #[arg(long)]
470 report: Option<String>,
471
472 #[arg(long)]
474 verbose: bool,
475 },
476
477 Config {
479 #[arg(long)]
481 show: bool,
482
483 #[arg(long)]
485 init: bool,
486
487 #[arg(long)]
489 path: Option<PathBuf>,
490 },
491
492 ListModels {
494 #[arg(long)]
496 backend: Option<String>,
497
498 #[arg(long)]
500 detailed: bool,
501 },
502
503 DownloadModel {
505 model_id: String,
507
508 #[arg(long)]
510 force: bool,
511 },
512
513 BenchmarkModels {
515 model_ids: Vec<String>,
517
518 #[arg(short, long, default_value = "3")]
520 iterations: u32,
521
522 #[arg(long)]
524 accuracy: bool,
525 },
526
527 OptimizeModel {
529 model_id: String,
531
532 #[arg(short, long)]
534 output: Option<String>,
535
536 #[arg(long, default_value = "balanced")]
538 strategy: String,
539 },
540
541 Batch {
543 input: PathBuf,
545
546 #[arg(short, long)]
548 output_dir: Option<PathBuf>,
549
550 #[arg(short, long)]
552 workers: Option<usize>,
553
554 #[arg(long, default_value = "1.0")]
556 rate: f32,
557
558 #[arg(long, default_value = "0.0")]
560 pitch: f32,
561
562 #[arg(long, default_value = "0.0")]
564 volume: f32,
565
566 #[arg(long, default_value = "high")]
568 quality: CliQualityLevel,
569
570 #[arg(long)]
572 resume: bool,
573 },
574
575 Server {
577 #[arg(short, long, default_value = "8080")]
579 port: u16,
580
581 #[arg(long, default_value = "127.0.0.1")]
583 host: String,
584 },
585
586 Interactive {
588 #[arg(short, long)]
590 voice: Option<String>,
591
592 #[arg(long)]
594 no_audio: bool,
595
596 #[arg(long)]
598 debug: bool,
599
600 #[arg(long)]
602 load_session: Option<PathBuf>,
603
604 #[arg(long)]
606 auto_save: bool,
607 },
608
609 Guide {
611 command: Option<String>,
613
614 #[arg(long)]
616 getting_started: bool,
617
618 #[arg(long)]
620 examples: bool,
621 },
622
623 GenerateCompletion {
625 #[arg(value_enum)]
627 shell: clap_complete::Shell,
628
629 #[arg(short, long)]
631 output: Option<std::path::PathBuf>,
632
633 #[arg(long)]
635 install_help: bool,
636
637 #[arg(long)]
639 install_script: bool,
640
641 #[arg(long)]
643 status: bool,
644 },
645
646 Dataset {
648 #[command(subcommand)]
650 command: DatasetCommands,
651 },
652
653 Dashboard {
655 #[arg(short, long, default_value = "500")]
657 interval: u64,
658 },
659
660 Cloud {
662 #[command(subcommand)]
664 command: CloudCommands,
665 },
666
667 Telemetry {
669 #[command(subcommand)]
671 command: commands::telemetry::TelemetryCommands,
672 },
673
674 Lsp {
676 #[arg(long)]
678 verbose: bool,
679 },
680
681 #[cfg(feature = "onnx")]
683 Kokoro {
684 #[command(subcommand)]
686 command: commands::kokoro::KokoroCommands,
687 },
688
689 Accuracy {
691 #[command(flatten)]
693 command: commands::accuracy::AccuracyCommand,
694 },
695
696 Performance {
698 #[command(flatten)]
700 command: commands::performance::PerformanceCommand,
701 },
702
703 #[cfg(feature = "emotion")]
705 Emotion {
706 #[command(subcommand)]
708 command: commands::emotion::EmotionCommand,
709 },
710
711 #[cfg(feature = "cloning")]
713 Clone {
714 #[command(subcommand)]
716 command: commands::cloning::CloningCommand,
717 },
718
719 #[cfg(feature = "conversion")]
721 Convert {
722 #[command(subcommand)]
724 command: commands::conversion::ConversionCommand,
725 },
726
727 #[cfg(feature = "singing")]
729 Sing {
730 #[command(subcommand)]
732 command: commands::singing::SingingCommand,
733 },
734
735 #[cfg(feature = "spatial")]
737 Spatial {
738 #[command(subcommand)]
740 command: commands::spatial::SpatialCommand,
741 },
742
743 Capabilities {
745 #[command(subcommand)]
747 command: commands::capabilities::CapabilitiesCommand,
748 },
749
750 Checkpoint {
752 #[command(subcommand)]
754 command: commands::checkpoint::CheckpointCommands,
755 },
756
757 Monitor {
759 #[command(subcommand)]
761 command: commands::monitoring::MonitoringCommand,
762 },
763
764 Train {
766 #[command(subcommand)]
768 command: commands::train::TrainCommands,
769 },
770
771 ConvertModel {
773 input: PathBuf,
775
776 #[arg(short, long)]
778 output: PathBuf,
779
780 #[arg(long)]
782 from: Option<String>,
783
784 #[arg(long)]
786 model_type: String,
787
788 #[arg(long)]
790 verify: bool,
791 },
792
793 VocoderInfer {
795 checkpoint: PathBuf,
797
798 #[arg(long)]
800 mel: Option<PathBuf>,
801
802 #[arg(short, long, default_value = "vocoder_output.wav")]
804 output: PathBuf,
805
806 #[arg(long, default_value = "50")]
808 steps: usize,
809
810 #[arg(long)]
812 quality: Option<String>,
813
814 #[arg(long)]
816 batch_input: Option<PathBuf>,
817
818 #[arg(long)]
820 batch_output: Option<PathBuf>,
821
822 #[arg(long)]
824 metrics: bool,
825 },
826
827 Stream {
829 text: Option<String>,
831
832 #[arg(long, default_value = "100")]
834 latency: u64,
835
836 #[arg(long, default_value = "512")]
838 chunk_size: usize,
839
840 #[arg(long, default_value = "4")]
842 buffer_chunks: usize,
843
844 #[arg(long)]
846 play: bool,
847 },
848
849 ModelInspect {
851 model: PathBuf,
853
854 #[arg(long)]
856 detailed: bool,
857
858 #[arg(long)]
860 export: Option<PathBuf>,
861
862 #[arg(long)]
864 verify: bool,
865 },
866
867 Export {
869 #[arg(long)]
871 export_type: String,
872
873 source: String,
875
876 #[arg(short, long)]
878 output: PathBuf,
879
880 #[arg(long)]
882 include_weights: bool,
883 },
884
885 Import {
887 input: PathBuf,
889
890 #[arg(long)]
892 name: Option<String>,
893
894 #[arg(long)]
896 force: bool,
897
898 #[arg(long, default_value = "true")]
900 validate: bool,
901 },
902
903 History {
905 #[arg(short = 'n', long, default_value = "20")]
907 limit: usize,
908
909 #[arg(long)]
911 stats: bool,
912
913 #[arg(long)]
915 suggest: bool,
916
917 #[arg(long)]
919 clear: bool,
920 },
921
922 Workflow {
924 #[command(subcommand)]
926 command: commands::workflow::WorkflowCommands,
927 },
928
929 Alias {
931 #[command(subcommand)]
933 command: AliasCommand,
934 },
935}
936
937#[derive(Subcommand)]
939pub enum AliasCommand {
940 Add {
942 name: String,
944
945 command: String,
947
948 #[arg(short, long)]
950 description: Option<String>,
951 },
952
953 Remove {
955 name: String,
957 },
958
959 List,
961
962 Show {
964 name: String,
966 },
967
968 Clear,
970}
971
972impl CliApp {
974 pub async fn run() -> Result<()> {
976 let app = Self::parse();
977
978 app.init_logging()?;
980
981 let config = app.load_config().await?;
983
984 app.execute_command(config).await
986 }
987
988 fn init_logging(&self) -> Result<()> {
990 let level = if self.global.quiet {
991 tracing::Level::ERROR
992 } else {
993 match self.global.verbose {
994 0 => tracing::Level::INFO,
995 1 => tracing::Level::DEBUG,
996 _ => tracing::Level::TRACE,
997 }
998 };
999
1000 tracing_subscriber::fmt()
1001 .with_max_level(level)
1002 .with_target(false)
1003 .with_writer(std::io::stderr) .init();
1005
1006 Ok(())
1007 }
1008
1009 async fn load_config(&self) -> Result<AppConfig> {
1011 let mut config = if let Some(config_path) = &self.global.config {
1012 tracing::info!("Loading configuration from {:?}", config_path);
1013 self.load_config_from_file(config_path).await?
1014 } else {
1015 self.load_config_from_default_locations().await?
1017 };
1018
1019 self.apply_cli_overrides(&mut config);
1021
1022 Ok(config)
1023 }
1024
1025 async fn load_config_from_file(&self, config_path: &std::path::Path) -> Result<AppConfig> {
1027 if !config_path.exists() {
1028 tracing::warn!(
1029 "Configuration file not found: {}, using defaults",
1030 config_path.display()
1031 );
1032 return Ok(AppConfig::default());
1033 }
1034
1035 let content =
1036 std::fs::read_to_string(config_path).map_err(|e| voirs_sdk::VoirsError::IoError {
1037 path: config_path.to_path_buf(),
1038 operation: voirs_sdk::error::IoOperation::Read,
1039 source: e,
1040 })?;
1041
1042 let config = match config_path.extension().and_then(|ext| ext.to_str()) {
1044 Some("toml") => {
1045 toml::from_str(&content).or_else(|_| {
1047 self.parse_config_auto_detect(&content)
1049 })?
1050 }
1051 Some("json") => {
1052 serde_json::from_str(&content).or_else(|_| {
1054 self.parse_config_auto_detect(&content)
1056 })?
1057 }
1058 Some("yaml") | Some("yml") => {
1059 serde_yaml::from_str(&content).or_else(|_| {
1061 self.parse_config_auto_detect(&content)
1063 })?
1064 }
1065 _ => {
1066 self.parse_config_auto_detect(&content)?
1068 }
1069 };
1070
1071 tracing::info!(
1072 "Successfully loaded configuration from {}",
1073 config_path.display()
1074 );
1075 Ok(config)
1076 }
1077
1078 async fn load_config_from_default_locations(&self) -> Result<AppConfig> {
1080 let possible_paths = get_default_config_paths();
1081
1082 for path in possible_paths {
1083 if path.exists() {
1084 tracing::info!("Found configuration file at: {}", path.display());
1085 return self.load_config_from_file(&path).await;
1086 }
1087 }
1088
1089 tracing::info!("No configuration file found, using defaults");
1091 Ok(AppConfig::default())
1092 }
1093
1094 fn parse_config_auto_detect(&self, content: &str) -> Result<AppConfig> {
1096 let trimmed = content.trim_start();
1099
1100 if trimmed.starts_with('{') {
1101 serde_json::from_str(content).map_err(|e| {
1103 voirs_sdk::VoirsError::config_error(format!(
1104 "Failed to parse JSON configuration: {}",
1105 e
1106 ))
1107 })
1108 } else if trimmed.contains("---") || content.contains(": ") {
1109 serde_yaml::from_str(content).or_else(|yaml_err| {
1111 toml::from_str(content).map_err(|toml_err| {
1113 voirs_sdk::VoirsError::config_error(format!(
1114 "Failed to parse configuration. YAML error: {}, TOML error: {}",
1115 yaml_err, toml_err
1116 ))
1117 })
1118 })
1119 } else {
1120 toml::from_str(content)
1122 .or_else(|_| serde_json::from_str(content))
1123 .or_else(|_| serde_yaml::from_str(content))
1124 .map_err(|e| {
1125 voirs_sdk::VoirsError::config_error(format!(
1126 "Unable to parse configuration file. Supported formats: TOML, JSON, YAML. Last error: {}", e
1127 ))
1128 })
1129 }
1130 }
1131
1132 fn apply_cli_overrides(&self, config: &mut AppConfig) {
1134 if self.global.gpu {
1135 config.pipeline.use_gpu = true;
1136 }
1137
1138 if let Some(threads) = self.global.threads {
1139 config.pipeline.num_threads = Some(threads);
1140 }
1141
1142 if let Some(ref voice) = self.global.voice {
1143 config.cli.default_voice = Some(voice.clone());
1144 }
1145
1146 if let Some(ref format) = self.global.format {
1147 config.cli.default_format = (*format).into();
1148 config.pipeline.default_synthesis.output_format = (*format).into();
1150 }
1151 }
1152
1153 async fn execute_command(&self, config: AppConfig) -> Result<()> {
1155 match &self.command {
1156 Commands::Synthesize {
1157 text,
1158 output,
1159 rate,
1160 pitch,
1161 volume,
1162 quality,
1163 enhance,
1164 play,
1165 auto_detect,
1166 } => {
1167 let args = commands::synthesize::SynthesizeArgs {
1168 text,
1169 output: output.as_deref(),
1170 rate: *rate,
1171 pitch: *pitch,
1172 volume: *volume,
1173 quality: (*quality).into(),
1174 enhance: *enhance,
1175 play: *play,
1176 auto_detect: *auto_detect,
1177 };
1178 commands::synthesize::run_synthesize(args, &config, &self.global).await
1179 }
1180
1181 Commands::SynthesizeFile {
1182 input,
1183 output_dir,
1184 rate,
1185 quality,
1186 } => {
1187 commands::synthesize::run_synthesize_file(
1188 input,
1189 output_dir.as_deref(),
1190 *rate,
1191 (*quality).into(),
1192 &config,
1193 &self.global,
1194 )
1195 .await
1196 }
1197
1198 Commands::ListVoices { language, detailed } => {
1199 commands::voices::run_list_voices(language.as_deref(), *detailed, &config).await
1200 }
1201
1202 Commands::VoiceInfo { voice_id } => {
1203 commands::voices::run_voice_info(voice_id, &config).await
1204 }
1205
1206 Commands::DownloadVoice { voice_id, force } => {
1207 commands::voices::run_download_voice(voice_id, *force, &config).await
1208 }
1209
1210 Commands::PreviewVoice {
1211 voice_id,
1212 text,
1213 output,
1214 no_play,
1215 } => {
1216 commands::voices::run_preview_voice(
1217 voice_id,
1218 text.as_deref(),
1219 output.as_ref(),
1220 *no_play,
1221 &config,
1222 &self.global,
1223 )
1224 .await
1225 }
1226
1227 Commands::CompareVoices { voice_ids } => {
1228 commands::voices::run_compare_voices(voice_ids.clone(), &config).await
1229 }
1230
1231 Commands::Test { text, play } => {
1232 commands::test::run_test(text, *play, &config, &self.global).await
1233 }
1234
1235 Commands::CrossLangTest {
1236 format,
1237 save_report,
1238 } => {
1239 commands::cross_lang_test::run_cross_lang_tests(
1240 format,
1241 *save_report,
1242 &config,
1243 &self.global,
1244 )
1245 .await
1246 }
1247
1248 Commands::TestApi {
1249 server_url,
1250 api_key,
1251 concurrent,
1252 report,
1253 verbose,
1254 } => commands::test_api::run_api_tests(
1255 server_url.clone(),
1256 api_key.clone(),
1257 *concurrent,
1258 report.clone(),
1259 *verbose,
1260 )
1261 .await
1262 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1263 component: "API Tester".to_string(),
1264 message: e.to_string(),
1265 }),
1266
1267 Commands::Config { show, init, path } => {
1268 commands::config::run_config(*show, *init, path.as_deref(), &config).await
1269 }
1270
1271 Commands::ListModels { backend, detailed } => {
1272 commands::models::run_list_models(
1273 backend.as_deref(),
1274 *detailed,
1275 &config,
1276 &self.global,
1277 )
1278 .await
1279 }
1280
1281 Commands::DownloadModel { model_id, force } => {
1282 commands::models::run_download_model(model_id, *force, &config, &self.global).await
1283 }
1284
1285 Commands::BenchmarkModels {
1286 model_ids,
1287 iterations,
1288 accuracy,
1289 } => {
1290 commands::models::run_benchmark_models(
1291 model_ids,
1292 *iterations,
1293 *accuracy,
1294 &config,
1295 &self.global,
1296 )
1297 .await
1298 }
1299
1300 Commands::OptimizeModel {
1301 model_id,
1302 output,
1303 strategy,
1304 } => {
1305 commands::models::run_optimize_model(
1306 model_id,
1307 output.as_deref(),
1308 Some(strategy),
1309 &config,
1310 &self.global,
1311 )
1312 .await
1313 }
1314
1315 Commands::Batch {
1316 input,
1317 output_dir,
1318 workers,
1319 rate,
1320 pitch,
1321 volume,
1322 quality,
1323 resume,
1324 } => {
1325 commands::batch::run_batch_process(
1326 commands::batch::BatchProcessArgs {
1327 input,
1328 output_dir: output_dir.as_deref(),
1329 workers: *workers,
1330 quality: (*quality).into(),
1331 rate: *rate,
1332 pitch: *pitch,
1333 volume: *volume,
1334 resume: *resume,
1335 },
1336 &config,
1337 &self.global,
1338 )
1339 .await
1340 }
1341
1342 Commands::Server { port, host } => {
1343 commands::server::run_server(host, *port, &config).await
1344 }
1345
1346 Commands::Interactive {
1347 voice,
1348 no_audio,
1349 debug,
1350 load_session,
1351 auto_save,
1352 } => {
1353 let options = commands::interactive::InteractiveOptions {
1354 voice: voice.clone(),
1355 no_audio: *no_audio,
1356 debug: *debug,
1357 load_session: load_session.clone(),
1358 auto_save: *auto_save,
1359 };
1360
1361 commands::interactive::run_interactive(options)
1362 .await
1363 .map_err(Into::into)
1364 }
1365
1366 Commands::Guide {
1367 command,
1368 getting_started,
1369 examples,
1370 } => {
1371 let help_system = help::HelpSystem::new();
1372
1373 if *getting_started {
1374 println!("{}", help::display_getting_started());
1375 } else if *examples {
1376 println!("{}", help_system.display_command_overview());
1377 } else if let Some(cmd) = command {
1378 println!("{}", help_system.display_command_help(cmd));
1379 } else {
1380 println!("{}", help_system.display_command_overview());
1381 }
1382
1383 Ok(())
1384 }
1385
1386 Commands::GenerateCompletion {
1387 shell,
1388 output,
1389 install_help,
1390 install_script,
1391 status,
1392 } => {
1393 if *status {
1394 println!("{}", completion::display_completion_status());
1395 } else if *install_script {
1396 println!("{}", completion::generate_install_script());
1397 } else if *install_help {
1398 println!("{}", completion::get_installation_instructions(*shell));
1399 } else if let Some(output_path) = output {
1400 completion::generate_completion_to_file(*shell, output_path).map_err(|e| {
1401 voirs_sdk::VoirsError::IoError {
1402 path: output_path.clone(),
1403 operation: voirs_sdk::error::IoOperation::Write,
1404 source: e,
1405 }
1406 })?;
1407 println!("Completion script generated: {}", output_path.display());
1408 } else {
1409 completion::generate_completion_to_stdout(*shell).map_err(|e| {
1410 voirs_sdk::VoirsError::IoError {
1411 path: std::env::current_dir().unwrap_or_default(),
1412 operation: voirs_sdk::error::IoOperation::Write,
1413 source: e,
1414 }
1415 })?;
1416 }
1417
1418 Ok(())
1419 }
1420
1421 Commands::Dataset { command } => {
1422 commands::dataset::execute_dataset_command(command, &config, &self.global).await
1423 }
1424
1425 Commands::Dashboard { interval } => commands::dashboard::run_dashboard(*interval)
1426 .await
1427 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1428 component: "Dashboard".to_string(),
1429 message: e.to_string(),
1430 }),
1431
1432 Commands::Cloud { command } => {
1433 commands::cloud::execute_cloud_command(command, &config, &self.global).await
1434 }
1435
1436 Commands::Telemetry { command } => commands::telemetry::execute(command.clone())
1437 .await
1438 .map_err(|e| {
1439 voirs_sdk::VoirsError::config_error(format!("Telemetry command failed: {}", e))
1440 }),
1441
1442 Commands::Lsp { verbose } => {
1443 if *verbose {
1444 eprintln!("Starting VoiRS LSP server in verbose mode...");
1445 }
1446
1447 let server = crate::lsp::LspServer::new();
1448 server.start().await.map_err(|e| {
1449 voirs_sdk::VoirsError::config_error(format!("LSP server failed: {}", e))
1450 })
1451 }
1452
1453 #[cfg(feature = "onnx")]
1454 Commands::Kokoro { command } => {
1455 commands::kokoro::execute_kokoro_command(command, &config, &self.global).await
1456 }
1457
1458 Commands::Accuracy { command } => {
1459 commands::accuracy::execute_accuracy_command(command.clone())
1460 .await
1461 .map_err(|e| {
1462 voirs_sdk::VoirsError::config_error(format!(
1463 "Accuracy command failed: {}",
1464 e
1465 ))
1466 })
1467 }
1468
1469 Commands::Performance { command } => {
1470 commands::performance::execute_performance_command(command.clone())
1471 .await
1472 .map_err(|e| {
1473 voirs_sdk::VoirsError::config_error(format!(
1474 "Performance command failed: {}",
1475 e
1476 ))
1477 })
1478 }
1479
1480 #[cfg(feature = "emotion")]
1481 Commands::Emotion { command } => {
1482 use crate::output::OutputFormatter;
1483 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1484 commands::emotion::execute_emotion_command(command.clone(), &output_formatter)
1485 .await
1486 .map_err(|e| {
1487 voirs_sdk::VoirsError::config_error(format!(
1488 "Emotion command failed: {}",
1489 e
1490 ))
1491 })
1492 }
1493
1494 #[cfg(feature = "cloning")]
1495 Commands::Clone { command } => {
1496 use crate::output::OutputFormatter;
1497 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1498 commands::cloning::execute_cloning_command(command.clone(), &output_formatter)
1499 .await
1500 .map_err(|e| {
1501 voirs_sdk::VoirsError::config_error(format!(
1502 "Cloning command failed: {}",
1503 e
1504 ))
1505 })
1506 }
1507
1508 #[cfg(feature = "conversion")]
1509 Commands::Convert { command } => {
1510 use crate::output::OutputFormatter;
1511 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1512 commands::conversion::execute_conversion_command(command.clone(), &output_formatter)
1513 .await
1514 .map_err(|e| {
1515 voirs_sdk::VoirsError::config_error(format!(
1516 "Conversion command failed: {}",
1517 e
1518 ))
1519 })
1520 }
1521
1522 #[cfg(feature = "singing")]
1523 Commands::Sing { command } => {
1524 use crate::output::OutputFormatter;
1525 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1526 commands::singing::execute_singing_command(command.clone(), &output_formatter)
1527 .await
1528 .map_err(|e| {
1529 voirs_sdk::VoirsError::config_error(format!(
1530 "Singing command failed: {}",
1531 e
1532 ))
1533 })
1534 }
1535
1536 #[cfg(feature = "spatial")]
1537 Commands::Spatial { command } => {
1538 use crate::output::OutputFormatter;
1539 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1540 commands::spatial::execute_spatial_command(command.clone(), &output_formatter)
1541 .await
1542 .map_err(|e| {
1543 voirs_sdk::VoirsError::config_error(format!(
1544 "Spatial command failed: {}",
1545 e
1546 ))
1547 })
1548 }
1549
1550 Commands::Capabilities { command } => {
1551 use crate::output::OutputFormatter;
1552 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1553 commands::capabilities::execute_capabilities_command(
1554 command.clone(),
1555 &output_formatter,
1556 &config,
1557 )
1558 .await
1559 .map_err(|e| {
1560 voirs_sdk::VoirsError::config_error(format!(
1561 "Capabilities command failed: {}",
1562 e
1563 ))
1564 })
1565 }
1566
1567 Commands::Checkpoint { command } => {
1568 commands::checkpoint::execute_checkpoint_command(command.clone(), &self.global)
1569 .await
1570 .map_err(|e| {
1571 voirs_sdk::VoirsError::config_error(format!(
1572 "Checkpoint command failed: {}",
1573 e
1574 ))
1575 })
1576 }
1577
1578 Commands::Monitor { command } => {
1579 use crate::output::OutputFormatter;
1580 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1581 commands::monitoring::execute_monitoring_command(
1582 command.clone(),
1583 &output_formatter,
1584 &config,
1585 )
1586 .await
1587 .map_err(|e| {
1588 voirs_sdk::VoirsError::config_error(format!("Monitoring command failed: {}", e))
1589 })
1590 }
1591
1592 Commands::Train { command } => {
1593 commands::train::execute_train_command(command.clone(), &self.global)
1594 .await
1595 .map_err(|e| {
1596 voirs_sdk::VoirsError::config_error(format!("Train command failed: {}", e))
1597 })
1598 }
1599
1600 Commands::ConvertModel {
1601 input,
1602 output,
1603 from,
1604 model_type,
1605 verify,
1606 } => commands::convert_model::run_convert_model(
1607 input.clone(),
1608 output.clone(),
1609 from.clone(),
1610 model_type.clone(),
1611 *verify,
1612 &self.global,
1613 )
1614 .await
1615 .map_err(|e| {
1616 voirs_sdk::VoirsError::config_error(format!("Model conversion failed: {}", e))
1617 }),
1618
1619 Commands::VocoderInfer {
1620 checkpoint,
1621 mel,
1622 output,
1623 steps,
1624 quality,
1625 batch_input,
1626 batch_output,
1627 metrics,
1628 } => {
1629 let config = commands::vocoder_inference::VocoderInferenceConfig {
1630 checkpoint: checkpoint.as_path(),
1631 mel_path: mel.as_deref(),
1632 output: output.as_path(),
1633 steps: *steps,
1634 quality: quality.as_deref(),
1635 batch_input: batch_input.as_ref(),
1636 batch_output: batch_output.as_ref(),
1637 show_metrics: *metrics,
1638 };
1639 commands::vocoder_inference::run_vocoder_inference(config, &self.global)
1640 .await
1641 .map_err(|e| {
1642 voirs_sdk::VoirsError::config_error(format!(
1643 "Vocoder inference failed: {}",
1644 e
1645 ))
1646 })
1647 }
1648
1649 Commands::Stream {
1650 text,
1651 latency,
1652 chunk_size,
1653 buffer_chunks,
1654 play,
1655 } => {
1656 commands::streaming::run_streaming_synthesis(
1657 text.as_deref(),
1658 *latency,
1659 *chunk_size,
1660 *buffer_chunks,
1661 *play,
1662 &config,
1663 &self.global,
1664 )
1665 .await
1666 }
1667
1668 Commands::ModelInspect {
1669 model,
1670 detailed,
1671 export,
1672 verify,
1673 } => {
1674 commands::model_inspect::run_model_inspect(
1675 model,
1676 *detailed,
1677 export.as_ref(),
1678 *verify,
1679 &self.global,
1680 )
1681 .await
1682 }
1683
1684 Commands::Export {
1685 export_type,
1686 source,
1687 output,
1688 include_weights,
1689 } => {
1690 commands::export_import::run_export(
1691 export_type,
1692 source,
1693 output,
1694 *include_weights,
1695 &config,
1696 &self.global,
1697 )
1698 .await
1699 }
1700
1701 Commands::Import {
1702 input,
1703 name,
1704 force,
1705 validate,
1706 } => {
1707 commands::export_import::run_import(
1708 input,
1709 name.as_deref(),
1710 *force,
1711 *validate,
1712 &config,
1713 &self.global,
1714 )
1715 .await
1716 }
1717
1718 Commands::History {
1719 limit,
1720 stats,
1721 suggest,
1722 clear,
1723 } => commands::history::run_history(*limit, *stats, *suggest, *clear).await,
1724
1725 Commands::Workflow { command } => {
1726 use commands::workflow::WorkflowCommands;
1727 match command {
1728 WorkflowCommands::Execute {
1729 workflow_file,
1730 variables,
1731 max_parallel,
1732 resume,
1733 state_dir,
1734 } => commands::workflow::run_workflow_execute(
1735 workflow_file.clone(),
1736 variables.clone(),
1737 *max_parallel,
1738 *resume,
1739 state_dir.clone(),
1740 )
1741 .await
1742 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1743 component: "Workflow".to_string(),
1744 message: e.to_string(),
1745 }),
1746 WorkflowCommands::Validate {
1747 workflow_file,
1748 detailed,
1749 format,
1750 } => commands::workflow::run_workflow_validate(
1751 workflow_file.clone(),
1752 *detailed,
1753 format.clone(),
1754 )
1755 .await
1756 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1757 component: "Workflow".to_string(),
1758 message: e.to_string(),
1759 }),
1760 WorkflowCommands::List {
1761 registry_dir,
1762 detailed,
1763 } => commands::workflow::run_workflow_list(registry_dir.clone(), *detailed)
1764 .await
1765 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1766 component: "Workflow".to_string(),
1767 message: e.to_string(),
1768 }),
1769 WorkflowCommands::Status {
1770 workflow_name,
1771 state_dir,
1772 format,
1773 } => commands::workflow::run_workflow_status(
1774 workflow_name.clone(),
1775 state_dir.clone(),
1776 format.clone(),
1777 )
1778 .await
1779 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1780 component: "Workflow".to_string(),
1781 message: e.to_string(),
1782 }),
1783 WorkflowCommands::Resume {
1784 workflow_name,
1785 state_dir,
1786 max_parallel,
1787 } => commands::workflow::run_workflow_resume(
1788 workflow_name.clone(),
1789 state_dir.clone(),
1790 *max_parallel,
1791 )
1792 .await
1793 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1794 component: "Workflow".to_string(),
1795 message: e.to_string(),
1796 }),
1797 WorkflowCommands::Stop {
1798 workflow_name,
1799 state_dir,
1800 force,
1801 } => commands::workflow::run_workflow_stop(
1802 workflow_name.clone(),
1803 state_dir.clone(),
1804 *force,
1805 )
1806 .await
1807 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1808 component: "Workflow".to_string(),
1809 message: e.to_string(),
1810 }),
1811 }
1812 }
1813
1814 Commands::Alias { command } => {
1815 use commands::alias::AliasSubcommand;
1816 let subcommand = match command {
1817 AliasCommand::Add {
1818 name,
1819 command,
1820 description,
1821 } => AliasSubcommand::Add {
1822 name: name.clone(),
1823 command: command.clone(),
1824 description: description.clone(),
1825 },
1826 AliasCommand::Remove { name } => AliasSubcommand::Remove { name: name.clone() },
1827 AliasCommand::List => AliasSubcommand::List,
1828 AliasCommand::Show { name } => AliasSubcommand::Show { name: name.clone() },
1829 AliasCommand::Clear => AliasSubcommand::Clear,
1830 };
1831 commands::alias::run_alias(subcommand).await
1832 }
1833 }
1834 }
1835}
1836
1837pub mod utils {
1839 use crate::cli_types::CliAudioFormat;
1840 use std::path::Path;
1841 use voirs_sdk::AudioFormat;
1842
1843 pub fn format_from_extension(path: &Path) -> Option<AudioFormat> {
1845 path.extension()
1846 .and_then(|ext| ext.to_str())
1847 .and_then(|ext| match ext.to_lowercase().as_str() {
1848 "wav" => Some(AudioFormat::Wav),
1849 "flac" => Some(AudioFormat::Flac),
1850 "mp3" => Some(AudioFormat::Mp3),
1851 "opus" => Some(AudioFormat::Opus),
1852 "ogg" => Some(AudioFormat::Ogg),
1853 _ => None,
1854 })
1855 }
1856
1857 pub fn generate_output_filename(text: &str, format: AudioFormat) -> String {
1859 let safe_text = text
1860 .chars()
1861 .take(30)
1862 .filter(|c| c.is_alphanumeric() || c.is_whitespace())
1863 .collect::<String>()
1864 .replace(' ', "_")
1865 .to_lowercase();
1866
1867 let timestamp = std::time::SystemTime::now()
1868 .duration_since(std::time::UNIX_EPOCH)
1869 .unwrap()
1870 .as_secs();
1871
1872 format!("voirs_{}_{}.{}", safe_text, timestamp, format.extension())
1873 }
1874}
1875
1876fn get_default_config_paths() -> Vec<std::path::PathBuf> {
1878 let mut paths = Vec::new();
1879
1880 paths.push(
1882 std::env::current_dir()
1883 .unwrap_or_default()
1884 .join("voirs.toml"),
1885 );
1886 paths.push(
1887 std::env::current_dir()
1888 .unwrap_or_default()
1889 .join("voirs.json"),
1890 );
1891 paths.push(
1892 std::env::current_dir()
1893 .unwrap_or_default()
1894 .join("voirs.yaml"),
1895 );
1896
1897 if let Some(config_dir) = dirs::config_dir() {
1899 let voirs_config_dir = config_dir.join("voirs");
1900 paths.push(voirs_config_dir.join("config.toml"));
1901 paths.push(voirs_config_dir.join("config.json"));
1902 paths.push(voirs_config_dir.join("config.yaml"));
1903 paths.push(voirs_config_dir.join("voirs.toml"));
1904 paths.push(voirs_config_dir.join("voirs.json"));
1905 paths.push(voirs_config_dir.join("voirs.yaml"));
1906 }
1907
1908 if let Some(home_dir) = dirs::home_dir() {
1910 paths.push(home_dir.join(".voirs.toml"));
1911 paths.push(home_dir.join(".voirs.json"));
1912 paths.push(home_dir.join(".voirs.yaml"));
1913 paths.push(home_dir.join(".voirsrc"));
1914 paths.push(home_dir.join(".config").join("voirs").join("config.toml"));
1915 }
1916
1917 paths
1918}
1919
1920#[cfg(test)]
1921mod tests {
1922 use super::*;
1923
1924 #[test]
1925 fn test_format_from_extension() {
1926 use std::path::Path;
1927
1928 assert_eq!(
1929 utils::format_from_extension(Path::new("test.wav")),
1930 Some(AudioFormat::Wav)
1931 );
1932 assert_eq!(
1933 utils::format_from_extension(Path::new("test.flac")),
1934 Some(AudioFormat::Flac)
1935 );
1936 assert_eq!(
1937 utils::format_from_extension(Path::new("test.unknown")),
1938 None
1939 );
1940 }
1941
1942 #[test]
1943 fn test_generate_output_filename() {
1944 let filename = utils::generate_output_filename("Hello World", AudioFormat::Wav);
1945 assert!(filename.starts_with("voirs_hello_world_"));
1946 assert!(filename.ends_with(".wav"));
1947 }
1948}