voicepeak-cli 0.8.0

A command-line interface wrapper for VOICEPEAK text-to-speech software
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionParam {
    pub name: String,
    pub value: i32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoicePreset {
    pub name: String,
    pub narrator: String,
    pub emotions: Vec<EmotionParam>,
    pub pitch: Option<i32>,
    pub speed: Option<i32>,
}

impl VoicePreset {
    pub fn get_emotion_string(&self) -> String {
        if self.emotions.is_empty() {
            String::new()
        } else {
            self.emotions
                .iter()
                .map(|e| format!("{}={}", e.name, e.value))
                .collect::<Vec<_>>()
                .join(",")
        }
    }
}

impl EmotionParam {
    pub fn new(name: &str, value: i32) -> Self {
        Self {
            name: name.to_string(),
            value,
        }
    }
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
    pub default_preset: Option<String>,
    pub presets: Vec<VoicePreset>,
}

pub fn get_config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let home_dir = dirs::home_dir().ok_or("Could not find home directory")?;
    let config_dir = home_dir.join(".config").join("vp");

    if !config_dir.exists() {
        fs::create_dir_all(&config_dir)?;
    }

    Ok(config_dir.join("config.toml"))
}

pub fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
    let config_path = get_config_path()?;

    if !config_path.exists() {
        let default_config = Config::default();
        save_config(&default_config)?;
        return Ok(default_config);
    }

    let content = fs::read_to_string(&config_path)?;
    let config: Config = toml::from_str(&content)?;
    Ok(config)
}

pub fn save_config(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
    let config_path = get_config_path()?;
    let content = toml::to_string_pretty(config)?;
    fs::write(&config_path, content)?;
    Ok(())
}

pub fn get_presets_map(config: &Config) -> HashMap<String, VoicePreset> {
    config
        .presets
        .iter()
        .map(|preset| (preset.name.clone(), preset.clone()))
        .collect()
}

pub fn list_presets(config: &Config) {
    println!("Available presets:");
    for preset in &config.presets {
        let marker = if Some(&preset.name) == config.default_preset.as_ref() {
            " (default)"
        } else {
            ""
        };
        let emotion_display = if preset.emotions.is_empty() {
            "normal".to_string()
        } else {
            preset.get_emotion_string()
        };
        let pitch_display = preset
            .pitch
            .map(|p| format!(", pitch={}", p))
            .unwrap_or_default();
        let speed_display = preset
            .speed
            .map(|s| format!(", speed={}", s))
            .unwrap_or_default();
        println!(
            "  {} - {} ({}{}{}){}",
            preset.name, preset.narrator, emotion_display, pitch_display, speed_display, marker
        );
    }

    if let Some(ref default) = config.default_preset {
        println!("\nDefault preset: {}", default);
    } else {
        println!("\nNo default preset set");
    }
}