Whole-workspace rustfmt pass picked up while iterating on Mamba GPU backward work. Verified formatting-only via diff sampling; no logic changed. Co-Authored-By: Claude Sonnet 5 <[email protected]>
846 lines
26 KiB
Rust
846 lines
26 KiB
Rust
//! Grapheme-to-phoneme conversion for TTS
|
|
//!
|
|
//! This module provides phonemization capabilities using:
|
|
//! - Dictionary-based lookup for common words
|
|
//! - Rule-based fallback for unknown words
|
|
//! - ARPAbet phoneme representation for English
|
|
|
|
use crate::error::{Result, TtsError};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
/// Phonemizer backend type
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum PhonemizerBackend {
|
|
/// Dictionary-based lookup
|
|
Dictionary,
|
|
/// Rule-based phonemization
|
|
Rule,
|
|
/// Grapheme-to-phoneme model (future)
|
|
G2P,
|
|
}
|
|
|
|
/// Configuration for phonemization
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PhonemizerConfig {
|
|
/// Language code (e.g., "en-us", "en-gb")
|
|
pub language: String,
|
|
/// Phonemization backend
|
|
pub backend: PhonemizerBackend,
|
|
/// Include stress markers (0=no stress, 1=primary, 2=secondary)
|
|
pub include_stress: bool,
|
|
}
|
|
|
|
impl Default for PhonemizerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
language: "en-us".to_string(),
|
|
backend: PhonemizerBackend::Dictionary,
|
|
include_stress: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Phoneme representation with optional stress
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Phoneme {
|
|
/// Phoneme symbol (ARPAbet for English)
|
|
pub symbol: String,
|
|
/// Stress level (0=unstressed, 1=primary, 2=secondary)
|
|
pub stress: Option<u8>,
|
|
}
|
|
|
|
impl Phoneme {
|
|
/// Create a new phoneme
|
|
#[must_use]
|
|
pub fn new(symbol: String, stress: Option<u8>) -> Self {
|
|
Self { symbol, stress }
|
|
}
|
|
|
|
/// Create a phoneme without stress
|
|
#[must_use]
|
|
pub fn unstressed(symbol: String) -> Self {
|
|
Self {
|
|
symbol,
|
|
stress: None,
|
|
}
|
|
}
|
|
|
|
/// Convert phoneme to string representation
|
|
#[must_use]
|
|
pub fn to_string_with_stress(&self) -> String {
|
|
if let Some(stress) = self.stress {
|
|
format!("{}{}", self.symbol, stress)
|
|
} else {
|
|
self.symbol.clone()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Grapheme-to-phoneme converter
|
|
#[derive(Debug)]
|
|
pub struct Phonemizer {
|
|
config: PhonemizerConfig,
|
|
dictionary: HashMap<String, Vec<Phoneme>>,
|
|
}
|
|
|
|
impl Phonemizer {
|
|
/// Create a new phonemizer with the given configuration
|
|
#[must_use]
|
|
pub fn new(config: PhonemizerConfig) -> Self {
|
|
let mut dictionary = HashMap::new();
|
|
|
|
// Build English pronunciation dictionary (ARPAbet)
|
|
// Common words with stress markers
|
|
Self::populate_dictionary(&mut dictionary);
|
|
|
|
Self { config, dictionary }
|
|
}
|
|
|
|
/// Create a phonemizer with default configuration
|
|
#[must_use]
|
|
pub fn default() -> Self {
|
|
Self::new(PhonemizerConfig::default())
|
|
}
|
|
|
|
/// Get the configuration
|
|
#[must_use]
|
|
pub fn config(&self) -> &PhonemizerConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Populate the pronunciation dictionary with common English words
|
|
fn populate_dictionary(dict: &mut HashMap<String, Vec<Phoneme>>) {
|
|
// Pronouns
|
|
dict.insert(
|
|
"i".to_string(),
|
|
vec![Phoneme::new("AY".to_string(), Some(1))],
|
|
);
|
|
dict.insert(
|
|
"you".to_string(),
|
|
vec![
|
|
Phoneme::new("Y".to_string(), None),
|
|
Phoneme::new("UW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"he".to_string(),
|
|
vec![
|
|
Phoneme::new("HH".to_string(), None),
|
|
Phoneme::new("IY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"she".to_string(),
|
|
vec![
|
|
Phoneme::new("SH".to_string(), None),
|
|
Phoneme::new("IY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"it".to_string(),
|
|
vec![
|
|
Phoneme::new("IH".to_string(), Some(1)),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"we".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("IY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"they".to_string(),
|
|
vec![
|
|
Phoneme::new("DH".to_string(), None),
|
|
Phoneme::new("EY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
|
|
// Common verbs
|
|
dict.insert(
|
|
"is".to_string(),
|
|
vec![
|
|
Phoneme::new("IH".to_string(), Some(1)),
|
|
Phoneme::new("Z".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"are".to_string(),
|
|
vec![
|
|
Phoneme::new("AA".to_string(), Some(1)),
|
|
Phoneme::new("R".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"was".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("AA".to_string(), Some(1)),
|
|
Phoneme::new("Z".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"have".to_string(),
|
|
vec![
|
|
Phoneme::new("HH".to_string(), None),
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("V".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"had".to_string(),
|
|
vec![
|
|
Phoneme::new("HH".to_string(), None),
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"do".to_string(),
|
|
vec![
|
|
Phoneme::new("D".to_string(), None),
|
|
Phoneme::new("UW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"did".to_string(),
|
|
vec![
|
|
Phoneme::new("D".to_string(), None),
|
|
Phoneme::new("IH".to_string(), Some(1)),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"said".to_string(),
|
|
vec![
|
|
Phoneme::new("S".to_string(), None),
|
|
Phoneme::new("EH".to_string(), Some(1)),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"go".to_string(),
|
|
vec![
|
|
Phoneme::new("G".to_string(), None),
|
|
Phoneme::new("OW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"went".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("EH".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"make".to_string(),
|
|
vec![
|
|
Phoneme::new("M".to_string(), None),
|
|
Phoneme::new("EY".to_string(), Some(1)),
|
|
Phoneme::new("K".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"get".to_string(),
|
|
vec![
|
|
Phoneme::new("G".to_string(), None),
|
|
Phoneme::new("EH".to_string(), Some(1)),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
|
|
// Common nouns
|
|
dict.insert(
|
|
"time".to_string(),
|
|
vec![
|
|
Phoneme::new("T".to_string(), None),
|
|
Phoneme::new("AY".to_string(), Some(1)),
|
|
Phoneme::new("M".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"day".to_string(),
|
|
vec![
|
|
Phoneme::new("D".to_string(), None),
|
|
Phoneme::new("EY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"man".to_string(),
|
|
vec![
|
|
Phoneme::new("M".to_string(), None),
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"woman".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("UH".to_string(), Some(1)),
|
|
Phoneme::new("M".to_string(), None),
|
|
Phoneme::new("AH".to_string(), Some(0)),
|
|
Phoneme::new("N".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"child".to_string(),
|
|
vec![
|
|
Phoneme::new("CH".to_string(), None),
|
|
Phoneme::new("AY".to_string(), Some(1)),
|
|
Phoneme::new("L".to_string(), None),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"world".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("ER".to_string(), Some(1)),
|
|
Phoneme::new("L".to_string(), None),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"house".to_string(),
|
|
vec![
|
|
Phoneme::new("HH".to_string(), None),
|
|
Phoneme::new("AW".to_string(), Some(1)),
|
|
Phoneme::new("S".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"book".to_string(),
|
|
vec![
|
|
Phoneme::new("B".to_string(), None),
|
|
Phoneme::new("UH".to_string(), Some(1)),
|
|
Phoneme::new("K".to_string(), None),
|
|
],
|
|
);
|
|
|
|
// Common adjectives
|
|
dict.insert(
|
|
"good".to_string(),
|
|
vec![
|
|
Phoneme::new("G".to_string(), None),
|
|
Phoneme::new("UH".to_string(), Some(1)),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"great".to_string(),
|
|
vec![
|
|
Phoneme::new("G".to_string(), None),
|
|
Phoneme::new("R".to_string(), None),
|
|
Phoneme::new("EY".to_string(), Some(1)),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"new".to_string(),
|
|
vec![
|
|
Phoneme::new("N".to_string(), None),
|
|
Phoneme::new("UW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"old".to_string(),
|
|
vec![
|
|
Phoneme::new("OW".to_string(), Some(1)),
|
|
Phoneme::new("L".to_string(), None),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"big".to_string(),
|
|
vec![
|
|
Phoneme::new("B".to_string(), None),
|
|
Phoneme::new("IH".to_string(), Some(1)),
|
|
Phoneme::new("G".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"small".to_string(),
|
|
vec![
|
|
Phoneme::new("S".to_string(), None),
|
|
Phoneme::new("M".to_string(), None),
|
|
Phoneme::new("AO".to_string(), Some(1)),
|
|
Phoneme::new("L".to_string(), None),
|
|
],
|
|
);
|
|
|
|
// Articles and common words
|
|
dict.insert(
|
|
"the".to_string(),
|
|
vec![
|
|
Phoneme::new("DH".to_string(), None),
|
|
Phoneme::new("AH".to_string(), Some(0)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"a".to_string(),
|
|
vec![Phoneme::new("AH".to_string(), Some(0))],
|
|
);
|
|
dict.insert(
|
|
"an".to_string(),
|
|
vec![
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"and".to_string(),
|
|
vec![
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
Phoneme::new("D".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"or".to_string(),
|
|
vec![
|
|
Phoneme::new("AO".to_string(), Some(1)),
|
|
Phoneme::new("R".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"but".to_string(),
|
|
vec![
|
|
Phoneme::new("B".to_string(), None),
|
|
Phoneme::new("AH".to_string(), Some(1)),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"to".to_string(),
|
|
vec![
|
|
Phoneme::new("T".to_string(), None),
|
|
Phoneme::new("UW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"in".to_string(),
|
|
vec![
|
|
Phoneme::new("IH".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"on".to_string(),
|
|
vec![
|
|
Phoneme::new("AA".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"at".to_string(),
|
|
vec![
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"for".to_string(),
|
|
vec![
|
|
Phoneme::new("F".to_string(), None),
|
|
Phoneme::new("AO".to_string(), Some(1)),
|
|
Phoneme::new("R".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"with".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("IH".to_string(), Some(1)),
|
|
Phoneme::new("DH".to_string(), None),
|
|
],
|
|
);
|
|
|
|
// Numbers
|
|
dict.insert(
|
|
"one".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("AH".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"two".to_string(),
|
|
vec![
|
|
Phoneme::new("T".to_string(), None),
|
|
Phoneme::new("UW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"three".to_string(),
|
|
vec![
|
|
Phoneme::new("TH".to_string(), None),
|
|
Phoneme::new("R".to_string(), None),
|
|
Phoneme::new("IY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"four".to_string(),
|
|
vec![
|
|
Phoneme::new("F".to_string(), None),
|
|
Phoneme::new("AO".to_string(), Some(1)),
|
|
Phoneme::new("R".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"five".to_string(),
|
|
vec![
|
|
Phoneme::new("F".to_string(), None),
|
|
Phoneme::new("AY".to_string(), Some(1)),
|
|
Phoneme::new("V".to_string(), None),
|
|
],
|
|
);
|
|
|
|
// Common contractions
|
|
dict.insert(
|
|
"can't".to_string(),
|
|
vec![
|
|
Phoneme::new("K".to_string(), None),
|
|
Phoneme::new("AE".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"don't".to_string(),
|
|
vec![
|
|
Phoneme::new("D".to_string(), None),
|
|
Phoneme::new("OW".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"won't".to_string(),
|
|
vec![
|
|
Phoneme::new("W".to_string(), None),
|
|
Phoneme::new("OW".to_string(), Some(1)),
|
|
Phoneme::new("N".to_string(), None),
|
|
Phoneme::new("T".to_string(), None),
|
|
],
|
|
);
|
|
|
|
// Greetings
|
|
dict.insert(
|
|
"hello".to_string(),
|
|
vec![
|
|
Phoneme::new("HH".to_string(), None),
|
|
Phoneme::new("AH".to_string(), Some(0)),
|
|
Phoneme::new("L".to_string(), None),
|
|
Phoneme::new("OW".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"hi".to_string(),
|
|
vec![
|
|
Phoneme::new("HH".to_string(), None),
|
|
Phoneme::new("AY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
dict.insert(
|
|
"goodbye".to_string(),
|
|
vec![
|
|
Phoneme::new("G".to_string(), None),
|
|
Phoneme::new("UH".to_string(), Some(1)),
|
|
Phoneme::new("D".to_string(), None),
|
|
Phoneme::new("B".to_string(), None),
|
|
Phoneme::new("AY".to_string(), Some(1)),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Convert text to phonemes
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if phonemization fails
|
|
pub fn g2p(&self, text: &str) -> Result<Vec<Vec<Phoneme>>> {
|
|
let words: Vec<&str> = text.split_whitespace().collect();
|
|
let mut result = Vec::new();
|
|
|
|
for word in words {
|
|
let word_lower = word.to_lowercase();
|
|
let phonemes = self.phonemize_word(&word_lower)?;
|
|
result.push(phonemes);
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Phonemize a single word
|
|
fn phonemize_word(&self, word: &str) -> Result<Vec<Phoneme>> {
|
|
// Try dictionary lookup first
|
|
if let Some(phonemes) = self.dictionary.get(word) {
|
|
return Ok(phonemes.clone());
|
|
}
|
|
|
|
// Fallback to rule-based phonemization
|
|
self.rule_based_phonemization(word)
|
|
}
|
|
|
|
/// Rule-based phonemization for unknown words
|
|
fn rule_based_phonemization(&self, word: &str) -> Result<Vec<Phoneme>> {
|
|
let mut phonemes = Vec::new();
|
|
let chars: Vec<char> = word.chars().collect();
|
|
let mut i = 0;
|
|
|
|
while i < chars.len() {
|
|
let ch = chars[i];
|
|
|
|
// Simple rule-based conversion (very basic)
|
|
let phoneme = match ch {
|
|
'a' => Phoneme::new("AE".to_string(), Some(1)),
|
|
'b' => Phoneme::unstressed("B".to_string()),
|
|
'c' => Phoneme::unstressed("K".to_string()),
|
|
'd' => Phoneme::unstressed("D".to_string()),
|
|
'e' => Phoneme::new("EH".to_string(), Some(1)),
|
|
'f' => Phoneme::unstressed("F".to_string()),
|
|
'g' => Phoneme::unstressed("G".to_string()),
|
|
'h' => Phoneme::unstressed("HH".to_string()),
|
|
'i' => Phoneme::new("IH".to_string(), Some(1)),
|
|
'j' => Phoneme::unstressed("JH".to_string()),
|
|
'k' => Phoneme::unstressed("K".to_string()),
|
|
'l' => Phoneme::unstressed("L".to_string()),
|
|
'm' => Phoneme::unstressed("M".to_string()),
|
|
'n' => Phoneme::unstressed("N".to_string()),
|
|
'o' => Phoneme::new("AA".to_string(), Some(1)),
|
|
'p' => Phoneme::unstressed("P".to_string()),
|
|
'q' => Phoneme::unstressed("K".to_string()),
|
|
'r' => Phoneme::unstressed("R".to_string()),
|
|
's' => Phoneme::unstressed("S".to_string()),
|
|
't' => Phoneme::unstressed("T".to_string()),
|
|
'u' => Phoneme::new("AH".to_string(), Some(1)),
|
|
'v' => Phoneme::unstressed("V".to_string()),
|
|
'w' => Phoneme::unstressed("W".to_string()),
|
|
'x' => Phoneme::unstressed("K".to_string()),
|
|
'y' => Phoneme::unstressed("Y".to_string()),
|
|
'z' => Phoneme::unstressed("Z".to_string()),
|
|
_ => {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
phonemes.push(phoneme);
|
|
i += 1;
|
|
}
|
|
|
|
if phonemes.is_empty() {
|
|
return Err(TtsError::Phonemization(format!(
|
|
"Could not phonemize word: {word}"
|
|
)));
|
|
}
|
|
|
|
Ok(phonemes)
|
|
}
|
|
|
|
/// Convert phonemes to string representation
|
|
#[must_use]
|
|
pub fn phonemes_to_string(&self, phonemes: &[Vec<Phoneme>]) -> String {
|
|
phonemes
|
|
.iter()
|
|
.map(|word_phonemes| {
|
|
word_phonemes
|
|
.iter()
|
|
.map(|p| {
|
|
if self.config.include_stress {
|
|
p.to_string_with_stress()
|
|
} else {
|
|
p.symbol.clone()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" | ")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = PhonemizerConfig::default();
|
|
assert_eq!(config.language, "en-us");
|
|
assert_eq!(config.backend, PhonemizerBackend::Dictionary);
|
|
assert!(config.include_stress);
|
|
}
|
|
|
|
#[test]
|
|
fn test_phoneme_creation() {
|
|
let phoneme = Phoneme::new("AE".to_string(), Some(1));
|
|
assert_eq!(phoneme.symbol, "AE");
|
|
assert_eq!(phoneme.stress, Some(1));
|
|
assert_eq!(phoneme.to_string_with_stress(), "AE1");
|
|
|
|
let unstressed = Phoneme::unstressed("B".to_string());
|
|
assert_eq!(unstressed.symbol, "B");
|
|
assert_eq!(unstressed.stress, None);
|
|
assert_eq!(unstressed.to_string_with_stress(), "B");
|
|
}
|
|
|
|
#[test]
|
|
fn test_phonemizer_creation() {
|
|
let phonemizer = Phonemizer::default();
|
|
assert!(!phonemizer.dictionary.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_dictionary_lookup() {
|
|
let phonemizer = Phonemizer::default();
|
|
let result = phonemizer.g2p("hello").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
assert!(!result[0].is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_common_words() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
let result = phonemizer.g2p("the").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
assert_eq!(result[0][0].symbol, "DH");
|
|
|
|
let result = phonemizer.g2p("hello").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
assert_eq!(result[0][0].symbol, "HH");
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_words() {
|
|
let phonemizer = Phonemizer::default();
|
|
let result = phonemizer.g2p("hello world").unwrap();
|
|
assert_eq!(result.len(), 2);
|
|
assert!(!result[0].is_empty());
|
|
assert!(!result[1].is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_pronouns() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
let result = phonemizer.g2p("i").unwrap();
|
|
assert_eq!(result[0][0].symbol, "AY");
|
|
assert_eq!(result[0][0].stress, Some(1));
|
|
|
|
let result = phonemizer.g2p("you").unwrap();
|
|
assert_eq!(result[0][0].symbol, "Y");
|
|
assert_eq!(result[0][1].symbol, "UW");
|
|
}
|
|
|
|
#[test]
|
|
fn test_verbs() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
let result = phonemizer.g2p("is").unwrap();
|
|
assert_eq!(result[0][0].symbol, "IH");
|
|
assert_eq!(result[0][1].symbol, "Z");
|
|
|
|
let result = phonemizer.g2p("have").unwrap();
|
|
assert_eq!(result[0].len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_numbers() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
let result = phonemizer.g2p("one two three").unwrap();
|
|
assert_eq!(result.len(), 3);
|
|
assert!(!result[0].is_empty());
|
|
assert!(!result[1].is_empty());
|
|
assert!(!result[2].is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_contractions() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
let result = phonemizer.g2p("can't").unwrap();
|
|
assert!(!result[0].is_empty());
|
|
|
|
let result = phonemizer.g2p("don't").unwrap();
|
|
assert!(!result[0].is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_rule_based_fallback() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
// Unknown word should use rule-based phonemization
|
|
let result = phonemizer.g2p("xyz").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
assert!(!result[0].is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_phonemes_to_string_with_stress() {
|
|
let phonemizer = Phonemizer::default();
|
|
let result = phonemizer.g2p("hello").unwrap();
|
|
let phoneme_str = phonemizer.phonemes_to_string(&result);
|
|
assert!(phoneme_str.contains("HH"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_phonemes_to_string_without_stress() {
|
|
let config = PhonemizerConfig {
|
|
language: "en-us".to_string(),
|
|
backend: PhonemizerBackend::Dictionary,
|
|
include_stress: false,
|
|
};
|
|
let phonemizer = Phonemizer::new(config);
|
|
let result = phonemizer.g2p("hello").unwrap();
|
|
let phoneme_str = phonemizer.phonemes_to_string(&result);
|
|
assert!(phoneme_str.contains("HH"));
|
|
assert!(!phoneme_str.contains('0'));
|
|
assert!(!phoneme_str.contains('1'));
|
|
}
|
|
|
|
#[test]
|
|
fn test_case_insensitive() {
|
|
let phonemizer = Phonemizer::default();
|
|
|
|
let result1 = phonemizer.g2p("HELLO").unwrap();
|
|
let result2 = phonemizer.g2p("hello").unwrap();
|
|
let result3 = phonemizer.g2p("Hello").unwrap();
|
|
|
|
assert_eq!(result1, result2);
|
|
assert_eq!(result2, result3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stress_markers() {
|
|
let phonemizer = Phonemizer::default();
|
|
let result = phonemizer.g2p("hello").unwrap();
|
|
|
|
// Check that stress markers are present
|
|
let has_stress = result[0].iter().any(|p| p.stress.is_some());
|
|
assert!(has_stress);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sentence_phonemization() {
|
|
let phonemizer = Phonemizer::default();
|
|
let result = phonemizer.g2p("the quick brown fox").unwrap();
|
|
assert_eq!(result.len(), 4);
|
|
|
|
// Verify each word has phonemes
|
|
for word_phonemes in &result {
|
|
assert!(!word_phonemes.is_empty());
|
|
}
|
|
}
|
|
}
|