use crate::error::{Result, TtsError};
use misaki_rs::{Language, G2P};
use std::sync::{LazyLock, Mutex};
pub(crate) static PHONEMIZER: LazyLock<MisakiG2p> = LazyLock::new(MisakiG2p::new);
pub struct MisakiG2p {
inner: Mutex<Option<G2P>>,
}
impl MisakiG2p {
pub fn new() -> Self {
Self {
inner: Mutex::new(None),
}
}
fn get_or_init(&self) -> Result<std::sync::MutexGuard<'_, Option<G2P>>> {
let mut guard = self
.inner
.lock()
.map_err(|e| TtsError::Phonemize(format!("misaki mutex poisoned: {e}")))?;
if guard.is_none() {
*guard = Some(G2P::new(Language::EnglishUS));
}
Ok(guard)
}
pub fn phonemize(&self, text: &str) -> Result<String> {
if text.trim().is_empty() {
return Ok(String::new());
}
let mut guard = self.get_or_init()?;
let g2p = guard.as_mut().expect("just initialised");
let (ipa, _tokens) = g2p
.g2p(text)
.map_err(|e| TtsError::Phonemize(format!("misaki g2p: {e}")))?;
Ok(ipa.trim().to_string())
}
}
impl Default for MisakiG2p {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn misaki_basic_phonemes() {
let g2p = MisakiG2p::new();
let ipa = g2p.phonemize("Hello, world!").expect("phonemize");
assert!(!ipa.is_empty(), "should produce phonemes");
assert!(
!ipa.contains('❓'),
"should not contain unknown marker: {ipa}"
);
}
#[test]
fn misaki_empty_input() {
let g2p = MisakiG2p::new();
assert_eq!(g2p.phonemize("").unwrap(), "");
assert_eq!(g2p.phonemize(" ").unwrap(), "");
}
#[test]
fn misaki_known_words() {
let g2p = MisakiG2p::new();
for word in ["hello", "world", "testing", "skadoosh", "voice", "agent"] {
let ipa = g2p.phonemize(word).expect(word);
assert!(!ipa.is_empty(), "{word}: empty output");
}
}
}