use crate::traits::{EvaluationResult, QualityEvaluationConfig, QualityMetric};
use crate::EvaluationError;
use async_trait::async_trait;
use scirs2_core::parallel_ops::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Instant;
use voirs_sdk::{AudioBuffer, LanguageCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EmotionType {
Neutral,
Happy,
Sad,
Angry,
Fearful,
Surprised,
Disgusted,
Excited,
Calm,
Loving,
Confident,
Disappointed,
}
impl Default for EmotionType {
fn default() -> Self {
Self::Neutral
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EmotionalIntensity {
VeryLow,
Low,
Medium,
High,
VeryHigh,
}
impl Default for EmotionalIntensity {
fn default() -> Self {
Self::Medium
}
}
impl EmotionalIntensity {
#[must_use]
pub fn to_value(self) -> f32 {
match self {
Self::VeryLow => 0.1,
Self::Low => 0.3,
Self::Medium => 0.5,
Self::High => 0.7,
Self::VeryHigh => 0.9,
}
}
#[must_use]
pub fn from_value(value: f32) -> Self {
match value {
v if v <= 0.2 => Self::VeryLow,
v if v <= 0.4 => Self::Low,
v if v <= 0.6 => Self::Medium,
v if v <= 0.8 => Self::High,
_ => Self::VeryHigh,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PersonalityTrait {
Extraversion,
Agreeableness,
Conscientiousness,
Neuroticism,
Openness,
Dominance,
Warmth,
Animation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExpressionStyle {
Conversational,
Professional,
Dramatic,
Broadcast,
Storytelling,
Educational,
Expressive,
Subtle,
}
impl Default for ExpressionStyle {
fn default() -> Self {
Self::Conversational
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CulturalRegion {
Western,
EastAsian,
SouthAsian,
MiddleEastern,
African,
LatinAmerican,
Nordic,
Mediterranean,
}
impl Default for CulturalRegion {
fn default() -> Self {
Self::Western
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionalEvaluationConfig {
pub emotion_recognition: bool,
pub expressiveness_transfer: bool,
pub style_consistency: bool,
pub personality_preservation: bool,
pub cross_cultural_expression: bool,
pub target_emotion: EmotionType,
pub target_intensity: EmotionalIntensity,
pub target_style: ExpressionStyle,
pub cultural_region: CulturalRegion,
pub personality_traits: Vec<PersonalityTrait>,
pub language: LanguageCode,
pub window_size: usize,
pub window_overlap: f32,
pub prosodic_analysis: bool,
pub spectral_analysis: bool,
pub temporal_analysis: bool,
}
impl Default for EmotionalEvaluationConfig {
fn default() -> Self {
Self {
emotion_recognition: true,
expressiveness_transfer: true,
style_consistency: true,
personality_preservation: true,
cross_cultural_expression: false,
target_emotion: EmotionType::default(),
target_intensity: EmotionalIntensity::default(),
target_style: ExpressionStyle::default(),
cultural_region: CulturalRegion::default(),
personality_traits: vec![
PersonalityTrait::Extraversion,
PersonalityTrait::Agreeableness,
PersonalityTrait::Warmth,
],
language: LanguageCode::EnUs,
window_size: 2048,
window_overlap: 0.5,
prosodic_analysis: true,
spectral_analysis: true,
temporal_analysis: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionRecognitionResult {
pub predicted_emotion: EmotionType,
pub confidence: f32,
pub intensity: EmotionalIntensity,
pub accuracy: f32,
pub emotion_probabilities: HashMap<EmotionType, f32>,
pub frame_predictions: Vec<EmotionFrameResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionFrameResult {
pub start_time: f32,
pub duration: f32,
pub emotion: EmotionType,
pub confidence: f32,
pub prosodic_features: ProsodicFeatures,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProsodicFeatures {
pub f0_mean: f32,
pub f0_std: f32,
pub f0_range: f32,
pub energy: f32,
pub speaking_rate: f32,
pub voice_quality: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpressivenessTransferResult {
pub transfer_accuracy: f32,
pub style_preservation: f32,
pub emotional_consistency: f32,
pub naturalness: f32,
pub intensity_match: f32,
pub feature_analysis: ExpressionFeatureAnalysis,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpressionFeatureAnalysis {
pub prosodic_expressiveness: f32,
pub spectral_expressiveness: f32,
pub temporal_expressiveness: f32,
pub voice_quality_expressiveness: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StyleConsistencyResult {
pub overall_consistency: f32,
pub temporal_consistency: f32,
pub cross_segment_consistency: f32,
pub style_stability: f32,
pub style_deviation: f32,
pub feature_consistency: HashMap<String, f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersonalityPreservationResult {
pub overall_preservation: f32,
pub trait_preservation: HashMap<PersonalityTrait, f32>,
pub cross_emotion_consistency: f32,
pub voice_characteristics_preservation: f32,
pub speaking_pattern_preservation: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossCulturalExpressionResult {
pub cultural_appropriateness: f32,
pub adaptation_accuracy: f32,
pub cultural_norm_compliance: f32,
pub cross_cultural_consistency: f32,
pub culture_specific_results: HashMap<CulturalRegion, f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionalSpeechEvaluationResult {
pub overall_score: f32,
pub emotion_recognition: Option<EmotionRecognitionResult>,
pub expressiveness_transfer: Option<ExpressivenessTransferResult>,
pub style_consistency: Option<StyleConsistencyResult>,
pub personality_preservation: Option<PersonalityPreservationResult>,
pub cross_cultural_expression: Option<CrossCulturalExpressionResult>,
pub processing_time_ms: u64,
pub config: EmotionalEvaluationConfig,
}
impl EmotionalSpeechEvaluationResult {
pub fn overall_score(&self) -> f32 {
self.overall_score
}
pub fn detailed_scores(&self) -> HashMap<String, f32> {
let mut scores = HashMap::new();
if let Some(ref emotion) = self.emotion_recognition {
scores.insert("emotion_accuracy".to_string(), emotion.accuracy);
scores.insert("emotion_confidence".to_string(), emotion.confidence);
}
if let Some(ref transfer) = self.expressiveness_transfer {
scores.insert("transfer_accuracy".to_string(), transfer.transfer_accuracy);
scores.insert(
"style_preservation".to_string(),
transfer.style_preservation,
);
scores.insert("naturalness".to_string(), transfer.naturalness);
}
if let Some(ref consistency) = self.style_consistency {
scores.insert(
"style_consistency".to_string(),
consistency.overall_consistency,
);
scores.insert(
"temporal_consistency".to_string(),
consistency.temporal_consistency,
);
}
if let Some(ref personality) = self.personality_preservation {
scores.insert(
"personality_preservation".to_string(),
personality.overall_preservation,
);
}
if let Some(ref cultural) = self.cross_cultural_expression {
scores.insert(
"cultural_appropriateness".to_string(),
cultural.cultural_appropriateness,
);
}
scores
}
pub fn processing_time(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.processing_time_ms)
}
}
#[async_trait]
pub trait EmotionalSpeechEvaluationTrait {
async fn evaluate_emotional_expression(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
config: &EmotionalEvaluationConfig,
) -> Result<EmotionalSpeechEvaluationResult, EvaluationError>;
async fn recognize_emotion(
&self,
audio: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<EmotionRecognitionResult, EvaluationError>;
async fn evaluate_expressiveness_transfer(
&self,
generated: &AudioBuffer,
reference: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<ExpressivenessTransferResult, EvaluationError>;
async fn analyze_style_consistency(
&self,
audio: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<StyleConsistencyResult, EvaluationError>;
async fn evaluate_personality_preservation(
&self,
generated: &AudioBuffer,
reference: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<PersonalityPreservationResult, EvaluationError>;
async fn evaluate_cross_cultural_expression(
&self,
audio: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<CrossCulturalExpressionResult, EvaluationError>;
}
pub struct EmotionalSpeechEvaluator {
config: EmotionalEvaluationConfig,
}
impl EmotionalSpeechEvaluator {
#[must_use]
pub fn new(config: EmotionalEvaluationConfig) -> Self {
Self { config }
}
#[must_use]
pub fn default() -> Self {
Self::new(EmotionalEvaluationConfig::default())
}
fn extract_prosodic_features(
&self,
audio: &AudioBuffer,
) -> Result<Vec<ProsodicFeatures>, EvaluationError> {
let samples = audio.samples();
let sample_rate = audio.sample_rate() as f32;
let window_size = self.config.window_size;
let hop_size = (window_size as f32 * (1.0 - self.config.window_overlap)) as usize;
let mut features = Vec::new();
for i in (0..samples.len()).step_by(hop_size) {
if i + window_size > samples.len() {
break;
}
let window = &samples[i..i + window_size];
let start_time = i as f32 / sample_rate;
let duration = window_size as f32 / sample_rate;
let f0_mean = self.extract_f0_mean(window, sample_rate);
let f0_std = self.extract_f0_std(window, sample_rate);
let f0_range = self.extract_f0_range(window, sample_rate);
let energy = self.extract_energy(window);
let speaking_rate = self.estimate_speaking_rate(window, sample_rate);
let voice_quality = self.extract_voice_quality(window, sample_rate);
features.push(ProsodicFeatures {
f0_mean,
f0_std,
f0_range,
energy,
speaking_rate,
voice_quality,
});
}
Ok(features)
}
fn extract_f0_mean(&self, window: &[f32], sample_rate: f32) -> f32 {
let min_period = (sample_rate / 500.0) as usize; let max_period = (sample_rate / 50.0) as usize;
let mut best_correlation = 0.0;
let mut best_period = min_period;
for period in min_period..=max_period.min(window.len() / 2) {
let correlation = self.autocorrelation(window, period);
if correlation > best_correlation {
best_correlation = correlation;
best_period = period;
}
}
if best_correlation > 0.3 {
sample_rate / best_period as f32
} else {
0.0 }
}
fn autocorrelation(&self, signal: &[f32], lag: usize) -> f32 {
if lag >= signal.len() {
return 0.0;
}
let mut sum = 0.0;
let mut sum_sq1 = 0.0;
let mut sum_sq2 = 0.0;
for i in 0..(signal.len() - lag) {
let x1 = signal[i];
let x2 = signal[i + lag];
sum += x1 * x2;
sum_sq1 += x1 * x1;
sum_sq2 += x2 * x2;
}
let denom = (sum_sq1 * sum_sq2).sqrt();
if denom > 0.0 {
sum / denom
} else {
0.0
}
}
fn extract_f0_std(&self, _window: &[f32], _sample_rate: f32) -> f32 {
15.0 }
fn extract_f0_range(&self, _window: &[f32], _sample_rate: f32) -> f32 {
50.0 }
fn extract_energy(&self, window: &[f32]) -> f32 {
let rms = (window.iter().map(|&x| x * x).sum::<f32>() / window.len() as f32).sqrt();
20.0 * rms.log10().max(-60.0) }
fn estimate_speaking_rate(&self, window: &[f32], sample_rate: f32) -> f32 {
let mut zero_crossings = 0;
for i in 1..window.len() {
if (window[i] >= 0.0) != (window[i - 1] >= 0.0) {
zero_crossings += 1;
}
}
let zcr = zero_crossings as f32 / (window.len() as f32 / sample_rate);
zcr / 100.0 }
fn extract_voice_quality(&self, window: &[f32], _sample_rate: f32) -> f32 {
let energy_low = window
.iter()
.take(window.len() / 4)
.map(|&x| x * x)
.sum::<f32>();
let energy_high = window
.iter()
.skip(3 * window.len() / 4)
.map(|&x| x * x)
.sum::<f32>();
if energy_high > 0.0 {
(energy_low / energy_high).log10()
} else {
0.0
}
}
fn classify_emotion(
&self,
features: &[ProsodicFeatures],
target_emotion: EmotionType,
) -> EmotionRecognitionResult {
let mut emotion_scores = HashMap::new();
let avg_f0 = features
.iter()
.map(|f| f.f0_mean)
.filter(|&f| f > 0.0)
.collect::<Vec<_>>();
let avg_f0_mean = if avg_f0.is_empty() {
0.0
} else {
avg_f0.iter().sum::<f32>() / avg_f0.len() as f32
};
let avg_energy = features.iter().map(|f| f.energy).sum::<f32>() / features.len() as f32;
let avg_speaking_rate =
features.iter().map(|f| f.speaking_rate).sum::<f32>() / features.len() as f32;
emotion_scores.insert(
EmotionType::Happy,
self.calculate_happiness_score(avg_f0_mean, avg_energy, avg_speaking_rate),
);
emotion_scores.insert(
EmotionType::Sad,
self.calculate_sadness_score(avg_f0_mean, avg_energy, avg_speaking_rate),
);
emotion_scores.insert(
EmotionType::Angry,
self.calculate_anger_score(avg_f0_mean, avg_energy, avg_speaking_rate),
);
emotion_scores.insert(
EmotionType::Neutral,
self.calculate_neutral_score(avg_f0_mean, avg_energy, avg_speaking_rate),
);
emotion_scores.insert(
EmotionType::Fearful,
self.calculate_fear_score(avg_f0_mean, avg_energy, avg_speaking_rate),
);
emotion_scores.insert(
EmotionType::Surprised,
self.calculate_surprise_score(avg_f0_mean, avg_energy, avg_speaking_rate),
);
let (predicted_emotion, confidence) = emotion_scores
.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(&emotion, &score)| (emotion, score))
.unwrap_or((EmotionType::Neutral, 0.5));
let accuracy = if predicted_emotion == target_emotion {
confidence
} else {
1.0 - confidence
};
let intensity = if avg_energy > -20.0 && avg_f0_mean > 150.0 {
EmotionalIntensity::High
} else if avg_energy > -30.0 && avg_f0_mean > 120.0 {
EmotionalIntensity::Medium
} else {
EmotionalIntensity::Low
};
let frame_predictions = features
.iter()
.enumerate()
.map(|(i, feature)| {
EmotionFrameResult {
start_time: i as f32 * 0.025, duration: 0.025,
emotion: predicted_emotion,
confidence: confidence * 0.9 + scirs2_core::random::random::<f32>() * 0.2, prosodic_features: feature.clone(),
}
})
.collect();
EmotionRecognitionResult {
predicted_emotion,
confidence,
intensity,
accuracy,
emotion_probabilities: emotion_scores,
frame_predictions,
}
}
fn calculate_happiness_score(&self, f0_mean: f32, energy: f32, speaking_rate: f32) -> f32 {
let f0_score = if f0_mean > 150.0 { 0.8 } else { 0.3 };
let energy_score = if energy > -25.0 { 0.7 } else { 0.3 };
let rate_score = if speaking_rate > 4.0 { 0.6 } else { 0.4 };
(f0_score + energy_score + rate_score) / 3.0
}
fn calculate_sadness_score(&self, f0_mean: f32, energy: f32, speaking_rate: f32) -> f32 {
let f0_score = if f0_mean < 120.0 { 0.8 } else { 0.2 };
let energy_score = if energy < -35.0 { 0.7 } else { 0.3 };
let rate_score = if speaking_rate < 3.0 { 0.6 } else { 0.4 };
(f0_score + energy_score + rate_score) / 3.0
}
fn calculate_anger_score(&self, f0_mean: f32, energy: f32, speaking_rate: f32) -> f32 {
let f0_score = if f0_mean > 140.0 { 0.7 } else { 0.3 };
let energy_score = if energy > -20.0 { 0.8 } else { 0.2 };
let rate_score = if speaking_rate > 4.5 { 0.7 } else { 0.3 };
(f0_score + energy_score + rate_score) / 3.0
}
fn calculate_neutral_score(&self, f0_mean: f32, energy: f32, speaking_rate: f32) -> f32 {
let f0_score = if (120.0..=150.0).contains(&f0_mean) {
0.8
} else {
0.4
};
let energy_score = if (-35.0..=-25.0).contains(&energy) {
0.7
} else {
0.4
};
let rate_score = if (3.0..=4.0).contains(&speaking_rate) {
0.6
} else {
0.4
};
(f0_score + energy_score + rate_score) / 3.0
}
fn calculate_fear_score(&self, f0_mean: f32, energy: f32, speaking_rate: f32) -> f32 {
let f0_score = if f0_mean > 160.0 { 0.7 } else { 0.3 };
let energy_score = if energy > -30.0 { 0.6 } else { 0.4 };
let rate_score = if speaking_rate > 4.0 { 0.5 } else { 0.5 };
(f0_score + energy_score + rate_score) / 3.0
}
fn calculate_surprise_score(&self, f0_mean: f32, energy: f32, speaking_rate: f32) -> f32 {
let f0_score = if f0_mean > 170.0 { 0.8 } else { 0.3 };
let energy_score = if energy > -25.0 { 0.7 } else { 0.3 };
let rate_score = if speaking_rate < 2.0 || speaking_rate > 5.0 {
0.6
} else {
0.4
};
(f0_score + energy_score + rate_score) / 3.0
}
}
#[async_trait]
impl EmotionalSpeechEvaluationTrait for EmotionalSpeechEvaluator {
async fn evaluate_emotional_expression(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
config: &EmotionalEvaluationConfig,
) -> Result<EmotionalSpeechEvaluationResult, EvaluationError> {
let start_time = Instant::now();
let mut emotion_recognition = None;
let mut expressiveness_transfer = None;
let mut style_consistency = None;
let mut personality_preservation = None;
let mut cross_cultural_expression = None;
if config.emotion_recognition {
emotion_recognition = Some(self.recognize_emotion(audio, config).await?);
}
if config.expressiveness_transfer {
if let Some(ref_audio) = reference {
expressiveness_transfer = Some(
self.evaluate_expressiveness_transfer(audio, ref_audio, config)
.await?,
);
}
}
if config.style_consistency {
style_consistency = Some(self.analyze_style_consistency(audio, config).await?);
}
if config.personality_preservation {
if let Some(ref_audio) = reference {
personality_preservation = Some(
self.evaluate_personality_preservation(audio, ref_audio, config)
.await?,
);
}
}
if config.cross_cultural_expression {
cross_cultural_expression = Some(
self.evaluate_cross_cultural_expression(audio, config)
.await?,
);
}
let mut scores = Vec::new();
if let Some(ref er) = emotion_recognition {
scores.push(er.accuracy);
}
if let Some(ref et) = expressiveness_transfer {
scores.push(et.transfer_accuracy);
}
if let Some(ref sc) = style_consistency {
scores.push(sc.overall_consistency);
}
if let Some(ref pp) = personality_preservation {
scores.push(pp.overall_preservation);
}
if let Some(ref ce) = cross_cultural_expression {
scores.push(ce.cultural_appropriateness);
}
let overall_score = if scores.is_empty() {
0.0
} else {
scores.iter().sum::<f32>() / scores.len() as f32
};
let processing_time_ms = start_time.elapsed().as_millis() as u64;
Ok(EmotionalSpeechEvaluationResult {
overall_score,
emotion_recognition,
expressiveness_transfer,
style_consistency,
personality_preservation,
cross_cultural_expression,
processing_time_ms,
config: config.clone(),
})
}
async fn recognize_emotion(
&self,
audio: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<EmotionRecognitionResult, EvaluationError> {
let features = self.extract_prosodic_features(audio)?;
let result = self.classify_emotion(&features, config.target_emotion);
Ok(result)
}
async fn evaluate_expressiveness_transfer(
&self,
generated: &AudioBuffer,
reference: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<ExpressivenessTransferResult, EvaluationError> {
let gen_features = self.extract_prosodic_features(generated)?;
let ref_features = self.extract_prosodic_features(reference)?;
let transfer_accuracy = self.calculate_feature_similarity(&gen_features, &ref_features);
let style_preservation = 0.85;
let emotional_consistency = 0.80;
let naturalness = 0.82;
let intensity_match = 0.78;
let feature_analysis = ExpressionFeatureAnalysis {
prosodic_expressiveness: 0.85,
spectral_expressiveness: 0.80,
temporal_expressiveness: 0.82,
voice_quality_expressiveness: 0.78,
};
Ok(ExpressivenessTransferResult {
transfer_accuracy,
style_preservation,
emotional_consistency,
naturalness,
intensity_match,
feature_analysis,
})
}
async fn analyze_style_consistency(
&self,
audio: &AudioBuffer,
_config: &EmotionalEvaluationConfig,
) -> Result<StyleConsistencyResult, EvaluationError> {
let features = self.extract_prosodic_features(audio)?;
let overall_consistency = self.calculate_temporal_consistency(&features);
let temporal_consistency = overall_consistency;
let cross_segment_consistency = overall_consistency * 0.95;
let style_stability = overall_consistency * 0.90;
let style_deviation = 1.0 - overall_consistency;
let mut feature_consistency = HashMap::new();
feature_consistency.insert("f0_consistency".to_string(), 0.82);
feature_consistency.insert("energy_consistency".to_string(), 0.85);
feature_consistency.insert("rate_consistency".to_string(), 0.78);
Ok(StyleConsistencyResult {
overall_consistency,
temporal_consistency,
cross_segment_consistency,
style_stability,
style_deviation,
feature_consistency,
})
}
async fn evaluate_personality_preservation(
&self,
generated: &AudioBuffer,
reference: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<PersonalityPreservationResult, EvaluationError> {
let gen_features = self.extract_prosodic_features(generated)?;
let ref_features = self.extract_prosodic_features(reference)?;
let overall_preservation = self.calculate_feature_similarity(&gen_features, &ref_features);
let mut trait_preservation = HashMap::new();
for personality_trait in &config.personality_traits {
let score = match personality_trait {
PersonalityTrait::Extraversion => 0.85,
PersonalityTrait::Agreeableness => 0.80,
PersonalityTrait::Warmth => 0.88,
_ => 0.75,
};
trait_preservation.insert(*personality_trait, score);
}
let cross_emotion_consistency = 0.82;
let voice_characteristics_preservation = 0.86;
let speaking_pattern_preservation = 0.79;
Ok(PersonalityPreservationResult {
overall_preservation,
trait_preservation,
cross_emotion_consistency,
voice_characteristics_preservation,
speaking_pattern_preservation,
})
}
async fn evaluate_cross_cultural_expression(
&self,
_audio: &AudioBuffer,
config: &EmotionalEvaluationConfig,
) -> Result<CrossCulturalExpressionResult, EvaluationError> {
let cultural_appropriateness = match config.cultural_region {
CulturalRegion::Western => 0.85,
CulturalRegion::EastAsian => 0.80,
CulturalRegion::SouthAsian => 0.75,
_ => 0.70,
};
let adaptation_accuracy = 0.78;
let cultural_norm_compliance = 0.82;
let cross_cultural_consistency = 0.76;
let mut culture_specific_results = HashMap::new();
culture_specific_results.insert(CulturalRegion::Western, 0.85);
culture_specific_results.insert(CulturalRegion::EastAsian, 0.80);
culture_specific_results.insert(CulturalRegion::SouthAsian, 0.75);
Ok(CrossCulturalExpressionResult {
cultural_appropriateness,
adaptation_accuracy,
cultural_norm_compliance,
cross_cultural_consistency,
culture_specific_results,
})
}
}
impl EmotionalSpeechEvaluator {
fn calculate_feature_similarity(
&self,
features1: &[ProsodicFeatures],
features2: &[ProsodicFeatures],
) -> f32 {
if features1.is_empty() || features2.is_empty() {
return 0.0;
}
let len = features1.len().min(features2.len());
let mut similarities = Vec::new();
for i in 0..len {
let f1 = &features1[i];
let f2 = &features2[i];
let f0_sim = 1.0 - ((f1.f0_mean - f2.f0_mean).abs() / 200.0).min(1.0);
let energy_sim = 1.0 - ((f1.energy - f2.energy).abs() / 60.0).min(1.0);
let rate_sim = 1.0 - ((f1.speaking_rate - f2.speaking_rate).abs() / 10.0).min(1.0);
let similarity = (f0_sim + energy_sim + rate_sim) / 3.0;
similarities.push(similarity);
}
similarities.iter().sum::<f32>() / similarities.len() as f32
}
fn calculate_temporal_consistency(&self, features: &[ProsodicFeatures]) -> f32 {
if features.len() < 2 {
return 1.0;
}
let mut consistencies = Vec::new();
for i in 1..features.len() {
let f1 = &features[i - 1];
let f2 = &features[i];
let f0_consistency = 1.0 - ((f1.f0_mean - f2.f0_mean).abs() / 100.0).min(1.0);
let energy_consistency = 1.0 - ((f1.energy - f2.energy).abs() / 30.0).min(1.0);
let rate_consistency =
1.0 - ((f1.speaking_rate - f2.speaking_rate).abs() / 5.0).min(1.0);
let consistency = (f0_consistency + energy_consistency + rate_consistency) / 3.0;
consistencies.push(consistency);
}
consistencies.iter().sum::<f32>() / consistencies.len() as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
use voirs_sdk::AudioBuffer;
#[test]
fn test_emotion_type_default() {
assert_eq!(EmotionType::default(), EmotionType::Neutral);
}
#[test]
fn test_emotional_intensity_conversion() {
assert_eq!(EmotionalIntensity::High.to_value(), 0.7);
assert_eq!(
EmotionalIntensity::from_value(0.9),
EmotionalIntensity::VeryHigh
);
assert_eq!(EmotionalIntensity::from_value(0.3), EmotionalIntensity::Low);
}
#[test]
fn test_emotional_evaluation_config_default() {
let config = EmotionalEvaluationConfig::default();
assert!(config.emotion_recognition);
assert!(config.expressiveness_transfer);
assert_eq!(config.target_emotion, EmotionType::Neutral);
assert_eq!(config.window_size, 2048);
}
#[tokio::test]
async fn test_emotion_recognition() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config.clone());
let samples = vec![0.1; 16000];
let audio = AudioBuffer::mono(samples, 16000);
let result = evaluator.recognize_emotion(&audio, &config).await;
assert!(result.is_ok());
let emotion_result = result.unwrap();
assert!(!emotion_result.emotion_probabilities.is_empty());
assert!(emotion_result.confidence >= 0.0 && emotion_result.confidence <= 1.0);
}
#[tokio::test]
async fn test_emotional_speech_evaluation() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config.clone());
let samples = vec![0.1; 16000];
let audio = AudioBuffer::mono(samples, 16000);
let result = evaluator
.evaluate_emotional_expression(&audio, None, &config)
.await;
assert!(result.is_ok());
let eval_result = result.unwrap();
assert!(eval_result.overall_score >= 0.0 && eval_result.overall_score <= 1.0);
assert!(eval_result.emotion_recognition.is_some());
}
#[tokio::test]
async fn test_expressiveness_transfer_evaluation() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config.clone());
let samples1 = vec![0.1; 16000];
let samples2 = vec![0.12; 16000];
let audio1 = AudioBuffer::mono(samples1, 16000);
let audio2 = AudioBuffer::mono(samples2, 16000);
let result = evaluator
.evaluate_expressiveness_transfer(&audio1, &audio2, &config)
.await;
assert!(result.is_ok());
let transfer_result = result.unwrap();
assert!(
transfer_result.transfer_accuracy >= 0.0 && transfer_result.transfer_accuracy <= 1.0
);
assert!(
transfer_result.style_preservation >= 0.0 && transfer_result.style_preservation <= 1.0
);
}
#[test]
fn test_prosodic_feature_extraction() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config);
let mut samples = Vec::new();
let sample_rate = 16000.0;
let frequency = 440.0; for i in 0..16000 {
let t = i as f32 / sample_rate;
samples.push(0.5 * (2.0 * std::f32::consts::PI * frequency * t).sin());
}
let audio = AudioBuffer::mono(samples, 16000);
let features = evaluator.extract_prosodic_features(&audio);
assert!(features.is_ok());
let features = features.unwrap();
assert!(!features.is_empty());
for feature in &features {
assert!(feature.f0_mean >= 0.0);
assert!(feature.energy > -60.0); assert!(feature.speaking_rate >= 0.0);
}
}
#[test]
fn test_autocorrelation() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config);
let signal = vec![1.0, 2.0, 3.0, 2.0, 1.0];
let correlation = evaluator.autocorrelation(&signal, 0);
assert!((correlation - 1.0).abs() < 0.001);
let correlation_lag1 = evaluator.autocorrelation(&signal, 1);
assert!(correlation_lag1 < 1.0);
}
#[test]
fn test_emotion_classification_scores() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config);
let happiness = evaluator.calculate_happiness_score(180.0, -20.0, 5.0);
assert!(happiness > 0.6);
let sadness = evaluator.calculate_sadness_score(100.0, -40.0, 2.0);
assert!(sadness > 0.6);
let neutral = evaluator.calculate_neutral_score(135.0, -30.0, 3.5);
assert!(neutral > 0.5);
}
#[test]
fn test_feature_similarity_calculation() {
let config = EmotionalEvaluationConfig::default();
let evaluator = EmotionalSpeechEvaluator::new(config);
let features1 = vec![ProsodicFeatures {
f0_mean: 150.0,
f0_std: 15.0,
f0_range: 50.0,
energy: -25.0,
speaking_rate: 3.5,
voice_quality: 0.5,
}];
let features2 = vec![ProsodicFeatures {
f0_mean: 155.0,
f0_std: 18.0,
f0_range: 55.0,
energy: -23.0,
speaking_rate: 3.8,
voice_quality: 0.6,
}];
let similarity = evaluator.calculate_feature_similarity(&features1, &features2);
assert!(similarity > 0.8);
let features3 = vec![ProsodicFeatures {
f0_mean: 80.0,
f0_std: 5.0,
f0_range: 20.0,
energy: -50.0,
speaking_rate: 1.0,
voice_quality: 0.1,
}];
let similarity_diff = evaluator.calculate_feature_similarity(&features1, &features3);
assert!(similarity_diff < similarity); }
}