use crate::cpu_gemm::PackedWeight;
use anyhow::{Context, Result};
use serde_json::Value;
use std::path::{Path, PathBuf};
pub mod cfg {
pub const TALKER_HIDDEN_1B7: usize = 2048;
pub const TALKER_HIDDEN_0B6: usize = 1024;
pub const TALKER_LAYERS: usize = 28;
pub const TALKER_HEADS: usize = 16;
pub const TALKER_KV_HEADS: usize = 8;
pub const HEAD_DIM: usize = 128;
pub const TALKER_INTERMEDIATE_1B7: usize = 6144;
pub const TALKER_INTERMEDIATE_0B6: usize = 3072;
pub const TALKER_RMS_EPS: f32 = 1e-6;
pub const TALKER_ROPE_THETA: f32 = 1_000_000.0;
pub const MROPE_SECTION: [usize; 3] = [24, 20, 20];
pub const POSITION_ID_PER_SECONDS: usize = 13;
pub const TALKER_CODEC_VOCAB: usize = 3072;
pub const CODEC_PAD: u32 = 2148;
pub const CODEC_BOS: u32 = 2149;
pub const CODEC_EOS: u32 = 2150;
pub const CODEC_THINK: u32 = 2154;
pub const CODEC_NOTHINK: u32 = 2155;
pub const CODEC_THINK_BOS: u32 = 2156;
pub const CODEC_THINK_EOS: u32 = 2157;
pub const TEXT_VOCAB: usize = 151_936;
pub const IM_START: u32 = 151_644;
pub const IM_END: u32 = 151_645;
pub const ASSISTANT: u32 = 77_091;
pub const TTS_PAD: u32 = 151_671;
pub const TTS_BOS: u32 = 151_672;
pub const TTS_EOS: u32 = 151_673;
pub const CP_HIDDEN: usize = 1024;
pub const CP_LAYERS: usize = 5;
pub const CP_HEADS: usize = 16;
pub const CP_KV_HEADS: usize = 8;
pub const CP_INTERMEDIATE: usize = 3072;
pub const CP_VOCAB: usize = 2048;
pub const NUM_CODE_GROUPS: usize = 16;
pub const CP_ROPE_THETA: f32 = 1_000_000.0;
pub const SAMPLE_RATE: u32 = 24_000;
pub const SAMPLES_PER_FRAME: usize = 1920;
pub const MAX_FRAMES: usize = 8192;
pub const DEC_HIDDEN: usize = 512;
pub const DEC_LAYERS: usize = 8;
pub const DEC_HEADS: usize = 16; pub const DEC_HEAD_DIM: usize = 64;
pub const DEC_LATENT: usize = 1024;
pub const DEC_CODEBOOK_DIM: usize = 512;
pub const DEC_DECODER_DIM: usize = 1536;
pub const DEC_SLIDING_WINDOW: usize = 72;
pub const DEC_UPSAMPLE_RATES: [usize; 4] = [8, 5, 4, 3];
pub const DEC_RMS_EPS: f32 = 1e-5;
pub const DEC_ROPE_THETA: f32 = 10_000.0;
pub const DEC_SEMANTIC_CODEBOOK_SIZE: usize = 4096;
pub const ENC_HIDDEN: usize = 512;
pub const ENC_LAYERS: usize = 8;
pub const ENC_HEADS: usize = 8;
pub const ENC_CODEBOOK_DIM: usize = 256;
pub const ENC_QUANTIZERS_TOTAL: usize = 32;
pub const ENC_QUANTIZERS_VALID: usize = 16;
pub const ENC_SLIDING_WINDOW: usize = 250;
pub const ENC_RATIOS: [usize; 4] = [8, 6, 5, 4];
pub const SAMPLE_REPETITION_PENALTY: f32 = 1.05; pub const SAMPLE_TEMPERATURE: f32 = 0.9;
pub const SAMPLE_TOP_K: usize = 50;
pub const SAMPLE_TOP_P: f32 = 1.0;
pub const FILE_MODEL: &str = "model.safetensors";
pub const FILE_CONFIG: &str = "config.json";
pub const FILE_GENERATION: &str = "generation_config.json";
pub const FILE_VOCAB: &str = "vocab.json";
pub const FILE_MERGES: &str = "merges.txt";
pub const DIR_CODEC: &str = "speech_tokenizer";
}
pub fn default_root() -> PathBuf {
std::env::var("OSFKB_QWEN3TTS_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".into()))
.join(".cache/inferencelayer/qwen3tts")
})
}
fn get<'a>(v: &'a Value, k: &str) -> Result<&'a Value> {
v.get(k)
.with_context(|| format!("missing config key {k:?}"))
}
fn ju(v: &Value, k: &str) -> Result<usize> {
Ok(get(v, k)?
.as_u64()
.with_context(|| format!("config key {k:?} is not an integer"))? as usize)
}
fn ju32(v: &Value, k: &str) -> Result<u32> {
Ok(get(v, k)?
.as_u64()
.with_context(|| format!("config key {k:?} is not an integer"))? as u32)
}
fn jf(v: &Value, k: &str) -> Result<f32> {
Ok(get(v, k)?
.as_f64()
.with_context(|| format!("config key {k:?} is not a number"))? as f32)
}
fn js(v: &Value, k: &str) -> Result<String> {
Ok(get(v, k)?
.as_str()
.with_context(|| format!("config key {k:?} is not a string"))?
.to_string())
}
fn jb(v: &Value, k: &str) -> Result<bool> {
get(v, k)?
.as_bool()
.with_context(|| format!("config key {k:?} is not a bool"))
}
fn juvec(v: &Value, k: &str) -> Result<Vec<usize>> {
get(v, k)?
.as_array()
.with_context(|| format!("config key {k:?} is not an array"))?
.iter()
.map(|x| {
x.as_u64()
.map(|x| x as usize)
.context("non-integer array element")
})
.collect()
}
fn jmap_u32(v: &Value, k: &str) -> std::collections::BTreeMap<String, u32> {
let mut out = std::collections::BTreeMap::new();
if let Some(m) = v.get(k).and_then(|m| m.as_object()) {
for (name, id) in m {
if let Some(id) = id.as_u64() {
out.insert(name.clone(), id as u32);
}
}
}
out
}
#[derive(Debug, Clone)]
pub struct TopConfig {
pub tts_model_size: String, pub tts_model_type: String, pub speaker_encoder_config: Option<SpkEncConfig>,
pub talker_config: TalkerConfig,
}
#[derive(Debug, Clone)]
pub struct SpkEncConfig {
pub enc_dim: usize,
pub sample_rate: u32,
}
#[derive(Debug, Clone)]
pub struct TalkerConfig {
pub hidden_size: usize,
pub num_hidden_layers: usize,
pub num_attention_heads: usize,
pub num_key_value_heads: usize,
pub head_dim: usize,
pub intermediate_size: usize,
pub rms_norm_eps: f32,
pub rope_theta: f32,
pub mrope_section: [usize; 3],
pub mrope_interleaved: bool,
pub vocab_size: usize, pub text_vocab_size: usize, pub text_hidden_size: usize,
pub num_code_groups: usize,
pub position_id_per_seconds: usize,
pub code_predictor_config: CodePredictorConfig,
pub codec_bos_id: u32,
pub codec_eos_id: u32,
pub codec_pad_id: u32,
pub codec_think_id: u32,
pub codec_nothink_id: u32,
pub codec_think_bos_id: u32,
pub codec_think_eos_id: u32,
pub codec_language_id: std::collections::BTreeMap<String, u32>,
pub spk_id: std::collections::BTreeMap<String, u32>,
pub spk_is_dialect: std::collections::BTreeMap<String, Option<String>>,
}
#[derive(Debug, Clone)]
pub struct CodePredictorConfig {
pub hidden_size: usize,
pub num_hidden_layers: usize,
pub num_attention_heads: usize,
pub num_key_value_heads: usize,
pub head_dim: usize,
pub intermediate_size: usize,
pub rms_norm_eps: f32,
pub rope_theta: f32,
pub vocab_size: usize,
pub num_code_groups: usize,
}
#[derive(Debug, Clone)]
pub struct CodecConfig {
pub encoder_valid_num_quantizers: usize,
pub input_sample_rate: u32,
pub output_sample_rate: u32,
pub decode_upsample_rate: usize,
pub encode_downsample_rate: usize,
pub decoder_config: CodecDecoderConfig,
pub encoder_config: CodecEncoderConfig,
}
#[derive(Debug, Clone)]
pub struct CodecDecoderConfig {
pub latent_dim: usize,
pub codebook_dim: usize,
pub codebook_size: usize,
pub decoder_dim: usize,
pub hidden_size: usize,
pub intermediate_size: usize,
pub layer_scale_initial_scale: f32,
pub head_dim: usize,
pub num_attention_heads: usize,
pub num_hidden_layers: usize,
pub num_key_value_heads: usize,
pub num_quantizers: usize,
pub num_semantic_quantizers: usize,
pub rms_norm_eps: f32,
pub rope_theta: f32,
pub semantic_codebook_size: usize,
pub sliding_window: usize,
pub upsample_rates: Vec<usize>,
pub upsampling_ratios: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct CodecEncoderConfig {
pub codebook_dim: usize,
pub codebook_size: usize,
pub hidden_size: usize,
pub intermediate_size: usize,
pub kernel_size: usize,
pub last_kernel_size: usize,
pub num_attention_heads: usize,
pub num_filters: usize,
pub num_hidden_layers: usize,
pub num_key_value_heads: usize,
pub num_quantizers: usize,
pub num_residual_layers: usize,
pub num_semantic_quantizers: usize,
pub residual_kernel_size: usize,
pub sliding_window: usize,
pub trim_right_ratio: f32,
pub upsampling_ratios: Vec<usize>,
pub use_causal_conv: bool,
}
#[derive(Debug, Clone)]
pub struct GenerationConfig {
pub do_sample: bool,
pub repetition_penalty: f32,
pub temperature: f32,
pub top_p: f32,
pub top_k: usize,
pub subtalker_temperature: f32,
pub subtalker_top_p: f32,
pub subtalker_top_k: usize,
pub max_new_tokens: usize,
}
impl TopConfig {
fn from_json(v: &Value) -> Result<Self> {
let spk = v
.get("speaker_encoder_config")
.filter(|s| !s.is_null())
.map(|s| -> Result<SpkEncConfig> {
Ok(SpkEncConfig {
enc_dim: ju(s, "enc_dim")?,
sample_rate: ju32(s, "sample_rate")?,
})
})
.transpose()?;
Ok(Self {
tts_model_size: js(v, "tts_model_size")?,
tts_model_type: js(v, "tts_model_type")?,
speaker_encoder_config: spk,
talker_config: TalkerConfig::from_json(get(v, "talker_config")?)?,
})
}
}
impl TalkerConfig {
fn from_json(v: &Value) -> Result<Self> {
let rs = get(v, "rope_scaling")?;
let sect = juvec(rs, "mrope_section")?;
anyhow::ensure!(
sect.len() == 3,
"mrope_section has {} entries, want 3",
sect.len()
);
Ok(Self {
hidden_size: ju(v, "hidden_size")?,
num_hidden_layers: ju(v, "num_hidden_layers")?,
num_attention_heads: ju(v, "num_attention_heads")?,
num_key_value_heads: ju(v, "num_key_value_heads")?,
head_dim: ju(v, "head_dim")?,
intermediate_size: ju(v, "intermediate_size")?,
rms_norm_eps: jf(v, "rms_norm_eps")?,
rope_theta: jf(v, "rope_theta")?,
mrope_section: [sect[0], sect[1], sect[2]],
mrope_interleaved: rs
.get("interleaved")
.and_then(|b| b.as_bool())
.unwrap_or(false),
vocab_size: ju(v, "vocab_size")?,
text_vocab_size: ju(v, "text_vocab_size")?,
text_hidden_size: ju(v, "text_hidden_size")?,
num_code_groups: ju(v, "num_code_groups")?,
position_id_per_seconds: ju(v, "position_id_per_seconds")?,
code_predictor_config: CodePredictorConfig::from_json(get(
v,
"code_predictor_config",
)?)?,
codec_bos_id: ju32(v, "codec_bos_id")?,
codec_eos_id: ju32(v, "codec_eos_token_id")?,
codec_pad_id: ju32(v, "codec_pad_id")?,
codec_think_id: ju32(v, "codec_think_id")?,
codec_nothink_id: ju32(v, "codec_nothink_id")?,
codec_think_bos_id: ju32(v, "codec_think_bos_id")?,
codec_think_eos_id: ju32(v, "codec_think_eos_id")?,
codec_language_id: jmap_u32(v, "codec_language_id"),
spk_id: jmap_u32(v, "spk_id"),
spk_is_dialect: {
let mut out = std::collections::BTreeMap::new();
if let Some(m) = v.get("spk_is_dialect").and_then(|m| m.as_object()) {
for (name, val) in m {
out.insert(name.clone(), val.as_str().map(|s| s.to_string()));
}
}
out
},
})
}
}
impl CodePredictorConfig {
fn from_json(v: &Value) -> Result<Self> {
Ok(Self {
hidden_size: ju(v, "hidden_size")?,
num_hidden_layers: ju(v, "num_hidden_layers")?,
num_attention_heads: ju(v, "num_attention_heads")?,
num_key_value_heads: ju(v, "num_key_value_heads")?,
head_dim: ju(v, "head_dim")?,
intermediate_size: ju(v, "intermediate_size")?,
rms_norm_eps: jf(v, "rms_norm_eps")?,
rope_theta: jf(v, "rope_theta")?,
vocab_size: ju(v, "vocab_size")?,
num_code_groups: ju(v, "num_code_groups")?,
})
}
}
impl CodecConfig {
fn from_json(v: &Value) -> Result<Self> {
Ok(Self {
encoder_valid_num_quantizers: ju(v, "encoder_valid_num_quantizers")?,
input_sample_rate: ju32(v, "input_sample_rate")?,
output_sample_rate: ju32(v, "output_sample_rate")?,
decode_upsample_rate: ju(v, "decode_upsample_rate")?,
encode_downsample_rate: ju(v, "encode_downsample_rate")?,
decoder_config: CodecDecoderConfig::from_json(get(v, "decoder_config")?)?,
encoder_config: CodecEncoderConfig::from_json(get(v, "encoder_config")?)?,
})
}
}
impl CodecDecoderConfig {
fn from_json(v: &Value) -> Result<Self> {
Ok(Self {
latent_dim: ju(v, "latent_dim")?,
codebook_dim: ju(v, "codebook_dim")?,
codebook_size: ju(v, "codebook_size")?,
decoder_dim: ju(v, "decoder_dim")?,
hidden_size: ju(v, "hidden_size")?,
intermediate_size: ju(v, "intermediate_size")?,
layer_scale_initial_scale: jf(v, "layer_scale_initial_scale")?,
head_dim: ju(v, "head_dim")?,
num_attention_heads: ju(v, "num_attention_heads")?,
num_hidden_layers: ju(v, "num_hidden_layers")?,
num_key_value_heads: ju(v, "num_key_value_heads")?,
num_quantizers: ju(v, "num_quantizers")?,
num_semantic_quantizers: ju(v, "num_semantic_quantizers")?,
rms_norm_eps: jf(v, "rms_norm_eps")?,
rope_theta: jf(v, "rope_theta")?,
semantic_codebook_size: ju(v, "semantic_codebook_size")?,
sliding_window: ju(v, "sliding_window")?,
upsample_rates: juvec(v, "upsample_rates")?,
upsampling_ratios: juvec(v, "upsampling_ratios")?,
})
}
}
impl CodecEncoderConfig {
fn from_json(v: &Value) -> Result<Self> {
Ok(Self {
codebook_dim: ju(v, "codebook_dim")?,
codebook_size: ju(v, "codebook_size")?,
hidden_size: ju(v, "hidden_size")?,
intermediate_size: ju(v, "intermediate_size")?,
kernel_size: ju(v, "kernel_size")?,
last_kernel_size: ju(v, "last_kernel_size")?,
num_attention_heads: ju(v, "num_attention_heads")?,
num_filters: ju(v, "num_filters")?,
num_hidden_layers: ju(v, "num_hidden_layers")?,
num_key_value_heads: ju(v, "num_key_value_heads")?,
num_quantizers: ju(v, "num_quantizers")?,
num_residual_layers: ju(v, "num_residual_layers")?,
num_semantic_quantizers: ju(v, "num_semantic_quantizers")?,
residual_kernel_size: ju(v, "residual_kernel_size")?,
sliding_window: ju(v, "sliding_window")?,
trim_right_ratio: jf(v, "trim_right_ratio")?,
upsampling_ratios: juvec(v, "upsampling_ratios")?,
use_causal_conv: jb(v, "use_causal_conv")?,
})
}
}
impl GenerationConfig {
fn from_json(v: &Value) -> Result<Self> {
Ok(Self {
do_sample: jb(v, "do_sample")?,
repetition_penalty: jf(v, "repetition_penalty")?,
temperature: jf(v, "temperature")?,
top_p: jf(v, "top_p")?,
top_k: ju(v, "top_k")?,
subtalker_temperature: jf(v, "subtalker_temperature")?,
subtalker_top_p: jf(v, "subtalker_top_p")?,
subtalker_top_k: ju(v, "subtalker_top_k")?,
max_new_tokens: ju(v, "max_new_tokens")?,
})
}
}
#[derive(Debug, Clone)]
pub struct Qwen3TtsConfig {
pub top: TopConfig,
pub codec: CodecConfig,
pub generation: GenerationConfig,
}
impl Qwen3TtsConfig {
pub fn load(dir: &Path) -> Result<Self> {
let read = |p: PathBuf| -> Result<Value> {
serde_json::from_slice(
&std::fs::read(&p).with_context(|| format!("read {}", p.display()))?,
)
.with_context(|| format!("parse {}", p.display()))
};
let top = TopConfig::from_json(&read(dir.join(cfg::FILE_CONFIG))?)
.context("parse config.json")?;
let codec = CodecConfig::from_json(&read(dir.join(cfg::DIR_CODEC).join(cfg::FILE_CONFIG))?)
.context("parse speech_tokenizer/config.json")?;
let generation = GenerationConfig::from_json(&read(dir.join(cfg::FILE_GENERATION))?)
.context("parse generation_config.json")?;
let s = Self {
top,
codec,
generation,
};
s.validate()?;
Ok(s)
}
fn validate(&self) -> Result<()> {
use anyhow::ensure;
let t = &self.top.talker_config;
let (want_h, want_i) = match self.top.tts_model_size.as_str() {
"1b7" => (cfg::TALKER_HIDDEN_1B7, cfg::TALKER_INTERMEDIATE_1B7),
"0b6" => (cfg::TALKER_HIDDEN_0B6, cfg::TALKER_INTERMEDIATE_0B6),
other => anyhow::bail!("unrecognized tts_model_size {other:?} (want 1b7|0b6)"),
};
ensure!(
t.hidden_size == want_h,
"talker hidden {} ≠ {want_h}",
t.hidden_size
);
ensure!(
t.intermediate_size == want_i,
"talker intermediate {}",
t.intermediate_size
);
ensure!(
t.num_hidden_layers == cfg::TALKER_LAYERS,
"talker layers {}",
t.num_hidden_layers
);
ensure!(t.num_attention_heads == cfg::TALKER_HEADS, "talker heads");
ensure!(
t.num_key_value_heads == cfg::TALKER_KV_HEADS,
"talker kv heads"
);
ensure!(
t.head_dim == cfg::HEAD_DIM,
"talker head_dim {} ≠ 128",
t.head_dim
);
ensure!(t.mrope_section == cfg::MROPE_SECTION, "mrope_section");
ensure!(t.mrope_interleaved, "expected interleaved mrope");
ensure!(
t.vocab_size == cfg::TALKER_CODEC_VOCAB,
"talker codec vocab {}",
t.vocab_size
);
ensure!(
t.text_vocab_size == cfg::TEXT_VOCAB,
"text vocab {}",
t.text_vocab_size
);
ensure!(t.num_code_groups == cfg::NUM_CODE_GROUPS, "num_code_groups");
ensure!(
t.codec_eos_id == cfg::CODEC_EOS,
"codec_eos {}",
t.codec_eos_id
);
ensure!(
t.codec_bos_id == cfg::CODEC_BOS && t.codec_pad_id == cfg::CODEC_PAD,
"codec bos/pad"
);
let p = &t.code_predictor_config;
ensure!(
p.hidden_size == cfg::CP_HIDDEN,
"cp hidden {}",
p.hidden_size
);
ensure!(p.num_hidden_layers == cfg::CP_LAYERS, "cp layers");
ensure!(
p.head_dim == cfg::HEAD_DIM,
"cp head_dim {} ≠ 128",
p.head_dim
);
ensure!(p.vocab_size == cfg::CP_VOCAB, "cp vocab {}", p.vocab_size);
ensure!(
p.num_code_groups == cfg::NUM_CODE_GROUPS,
"cp num_code_groups"
);
let d = &self.codec.decoder_config;
ensure!(
self.codec.decode_upsample_rate == cfg::SAMPLES_PER_FRAME,
"frame size"
);
ensure!(
self.codec.encode_downsample_rate == cfg::SAMPLES_PER_FRAME,
"frame size (enc)"
);
ensure!(
self.codec.output_sample_rate == cfg::SAMPLE_RATE,
"sample rate"
);
ensure!(
self.codec.encoder_valid_num_quantizers == cfg::ENC_QUANTIZERS_VALID,
"valid quantizers"
);
ensure!(
d.hidden_size == cfg::DEC_HIDDEN && d.num_hidden_layers == cfg::DEC_LAYERS,
"dec core"
);
ensure!(
d.sliding_window == cfg::DEC_SLIDING_WINDOW,
"dec sliding window"
);
ensure!(
d.codebook_size == cfg::CP_VOCAB,
"dec codebook size {}",
d.codebook_size
);
ensure!(
d.upsample_rates == cfg::DEC_UPSAMPLE_RATES,
"dec upsample rates"
);
ensure!(d.num_quantizers == cfg::NUM_CODE_GROUPS, "dec quantizers");
let e = &self.codec.encoder_config;
ensure!(e.upsampling_ratios == cfg::ENC_RATIOS, "enc ratios");
ensure!(
e.num_quantizers == cfg::ENC_QUANTIZERS_TOTAL,
"enc quantizers"
);
ensure!(e.use_causal_conv, "enc must be causal");
ensure!(
d.rms_norm_eps == cfg::DEC_RMS_EPS,
"dec rms eps {}",
d.rms_norm_eps
);
ensure!(
d.rope_theta == cfg::DEC_ROPE_THETA,
"dec rope theta {}",
d.rope_theta
);
ensure!(
d.head_dim == cfg::DEC_HEAD_DIM,
"dec head_dim {}",
d.head_dim
);
ensure!(
d.num_attention_heads == cfg::DEC_HEADS && d.latent_dim == cfg::DEC_LATENT,
"dec attn/latent dims"
);
ensure!(
e.sliding_window == cfg::ENC_SLIDING_WINDOW,
"enc sliding window"
);
ensure!(
e.hidden_size == cfg::ENC_HIDDEN && e.codebook_dim == cfg::ENC_CODEBOOK_DIM,
"enc dims"
);
Ok(())
}
}
#[cfg(feature = "cli")]
mod text_tokenizer {
use super::cfg;
use anyhow::{Context, Result};
use std::path::Path;
pub const QWEN2_SPLIT_REGEX: &str = r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+";
pub struct TextTokenizer {
inner: tokenizers::Tokenizer,
}
impl TextTokenizer {
pub fn load(dir: &Path) -> Result<Self> {
use tokenizers::AddedToken;
use tokenizers::SplitDelimiterBehavior;
use tokenizers::models::bpe::BPE;
use tokenizers::normalizers::unicode::NFC;
use tokenizers::pre_tokenizers::byte_level::ByteLevel;
use tokenizers::pre_tokenizers::sequence::Sequence;
use tokenizers::pre_tokenizers::split::{Split, SplitPattern};
let vocab = dir.join(cfg::FILE_VOCAB);
let merges = dir.join(cfg::FILE_MERGES);
let bpe = BPE::from_file(
vocab.to_str().context("vocab path not utf-8")?,
merges.to_str().context("merges path not utf-8")?,
)
.build()
.map_err(|e| anyhow::anyhow!("build BPE from {}: {e}", dir.display()))?;
let mut tk = tokenizers::Tokenizer::new(bpe);
tk.with_normalizer(Some(NFC));
let split = Split::new(
SplitPattern::Regex(QWEN2_SPLIT_REGEX.into()),
SplitDelimiterBehavior::Isolated,
false,
)
.map_err(|e| anyhow::anyhow!("split pre-tokenizer: {e}"))?;
let byte_level = ByteLevel::new(false, false, false);
tk.with_pre_tokenizer(Some(Sequence::new(vec![split.into(), byte_level.into()])));
tk.with_decoder(Some(tokenizers::decoders::byte_level::ByteLevel::new(
false, false, false,
)));
let tc: serde_json::Value = serde_json::from_slice(
&std::fs::read(dir.join("tokenizer_config.json"))
.with_context(|| format!("read tokenizer_config.json in {}", dir.display()))?,
)
.context("parse tokenizer_config.json")?;
let decoder = tc
.get("added_tokens_decoder")
.and_then(|d| d.as_object())
.context("tokenizer_config.json: missing added_tokens_decoder")?;
let mut added: Vec<(u32, String, bool)> = Vec::with_capacity(decoder.len());
for (id, meta) in decoder {
let id: u32 = id.parse().context("added token id not an integer")?;
let content = meta
.get("content")
.and_then(|c| c.as_str())
.context("added token without content")?
.to_string();
let special = meta
.get("special")
.and_then(|s| s.as_bool())
.unwrap_or(false);
added.push((id, content, special));
}
added.sort_by_key(|(id, _, _)| *id);
for (id, content, special) in &added {
let tok = AddedToken::from(content.clone(), *special);
if *special {
tk.add_special_tokens(&[tok]);
} else {
tk.add_tokens(&[tok]);
}
let got = tk.token_to_id(content);
anyhow::ensure!(
got == Some(*id),
"added token {content:?} landed on id {got:?}, want {id} — \
added_tokens_decoder is not contiguous from the base vocab"
);
}
Ok(Self { inner: tk })
}
pub fn encode(&self, text: &str) -> Result<Vec<u32>> {
let enc = self
.inner
.encode(text, false)
.map_err(|e| anyhow::anyhow!("encode: {e}"))?;
Ok(enc.get_ids().to_vec())
}
pub fn decode(&self, ids: &[u32]) -> Result<String> {
self.inner
.decode(ids, false)
.map_err(|e| anyhow::anyhow!("decode: {e}"))
}
pub fn token_to_id(&self, token: &str) -> Option<u32> {
self.inner.token_to_id(token)
}
pub fn vocab_size(&self) -> usize {
self.inner.get_vocab_size(true)
}
}
}
#[cfg(feature = "cli")]
pub use text_tokenizer::{QWEN2_SPLIT_REGEX, TextTokenizer};
pub(crate) fn matvec(m: &[f32], x: &[f32], rows: usize, cols: usize) -> Vec<f32> {
debug_assert_eq!(m.len(), rows * cols);
debug_assert_eq!(x.len(), cols);
(0..rows)
.map(|r| {
m[r * cols..(r + 1) * cols]
.iter()
.zip(x)
.map(|(a, b)| a * b)
.sum()
})
.collect()
}
pub(crate) fn rms_norm(x: &[f32], w: &[f32], eps: f32) -> Vec<f32> {
let ms = x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32;
let inv = 1.0 / (ms + eps).sqrt();
x.iter().zip(w).map(|(v, g)| v * inv * g).collect()
}
pub(crate) fn softmax_inplace(v: &mut [f32]) {
let m = v.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut s = 0.0;
for x in v.iter_mut() {
*x = (*x - m).exp();
s += *x;
}
for x in v.iter_mut() {
*x /= s;
}
}
pub(crate) fn silu(x: f32) -> f32 {
x / (1.0 + (-x).exp())
}
fn mrope_axis_map(section: [usize; 3], half: usize) -> Vec<u8> {
let mut axes = vec![0u8; half];
for (d, &len) in section.iter().enumerate().skip(1) {
let mut i = d;
while i < (len * 3).min(half) {
axes[i] = d as u8;
i += 3;
}
}
axes
}
fn mrope_cs(hd: usize, theta: f32, axes: &[u8], pos3: [u32; 3]) -> (Vec<f32>, Vec<f32>) {
let half = hd / 2;
debug_assert_eq!(axes.len(), half);
let (mut cos, mut sin) = (vec![0f32; hd], vec![0f32; hd]);
for i in 0..half {
let pos = pos3[axes[i] as usize];
let f = (pos as f32) * theta.powf(-2.0 * i as f32 / hd as f32);
let (s, c) = f.sin_cos();
cos[i] = c;
cos[i + half] = c;
sin[i] = s;
sin[i + half] = s;
}
(cos, sin)
}
pub(crate) fn rope_cs_1d(hd: usize, theta: f32, pos: u32) -> (Vec<f32>, Vec<f32>) {
let half = hd / 2;
let (mut cos, mut sin) = (vec![0f32; hd], vec![0f32; hd]);
for i in 0..half {
let f = (pos as f32) * theta.powf(-2.0 * i as f32 / hd as f32);
let (s, c) = f.sin_cos();
cos[i] = c;
cos[i + half] = c;
sin[i] = s;
sin[i + half] = s;
}
(cos, sin)
}
pub(crate) fn apply_rope(x: &mut [f32], cos: &[f32], sin: &[f32], heads: usize, hd: usize) {
let half = hd / 2;
for head in 0..heads {
let base = head * hd;
let mut rot = vec![0f32; hd];
for d in 0..half {
rot[d] = -x[base + half + d];
rot[half + d] = x[base + d];
}
for d in 0..hd {
x[base + d] = x[base + d] * cos[d] + rot[d] * sin[d];
}
}
}
struct DecoderLayer {
input_ln: Vec<f32>,
q: PackedWeight, k: PackedWeight, v: PackedWeight, o: PackedWeight, q_norm: Vec<f32>, k_norm: Vec<f32>, post_ln: Vec<f32>,
gate: PackedWeight,
up: PackedWeight,
down: PackedWeight,
}
fn pw_mul(w: &PackedWeight, x: &[f32]) -> Vec<f32> {
debug_assert_eq!(x.len(), w.k());
let mut out = vec![0f32; w.n()];
crate::cpu_gemm::gemm_packed(&mut out, x, w, 1, None);
out
}
fn pack(v: Vec<f32>, n: usize, k: usize) -> PackedWeight {
PackedWeight::new(&v, n, k)
}
#[derive(Debug, Clone, Copy)]
struct StackDims {
h: usize,
layers: usize,
heads: usize,
kv_heads: usize,
hd: usize,
inter: usize,
eps: f32,
theta: f32,
}
pub struct TalkerState {
k: Vec<Vec<f32>>,
v: Vec<Vec<f32>>,
len: usize,
}
impl TalkerState {
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
struct Stack {
dims: StackDims,
layers: Vec<DecoderLayer>,
final_norm: Vec<f32>,
axes: Option<Vec<u8>>, }
impl Stack {
fn load(
st: &chatterbox_st::St,
prefix: &str,
dims: StackDims,
mrope: Option<[usize; 3]>,
) -> Result<Self> {
let (h, qw, kvw) = (dims.h, dims.heads * dims.hd, dims.kv_heads * dims.hd);
let mut layers = Vec::with_capacity(dims.layers);
for l in 0..dims.layers {
let p = format!("{prefix}.layers.{l}");
layers.push(DecoderLayer {
input_ln: st.f32(&format!("{p}.input_layernorm.weight"))?,
q: pack(
st.mat(&format!("{p}.self_attn.q_proj.weight"), qw, h)?,
qw,
h,
),
k: pack(
st.mat(&format!("{p}.self_attn.k_proj.weight"), kvw, h)?,
kvw,
h,
),
v: pack(
st.mat(&format!("{p}.self_attn.v_proj.weight"), kvw, h)?,
kvw,
h,
),
o: pack(
st.mat(&format!("{p}.self_attn.o_proj.weight"), h, qw)?,
h,
qw,
),
q_norm: st.f32(&format!("{p}.self_attn.q_norm.weight"))?,
k_norm: st.f32(&format!("{p}.self_attn.k_norm.weight"))?,
post_ln: st.f32(&format!("{p}.post_attention_layernorm.weight"))?,
gate: pack(
st.mat(&format!("{p}.mlp.gate_proj.weight"), dims.inter, h)?,
dims.inter,
h,
),
up: pack(
st.mat(&format!("{p}.mlp.up_proj.weight"), dims.inter, h)?,
dims.inter,
h,
),
down: pack(
st.mat(&format!("{p}.mlp.down_proj.weight"), h, dims.inter)?,
h,
dims.inter,
),
});
}
Ok(Self {
dims,
layers,
final_norm: st.f32(&format!("{prefix}.norm.weight"))?,
axes: mrope.map(|s| mrope_axis_map(s, dims.hd / 2)),
})
}
fn rope_at(&self, pos: u32) -> (Vec<f32>, Vec<f32>) {
match &self.axes {
Some(a) => mrope_cs(self.dims.hd, self.dims.theta, a, [pos, pos, pos]),
None => rope_cs_1d(self.dims.hd, self.dims.theta, pos),
}
}
fn new_state(&self) -> TalkerState {
TalkerState {
k: vec![Vec::new(); self.dims.layers],
v: vec![Vec::new(); self.dims.layers],
len: 0,
}
}
fn step(&self, state: &mut TalkerState, embed: &[f32], pos: u32) -> Vec<f32> {
let d = self.dims;
let (h, hd, qw, kvw) = (d.h, d.hd, d.heads * d.hd, d.kv_heads * d.hd);
let group = d.heads / d.kv_heads;
let scale = 1.0 / (hd as f32).sqrt();
let (cos, sin) = self.rope_at(pos);
let mut x = embed.to_vec();
for (l, layer) in self.layers.iter().enumerate() {
let xn = rms_norm(&x, &layer.input_ln, d.eps);
let mut q = pw_mul(&layer.q, &xn);
let mut kx = pw_mul(&layer.k, &xn);
let vx = pw_mul(&layer.v, &xn);
for head in 0..d.heads {
let s = &mut q[head * hd..(head + 1) * hd];
s.copy_from_slice(&rms_norm(s, &layer.q_norm, d.eps));
}
for head in 0..d.kv_heads {
let s = &mut kx[head * hd..(head + 1) * hd];
s.copy_from_slice(&rms_norm(s, &layer.k_norm, d.eps));
}
apply_rope(&mut q, &cos, &sin, d.heads, hd);
apply_rope(&mut kx, &cos, &sin, d.kv_heads, hd);
state.k[l].extend_from_slice(&kx);
state.v[l].extend_from_slice(&vx);
let t_len = state.k[l].len() / kvw;
let mut attn = vec![0f32; qw];
for head in 0..d.heads {
let kvh = head / group;
let qh = &q[head * hd..(head + 1) * hd];
let mut scores = vec![0f32; t_len];
for (t, sc) in scores.iter_mut().enumerate() {
let kh = &state.k[l][t * kvw + kvh * hd..t * kvw + kvh * hd + hd];
*sc = qh.iter().zip(kh).map(|(a, b)| a * b).sum::<f32>() * scale;
}
softmax_inplace(&mut scores);
let out = &mut attn[head * hd..(head + 1) * hd];
for (t, &w) in scores.iter().enumerate() {
let vh = &state.v[l][t * kvw + kvh * hd..t * kvw + kvh * hd + hd];
for (dd, ov) in out.iter_mut().enumerate() {
*ov += w * vh[dd];
}
}
}
let o = pw_mul(&layer.o, &attn);
for j in 0..h {
x[j] += o[j];
}
let xn = rms_norm(&x, &layer.post_ln, d.eps);
let g = pw_mul(&layer.gate, &xn);
let u = pw_mul(&layer.up, &xn);
let act: Vec<f32> = g.iter().zip(&u).map(|(gv, uv)| silu(*gv) * uv).collect();
let dn = pw_mul(&layer.down, &act);
for j in 0..h {
x[j] += dn[j];
}
}
state.len += 1;
rms_norm(&x, &self.final_norm, d.eps)
}
fn forward_hidden(&self, embeds: &[f32], pos0: u32) -> Vec<f32> {
let d = self.dims;
let (h, hd, qw, kvw) = (d.h, d.hd, d.heads * d.hd, d.kv_heads * d.hd);
let group = d.heads / d.kv_heads;
let seq = embeds.len() / h;
let scale = 1.0 / (hd as f32).sqrt();
let tables: Vec<(Vec<f32>, Vec<f32>)> =
(0..seq).map(|s| self.rope_at(pos0 + s as u32)).collect();
let mut x = embeds.to_vec();
for layer in &self.layers {
let mut q = vec![0f32; seq * qw];
let mut k = vec![0f32; seq * kvw];
let mut v = vec![0f32; seq * kvw];
for s in 0..seq {
let xn = rms_norm(&x[s * h..(s + 1) * h], &layer.input_ln, d.eps);
let qs = pw_mul(&layer.q, &xn);
let ks = pw_mul(&layer.k, &xn);
let vs = pw_mul(&layer.v, &xn);
q[s * qw..(s + 1) * qw].copy_from_slice(&qs);
k[s * kvw..(s + 1) * kvw].copy_from_slice(&ks);
v[s * kvw..(s + 1) * kvw].copy_from_slice(&vs);
let (cos, sin) = &tables[s];
for head in 0..d.heads {
let sl = &mut q[s * qw + head * hd..s * qw + (head + 1) * hd];
sl.copy_from_slice(&rms_norm(sl, &layer.q_norm, d.eps));
}
for head in 0..d.kv_heads {
let sl = &mut k[s * kvw + head * hd..s * kvw + (head + 1) * hd];
sl.copy_from_slice(&rms_norm(sl, &layer.k_norm, d.eps));
}
apply_rope(&mut q[s * qw..(s + 1) * qw], cos, sin, d.heads, hd);
apply_rope(&mut k[s * kvw..(s + 1) * kvw], cos, sin, d.kv_heads, hd);
}
let mut attn = vec![0f32; seq * qw];
for head in 0..d.heads {
let kvh = head / group;
for s in 0..seq {
let qh = &q[s * qw + head * hd..s * qw + (head + 1) * hd];
let mut scores = vec![0f32; s + 1];
for (t, sc) in scores.iter_mut().enumerate() {
let kh = &k[t * kvw + kvh * hd..t * kvw + kvh * hd + hd];
*sc = qh.iter().zip(kh).map(|(a, b)| a * b).sum::<f32>() * scale;
}
softmax_inplace(&mut scores);
let out = &mut attn[s * qw + head * hd..s * qw + (head + 1) * hd];
for (t, &w) in scores.iter().enumerate() {
let vh = &v[t * kvw + kvh * hd..t * kvw + kvh * hd + hd];
for (dd, ov) in out.iter_mut().enumerate() {
*ov += w * vh[dd];
}
}
}
}
for s in 0..seq {
let o = pw_mul(&layer.o, &attn[s * qw..(s + 1) * qw]);
for j in 0..h {
x[s * h + j] += o[j];
}
let xn = rms_norm(&x[s * h..(s + 1) * h], &layer.post_ln, d.eps);
let g = pw_mul(&layer.gate, &xn);
let u = pw_mul(&layer.up, &xn);
let act: Vec<f32> = g.iter().zip(&u).map(|(gv, uv)| silu(*gv) * uv).collect();
let dn = pw_mul(&layer.down, &act);
for j in 0..h {
x[s * h + j] += dn[j];
}
}
}
(0..seq)
.flat_map(|s| rms_norm(&x[s * h..(s + 1) * h], &self.final_norm, d.eps))
.collect()
}
}
pub(crate) mod chatterbox_st {
use anyhow::Result;
use std::path::Path;
pub struct St(crate::chatterbox::StReader);
impl St {
pub fn open(path: &Path) -> Result<Self> {
Ok(Self(crate::chatterbox::StReader::open(path)?))
}
pub fn f32(&self, name: &str) -> Result<Vec<f32>> {
self.0.f32(name)
}
pub fn shape(&self, name: &str) -> Result<&[usize]> {
self.0.shape(name)
}
pub fn mat(&self, name: &str, rows: usize, cols: usize) -> Result<Vec<f32>> {
let s = self.0.shape(name)?;
anyhow::ensure!(s == [rows, cols], "{name}: shape {s:?} != [{rows}, {cols}]");
self.0.f32(name)
}
}
}
pub struct Talker {
stack: Stack,
text_embedding: Vec<f32>,
codec_embedding: Vec<f32>,
text_fc1_w: PackedWeight, text_fc1_b: Vec<f32>,
text_fc2_w: PackedWeight, text_fc2_b: Vec<f32>,
codec_head: PackedWeight, h: usize,
th: usize,
}
impl Talker {
pub fn load(dir: &Path, config: &Qwen3TtsConfig) -> Result<Self> {
let t = &config.top.talker_config;
let st = chatterbox_st::St::open(&dir.join(cfg::FILE_MODEL))?;
let dims = StackDims {
h: t.hidden_size,
layers: t.num_hidden_layers,
heads: t.num_attention_heads,
kv_heads: t.num_key_value_heads,
hd: t.head_dim,
inter: t.intermediate_size,
eps: t.rms_norm_eps,
theta: t.rope_theta,
};
let stack = Stack::load(&st, "talker.model", dims, Some(t.mrope_section))?;
let h = t.hidden_size;
let th = t.text_hidden_size;
Ok(Self {
stack,
text_embedding: st.mat("talker.model.text_embedding.weight", t.text_vocab_size, th)?,
codec_embedding: st.mat("talker.model.codec_embedding.weight", t.vocab_size, h)?,
text_fc1_w: pack(
st.mat("talker.text_projection.linear_fc1.weight", th, th)?,
th,
th,
),
text_fc1_b: st.f32("talker.text_projection.linear_fc1.bias")?,
text_fc2_w: pack(
st.mat("talker.text_projection.linear_fc2.weight", h, th)?,
h,
th,
),
text_fc2_b: st.f32("talker.text_projection.linear_fc2.bias")?,
codec_head: pack(
st.mat("talker.codec_head.weight", t.vocab_size, h)?,
t.vocab_size,
h,
),
h,
th,
})
}
pub fn embed_text(&self, id: u32) -> Vec<f32> {
let e = &self.text_embedding[id as usize * self.th..(id as usize + 1) * self.th];
self.project_text(e)
}
pub fn project_text(&self, e: &[f32]) -> Vec<f32> {
let mut a = vec![0f32; self.th];
crate::cpu_gemm::gemm_packed(&mut a, e, &self.text_fc1_w, 1, Some(&self.text_fc1_b));
for v in a.iter_mut() {
*v = silu(*v);
}
let mut o = vec![0f32; self.h];
crate::cpu_gemm::gemm_packed(&mut o, &a, &self.text_fc2_w, 1, Some(&self.text_fc2_b));
o
}
pub fn embed_codec(&self, id: u32) -> &[f32] {
&self.codec_embedding[id as usize * self.h..(id as usize + 1) * self.h]
}
pub fn new_state(&self) -> TalkerState {
self.stack.new_state()
}
pub fn step(&self, state: &mut TalkerState, embed: &[f32], pos: u32) -> Vec<f32> {
self.stack.step(state, embed, pos)
}
pub fn prefill(&self, state: &mut TalkerState, embeds: &[f32]) -> Vec<f32> {
let seq = embeds.len() / self.h;
let mut last = Vec::new();
for s in 0..seq {
let pos = state.len as u32;
last = self
.stack
.step(state, &embeds[s * self.h..(s + 1) * self.h], pos);
}
last
}
pub fn forward_hidden(&self, embeds: &[f32], pos0: u32) -> Vec<f32> {
self.stack.forward_hidden(embeds, pos0)
}
pub fn codec_logits(&self, hidden: &[f32]) -> Vec<f32> {
pw_mul(&self.codec_head, hidden)
}
pub fn hidden_size(&self) -> usize {
self.h
}
}
pub fn mrope_uniform_tables(
hd: usize,
theta: f32,
section: [usize; 3],
pos: u32,
) -> (Vec<f32>, Vec<f32>) {
let axes = mrope_axis_map(section, hd / 2);
mrope_cs(hd, theta, &axes, [pos, pos, pos])
}
pub fn rope_1d_tables(hd: usize, theta: f32, pos: u32) -> (Vec<f32>, Vec<f32>) {
rope_cs_1d(hd, theta, pos)
}
#[derive(Debug, Clone, Copy)]
pub struct SamplingCfg {
pub temperature: f32,
pub top_k: usize,
pub top_p: f32,
pub repetition_penalty: f32,
}
impl SamplingCfg {
pub fn from_generation(g: &GenerationConfig) -> (Self, Self) {
(
Self {
temperature: g.temperature,
top_k: g.top_k,
top_p: g.top_p,
repetition_penalty: g.repetition_penalty,
},
Self {
temperature: g.subtalker_temperature,
top_k: g.subtalker_top_k,
top_p: g.subtalker_top_p,
repetition_penalty: 1.0,
},
)
}
}
pub struct CodePredictor {
stack: Stack,
tables: Vec<Vec<f32>>,
heads: Vec<PackedWeight>,
proj: Option<(PackedWeight, Vec<f32>)>,
talker_h: usize,
cp_h: usize,
}
impl CodePredictor {
pub fn load(dir: &Path, config: &Qwen3TtsConfig) -> Result<Self> {
let t = &config.top.talker_config;
let p = &t.code_predictor_config;
let st = chatterbox_st::St::open(&dir.join(cfg::FILE_MODEL))?;
let dims = StackDims {
h: p.hidden_size,
layers: p.num_hidden_layers,
heads: p.num_attention_heads,
kv_heads: p.num_key_value_heads,
hd: p.head_dim,
inter: p.intermediate_size,
eps: p.rms_norm_eps,
theta: p.rope_theta,
};
let stack = Stack::load(&st, "talker.code_predictor.model", dims, None)?;
let n = p.num_code_groups - 1;
let mut tables = Vec::with_capacity(n);
let mut heads = Vec::with_capacity(n);
for g in 0..n {
tables.push(st.mat(
&format!("talker.code_predictor.model.codec_embedding.{g}.weight"),
p.vocab_size,
t.hidden_size,
)?);
heads.push(pack(
st.mat(
&format!("talker.code_predictor.lm_head.{g}.weight"),
p.vocab_size,
p.hidden_size,
)?,
p.vocab_size,
p.hidden_size,
));
}
let proj = if p.hidden_size == t.hidden_size {
anyhow::ensure!(
st.shape("talker.code_predictor.small_to_mtp_projection.weight")
.is_err(),
"cp_h == talker_h but a small_to_mtp_projection exists — layout drift"
);
None
} else {
Some((
pack(
st.mat(
"talker.code_predictor.small_to_mtp_projection.weight",
p.hidden_size,
t.hidden_size,
)?,
p.hidden_size,
t.hidden_size,
),
st.f32("talker.code_predictor.small_to_mtp_projection.bias")?,
))
};
Ok(Self {
stack,
tables,
heads,
proj,
talker_h: t.hidden_size,
cp_h: p.hidden_size,
})
}
fn project(&self, x: &[f32]) -> Vec<f32> {
match &self.proj {
None => x.to_vec(),
Some((w, b)) => {
let mut o = vec![0f32; self.cp_h];
crate::cpu_gemm::gemm_packed(&mut o, x, w, 1, Some(b));
o
}
}
}
pub fn embed_group(&self, group: usize, code: u32) -> &[f32] {
debug_assert!((1..cfg::NUM_CODE_GROUPS).contains(&group));
let t = &self.tables[group - 1];
&t[code as usize * self.talker_h..(code as usize + 1) * self.talker_h]
}
pub fn predict_frame(
&self,
past_hidden: &[f32],
sem_embed: &[f32],
sampling: &SamplingCfg,
rng: &mut u64,
) -> Vec<u32> {
let mut state = self.stack.new_state();
self.stack.step(&mut state, &self.project(past_hidden), 0);
let mut h = self.stack.step(&mut state, &self.project(sem_embed), 1);
let mut codes = Vec::with_capacity(cfg::NUM_CODE_GROUPS - 1);
for k in 0..cfg::NUM_CODE_GROUPS - 1 {
let logits = pw_mul(&self.heads[k], &h);
let code = crate::sampling::sample(
&logits,
sampling.temperature,
Some(sampling.top_k),
sampling.top_p,
None,
rng,
);
codes.push(code);
if k + 1 < cfg::NUM_CODE_GROUPS - 1 {
let e = self.embed_group(k + 1, code).to_vec();
h = self
.stack
.step(&mut state, &self.project(&e), (k + 2) as u32);
}
}
codes
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FrameTrace {
pub semantic: u32,
pub acoustic: Vec<u32>,
}
#[allow(clippy::too_many_arguments)]
pub fn generate_frames(
talker: &Talker,
cp: &CodePredictor,
prompt_embeds: &[f32],
prompt_codec_ids: &[u32],
trailing_text: &[Vec<f32>],
tts_pad_embed: &[f32],
talker_sampling: &SamplingCfg,
subtalker_sampling: &SamplingCfg,
seed: u64,
max_frames: usize,
) -> Vec<FrameTrace> {
let mut frames = Vec::new();
generate_frames_cb(
talker,
cp,
prompt_embeds,
prompt_codec_ids,
trailing_text,
tts_pad_embed,
talker_sampling,
subtalker_sampling,
seed,
max_frames,
&mut |f| {
frames.push(f);
true
},
);
frames
}
#[allow(clippy::too_many_arguments)]
pub fn generate_frames_cb(
talker: &Talker,
cp: &CodePredictor,
prompt_embeds: &[f32],
prompt_codec_ids: &[u32],
trailing_text: &[Vec<f32>],
tts_pad_embed: &[f32],
talker_sampling: &SamplingCfg,
subtalker_sampling: &SamplingCfg,
seed: u64,
max_frames: usize,
on_frame: &mut dyn FnMut(FrameTrace) -> bool,
) {
const MIN_NEW_FRAMES: usize = 2;
let h = talker.hidden_size();
let mut rng = if seed == 0 { 0x9E3779B97F4A7C15 } else { seed };
let mut state = talker.new_state();
let mut last_hidden = talker.prefill(&mut state, prompt_embeds);
let mut emitted: Vec<u32> = Vec::new();
for step in 0..max_frames {
let mut logits = talker.codec_logits(&last_hidden);
crate::sampling::apply_penalties(
&mut logits,
prompt_codec_ids,
&emitted,
0.0,
0.0,
talker_sampling.repetition_penalty,
);
for id in cfg::CP_VOCAB..cfg::TALKER_CODEC_VOCAB {
if id as u32 != cfg::CODEC_EOS || emitted.len() < MIN_NEW_FRAMES {
logits[id] = f32::NEG_INFINITY;
}
}
let c0 = crate::sampling::sample(
&logits,
talker_sampling.temperature,
Some(talker_sampling.top_k),
talker_sampling.top_p,
None,
&mut rng,
);
emitted.push(c0);
if c0 == cfg::CODEC_EOS {
on_frame(FrameTrace {
semantic: c0,
acoustic: Vec::new(),
});
break;
}
let sem_embed = talker.embed_codec(c0).to_vec();
let acoustic = cp.predict_frame(&last_hidden, &sem_embed, subtalker_sampling, &mut rng);
let mut next = sem_embed.clone();
for (i, &code) in acoustic.iter().enumerate() {
let e = cp.embed_group(i + 1, code);
for (n, v) in next.iter_mut().zip(e) {
*n += *v;
}
}
let text_row: &[f32] = trailing_text
.get(step)
.map(|v| v.as_slice())
.unwrap_or(tts_pad_embed);
for (n, v) in next.iter_mut().zip(text_row) {
*n += *v;
}
debug_assert_eq!(next.len(), h);
if !on_frame(FrameTrace {
semantic: c0,
acoustic,
}) {
break;
}
let pos = state.len() as u32;
last_hidden = talker.step(&mut state, &next, pos);
}
}
pub struct TtsPrompt {
pub embeds: Vec<f32>,
pub trailing_text: Vec<Vec<f32>>,
pub tts_pad: Vec<f32>,
}
pub fn wrap_text_template(text: &str) -> String {
format!("<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n")
}
fn wrap_ref_template(text: &str) -> String {
format!("<|im_start|>assistant\n{text}<|im_end|>\n")
}
#[cfg(feature = "cli")]
#[allow(clippy::too_many_arguments)]
pub fn build_clone_prompt(
talker: &Talker,
cp: &CodePredictor,
tk: &TextTokenizer,
config: &Qwen3TtsConfig,
text: &str,
language: Option<&str>,
x_vector: Option<&[f32]>,
icl: Option<(&str, &[[u32; cfg::NUM_CODE_GROUPS]])>,
) -> Result<TtsPrompt> {
let lang_id = resolve_language(config, language)?;
let speaker_row = x_vector.map(|v| v.to_vec());
build_prompt_inner(
talker,
cp,
tk,
config,
text,
lang_id,
speaker_row,
icl,
None,
)
}
#[cfg(feature = "cli")]
pub fn build_preset_prompt(
talker: &Talker,
cp: &CodePredictor,
tk: &TextTokenizer,
config: &Qwen3TtsConfig,
text: &str,
language: Option<&str>,
speaker: &str,
instruct: Option<&str>,
) -> Result<TtsPrompt> {
let t = &config.top.talker_config;
let key = speaker.to_lowercase();
let spk_id = *t
.spk_id
.get(&key)
.with_context(|| format!("speaker {speaker:?} not implemented"))?;
let mut lang_id = resolve_language(config, language)?;
let auto_or_zh = matches!(
language.map(|l| l.to_lowercase()).as_deref(),
None | Some("auto") | Some("chinese")
);
if auto_or_zh && let Some(Some(dialect)) = t.spk_is_dialect.get(&key) {
lang_id = Some(
*t.codec_language_id
.get(dialect)
.with_context(|| format!("dialect {dialect:?} missing from codec_language_id"))?,
);
}
let speaker_row = Some(talker.embed_codec(spk_id).to_vec());
build_prompt_inner(
talker,
cp,
tk,
config,
text,
lang_id,
speaker_row,
None,
instruct,
)
}
#[cfg(feature = "cli")]
pub fn build_design_prompt(
talker: &Talker,
cp: &CodePredictor,
tk: &TextTokenizer,
config: &Qwen3TtsConfig,
text: &str,
language: Option<&str>,
instruct: &str,
) -> Result<TtsPrompt> {
let lang_id = resolve_language(config, language)?;
build_prompt_inner(
talker,
cp,
tk,
config,
text,
lang_id,
None,
None,
Some(instruct),
)
}
fn resolve_language(config: &Qwen3TtsConfig, language: Option<&str>) -> Result<Option<u32>> {
match language {
None => Ok(None),
Some(l) if l.eq_ignore_ascii_case("auto") => Ok(None),
Some(l) => Ok(Some(
*config
.top
.talker_config
.codec_language_id
.get(&l.to_lowercase())
.with_context(|| format!("language {l:?} not implemented"))?,
)),
}
}
#[cfg(feature = "cli")]
#[allow(clippy::too_many_arguments)]
fn build_prompt_inner(
talker: &Talker,
cp: &CodePredictor,
tk: &TextTokenizer,
config: &Qwen3TtsConfig,
text: &str,
lang_id: Option<u32>,
speaker_row: Option<Vec<f32>>,
icl: Option<(&str, &[[u32; cfg::NUM_CODE_GROUPS]])>,
instruct: Option<&str>,
) -> Result<TtsPrompt> {
let h = talker.hidden_size();
let _ = &config.top.talker_config;
let ids = tk.encode(&wrap_text_template(text))?;
anyhow::ensure!(
ids.len() >= 9,
"text tokenizes too short: {} ids",
ids.len()
);
let text_ids = &ids[3..ids.len() - 5];
let tts_bos = talker.embed_text(cfg::TTS_BOS);
let tts_eos = talker.embed_text(cfg::TTS_EOS);
let tts_pad = talker.embed_text(cfg::TTS_PAD);
let mut codec_rows: Vec<Vec<f32>> = Vec::new();
match lang_id {
None => {
for id in [
cfg::CODEC_NOTHINK,
cfg::CODEC_THINK_BOS,
cfg::CODEC_THINK_EOS,
] {
codec_rows.push(talker.embed_codec(id).to_vec());
}
}
Some(lang_id) => {
for id in [
cfg::CODEC_THINK,
cfg::CODEC_THINK_BOS,
lang_id,
cfg::CODEC_THINK_EOS,
] {
codec_rows.push(talker.embed_codec(id).to_vec());
}
}
}
if let Some(row) = speaker_row {
anyhow::ensure!(row.len() == h, "speaker row dim {} ≠ hidden {h}", row.len());
codec_rows.push(row);
}
codec_rows.push(talker.embed_codec(cfg::CODEC_PAD).to_vec());
codec_rows.push(talker.embed_codec(cfg::CODEC_BOS).to_vec());
let l = codec_rows.len();
let mut embeds: Vec<f32> = Vec::new();
if let Some(instr) = instruct
&& !instr.trim().is_empty()
{
for id in tk.encode(&format!("<|im_start|>user\n{instr}<|im_end|>\n"))? {
embeds.extend(talker.embed_text(id));
}
}
for &id in &ids[..3] {
embeds.extend(talker.embed_text(id));
}
for (j, crow) in codec_rows[..l - 1].iter().enumerate() {
let trow = if j < l - 2 { &tts_pad } else { &tts_bos };
embeds.extend(trow.iter().zip(crow).map(|(a, b)| a + b));
}
let trailing_text: Vec<Vec<f32>>;
match icl {
None => {
let first = talker.embed_text(text_ids[0]);
embeds.extend(first.iter().zip(&codec_rows[l - 1]).map(|(a, b)| a + b));
let mut rows: Vec<Vec<f32>> = text_ids[1..]
.iter()
.map(|&id| talker.embed_text(id))
.collect();
rows.push(tts_eos);
trailing_text = rows;
}
Some((ref_text, ref_codes)) => {
let rids = tk.encode(&wrap_ref_template(ref_text))?;
anyhow::ensure!(rids.len() >= 6, "ref text tokenizes too short");
let ref_ids = &rids[3..rids.len() - 2];
let mut text_rows: Vec<Vec<f32>> = ref_ids
.iter()
.chain(text_ids)
.map(|&id| talker.embed_text(id))
.collect();
text_rows.push(tts_eos);
let mut icl_codec: Vec<Vec<f32>> = vec![talker.embed_codec(cfg::CODEC_BOS).to_vec()];
for f in ref_codes {
let mut sum = talker.embed_codec(f[0]).to_vec();
for (g, &code) in f[1..].iter().enumerate() {
for (s, v) in sum.iter_mut().zip(cp.embed_group(g + 1, code)) {
*s += *v;
}
}
icl_codec.push(sum);
}
let (t1, t2) = (text_rows.len(), icl_codec.len());
if t1 > t2 {
for (trow, crow) in text_rows[..t2].iter().zip(&icl_codec) {
embeds.extend(trow.iter().zip(crow).map(|(a, b)| a + b));
}
trailing_text = text_rows[t2..].to_vec();
} else {
for (j, crow) in icl_codec.iter().enumerate() {
let trow = if j < t1 { &text_rows[j] } else { &tts_pad };
embeds.extend(trow.iter().zip(crow).map(|(a, b)| a + b));
}
trailing_text = Vec::new();
}
}
}
Ok(TtsPrompt {
embeds,
trailing_text,
tts_pad,
})
}