use std::sync::Arc;
use super::chunk::{en_tokenize, split_segments};
use super::dialect::Dialect;
use crate::error::{Error, Result};
use crate::g2p::misaki::{KOKORO_UNK, MisakiG2p, OovFallback, TextFrontEnd};
pub use super::chunk::TextChunk;
pub struct KokoroG2p {
us: MisakiG2p,
gb: MisakiG2p,
}
impl KokoroG2p {
pub fn new(front: Arc<TextFrontEnd>) -> Result<Self> {
Ok(Self {
us: MisakiG2p::new(false)?
.with_unk(KOKORO_UNK)
.with_text_front(front.clone()),
gb: MisakiG2p::new(true)?
.with_unk(KOKORO_UNK)
.with_text_front(front),
})
}
pub fn with_fallback(
mut self,
us_fallback: Box<dyn OovFallback + Send + Sync>,
gb_fallback: Box<dyn OovFallback + Send + Sync>,
) -> Self {
self.us = self.us.with_fallback(us_fallback);
self.gb = self.gb.with_fallback(gb_fallback);
self
}
pub fn chunks(&self, text: &str, dialect: Dialect) -> Result<Vec<TextChunk>> {
let g2p = match dialect {
Dialect::Us => &self.us,
Dialect::Gb => &self.gb,
};
let mut out = Vec::new();
for segment in split_segments(text) {
if segment.trim().is_empty() {
continue;
}
let phonemized = g2p.phonemize_text(segment)?;
out.extend(
en_tokenize(phonemized.tokens)
.into_iter()
.filter(|c| !c.phonemes.is_empty()),
);
}
if out.is_empty() {
return Err(Error::InvalidArgument {
arg: "text",
reason: "no phonemizable text — every segment produced empty phonemes".into(),
});
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::g2p::spacy_tagger::EnglishTagger;
use crate::g2p::spacy_tagger::test_support::tagger_dir;
fn kokoro_g2p() -> Option<KokoroG2p> {
let dir = tagger_dir()?;
let tagger = EnglishTagger::from_dir(&dir).unwrap_or_else(|e| panic!("tagger: {e}"));
let front = Arc::new(TextFrontEnd::new(tagger).unwrap_or_else(|e| panic!("{e}")));
Some(KokoroG2p::new(front).unwrap_or_else(|e| panic!("KokoroG2p::new: {e}")))
}
macro_rules! require_g2p {
() => {
match kokoro_g2p() {
Some(g2p) => g2p,
None => {
eprintln!(
"skipping: spaCy tagger dir unavailable; set SPACY_EN_TAGGER_DIR or \
BOOSTR_MODELS_DIR"
);
return;
}
}
};
}
#[test]
fn chunks_splits_on_blank_lines_and_phonemizes() {
let g2p = require_g2p!();
let chunks = g2p
.chunks("Hello world.\n\nGoodbye.", Dialect::Us)
.unwrap_or_else(|e| panic!("chunks: {e}"));
assert_eq!(chunks.len(), 2);
assert!(!chunks[0].phonemes.is_empty());
assert!(!chunks[1].phonemes.is_empty());
}
#[test]
fn chunks_uses_gb_dialect_lexicon() {
let g2p = require_g2p!();
let us = g2p
.chunks("schedule", Dialect::Us)
.unwrap_or_else(|e| panic!("chunks: {e}"));
let gb = g2p
.chunks("schedule", Dialect::Gb)
.unwrap_or_else(|e| panic!("chunks: {e}"));
assert!(!us[0].phonemes.is_empty());
assert!(!gb[0].phonemes.is_empty());
}
#[test]
fn chunks_errs_on_all_blank_text() {
let g2p = require_g2p!();
assert!(g2p.chunks(" \n \n ", Dialect::Us).is_err());
}
}