use crate::perceptual::{CulturalProfile, CulturalRegion, DemographicProfile};
use crate::traits::{EvaluationResult, QualityScore};
use crate::EvaluationError;
use scirs2_core::random::prelude::*;
use std::collections::HashMap;
use voirs_sdk::AudioBuffer;
#[derive(Debug, Clone)]
pub struct CrossCulturalConfig {
pub enable_phonetic_distance: bool,
pub enable_prosodic_preferences: bool,
pub enable_accent_familiarity: bool,
pub enable_communication_styles: bool,
pub enable_linguistic_distance: bool,
}
impl Default for CrossCulturalConfig {
fn default() -> Self {
Self {
enable_phonetic_distance: true,
enable_prosodic_preferences: true,
enable_accent_familiarity: true,
enable_communication_styles: true,
enable_linguistic_distance: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LanguageFamily {
IndoEuropean,
SinoTibetan,
AfroAsiatic,
NigerCongo,
TransNewGuinea,
Austronesian,
Japonic,
Koreanic,
Other,
}
#[derive(Debug, Clone)]
pub struct PhoneticInventory {
pub vowel_count: usize,
pub consonant_count: usize,
pub has_tones: bool,
pub has_consonant_clusters: bool,
pub common_phonemes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ProsodicPreferences {
pub preferred_speech_rate: f32,
pub pitch_variation_tolerance: f32,
pub stress_pattern_preference: StressPattern,
pub intonation_preference: IntonationPattern,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StressPattern {
Fixed,
Variable,
Tonal,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IntonationPattern {
Rising,
Falling,
Level,
Complex,
}
#[derive(Debug, Clone)]
pub struct CommunicationStyle {
pub directness_preference: f32, pub silence_tolerance: f32,
pub expressiveness_preference: f32,
pub formality_expectation: f32,
}
#[derive(Debug, Clone)]
pub struct CrossCulturalAdaptation {
pub phonetic_distance_factor: f32,
pub prosodic_mismatch_factor: f32,
pub accent_familiarity_factor: f32,
pub communication_style_factor: f32,
pub linguistic_distance_factor: f32,
}
pub struct CrossCulturalPerceptualModel {
config: CrossCulturalConfig,
phonetic_inventories: HashMap<String, PhoneticInventory>,
prosodic_preferences: HashMap<CulturalRegion, ProsodicPreferences>,
communication_styles: HashMap<CulturalRegion, CommunicationStyle>,
language_families: HashMap<String, LanguageFamily>,
}
impl CrossCulturalPerceptualModel {
pub fn new(config: CrossCulturalConfig) -> Self {
let mut model = Self {
config,
phonetic_inventories: HashMap::new(),
prosodic_preferences: HashMap::new(),
communication_styles: HashMap::new(),
language_families: HashMap::new(),
};
model.initialize_language_data();
model.initialize_cultural_preferences();
model
}
fn initialize_language_data(&mut self) {
self.phonetic_inventories.insert(
"en".to_string(),
PhoneticInventory {
vowel_count: 12,
consonant_count: 24,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["t", "n", "r", "s", "l", "d", "k", "m", "p", "w"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("en".to_string(), LanguageFamily::IndoEuropean);
self.phonetic_inventories.insert(
"zh".to_string(),
PhoneticInventory {
vowel_count: 8,
consonant_count: 21,
has_tones: true,
has_consonant_clusters: false,
common_phonemes: vec!["n", "t", "l", "s", "k", "x", "w", "m", "p", "f"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("zh".to_string(), LanguageFamily::SinoTibetan);
self.phonetic_inventories.insert(
"es".to_string(),
PhoneticInventory {
vowel_count: 5,
consonant_count: 19,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["s", "n", "r", "l", "t", "d", "k", "m", "p", "b"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("es".to_string(), LanguageFamily::IndoEuropean);
self.phonetic_inventories.insert(
"ja".to_string(),
PhoneticInventory {
vowel_count: 5,
consonant_count: 15,
has_tones: false,
has_consonant_clusters: false,
common_phonemes: vec!["n", "k", "s", "t", "r", "m", "w", "h", "g", "d"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("ja".to_string(), LanguageFamily::Japonic);
self.phonetic_inventories.insert(
"hi".to_string(),
PhoneticInventory {
vowel_count: 11,
consonant_count: 33,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["n", "r", "k", "t", "s", "m", "l", "d", "p", "h"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("hi".to_string(), LanguageFamily::IndoEuropean);
self.phonetic_inventories.insert(
"ar".to_string(),
PhoneticInventory {
vowel_count: 6,
consonant_count: 28,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["l", "n", "m", "r", "t", "k", "s", "h", "d", "b"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("ar".to_string(), LanguageFamily::AfroAsiatic);
self.phonetic_inventories.insert(
"pt".to_string(),
PhoneticInventory {
vowel_count: 14,
consonant_count: 19,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["s", "r", "n", "t", "l", "d", "m", "k", "p", "v"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("pt".to_string(), LanguageFamily::IndoEuropean);
self.phonetic_inventories.insert(
"ru".to_string(),
PhoneticInventory {
vowel_count: 6,
consonant_count: 35,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["n", "t", "r", "s", "l", "v", "k", "d", "m", "p"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("ru".to_string(), LanguageFamily::IndoEuropean);
self.phonetic_inventories.insert(
"de".to_string(),
PhoneticInventory {
vowel_count: 16,
consonant_count: 23,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["n", "r", "s", "t", "l", "d", "k", "m", "h", "g"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("de".to_string(), LanguageFamily::IndoEuropean);
self.phonetic_inventories.insert(
"fr".to_string(),
PhoneticInventory {
vowel_count: 16,
consonant_count: 20,
has_tones: false,
has_consonant_clusters: true,
common_phonemes: vec!["r", "n", "t", "s", "l", "d", "k", "m", "p", "v"]
.iter()
.map(|s| s.to_string())
.collect(),
},
);
self.language_families
.insert("fr".to_string(), LanguageFamily::IndoEuropean);
}
fn initialize_cultural_preferences(&mut self) {
self.prosodic_preferences.insert(
CulturalRegion::NorthAmerica,
ProsodicPreferences {
preferred_speech_rate: 4.5,
pitch_variation_tolerance: 0.8,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Falling,
},
);
self.communication_styles.insert(
CulturalRegion::NorthAmerica,
CommunicationStyle {
directness_preference: 0.7,
silence_tolerance: 0.3,
expressiveness_preference: 0.6,
formality_expectation: 0.4,
},
);
self.prosodic_preferences.insert(
CulturalRegion::Europe,
ProsodicPreferences {
preferred_speech_rate: 4.2,
pitch_variation_tolerance: 0.7,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Complex,
},
);
self.communication_styles.insert(
CulturalRegion::Europe,
CommunicationStyle {
directness_preference: 0.6,
silence_tolerance: 0.5,
expressiveness_preference: 0.5,
formality_expectation: 0.6,
},
);
self.prosodic_preferences.insert(
CulturalRegion::EastAsia,
ProsodicPreferences {
preferred_speech_rate: 3.8,
pitch_variation_tolerance: 0.9,
stress_pattern_preference: StressPattern::Tonal,
intonation_preference: IntonationPattern::Rising,
},
);
self.communication_styles.insert(
CulturalRegion::EastAsia,
CommunicationStyle {
directness_preference: 0.3,
silence_tolerance: 0.8,
expressiveness_preference: 0.4,
formality_expectation: 0.8,
},
);
self.prosodic_preferences.insert(
CulturalRegion::SouthAsia,
ProsodicPreferences {
preferred_speech_rate: 4.0,
pitch_variation_tolerance: 0.9,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Complex,
},
);
self.communication_styles.insert(
CulturalRegion::SouthAsia,
CommunicationStyle {
directness_preference: 0.4,
silence_tolerance: 0.6,
expressiveness_preference: 0.7,
formality_expectation: 0.7,
},
);
self.prosodic_preferences.insert(
CulturalRegion::MiddleEast,
ProsodicPreferences {
preferred_speech_rate: 4.3,
pitch_variation_tolerance: 0.8,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Complex,
},
);
self.communication_styles.insert(
CulturalRegion::MiddleEast,
CommunicationStyle {
directness_preference: 0.5,
silence_tolerance: 0.4,
expressiveness_preference: 0.8,
formality_expectation: 0.7,
},
);
self.prosodic_preferences.insert(
CulturalRegion::Africa,
ProsodicPreferences {
preferred_speech_rate: 4.1,
pitch_variation_tolerance: 0.9,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Complex,
},
);
self.communication_styles.insert(
CulturalRegion::Africa,
CommunicationStyle {
directness_preference: 0.6,
silence_tolerance: 0.7,
expressiveness_preference: 0.8,
formality_expectation: 0.6,
},
);
self.prosodic_preferences.insert(
CulturalRegion::SouthAmerica,
ProsodicPreferences {
preferred_speech_rate: 4.4,
pitch_variation_tolerance: 0.8,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Rising,
},
);
self.communication_styles.insert(
CulturalRegion::SouthAmerica,
CommunicationStyle {
directness_preference: 0.5,
silence_tolerance: 0.3,
expressiveness_preference: 0.9,
formality_expectation: 0.5,
},
);
self.prosodic_preferences.insert(
CulturalRegion::Oceania,
ProsodicPreferences {
preferred_speech_rate: 4.3,
pitch_variation_tolerance: 0.7,
stress_pattern_preference: StressPattern::Variable,
intonation_preference: IntonationPattern::Falling,
},
);
self.communication_styles.insert(
CulturalRegion::Oceania,
CommunicationStyle {
directness_preference: 0.6,
silence_tolerance: 0.4,
expressiveness_preference: 0.6,
formality_expectation: 0.4,
},
);
}
pub fn calculate_adaptation_factors(
&self,
listener_cultural: &CulturalProfile,
listener_demographic: &DemographicProfile,
audio: &AudioBuffer,
target_language: &str,
) -> EvaluationResult<CrossCulturalAdaptation> {
let mut adaptation = CrossCulturalAdaptation {
phonetic_distance_factor: 1.0,
prosodic_mismatch_factor: 1.0,
accent_familiarity_factor: 1.0,
communication_style_factor: 1.0,
linguistic_distance_factor: 1.0,
};
if self.config.enable_phonetic_distance {
adaptation.phonetic_distance_factor = self.calculate_phonetic_distance_factor(
&listener_demographic.native_language,
target_language,
)?;
}
if self.config.enable_prosodic_preferences {
adaptation.prosodic_mismatch_factor =
self.calculate_prosodic_mismatch_factor(listener_cultural.region, audio)?;
}
if self.config.enable_accent_familiarity {
adaptation.accent_familiarity_factor =
self.calculate_accent_familiarity_factor(listener_cultural, target_language);
}
if self.config.enable_communication_styles {
adaptation.communication_style_factor =
self.calculate_communication_style_factor(listener_cultural.region, audio)?;
}
if self.config.enable_linguistic_distance {
adaptation.linguistic_distance_factor = self.calculate_linguistic_distance_factor(
&listener_demographic.native_language,
target_language,
);
}
Ok(adaptation)
}
fn calculate_phonetic_distance_factor(
&self,
native_lang: &str,
target_lang: &str,
) -> EvaluationResult<f32> {
let native_inventory = self.phonetic_inventories.get(native_lang).ok_or_else(|| {
EvaluationError::QualityEvaluationError {
message: format!("Language {} not supported", native_lang),
source: None,
}
})?;
let target_inventory = self.phonetic_inventories.get(target_lang).ok_or_else(|| {
EvaluationError::QualityEvaluationError {
message: format!("Language {} not supported", target_lang),
source: None,
}
})?;
let vowel_similarity = 1.0
- ((native_inventory.vowel_count as f32 - target_inventory.vowel_count as f32).abs()
/ 20.0)
.min(1.0);
let consonant_similarity = 1.0
- ((native_inventory.consonant_count as f32 - target_inventory.consonant_count as f32)
.abs()
/ 40.0)
.min(1.0);
let tone_penalty = if native_inventory.has_tones != target_inventory.has_tones {
0.8
} else {
1.0
};
let cluster_penalty =
if native_inventory.has_consonant_clusters != target_inventory.has_consonant_clusters {
0.9
} else {
1.0
};
let common_phonemes: std::collections::HashSet<_> =
native_inventory.common_phonemes.iter().collect();
let target_phonemes: std::collections::HashSet<_> =
target_inventory.common_phonemes.iter().collect();
let intersection_size = common_phonemes.intersection(&target_phonemes).count();
let union_size = common_phonemes.union(&target_phonemes).count();
let phoneme_overlap = if union_size > 0 {
intersection_size as f32 / union_size as f32
} else {
0.0
};
let similarity =
(vowel_similarity * 0.3 + consonant_similarity * 0.3 + phoneme_overlap * 0.4)
* tone_penalty
* cluster_penalty;
Ok(0.7 + 0.3 * similarity)
}
fn calculate_prosodic_mismatch_factor(
&self,
listener_region: CulturalRegion,
audio: &AudioBuffer,
) -> EvaluationResult<f32> {
let preferences = self
.prosodic_preferences
.get(&listener_region)
.ok_or_else(|| EvaluationError::QualityEvaluationError {
message: format!("Region {:?} not supported", listener_region),
source: None,
})?;
let samples = audio.samples();
if samples.is_empty() {
return Ok(1.0);
}
let estimated_rate = self.estimate_speech_rate(samples);
let rate_factor =
1.0 - ((estimated_rate - preferences.preferred_speech_rate).abs() / 3.0).min(0.3);
let pitch_variation = self.estimate_pitch_variation(samples);
let pitch_factor =
1.0 - ((pitch_variation - preferences.pitch_variation_tolerance).abs()).min(0.2);
Ok(rate_factor * 0.6 + pitch_factor * 0.4)
}
fn estimate_speech_rate(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 4.0; }
let chunk_size = 800; let mut energy_peaks = 0;
let mut prev_energy = 0.0;
for chunk in samples.chunks(chunk_size) {
let energy = chunk.iter().map(|&x| x * x).sum::<f32>() / chunk.len() as f32;
if energy > prev_energy * 1.5 && energy > 0.01 {
energy_peaks += 1;
}
prev_energy = energy;
}
let duration_seconds = samples.len() as f32 / 16000.0; if duration_seconds > 0.0 {
(energy_peaks as f32 / duration_seconds).clamp(2.0, 8.0)
} else {
4.0
}
}
fn estimate_pitch_variation(&self, samples: &[f32]) -> f32 {
if samples.len() < 1600 {
return 0.5; }
let mut variations = Vec::new();
let chunk_size = 1600;
for chunk in samples.chunks(chunk_size) {
if chunk.len() == chunk_size {
let pitch_estimate = self.simple_pitch_detection(chunk);
variations.push(pitch_estimate);
}
}
if variations.len() < 2 {
return 0.5;
}
let mean = variations.iter().sum::<f32>() / variations.len() as f32;
let variance =
variations.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / variations.len() as f32;
let std_dev = variance.sqrt();
if mean > 0.0 {
(std_dev / mean).clamp(0.1, 1.0)
} else {
0.5
}
}
fn simple_pitch_detection(&self, samples: &[f32]) -> f32 {
let min_period = 40; let max_period = 400;
let mut best_corr = 0.0;
let mut best_period = min_period;
for period in min_period..=max_period.min(samples.len() / 2) {
let mut correlation = 0.0;
let mut count = 0;
for i in 0..(samples.len() - period) {
correlation += samples[i] * samples[i + period];
count += 1;
}
if count > 0 {
correlation /= count as f32;
if correlation > best_corr {
best_corr = correlation;
best_period = period;
}
}
}
16000.0 / best_period as f32 }
fn calculate_accent_familiarity_factor(
&self,
listener_cultural: &CulturalProfile,
target_language: &str,
) -> f32 {
let is_familiar = listener_cultural
.language_familiarity
.contains(&target_language.to_string());
if is_familiar {
1.0 } else {
0.6 + 0.4 * listener_cultural.accent_tolerance
}
}
fn calculate_communication_style_factor(
&self,
listener_region: CulturalRegion,
audio: &AudioBuffer,
) -> EvaluationResult<f32> {
let style = self
.communication_styles
.get(&listener_region)
.ok_or_else(|| EvaluationError::QualityEvaluationError {
message: format!("Region {:?} not supported", listener_region),
source: None,
})?;
let samples = audio.samples();
if samples.is_empty() {
return Ok(1.0);
}
let rms = (samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
let peak = samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
let dynamic_range = if rms > 0.0 { peak / rms } else { 1.0 };
let audio_expressiveness = (dynamic_range / 10.0).clamp(0.0, 1.0);
let expressiveness_alignment =
1.0 - (audio_expressiveness - style.expressiveness_preference).abs();
let pitch_variation = self.estimate_pitch_variation(samples);
let audio_formality = 1.0 - pitch_variation; let formality_alignment = 1.0 - (audio_formality - style.formality_expectation).abs();
Ok(expressiveness_alignment * 0.6 + formality_alignment * 0.4)
}
pub fn calculate_linguistic_distance_factor(
&self,
native_lang: &str,
target_lang: &str,
) -> f32 {
if native_lang == target_lang {
return 1.0; }
let native_family = self
.language_families
.get(native_lang)
.unwrap_or(&LanguageFamily::Other);
let target_family = self
.language_families
.get(target_lang)
.unwrap_or(&LanguageFamily::Other);
if native_family == target_family {
0.9 } else {
match (native_family, target_family) {
(LanguageFamily::IndoEuropean, _) | (_, LanguageFamily::IndoEuropean) => 0.8,
_ => 0.7,
}
}
}
pub fn apply_cultural_adaptation(
&self,
base_score: f32,
adaptation: &CrossCulturalAdaptation,
) -> f32 {
let mut adapted_score = base_score;
adapted_score *= adaptation.phonetic_distance_factor;
adapted_score *= adaptation.prosodic_mismatch_factor;
adapted_score *= adaptation.accent_familiarity_factor;
adapted_score *= adaptation.communication_style_factor;
adapted_score *= adaptation.linguistic_distance_factor;
adapted_score.clamp(0.0, 1.0)
}
pub fn get_supported_languages(&self) -> Vec<String> {
self.phonetic_inventories.keys().cloned().collect()
}
pub fn get_cultural_regions(&self) -> Vec<CulturalRegion> {
self.prosodic_preferences.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::perceptual::{CulturalProfile, CulturalRegion, DemographicProfile};
#[test]
fn test_cross_cultural_model_creation() {
let config = CrossCulturalConfig::default();
let model = CrossCulturalPerceptualModel::new(config);
assert!(!model.get_supported_languages().is_empty());
assert!(!model.get_cultural_regions().is_empty());
}
#[test]
fn test_phonetic_distance_calculation() {
let model = CrossCulturalPerceptualModel::new(CrossCulturalConfig::default());
let same_lang_factor = model
.calculate_phonetic_distance_factor("en", "en")
.unwrap();
assert!(same_lang_factor > 0.9);
let related_factor = model
.calculate_phonetic_distance_factor("en", "de")
.unwrap();
assert!(related_factor > 0.6 && related_factor < 1.0);
let distant_factor = model
.calculate_phonetic_distance_factor("en", "zh")
.unwrap();
assert!(distant_factor < 1.0); }
#[test]
fn test_linguistic_distance_factor() {
let model = CrossCulturalPerceptualModel::new(CrossCulturalConfig::default());
assert_eq!(model.calculate_linguistic_distance_factor("en", "en"), 1.0);
let same_family = model.calculate_linguistic_distance_factor("en", "de");
assert_eq!(same_family, 0.9);
let diff_family = model.calculate_linguistic_distance_factor("en", "zh");
assert_eq!(diff_family, 0.8);
}
#[tokio::test]
async fn test_adaptation_factor_calculation() {
let model = CrossCulturalPerceptualModel::new(CrossCulturalConfig::default());
let cultural_profile = CulturalProfile {
region: CulturalRegion::NorthAmerica,
language_familiarity: vec!["en".to_string()],
musical_training: false,
accent_tolerance: 0.7,
};
let demographic_profile = DemographicProfile {
age_group: crate::perceptual::AgeGroup::MiddleAged,
gender: crate::perceptual::Gender::Other,
education_level: crate::perceptual::EducationLevel::Bachelor,
native_language: "en".to_string(),
audio_experience: crate::perceptual::ExperienceLevel::Intermediate,
};
let samples = (0..1000)
.map(|i| [0.1, 0.2, -0.1, -0.2][i % 4])
.collect::<Vec<f32>>();
let audio = AudioBuffer::new(samples, 16000, 1);
let adaptation = model
.calculate_adaptation_factors(&cultural_profile, &demographic_profile, &audio, "en")
.unwrap();
assert!(
adaptation.phonetic_distance_factor >= 0.5
&& adaptation.phonetic_distance_factor <= 1.0
);
assert!(
adaptation.prosodic_mismatch_factor >= 0.5
&& adaptation.prosodic_mismatch_factor <= 1.0
);
assert!(
adaptation.accent_familiarity_factor >= 0.5
&& adaptation.accent_familiarity_factor <= 1.0
);
assert!(
adaptation.communication_style_factor >= 0.5
&& adaptation.communication_style_factor <= 1.0
);
assert!(
adaptation.linguistic_distance_factor >= 0.5
&& adaptation.linguistic_distance_factor <= 1.0
);
}
#[test]
fn test_cultural_adaptation_application() {
let model = CrossCulturalPerceptualModel::new(CrossCulturalConfig::default());
let adaptation = CrossCulturalAdaptation {
phonetic_distance_factor: 0.9,
prosodic_mismatch_factor: 0.8,
accent_familiarity_factor: 1.0,
communication_style_factor: 0.9,
linguistic_distance_factor: 0.9,
};
let base_score = 0.8;
let adapted_score = model.apply_cultural_adaptation(base_score, &adaptation);
assert!(adapted_score < base_score);
assert!(adapted_score >= 0.0 && adapted_score <= 1.0);
}
}