use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoicePersonality {
pub voice_id: String,
#[serde(default = "default_provider")]
pub provider: String,
#[serde(default = "default_speed")]
pub speed: f32,
#[serde(default)]
pub pitch: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub greeting_prefix: Option<String>,
#[serde(default = "default_language")]
pub language: String,
}
fn default_provider() -> String {
"local".to_string()
}
fn default_speed() -> f32 {
1.0
}
fn default_language() -> String {
"en".to_string()
}
impl Default for VoicePersonality {
fn default() -> Self {
Self {
voice_id: "default".to_string(),
provider: default_provider(),
speed: default_speed(),
pitch: 0.0,
greeting_prefix: None,
language: default_language(),
}
}
}
impl VoicePersonality {
pub fn validate(&self) -> Result<(), String> {
if self.speed < 0.5 || self.speed > 2.0 {
return Err(format!(
"speed must be between 0.5 and 2.0, got {}",
self.speed
));
}
if self.pitch < -1.0 || self.pitch > 1.0 {
return Err(format!(
"pitch must be between -1.0 and 1.0, got {}",
self.pitch
));
}
if self.voice_id.is_empty() {
return Err("voice_id must not be empty".into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_personality() {
let p = VoicePersonality::default();
assert_eq!(p.voice_id, "default");
assert_eq!(p.provider, "local");
assert!((p.speed - 1.0).abs() < f32::EPSILON);
assert!((p.pitch - 0.0).abs() < f32::EPSILON);
assert!(p.greeting_prefix.is_none());
assert_eq!(p.language, "en");
}
#[test]
fn serde_roundtrip() {
let p = VoicePersonality {
voice_id: "nova".into(),
provider: "openai".into(),
speed: 1.2,
pitch: -0.5,
greeting_prefix: Some("I am Agent Alpha.".into()),
language: "en-US".into(),
};
let json = serde_json::to_string(&p).unwrap();
let restored: VoicePersonality = serde_json::from_str(&json).unwrap();
assert_eq!(restored.voice_id, "nova");
assert_eq!(restored.provider, "openai");
assert!((restored.speed - 1.2).abs() < f32::EPSILON);
assert!((restored.pitch - (-0.5)).abs() < f32::EPSILON);
assert_eq!(restored.greeting_prefix.as_deref(), Some("I am Agent Alpha."));
assert_eq!(restored.language, "en-US");
}
#[test]
fn serde_defaults_applied() {
let json = r#"{"voice_id": "alloy"}"#;
let p: VoicePersonality = serde_json::from_str(json).unwrap();
assert_eq!(p.voice_id, "alloy");
assert_eq!(p.provider, "local");
assert!((p.speed - 1.0).abs() < f32::EPSILON);
assert!((p.pitch - 0.0).abs() < f32::EPSILON);
assert_eq!(p.language, "en");
}
#[test]
fn greeting_prefix_omitted_when_none() {
let p = VoicePersonality::default();
let json = serde_json::to_string(&p).unwrap();
assert!(!json.contains("greeting_prefix"));
}
#[test]
fn validate_ok() {
let p = VoicePersonality::default();
assert!(p.validate().is_ok());
}
#[test]
fn validate_speed_out_of_range() {
let mut p = VoicePersonality::default();
p.speed = 3.0;
assert!(p.validate().is_err());
assert!(p.validate().unwrap_err().contains("speed"));
}
#[test]
fn validate_pitch_out_of_range() {
let mut p = VoicePersonality::default();
p.pitch = -1.5;
assert!(p.validate().is_err());
assert!(p.validate().unwrap_err().contains("pitch"));
}
#[test]
fn validate_empty_voice_id() {
let mut p = VoicePersonality::default();
p.voice_id = "".into();
assert!(p.validate().is_err());
assert!(p.validate().unwrap_err().contains("voice_id"));
}
}