use std::sync::Arc;
use std::sync::mpsc::{Receiver, channel};
use numr::dtype::DType;
use numr::runtime::Runtime;
use numr::tensor::Tensor;
use boostr::model::audio::voxcpm::model::{PadSide, VoxCpm2Model};
use crate::error::{Error, Result};
use crate::resample::resample_like_librosa;
use crate::voxcpm::engine::batch::{EncodeJob, Work};
use crate::voxcpm::engine::render::worker_gone;
use crate::voxcpm::engine::request::{Prompt, Reference};
use crate::voxcpm::engine::types::{EngineInner, REF_RATE, VoxCpm2Engine, VoxCpm2RenderClient};
impl<R> VoxCpm2Engine<R>
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
pub fn encode_reference(&self, samples: &[f32], sample_rate: u32) -> Result<Reference<R>> {
let samples = to_encoder_rate(samples, sample_rate, "reference")?;
let feat = wait_encoded(self.queue_encode(samples, PadSide::Right)?)?;
Ok(Reference {
feat: Arc::new(feat),
engine: self.inner.id,
})
}
pub fn encode_prompt(&self, samples: &[f32], sample_rate: u32) -> Result<Prompt<R>> {
let samples = to_encoder_rate(samples, sample_rate, "prompt")?;
let feat = wait_encoded(self.queue_encode(samples, PadSide::Left)?)?;
Ok(Prompt {
feat: Arc::new(feat),
engine: self.inner.id,
})
}
pub fn encode_reference_and_prompt(
&self,
samples: &[f32],
sample_rate: u32,
) -> Result<(Reference<R>, Prompt<R>)> {
let samples = to_encoder_rate(samples, sample_rate, "Hi-Fi")?;
let reference = self.queue_encode(samples.clone(), PadSide::Right)?;
let prompt = self.queue_encode(samples, PadSide::Left)?;
let reference = Reference {
feat: Arc::new(wait_encoded(reference)?),
engine: self.inner.id,
};
let prompt = Prompt {
feat: Arc::new(wait_encoded(prompt)?),
engine: self.inner.id,
};
Ok((reference, prompt))
}
fn queue_encode(
&self,
samples: Vec<f32>,
side: PadSide,
) -> Result<Receiver<Result<Tensor<R>>>> {
let (reply, encoded) = channel();
self.submit(Work::Encode(EncodeJob {
samples,
side,
reply,
}))?;
Ok(encoded)
}
}
impl<R> EngineInner<R>
where
R: Runtime<DType = DType> + 'static,
R::Client: VoxCpm2RenderClient<R>,
VoxCpm2Model<R>: Send + Sync,
Tensor<R>: Send + Sync,
{
pub(super) fn serve_encode(&self, job: EncodeJob<R>) {
let client = self.client.as_ref();
let encoded = match job.side {
PadSide::Right => self.model.encode_reference(client, &job.samples),
PadSide::Left => self.model.encode_prompt(client, &job.samples),
};
let _ = job.reply.send(encoded.map_err(Error::from));
}
}
fn to_encoder_rate(samples: &[f32], sample_rate: u32, what: &str) -> Result<Vec<f32>> {
if samples.is_empty() {
return Err(Error::InvalidArgument {
arg: "samples",
reason: format!(
"{what} recording at {sample_rate} Hz has 0 samples; pass mono PCM holding \
the {what} clip"
),
});
}
resample_like_librosa(samples, sample_rate, REF_RATE)
}
fn wait_encoded<R: Runtime>(encoded: Receiver<Result<Tensor<R>>>) -> Result<Tensor<R>> {
encoded.recv().map_err(|_| worker_gone())?
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_clip_is_refused_with_its_name_and_rate() {
let err = to_encoder_rate(&[], 44_100, "prompt").unwrap_err();
let text = err.to_string();
assert!(text.contains("prompt"), "{text}");
assert!(text.contains("44100"), "{text}");
}
#[test]
fn a_clip_at_the_encoder_rate_keeps_its_length() {
let clip = vec![0.25f32; 1600];
let out = to_encoder_rate(&clip, REF_RATE, "reference").expect("resample");
assert_eq!(out.len(), clip.len());
}
}