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    /// ONNX model tools (inspect, profile, graph export, info)
690    #[cfg(feature = "onnx")]
691    Onnx {
692        /// ONNX tool subcommand to execute
693        #[command(subcommand)]
694        command: commands::onnx_tools::OnnxCommand,
695    },
696
697    /// Accuracy benchmarking commands
698    Accuracy {
699        /// Accuracy command configuration
700        #[command(flatten)]
701        command: commands::accuracy::AccuracyCommand,
702    },
703
704    /// Performance targets testing and monitoring
705    Performance {
706        /// Performance command configuration
707        #[command(flatten)]
708        command: commands::performance::PerformanceCommand,
709    },
710
711    /// Emotion control commands
712    #[cfg(feature = "emotion")]
713    Emotion {
714        /// Emotion subcommand to execute
715        #[command(subcommand)]
716        command: commands::emotion::EmotionCommand,
717    },
718
719    /// Voice cloning commands
720    #[cfg(feature = "cloning")]
721    Clone {
722        /// Cloning subcommand to execute
723        #[command(subcommand)]
724        command: commands::cloning::CloningCommand,
725    },
726
727    /// Voice conversion commands
728    #[cfg(feature = "conversion")]
729    Convert {
730        /// Conversion subcommand to execute
731        #[command(subcommand)]
732        command: commands::conversion::ConversionCommand,
733    },
734
735    /// Singing voice synthesis commands
736    #[cfg(feature = "singing")]
737    Sing {
738        /// Singing subcommand to execute
739        #[command(subcommand)]
740        command: commands::singing::SingingCommand,
741    },
742
743    /// 3D spatial audio commands
744    #[cfg(feature = "spatial")]
745    Spatial {
746        /// Spatial subcommand to execute
747        #[command(subcommand)]
748        command: commands::spatial::SpatialCommand,
749    },
750
751    /// Feature detection and capability reporting
752    Capabilities {
753        /// Capabilities subcommand to execute
754        #[command(subcommand)]
755        command: commands::capabilities::CapabilitiesCommand,
756    },
757
758    /// Checkpoint management commands
759    Checkpoint {
760        /// Checkpoint subcommand to execute
761        #[command(subcommand)]
762        command: commands::checkpoint::CheckpointCommands,
763    },
764
765    /// Advanced monitoring and debugging commands
766    Monitor {
767        /// Monitoring subcommand to execute
768        #[command(subcommand)]
769        command: commands::monitoring::MonitoringCommand,
770    },
771
772    /// Train models (vocoder, acoustic, g2p)
773    Train {
774        /// Training subcommand to execute
775        #[command(subcommand)]
776        command: commands::train::TrainCommands,
777    },
778
779    /// Convert model formats (ONNX, PyTorch → SafeTensors)
780    ConvertModel {
781        /// Input model path
782        input: PathBuf,
783
784        /// Output path (SafeTensors format)
785        #[arg(short, long)]
786        output: PathBuf,
787
788        /// Source format (auto-detect if not specified)
789        #[arg(long)]
790        from: Option<String>,
791
792        /// Model type (vocoder, acoustic, g2p)
793        #[arg(long)]
794        model_type: String,
795
796        /// Verify conversion by running test inference
797        #[arg(long)]
798        verify: bool,
799    },
800
801    /// Run vocoder inference (mel → audio)
802    VocoderInfer {
803        /// Path to vocoder checkpoint (SafeTensors)
804        checkpoint: PathBuf,
805
806        /// Path to mel spectrogram file (optional, generates dummy if omitted)
807        #[arg(long)]
808        mel: Option<PathBuf>,
809
810        /// Output audio file path
811        #[arg(short, long, default_value = "vocoder_output.wav")]
812        output: PathBuf,
813
814        /// Number of diffusion sampling steps
815        #[arg(long, default_value = "50")]
816        steps: usize,
817
818        /// Quality preset (fast, balanced, high)
819        #[arg(long)]
820        quality: Option<String>,
821
822        /// Batch processing: input directory
823        #[arg(long)]
824        batch_input: Option<PathBuf>,
825
826        /// Batch processing: output directory
827        #[arg(long)]
828        batch_output: Option<PathBuf>,
829
830        /// Show performance metrics
831        #[arg(long)]
832        metrics: bool,
833    },
834
835    /// Stream real-time text-to-speech synthesis
836    Stream {
837        /// Initial text to synthesize
838        text: Option<String>,
839
840        /// Target latency in milliseconds
841        #[arg(long, default_value = "100")]
842        latency: u64,
843
844        /// Chunk size in frames
845        #[arg(long, default_value = "512")]
846        chunk_size: usize,
847
848        /// Buffer size in chunks
849        #[arg(long, default_value = "4")]
850        buffer_chunks: usize,
851
852        /// Enable audio output
853        #[arg(long)]
854        play: bool,
855    },
856
857    /// Inspect and analyze model files
858    ModelInspect {
859        /// Model file path
860        model: PathBuf,
861
862        /// Show detailed layer information
863        #[arg(long)]
864        detailed: bool,
865
866        /// Export architecture to file
867        #[arg(long)]
868        export: Option<PathBuf>,
869
870        /// Verify model integrity
871        #[arg(long)]
872        verify: bool,
873    },
874
875    /// Export voice profile or preset
876    Export {
877        /// Export type (voice-profile, emotion-preset, config)
878        #[arg(long)]
879        export_type: String,
880
881        /// Source identifier (voice ID, preset name, etc.)
882        source: String,
883
884        /// Output file path
885        #[arg(short, long)]
886        output: PathBuf,
887
888        /// Include model weights
889        #[arg(long)]
890        include_weights: bool,
891    },
892
893    /// Import voice profile or preset
894    Import {
895        /// Import file path
896        input: PathBuf,
897
898        /// Installation name/identifier
899        #[arg(long)]
900        name: Option<String>,
901
902        /// Force overwrite if exists
903        #[arg(long)]
904        force: bool,
905
906        /// Validate before importing
907        #[arg(long, default_value = "true")]
908        validate: bool,
909    },
910
911    /// View command history and get suggestions
912    History {
913        /// Number of recent commands to show
914        #[arg(short = 'n', long, default_value = "20")]
915        limit: usize,
916
917        /// Show usage statistics
918        #[arg(long)]
919        stats: bool,
920
921        /// Show command suggestions based on history
922        #[arg(long)]
923        suggest: bool,
924
925        /// Clear all history
926        #[arg(long)]
927        clear: bool,
928    },
929
930    /// Workflow automation commands
931    Workflow {
932        /// Workflow subcommand to execute
933        #[command(subcommand)]
934        command: commands::workflow::WorkflowCommands,
935    },
936
937    /// Manage command aliases
938    Alias {
939        /// Alias subcommand
940        #[command(subcommand)]
941        command: AliasCommand,
942    },
943}
944
945/// Alias management subcommands
946#[derive(Subcommand)]
947pub enum AliasCommand {
948    /// Add a new alias
949    Add {
950        /// Alias name
951        name: String,
952
953        /// Command to execute (without 'voirs' prefix)
954        command: String,
955
956        /// Optional description
957        #[arg(short, long)]
958        description: Option<String>,
959    },
960
961    /// Remove an alias
962    Remove {
963        /// Alias name to remove
964        name: String,
965    },
966
967    /// List all aliases
968    List,
969
970    /// Show details of a specific alias
971    Show {
972        /// Alias name to show
973        name: String,
974    },
975
976    /// Clear all aliases
977    Clear,
978}
979
980/// CLI application implementation
981impl CliApp {
982    /// Run the CLI application
983    pub async fn run() -> Result<()> {
984        let app = Self::parse();
985
986        // Initialize logging
987        app.init_logging()?;
988
989        // Load configuration
990        let config = app.load_config().await?;
991
992        // Execute command
993        app.execute_command(config).await
994    }
995
996    /// Initialize logging based on verbosity
997    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) // Always write logs to stderr, not stdout
1012            .init();
1013
1014        Ok(())
1015    }
1016
1017    /// Load configuration from file or use defaults
1018    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            // Try to load from default locations
1024            self.load_config_from_default_locations().await?
1025        };
1026
1027        // Apply CLI overrides
1028        self.apply_cli_overrides(&mut config);
1029
1030        Ok(config)
1031    }
1032
1033    /// Load configuration from a specific file
1034    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        // Optimized format detection - use content analysis for better performance
1051        let config = match config_path.extension().and_then(|ext| ext.to_str()) {
1052            Some("toml") => {
1053                // For TOML files, try TOML first but allow fallback for compatibility
1054                toml::from_str(&content).or_else(|_| {
1055                    // Fallback to auto-detection for compatibility with tests
1056                    self.parse_config_auto_detect(&content)
1057                })?
1058            }
1059            Some("json") => {
1060                // For JSON files, try JSON first but allow fallback for compatibility
1061                serde_json::from_str(&content).or_else(|_| {
1062                    // Fallback to auto-detection for compatibility
1063                    self.parse_config_auto_detect(&content)
1064                })?
1065            }
1066            Some("yaml") | Some("yml") => {
1067                // For YAML files, try YAML first but allow fallback for compatibility
1068                serde_yaml::from_str(&content).or_else(|_| {
1069                    // Fallback to auto-detection for compatibility
1070                    self.parse_config_auto_detect(&content)
1071                })?
1072            }
1073            _ => {
1074                // Auto-detect format with optimized content analysis
1075                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    /// Load configuration from default locations
1087    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        // No config file found, use defaults
1098        tracing::info!("No configuration file found, using defaults");
1099        Ok(AppConfig::default())
1100    }
1101
1102    /// Parse configuration with optimized auto-detection
1103    fn parse_config_auto_detect(&self, content: &str) -> Result<AppConfig> {
1104        // Optimized format detection using content analysis
1105        // Check for format indicators without parsing the entire content
1106        let trimmed = content.trim_start();
1107
1108        if trimmed.starts_with('{') {
1109            // Likely JSON format
1110            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            // Likely YAML format (contains YAML indicators)
1118            serde_yaml::from_str(content).or_else(|yaml_err| {
1119                // Try TOML as fallback
1120                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            // Try TOML first, then JSON, then YAML
1129            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    /// Apply CLI overrides to configuration
1141    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            // Also update the synthesis config format
1157            config.pipeline.default_synthesis.output_format = (*format).into();
1158        }
1159    }
1160
1161    /// Execute the specified command
1162    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
1851/// Utility functions for CLI
1852pub mod utils {
1853    use crate::cli_types::CliAudioFormat;
1854    use std::path::Path;
1855    use voirs_sdk::AudioFormat;
1856
1857    /// Determine output format from file extension
1858    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    /// Generate output filename for text
1872    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
1890/// Get default configuration file paths in order of preference
1891fn get_default_config_paths() -> Vec<std::path::PathBuf> {
1892    let mut paths = Vec::new();
1893
1894    // 1. Current directory
1895    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    // 2. User config directory
1912    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    // 3. Home directory
1923    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}