Skip to main content

voirs_cli/
lib.rs

1//! # VoiRS CLI
2//!
3//! Command-line interface for VoiRS speech synthesis framework.
4//! Provides easy-to-use commands for synthesis, voice management, and more.
5
6// Allow pedantic lints that are acceptable for audio/DSP processing code
7#![allow(clippy::cast_precision_loss)] // Acceptable for audio sample conversions
8#![allow(clippy::cast_possible_truncation)] // Controlled truncation in audio processing
9#![allow(clippy::cast_sign_loss)] // Intentional in index calculations
10#![allow(clippy::missing_errors_doc)] // Many internal functions with self-documenting error types
11#![allow(clippy::missing_panics_doc)] // Panics are documented where relevant
12#![allow(clippy::unused_self)] // Some trait implementations require &self for consistency
13#![allow(clippy::must_use_candidate)] // Not all return values need must_use annotation
14#![allow(clippy::doc_markdown)] // Technical terms don't all need backticks
15#![allow(clippy::unnecessary_wraps)] // Result wrappers maintained for API consistency
16#![allow(clippy::float_cmp)] // Exact float comparisons are intentional in some contexts
17#![allow(clippy::match_same_arms)] // Pattern matching clarity sometimes requires duplication
18#![allow(clippy::module_name_repetitions)] // Type names often repeat module names
19#![allow(clippy::struct_excessive_bools)] // Config structs naturally have many boolean flags
20#![allow(clippy::too_many_lines)] // Some functions are inherently complex
21#![allow(clippy::needless_pass_by_value)] // Some functions designed for ownership transfer
22#![allow(clippy::similar_names)] // Many similar variable names in algorithms
23#![allow(clippy::unused_async)] // Public API functions may need async for consistency
24#![allow(clippy::needless_range_loop)] // Range loops sometimes clearer than iterators
25#![allow(clippy::uninlined_format_args)] // Explicit argument names can improve clarity
26#![allow(clippy::manual_clamp)] // Manual clamping sometimes clearer
27#![allow(clippy::return_self_not_must_use)] // Not all builder methods need must_use
28#![allow(clippy::cast_possible_wrap)] // Controlled wrapping in processing code
29#![allow(clippy::cast_lossless)] // Explicit casts preferred for clarity
30#![allow(clippy::wildcard_imports)] // Prelude imports are convenient and standard
31#![allow(clippy::format_push_string)] // Sometimes more readable than alternative
32#![allow(clippy::redundant_closure_for_method_calls)] // Closures sometimes needed for type inference
33#![allow(clippy::too_many_arguments)] // Some functions naturally need many parameters
34#![allow(clippy::field_reassign_with_default)] // Sometimes clearer than builder pattern
35#![allow(clippy::trivially_copy_pass_by_ref)] // API consistency more important
36#![allow(clippy::await_holding_lock)] // Controlled lock holding in async contexts
37
38use 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// Re-export important types are already imported above
67
68/// VoiRS CLI application
69#[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    /// Global options
75    #[command(flatten)]
76    pub global: GlobalOptions,
77
78    /// Subcommands
79    #[command(subcommand)]
80    pub command: Commands,
81}
82
83/// Global CLI options
84#[derive(Parser)]
85pub struct GlobalOptions {
86    /// Configuration file path
87    #[arg(short, long)]
88    pub config: Option<PathBuf>,
89
90    /// Verbose output
91    #[arg(short, long, action = clap::ArgAction::Count)]
92    pub verbose: u8,
93
94    /// Quiet mode (suppress most output)
95    #[arg(short, long)]
96    pub quiet: bool,
97
98    /// Output format (overrides config)
99    #[arg(long)]
100    pub format: Option<CliAudioFormat>,
101
102    /// Voice to use (overrides config)
103    #[arg(long)]
104    pub voice: Option<String>,
105
106    /// Enable GPU acceleration
107    #[arg(long)]
108    pub gpu: bool,
109
110    /// Number of threads to use
111    #[arg(long)]
112    pub threads: Option<usize>,
113}
114
115/// Cloud-specific commands
116#[derive(Subcommand)]
117pub enum CloudCommands {
118    /// Synchronize files with cloud storage
119    Sync {
120        /// Force full synchronization
121        #[arg(long)]
122        force: bool,
123
124        /// Specific directory to sync
125        #[arg(long)]
126        directory: Option<PathBuf>,
127
128        /// Dry run (show what would be synced)
129        #[arg(long)]
130        dry_run: bool,
131    },
132
133    /// Add file or directory to cloud sync
134    AddToSync {
135        /// Local path to sync
136        local_path: PathBuf,
137
138        /// Remote path in cloud storage
139        remote_path: String,
140
141        /// Sync direction (upload, download, bidirectional)
142        #[arg(long, default_value = "bidirectional")]
143        direction: String,
144    },
145
146    /// Show cloud storage statistics
147    StorageStats,
148
149    /// Clean up old cached files
150    CleanupCache {
151        /// Maximum age in days
152        #[arg(long, default_value = "30")]
153        max_age_days: u32,
154
155        /// Dry run (show what would be cleaned)
156        #[arg(long)]
157        dry_run: bool,
158    },
159
160    /// Translate text using cloud services
161    Translate {
162        /// Text to translate
163        text: String,
164
165        /// Source language code
166        #[arg(long)]
167        from: String,
168
169        /// Target language code  
170        #[arg(long)]
171        to: String,
172
173        /// Translation quality (fast, balanced, high-quality)
174        #[arg(long, default_value = "balanced")]
175        quality: String,
176    },
177
178    /// Analyze content using cloud AI
179    AnalyzeContent {
180        /// Text content to analyze
181        text: String,
182
183        /// Analysis types (comma-separated: sentiment, entities, keywords, etc.)
184        #[arg(long, default_value = "sentiment,entities")]
185        analysis_types: String,
186
187        /// Language code (optional)
188        #[arg(long)]
189        language: Option<String>,
190    },
191
192    /// Assess audio quality using cloud services
193    AssessQuality {
194        /// Audio file to assess
195        audio_file: PathBuf,
196
197        /// Text that was synthesized
198        text: String,
199
200        /// Assessment metrics (comma-separated)
201        #[arg(long, default_value = "naturalness,intelligibility,overall")]
202        metrics: String,
203    },
204
205    /// Check cloud service health
206    HealthCheck,
207
208    /// Configure cloud integration
209    Configure {
210        /// Show current configuration
211        #[arg(long)]
212        show: bool,
213
214        /// Set cloud storage provider
215        #[arg(long)]
216        storage_provider: Option<String>,
217
218        /// Set API base URL
219        #[arg(long)]
220        api_url: Option<String>,
221
222        /// Enable/disable specific services
223        #[arg(long)]
224        enable_service: Option<String>,
225
226        /// Initialize cloud configuration
227        #[arg(long)]
228        init: bool,
229    },
230}
231
232/// Dataset-specific commands
233#[derive(Subcommand)]
234pub enum DatasetCommands {
235    /// Validate dataset structure and quality
236    Validate {
237        /// Dataset directory path
238        path: PathBuf,
239
240        /// Dataset type (auto-detect if not specified)
241        #[arg(long)]
242        dataset_type: Option<String>,
243
244        /// Perform detailed quality analysis
245        #[arg(long)]
246        detailed: bool,
247    },
248
249    /// Convert between dataset formats
250    Convert {
251        /// Input dataset path
252        input: PathBuf,
253
254        /// Output dataset path
255        output: PathBuf,
256
257        /// Source dataset format
258        #[arg(long)]
259        from: String,
260
261        /// Target dataset format
262        #[arg(long)]
263        to: String,
264    },
265
266    /// Split dataset into train/validation/test sets
267    Split {
268        /// Dataset directory path
269        path: PathBuf,
270
271        /// Training set ratio (0.0-1.0)
272        #[arg(long, default_value = "0.8")]
273        train_ratio: f32,
274
275        /// Validation set ratio (0.0-1.0)
276        #[arg(long, default_value = "0.1")]
277        val_ratio: f32,
278
279        /// Test set ratio (auto-calculated if not specified)
280        #[arg(long)]
281        test_ratio: Option<f32>,
282
283        /// Seed for reproducible splits
284        #[arg(long)]
285        seed: Option<u64>,
286    },
287
288    /// Preprocess dataset for training
289    Preprocess {
290        /// Input dataset path
291        input: PathBuf,
292
293        /// Output directory for preprocessed data
294        output: PathBuf,
295
296        /// Target sample rate
297        #[arg(long, default_value = "22050")]
298        sample_rate: u32,
299
300        /// Normalize audio levels
301        #[arg(long)]
302        normalize: bool,
303
304        /// Apply audio filters
305        #[arg(long)]
306        filter: bool,
307    },
308
309    /// Generate dataset statistics and analysis
310    Analyze {
311        /// Dataset directory path
312        path: PathBuf,
313
314        /// Output file for analysis report
315        #[arg(short, long)]
316        output: Option<PathBuf>,
317
318        /// Include detailed per-file statistics
319        #[arg(long)]
320        detailed: bool,
321    },
322}
323
324/// CLI commands
325#[derive(Subcommand)]
326pub enum Commands {
327    /// Synthesize text to speech
328    Synthesize {
329        /// Text to synthesize
330        text: String,
331
332        /// Output file path (use '-' for stdout, omit for auto-generated filename)
333        output: Option<PathBuf>,
334
335        /// Speaking rate (0.5 - 2.0)
336        #[arg(long, default_value = "1.0")]
337        rate: f32,
338
339        /// Pitch shift in semitones (-12.0 - 12.0)
340        #[arg(long, default_value = "0.0")]
341        pitch: f32,
342
343        /// Volume gain in dB (-20.0 - 20.0)
344        #[arg(long, default_value = "0.0")]
345        volume: f32,
346
347        /// Quality level
348        #[arg(long, default_value = "high")]
349        quality: CliQualityLevel,
350
351        /// Enable audio enhancement
352        #[arg(long)]
353        enhance: bool,
354
355        /// Play audio after synthesis
356        #[arg(short, long)]
357        play: bool,
358
359        /// Auto-detect input format (SSML, Markdown, JSON, or plain text)
360        #[arg(long)]
361        auto_detect: bool,
362    },
363
364    /// Synthesize from file
365    SynthesizeFile {
366        /// Input text file
367        input: PathBuf,
368
369        /// Output directory
370        #[arg(short, long)]
371        output_dir: Option<PathBuf>,
372
373        /// Speaking rate
374        #[arg(long, default_value = "1.0")]
375        rate: f32,
376
377        /// Quality level
378        #[arg(long, default_value = "high")]
379        quality: CliQualityLevel,
380    },
381
382    /// List available voices
383    ListVoices {
384        /// Filter by language
385        #[arg(long)]
386        language: Option<String>,
387
388        /// Show detailed information
389        #[arg(long)]
390        detailed: bool,
391    },
392
393    /// Get voice information
394    VoiceInfo {
395        /// Voice ID
396        voice_id: String,
397    },
398
399    /// Download voice
400    DownloadVoice {
401        /// Voice ID to download
402        voice_id: String,
403
404        /// Force download even if voice exists
405        #[arg(long)]
406        force: bool,
407    },
408
409    /// Preview a voice with a sample text
410    PreviewVoice {
411        /// Voice ID to preview
412        voice_id: String,
413
414        /// Custom preview text (default: "This is a preview of this voice.")
415        #[arg(long)]
416        text: Option<String>,
417
418        /// Save preview to file instead of just playing
419        #[arg(short, long)]
420        output: Option<PathBuf>,
421
422        /// Skip audio playback (only save to file)
423        #[arg(long)]
424        no_play: bool,
425    },
426
427    /// Compare multiple voices side by side
428    CompareVoices {
429        /// Voice IDs to compare
430        voice_ids: Vec<String>,
431    },
432
433    /// Test synthesis pipeline
434    Test {
435        /// Test text
436        #[arg(default_value = "Hello, this is a test of VoiRS speech synthesis.")]
437        text: String,
438
439        /// Play audio instead of saving
440        #[arg(long)]
441        play: bool,
442    },
443
444    /// Run cross-language consistency tests for FFI bindings
445    CrossLangTest {
446        /// Output format for test report (json, yaml)
447        #[arg(long, default_value = "json")]
448        format: String,
449
450        /// Save detailed test report to file
451        #[arg(long)]
452        save_report: bool,
453    },
454
455    /// Test API endpoints for VoiRS server
456    TestApi {
457        /// Server URL (e.g., http://localhost:8080)
458        server_url: String,
459
460        /// API key for authentication
461        #[arg(long)]
462        api_key: Option<String>,
463
464        /// Number of concurrent requests for load testing
465        #[arg(long)]
466        concurrent: Option<usize>,
467
468        /// Path to save test report (JSON or Markdown)
469        #[arg(long)]
470        report: Option<String>,
471
472        /// Enable verbose output
473        #[arg(long)]
474        verbose: bool,
475    },
476
477    /// Show configuration
478    Config {
479        /// Show configuration and exit
480        #[arg(long)]
481        show: bool,
482
483        /// Initialize default configuration
484        #[arg(long)]
485        init: bool,
486
487        /// Configuration file path for init
488        #[arg(long)]
489        path: Option<PathBuf>,
490    },
491
492    /// List available models
493    ListModels {
494        /// Filter by backend
495        #[arg(long)]
496        backend: Option<String>,
497
498        /// Show detailed information
499        #[arg(long)]
500        detailed: bool,
501    },
502
503    /// Download a model
504    DownloadModel {
505        /// Model ID to download
506        model_id: String,
507
508        /// Force download even if model exists
509        #[arg(long)]
510        force: bool,
511    },
512
513    /// Benchmark models
514    BenchmarkModels {
515        /// Model IDs to benchmark
516        model_ids: Vec<String>,
517
518        /// Number of iterations
519        #[arg(short, long, default_value = "3")]
520        iterations: u32,
521
522        /// Include accuracy testing against CMU test set (>95% phoneme accuracy target)
523        #[arg(long)]
524        accuracy: bool,
525    },
526
527    /// Optimize model for current hardware
528    OptimizeModel {
529        /// Model ID to optimize
530        model_id: String,
531
532        /// Output path for optimized model
533        #[arg(short, long)]
534        output: Option<String>,
535
536        /// Optimization strategy (speed, quality, memory, balanced)
537        #[arg(long, default_value = "balanced")]
538        strategy: String,
539    },
540
541    /// Batch process multiple texts
542    Batch {
543        /// Input file or directory
544        input: PathBuf,
545
546        /// Output directory
547        #[arg(short, long)]
548        output_dir: Option<PathBuf>,
549
550        /// Number of parallel workers
551        #[arg(short, long)]
552        workers: Option<usize>,
553
554        /// Speaking rate
555        #[arg(long, default_value = "1.0")]
556        rate: f32,
557
558        /// Pitch shift in semitones
559        #[arg(long, default_value = "0.0")]
560        pitch: f32,
561
562        /// Volume gain in dB
563        #[arg(long, default_value = "0.0")]
564        volume: f32,
565
566        /// Quality level
567        #[arg(long, default_value = "high")]
568        quality: CliQualityLevel,
569
570        /// Enable resume functionality
571        #[arg(long)]
572        resume: bool,
573    },
574
575    /// Server mode (future feature)
576    Server {
577        /// Port to bind to
578        #[arg(short, long, default_value = "8080")]
579        port: u16,
580
581        /// Host to bind to
582        #[arg(long, default_value = "127.0.0.1")]
583        host: String,
584    },
585
586    /// Interactive mode for real-time synthesis
587    Interactive {
588        /// Initial voice to use
589        #[arg(short, long)]
590        voice: Option<String>,
591
592        /// Disable audio playback (synthesis only)
593        #[arg(long)]
594        no_audio: bool,
595
596        /// Enable debug output
597        #[arg(long)]
598        debug: bool,
599
600        /// Load session from file
601        #[arg(long)]
602        load_session: Option<PathBuf>,
603
604        /// Auto-save session changes
605        #[arg(long)]
606        auto_save: bool,
607    },
608
609    /// Show detailed help and guides
610    Guide {
611        /// Command to get help for
612        command: Option<String>,
613
614        /// Show getting started guide
615        #[arg(long)]
616        getting_started: bool,
617
618        /// Show examples for all commands
619        #[arg(long)]
620        examples: bool,
621    },
622
623    /// Generate shell completion scripts
624    GenerateCompletion {
625        /// Shell to generate completion for
626        #[arg(value_enum)]
627        shell: clap_complete::Shell,
628
629        /// Output file (default: stdout)
630        #[arg(short, long)]
631        output: Option<std::path::PathBuf>,
632
633        /// Show installation instructions
634        #[arg(long)]
635        install_help: bool,
636
637        /// Generate installation script
638        #[arg(long)]
639        install_script: bool,
640
641        /// Show completion status for all shells
642        #[arg(long)]
643        status: bool,
644    },
645
646    /// Dataset management and validation commands
647    Dataset {
648        /// Dataset subcommand to execute
649        #[command(subcommand)]
650        command: DatasetCommands,
651    },
652
653    /// Real-time monitoring dashboard
654    Dashboard {
655        /// Update interval in milliseconds
656        #[arg(short, long, default_value = "500")]
657        interval: u64,
658    },
659
660    /// Cloud integration commands
661    Cloud {
662        /// Cloud subcommand to execute
663        #[command(subcommand)]
664        command: CloudCommands,
665    },
666
667    /// Telemetry management commands
668    Telemetry {
669        /// Telemetry subcommand to execute
670        #[command(subcommand)]
671        command: commands::telemetry::TelemetryCommands,
672    },
673
674    /// Start Language Server Protocol (LSP) server for editor integrations
675    Lsp {
676        /// Enable verbose logging
677        #[arg(long)]
678        verbose: bool,
679    },
680
681    /// Kokoro multilingual TTS commands (requires onnx feature)
682    #[cfg(feature = "onnx")]
683    Kokoro {
684        /// Kokoro subcommand to execute
685        #[command(subcommand)]
686        command: commands::kokoro::KokoroCommands,
687    },
688
689    /// Accuracy benchmarking commands
690    Accuracy {
691        /// Accuracy command configuration
692        #[command(flatten)]
693        command: commands::accuracy::AccuracyCommand,
694    },
695
696    /// Performance targets testing and monitoring
697    Performance {
698        /// Performance command configuration
699        #[command(flatten)]
700        command: commands::performance::PerformanceCommand,
701    },
702
703    /// Emotion control commands
704    #[cfg(feature = "emotion")]
705    Emotion {
706        /// Emotion subcommand to execute
707        #[command(subcommand)]
708        command: commands::emotion::EmotionCommand,
709    },
710
711    /// Voice cloning commands
712    #[cfg(feature = "cloning")]
713    Clone {
714        /// Cloning subcommand to execute
715        #[command(subcommand)]
716        command: commands::cloning::CloningCommand,
717    },
718
719    /// Voice conversion commands
720    #[cfg(feature = "conversion")]
721    Convert {
722        /// Conversion subcommand to execute
723        #[command(subcommand)]
724        command: commands::conversion::ConversionCommand,
725    },
726
727    /// Singing voice synthesis commands
728    #[cfg(feature = "singing")]
729    Sing {
730        /// Singing subcommand to execute
731        #[command(subcommand)]
732        command: commands::singing::SingingCommand,
733    },
734
735    /// 3D spatial audio commands
736    #[cfg(feature = "spatial")]
737    Spatial {
738        /// Spatial subcommand to execute
739        #[command(subcommand)]
740        command: commands::spatial::SpatialCommand,
741    },
742
743    /// Feature detection and capability reporting
744    Capabilities {
745        /// Capabilities subcommand to execute
746        #[command(subcommand)]
747        command: commands::capabilities::CapabilitiesCommand,
748    },
749
750    /// Checkpoint management commands
751    Checkpoint {
752        /// Checkpoint subcommand to execute
753        #[command(subcommand)]
754        command: commands::checkpoint::CheckpointCommands,
755    },
756
757    /// Advanced monitoring and debugging commands
758    Monitor {
759        /// Monitoring subcommand to execute
760        #[command(subcommand)]
761        command: commands::monitoring::MonitoringCommand,
762    },
763
764    /// Train models (vocoder, acoustic, g2p)
765    Train {
766        /// Training subcommand to execute
767        #[command(subcommand)]
768        command: commands::train::TrainCommands,
769    },
770
771    /// Convert model formats (ONNX, PyTorch → SafeTensors)
772    ConvertModel {
773        /// Input model path
774        input: PathBuf,
775
776        /// Output path (SafeTensors format)
777        #[arg(short, long)]
778        output: PathBuf,
779
780        /// Source format (auto-detect if not specified)
781        #[arg(long)]
782        from: Option<String>,
783
784        /// Model type (vocoder, acoustic, g2p)
785        #[arg(long)]
786        model_type: String,
787
788        /// Verify conversion by running test inference
789        #[arg(long)]
790        verify: bool,
791    },
792
793    /// Run vocoder inference (mel → audio)
794    VocoderInfer {
795        /// Path to vocoder checkpoint (SafeTensors)
796        checkpoint: PathBuf,
797
798        /// Path to mel spectrogram file (optional, generates dummy if omitted)
799        #[arg(long)]
800        mel: Option<PathBuf>,
801
802        /// Output audio file path
803        #[arg(short, long, default_value = "vocoder_output.wav")]
804        output: PathBuf,
805
806        /// Number of diffusion sampling steps
807        #[arg(long, default_value = "50")]
808        steps: usize,
809
810        /// Quality preset (fast, balanced, high)
811        #[arg(long)]
812        quality: Option<String>,
813
814        /// Batch processing: input directory
815        #[arg(long)]
816        batch_input: Option<PathBuf>,
817
818        /// Batch processing: output directory
819        #[arg(long)]
820        batch_output: Option<PathBuf>,
821
822        /// Show performance metrics
823        #[arg(long)]
824        metrics: bool,
825    },
826
827    /// Stream real-time text-to-speech synthesis
828    Stream {
829        /// Initial text to synthesize
830        text: Option<String>,
831
832        /// Target latency in milliseconds
833        #[arg(long, default_value = "100")]
834        latency: u64,
835
836        /// Chunk size in frames
837        #[arg(long, default_value = "512")]
838        chunk_size: usize,
839
840        /// Buffer size in chunks
841        #[arg(long, default_value = "4")]
842        buffer_chunks: usize,
843
844        /// Enable audio output
845        #[arg(long)]
846        play: bool,
847    },
848
849    /// Inspect and analyze model files
850    ModelInspect {
851        /// Model file path
852        model: PathBuf,
853
854        /// Show detailed layer information
855        #[arg(long)]
856        detailed: bool,
857
858        /// Export architecture to file
859        #[arg(long)]
860        export: Option<PathBuf>,
861
862        /// Verify model integrity
863        #[arg(long)]
864        verify: bool,
865    },
866
867    /// Export voice profile or preset
868    Export {
869        /// Export type (voice-profile, emotion-preset, config)
870        #[arg(long)]
871        export_type: String,
872
873        /// Source identifier (voice ID, preset name, etc.)
874        source: String,
875
876        /// Output file path
877        #[arg(short, long)]
878        output: PathBuf,
879
880        /// Include model weights
881        #[arg(long)]
882        include_weights: bool,
883    },
884
885    /// Import voice profile or preset
886    Import {
887        /// Import file path
888        input: PathBuf,
889
890        /// Installation name/identifier
891        #[arg(long)]
892        name: Option<String>,
893
894        /// Force overwrite if exists
895        #[arg(long)]
896        force: bool,
897
898        /// Validate before importing
899        #[arg(long, default_value = "true")]
900        validate: bool,
901    },
902
903    /// View command history and get suggestions
904    History {
905        /// Number of recent commands to show
906        #[arg(short = 'n', long, default_value = "20")]
907        limit: usize,
908
909        /// Show usage statistics
910        #[arg(long)]
911        stats: bool,
912
913        /// Show command suggestions based on history
914        #[arg(long)]
915        suggest: bool,
916
917        /// Clear all history
918        #[arg(long)]
919        clear: bool,
920    },
921
922    /// Workflow automation commands
923    Workflow {
924        /// Workflow subcommand to execute
925        #[command(subcommand)]
926        command: commands::workflow::WorkflowCommands,
927    },
928
929    /// Manage command aliases
930    Alias {
931        /// Alias subcommand
932        #[command(subcommand)]
933        command: AliasCommand,
934    },
935}
936
937/// Alias management subcommands
938#[derive(Subcommand)]
939pub enum AliasCommand {
940    /// Add a new alias
941    Add {
942        /// Alias name
943        name: String,
944
945        /// Command to execute (without 'voirs' prefix)
946        command: String,
947
948        /// Optional description
949        #[arg(short, long)]
950        description: Option<String>,
951    },
952
953    /// Remove an alias
954    Remove {
955        /// Alias name to remove
956        name: String,
957    },
958
959    /// List all aliases
960    List,
961
962    /// Show details of a specific alias
963    Show {
964        /// Alias name to show
965        name: String,
966    },
967
968    /// Clear all aliases
969    Clear,
970}
971
972/// CLI application implementation
973impl CliApp {
974    /// Run the CLI application
975    pub async fn run() -> Result<()> {
976        let app = Self::parse();
977
978        // Initialize logging
979        app.init_logging()?;
980
981        // Load configuration
982        let config = app.load_config().await?;
983
984        // Execute command
985        app.execute_command(config).await
986    }
987
988    /// Initialize logging based on verbosity
989    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) // Always write logs to stderr, not stdout
1004            .init();
1005
1006        Ok(())
1007    }
1008
1009    /// Load configuration from file or use defaults
1010    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            // Try to load from default locations
1016            self.load_config_from_default_locations().await?
1017        };
1018
1019        // Apply CLI overrides
1020        self.apply_cli_overrides(&mut config);
1021
1022        Ok(config)
1023    }
1024
1025    /// Load configuration from a specific file
1026    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        // Optimized format detection - use content analysis for better performance
1043        let config = match config_path.extension().and_then(|ext| ext.to_str()) {
1044            Some("toml") => {
1045                // For TOML files, try TOML first but allow fallback for compatibility
1046                toml::from_str(&content).or_else(|_| {
1047                    // Fallback to auto-detection for compatibility with tests
1048                    self.parse_config_auto_detect(&content)
1049                })?
1050            }
1051            Some("json") => {
1052                // For JSON files, try JSON first but allow fallback for compatibility
1053                serde_json::from_str(&content).or_else(|_| {
1054                    // Fallback to auto-detection for compatibility
1055                    self.parse_config_auto_detect(&content)
1056                })?
1057            }
1058            Some("yaml") | Some("yml") => {
1059                // For YAML files, try YAML first but allow fallback for compatibility
1060                serde_yaml::from_str(&content).or_else(|_| {
1061                    // Fallback to auto-detection for compatibility
1062                    self.parse_config_auto_detect(&content)
1063                })?
1064            }
1065            _ => {
1066                // Auto-detect format with optimized content analysis
1067                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    /// Load configuration from default locations
1079    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        // No config file found, use defaults
1090        tracing::info!("No configuration file found, using defaults");
1091        Ok(AppConfig::default())
1092    }
1093
1094    /// Parse configuration with optimized auto-detection
1095    fn parse_config_auto_detect(&self, content: &str) -> Result<AppConfig> {
1096        // Optimized format detection using content analysis
1097        // Check for format indicators without parsing the entire content
1098        let trimmed = content.trim_start();
1099
1100        if trimmed.starts_with('{') {
1101            // Likely JSON format
1102            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            // Likely YAML format (contains YAML indicators)
1110            serde_yaml::from_str(content).or_else(|yaml_err| {
1111                // Try TOML as fallback
1112                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            // Try TOML first, then JSON, then YAML
1121            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    /// Apply CLI overrides to configuration
1133    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            // Also update the synthesis config format
1149            config.pipeline.default_synthesis.output_format = (*format).into();
1150        }
1151    }
1152
1153    /// Execute the specified command
1154    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
1837/// Utility functions for CLI
1838pub mod utils {
1839    use crate::cli_types::CliAudioFormat;
1840    use std::path::Path;
1841    use voirs_sdk::AudioFormat;
1842
1843    /// Determine output format from file extension
1844    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    /// Generate output filename for text
1858    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
1876/// Get default configuration file paths in order of preference
1877fn get_default_config_paths() -> Vec<std::path::PathBuf> {
1878    let mut paths = Vec::new();
1879
1880    // 1. Current directory
1881    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    // 2. User config directory
1898    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    // 3. Home directory
1909    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}