use crate::traits::{QualityMetric, QualityScore};
use crate::EvaluationError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use voirs_sdk::AudioBuffer;
#[derive(Debug, Clone)]
pub struct AdvancedQualityEvaluator {
config: AdvancedQualityConfig,
adaptive_weights: HashMap<String, f64>,
quality_history: Vec<QualityMeasurement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedQualityConfig {
pub adaptive_weighting: bool,
pub predictive_assessment: bool,
pub history_window: usize,
pub confidence_threshold: f64,
pub cross_modal_analysis: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiDomainQualityScore {
pub overall_score: f64,
pub perceptual: PerceptualDomainScores,
pub intelligibility: IntelligibilityDomainScores,
pub naturalness: NaturalnessDomainScores,
pub technical: TechnicalDomainScores,
pub confidence: f64,
pub trend_prediction: Option<QualityTrendPrediction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerceptualDomainScores {
pub loudness_quality: f64,
pub spectral_balance: f64,
pub temporal_coherence: f64,
pub dynamic_range: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntelligibilityDomainScores {
pub phoneme_clarity: f64,
pub word_boundaries: f64,
pub prosodic_clarity: f64,
pub articulation: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NaturalnessDomainScores {
pub voice_quality: f64,
pub emotional_appropriateness: f64,
pub speaking_rate: f64,
pub intonation: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TechnicalDomainScores {
pub snr: f64,
pub thd: f64,
pub frequency_response: f64,
pub dynamic_range: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityTrendPrediction {
pub predicted_score: f64,
pub prediction_confidence: f64,
pub drift_direction: QualityDriftDirection,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum QualityDriftDirection {
Improving,
Stable,
Degrading,
Unstable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityMeasurement {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub score: MultiDomainQualityScore,
pub content_type: String,
pub audio_metadata: AudioMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioMetadata {
pub duration: f64,
pub sample_rate: u32,
pub channels: u32,
pub content_type: String,
pub speaker_characteristics: SpeakerCharacteristics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpeakerCharacteristics {
pub age_group: String,
pub gender: String,
pub accent: String,
pub style: String,
}
impl Default for AdvancedQualityConfig {
fn default() -> Self {
Self {
adaptive_weighting: true,
predictive_assessment: true,
history_window: 50,
confidence_threshold: 0.8,
cross_modal_analysis: true,
}
}
}
impl AdvancedQualityEvaluator {
pub fn new(config: AdvancedQualityConfig) -> Self {
Self {
config,
adaptive_weights: HashMap::new(),
quality_history: Vec::new(),
}
}
pub async fn evaluate_quality(
&mut self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<MultiDomainQualityScore, EvaluationError> {
let metadata = self.extract_audio_metadata(audio)?;
let perceptual = self.evaluate_perceptual_domain(audio, reference).await?;
let intelligibility = self
.evaluate_intelligibility_domain(audio, reference)
.await?;
let naturalness = self.evaluate_naturalness_domain(audio, reference).await?;
let technical = self.evaluate_technical_domain(audio, reference).await?;
let weights = self.calculate_adaptive_weights(&metadata.content_type);
let overall_score = self.compute_weighted_score(
&perceptual,
&intelligibility,
&naturalness,
&technical,
&weights,
);
let confidence = self.calculate_confidence(
&metadata,
&[
perceptual.loudness_quality,
intelligibility.phoneme_clarity,
naturalness.voice_quality,
technical.snr,
],
);
let trend_prediction = if self.config.predictive_assessment {
self.predict_quality_trend(&overall_score, &metadata)?
} else {
None
};
let score = MultiDomainQualityScore {
overall_score,
perceptual,
intelligibility,
naturalness,
technical,
confidence,
trend_prediction,
};
let measurement = QualityMeasurement {
timestamp: chrono::Utc::now(),
score: score.clone(),
content_type: metadata.content_type.clone(),
audio_metadata: metadata,
};
self.quality_history.push(measurement);
if self.quality_history.len() > self.config.history_window {
self.quality_history.remove(0);
}
Ok(score)
}
async fn evaluate_perceptual_domain(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<PerceptualDomainScores, EvaluationError> {
let loudness_quality = self.analyze_loudness_perception(audio)?;
let spectral_balance = self.analyze_spectral_balance(audio)?;
let temporal_coherence = self.analyze_temporal_coherence(audio)?;
let dynamic_range = self.analyze_dynamic_range(audio)?;
Ok(PerceptualDomainScores {
loudness_quality,
spectral_balance,
temporal_coherence,
dynamic_range,
})
}
async fn evaluate_intelligibility_domain(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<IntelligibilityDomainScores, EvaluationError> {
let phoneme_clarity = self.analyze_phoneme_clarity(audio)?;
let word_boundaries = self.analyze_word_boundaries(audio)?;
let prosodic_clarity = self.analyze_prosodic_clarity(audio)?;
let articulation = self.analyze_articulation(audio)?;
Ok(IntelligibilityDomainScores {
phoneme_clarity,
word_boundaries,
prosodic_clarity,
articulation,
})
}
async fn evaluate_naturalness_domain(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<NaturalnessDomainScores, EvaluationError> {
let voice_quality = self.analyze_voice_naturalness(audio)?;
let emotional_appropriateness = self.analyze_emotional_appropriateness(audio)?;
let speaking_rate = self.analyze_speaking_rate_naturalness(audio)?;
let intonation = self.analyze_intonation_naturalness(audio)?;
Ok(NaturalnessDomainScores {
voice_quality,
emotional_appropriateness,
speaking_rate,
intonation,
})
}
async fn evaluate_technical_domain(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<TechnicalDomainScores, EvaluationError> {
let snr = self.calculate_snr(audio, reference)?;
let thd = self.calculate_thd(audio)?;
let frequency_response = self.analyze_frequency_response(audio)?;
let dynamic_range = self.calculate_dynamic_range(audio)?;
Ok(TechnicalDomainScores {
snr,
thd,
frequency_response,
dynamic_range,
})
}
fn extract_audio_metadata(
&self,
audio: &AudioBuffer,
) -> Result<AudioMetadata, EvaluationError> {
let duration = audio.samples().len() as f64 / audio.sample_rate() as f64;
let content_type = self.estimate_content_type(audio)?;
let speaker_characteristics = self.estimate_speaker_characteristics(audio)?;
Ok(AudioMetadata {
duration,
sample_rate: audio.sample_rate(),
channels: audio.channels() as u32,
content_type,
speaker_characteristics,
})
}
fn calculate_adaptive_weights(&self, content_type: &str) -> HashMap<String, f64> {
let mut weights = HashMap::new();
match content_type {
"speech" => {
weights.insert("perceptual".to_string(), 0.20);
weights.insert("intelligibility".to_string(), 0.40);
weights.insert("naturalness".to_string(), 0.30);
weights.insert("technical".to_string(), 0.10);
}
"music" => {
weights.insert("perceptual".to_string(), 0.50);
weights.insert("intelligibility".to_string(), 0.05);
weights.insert("naturalness".to_string(), 0.15);
weights.insert("technical".to_string(), 0.30);
}
"singing" => {
weights.insert("perceptual".to_string(), 0.35);
weights.insert("intelligibility".to_string(), 0.25);
weights.insert("naturalness".to_string(), 0.25);
weights.insert("technical".to_string(), 0.15);
}
_ => {
weights.insert("perceptual".to_string(), 0.25);
weights.insert("intelligibility".to_string(), 0.25);
weights.insert("naturalness".to_string(), 0.25);
weights.insert("technical".to_string(), 0.25);
}
}
weights
}
fn compute_weighted_score(
&self,
perceptual: &PerceptualDomainScores,
intelligibility: &IntelligibilityDomainScores,
naturalness: &NaturalnessDomainScores,
technical: &TechnicalDomainScores,
weights: &HashMap<String, f64>,
) -> f64 {
let perceptual_avg = (perceptual.loudness_quality
+ perceptual.spectral_balance
+ perceptual.temporal_coherence
+ perceptual.dynamic_range)
/ 4.0;
let intelligibility_avg = (intelligibility.phoneme_clarity
+ intelligibility.word_boundaries
+ intelligibility.prosodic_clarity
+ intelligibility.articulation)
/ 4.0;
let naturalness_avg = (naturalness.voice_quality
+ naturalness.emotional_appropriateness
+ naturalness.speaking_rate
+ naturalness.intonation)
/ 4.0;
let technical_avg = (technical.snr
+ technical.thd
+ technical.frequency_response
+ technical.dynamic_range)
/ 4.0;
perceptual_avg * weights.get("perceptual").unwrap_or(&0.25)
+ intelligibility_avg * weights.get("intelligibility").unwrap_or(&0.25)
+ naturalness_avg * weights.get("naturalness").unwrap_or(&0.25)
+ technical_avg * weights.get("technical").unwrap_or(&0.25)
}
fn calculate_confidence(&self, metadata: &AudioMetadata, scores: &[f64]) -> f64 {
let duration_factor = (metadata.duration.min(10.0) / 10.0).max(0.1);
let score_variance = {
let mean = scores.iter().sum::<f64>() / scores.len() as f64;
let variance =
scores.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / scores.len() as f64;
variance.sqrt()
};
let consistency_factor = (1.0 - score_variance.min(1.0)).max(0.1);
(duration_factor * consistency_factor).min(1.0)
}
fn predict_quality_trend(
&self,
current_score: &f64,
metadata: &AudioMetadata,
) -> Result<Option<QualityTrendPrediction>, EvaluationError> {
if self.quality_history.len() < 3 {
return Ok(None);
}
let recent_scores: Vec<f64> = self
.quality_history
.iter()
.rev()
.take(10)
.map(|m| m.score.overall_score)
.collect();
let trend_slope = self.calculate_trend_slope(&recent_scores);
let predicted_score = (current_score + trend_slope).max(0.0).min(1.0);
let drift_direction = match trend_slope {
x if x > 0.01 => QualityDriftDirection::Improving,
x if x < -0.01 => QualityDriftDirection::Degrading,
_ => QualityDriftDirection::Stable,
};
let prediction_confidence = self.calculate_prediction_confidence(&recent_scores);
let recommendations = self.generate_trend_recommendations(&drift_direction, &trend_slope);
Ok(Some(QualityTrendPrediction {
predicted_score,
prediction_confidence,
drift_direction,
recommendations,
}))
}
fn analyze_loudness_perception(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
let rms = self.calculate_rms(audio.samples());
Ok((rms * 10.0).min(1.0).max(0.0) as f64)
}
fn analyze_spectral_balance(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.8) }
fn analyze_temporal_coherence(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.75) }
fn analyze_dynamic_range(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
let max_val = audio
.samples()
.iter()
.map(|&x| x.abs())
.fold(0.0f32, f32::max);
let min_val = audio
.samples()
.iter()
.map(|&x| x.abs())
.fold(f32::INFINITY, f32::min);
let dynamic_range = (max_val / min_val.max(f32::EPSILON)).log10() / 6.0; Ok(dynamic_range.min(1.0).max(0.0) as f64)
}
fn analyze_phoneme_clarity(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.85) }
fn analyze_word_boundaries(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.80) }
fn analyze_prosodic_clarity(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.78) }
fn analyze_articulation(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.82) }
fn analyze_voice_naturalness(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.77) }
fn analyze_emotional_appropriateness(
&self,
audio: &AudioBuffer,
) -> Result<f64, EvaluationError> {
Ok(0.85) }
fn analyze_speaking_rate_naturalness(
&self,
audio: &AudioBuffer,
) -> Result<f64, EvaluationError> {
Ok(0.83) }
fn analyze_intonation_naturalness(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.79) }
fn calculate_snr(
&self,
audio: &AudioBuffer,
reference: Option<&AudioBuffer>,
) -> Result<f64, EvaluationError> {
Ok(0.88) }
fn calculate_thd(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.92) }
fn analyze_frequency_response(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
Ok(0.86) }
fn calculate_dynamic_range(&self, audio: &AudioBuffer) -> Result<f64, EvaluationError> {
self.analyze_dynamic_range(audio)
}
fn estimate_content_type(&self, audio: &AudioBuffer) -> Result<String, EvaluationError> {
Ok("speech".to_string())
}
fn estimate_speaker_characteristics(
&self,
audio: &AudioBuffer,
) -> Result<SpeakerCharacteristics, EvaluationError> {
Ok(SpeakerCharacteristics {
age_group: "adult".to_string(),
gender: "unknown".to_string(),
accent: "neutral".to_string(),
style: "conversational".to_string(),
})
}
fn calculate_rms(&self, samples: &[f32]) -> f32 {
let sum_squares: f32 = samples.iter().map(|&x| x * x).sum();
(sum_squares / samples.len() as f32).sqrt()
}
fn calculate_trend_slope(&self, scores: &[f64]) -> f64 {
if scores.len() < 2 {
return 0.0;
}
let n = scores.len() as f64;
let x_sum: f64 = (0..scores.len()).map(|i| i as f64).sum();
let y_sum: f64 = scores.iter().sum();
let xy_sum: f64 = scores.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
let x2_sum: f64 = (0..scores.len()).map(|i| (i as f64).powi(2)).sum();
let denominator = n * x2_sum - x_sum * x_sum;
if denominator.abs() < f64::EPSILON {
return 0.0;
}
(n * xy_sum - x_sum * y_sum) / denominator
}
fn calculate_prediction_confidence(&self, scores: &[f64]) -> f64 {
if scores.len() < 2 {
return 0.5;
}
let mean = scores.iter().sum::<f64>() / scores.len() as f64;
let variance =
scores.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / scores.len() as f64;
let stability = 1.0 - variance.sqrt().min(1.0);
stability * 0.8 + 0.2 }
fn generate_trend_recommendations(
&self,
direction: &QualityDriftDirection,
slope: &f64,
) -> Vec<String> {
match direction {
QualityDriftDirection::Degrading => vec![
"Consider reviewing recent model changes".to_string(),
"Check for data quality issues".to_string(),
"Investigate environmental factors".to_string(),
],
QualityDriftDirection::Unstable => vec![
"Examine input data consistency".to_string(),
"Consider model regularization".to_string(),
"Review system stability".to_string(),
],
QualityDriftDirection::Improving => vec![
"Continue current optimization approach".to_string(),
"Document successful changes".to_string(),
],
QualityDriftDirection::Stable => vec![
"Maintain current configuration".to_string(),
"Consider exploring optimization opportunities".to_string(),
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_advanced_quality_evaluator_creation() {
let config = AdvancedQualityConfig::default();
let evaluator = AdvancedQualityEvaluator::new(config);
assert_eq!(evaluator.quality_history.len(), 0);
}
#[tokio::test]
async fn test_multi_domain_evaluation() {
let config = AdvancedQualityConfig::default();
let mut evaluator = AdvancedQualityEvaluator::new(config);
let audio = AudioBuffer::new(vec![0.1; 16000], 16000, 1);
let result = evaluator.evaluate_quality(&audio, None).await;
assert!(result.is_ok());
let score = result.unwrap();
assert!(score.overall_score >= 0.0 && score.overall_score <= 1.0);
assert!(score.confidence >= 0.0 && score.confidence <= 1.0);
}
#[test]
fn test_adaptive_weights() {
let config = AdvancedQualityConfig::default();
let evaluator = AdvancedQualityEvaluator::new(config);
let speech_weights = evaluator.calculate_adaptive_weights("speech");
assert_eq!(*speech_weights.get("intelligibility").unwrap(), 0.40);
let music_weights = evaluator.calculate_adaptive_weights("music");
assert_eq!(*music_weights.get("perceptual").unwrap(), 0.50);
}
#[test]
fn test_trend_slope_calculation() {
let config = AdvancedQualityConfig::default();
let evaluator = AdvancedQualityEvaluator::new(config);
let improving_scores = vec![0.6, 0.65, 0.7, 0.75, 0.8];
let slope = evaluator.calculate_trend_slope(&improving_scores);
assert!(slope > 0.0);
let degrading_scores = vec![0.8, 0.75, 0.7, 0.65, 0.6];
let slope = evaluator.calculate_trend_slope(°rading_scores);
assert!(slope < 0.0);
}
}