use alloc::vec::Vec;
use svara::phoneme::Phoneme;
use svara::prosody::Stress;
use svara::sequence::PhonemeEvent;
use crate::normalize::SentenceType;
use crate::syllable::Syllable;
#[must_use]
pub fn assign_stress(phonemes: &[Phoneme], is_content_word: bool) -> Vec<PhonemeEvent> {
let mut events = Vec::with_capacity(phonemes.len());
let mut found_primary = false;
for &ph in phonemes {
let dur = svara::phoneme::phoneme_duration(&ph);
let stress = if !is_content_word {
Stress::Unstressed
} else if is_vowel_like(&ph) && !found_primary {
found_primary = true;
Stress::Primary
} else if is_vowel_like(&ph) {
Stress::Secondary
} else {
Stress::Unstressed
};
events.push(PhonemeEvent::new(ph, dur, stress));
}
events
}
#[must_use]
pub fn assign_stress_syllabic(syllables: &[Syllable], is_content_word: bool) -> Vec<PhonemeEvent> {
if syllables.is_empty() {
return Vec::new();
}
let primary_idx = if !is_content_word {
usize::MAX } else if syllables.len() <= 2 {
0 } else {
let penult = syllables.len() - 2;
if syllables[penult].is_heavy() {
penult
} else {
syllables.len().saturating_sub(3)
}
};
let mut events = Vec::new();
for (syl_idx, syllable) in syllables.iter().enumerate() {
let syl_stress = if syl_idx == primary_idx {
Stress::Primary
} else {
Stress::Unstressed
};
for &ph in &syllable.onset {
let dur = svara::phoneme::phoneme_duration(&ph);
events.push(PhonemeEvent::new(ph, dur, Stress::Unstressed));
}
let dur = svara::phoneme::phoneme_duration(&syllable.nucleus);
events.push(PhonemeEvent::new(syllable.nucleus, dur, syl_stress));
for &ph in &syllable.coda {
let dur = svara::phoneme::phoneme_duration(&ph);
events.push(PhonemeEvent::new(ph, dur, Stress::Unstressed));
}
}
events
}
#[must_use]
fn is_vowel_like(ph: &Phoneme) -> bool {
use svara::phoneme::PhonemeClass;
matches!(ph.class(), PhonemeClass::Vowel | PhonemeClass::Diphthong)
}
#[must_use]
pub fn is_content_word(word: &str) -> bool {
!matches!(
word.to_lowercase().as_str(),
"a" | "an"
| "the"
| "is"
| "am"
| "are"
| "was"
| "were"
| "be"
| "been"
| "to"
| "of"
| "in"
| "on"
| "at"
| "by"
| "for"
| "and"
| "or"
| "but"
| "if"
| "it"
| "he"
| "she"
| "we"
| "they"
| "that"
| "this"
| "with"
| "not"
| "do"
| "did"
| "has"
| "had"
| "have"
)
}
const DEFAULT_WPM: f32 = 150.0;
const MIN_WPM: f32 = 50.0;
const MAX_WPM: f32 = 300.0;
const MIN_CONSONANT_DURATION: f32 = 0.03;
const MIN_VOWEL_DURATION: f32 = 0.05;
const EMPHASIS_DURATION_SCALE: f32 = 1.3;
pub fn apply_emphasis(events: &mut [PhonemeEvent]) {
for event in events.iter_mut() {
if is_vowel_like(&event.phoneme) {
event.stress = Stress::Primary;
event.duration *= EMPHASIS_DURATION_SCALE;
}
}
}
pub fn apply_rate(events: &mut [PhonemeEvent], target_wpm: f32) {
let clamped_wpm = target_wpm.clamp(MIN_WPM, MAX_WPM);
let scale = DEFAULT_WPM / clamped_wpm;
for event in events.iter_mut() {
let scaled = event.duration * scale;
let min = if is_vowel_like(&event.phoneme) {
MIN_VOWEL_DURATION
} else {
MIN_CONSONANT_DURATION
};
event.duration = scaled.max(min);
}
}
pub fn apply_timing(events: &mut [PhonemeEvent], profile: &crate::engine::TimingProfile) {
for event in events.iter_mut() {
let scale = if event.phoneme == Phoneme::Silence {
profile.pause_scale
} else if is_vowel_like(&event.phoneme) {
profile.vowel_scale
} else {
profile.consonant_scale
};
event.duration *= scale;
}
}
#[must_use]
pub fn sentence_intonation(sentence_type: SentenceType) -> svara::prosody::IntonationPattern {
match sentence_type {
SentenceType::Statement => svara::prosody::IntonationPattern::Declarative,
SentenceType::Question => svara::prosody::IntonationPattern::Interrogative,
SentenceType::Exclamation => svara::prosody::IntonationPattern::Exclamatory,
}
}