use std::path::Path;
use std::sync::Arc;
use boostr::format::SafeTensors;
use boostr::model::{LoadedModel, UniversalConfig};
use boostr::{VarBuilder, VarMap};
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
use super::checkpoint;
use super::config::parse_checkpoint_config;
use super::normalize::fold_to_model_chars;
use super::vocab::G2pVocab;
use crate::error::{Error, Result};
use crate::g2p::misaki::{MToken, OovFallback};
const MAX_NEW_TOKENS: usize = 64;
struct Shared {
client: CpuClient,
model: LoadedModel<CpuRuntime>,
vocab: G2pVocab,
}
pub struct NeuralG2pFallback {
shared: Arc<Shared>,
british: bool,
}
impl NeuralG2pFallback {
pub fn from_dir(dir: impl AsRef<Path>, british: bool) -> Result<Self> {
let dir = dir.as_ref();
let config_text =
std::fs::read_to_string(dir.join("config.json")).map_err(|e| Error::ModelError {
reason: format!("reading {}: {e}", dir.join("config.json").display()),
})?;
let vocab_text =
std::fs::read_to_string(dir.join("vocab.json")).map_err(|e| Error::ModelError {
reason: format!("reading {}: {e}", dir.join("vocab.json").display()),
})?;
let checkpoint_path = checkpoint::locate(dir)?;
let checkpoint_bytes = std::fs::read(&checkpoint_path).map_err(|e| Error::ModelError {
reason: format!("reading {}: {e}", checkpoint_path.display()),
})?;
let universal: UniversalConfig = parse_checkpoint_config(&config_text)?;
let vocab = G2pVocab::from_json(&vocab_text)?;
if universal.vocab_size as u32 != vocab.vocab_size {
return Err(Error::ModelError {
reason: format!(
"g2p_lm config.json vocab_size {} does not match vocab.json vocab_size {}",
universal.vocab_size, vocab.vocab_size
),
});
}
let device = CpuDevice::default();
let client = CpuClient::new(device.clone());
let mut varmap = varmap_from_bytes(checkpoint_bytes, &device)?;
let mut vb = VarBuilder::new(&mut varmap, &device);
let model = LoadedModel::<CpuRuntime>::load(&universal, &mut vb)?;
Ok(Self {
shared: Arc::new(Shared {
client,
model,
vocab,
}),
british,
})
}
pub fn with_british(&self, british: bool) -> Self {
Self {
shared: self.shared.clone(),
british,
}
}
pub fn predict(&self, dialect: &str, word: &str) -> Result<Option<String>> {
let vocab = &self.shared.vocab;
let graphemes = match vocab.encode_word(word) {
Some(g) if !g.is_empty() => g,
_ => return Ok(None),
};
let mut prompt = Vec::with_capacity(graphemes.len() + 3);
prompt.push(vocab.bos()?);
prompt.push(vocab.dialect(dialect)?);
prompt.extend(graphemes);
prompt.push(vocab.sep()?);
let opts = boostr::inference::GenerateOptions {
max_new_tokens: MAX_NEW_TOKENS,
stop_tokens: vec![vocab.eos],
};
let generated = boostr::inference::generate_greedy(
&self.shared.client,
&self.shared.model,
&prompt,
&opts,
)?;
let mut phonemes = String::new();
for id in generated {
if id == vocab.eos {
break;
}
if id < vocab.target_base {
return Ok(None);
}
match vocab.phoneme_symbol(id) {
Some(sym) => phonemes.push(sym),
None => return Ok(None),
}
}
if phonemes.is_empty() {
Ok(None)
} else {
Ok(Some(phonemes))
}
}
}
fn varmap_from_bytes(bytes: Vec<u8>, device: &CpuDevice) -> Result<VarMap<CpuRuntime>> {
let mut st = SafeTensors::from_bytes(bytes)?;
let mut map = VarMap::new();
for (name, tensor) in st.load_all::<CpuRuntime>(device)? {
map.insert(name, tensor);
}
Ok(map)
}
impl OovFallback for NeuralG2pFallback {
fn phonemize(&self, token: &MToken) -> (Option<String>, Option<u8>) {
let Some(folded) = fold_to_model_chars(&token.text) else {
return (None, None);
};
let dialect = if self.british { "gb" } else { "us" };
match self.predict(dialect, &folded) {
Ok(Some(ps)) => (Some(ps), Some(2)),
Ok(None) | Err(_) => (None, None),
}
}
}