use std::sync::Arc;
use super::chars::SUBTOKEN_JUNKS;
use super::front::TextFrontEnd;
use super::lexicon::Lexicon;
use super::resolve::{resolve_tokens, token_context};
use super::retokenize::{Word, fold_left, retokenize};
use super::token::{MToken, TokenContext, merge_tokens};
use crate::error::{Error, Result};
pub const DEFAULT_UNK: &str = "❓";
pub const KOKORO_UNK: &str = "";
pub trait OovFallback {
fn phonemize(&self, token: &MToken) -> (Option<String>, Option<u8>);
}
#[derive(Debug, Clone, PartialEq)]
pub struct G2pOutput {
pub phonemes: String,
pub tokens: Vec<MToken>,
}
pub struct MisakiG2p {
lexicon: Lexicon,
unk: String,
fallback: Option<Box<dyn OovFallback + Send + Sync>>,
front: Option<Arc<TextFrontEnd>>,
}
impl std::fmt::Debug for MisakiG2p {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MisakiG2p")
.field("british", &self.lexicon.british)
.field("unk", &self.unk)
.field("fallback", &self.fallback.is_some())
.field("text_front", &self.front.is_some())
.finish()
}
}
impl MisakiG2p {
pub fn new(british: bool) -> Result<Self> {
Ok(Self {
lexicon: Lexicon::new(british)?,
unk: DEFAULT_UNK.to_string(),
fallback: None,
front: None,
})
}
pub fn with_unk(mut self, unk: impl Into<String>) -> Self {
self.unk = unk.into();
self
}
pub fn with_fallback(mut self, fallback: Box<dyn OovFallback + Send + Sync>) -> Self {
self.fallback = Some(fallback);
self
}
pub fn with_text_front(mut self, front: Arc<TextFrontEnd>) -> Self {
self.front = Some(front);
self
}
pub fn british(&self) -> bool {
self.lexicon.british
}
pub fn unk(&self) -> &str {
&self.unk
}
pub fn phonemize_text(&self, text: &str) -> Result<G2pOutput> {
let front = self.front.as_ref().ok_or_else(|| Error::InvalidArgument {
arg: "text",
reason: "MisakiG2p::phonemize_text needs a text front end; call \
with_text_front first"
.into(),
})?;
let tokens = front.tokens(text)?;
Ok(self.phonemize(tokens))
}
pub fn phonemize(&self, tokens: Vec<MToken>) -> G2pOutput {
let tokens = fold_left(tokens, &self.unk);
let mut words = retokenize(tokens);
let mut ctx = TokenContext::default();
for word in words.iter_mut().rev() {
match word {
Word::Single(w) => {
if w.phonemes.is_none() {
w.phonemes = self.lexicon.resolve(w, &ctx).0;
}
let fallback = self.fallback.as_ref().filter(|_| w.phonemes.is_none());
if let Some(fb) = fallback {
w.phonemes = fb.phonemize(w).0;
}
ctx = token_context(ctx, w.phonemes.as_deref(), w);
}
Word::Group(group) => ctx = self.resolve_group(group, ctx),
}
}
let mut out_tokens: Vec<MToken> = Vec::with_capacity(words.len());
for word in words {
let tk = match word {
Word::Single(t) => Some(t),
Word::Group(g) => merge_tokens(&g, Some(&self.unk)),
};
if let Some(mut tk) = tk {
if let Some(ps) = tk.phonemes.as_mut().filter(|p| !p.is_empty()) {
*ps = ps.replace('ɾ', "T").replace('ʔ', "t");
}
out_tokens.push(tk);
}
}
let mut phonemes = String::new();
for tk in &out_tokens {
phonemes.push_str(tk.phonemes.as_deref().unwrap_or(&self.unk));
phonemes.push_str(&tk.whitespace);
}
G2pOutput {
phonemes,
tokens: out_tokens,
}
}
fn resolve_group(&self, w: &mut [MToken], mut ctx: TokenContext) -> TokenContext {
let (mut left, mut right) = (0usize, w.len());
let mut should_fallback = false;
while left < right {
let span = &w[left..right];
let merged = if span
.iter()
.any(|t| t.alias.is_some() || t.phonemes.is_some())
{
None
} else {
merge_tokens(span, None)
};
let (ps, rating) = match &merged {
Some(tk) => self.lexicon.resolve(tk, &ctx),
None => (None, None),
};
if let (Some(ps), Some(tk)) = (ps, merged.as_ref()) {
ctx = token_context(ctx, Some(&ps), tk);
w[left].phonemes = Some(ps);
w[left].rating = rating;
for x in &mut w[left + 1..right] {
x.phonemes = Some(String::new());
}
right = left;
left = 0;
} else if left + 1 < right {
left += 1;
} else {
right -= 1;
let tk = &mut w[right];
if tk.phonemes.is_none() {
if tk.text.chars().all(|c| SUBTOKEN_JUNKS.contains(c)) {
tk.phonemes = Some(String::new());
tk.rating = Some(3);
} else if self.fallback.is_some() {
should_fallback = true;
break;
}
}
left = 0;
}
}
match (should_fallback, &self.fallback) {
(true, Some(fb)) => {
if let Some(tk) = merge_tokens(w, None) {
let (ps, rating) = fb.phonemize(&tk);
if let Some((head, rest)) = w.split_first_mut() {
head.phonemes = ps;
head.rating = rating;
for x in rest {
x.phonemes = Some(String::new());
x.rating = rating;
}
}
}
}
_ => resolve_tokens(w),
}
ctx
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::g2p::spacy_tagger::EnglishTagger;
use crate::g2p::spacy_tagger::test_support::tagger_dir;
fn with_front(british: bool) -> Option<MisakiG2p> {
let dir = tagger_dir()?;
let tagger = EnglishTagger::from_dir(&dir).unwrap_or_else(|e| panic!("tagger: {e}"));
let front =
std::sync::Arc::new(TextFrontEnd::new(tagger).unwrap_or_else(|e| panic!("{e}")));
Some(
MisakiG2p::new(british)
.unwrap_or_else(|e| panic!("MisakiG2p: {e}"))
.with_text_front(front),
)
}
#[test]
fn phonemize_text_applies_link_features() {
let Some(g2p) = with_front(false) else {
eprintln!(
"skipping: spaCy tagger dir unavailable; set SPACY_EN_TAGGER_DIR or \
BOOSTR_MODELS_DIR"
);
return;
};
let cases = [
(" [Kokoro](/kˈOkəɹO/) is great.", "kˈOkəɹO ɪz ɡɹˈAt."),
(
"[Hello world](/həlˈO/) and [read](-1) [it](+2) [a](0.5) [b](-0.5) [c](#&#) [d](x)",
"həlˈO ænd ɹˌɛd ˈɪt ɐ bˌi sˈi dˈi",
),
("Say [don't](/dˈOnt/) now", "sˈA dˈOnt nˈW"),
("[12](#a#)", "twˈɛlv"),
("x [a]() y", "ˈɛks ɐ wˈI"),
("[New York](/nuː jɔːk/) rocks", "nuː jɔːk ɹˈɑks"),
(
"Visit [U.S.](/juː ɛs/) today [5](//ab//)",
"vˈɪzət juː ɛs tədˈA ab",
),
];
for (text, want) in cases {
let got = g2p
.phonemize_text(text)
.unwrap_or_else(|e| panic!("phonemize_text({text:?}): {e}"));
assert_eq!(got.phonemes, want, "{text:?}");
}
}
#[test]
fn phonemize_text_of_empty_text() {
let Some(g2p) = with_front(true) else {
eprintln!(
"skipping: spaCy tagger dir unavailable; set SPACY_EN_TAGGER_DIR or \
BOOSTR_MODELS_DIR"
);
return;
};
for text in ["", " "] {
let got = g2p
.phonemize_text(text)
.unwrap_or_else(|e| panic!("phonemize_text({text:?}): {e}"));
assert_eq!(got.phonemes, "");
assert!(got.tokens.is_empty());
}
}
}