1pub 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#[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 pub language_id: i32,
72 pub vocoder_thread_count: usize,
75 pub vocoder_chunk_size: usize,
79 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 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 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 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 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 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 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}