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 #[cfg(feature = "onnx")]
691 Onnx {
692 #[command(subcommand)]
694 command: commands::onnx_tools::OnnxCommand,
695 },
696
697 Accuracy {
699 #[command(flatten)]
701 command: commands::accuracy::AccuracyCommand,
702 },
703
704 Performance {
706 #[command(flatten)]
708 command: commands::performance::PerformanceCommand,
709 },
710
711 #[cfg(feature = "emotion")]
713 Emotion {
714 #[command(subcommand)]
716 command: commands::emotion::EmotionCommand,
717 },
718
719 #[cfg(feature = "cloning")]
721 Clone {
722 #[command(subcommand)]
724 command: commands::cloning::CloningCommand,
725 },
726
727 #[cfg(feature = "conversion")]
729 Convert {
730 #[command(subcommand)]
732 command: commands::conversion::ConversionCommand,
733 },
734
735 #[cfg(feature = "singing")]
737 Sing {
738 #[command(subcommand)]
740 command: commands::singing::SingingCommand,
741 },
742
743 #[cfg(feature = "spatial")]
745 Spatial {
746 #[command(subcommand)]
748 command: commands::spatial::SpatialCommand,
749 },
750
751 Capabilities {
753 #[command(subcommand)]
755 command: commands::capabilities::CapabilitiesCommand,
756 },
757
758 Checkpoint {
760 #[command(subcommand)]
762 command: commands::checkpoint::CheckpointCommands,
763 },
764
765 Monitor {
767 #[command(subcommand)]
769 command: commands::monitoring::MonitoringCommand,
770 },
771
772 Train {
774 #[command(subcommand)]
776 command: commands::train::TrainCommands,
777 },
778
779 ConvertModel {
781 input: PathBuf,
783
784 #[arg(short, long)]
786 output: PathBuf,
787
788 #[arg(long)]
790 from: Option<String>,
791
792 #[arg(long)]
794 model_type: String,
795
796 #[arg(long)]
798 verify: bool,
799 },
800
801 VocoderInfer {
803 checkpoint: PathBuf,
805
806 #[arg(long)]
808 mel: Option<PathBuf>,
809
810 #[arg(short, long, default_value = "vocoder_output.wav")]
812 output: PathBuf,
813
814 #[arg(long, default_value = "50")]
816 steps: usize,
817
818 #[arg(long)]
820 quality: Option<String>,
821
822 #[arg(long)]
824 batch_input: Option<PathBuf>,
825
826 #[arg(long)]
828 batch_output: Option<PathBuf>,
829
830 #[arg(long)]
832 metrics: bool,
833 },
834
835 Stream {
837 text: Option<String>,
839
840 #[arg(long, default_value = "100")]
842 latency: u64,
843
844 #[arg(long, default_value = "512")]
846 chunk_size: usize,
847
848 #[arg(long, default_value = "4")]
850 buffer_chunks: usize,
851
852 #[arg(long)]
854 play: bool,
855 },
856
857 ModelInspect {
859 model: PathBuf,
861
862 #[arg(long)]
864 detailed: bool,
865
866 #[arg(long)]
868 export: Option<PathBuf>,
869
870 #[arg(long)]
872 verify: bool,
873 },
874
875 Export {
877 #[arg(long)]
879 export_type: String,
880
881 source: String,
883
884 #[arg(short, long)]
886 output: PathBuf,
887
888 #[arg(long)]
890 include_weights: bool,
891 },
892
893 Import {
895 input: PathBuf,
897
898 #[arg(long)]
900 name: Option<String>,
901
902 #[arg(long)]
904 force: bool,
905
906 #[arg(long, default_value = "true")]
908 validate: bool,
909 },
910
911 History {
913 #[arg(short = 'n', long, default_value = "20")]
915 limit: usize,
916
917 #[arg(long)]
919 stats: bool,
920
921 #[arg(long)]
923 suggest: bool,
924
925 #[arg(long)]
927 clear: bool,
928 },
929
930 Workflow {
932 #[command(subcommand)]
934 command: commands::workflow::WorkflowCommands,
935 },
936
937 Alias {
939 #[command(subcommand)]
941 command: AliasCommand,
942 },
943}
944
945#[derive(Subcommand)]
947pub enum AliasCommand {
948 Add {
950 name: String,
952
953 command: String,
955
956 #[arg(short, long)]
958 description: Option<String>,
959 },
960
961 Remove {
963 name: String,
965 },
966
967 List,
969
970 Show {
972 name: String,
974 },
975
976 Clear,
978}
979
980impl CliApp {
982 pub async fn run() -> Result<()> {
984 let app = Self::parse();
985
986 app.init_logging()?;
988
989 let config = app.load_config().await?;
991
992 app.execute_command(config).await
994 }
995
996 fn init_logging(&self) -> Result<()> {
998 let level = if self.global.quiet {
999 tracing::Level::ERROR
1000 } else {
1001 match self.global.verbose {
1002 0 => tracing::Level::INFO,
1003 1 => tracing::Level::DEBUG,
1004 _ => tracing::Level::TRACE,
1005 }
1006 };
1007
1008 tracing_subscriber::fmt()
1009 .with_max_level(level)
1010 .with_target(false)
1011 .with_writer(std::io::stderr) .init();
1013
1014 Ok(())
1015 }
1016
1017 async fn load_config(&self) -> Result<AppConfig> {
1019 let mut config = if let Some(config_path) = &self.global.config {
1020 tracing::info!("Loading configuration from {:?}", config_path);
1021 self.load_config_from_file(config_path).await?
1022 } else {
1023 self.load_config_from_default_locations().await?
1025 };
1026
1027 self.apply_cli_overrides(&mut config);
1029
1030 Ok(config)
1031 }
1032
1033 async fn load_config_from_file(&self, config_path: &std::path::Path) -> Result<AppConfig> {
1035 if !config_path.exists() {
1036 tracing::warn!(
1037 "Configuration file not found: {}, using defaults",
1038 config_path.display()
1039 );
1040 return Ok(AppConfig::default());
1041 }
1042
1043 let content =
1044 std::fs::read_to_string(config_path).map_err(|e| voirs_sdk::VoirsError::IoError {
1045 path: config_path.to_path_buf(),
1046 operation: voirs_sdk::error::IoOperation::Read,
1047 source: e,
1048 })?;
1049
1050 let config = match config_path.extension().and_then(|ext| ext.to_str()) {
1052 Some("toml") => {
1053 toml::from_str(&content).or_else(|_| {
1055 self.parse_config_auto_detect(&content)
1057 })?
1058 }
1059 Some("json") => {
1060 serde_json::from_str(&content).or_else(|_| {
1062 self.parse_config_auto_detect(&content)
1064 })?
1065 }
1066 Some("yaml") | Some("yml") => {
1067 serde_yaml::from_str(&content).or_else(|_| {
1069 self.parse_config_auto_detect(&content)
1071 })?
1072 }
1073 _ => {
1074 self.parse_config_auto_detect(&content)?
1076 }
1077 };
1078
1079 tracing::info!(
1080 "Successfully loaded configuration from {}",
1081 config_path.display()
1082 );
1083 Ok(config)
1084 }
1085
1086 async fn load_config_from_default_locations(&self) -> Result<AppConfig> {
1088 let possible_paths = get_default_config_paths();
1089
1090 for path in possible_paths {
1091 if path.exists() {
1092 tracing::info!("Found configuration file at: {}", path.display());
1093 return self.load_config_from_file(&path).await;
1094 }
1095 }
1096
1097 tracing::info!("No configuration file found, using defaults");
1099 Ok(AppConfig::default())
1100 }
1101
1102 fn parse_config_auto_detect(&self, content: &str) -> Result<AppConfig> {
1104 let trimmed = content.trim_start();
1107
1108 if trimmed.starts_with('{') {
1109 serde_json::from_str(content).map_err(|e| {
1111 voirs_sdk::VoirsError::config_error(format!(
1112 "Failed to parse JSON configuration: {}",
1113 e
1114 ))
1115 })
1116 } else if trimmed.contains("---") || content.contains(": ") {
1117 serde_yaml::from_str(content).or_else(|yaml_err| {
1119 toml::from_str(content).map_err(|toml_err| {
1121 voirs_sdk::VoirsError::config_error(format!(
1122 "Failed to parse configuration. YAML error: {}, TOML error: {}",
1123 yaml_err, toml_err
1124 ))
1125 })
1126 })
1127 } else {
1128 toml::from_str(content)
1130 .or_else(|_| serde_json::from_str(content))
1131 .or_else(|_| serde_yaml::from_str(content))
1132 .map_err(|e| {
1133 voirs_sdk::VoirsError::config_error(format!(
1134 "Unable to parse configuration file. Supported formats: TOML, JSON, YAML. Last error: {}", e
1135 ))
1136 })
1137 }
1138 }
1139
1140 fn apply_cli_overrides(&self, config: &mut AppConfig) {
1142 if self.global.gpu {
1143 config.pipeline.use_gpu = true;
1144 }
1145
1146 if let Some(threads) = self.global.threads {
1147 config.pipeline.num_threads = Some(threads);
1148 }
1149
1150 if let Some(ref voice) = self.global.voice {
1151 config.cli.default_voice = Some(voice.clone());
1152 }
1153
1154 if let Some(ref format) = self.global.format {
1155 config.cli.default_format = (*format).into();
1156 config.pipeline.default_synthesis.output_format = (*format).into();
1158 }
1159 }
1160
1161 async fn execute_command(&self, config: AppConfig) -> Result<()> {
1163 match &self.command {
1164 Commands::Synthesize {
1165 text,
1166 output,
1167 rate,
1168 pitch,
1169 volume,
1170 quality,
1171 enhance,
1172 play,
1173 auto_detect,
1174 } => {
1175 let args = commands::synthesize::SynthesizeArgs {
1176 text,
1177 output: output.as_deref(),
1178 rate: *rate,
1179 pitch: *pitch,
1180 volume: *volume,
1181 quality: (*quality).into(),
1182 enhance: *enhance,
1183 play: *play,
1184 auto_detect: *auto_detect,
1185 };
1186 commands::synthesize::run_synthesize(args, &config, &self.global).await
1187 }
1188
1189 Commands::SynthesizeFile {
1190 input,
1191 output_dir,
1192 rate,
1193 quality,
1194 } => {
1195 commands::synthesize::run_synthesize_file(
1196 input,
1197 output_dir.as_deref(),
1198 *rate,
1199 (*quality).into(),
1200 &config,
1201 &self.global,
1202 )
1203 .await
1204 }
1205
1206 Commands::ListVoices { language, detailed } => {
1207 commands::voices::run_list_voices(language.as_deref(), *detailed, &config).await
1208 }
1209
1210 Commands::VoiceInfo { voice_id } => {
1211 commands::voices::run_voice_info(voice_id, &config).await
1212 }
1213
1214 Commands::DownloadVoice { voice_id, force } => {
1215 commands::voices::run_download_voice(voice_id, *force, &config).await
1216 }
1217
1218 Commands::PreviewVoice {
1219 voice_id,
1220 text,
1221 output,
1222 no_play,
1223 } => {
1224 commands::voices::run_preview_voice(
1225 voice_id,
1226 text.as_deref(),
1227 output.as_ref(),
1228 *no_play,
1229 &config,
1230 &self.global,
1231 )
1232 .await
1233 }
1234
1235 Commands::CompareVoices { voice_ids } => {
1236 commands::voices::run_compare_voices(voice_ids.clone(), &config).await
1237 }
1238
1239 Commands::Test { text, play } => {
1240 commands::test::run_test(text, *play, &config, &self.global).await
1241 }
1242
1243 Commands::CrossLangTest {
1244 format,
1245 save_report,
1246 } => {
1247 commands::cross_lang_test::run_cross_lang_tests(
1248 format,
1249 *save_report,
1250 &config,
1251 &self.global,
1252 )
1253 .await
1254 }
1255
1256 Commands::TestApi {
1257 server_url,
1258 api_key,
1259 concurrent,
1260 report,
1261 verbose,
1262 } => commands::test_api::run_api_tests(
1263 server_url.clone(),
1264 api_key.clone(),
1265 *concurrent,
1266 report.clone(),
1267 *verbose,
1268 )
1269 .await
1270 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1271 component: "API Tester".to_string(),
1272 message: e.to_string(),
1273 }),
1274
1275 Commands::Config { show, init, path } => {
1276 commands::config::run_config(*show, *init, path.as_deref(), &config).await
1277 }
1278
1279 Commands::ListModels { backend, detailed } => {
1280 commands::models::run_list_models(
1281 backend.as_deref(),
1282 *detailed,
1283 &config,
1284 &self.global,
1285 )
1286 .await
1287 }
1288
1289 Commands::DownloadModel { model_id, force } => {
1290 commands::models::run_download_model(model_id, *force, &config, &self.global).await
1291 }
1292
1293 Commands::BenchmarkModels {
1294 model_ids,
1295 iterations,
1296 accuracy,
1297 } => {
1298 commands::models::run_benchmark_models(
1299 model_ids,
1300 *iterations,
1301 *accuracy,
1302 &config,
1303 &self.global,
1304 )
1305 .await
1306 }
1307
1308 Commands::OptimizeModel {
1309 model_id,
1310 output,
1311 strategy,
1312 } => {
1313 commands::models::run_optimize_model(
1314 model_id,
1315 output.as_deref(),
1316 Some(strategy),
1317 &config,
1318 &self.global,
1319 )
1320 .await
1321 }
1322
1323 Commands::Batch {
1324 input,
1325 output_dir,
1326 workers,
1327 rate,
1328 pitch,
1329 volume,
1330 quality,
1331 resume,
1332 } => {
1333 commands::batch::run_batch_process(
1334 commands::batch::BatchProcessArgs {
1335 input,
1336 output_dir: output_dir.as_deref(),
1337 workers: *workers,
1338 quality: (*quality).into(),
1339 rate: *rate,
1340 pitch: *pitch,
1341 volume: *volume,
1342 resume: *resume,
1343 },
1344 &config,
1345 &self.global,
1346 )
1347 .await
1348 }
1349
1350 Commands::Server { port, host } => {
1351 commands::server::run_server(host, *port, &config).await
1352 }
1353
1354 Commands::Interactive {
1355 voice,
1356 no_audio,
1357 debug,
1358 load_session,
1359 auto_save,
1360 } => {
1361 let options = commands::interactive::InteractiveOptions {
1362 voice: voice.clone(),
1363 no_audio: *no_audio,
1364 debug: *debug,
1365 load_session: load_session.clone(),
1366 auto_save: *auto_save,
1367 };
1368
1369 commands::interactive::run_interactive(options)
1370 .await
1371 .map_err(Into::into)
1372 }
1373
1374 Commands::Guide {
1375 command,
1376 getting_started,
1377 examples,
1378 } => {
1379 let help_system = help::HelpSystem::new();
1380
1381 if *getting_started {
1382 println!("{}", help::display_getting_started());
1383 } else if *examples {
1384 println!("{}", help_system.display_command_overview());
1385 } else if let Some(cmd) = command {
1386 println!("{}", help_system.display_command_help(cmd));
1387 } else {
1388 println!("{}", help_system.display_command_overview());
1389 }
1390
1391 Ok(())
1392 }
1393
1394 Commands::GenerateCompletion {
1395 shell,
1396 output,
1397 install_help,
1398 install_script,
1399 status,
1400 } => {
1401 if *status {
1402 println!("{}", completion::display_completion_status());
1403 } else if *install_script {
1404 println!("{}", completion::generate_install_script());
1405 } else if *install_help {
1406 println!("{}", completion::get_installation_instructions(*shell));
1407 } else if let Some(output_path) = output {
1408 completion::generate_completion_to_file(*shell, output_path).map_err(|e| {
1409 voirs_sdk::VoirsError::IoError {
1410 path: output_path.clone(),
1411 operation: voirs_sdk::error::IoOperation::Write,
1412 source: e,
1413 }
1414 })?;
1415 println!("Completion script generated: {}", output_path.display());
1416 } else {
1417 completion::generate_completion_to_stdout(*shell).map_err(|e| {
1418 voirs_sdk::VoirsError::IoError {
1419 path: std::env::current_dir().unwrap_or_default(),
1420 operation: voirs_sdk::error::IoOperation::Write,
1421 source: e,
1422 }
1423 })?;
1424 }
1425
1426 Ok(())
1427 }
1428
1429 Commands::Dataset { command } => {
1430 commands::dataset::execute_dataset_command(command, &config, &self.global).await
1431 }
1432
1433 Commands::Dashboard { interval } => commands::dashboard::run_dashboard(*interval)
1434 .await
1435 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1436 component: "Dashboard".to_string(),
1437 message: e.to_string(),
1438 }),
1439
1440 Commands::Cloud { command } => {
1441 commands::cloud::execute_cloud_command(command, &config, &self.global).await
1442 }
1443
1444 Commands::Telemetry { command } => commands::telemetry::execute(command.clone())
1445 .await
1446 .map_err(|e| {
1447 voirs_sdk::VoirsError::config_error(format!("Telemetry command failed: {}", e))
1448 }),
1449
1450 Commands::Lsp { verbose } => {
1451 if *verbose {
1452 eprintln!("Starting VoiRS LSP server in verbose mode...");
1453 }
1454
1455 let server = crate::lsp::LspServer::new();
1456 server.start().await.map_err(|e| {
1457 voirs_sdk::VoirsError::config_error(format!("LSP server failed: {}", e))
1458 })
1459 }
1460
1461 #[cfg(feature = "onnx")]
1462 Commands::Kokoro { command } => {
1463 commands::kokoro::execute_kokoro_command(command, &config, &self.global).await
1464 }
1465
1466 #[cfg(feature = "onnx")]
1467 Commands::Onnx { command } => commands::onnx_tools::handle_onnx_command(command)
1468 .map_err(|e| {
1469 voirs_sdk::VoirsError::config_error(format!("ONNX tool failed: {}", e))
1470 }),
1471
1472 Commands::Accuracy { command } => {
1473 commands::accuracy::execute_accuracy_command(command.clone())
1474 .await
1475 .map_err(|e| {
1476 voirs_sdk::VoirsError::config_error(format!(
1477 "Accuracy command failed: {}",
1478 e
1479 ))
1480 })
1481 }
1482
1483 Commands::Performance { command } => {
1484 commands::performance::execute_performance_command(command.clone())
1485 .await
1486 .map_err(|e| {
1487 voirs_sdk::VoirsError::config_error(format!(
1488 "Performance command failed: {}",
1489 e
1490 ))
1491 })
1492 }
1493
1494 #[cfg(feature = "emotion")]
1495 Commands::Emotion { command } => {
1496 use crate::output::OutputFormatter;
1497 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1498 commands::emotion::execute_emotion_command(command.clone(), &output_formatter)
1499 .await
1500 .map_err(|e| {
1501 voirs_sdk::VoirsError::config_error(format!(
1502 "Emotion command failed: {}",
1503 e
1504 ))
1505 })
1506 }
1507
1508 #[cfg(feature = "cloning")]
1509 Commands::Clone { command } => {
1510 use crate::output::OutputFormatter;
1511 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1512 commands::cloning::execute_cloning_command(command.clone(), &output_formatter)
1513 .await
1514 .map_err(|e| {
1515 voirs_sdk::VoirsError::config_error(format!(
1516 "Cloning command failed: {}",
1517 e
1518 ))
1519 })
1520 }
1521
1522 #[cfg(feature = "conversion")]
1523 Commands::Convert { command } => {
1524 use crate::output::OutputFormatter;
1525 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1526 commands::conversion::execute_conversion_command(command.clone(), &output_formatter)
1527 .await
1528 .map_err(|e| {
1529 voirs_sdk::VoirsError::config_error(format!(
1530 "Conversion command failed: {}",
1531 e
1532 ))
1533 })
1534 }
1535
1536 #[cfg(feature = "singing")]
1537 Commands::Sing { command } => {
1538 use crate::output::OutputFormatter;
1539 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1540 commands::singing::execute_singing_command(command.clone(), &output_formatter)
1541 .await
1542 .map_err(|e| {
1543 voirs_sdk::VoirsError::config_error(format!(
1544 "Singing command failed: {}",
1545 e
1546 ))
1547 })
1548 }
1549
1550 #[cfg(feature = "spatial")]
1551 Commands::Spatial { command } => {
1552 use crate::output::OutputFormatter;
1553 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1554 commands::spatial::execute_spatial_command(command.clone(), &output_formatter)
1555 .await
1556 .map_err(|e| {
1557 voirs_sdk::VoirsError::config_error(format!(
1558 "Spatial command failed: {}",
1559 e
1560 ))
1561 })
1562 }
1563
1564 Commands::Capabilities { command } => {
1565 use crate::output::OutputFormatter;
1566 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1567 commands::capabilities::execute_capabilities_command(
1568 command.clone(),
1569 &output_formatter,
1570 &config,
1571 )
1572 .await
1573 .map_err(|e| {
1574 voirs_sdk::VoirsError::config_error(format!(
1575 "Capabilities command failed: {}",
1576 e
1577 ))
1578 })
1579 }
1580
1581 Commands::Checkpoint { command } => {
1582 commands::checkpoint::execute_checkpoint_command(command.clone(), &self.global)
1583 .await
1584 .map_err(|e| {
1585 voirs_sdk::VoirsError::config_error(format!(
1586 "Checkpoint command failed: {}",
1587 e
1588 ))
1589 })
1590 }
1591
1592 Commands::Monitor { command } => {
1593 use crate::output::OutputFormatter;
1594 let output_formatter = OutputFormatter::new(!self.global.quiet, false);
1595 commands::monitoring::execute_monitoring_command(
1596 command.clone(),
1597 &output_formatter,
1598 &config,
1599 )
1600 .await
1601 .map_err(|e| {
1602 voirs_sdk::VoirsError::config_error(format!("Monitoring command failed: {}", e))
1603 })
1604 }
1605
1606 Commands::Train { command } => {
1607 commands::train::execute_train_command(command.clone(), &self.global)
1608 .await
1609 .map_err(|e| {
1610 voirs_sdk::VoirsError::config_error(format!("Train command failed: {}", e))
1611 })
1612 }
1613
1614 Commands::ConvertModel {
1615 input,
1616 output,
1617 from,
1618 model_type,
1619 verify,
1620 } => commands::convert_model::run_convert_model(
1621 input.clone(),
1622 output.clone(),
1623 from.clone(),
1624 model_type.clone(),
1625 *verify,
1626 &self.global,
1627 )
1628 .await
1629 .map_err(|e| {
1630 voirs_sdk::VoirsError::config_error(format!("Model conversion failed: {}", e))
1631 }),
1632
1633 Commands::VocoderInfer {
1634 checkpoint,
1635 mel,
1636 output,
1637 steps,
1638 quality,
1639 batch_input,
1640 batch_output,
1641 metrics,
1642 } => {
1643 let config = commands::vocoder_inference::VocoderInferenceConfig {
1644 checkpoint: checkpoint.as_path(),
1645 mel_path: mel.as_deref(),
1646 output: output.as_path(),
1647 steps: *steps,
1648 quality: quality.as_deref(),
1649 batch_input: batch_input.as_ref(),
1650 batch_output: batch_output.as_ref(),
1651 show_metrics: *metrics,
1652 };
1653 commands::vocoder_inference::run_vocoder_inference(config, &self.global)
1654 .await
1655 .map_err(|e| {
1656 voirs_sdk::VoirsError::config_error(format!(
1657 "Vocoder inference failed: {}",
1658 e
1659 ))
1660 })
1661 }
1662
1663 Commands::Stream {
1664 text,
1665 latency,
1666 chunk_size,
1667 buffer_chunks,
1668 play,
1669 } => {
1670 commands::streaming::run_streaming_synthesis(
1671 text.as_deref(),
1672 *latency,
1673 *chunk_size,
1674 *buffer_chunks,
1675 *play,
1676 &config,
1677 &self.global,
1678 )
1679 .await
1680 }
1681
1682 Commands::ModelInspect {
1683 model,
1684 detailed,
1685 export,
1686 verify,
1687 } => {
1688 commands::model_inspect::run_model_inspect(
1689 model,
1690 *detailed,
1691 export.as_ref(),
1692 *verify,
1693 &self.global,
1694 )
1695 .await
1696 }
1697
1698 Commands::Export {
1699 export_type,
1700 source,
1701 output,
1702 include_weights,
1703 } => {
1704 commands::export_import::run_export(
1705 export_type,
1706 source,
1707 output,
1708 *include_weights,
1709 &config,
1710 &self.global,
1711 )
1712 .await
1713 }
1714
1715 Commands::Import {
1716 input,
1717 name,
1718 force,
1719 validate,
1720 } => {
1721 commands::export_import::run_import(
1722 input,
1723 name.as_deref(),
1724 *force,
1725 *validate,
1726 &config,
1727 &self.global,
1728 )
1729 .await
1730 }
1731
1732 Commands::History {
1733 limit,
1734 stats,
1735 suggest,
1736 clear,
1737 } => commands::history::run_history(*limit, *stats, *suggest, *clear).await,
1738
1739 Commands::Workflow { command } => {
1740 use commands::workflow::WorkflowCommands;
1741 match command {
1742 WorkflowCommands::Execute {
1743 workflow_file,
1744 variables,
1745 max_parallel,
1746 resume,
1747 state_dir,
1748 } => commands::workflow::run_workflow_execute(
1749 workflow_file.clone(),
1750 variables.clone(),
1751 *max_parallel,
1752 *resume,
1753 state_dir.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::Validate {
1761 workflow_file,
1762 detailed,
1763 format,
1764 } => commands::workflow::run_workflow_validate(
1765 workflow_file.clone(),
1766 *detailed,
1767 format.clone(),
1768 )
1769 .await
1770 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1771 component: "Workflow".to_string(),
1772 message: e.to_string(),
1773 }),
1774 WorkflowCommands::List {
1775 registry_dir,
1776 detailed,
1777 } => commands::workflow::run_workflow_list(registry_dir.clone(), *detailed)
1778 .await
1779 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1780 component: "Workflow".to_string(),
1781 message: e.to_string(),
1782 }),
1783 WorkflowCommands::Status {
1784 workflow_name,
1785 state_dir,
1786 format,
1787 } => commands::workflow::run_workflow_status(
1788 workflow_name.clone(),
1789 state_dir.clone(),
1790 format.clone(),
1791 )
1792 .await
1793 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1794 component: "Workflow".to_string(),
1795 message: e.to_string(),
1796 }),
1797 WorkflowCommands::Resume {
1798 workflow_name,
1799 state_dir,
1800 max_parallel,
1801 } => commands::workflow::run_workflow_resume(
1802 workflow_name.clone(),
1803 state_dir.clone(),
1804 *max_parallel,
1805 )
1806 .await
1807 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1808 component: "Workflow".to_string(),
1809 message: e.to_string(),
1810 }),
1811 WorkflowCommands::Stop {
1812 workflow_name,
1813 state_dir,
1814 force,
1815 } => commands::workflow::run_workflow_stop(
1816 workflow_name.clone(),
1817 state_dir.clone(),
1818 *force,
1819 )
1820 .await
1821 .map_err(|e| voirs_sdk::VoirsError::InternalError {
1822 component: "Workflow".to_string(),
1823 message: e.to_string(),
1824 }),
1825 }
1826 }
1827
1828 Commands::Alias { command } => {
1829 use commands::alias::AliasSubcommand;
1830 let subcommand = match command {
1831 AliasCommand::Add {
1832 name,
1833 command,
1834 description,
1835 } => AliasSubcommand::Add {
1836 name: name.clone(),
1837 command: command.clone(),
1838 description: description.clone(),
1839 },
1840 AliasCommand::Remove { name } => AliasSubcommand::Remove { name: name.clone() },
1841 AliasCommand::List => AliasSubcommand::List,
1842 AliasCommand::Show { name } => AliasSubcommand::Show { name: name.clone() },
1843 AliasCommand::Clear => AliasSubcommand::Clear,
1844 };
1845 commands::alias::run_alias(subcommand).await
1846 }
1847 }
1848 }
1849}
1850
1851pub mod utils {
1853 use crate::cli_types::CliAudioFormat;
1854 use std::path::Path;
1855 use voirs_sdk::AudioFormat;
1856
1857 pub fn format_from_extension(path: &Path) -> Option<AudioFormat> {
1859 path.extension()
1860 .and_then(|ext| ext.to_str())
1861 .and_then(|ext| match ext.to_lowercase().as_str() {
1862 "wav" => Some(AudioFormat::Wav),
1863 "flac" => Some(AudioFormat::Flac),
1864 "mp3" => Some(AudioFormat::Mp3),
1865 "opus" => Some(AudioFormat::Opus),
1866 "ogg" => Some(AudioFormat::Ogg),
1867 _ => None,
1868 })
1869 }
1870
1871 pub fn generate_output_filename(text: &str, format: AudioFormat) -> String {
1873 let safe_text = text
1874 .chars()
1875 .take(30)
1876 .filter(|c| c.is_alphanumeric() || c.is_whitespace())
1877 .collect::<String>()
1878 .replace(' ', "_")
1879 .to_lowercase();
1880
1881 let timestamp = std::time::SystemTime::now()
1882 .duration_since(std::time::UNIX_EPOCH)
1883 .expect("SystemTime should be after UNIX_EPOCH")
1884 .as_secs();
1885
1886 format!("voirs_{}_{}.{}", safe_text, timestamp, format.extension())
1887 }
1888}
1889
1890fn get_default_config_paths() -> Vec<std::path::PathBuf> {
1892 let mut paths = Vec::new();
1893
1894 paths.push(
1896 std::env::current_dir()
1897 .unwrap_or_default()
1898 .join("voirs.toml"),
1899 );
1900 paths.push(
1901 std::env::current_dir()
1902 .unwrap_or_default()
1903 .join("voirs.json"),
1904 );
1905 paths.push(
1906 std::env::current_dir()
1907 .unwrap_or_default()
1908 .join("voirs.yaml"),
1909 );
1910
1911 if let Some(config_dir) = dirs::config_dir() {
1913 let voirs_config_dir = config_dir.join("voirs");
1914 paths.push(voirs_config_dir.join("config.toml"));
1915 paths.push(voirs_config_dir.join("config.json"));
1916 paths.push(voirs_config_dir.join("config.yaml"));
1917 paths.push(voirs_config_dir.join("voirs.toml"));
1918 paths.push(voirs_config_dir.join("voirs.json"));
1919 paths.push(voirs_config_dir.join("voirs.yaml"));
1920 }
1921
1922 if let Some(home_dir) = dirs::home_dir() {
1924 paths.push(home_dir.join(".voirs.toml"));
1925 paths.push(home_dir.join(".voirs.json"));
1926 paths.push(home_dir.join(".voirs.yaml"));
1927 paths.push(home_dir.join(".voirsrc"));
1928 paths.push(home_dir.join(".config").join("voirs").join("config.toml"));
1929 }
1930
1931 paths
1932}
1933
1934#[cfg(test)]
1935mod tests {
1936 use super::*;
1937
1938 #[test]
1939 fn test_format_from_extension() {
1940 use std::path::Path;
1941
1942 assert_eq!(
1943 utils::format_from_extension(Path::new("test.wav")),
1944 Some(AudioFormat::Wav)
1945 );
1946 assert_eq!(
1947 utils::format_from_extension(Path::new("test.flac")),
1948 Some(AudioFormat::Flac)
1949 );
1950 assert_eq!(
1951 utils::format_from_extension(Path::new("test.unknown")),
1952 None
1953 );
1954 }
1955
1956 #[test]
1957 fn test_generate_output_filename() {
1958 let filename = utils::generate_output_filename("Hello World", AudioFormat::Wav);
1959 assert!(filename.starts_with("voirs_hello_world_"));
1960 assert!(filename.ends_with(".wav"));
1961 }
1962}