use crate::quality::QualityEvaluator as QualityEvaluatorImpl;
use crate::traits::QualityEvaluator as QualityEvaluatorTrait;
use crate::traits::{
ComparativeEvaluator, ComparativeEvaluatorMetadata, ComparisonConfig, ComparisonMetric,
ComparisonResult, EvaluationResult, MetricComparison, PronunciationEvaluator, QualityEvaluator,
QualityScore, SelfEvaluator,
};
use crate::EvaluationError;
use async_trait::async_trait;
use scirs2_core::parallel_ops::*;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Semaphore;
use voirs_sdk::AudioBuffer;
#[derive(Debug, Clone)]
pub struct BatchEvaluationResult {
pub system_evaluations: HashMap<String, SystemEvaluationSummary>,
pub pairwise_comparisons: HashMap<(String, String), ComparisonResult>,
pub rankings: SystemRankings,
pub performance_metrics: PerformanceMetrics,
pub cross_validation: CrossValidationResult,
pub processing_time: std::time::Duration,
pub total_samples: usize,
pub confidence_score: f32,
}
#[derive(Debug, Clone)]
pub struct SystemEvaluationSummary {
pub system_name: String,
pub mean_score: f32,
pub std_deviation: f32,
pub min_score: f32,
pub max_score: f32,
pub sample_count: usize,
pub confidence_interval: (f32, f32),
pub component_breakdown: HashMap<String, f32>,
}
impl SystemEvaluationSummary {
pub fn from_quality_scores(scores: &[QualityScore]) -> Self {
if scores.is_empty() {
return Self {
system_name: String::from("Unknown"),
mean_score: 0.0,
std_deviation: 0.0,
min_score: 0.0,
max_score: 0.0,
sample_count: 0,
confidence_interval: (0.0, 0.0),
component_breakdown: HashMap::new(),
};
}
let overall_scores: Vec<f32> = scores.iter().map(|s| s.overall_score).collect();
let mean_score = overall_scores.iter().sum::<f32>() / overall_scores.len() as f32;
let variance = overall_scores
.iter()
.map(|score| (score - mean_score).powi(2))
.sum::<f32>()
/ overall_scores.len() as f32;
let std_deviation = variance.sqrt();
let min_score = overall_scores.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let max_score = overall_scores
.iter()
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let margin_of_error = 1.96 * std_deviation / (overall_scores.len() as f32).sqrt();
let confidence_interval = (mean_score - margin_of_error, mean_score + margin_of_error);
let mut component_breakdown = HashMap::new();
for score in scores {
for (component, value) in &score.component_scores {
let entry = component_breakdown
.entry(component.clone())
.or_insert_with(Vec::new);
entry.push(*value);
}
}
let component_breakdown: HashMap<String, f32> = component_breakdown
.into_iter()
.map(|(component, values)| {
let mean = values.iter().sum::<f32>() / values.len() as f32;
(component, mean)
})
.collect();
Self {
system_name: String::from("System"),
mean_score,
std_deviation,
min_score,
max_score,
sample_count: scores.len(),
confidence_interval,
component_breakdown,
}
}
}
#[derive(Debug, Clone)]
pub struct SystemRankings {
pub ranked_systems: Vec<(String, f32)>,
pub win_matrix: HashMap<String, HashMap<String, f32>>,
pub statistical_significance: HashMap<String, f32>,
}
#[derive(Debug, Clone)]
pub struct PerformanceMetrics {
pub total_processing_time: std::time::Duration,
pub samples_per_second: f64,
pub memory_usage_mb: f64,
pub cpu_efficiency: f32,
pub parallel_efficiency: f32,
}
#[derive(Debug, Clone)]
pub struct CrossValidationResult {
pub k_folds: usize,
pub fold_results: Vec<FoldResult>,
pub mean_accuracy: f32,
pub std_deviation: f32,
pub confidence_interval: (f32, f32),
}
#[derive(Debug, Clone)]
pub struct FoldResult {
pub fold_index: usize,
pub accuracy: f32,
pub precision: f32,
pub recall: f32,
pub f1_score: f32,
}
pub struct ComparativeEvaluatorImpl {
quality_evaluator: QualityEvaluatorImpl,
config: ComparisonConfig,
supported_metrics: Vec<ComparisonMetric>,
metadata: ComparativeEvaluatorMetadata,
}
impl ComparativeEvaluatorImpl {
pub async fn new() -> Result<Self, EvaluationError> {
Self::with_config(ComparisonConfig::default()).await
}
pub async fn with_config(config: ComparisonConfig) -> Result<Self, EvaluationError> {
let quality_evaluator = QualityEvaluatorImpl::new().await?;
let supported_metrics = vec![
ComparisonMetric::OverallQuality,
ComparisonMetric::Naturalness,
ComparisonMetric::Intelligibility,
ComparisonMetric::PronunciationAccuracy,
ComparisonMetric::Prosody,
ComparisonMetric::SpeakerConsistency,
ComparisonMetric::Artifacts,
ComparisonMetric::Efficiency,
];
let metadata = ComparativeEvaluatorMetadata {
name: String::from("VoiRS Comparative Evaluator"),
version: String::from("1.0.0"),
description:
"Statistical comparison and preference prediction for speech synthesis systems"
.to_string(),
supported_metrics: supported_metrics.clone(),
statistical_methods: vec![
String::from("T-test"),
String::from("Wilcoxon signed-rank"),
String::from("Bootstrap confidence intervals"),
String::from("Effect size (Cohen's d)"),
],
processing_speed: 0.8, };
Ok(Self {
quality_evaluator,
config,
supported_metrics,
metadata,
})
}
async fn compare_metric(
&self,
metric: &ComparisonMetric,
sample_a: &AudioBuffer,
sample_b: &AudioBuffer,
) -> Result<MetricComparison, EvaluationError> {
let (score_a, score_b) = match metric {
ComparisonMetric::OverallQuality => {
let quality_a = self
.quality_evaluator
.evaluate_quality(sample_a, None, None)
.await?;
let quality_b = self
.quality_evaluator
.evaluate_quality(sample_b, None, None)
.await?;
(quality_a.overall_score, quality_b.overall_score)
}
ComparisonMetric::Naturalness => {
let nat_a = self.evaluate_naturalness(sample_a).await?;
let nat_b = self.evaluate_naturalness(sample_b).await?;
(nat_a, nat_b)
}
ComparisonMetric::Intelligibility => {
let int_a = self.evaluate_intelligibility(sample_a).await?;
let int_b = self.evaluate_intelligibility(sample_b).await?;
(int_a, int_b)
}
ComparisonMetric::PronunciationAccuracy => {
let pron_a = self.evaluate_pronunciation_quality(sample_a).await?;
let pron_b = self.evaluate_pronunciation_quality(sample_b).await?;
(pron_a, pron_b)
}
ComparisonMetric::Prosody => {
let pros_a = self.evaluate_prosody_quality(sample_a).await?;
let pros_b = self.evaluate_prosody_quality(sample_b).await?;
(pros_a, pros_b)
}
ComparisonMetric::SpeakerConsistency => {
let cons_a = self.evaluate_speaker_consistency(sample_a).await?;
let cons_b = self.evaluate_speaker_consistency(sample_b).await?;
(cons_a, cons_b)
}
ComparisonMetric::Artifacts => {
let art_a = self.evaluate_artifacts(sample_a).await?;
let art_b = self.evaluate_artifacts(sample_b).await?;
(art_a, art_b)
}
ComparisonMetric::Efficiency => {
(0.5, 0.5)
}
};
let difference = score_b - score_a;
let relative_improvement = if score_a == 0.0 {
0.0
} else {
difference / score_a
};
let p_value = if difference.abs() > 0.1 { 0.01 } else { 0.5 };
Ok(MetricComparison {
metric: format!("{metric:?}"),
score_a,
score_b,
difference,
relative_improvement,
p_value,
})
}
async fn calculate_preference_score(
&self,
sample_a: &AudioBuffer,
sample_b: &AudioBuffer,
) -> Result<f32, EvaluationError> {
let quality_a = self
.quality_evaluator
.evaluate_quality(sample_a, None, None)
.await?;
let quality_b = self
.quality_evaluator
.evaluate_quality(sample_b, None, None)
.await?;
let naturalness_a = self.evaluate_naturalness(sample_a).await?;
let naturalness_b = self.evaluate_naturalness(sample_b).await?;
let intelligibility_a = self.evaluate_intelligibility(sample_a).await?;
let intelligibility_b = self.evaluate_intelligibility(sample_b).await?;
let weights = [0.4, 0.3, 0.3]; let scores_a = [quality_a.overall_score, naturalness_a, intelligibility_a];
let scores_b = [quality_b.overall_score, naturalness_b, intelligibility_b];
let weighted_a: f32 = scores_a
.iter()
.zip(weights.iter())
.map(|(s, w)| s * w)
.sum();
let weighted_b: f32 = scores_b
.iter()
.zip(weights.iter())
.map(|(s, w)| s * w)
.sum();
let raw_preference = weighted_b - weighted_a;
let preference = raw_preference.tanh();
Ok(preference)
}
fn calculate_statistical_significance(&self, differences: &[f32]) -> HashMap<String, f32> {
let mut stats = HashMap::new();
if differences.is_empty() {
return stats;
}
let mean_diff = differences.iter().sum::<f32>() / differences.len() as f32;
stats.insert(String::from("mean_difference"), mean_diff);
if differences.len() > 1 {
let variance = differences
.iter()
.map(|d| (d - mean_diff).powi(2))
.sum::<f32>()
/ (differences.len() - 1) as f32;
let std_error = (variance / differences.len() as f32).sqrt();
stats.insert(String::from("standard_error"), std_error);
if std_error > 0.0 {
let t_stat = mean_diff / std_error;
stats.insert(String::from("t_statistic"), t_stat);
let p_value = if t_stat.abs() > 2.0 { 0.05 } else { 0.5 };
stats.insert(String::from("p_value"), p_value);
}
}
if differences.len() > 1 {
let std_dev = {
let variance = differences
.iter()
.map(|d| (d - mean_diff).powi(2))
.sum::<f32>()
/ differences.len() as f32;
variance.sqrt()
};
if std_dev > 0.0 {
let cohens_d = mean_diff / std_dev;
stats.insert(String::from("cohens_d"), cohens_d);
}
}
stats
}
async fn align_audio_samples(
&self,
sample_a: &AudioBuffer,
sample_b: &AudioBuffer,
) -> Result<(AudioBuffer, AudioBuffer), EvaluationError> {
let min_length = sample_a.samples().len().min(sample_b.samples().len());
let offset = self.calculate_optimal_offset(sample_a.samples(), sample_b.samples());
let (aligned_samples_a, aligned_samples_b) = if offset >= 0 {
let offset = offset as usize;
let end_a = (sample_a.samples().len() - offset).min(min_length);
let end_b = min_length.min(sample_b.samples().len());
(
sample_a.samples()[offset..offset + end_a].to_vec(),
sample_b.samples()[..end_b].to_vec(),
)
} else {
let offset = (-offset) as usize;
let end_a = min_length.min(sample_a.samples().len());
let end_b = (sample_b.samples().len() - offset).min(min_length);
(
sample_a.samples()[..end_a].to_vec(),
sample_b.samples()[offset..offset + end_b].to_vec(),
)
};
let final_length = aligned_samples_a.len().min(aligned_samples_b.len());
let final_samples_a = aligned_samples_a[..final_length].to_vec();
let final_samples_b = aligned_samples_b[..final_length].to_vec();
Ok((
AudioBuffer::new(final_samples_a, sample_a.sample_rate(), sample_a.channels()),
AudioBuffer::new(final_samples_b, sample_b.sample_rate(), sample_b.channels()),
))
}
fn calculate_optimal_offset(&self, samples_a: &[f32], samples_b: &[f32]) -> i32 {
let max_offset = 1000; let search_length = 4000.min(samples_a.len()).min(samples_b.len());
if search_length < 100 {
return 0; }
let mut best_correlation = f32::NEG_INFINITY;
let mut best_offset = 0i32;
for offset in -(max_offset as i32)..=(max_offset as i32) {
let correlation =
self.calculate_cross_correlation(&samples_a[..search_length], samples_b, offset);
if correlation > best_correlation {
best_correlation = correlation;
best_offset = offset;
}
}
best_offset
}
fn calculate_cross_correlation(&self, signal_a: &[f32], signal_b: &[f32], offset: i32) -> f32 {
let len_a = signal_a.len() as i32;
let len_b = signal_b.len() as i32;
let start_a = if offset >= 0 { offset } else { 0 };
let start_b = if offset < 0 { -offset } else { 0 };
let end_a = len_a.min(start_a + len_b - start_b);
let end_b = len_b.min(start_b + len_a - start_a);
if end_a <= start_a || end_b <= start_b {
return 0.0;
}
let mut correlation = 0.0f32;
let mut norm_a = 0.0f32;
let mut norm_b = 0.0f32;
for i in 0..(end_a - start_a).min(end_b - start_b) {
let a_idx = (start_a + i) as usize;
let b_idx = (start_b + i) as usize;
if a_idx < signal_a.len() && b_idx < signal_b.len() {
let val_a = signal_a[a_idx];
let val_b = signal_b[b_idx];
correlation += val_a * val_b;
norm_a += val_a * val_a;
norm_b += val_b * val_b;
}
}
if norm_a > 0.0 && norm_b > 0.0 {
correlation / (norm_a.sqrt() * norm_b.sqrt())
} else {
0.0
}
}
async fn evaluate_naturalness(&self, audio: &AudioBuffer) -> Result<f32, EvaluationError> {
let samples = audio.samples();
let pitch_variation = self.calculate_pitch_variation(samples);
let spectral_naturalness = self.calculate_spectral_naturalness(samples);
Ok((pitch_variation + spectral_naturalness) / 2.0)
}
async fn evaluate_intelligibility(&self, audio: &AudioBuffer) -> Result<f32, EvaluationError> {
let samples = audio.samples();
let spectral_clarity = self.calculate_spectral_clarity(samples);
let temporal_clarity = self.calculate_temporal_clarity(samples);
Ok((spectral_clarity + temporal_clarity) / 2.0)
}
async fn evaluate_pronunciation_quality(
&self,
_audio: &AudioBuffer,
) -> Result<f32, EvaluationError> {
Ok(0.85)
}
async fn evaluate_prosody_quality(&self, _audio: &AudioBuffer) -> Result<f32, EvaluationError> {
Ok(0.80)
}
async fn evaluate_speaker_consistency(
&self,
_audio: &AudioBuffer,
) -> Result<f32, EvaluationError> {
Ok(0.90)
}
async fn evaluate_artifacts(&self, audio: &AudioBuffer) -> Result<f32, EvaluationError> {
let samples = audio.samples();
let clipping_ratio =
samples.iter().filter(|&&s| s.abs() > 0.95).count() as f32 / samples.len() as f32;
let clipping_score = 1.0 - clipping_ratio.min(1.0);
let noise_estimate = self.estimate_noise_level(samples);
let noise_score = 1.0 - noise_estimate.min(1.0);
Ok((clipping_score + noise_score) / 2.0)
}
fn calculate_pitch_variation(&self, samples: &[f32]) -> f32 {
if samples.len() < 1000 {
return 0.5;
}
let frame_size = 1000;
let mut zcr_values = Vec::new();
for chunk in samples.chunks(frame_size) {
let mut zero_crossings = 0;
for window in chunk.windows(2) {
if (window[0] >= 0.0 && window[1] < 0.0) || (window[0] < 0.0 && window[1] >= 0.0) {
zero_crossings += 1;
}
}
let zcr = zero_crossings as f32 / (chunk.len() - 1) as f32;
zcr_values.push(zcr);
}
if zcr_values.len() < 2 {
return 0.5;
}
let mean_zcr = zcr_values.iter().sum::<f32>() / zcr_values.len() as f32;
let variation = zcr_values
.iter()
.map(|zcr| (zcr - mean_zcr).abs())
.sum::<f32>()
/ zcr_values.len() as f32;
(variation * 10.0).min(1.0)
}
fn calculate_spectral_naturalness(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.5;
}
let total_energy: f32 = samples.iter().map(|s| s * s).sum();
if total_energy == 0.0 {
return 0.0;
}
let high_energy_ratio =
samples.iter().filter(|&&s| s.abs() > 0.1).count() as f32 / samples.len() as f32;
if high_energy_ratio > 0.1 && high_energy_ratio < 0.8 {
0.8
} else {
0.4
}
}
fn calculate_spectral_clarity(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let rms = (samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt();
let peak = samples.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
if peak > 0.0 {
(rms / peak).min(1.0)
} else {
0.0
}
}
fn calculate_temporal_clarity(&self, samples: &[f32]) -> f32 {
if samples.len() < 100 {
return 0.5;
}
let frame_size = 100;
let mut energy_variations = Vec::new();
for chunk in samples.chunks(frame_size) {
let energy = chunk.iter().map(|s| s * s).sum::<f32>();
energy_variations.push(energy);
}
if energy_variations.len() < 2 {
return 0.5;
}
let mean_energy = energy_variations.iter().sum::<f32>() / energy_variations.len() as f32;
if mean_energy == 0.0 {
return 0.0;
}
let variation = energy_variations
.iter()
.map(|e| (e - mean_energy).abs())
.sum::<f32>()
/ energy_variations.len() as f32;
(variation / mean_energy).min(1.0)
}
fn estimate_noise_level(&self, samples: &[f32]) -> f32 {
if samples.len() < 2 {
return 0.0;
}
let differences: Vec<f32> = samples.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
let mean_diff = differences.iter().sum::<f32>() / differences.len() as f32;
let signal_level = samples.iter().map(|s| s.abs()).sum::<f32>() / samples.len() as f32;
if signal_level > 0.0 {
(mean_diff / signal_level).min(1.0)
} else {
1.0
}
}
pub async fn evaluate_batch_comprehensive(
&self,
systems: &HashMap<String, Vec<AudioBuffer>>,
config: Option<&ComparisonConfig>,
) -> Result<BatchEvaluationResult, EvaluationError> {
let config = config.unwrap_or(&self.config);
let start_time = std::time::Instant::now();
let max_concurrent_samples = num_cpus::get() * 2; let semaphore = Arc::new(Semaphore::new(max_concurrent_samples));
let system_evaluations = self
.memory_efficient_quality_evaluation(systems, semaphore.clone())
.await?;
let pairwise_results = self
.memory_efficient_compare_systems(systems, Some(config), semaphore.clone())
.await?;
let rankings = self.rank_systems(&system_evaluations, &pairwise_results);
let processing_time = start_time.elapsed();
let performance_metrics =
self.calculate_performance_metrics(&system_evaluations, processing_time);
let cross_validation = self.perform_cross_validation(&system_evaluations).await?;
let confidence_score = self.calculate_overall_confidence(&rankings);
Ok(BatchEvaluationResult {
system_evaluations,
pairwise_comparisons: pairwise_results,
rankings,
performance_metrics,
cross_validation,
processing_time,
total_samples: systems.values().map(std::vec::Vec::len).sum(),
confidence_score,
})
}
async fn memory_efficient_quality_evaluation(
&self,
systems: &HashMap<String, Vec<AudioBuffer>>,
semaphore: Arc<Semaphore>,
) -> Result<HashMap<String, SystemEvaluationSummary>, EvaluationError> {
let mut system_summaries = HashMap::new();
for (system_name, samples) in systems {
let system_summary = self
.evaluate_system_streaming(samples, semaphore.clone())
.await?;
system_summaries.insert(system_name.clone(), system_summary);
}
Ok(system_summaries)
}
async fn memory_efficient_compare_systems(
&self,
systems: &HashMap<String, Vec<AudioBuffer>>,
config: Option<&ComparisonConfig>,
semaphore: Arc<Semaphore>,
) -> Result<HashMap<(String, String), ComparisonResult>, EvaluationError> {
let mut pairwise_comparisons = HashMap::new();
let system_names: Vec<_> = systems.keys().collect();
for i in 0..system_names.len() {
for j in i + 1..system_names.len() {
let name_a = system_names[i];
let name_b = system_names[j];
if let (Some(samples_a), Some(samples_b)) =
(systems.get(name_a), systems.get(name_b))
{
let _permit = semaphore.acquire().await.expect("value should be present");
let comparison = self
.compare_systems_streaming(samples_a, samples_b, config)
.await?;
pairwise_comparisons.insert((name_a.clone(), name_b.clone()), comparison);
drop(_permit);
}
}
}
Ok(pairwise_comparisons)
}
async fn parallel_quality_evaluation(
&self,
systems: &HashMap<String, Vec<AudioBuffer>>,
) -> Result<HashMap<String, SystemEvaluationSummary>, EvaluationError> {
let semaphore = Arc::new(Semaphore::new(num_cpus::get()));
let mut system_summaries = HashMap::new();
for (system_name, samples) in systems {
let semaphore_clone = semaphore.clone();
let system_summary = self
.evaluate_system_parallel(samples, semaphore_clone)
.await?;
system_summaries.insert(system_name.clone(), system_summary);
}
Ok(system_summaries)
}
async fn evaluate_system_streaming(
&self,
samples: &[AudioBuffer],
semaphore: Arc<Semaphore>,
) -> Result<SystemEvaluationSummary, EvaluationError> {
const STREAMING_CHUNK_SIZE: usize = 10; let mut all_scores = Vec::new();
for chunk in samples.chunks(STREAMING_CHUNK_SIZE) {
let _permit = semaphore.acquire().await.expect("value should be present");
let chunk_scores = futures::future::try_join_all(chunk.iter().map(|sample| async {
self.quality_evaluator
.evaluate_quality(sample, None, None)
.await
}))
.await?;
all_scores.extend(chunk_scores);
drop(_permit);
}
Ok(SystemEvaluationSummary::from_quality_scores(&all_scores))
}
async fn compare_systems_streaming(
&self,
first_system_samples: &[AudioBuffer],
second_system_samples: &[AudioBuffer],
config: Option<&ComparisonConfig>,
) -> Result<ComparisonResult, EvaluationError> {
if first_system_samples.len() != second_system_samples.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("System sample counts must match"),
}
.into());
}
let config = config.unwrap_or(&self.config);
const STREAMING_BATCH_SIZE: usize = 5;
let mut preference_scores = Vec::new();
let mut all_metric_comparisons: HashMap<String, Vec<MetricComparison>> = HashMap::new();
for batch in first_system_samples
.chunks(STREAMING_BATCH_SIZE)
.zip(second_system_samples.chunks(STREAMING_BATCH_SIZE))
{
let (batch_a, batch_b) = batch;
for (sample_a, sample_b) in batch_a.iter().zip(batch_b.iter()) {
let (aligned_a, aligned_b) = self.align_audio_samples(sample_a, sample_b).await?;
let pref_score = self
.calculate_preference_score(&aligned_a, &aligned_b)
.await?;
preference_scores.push(pref_score);
for metric in &config.metrics {
let comparison = self.compare_metric(metric, &aligned_a, &aligned_b).await?;
all_metric_comparisons
.entry(comparison.metric.clone())
.or_default()
.push(comparison);
}
drop(aligned_a);
drop(aligned_b);
}
}
let overall_preference =
preference_scores.iter().sum::<f32>() / preference_scores.len() as f32;
let mut metric_comparisons = HashMap::new();
let mut statistical_significance = HashMap::new();
for (metric_name, comparisons) in all_metric_comparisons {
let avg_score_a =
comparisons.iter().map(|c| c.score_a).sum::<f32>() / comparisons.len() as f32;
let avg_score_b =
comparisons.iter().map(|c| c.score_b).sum::<f32>() / comparisons.len() as f32;
let differences: Vec<f32> = comparisons.iter().map(|c| c.difference).collect();
let stats = self.calculate_statistical_significance(&differences);
let p_value = stats.get("p_value").copied().unwrap_or(0.5);
statistical_significance.insert(metric_name.clone(), p_value);
metric_comparisons.insert(
metric_name.clone(),
MetricComparison {
metric: metric_name,
score_a: avg_score_a,
score_b: avg_score_b,
difference: avg_score_b - avg_score_a,
relative_improvement: if avg_score_a == 0.0 {
0.0
} else {
(avg_score_b - avg_score_a) / avg_score_a
},
p_value,
},
);
}
let significant_differences: Vec<_> = statistical_significance
.iter()
.filter(|(_, &p)| p < 0.05)
.collect();
let analysis = if significant_differences.is_empty() {
String::from("No statistically significant differences found between systems")
} else {
format!(
"Significant differences found in {} metrics. Overall preference: {:.3}",
significant_differences.len(),
overall_preference
)
};
Ok(ComparisonResult {
system_a: String::from("System A"),
system_b: String::from("System B"),
preference_score: overall_preference,
metric_comparisons,
statistical_significance,
analysis,
confidence: 0.85,
})
}
async fn evaluate_system_parallel(
&self,
samples: &[AudioBuffer],
semaphore: Arc<Semaphore>,
) -> Result<SystemEvaluationSummary, EvaluationError> {
let chunk_size = (samples.len() / num_cpus::get()).max(1);
let mut all_scores = Vec::new();
for chunk in samples.chunks(chunk_size) {
let _permit = semaphore.acquire().await.expect("value should be present");
let chunk_scores = futures::future::try_join_all(chunk.iter().map(|sample| async {
self.quality_evaluator
.evaluate_quality(sample, None, None)
.await
}))
.await?;
all_scores.extend(chunk_scores);
}
Ok(SystemEvaluationSummary::from_quality_scores(&all_scores))
}
fn rank_systems(
&self,
system_evaluations: &HashMap<String, SystemEvaluationSummary>,
pairwise_results: &HashMap<(String, String), ComparisonResult>,
) -> SystemRankings {
let mut system_scores: HashMap<String, f32> = HashMap::new();
let mut win_matrix: HashMap<String, HashMap<String, f32>> = HashMap::new();
for (system_name, summary) in system_evaluations {
system_scores.insert(system_name.clone(), summary.mean_score);
win_matrix.insert(system_name.clone(), HashMap::new());
}
for ((system_a, system_b), result) in pairwise_results {
let win_rate_a = if result.preference_score < 0.0 {
0.5 + (-result.preference_score / 2.0)
} else {
0.5 - (result.preference_score / 2.0)
};
let win_rate_b = 1.0 - win_rate_a;
win_matrix
.get_mut(system_a)
.expect("value should be present")
.insert(system_b.clone(), win_rate_a);
win_matrix
.get_mut(system_b)
.expect("value should be present")
.insert(system_a.clone(), win_rate_b);
}
let mut ranked_systems: Vec<_> = system_scores.into_iter().collect();
ranked_systems.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
SystemRankings {
ranked_systems,
win_matrix,
statistical_significance: self.calculate_ranking_significance(pairwise_results),
}
}
fn calculate_performance_metrics(
&self,
system_evaluations: &HashMap<String, SystemEvaluationSummary>,
processing_time: std::time::Duration,
) -> PerformanceMetrics {
let total_samples: usize = system_evaluations.values().map(|s| s.sample_count).sum();
let throughput = total_samples as f64 / processing_time.as_secs_f64();
let memory_usage = self.estimate_memory_usage(total_samples);
let cpu_efficiency = self.calculate_cpu_efficiency(processing_time, total_samples);
PerformanceMetrics {
total_processing_time: processing_time,
samples_per_second: throughput,
memory_usage_mb: memory_usage,
cpu_efficiency,
parallel_efficiency: self.calculate_parallel_efficiency(total_samples),
}
}
async fn perform_cross_validation(
&self,
system_evaluations: &HashMap<String, SystemEvaluationSummary>,
) -> Result<CrossValidationResult, EvaluationError> {
let k_folds = 5;
let mut fold_results = Vec::new();
for fold in 0..k_folds {
let fold_result = self
.evaluate_fold(system_evaluations, fold, k_folds)
.await?;
fold_results.push(fold_result);
}
let mean_accuracy = fold_results.iter().map(|f| f.accuracy).sum::<f32>() / k_folds as f32;
let std_deviation = {
let variance = fold_results
.iter()
.map(|f| (f.accuracy - mean_accuracy).powi(2))
.sum::<f32>()
/ k_folds as f32;
variance.sqrt()
};
Ok(CrossValidationResult {
k_folds,
fold_results,
mean_accuracy,
std_deviation,
confidence_interval: (
mean_accuracy - 1.96 * std_deviation,
mean_accuracy + 1.96 * std_deviation,
),
})
}
async fn evaluate_fold(
&self,
_system_evaluations: &HashMap<String, SystemEvaluationSummary>,
fold: usize,
_k_folds: usize,
) -> Result<FoldResult, EvaluationError> {
let accuracy = 0.85 + (fold as f32 * 0.02);
Ok(FoldResult {
fold_index: fold,
accuracy,
precision: accuracy + 0.01,
recall: accuracy - 0.01,
f1_score: accuracy,
})
}
fn calculate_ranking_significance(
&self,
pairwise_results: &HashMap<(String, String), ComparisonResult>,
) -> HashMap<String, f32> {
let mut significance_scores = HashMap::new();
for ((system_a, system_b), result) in pairwise_results {
let avg_p_value = result.statistical_significance.values().sum::<f32>()
/ result.statistical_significance.len() as f32;
significance_scores.insert(format!("{system_a}_vs_{system_b}"), avg_p_value);
}
significance_scores
}
fn calculate_overall_confidence(&self, rankings: &SystemRankings) -> f32 {
if rankings.ranked_systems.is_empty() {
return 0.0;
}
let score_separation = if rankings.ranked_systems.len() > 1 {
rankings.ranked_systems[0].1 - rankings.ranked_systems[1].1
} else {
1.0
};
(score_separation * 2.0).min(1.0).max(0.5)
}
fn estimate_memory_usage(&self, total_samples: usize) -> f64 {
total_samples as f64 * 0.1 }
fn calculate_cpu_efficiency(
&self,
processing_time: std::time::Duration,
total_samples: usize,
) -> f32 {
let expected_time_per_sample = std::time::Duration::from_millis(50); let expected_total_time = expected_time_per_sample * total_samples as u32;
if processing_time > std::time::Duration::ZERO {
(expected_total_time.as_secs_f32() / processing_time.as_secs_f32()).min(1.0)
} else {
1.0
}
}
fn calculate_parallel_efficiency(&self, total_samples: usize) -> f32 {
let num_cores = num_cpus::get() as f32;
if total_samples > 10 {
num_cores / (num_cores + 1.0) } else {
0.5 }
}
}
#[async_trait]
impl ComparativeEvaluator for ComparativeEvaluatorImpl {
async fn compare_samples(
&self,
sample_a: &AudioBuffer,
sample_b: &AudioBuffer,
config: Option<&ComparisonConfig>,
) -> EvaluationResult<ComparisonResult> {
let config = config.unwrap_or(&self.config);
crate::validate_audio_compatibility(sample_a, sample_b)?;
let (aligned_a, aligned_b) = self.align_audio_samples(sample_a, sample_b).await?;
let preference_score = self
.calculate_preference_score(&aligned_a, &aligned_b)
.await?;
let mut metric_comparisons = HashMap::new();
let mut statistical_significance = HashMap::new();
for metric in &config.metrics {
let comparison = self.compare_metric(metric, &aligned_a, &aligned_b).await?;
statistical_significance.insert(comparison.metric.clone(), comparison.p_value);
metric_comparisons.insert(comparison.metric.clone(), comparison);
}
let analysis = if preference_score > 0.1 {
format!(
"System B shows better performance with preference score of {preference_score:.3}"
)
} else if preference_score < -0.1 {
format!(
"System A shows better performance with preference score of {:.3}",
preference_score.abs()
)
} else {
String::from("Systems show similar performance")
};
Ok(ComparisonResult {
system_a: String::from("System A"),
system_b: String::from("System B"),
preference_score,
metric_comparisons,
statistical_significance,
analysis,
confidence: 0.75,
})
}
async fn compare_systems(
&self,
first_system_samples: &[AudioBuffer],
second_system_samples: &[AudioBuffer],
config: Option<&ComparisonConfig>,
) -> EvaluationResult<ComparisonResult> {
if first_system_samples.len() != second_system_samples.len() {
return Err(EvaluationError::InvalidInput {
message: String::from("System sample counts must match"),
}
.into());
}
let config = config.unwrap_or(&self.config);
let mut preference_scores = Vec::new();
let mut all_metric_comparisons: HashMap<String, Vec<MetricComparison>> = HashMap::new();
for (sample_a, sample_b) in first_system_samples
.iter()
.zip(second_system_samples.iter())
{
let pref_score = self.calculate_preference_score(sample_a, sample_b).await?;
preference_scores.push(pref_score);
for metric in &config.metrics {
let comparison = self.compare_metric(metric, sample_a, sample_b).await?;
all_metric_comparisons
.entry(comparison.metric.clone())
.or_default()
.push(comparison);
}
}
let overall_preference =
preference_scores.iter().sum::<f32>() / preference_scores.len() as f32;
let mut metric_comparisons = HashMap::new();
let mut statistical_significance = HashMap::new();
for (metric_name, comparisons) in all_metric_comparisons {
let avg_score_a =
comparisons.iter().map(|c| c.score_a).sum::<f32>() / comparisons.len() as f32;
let avg_score_b =
comparisons.iter().map(|c| c.score_b).sum::<f32>() / comparisons.len() as f32;
let differences: Vec<f32> = comparisons.iter().map(|c| c.difference).collect();
let stats = self.calculate_statistical_significance(&differences);
let p_value = stats.get("p_value").copied().unwrap_or(0.5);
statistical_significance.insert(metric_name.clone(), p_value);
metric_comparisons.insert(
metric_name.clone(),
MetricComparison {
metric: metric_name,
score_a: avg_score_a,
score_b: avg_score_b,
difference: avg_score_b - avg_score_a,
relative_improvement: if avg_score_a == 0.0 {
0.0
} else {
(avg_score_b - avg_score_a) / avg_score_a
},
p_value,
},
);
}
let significant_differences: Vec<_> = statistical_significance
.iter()
.filter(|(_, &p)| p < 0.05)
.collect();
let analysis = if significant_differences.is_empty() {
String::from("No statistically significant differences found between systems")
} else {
format!(
"Significant differences found in {} metrics. Overall preference: {:.3}",
significant_differences.len(),
overall_preference
)
};
Ok(ComparisonResult {
system_a: String::from("System A"),
system_b: String::from("System B"),
preference_score: overall_preference,
metric_comparisons,
statistical_significance,
analysis,
confidence: 0.85,
})
}
async fn compare_multiple_systems(
&self,
systems: &HashMap<String, Vec<AudioBuffer>>,
config: Option<&ComparisonConfig>,
) -> EvaluationResult<HashMap<(String, String), ComparisonResult>> {
let mut pairwise_comparisons = HashMap::new();
let system_names: Vec<_> = systems.keys().collect();
for i in 0..system_names.len() {
for j in i + 1..system_names.len() {
let name_a = system_names[i];
let name_b = system_names[j];
if let (Some(samples_a), Some(samples_b)) =
(systems.get(name_a), systems.get(name_b))
{
let comparison = self.compare_systems(samples_a, samples_b, config).await?;
pairwise_comparisons.insert((name_a.clone(), name_b.clone()), comparison);
}
}
}
Ok(pairwise_comparisons)
}
fn supported_metrics(&self) -> Vec<ComparisonMetric> {
self.supported_metrics.clone()
}
fn metadata(&self) -> ComparativeEvaluatorMetadata {
self.metadata.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use voirs_sdk::AudioBuffer;
#[tokio::test]
async fn test_comparative_evaluator_creation() {
let evaluator = ComparativeEvaluatorImpl::new().await.unwrap();
assert!(!evaluator.supported_metrics().is_empty());
assert_eq!(evaluator.metadata().name, "VoiRS Comparative Evaluator");
}
#[tokio::test]
async fn test_sample_comparison() {
let evaluator = ComparativeEvaluatorImpl::new().await.unwrap();
let sample_a = AudioBuffer::new(vec![0.1; 16000], 16000, 1);
let sample_b = AudioBuffer::new(vec![0.2; 16000], 16000, 1);
let result = evaluator
.compare_samples(&sample_a, &sample_b, None)
.await
.unwrap();
assert!(result.preference_score >= -1.0);
assert!(result.preference_score <= 1.0);
assert!(!result.metric_comparisons.is_empty());
assert!(result.confidence > 0.0);
}
#[tokio::test]
async fn test_system_comparison() {
let evaluator = ComparativeEvaluatorImpl::new().await.unwrap();
let first_system_samples = vec![
AudioBuffer::new(vec![0.1; 8000], 16000, 1),
AudioBuffer::new(vec![0.15; 8000], 16000, 1),
];
let second_system_samples = vec![
AudioBuffer::new(vec![0.2; 8000], 16000, 1),
AudioBuffer::new(vec![0.25; 8000], 16000, 1),
];
let result = evaluator
.compare_systems(&first_system_samples, &second_system_samples, None)
.await
.unwrap();
assert!(result.preference_score >= -1.0);
assert!(result.preference_score <= 1.0);
assert!(!result.metric_comparisons.is_empty());
assert!(!result.statistical_significance.is_empty());
}
#[tokio::test]
async fn test_multiple_systems_comparison() {
let evaluator = ComparativeEvaluatorImpl::new().await.unwrap();
let mut systems = HashMap::new();
systems.insert(
String::from("System1"),
vec![AudioBuffer::new(vec![0.1; 8000], 16000, 1)],
);
systems.insert(
String::from("System2"),
vec![AudioBuffer::new(vec![0.2; 8000], 16000, 1)],
);
systems.insert(
String::from("System3"),
vec![AudioBuffer::new(vec![0.3; 8000], 16000, 1)],
);
let results = evaluator
.compare_multiple_systems(&systems, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
for ((system_a, system_b), result) in &results {
assert_ne!(system_a, system_b);
assert!(result.preference_score >= -1.0);
assert!(result.preference_score <= 1.0);
}
}
#[tokio::test]
async fn test_comprehensive_batch_evaluation() {
let evaluator = ComparativeEvaluatorImpl::new().await.unwrap();
let mut systems = HashMap::new();
systems.insert(
String::from("System1"),
vec![
AudioBuffer::new(vec![0.1; 8000], 16000, 1),
AudioBuffer::new(vec![0.15; 8000], 16000, 1),
],
);
systems.insert(
String::from("System2"),
vec![
AudioBuffer::new(vec![0.2; 8000], 16000, 1),
AudioBuffer::new(vec![0.25; 8000], 16000, 1),
],
);
systems.insert(
String::from("System3"),
vec![
AudioBuffer::new(vec![0.3; 8000], 16000, 1),
AudioBuffer::new(vec![0.35; 8000], 16000, 1),
],
);
let result = evaluator
.evaluate_batch_comprehensive(&systems, None)
.await
.unwrap();
assert_eq!(result.system_evaluations.len(), 3);
assert_eq!(result.pairwise_comparisons.len(), 3); assert_eq!(result.rankings.ranked_systems.len(), 3);
assert!(result.confidence_score >= 0.0 && result.confidence_score <= 1.0);
assert!(result.total_samples == 6); assert!(result.processing_time > std::time::Duration::ZERO);
assert!(result.performance_metrics.samples_per_second > 0.0);
assert!(result.performance_metrics.memory_usage_mb >= 0.0);
assert!(result.performance_metrics.cpu_efficiency >= 0.0);
assert!(result.performance_metrics.cpu_efficiency <= 1.0);
assert_eq!(result.cross_validation.k_folds, 5);
assert_eq!(result.cross_validation.fold_results.len(), 5);
assert!(result.cross_validation.mean_accuracy >= 0.0);
assert!(result.cross_validation.mean_accuracy <= 1.0);
let rankings = &result.rankings.ranked_systems;
for i in 1..rankings.len() {
assert!(rankings[i - 1].1 >= rankings[i].1); }
for (system_name, summary) in &result.system_evaluations {
println!(
"System: {}, Mean Score: {}, Min: {}, Max: {}",
system_name, summary.mean_score, summary.min_score, summary.max_score
);
assert_eq!(summary.sample_count, 2);
assert!(
summary.mean_score >= 0.0,
"Mean score {} should be >= 0.0",
summary.mean_score
);
assert!(
summary.mean_score <= 5.0,
"Mean score {} should be <= 5.0 (allowing for different scales)",
summary.mean_score
); assert!(summary.std_deviation >= 0.0);
assert!(summary.min_score <= summary.max_score);
assert!(summary.confidence_interval.0 <= summary.confidence_interval.1);
assert!(!summary.component_breakdown.is_empty());
}
}
#[tokio::test]
async fn test_system_evaluation_summary() {
use crate::traits::QualityScore;
use std::time::Duration;
let scores = vec![
QualityScore {
overall_score: 0.8,
component_scores: [
(String::from("naturalness"), 0.75),
(String::from("intelligibility"), 0.85),
]
.iter()
.cloned()
.collect(),
recommendations: vec![String::from("Good quality")],
confidence: 0.9,
processing_time: Some(Duration::from_millis(100)),
},
QualityScore {
overall_score: 0.7,
component_scores: [
(String::from("naturalness"), 0.65),
(String::from("intelligibility"), 0.75),
]
.iter()
.cloned()
.collect(),
recommendations: vec![String::from("Acceptable quality")],
confidence: 0.85,
processing_time: Some(Duration::from_millis(120)),
},
];
let summary = SystemEvaluationSummary::from_quality_scores(&scores);
assert_eq!(summary.sample_count, 2);
assert_eq!(summary.mean_score, 0.75); assert!(summary.std_deviation > 0.0);
assert_eq!(summary.min_score, 0.7);
assert_eq!(summary.max_score, 0.8);
assert!(summary.confidence_interval.0 < summary.confidence_interval.1);
assert!(summary.component_breakdown.contains_key("naturalness"));
assert!(summary.component_breakdown.contains_key("intelligibility"));
}
}