Skip to main content

qts/
lib.rs

1//! Qwen3 TTS (GGUF + GGML) — native inference library.
2//!
3//! Output sample rate for the published Qwen3-TTS checkpoints.
4pub const SAMPLE_RATE_HZ: u32 = 24_000;
5
6mod error;
7mod mode;
8mod model;
9pub mod pipeline;
10mod synthesis_profile;
11mod voice_clone_prompt;
12
13pub use error::Qwen3TtsError;
14pub use mode::{ModeSynthesizeRequest, PresetSpeaker, QtsMode};
15pub use model::{load_and_validate, GgufFile, ModeModelPaths, ModelPaths};
16pub use pipeline::backend::BackendKind;
17pub use pipeline::speaker_encoder::{SpeakerEncoder, SpeakerEncoderConfig};
18pub use pipeline::tokenizer::{TextTokenizer, TokenizerConfig};
19pub use pipeline::tts_transformer::{
20    CodePredictorConfig, CodecRollout, CodecRolloutSubTimings, IclPrefillConditioning,
21    PrefillConditioning, PrefillForwardOutputs, PreparedPrefillInputs, SelectedCodecFrame,
22    TtsTransformer, TtsTransformerConfig, VocoderChunk,
23};
24pub use pipeline::vocoder::{
25    Vocoder, VocoderConfig, VocoderExecutionProvider, VocoderGraphTemplate,
26};
27pub use synthesis_profile::SynthesisStageTimings;
28pub use voice_clone_prompt::{
29    TensorF32, TensorI32, VoiceClonePromptV2, VOICE_CLONE_PROMPT_V2_SCHEMA_VERSION,
30};
31
32use std::time::Instant;
33
34/// User-facing synthesis parameters (stable for future `gdext` bindings).
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum TalkerKvMode {
37    #[default]
38    F16,
39    TurboQuant,
40}
41
42impl TalkerKvMode {
43    pub fn as_str(self) -> &'static str {
44        match self {
45            Self::F16 => "f16",
46            Self::TurboQuant => "turboquant",
47        }
48    }
49
50    pub fn parse(value: &str) -> Result<Self, Qwen3TtsError> {
51        match value.trim().to_ascii_lowercase().as_str() {
52            "f16" => Ok(Self::F16),
53            "turboquant" | "turbo" | "q8_0" | "q8" => Ok(Self::TurboQuant),
54            other => Err(Qwen3TtsError::InvalidInput(format!(
55                "unknown talker KV mode '{other}' (expected f16 or turboquant)"
56            ))),
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct SynthesizeRequest {
63    pub text: String,
64    pub temperature: f32,
65    pub top_p: f32,
66    pub top_k: i32,
67    pub max_audio_frames: usize,
68    pub thread_count: usize,
69    pub repetition_penalty: f32,
70    /// Codec language id (e.g. 2050=en, 2055=zh, 2058=ja).
71    pub language_id: i32,
72    /// Number of CPU threads for vocoder decode when pipelining is enabled.
73    /// Defaults to 4. Set to 0 to derive a backend-agnostic fallback from `thread_count`.
74    pub vocoder_thread_count: usize,
75    /// When > 0, pipeline transformer (GPU) and vocoder (CPU) by processing
76    /// vocoder chunks of this many frames in a background thread while the
77    /// transformer continues generating. Set to 0 to disable (sequential).
78    pub vocoder_chunk_size: usize,
79    /// Experimental talker KV cache storage mode.
80    pub talker_kv_mode: TalkerKvMode,
81}
82
83impl Default for SynthesizeRequest {
84    fn default() -> Self {
85        Self {
86            text: String::new(),
87            temperature: 0.9,
88            top_p: 1.0,
89            top_k: 50,
90            max_audio_frames: 4096,
91            thread_count: 4,
92            repetition_penalty: 1.05,
93            language_id: 2050,
94            vocoder_thread_count: 4,
95            vocoder_chunk_size: 0,
96            talker_kv_mode: TalkerKvMode::F16,
97        }
98    }
99}
100
101#[derive(Debug, Clone)]
102pub struct SynthesizeResult {
103    pub pcm_f32: Vec<f32>,
104    pub sample_rate_hz: u32,
105    /// Best-effort: frame count not yet exposed by C API; 0 for now.
106    pub generated_frames: usize,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct StreamingSynthesizeResult {
111    pub sample_rate_hz: u32,
112    pub generated_frames: usize,
113    pub generated_samples: usize,
114}
115
116struct PreparedSynthesis {
117    prepared_inputs: PreparedPrefillInputs,
118    prompt_frames: Vec<Vec<i32>>,
119    prefix_frame_count: usize,
120    speaker_encode: std::time::Duration,
121    tokenize: std::time::Duration,
122    prefill_build: std::time::Duration,
123}
124
125enum SpeakerEmbeddingStorage<'a> {
126    Borrowed(&'a [f32]),
127    Owned(Vec<f32>),
128}
129
130impl<'a> SpeakerEmbeddingStorage<'a> {
131    fn as_slice(&self) -> &[f32] {
132        match self {
133            Self::Borrowed(values) => values,
134            Self::Owned(values) => values,
135        }
136    }
137}
138
139pub struct Qwen3TtsEngine {
140    paths: ModelPaths,
141    tokenizer: TextTokenizer,
142    transformer: TtsTransformer,
143    vocoder: Vocoder,
144    speaker_encoder: SpeakerEncoder,
145    mode: QtsMode,
146    instruction_control: bool,
147}
148
149impl Qwen3TtsEngine {
150    pub fn load(paths: ModelPaths) -> Result<Self, Qwen3TtsError> {
151        load_and_validate(&paths)?;
152        let main = GgufFile::open(&paths.main_gguf)?;
153        let tokenizer = TextTokenizer::load_from_gguf(&main)?;
154        let transformer = TtsTransformer::load_from_gguf(&main)?;
155        let vocoder = Vocoder::load_from_onnx(&paths.vocoder_onnx)?;
156        let speaker_encoder = SpeakerEncoder::new(transformer.config().hidden_size as usize)?;
157
158        Ok(Self {
159            paths,
160            tokenizer,
161            transformer,
162            vocoder,
163            speaker_encoder,
164            mode: QtsMode::Base,
165            instruction_control: false,
166        })
167    }
168
169    pub fn from_model_dir(dir: impl AsRef<std::path::Path>) -> Result<Self, Qwen3TtsError> {
170        Self::load(ModelPaths::from_model_dir(dir))
171    }
172
173    /// Load mode-specific 1.7B artifacts, or explicit bundled QTS exports.
174    pub fn load_mode(paths: ModeModelPaths, mode: QtsMode) -> Result<Self, Qwen3TtsError> {
175        let talker = GgufFile::open(&paths.talker_gguf)?;
176        if let Some(model_mode) = talker.get_str("qwen3-tts.tts_model_type") {
177            if model_mode.replace('_', "").to_ascii_lowercase() != mode.as_str() {
178                return Err(Qwen3TtsError::InvalidInput(
179                    "requested mode does not match GGUF model type".into(),
180                ));
181            }
182        }
183        let predictor = GgufFile::open(&paths.code_predictor_gguf)?;
184        let tokenizer_file = GgufFile::open(&paths.tokenizer_gguf)?;
185        let tokenizer = TextTokenizer::load_from_gguf(&tokenizer_file)?;
186        let transformer = TtsTransformer::load_from_ggufs(&talker, &predictor)?;
187        let instruction_control = talker
188            .get_str("qwen3-tts.tts_model_size")
189            .map_or(transformer.config().hidden_size != 1024, |size| {
190                size != "0.6b" && size != "0.6B"
191            });
192        let vocoder = Vocoder::load_from_onnx(&paths.vocoder_onnx)?;
193        let speaker_encoder = SpeakerEncoder::new(transformer.config().hidden_size as usize)?;
194        Ok(Self {
195            paths: ModelPaths::new(paths.talker_gguf, paths.vocoder_onnx),
196            tokenizer,
197            transformer,
198            vocoder,
199            speaker_encoder,
200            mode,
201            instruction_control,
202        })
203    }
204
205    pub fn from_model_dir_for_mode(
206        dir: impl AsRef<std::path::Path>,
207        mode: QtsMode,
208    ) -> Result<Self, Qwen3TtsError> {
209        Self::load_mode(ModeModelPaths::from_model_dir(dir, mode), mode)
210    }
211
212    pub fn mode(&self) -> QtsMode {
213        self.mode
214    }
215
216    pub fn synthesize_mode(
217        &self,
218        req: &ModeSynthesizeRequest<'_>,
219    ) -> Result<SynthesizeResult, Qwen3TtsError> {
220        let prepared = self.prepare_mode_synthesis(req)?;
221        if req.synthesis.vocoder_chunk_size > 0 {
222            self.synthesize_pipelined(&req.synthesis, &prepared, None)
223        } else {
224            self.synthesize_sequential(&req.synthesis, &prepared, None)
225        }
226    }
227
228    pub fn synthesize_mode_with_profile(
229        &self,
230        req: &ModeSynthesizeRequest<'_>,
231    ) -> Result<(SynthesizeResult, SynthesisStageTimings), Qwen3TtsError> {
232        let prepared = self.prepare_mode_synthesis(req)?;
233        let mut timings = SynthesisStageTimings::default();
234        let result = if req.synthesis.vocoder_chunk_size > 0 {
235            self.synthesize_pipelined(&req.synthesis, &prepared, Some(&mut timings))?
236        } else {
237            self.synthesize_sequential(&req.synthesis, &prepared, Some(&mut timings))?
238        };
239        Ok((result, timings))
240    }
241
242    pub fn synthesize_mode_streaming<S: StreamingSynthesis + Send>(
243        &self,
244        req: &ModeSynthesizeRequest<'_>,
245        sink: &mut S,
246    ) -> Result<StreamingSynthesizeResult, Qwen3TtsError> {
247        let prepared = self.prepare_mode_synthesis(req)?;
248        if req.synthesis.vocoder_chunk_size > 0 {
249            self.synthesize_pipelined_streaming(&req.synthesis, &prepared, sink, None)
250        } else {
251            self.synthesize_sequential_streaming(&req.synthesis, &prepared, sink, None)
252        }
253    }
254
255    fn prepare_mode_synthesis(
256        &self,
257        req: &ModeSynthesizeRequest<'_>,
258    ) -> Result<PreparedSynthesis, Qwen3TtsError> {
259        req.validate()?;
260        if req.mode != self.mode {
261            return Err(Qwen3TtsError::InvalidInput(
262                "request mode does not match loaded checkpoint mode".into(),
263            ));
264        }
265        if let Some(embedding) = req.speaker_embedding {
266            self.validate_speaker_embedding(embedding)?;
267        }
268        if let Some(prompt) = req.voice_clone_prompt {
269            self.validate_speaker_embedding(prompt.speaker_embedding())?;
270            if prompt
271                .ref_code_shape()
272                .is_some_and(|(_, books)| books != self.transformer.config().n_codebooks as usize)
273            {
274                return Err(Qwen3TtsError::InvalidInput(
275                    "voice clone codebook count does not match model".into(),
276                ));
277            }
278            return self.prepare_synthesis(&req.synthesis, req.speaker_embedding, Some(prompt));
279        }
280        let start = Instant::now();
281        let speaker = req
282            .speaker
283            .map(|speaker| {
284                self.transformer
285                    .preset_speaker_embedding(speaker, req.synthesis.thread_count)
286            })
287            .transpose()?;
288        let speaker_encode = start.elapsed();
289        let start = Instant::now();
290        let text_tokens = self.tokenizer.encode_for_tts(&req.synthesis.text);
291        // Upstream ignores CustomVoice instructions on 0.6B.
292        let instruct_tokens = self.tokenizer.encode_optional_instruction_for_tts(
293            req.instruct.as_deref(),
294            self.instruction_control,
295        )?;
296        let tokenize = start.elapsed();
297        let start = Instant::now();
298        let mut prepared_inputs = self.transformer.build_non_streaming_prefill_inputs(
299            PrefillConditioning {
300                text_tokens: &text_tokens,
301                speaker_embd: speaker.as_deref(),
302                ref_codebook_0: &[],
303                language_id: req.speaker.map_or(req.synthesis.language_id, |speaker| {
304                    self.transformer
305                        .preset_language_id(speaker, req.synthesis.language_id)
306                }),
307            },
308            req.synthesis.thread_count,
309        )?;
310        self.transformer.prepend_instruction(
311            &mut prepared_inputs,
312            &instruct_tokens,
313            req.synthesis.thread_count,
314        )?;
315        Ok(PreparedSynthesis {
316            prepared_inputs,
317            prompt_frames: Vec::new(),
318            prefix_frame_count: 0,
319            speaker_encode,
320            tokenize,
321            prefill_build: start.elapsed(),
322        })
323    }
324
325    pub fn model_paths(&self) -> &ModelPaths {
326        &self.paths
327    }
328
329    #[must_use]
330    pub fn tokenizer(&self) -> &TextTokenizer {
331        &self.tokenizer
332    }
333
334    #[must_use]
335    pub fn transformer(&self) -> &TtsTransformer {
336        &self.transformer
337    }
338
339    #[must_use]
340    pub fn vocoder(&self) -> &Vocoder {
341        &self.vocoder
342    }
343
344    #[must_use]
345    pub fn vocoder_execution_provider(&self) -> VocoderExecutionProvider {
346        self.vocoder.execution_provider()
347    }
348
349    #[must_use]
350    pub fn vocoder_backend_label(&self) -> &'static str {
351        self.vocoder.execution_provider_label()
352    }
353
354    #[must_use]
355    pub fn speaker_encoder(&self) -> &SpeakerEncoder {
356        &self.speaker_encoder
357    }
358
359    #[must_use]
360    pub fn primary_backend_kind(&self) -> BackendKind {
361        self.transformer.primary_backend_kind()
362    }
363
364    #[must_use]
365    pub fn encode_for_tts(&self, text: &str) -> Vec<i32> {
366        self.tokenizer.encode_for_tts(text)
367    }
368
369    pub fn encode_reference_speaker(&self, wav_bytes: &[u8]) -> Result<Vec<f32>, Qwen3TtsError> {
370        self.speaker_encoder.encode_wav_bytes(wav_bytes)
371    }
372
373    #[must_use]
374    pub fn speaker_embedding_size(&self) -> usize {
375        self.transformer.config().hidden_size as usize
376    }
377
378    pub fn decode_voice_clone_prompt(
379        &self,
380        pb_bytes: &[u8],
381    ) -> Result<VoiceClonePromptV2, Qwen3TtsError> {
382        let prompt = VoiceClonePromptV2::from_protobuf_bytes(pb_bytes)?;
383        self.validate_speaker_embedding(prompt.speaker_embedding())?;
384        if let Some((_, codebooks)) = prompt.ref_code_shape() {
385            if codebooks != self.transformer.config().n_codebooks as usize {
386                return Err(Qwen3TtsError::InvalidInput(format!(
387                    "voice clone prompt ref_code must have {} codebooks per frame",
388                    self.transformer.config().n_codebooks
389                )));
390            }
391        }
392        Ok(prompt)
393    }
394
395    pub fn synthesize_with_voice_clone_prompt(
396        &self,
397        req: &SynthesizeRequest,
398        prompt: &VoiceClonePromptV2,
399    ) -> Result<SynthesizeResult, Qwen3TtsError> {
400        self.validate_speaker_embedding(prompt.speaker_embedding())?;
401        self.synthesize_impl(req, None, Some(prompt), None)
402    }
403
404    pub fn synthesize(&self, req: &SynthesizeRequest) -> Result<SynthesizeResult, Qwen3TtsError> {
405        self.synthesize_impl(req, None, None, None)
406    }
407
408    pub fn synthesize_streaming<S>(
409        &self,
410        req: &SynthesizeRequest,
411        sink: &mut S,
412    ) -> Result<StreamingSynthesizeResult, Qwen3TtsError>
413    where
414        S: StreamingSynthesis + Send,
415    {
416        self.synthesize_streaming_impl(req, None, None, sink, None)
417    }
418
419    pub fn synthesize_with_voice_clone_prompt_streaming<S>(
420        &self,
421        req: &SynthesizeRequest,
422        prompt: &VoiceClonePromptV2,
423        sink: &mut S,
424    ) -> Result<StreamingSynthesizeResult, Qwen3TtsError>
425    where
426        S: StreamingSynthesis + Send,
427    {
428        self.validate_speaker_embedding(prompt.speaker_embedding())?;
429        self.synthesize_streaming_impl(req, None, Some(prompt), sink, None)
430    }
431
432    /// Same as [`Self::synthesize`], plus wall-clock timings per pipeline stage.
433    pub fn synthesize_with_profile(
434        &self,
435        req: &SynthesizeRequest,
436    ) -> Result<(SynthesizeResult, SynthesisStageTimings), Qwen3TtsError> {
437        let mut timings = SynthesisStageTimings::default();
438        let result = self.synthesize_impl(req, None, None, Some(&mut timings))?;
439        Ok((result, timings))
440    }
441
442    /// Same as [`Self::synthesize_with_voice_clone_prompt`], plus stage timings.
443    pub fn synthesize_with_voice_clone_prompt_profile(
444        &self,
445        req: &SynthesizeRequest,
446        prompt: &VoiceClonePromptV2,
447    ) -> Result<(SynthesizeResult, SynthesisStageTimings), Qwen3TtsError> {
448        self.validate_speaker_embedding(prompt.speaker_embedding())?;
449        let mut timings = SynthesisStageTimings::default();
450        let result = self.synthesize_impl(req, None, Some(prompt), Some(&mut timings))?;
451        Ok((result, timings))
452    }
453
454    fn synthesize_impl(
455        &self,
456        req: &SynthesizeRequest,
457        speaker_embedding_override: Option<&[f32]>,
458        voice_clone_prompt: Option<&VoiceClonePromptV2>,
459        timings: Option<&mut SynthesisStageTimings>,
460    ) -> Result<SynthesizeResult, Qwen3TtsError> {
461        let prepared =
462            self.prepare_synthesis(req, speaker_embedding_override, voice_clone_prompt)?;
463
464        if req.vocoder_chunk_size > 0 {
465            self.synthesize_pipelined(req, &prepared, timings)
466        } else {
467            self.synthesize_sequential(req, &prepared, timings)
468        }
469    }
470
471    fn synthesize_streaming_impl<S>(
472        &self,
473        req: &SynthesizeRequest,
474        speaker_embedding_override: Option<&[f32]>,
475        voice_clone_prompt: Option<&VoiceClonePromptV2>,
476        sink: &mut S,
477        timings: Option<&mut SynthesisStageTimings>,
478    ) -> Result<StreamingSynthesizeResult, Qwen3TtsError>
479    where
480        S: StreamingSynthesis + Send,
481    {
482        let prepared =
483            self.prepare_synthesis(req, speaker_embedding_override, voice_clone_prompt)?;
484
485        if req.vocoder_chunk_size > 0 {
486            self.synthesize_pipelined_streaming(req, &prepared, sink, timings)
487        } else {
488            self.synthesize_sequential_streaming(req, &prepared, sink, timings)
489        }
490    }
491
492    fn prepare_synthesis<'a>(
493        &self,
494        req: &SynthesizeRequest,
495        speaker_embedding_override: Option<&'a [f32]>,
496        voice_clone_prompt: Option<&'a VoiceClonePromptV2>,
497    ) -> Result<PreparedSynthesis, Qwen3TtsError> {
498        if self.mode != QtsMode::Base {
499            return Err(Qwen3TtsError::InvalidInput(
500                "use mode synthesis for CustomVoice or VoiceDesign".into(),
501            ));
502        }
503        let speaker_encode = std::time::Duration::ZERO;
504        let speaker_embedding = if let Some(speaker_embedding) = speaker_embedding_override {
505            SpeakerEmbeddingStorage::Borrowed(speaker_embedding)
506        } else if let Some(speaker_embedding) =
507            voice_clone_prompt.map(VoiceClonePromptV2::speaker_embedding)
508        {
509            SpeakerEmbeddingStorage::Borrowed(speaker_embedding)
510        } else {
511            SpeakerEmbeddingStorage::Owned(vec![0.0f32; self.speaker_embedding_size()])
512        };
513
514        let t_tok = Instant::now();
515        let prompt_frames = if let Some(prompt) = voice_clone_prompt {
516            prompt
517                .ref_code_shape()
518                .map_or_else(Vec::new, |(frames, codebooks)| {
519                    let values = prompt.ref_code_values().unwrap_or(&[]);
520                    (0..frames)
521                        .map(|frame_idx| {
522                            let start = frame_idx * codebooks;
523                            let end = start + codebooks;
524                            values[start..end].to_vec()
525                        })
526                        .collect::<Vec<_>>()
527                })
528        } else {
529            Vec::new()
530        };
531        let prefix_frame_count = prompt_frames.len();
532        let tokenize = t_tok.elapsed();
533
534        let t_prefill = Instant::now();
535        let prepared_inputs = if let Some(prompt) = voice_clone_prompt {
536            if prompt.icl_mode {
537                let text_tokens = self.tokenizer.encode_for_tts(&req.text);
538                let ref_text_tokens = self.tokenizer.encode_ref_for_tts(&prompt.ref_text);
539                self.transformer.build_icl_prefill_inputs(
540                    IclPrefillConditioning {
541                        text_tokens: &text_tokens,
542                        ref_text_tokens: &ref_text_tokens,
543                        speaker_embd: Some(speaker_embedding.as_slice()),
544                        ref_code_frames: &prompt_frames,
545                        language_id: req.language_id,
546                    },
547                    req.thread_count,
548                )?
549            } else {
550                let ref_codebook_0 = prompt_frames
551                    .iter()
552                    .filter_map(|frame| frame.first().copied())
553                    .collect::<Vec<_>>();
554                let text_tokens = self.tokenizer.encode_for_tts(&req.text);
555                self.transformer.build_prefill_inputs(
556                    PrefillConditioning {
557                        text_tokens: &text_tokens,
558                        speaker_embd: Some(speaker_embedding.as_slice()),
559                        ref_codebook_0: &ref_codebook_0,
560                        language_id: req.language_id,
561                    },
562                    req.thread_count,
563                )?
564            }
565        } else {
566            let text_tokens = self.tokenizer.encode_for_tts(&req.text);
567            self.transformer.build_prefill_inputs(
568                PrefillConditioning {
569                    text_tokens: &text_tokens,
570                    speaker_embd: Some(speaker_embedding.as_slice()),
571                    ref_codebook_0: &[],
572                    language_id: req.language_id,
573                },
574                req.thread_count,
575            )?
576        };
577        let prefill_build = t_prefill.elapsed();
578
579        Ok(PreparedSynthesis {
580            prepared_inputs,
581            prompt_frames,
582            prefix_frame_count,
583            speaker_encode,
584            tokenize,
585            prefill_build,
586        })
587    }
588
589    fn synthesize_sequential(
590        &self,
591        req: &SynthesizeRequest,
592        prepared: &PreparedSynthesis,
593        timings: Option<&mut SynthesisStageTimings>,
594    ) -> Result<SynthesizeResult, Qwen3TtsError> {
595        let t_roll = Instant::now();
596        let codec_rollout = self.transformer.rollout_codec_frames_kv(
597            &prepared.prepared_inputs.prefill_embd,
598            &prepared.prepared_inputs.trailing_text_hidden,
599            &prepared.prepared_inputs.tts_pad_embed,
600            &prepared.prompt_frames,
601            req.talker_kv_mode,
602            req.thread_count,
603            req.max_audio_frames,
604            req.repetition_penalty,
605            req.temperature,
606            req.top_k,
607            req.top_p,
608        )?;
609        let codec_rollout_dur = t_roll.elapsed();
610
611        let generated_frames = codec_rollout
612            .frames
613            .len()
614            .saturating_sub(prepared.prefix_frame_count);
615
616        let t_post = Instant::now();
617        let flattened_codes = codec_rollout
618            .frames
619            .iter()
620            .flat_map(|frame| frame.codebook_tokens.iter().copied())
621            .collect::<Vec<_>>();
622        let flatten_dur = t_post.elapsed();
623
624        let t_voc = Instant::now();
625        let pcm_all = self.vocoder.decode(
626            &flattened_codes,
627            codec_rollout.frames.len(),
628            req.thread_count,
629        )?;
630        let vocoder_decode = t_voc.elapsed();
631
632        let t_trim = Instant::now();
633        let pcm_f32 = if prepared.prefix_frame_count == 0 || codec_rollout.frames.is_empty() {
634            pcm_all
635        } else {
636            let cut = prepared
637                .prefix_frame_count
638                .saturating_mul(pcm_all.len())
639                .checked_div(codec_rollout.frames.len())
640                .unwrap_or(0)
641                .min(pcm_all.len());
642            pcm_all[cut..].to_vec()
643        };
644        let post = t_trim.elapsed() + flatten_dur;
645
646        let sample_rate_hz = self.vocoder.config().sample_rate as u32;
647        if let Some(t) = timings {
648            t.speaker_encode = prepared.speaker_encode;
649            t.tokenize = prepared.tokenize;
650            t.prefill_build = prepared.prefill_build;
651            t.codec_rollout = codec_rollout_dur;
652            t.vocoder_decode = vocoder_decode;
653            t.post = post;
654            t.codec_rollout_detail = codec_rollout.sub_timings;
655            t.first_frame_latency = prepared.speaker_encode
656                + prepared.tokenize
657                + prepared.prefill_build
658                + codec_rollout.first_frame_elapsed;
659            t.generated_samples = pcm_f32.len();
660            t.sample_rate_hz = sample_rate_hz;
661        }
662
663        Ok(SynthesizeResult {
664            pcm_f32,
665            sample_rate_hz,
666            generated_frames,
667        })
668    }
669
670    fn synthesize_sequential_streaming<S>(
671        &self,
672        req: &SynthesizeRequest,
673        prepared: &PreparedSynthesis,
674        sink: &mut S,
675        timings: Option<&mut SynthesisStageTimings>,
676    ) -> Result<StreamingSynthesizeResult, Qwen3TtsError>
677    where
678        S: StreamingSynthesis + Send,
679    {
680        let result = self.synthesize_sequential(req, prepared, timings)?;
681        sink.push_pcm_chunk(&result.pcm_f32)?;
682        Ok(StreamingSynthesizeResult {
683            sample_rate_hz: result.sample_rate_hz,
684            generated_frames: result.generated_frames,
685            generated_samples: result.pcm_f32.len(),
686        })
687    }
688
689    /// Pipeline transformer (GPU/main thread) with vocoder (CPU/background thread).
690    ///
691    /// The transformer generates frames autoregressively; every `chunk_size` generated
692    /// frames the codebook tokens are sent to a vocoder worker that decodes them in
693    /// parallel on CPU. When the transformer finishes the last chunk is flushed and
694    /// all audio is concatenated.
695    ///
696    /// To avoid audible clicks at chunk boundaries the vocoder thread uses
697    /// **overlap-and-add**: each chunk (after the first) is decoded with a few
698    /// extra context frames from the previous chunk prepended, then the
699    /// overlapping audio region is linearly crossfaded with the previous
700    /// chunk's tail.
701    fn synthesize_pipelined(
702        &self,
703        req: &SynthesizeRequest,
704        prepared: &PreparedSynthesis,
705        timings: Option<&mut SynthesisStageTimings>,
706    ) -> Result<SynthesizeResult, Qwen3TtsError> {
707        use std::sync::mpsc;
708
709        let chunk_size = req.vocoder_chunk_size;
710        let thread_count = req.thread_count;
711        let vocoder_thread_count = if req.vocoder_thread_count > 0 {
712            req.vocoder_thread_count
713        } else {
714            (thread_count / 2).max(1)
715        };
716
717        let (chunk_tx, chunk_rx) = mpsc::sync_channel::<VocoderChunk>(2);
718
719        let prompt_chunk = if prepared.prefix_frame_count > 0 {
720            let codes = prepared
721                .prompt_frames
722                .iter()
723                .flat_map(|f| f.iter().copied())
724                .collect::<Vec<_>>();
725            Some(VocoderChunk {
726                codes,
727                n_frames: prepared.prefix_frame_count,
728            })
729        } else {
730            None
731        };
732
733        let t_pipeline_start = Instant::now();
734
735        std::thread::scope(|s| {
736            let vocoder = &self.vocoder;
737            let n_codebooks = vocoder.config().n_codebooks as usize;
738
739            let vocoder_handle = s.spawn(
740                move || -> Result<(Vec<f32>, std::time::Duration), Qwen3TtsError> {
741                    const OVERLAP_FRAMES: usize = 3;
742                    let standard_frames = OVERLAP_FRAMES + chunk_size;
743
744                    let t_voc_start = Instant::now();
745                    let mut all_pcm = Vec::<f32>::new();
746                    let mut prev_codes: Vec<i32> = Vec::new();
747                    let mut prev_n_frames: usize = 0;
748                    let mut template: Option<VocoderGraphTemplate> = None;
749
750                    if let Some(prompt_chunk) = prompt_chunk {
751                        let audio = vocoder.decode(
752                            &prompt_chunk.codes,
753                            prompt_chunk.n_frames,
754                            vocoder_thread_count,
755                        )?;
756                        all_pcm.extend_from_slice(&audio);
757                        prev_n_frames = prompt_chunk.n_frames;
758                        prev_codes = prompt_chunk.codes;
759                    }
760
761                    while let Ok(chunk) = chunk_rx.recv() {
762                        let ctx_frames = OVERLAP_FRAMES.min(prev_n_frames);
763
764                        let mut combined_buf = Vec::new();
765                        let (codes_slice, decode_frames) = if ctx_frames > 0 {
766                            combined_buf.reserve(ctx_frames * n_codebooks + chunk.codes.len());
767                            let ctx_start = prev_codes.len() - ctx_frames * n_codebooks;
768                            combined_buf.extend_from_slice(&prev_codes[ctx_start..]);
769                            combined_buf.extend_from_slice(&chunk.codes);
770                            (combined_buf.as_slice(), ctx_frames + chunk.n_frames)
771                        } else {
772                            (chunk.codes.as_slice(), chunk.n_frames)
773                        };
774
775                        let audio = if decode_frames == standard_frames {
776                            if template.is_none() {
777                                template = Some(vocoder.build_decode_template(standard_frames)?);
778                            }
779                            vocoder.decode_with_template(
780                                template.as_mut().unwrap(),
781                                codes_slice,
782                                vocoder_thread_count,
783                            )?
784                        } else {
785                            vocoder.decode(codes_slice, decode_frames, vocoder_thread_count)?
786                        };
787
788                        if ctx_frames > 0 && !all_pcm.is_empty() {
789                            let total_frames = ctx_frames + chunk.n_frames;
790                            let overlap_samples =
791                                (audio.len() * ctx_frames / total_frames).min(all_pcm.len());
792                            let start = all_pcm.len() - overlap_samples;
793                            for i in 0..overlap_samples {
794                                let t = (i as f32 + 0.5) / overlap_samples as f32;
795                                all_pcm[start + i] = all_pcm[start + i] * (1.0 - t) + audio[i] * t;
796                            }
797                            all_pcm.extend_from_slice(&audio[overlap_samples..]);
798                        } else {
799                            all_pcm.extend_from_slice(&audio);
800                        }
801
802                        prev_n_frames = chunk.n_frames;
803                        prev_codes = chunk.codes;
804                    }
805
806                    Ok((all_pcm, t_voc_start.elapsed()))
807                },
808            );
809
810            let t_roll = Instant::now();
811            let codec_rollout = self.transformer.rollout_codec_frames_kv_streaming(
812                &prepared.prepared_inputs.prefill_embd,
813                &prepared.prepared_inputs.trailing_text_hidden,
814                &prepared.prepared_inputs.tts_pad_embed,
815                &prepared.prompt_frames,
816                req.talker_kv_mode,
817                thread_count,
818                req.max_audio_frames,
819                req.repetition_penalty,
820                req.temperature,
821                req.top_k,
822                req.top_p,
823                chunk_size,
824                &chunk_tx,
825            );
826            let codec_rollout_dur = t_roll.elapsed();
827            drop(chunk_tx);
828
829            let codec_rollout = codec_rollout?;
830            let generated_frames = codec_rollout
831                .frames
832                .len()
833                .saturating_sub(prepared.prefix_frame_count);
834
835            let (pcm_all, vocoder_decode) = vocoder_handle.join().unwrap()?;
836
837            let pipeline_wall_clock = t_pipeline_start.elapsed();
838
839            let t_trim = Instant::now();
840            let pcm_f32 = if prepared.prefix_frame_count == 0 || pcm_all.is_empty() {
841                pcm_all
842            } else {
843                let total_frames = prepared.prefix_frame_count + generated_frames;
844                let cut = prepared
845                    .prefix_frame_count
846                    .saturating_mul(pcm_all.len())
847                    .checked_div(total_frames)
848                    .unwrap_or(0)
849                    .min(pcm_all.len());
850                pcm_all[cut..].to_vec()
851            };
852            let post = t_trim.elapsed();
853
854            let sample_rate_hz = self.vocoder.config().sample_rate as u32;
855            if let Some(t) = timings {
856                t.speaker_encode = prepared.speaker_encode;
857                t.tokenize = prepared.tokenize;
858                t.prefill_build = prepared.prefill_build;
859                t.codec_rollout = codec_rollout_dur;
860                t.vocoder_decode = vocoder_decode;
861                t.post = post;
862                t.codec_rollout_detail = codec_rollout.sub_timings;
863                let sequential_sum = codec_rollout_dur + vocoder_decode;
864                t.pipeline_overlap = sequential_sum.saturating_sub(pipeline_wall_clock);
865                t.first_frame_latency = prepared.speaker_encode
866                    + prepared.tokenize
867                    + prepared.prefill_build
868                    + codec_rollout.first_frame_elapsed;
869                t.generated_samples = pcm_f32.len();
870                t.sample_rate_hz = sample_rate_hz;
871            }
872
873            Ok(SynthesizeResult {
874                pcm_f32,
875                sample_rate_hz,
876                generated_frames,
877            })
878        })
879    }
880
881    fn synthesize_pipelined_streaming<S>(
882        &self,
883        req: &SynthesizeRequest,
884        prepared: &PreparedSynthesis,
885        sink: &mut S,
886        timings: Option<&mut SynthesisStageTimings>,
887    ) -> Result<StreamingSynthesizeResult, Qwen3TtsError>
888    where
889        S: StreamingSynthesis + Send,
890    {
891        use std::sync::mpsc;
892
893        let chunk_size = req.vocoder_chunk_size;
894        let thread_count = req.thread_count;
895        let vocoder_thread_count = if req.vocoder_thread_count > 0 {
896            req.vocoder_thread_count
897        } else {
898            (thread_count / 2).max(1)
899        };
900
901        let (chunk_tx, chunk_rx) = mpsc::sync_channel::<VocoderChunk>(2);
902
903        let t_pipeline_start = Instant::now();
904
905        std::thread::scope(|s| {
906            let vocoder = &self.vocoder;
907            let n_codebooks = vocoder.config().n_codebooks as usize;
908
909            let vocoder_handle = s.spawn(
910                move || -> Result<(usize, std::time::Duration), Qwen3TtsError> {
911                    const OVERLAP_FRAMES: usize = 1;
912                    let standard_frames = OVERLAP_FRAMES + chunk_size;
913                    let t_voc_start = Instant::now();
914                    let mut total_samples = 0usize;
915                    let mut all_pcm = Vec::<f32>::new();
916                    let mut emitted_samples = 0usize;
917                    let mut prev_codes: Vec<i32> = Vec::new();
918                    let mut prev_n_frames: usize = 0;
919                    let mut template: Option<VocoderGraphTemplate> = None;
920
921                    while let Ok(chunk) = chunk_rx.recv() {
922                        let ctx_frames = OVERLAP_FRAMES.min(prev_n_frames);
923
924                        let mut combined_buf = Vec::new();
925                        let (codes_slice, decode_frames) = if ctx_frames > 0 {
926                            combined_buf.reserve(ctx_frames * n_codebooks + chunk.codes.len());
927                            let ctx_start = prev_codes.len() - ctx_frames * n_codebooks;
928                            combined_buf.extend_from_slice(&prev_codes[ctx_start..]);
929                            combined_buf.extend_from_slice(&chunk.codes);
930                            (combined_buf.as_slice(), ctx_frames + chunk.n_frames)
931                        } else {
932                            (chunk.codes.as_slice(), chunk.n_frames)
933                        };
934
935                        let audio = if decode_frames == standard_frames {
936                            if template.is_none() {
937                                template = Some(vocoder.build_decode_template(standard_frames)?);
938                            }
939                            vocoder.decode_with_template(
940                                template.as_mut().unwrap(),
941                                codes_slice,
942                                vocoder_thread_count,
943                            )?
944                        } else {
945                            vocoder.decode(codes_slice, decode_frames, vocoder_thread_count)?
946                        };
947
948                        let current_chunk_samples = if ctx_frames > 0 && !all_pcm.is_empty() {
949                            let total_frames = ctx_frames + chunk.n_frames;
950                            let overlap_samples =
951                                (audio.len() * ctx_frames / total_frames).min(all_pcm.len());
952                            let start = all_pcm.len() - overlap_samples;
953                            for i in 0..overlap_samples {
954                                let t = (i as f32 + 0.5) / overlap_samples as f32;
955                                all_pcm[start + i] = all_pcm[start + i] * (1.0 - t) + audio[i] * t;
956                            }
957                            let appended = audio.len().saturating_sub(overlap_samples);
958                            all_pcm.extend_from_slice(&audio[overlap_samples..]);
959                            appended
960                        } else {
961                            let appended = audio.len();
962                            all_pcm.extend_from_slice(&audio);
963                            appended
964                        };
965
966                        let hold_back_frames = OVERLAP_FRAMES.min(chunk.n_frames);
967                        let hold_back_samples = if chunk.n_frames == 0 {
968                            0
969                        } else {
970                            current_chunk_samples
971                                .saturating_mul(hold_back_frames)
972                                .checked_div(chunk.n_frames)
973                                .unwrap_or(0)
974                                .min(all_pcm.len().saturating_sub(emitted_samples))
975                        };
976                        let finalized_end = all_pcm.len().saturating_sub(hold_back_samples);
977
978                        if finalized_end > emitted_samples {
979                            let finalized = &all_pcm[emitted_samples..finalized_end];
980                            total_samples += finalized.len();
981                            sink.push_pcm_chunk(finalized)?;
982                            emitted_samples = finalized_end;
983                        }
984
985                        prev_n_frames = chunk.n_frames;
986                        prev_codes = chunk.codes;
987                    }
988
989                    if all_pcm.len() > emitted_samples {
990                        let finalized = &all_pcm[emitted_samples..];
991                        total_samples += finalized.len();
992                        sink.push_pcm_chunk(finalized)?;
993                    }
994
995                    Ok((total_samples, t_voc_start.elapsed()))
996                },
997            );
998
999            let t_roll = Instant::now();
1000            let codec_rollout = self.transformer.rollout_codec_frames_kv_streaming(
1001                &prepared.prepared_inputs.prefill_embd,
1002                &prepared.prepared_inputs.trailing_text_hidden,
1003                &prepared.prepared_inputs.tts_pad_embed,
1004                &prepared.prompt_frames,
1005                req.talker_kv_mode,
1006                thread_count,
1007                req.max_audio_frames,
1008                req.repetition_penalty,
1009                req.temperature,
1010                req.top_k,
1011                req.top_p,
1012                chunk_size,
1013                &chunk_tx,
1014            );
1015            let codec_rollout_dur = t_roll.elapsed();
1016            drop(chunk_tx);
1017
1018            let codec_rollout = codec_rollout?;
1019            let generated_frames = codec_rollout
1020                .frames
1021                .len()
1022                .saturating_sub(prepared.prefix_frame_count);
1023
1024            let (generated_samples, vocoder_decode) = vocoder_handle.join().unwrap()?;
1025
1026            let pipeline_wall_clock = t_pipeline_start.elapsed();
1027            let post = std::time::Duration::ZERO;
1028            let sample_rate_hz = self.vocoder.config().sample_rate as u32;
1029            if let Some(t) = timings {
1030                t.speaker_encode = prepared.speaker_encode;
1031                t.tokenize = prepared.tokenize;
1032                t.prefill_build = prepared.prefill_build;
1033                t.codec_rollout = codec_rollout_dur;
1034                t.vocoder_decode = vocoder_decode;
1035                t.post = post;
1036                t.codec_rollout_detail = codec_rollout.sub_timings;
1037                let sequential_sum = codec_rollout_dur + vocoder_decode;
1038                t.pipeline_overlap = sequential_sum.saturating_sub(pipeline_wall_clock);
1039                t.first_frame_latency = prepared.speaker_encode
1040                    + prepared.tokenize
1041                    + prepared.prefill_build
1042                    + codec_rollout.first_frame_elapsed;
1043                t.generated_samples = generated_samples;
1044                t.sample_rate_hz = sample_rate_hz;
1045            }
1046
1047            Ok(StreamingSynthesizeResult {
1048                sample_rate_hz,
1049                generated_frames,
1050                generated_samples,
1051            })
1052        })
1053    }
1054
1055    fn validate_speaker_embedding(&self, speaker_embedding: &[f32]) -> Result<(), Qwen3TtsError> {
1056        let expected = self.speaker_embedding_size();
1057        if speaker_embedding.len() != expected {
1058            return Err(Qwen3TtsError::InvalidInput(format!(
1059                "speaker embedding must have {expected} elements"
1060            )));
1061        }
1062        Ok(())
1063    }
1064}
1065
1066pub trait StreamingSynthesis {
1067    fn push_pcm_chunk(&mut self, pcm_f32: &[f32]) -> Result<(), Qwen3TtsError>;
1068}
1069
1070impl<F> StreamingSynthesis for F
1071where
1072    F: FnMut(&[f32]) -> Result<(), Qwen3TtsError>,
1073{
1074    fn push_pcm_chunk(&mut self, pcm_f32: &[f32]) -> Result<(), Qwen3TtsError> {
1075        self(pcm_f32)
1076    }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082
1083    #[test]
1084    fn request_defaults() {
1085        let r = SynthesizeRequest::default();
1086        assert_eq!(r.temperature, 0.9);
1087        assert_eq!(r.top_k, 50);
1088        assert_eq!(r.language_id, 2050);
1089        assert_eq!(r.talker_kv_mode, TalkerKvMode::F16);
1090    }
1091
1092    #[test]
1093    fn talker_kv_mode_parse_accepts_aliases() {
1094        assert_eq!(TalkerKvMode::parse("f16").unwrap(), TalkerKvMode::F16);
1095        assert_eq!(
1096            TalkerKvMode::parse("turboquant").unwrap(),
1097            TalkerKvMode::TurboQuant
1098        );
1099        assert_eq!(TalkerKvMode::parse("q8").unwrap(), TalkerKvMode::TurboQuant);
1100    }
1101}