Skip to main content

voirs_cli/commands/batch/
mod.rs

1//! Batch processing commands.
2//!
3//! This module provides commands for batch processing multiple text inputs,
4//! supporting various input formats (TXT, CSV, JSON) and parallel processing.
5
6use crate::GlobalOptions;
7use std::path::{Path, PathBuf};
8use voirs_sdk::config::AppConfig;
9use voirs_sdk::{AudioFormat, QualityLevel, Result};
10
11pub mod files;
12pub mod parallel;
13pub mod resume;
14pub mod templates;
15
16/// Batch processing configuration
17#[derive(Debug, Clone)]
18pub struct BatchConfig {
19    /// Input file or directory
20    pub input_path: PathBuf,
21    /// Output directory
22    pub output_dir: PathBuf,
23    /// Number of parallel workers
24    pub workers: usize,
25    /// Quality level for synthesis
26    pub quality: QualityLevel,
27    /// Speaking rate
28    pub speaking_rate: f32,
29    /// Pitch adjustment
30    pub pitch: f32,
31    /// Volume gain
32    pub volume: f32,
33    /// Audio output format
34    pub format: AudioFormat,
35    /// Enable resume functionality
36    pub enable_resume: bool,
37    /// Maximum retries for failed items
38    pub max_retries: u32,
39}
40
41impl Default for BatchConfig {
42    fn default() -> Self {
43        Self {
44            input_path: PathBuf::new(),
45            output_dir: PathBuf::new(),
46            workers: num_cpus::get(),
47            quality: QualityLevel::High,
48            speaking_rate: 1.0,
49            pitch: 0.0,
50            volume: 0.0,
51            format: AudioFormat::Wav,
52            enable_resume: true,
53            max_retries: 3,
54        }
55    }
56}
57
58/// Configuration for batch processing operations
59pub struct BatchProcessArgs<'a> {
60    pub input: &'a Path,
61    pub output_dir: Option<&'a Path>,
62    pub workers: Option<usize>,
63    pub quality: QualityLevel,
64    pub rate: f32,
65    pub pitch: f32,
66    pub volume: f32,
67    pub resume: bool,
68}
69
70/// Run batch processing command
71pub async fn run_batch_process(
72    args: BatchProcessArgs<'_>,
73    config: &AppConfig,
74    global: &GlobalOptions,
75) -> Result<()> {
76    // Create batch configuration
77    let mut batch_config = BatchConfig {
78        input_path: args.input.to_path_buf(),
79        output_dir: args.output_dir.map(|p| p.to_path_buf()).unwrap_or_else(|| {
80            args.input
81                .parent()
82                .unwrap_or(std::path::Path::new("."))
83                .to_path_buf()
84        }),
85        workers: args.workers.unwrap_or_else(num_cpus::get),
86        quality: args.quality,
87        speaking_rate: args.rate,
88        pitch: args.pitch,
89        volume: args.volume,
90        format: AudioFormat::Wav, // Default to WAV format
91        enable_resume: args.resume,
92        max_retries: 3,
93    };
94
95    // Ensure output directory exists
96    std::fs::create_dir_all(&batch_config.output_dir)?;
97
98    if !global.quiet {
99        println!("Batch Processing Configuration:");
100        println!("==============================");
101        println!("Input: {}", batch_config.input_path.display());
102        println!("Output: {}", batch_config.output_dir.display());
103        println!("Workers: {}", batch_config.workers);
104        println!("Quality: {:?}", batch_config.quality);
105        println!("Resume: {}", batch_config.enable_resume);
106        println!();
107    }
108
109    // Detect input format and process
110    if batch_config.input_path.is_file() {
111        files::process_file(&batch_config, config, global).await
112    } else if batch_config.input_path.is_dir() {
113        files::process_directory(&batch_config, config, global).await
114    } else {
115        Err(voirs_sdk::VoirsError::config_error(format!(
116            "Input path does not exist: {}",
117            batch_config.input_path.display()
118        )))
119    }
120}
121
122/// Get supported input file extensions
123pub fn get_supported_extensions() -> Vec<&'static str> {
124    vec!["txt", "csv", "json", "jsonl"]
125}
126
127/// Check if file extension is supported
128pub fn is_supported_extension(path: &Path) -> bool {
129    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
130        get_supported_extensions().contains(&ext.to_lowercase().as_str())
131    } else {
132        false
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_batch_config_default() {
142        let config = BatchConfig::default();
143        assert!(config.workers > 0);
144        assert_eq!(config.quality, QualityLevel::High);
145        assert!(config.enable_resume);
146    }
147
148    #[test]
149    fn test_get_supported_extensions() {
150        let extensions = get_supported_extensions();
151        assert!(extensions.contains(&"txt"));
152        assert!(extensions.contains(&"csv"));
153        assert!(extensions.contains(&"json"));
154    }
155
156    #[test]
157    fn test_is_supported_extension() {
158        assert!(is_supported_extension(&PathBuf::from("test.txt")));
159        assert!(is_supported_extension(&PathBuf::from("data.csv")));
160        assert!(is_supported_extension(&PathBuf::from("config.json")));
161        assert!(!is_supported_extension(&PathBuf::from("image.png")));
162        assert!(!is_supported_extension(&PathBuf::from("noextension")));
163    }
164}