use std::io::Write;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use anyhow::{anyhow, Context, Result};
use tokenizers::Tokenizer;
use crate::codec::{self, Codec};
use crate::engine::{Engine, SampleParams};
use crate::model::{ExpertMode, Model};
use crate::prompt::{self, Reference};
pub struct Tts {
pub engine: Engine,
pub codec: Codec,
tok: Tokenizer,
}
pub struct SpeakOpts {
pub params: SampleParams,
pub max_new: usize,
pub first_chunk: usize,
pub chunk: usize,
pub split: bool,
}
pub struct Spoken {
pub audio: Vec<f32>,
pub frames: Vec<Vec<i32>>,
pub prompt_rows: usize,
pub prompt: Vec<i32>,
pub segments: usize,
pub first_audio: Option<Duration>,
pub elapsed: Duration,
}
pub struct Player {
child: Child,
}
impl Player {
pub fn new() -> Result<Self> {
#[cfg(target_os = "macos")]
let child = Command::new("ffplay")
.args(["-nodisp", "-autoexit", "-loglevel", "error", "-f", "f32le", "-ar", &codec::SAMPLE_RATE.to_string(), "-ac", "1", "-i", "pipe:0"])
.stdin(Stdio::piped()).spawn().context("starting ffplay for --play (install ffmpeg)")?;
#[cfg(not(target_os = "macos"))]
let child = Command::new("pw-play")
.args(["--raw", "--rate", &codec::SAMPLE_RATE.to_string(), "--channels", "1", "--format", "f32", "-"])
.stdin(Stdio::piped())
.spawn()
.context("starting pw-play (PipeWire) for --play")?;
Ok(Self { child })
}
pub fn write(&mut self, audio: &[f32]) -> Result<()> {
let bytes = unsafe { std::slice::from_raw_parts(audio.as_ptr() as *const u8, std::mem::size_of_val(audio)) };
self.child.stdin.as_mut().unwrap().write_all(bytes)?;
Ok(())
}
pub fn finish(mut self) -> Result<()> {
drop(self.child.stdin.take());
self.child.wait()?;
Ok(())
}
}
impl Tts {
pub fn load(model_dir: &Path, max_len: usize, chunk: usize) -> Result<Self> {
Self::load_with(model_dir, max_len, chunk, &ExpertMode::Q4)
}
pub fn load_with(model_dir: &Path, max_len: usize, chunk: usize, experts: &ExpertMode) -> Result<Self> {
let codec = Codec::load(&model_dir.join("codec.safetensors"))?;
let st = crate::gpu::Stream::new()?;
let model = Model::load_with(model_dir, &st, experts)?;
let engine = Engine::new(model, max_len, chunk)?;
let tok = Tokenizer::from_file(model_dir.join("tokenizer.json")).map_err(|e| anyhow!("tokenizer: {e}"))?;
Ok(Self { engine, codec, tok })
}
pub fn reference(&self, wav: &Path, transcript: &str) -> Result<Reference> {
let (audio, sr) = codec::read_wav(wav)?;
self.reference_audio(&audio, sr, transcript)
}
pub fn reference_audio(&self, audio: &[f32], sr: u32, transcript: &str) -> Result<Reference> {
let audio = codec::resample(audio, sr, codec::SAMPLE_RATE);
let flat = self.codec.encode(&audio)?;
let frames = flat.len() / 10;
let codes = (0..10).map(|c| (0..frames).map(|t| flat[t * 10 + c]).collect()).collect();
Ok(Reference { transcript: transcript.to_string(), codes })
}
pub fn speak(&mut self, text: &str, refs: &[Reference], opts: &SpeakOpts, mut sink: impl FnMut(&[f32]) -> Result<()> + Send) -> Result<Spoken> {
let dc = &self.engine.model.cfg.audio_decoder_config;
let (ncb, cb) = (dc.num_codebooks, dc.vocab_size);
let width = ncb + 1;
let segments = if opts.split { prompt::split_sentences(text) } else { vec![text.to_string()] };
prompt::build_prompt_rows(text, refs, &self.tok, ncb, cb)?;
let covered: Vec<u32> = refs.iter().map(|r| prompt::leading_speaker(&r.transcript).unwrap_or(0)).collect();
let t0 = Instant::now();
let (tx, rx) = mpsc::channel::<Vec<i32>>();
let codec = &self.codec;
let engine = &mut self.engine;
let tok = &self.tok;
let (first, chunk) = (opts.first_chunk.max(1), opts.chunk.max(1));
let (gen, decoded) = std::thread::scope(|scope| -> Result<_> {
let worker = scope.spawn(move || -> Result<(Vec<f32>, Option<Duration>)> {
let mut ds = codec.stream_for(first.max(chunk))?;
let mut pending: Vec<i32> = Vec::new();
let mut audio = Vec::new();
let mut first_audio = None;
let mut want = first;
let mut flush = |pending: &mut Vec<i32>, audio: &mut Vec<f32>, first_audio: &mut Option<Duration>| -> Result<()> {
if pending.is_empty() {
return Ok(());
}
let a = codec.decode_chunk(&mut ds, pending)?;
pending.clear();
first_audio.get_or_insert_with(|| t0.elapsed());
sink(&a)?;
audio.extend_from_slice(&a);
Ok(())
};
while let Ok(codes) = rx.recv() {
pending.extend_from_slice(&codes);
while let Ok(more) = rx.try_recv() {
pending.extend_from_slice(&more);
}
if pending.len() / 10 >= want {
flush(&mut pending, &mut audio, &mut first_audio)?;
want = chunk;
}
}
flush(&mut pending, &mut audio, &mut first_audio)?;
Ok((audio, first_audio))
});
let gen = (|| -> Result<_> {
let mut frames = Vec::new();
let mut prompt_rows = 0;
let mut first_prompt = Vec::new();
let mut voices: Vec<Reference> = refs.to_vec();
let mut voiced: Vec<u32> = covered;
for (i, seg) in segments.iter().enumerate() {
let rows = prompt::build_prompt_rows(seg, &voices, tok, ncb, cb)?;
prompt_rows += rows.len() / width;
if i == 0 {
first_prompt = rows.clone();
}
let cap = if opts.split { frame_cap(seg) } else { usize::MAX };
let mut seg_frames = Vec::new();
let mut stalled = true;
for attempt in 0..=STALL_RETRIES {
let mut params = opts.params;
params.seed = params.seed.wrapping_add(i as u64).wrapping_add(attempt as u64 * 1000);
seg_frames = engine.generate(&rows, params, opts.max_new.min(cap), |frame| {
let _ = tx.send(frame[1..].to_vec());
})?;
stalled = seg_frames.len() >= cap;
if !stalled {
break;
}
frames.extend(seg_frames.drain(..));
eprintln!("sentence {} ran into its {cap}-frame cap without ending (attempt {})", i + 1, attempt + 1);
}
if let Some(sp) = prompt::sole_speaker(seg) {
if opts.split && !stalled && !seg_frames.is_empty() && !voiced.contains(&sp) {
let codes = (0..ncb).map(|c| seg_frames.iter().map(|f| f[1 + c]).collect()).collect();
voices.push(Reference { transcript: seg.clone(), codes });
voiced.push(sp);
}
}
frames.extend(seg_frames);
}
Ok((frames, prompt_rows, first_prompt, segments.len()))
})();
drop(tx);
let decoded = worker.join().map_err(|_| anyhow!("codec thread panicked"))?;
Ok((gen?, decoded?))
})?;
let ((frames, prompt_rows, prompt, segments), (audio, first_audio)) = (gen, decoded);
Ok(Spoken { audio, frames, prompt_rows, prompt, segments, first_audio, elapsed: t0.elapsed() })
}
}
const STALL_RETRIES: usize = 2;
fn frame_cap(sentence: &str) -> usize {
let mut text = sentence.to_string();
while let Some(i) = text.find("<|speaker:") {
let end = text[i..].find("|>").map_or(text.len(), |e| i + e + 2);
text.replace_range(i..end, "");
}
let text = text.trim();
30 + text.chars().map(|c| if c as u32 >= 0x2E80 { 8 } else { 5 }).sum::<usize>() / 2
}
#[cfg(test)]
mod tests {
#[test]
fn frame_cap_ignores_speaker_tags() {
assert_eq!(super::frame_cap("<|speaker:1|> Hi."), 30 + 3 * 5 / 2);
assert_eq!(super::frame_cap("你好"), 30 + 8);
}
}