use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::text_front::KokoroG2p;
use crate::error::{Error, Result};
use crate::g2p::misaki::TextFrontEnd;
use crate::g2p::neural::NeuralG2pFallback;
use crate::g2p::spacy_tagger::EnglishTagger;
#[derive(Debug, Clone)]
pub struct KokoroG2pModels {
pub tagger_dir: PathBuf,
pub oov_dir: Option<PathBuf>,
}
impl KokoroG2pModels {
pub fn resolve(
kokoro_dir: impl AsRef<Path>,
tagger_dir: Option<PathBuf>,
oov_dir: Option<PathBuf>,
) -> Result<Self> {
let kokoro_dir = kokoro_dir.as_ref();
let tagger_dir = tagger_dir.unwrap_or_else(|| kokoro_dir.join("g2p/tagger"));
if !tagger_dir.join("tagger.safetensors").is_file()
|| !tagger_dir.join("tagger.json").is_file()
{
return Err(Error::ModelError {
reason: format!(
"no spaCy tagger at {} (expected tagger.safetensors + tagger.json); \
download `tagger/` from the Hugging Face repo fs90/kokoro-g2p into it, \
or run from a voxr checkout:\n\
\n python3 crates/voxr/assets/spacy_en/convert_tagger.py \\\n \
<en_core_web_sm model dir> {}\n",
tagger_dir.display(),
tagger_dir.display(),
),
});
}
let oov_dir = oov_dir.or_else(|| {
let dir = kokoro_dir.join("g2p/oov");
dir.is_dir().then_some(dir)
});
Ok(Self {
tagger_dir,
oov_dir,
})
}
pub fn load(&self) -> Result<KokoroG2p> {
let tagger = EnglishTagger::from_dir(&self.tagger_dir)?;
let front = Arc::new(TextFrontEnd::new(tagger)?);
let g2p = KokoroG2p::new(front)?;
let Some(oov_dir) = &self.oov_dir else {
return Ok(g2p);
};
let us_fallback = NeuralG2pFallback::from_dir(oov_dir, false)?;
let gb_fallback = us_fallback.with_british(true);
Ok(g2p.with_fallback(Box::new(us_fallback), Box::new(gb_fallback)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_errs_naming_the_missing_tagger_and_the_convert_command() {
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
let err = KokoroG2pModels::resolve(dir.path(), None, None).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("tagger.safetensors"), "{msg}");
assert!(msg.contains("convert_tagger.py"), "{msg}");
}
#[test]
fn resolve_defaults_oov_dir_to_none_when_absent() {
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
std::fs::create_dir_all(dir.path().join("g2p/tagger")).unwrap_or_else(|e| panic!("{e}"));
std::fs::write(dir.path().join("g2p/tagger/tagger.safetensors"), b"")
.unwrap_or_else(|e| panic!("{e}"));
std::fs::write(dir.path().join("g2p/tagger/tagger.json"), b"{}")
.unwrap_or_else(|e| panic!("{e}"));
let models = KokoroG2pModels::resolve(dir.path(), None, None)
.unwrap_or_else(|e| panic!("resolve: {e}"));
assert!(models.oov_dir.is_none());
}
#[test]
fn resolve_honors_explicit_overrides() {
let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
let tagger_dir = dir.path().join("elsewhere/tagger");
let oov_dir = dir.path().join("elsewhere/oov");
std::fs::create_dir_all(&tagger_dir).unwrap_or_else(|e| panic!("{e}"));
std::fs::create_dir_all(&oov_dir).unwrap_or_else(|e| panic!("{e}"));
std::fs::write(tagger_dir.join("tagger.safetensors"), b"")
.unwrap_or_else(|e| panic!("{e}"));
std::fs::write(tagger_dir.join("tagger.json"), b"{}").unwrap_or_else(|e| panic!("{e}"));
let models =
KokoroG2pModels::resolve(dir.path(), Some(tagger_dir.clone()), Some(oov_dir.clone()))
.unwrap_or_else(|e| panic!("resolve: {e}"));
assert_eq!(models.tagger_dir, tagger_dir);
assert_eq!(models.oov_dir, Some(oov_dir));
}
}