use std::path::Path;
use std::sync::Arc;
use boostr::model::audio::vits::{VitsModel, VitsModelClient, VitsSynthOptions};
use boostr::model::config::VitsConfig;
use boostr::nn::{VarBuilder, VarMap};
use numr::dtype::DType;
use numr::runtime::Runtime;
use numr::runtime::cpu::{CpuDevice, CpuRuntime};
use numr::tensor::Tensor;
use crate::error::{Error, Result};
use crate::lang::Lang;
use crate::tts::bundle::{TtsBundle, Voice};
use crate::tts::vits::tokenizer::VitsTokenizer;
pub struct VitsEngine<R: Runtime<DType = DType> = CpuRuntime> {
model: VitsModel<R>,
tokenizer: VitsTokenizer,
client: Arc<R::Client>,
device: Arc<R::Device>,
voice: Voice,
seed: Option<u64>,
}
impl VitsEngine<CpuRuntime> {
pub fn from_dir(dir: &Path) -> Result<Self> {
Self::from_dir_on(dir, &CpuDevice::new())
}
}
impl<R: Runtime<DType = DType>> VitsEngine<R>
where
R::Client: VitsModelClient<R>,
{
pub fn from_dir_on(dir: &Path, device: &R::Device) -> Result<Self> {
let config_path = dir.join("config.json");
let config = VitsConfig::from_json_file(&config_path)?;
config.validate()?;
let tokenizer = VitsTokenizer::from_dir(dir)?;
let device = Arc::new(device.clone());
let client = Arc::new(R::default_client(&device));
let weights_path = dir.join("model.safetensors");
let mut weights = VarMap::<R>::from_safetensors(&weights_path, &device)?;
let mut vb = VarBuilder::new(&mut weights, &device);
let model = VitsModel::<R>::from_varbuilder(client.as_ref(), &mut vb, &config)?;
let language_code = read_language_code(dir)?;
let lang = Lang::parse(&language_code).map_err(|e| Error::ModelError {
reason: format!("{}/tokenizer_config.json: {e}", dir.display()),
})?;
let voice = Voice::new(language_code.clone(), lang, language_code);
Ok(Self {
model,
tokenizer,
client,
device,
voice,
seed: None,
})
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn synthesize(&self, text: &str, voice: &str, speed: f32) -> Result<Vec<f32>> {
self.synthesize_seeded(text, voice, speed, self.seed)
}
pub fn synthesize_seeded(
&self,
text: &str,
voice: &str,
speed: f32,
seed: Option<u64>,
) -> Result<Vec<f32>> {
if voice != self.voice.id {
return Err(Error::InvalidArgument {
arg: "voice",
reason: format!(
"unknown voice {voice:?}; this checkpoint serves only {:?}",
self.voice.id
),
});
}
if !(speed.is_finite() && speed > 0.0) {
return Err(Error::InvalidArgument {
arg: "speed",
reason: format!("expected a finite positive rate, got {speed}"),
});
}
if text.is_empty() {
return Err(Error::InvalidArgument {
arg: "text",
reason: "input text must not be empty".into(),
});
}
let ids = self.tokenizer.encode(text)?;
if ids.is_empty() {
return Err(Error::DataError {
reason: "tokenizer produced no input ids for this text (empty or fully \
out-of-vocabulary)"
.into(),
});
}
let seq_len = ids.len();
let input_ids: Vec<i64> = ids.iter().map(|&id| id as i64).collect();
let input_ids = Tensor::<R>::from_slice(&input_ids, &[1, seq_len], &self.device)?;
let attention_mask =
Tensor::<R>::from_slice(&vec![1.0f32; seq_len], &[1, seq_len], &self.device)?;
let mut opts: VitsSynthOptions = self.model.default_synth_options();
opts.speaking_rate = speed as f64;
if let Some(seed) = seed {
opts.seed = seed;
}
let out =
self.model
.synthesize(self.client.as_ref(), &input_ids, &attention_mask, &opts)?;
Ok(out.waveform.contiguous()?.to_vec())
}
pub fn sample_rate(&self) -> u32 {
self.model.config().sampling_rate as u32
}
pub fn tokenizer(&self) -> &VitsTokenizer {
&self.tokenizer
}
}
impl<R: Runtime<DType = DType>> crate::tts::TtsEngine for VitsEngine<R>
where
R::Client: VitsModelClient<R>,
VitsModel<R>: Send + Sync,
{
fn synthesize(&self, text: &str, voice: &str, speed: f32) -> Result<Vec<f32>> {
VitsEngine::synthesize(self, text, voice, speed)
}
fn sample_rate(&self) -> u32 {
VitsEngine::sample_rate(self)
}
fn voices(&self) -> Vec<Voice> {
vec![self.voice.clone()]
}
}
fn read_language_code(dir: &Path) -> Result<String> {
let path = dir.join("tokenizer_config.json");
let bytes = std::fs::read(&path)?;
let value: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|e| Error::ModelError {
reason: format!("parsing {}: {e}", path.display()),
})?;
Ok(value
.get("language")
.and_then(|v| v.as_str())
.map(str::to_string)
.unwrap_or_else(|| "und".to_string()))
}
pub fn is_vits_config(config_path: &Path) -> Result<bool> {
let bytes = std::fs::read(config_path)?;
let value: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|e| Error::ModelError {
reason: format!("parsing {}: {e}", config_path.display()),
})?;
Ok(value.get("model_type").and_then(|v| v.as_str()) == Some("vits"))
}
pub fn load_vits_from_dir(dir: &Path) -> Result<TtsBundle> {
load_vits_from_dir_on::<CpuRuntime>(dir, &CpuDevice::new())
}
pub fn load_vits_from_dir_on<R: Runtime<DType = DType>>(
dir: &Path,
device: &R::Device,
) -> Result<TtsBundle>
where
R::Client: VitsModelClient<R>,
VitsModel<R>: Send + Sync,
{
let engine = VitsEngine::<R>::from_dir_on(dir, device)?;
let sample_rate = engine.sample_rate();
let voices = crate::tts::TtsEngine::voices(&engine);
Ok(TtsBundle::scaffolding(voices, sample_rate).with_engine(Arc::new(engine)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_vits_config_reads_the_model_type_field() {
let tmp = std::env::temp_dir().join("voxr_vits_engine_config_check");
std::fs::create_dir_all(&tmp).unwrap();
let path = tmp.join("config.json");
std::fs::write(&path, br#"{"model_type": "vits"}"#).unwrap();
assert!(is_vits_config(&path).unwrap());
std::fs::write(&path, br#"{"model_type": "kokoro"}"#).unwrap();
assert!(!is_vits_config(&path).unwrap());
let _ = std::fs::remove_dir_all(&tmp);
}
}