use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::channel;
use numr::dtype::DType;
use numr::runtime::Runtime;
use numr::tensor::Tensor;
use boostr::model::audio::voxcpm::model::VoxCpm2Model;
use boostr::model::audio::voxcpm::vae::decoder::SAMPLE_RATE;
use crate::error::{Error, Result};
use crate::lang::Lang;
use crate::tts::{TtsEngine, Voice};
use crate::voxcpm::engine::batch::Work;
use crate::voxcpm::engine::reply::Reply;
use crate::voxcpm::engine::request::{Rendered, SynthRequest, VoiceSource};
use crate::voxcpm::engine::types::{VoxCpm2Engine, VoxCpm2RenderClient, ZERO_SHOT_VOICE_ID};
impl<R> VoxCpm2Engine<R>
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
pub(super) fn submit(&self, work: Work<R>) -> Result<()> {
let Some(jobs) = self.jobs.as_ref() else {
return Err(worker_gone());
};
jobs.send(work).map_err(|_| worker_gone())
}
pub fn render_request(&self, request: &SynthRequest<'_, R>) -> Result<Vec<f32>> {
Ok(self.render_request_with_attempts(request)?.samples)
}
pub fn render_request_with_attempts(&self, request: &SynthRequest<'_, R>) -> Result<Rendered> {
let (done, reply) = channel();
self.submit(Work::Render(self.plan(request, Reply::Buffered(done))?))?;
reply.recv().map_err(|_| worker_gone())?
}
pub fn render_request_stream(
&self,
request: &SynthRequest<'_, R>,
sink: &mut dyn FnMut(&[f32]) -> Result<()>,
) -> Result<()> {
if self.inner.options.stream_chunk_patches == 0 {
return Err(Error::InvalidArgument {
arg: "stream_chunk_patches",
reason: "expected at least 1 patch per chunk, got 0".into(),
});
}
let (chunks, reply) = channel();
let cancelled = Arc::new(AtomicBool::new(false));
let job = self.plan(
request,
Reply::Streaming {
chunks,
cancelled: Arc::clone(&cancelled),
},
)?;
self.submit(Work::Render(job))?;
let mut delivered = false;
while let Ok(message) = reply.recv() {
let samples = message?;
if let Err(err) = sink(&samples) {
cancelled.store(true, Ordering::Relaxed);
return Err(err);
}
delivered = true;
}
if delivered {
Ok(())
} else {
Err(worker_gone())
}
}
fn render(&self, text: &str, voice_id: &str) -> Result<Vec<f32>> {
self.render_request(&SynthRequest::new(text, VoiceSource::Named(voice_id)))
}
fn render_stream(
&self,
text: &str,
voice_id: &str,
sink: &mut dyn FnMut(&[f32]) -> Result<()>,
) -> Result<()> {
let request = SynthRequest::new(text, VoiceSource::Named(voice_id));
self.render_request_stream(&request, sink)
}
}
impl<R> TtsEngine for VoxCpm2Engine<R>
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
fn synthesize(&self, text: &str, voice: &str, speed: f32) -> Result<Vec<f32>> {
check_speed(speed)?;
self.render(text, voice)
}
fn synthesize_stream(
&self,
text: &str,
voice: &str,
speed: f32,
sink: &mut dyn FnMut(&[f32]) -> Result<()>,
) -> Result<()> {
check_speed(speed)?;
self.render_stream(text, voice, sink)
}
fn sample_rate(&self) -> u32 {
SAMPLE_RATE as u32
}
fn voices(&self) -> Vec<Voice> {
std::iter::once(Voice::new(ZERO_SHOT_VOICE_ID, Lang::Ms, ZERO_SHOT_VOICE_ID))
.chain(
self.inner
.voices
.keys()
.map(|id| Voice::new(id.clone(), Lang::Ms, id.clone())),
)
.collect()
}
}
pub(super) fn worker_gone() -> Error {
Error::ModelError {
reason: "VoxCPM2 render thread is not running; an earlier render panicked or the \
engine is shutting down"
.into(),
}
}
fn check_speed(speed: f32) -> Result<()> {
if speed != 1.0 {
return Err(Error::InvalidArgument {
arg: "speed",
reason: format!("VoxCPM2 renders at its natural pace only; got {speed}"),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn speed_other_than_natural_is_refused() {
assert!(check_speed(1.0).is_ok());
let err = check_speed(1.5).unwrap_err();
assert!(err.to_string().contains("1.5"), "{err}");
}
#[test]
fn a_dead_worker_is_an_error_not_a_hang() {
let err = worker_gone();
assert!(err.to_string().contains("not running"), "{err}");
}
}