add qwen3 tts repo files

This commit is contained in:
Concedo 2026-02-21 10:54:55 +08:00
parent ad0618e351
commit 1af7095cb5
18 changed files with 7071 additions and 0 deletions

View file

@ -740,6 +740,8 @@ embeddingvk: examples/embedding/embedding.cpp common/arg.cpp common/speculative.
$(CXX) $(CXXFLAGS) -DGGML_USE_VULKAN -DSD_USE_VULKAN $(filter-out %.h,$^) -o $@ $(LDFLAGS)
ttscppmain: otherarch/ttscpp/cli/cli.cpp otherarch/ttscpp/cli/playback.cpp otherarch/ttscpp/cli/playback.h otherarch/ttscpp/cli/write_file.cpp otherarch/ttscpp/cli/write_file.h otherarch/ttscpp/cli/vad.cpp otherarch/ttscpp/cli/vad.h otherarch/ttscpp/src/ttscpp.cpp otherarch/ttscpp/src/ttstokenizer.cpp otherarch/ttscpp/src/ttssampler.cpp otherarch/ttscpp/src/parler_model.cpp otherarch/ttscpp/src/dac_model.cpp otherarch/ttscpp/src/ttsutil.cpp otherarch/ttscpp/src/ttsargs.cpp otherarch/ttscpp/src/ttst5_encoder_model.cpp otherarch/ttscpp/src/phonemizer.cpp otherarch/ttscpp/src/tts_model.cpp otherarch/ttscpp/src/kokoro_model.cpp otherarch/ttscpp/src/dia_model.cpp otherarch/ttscpp/src/orpheus_model.cpp otherarch/ttscpp/src/snac_model.cpp otherarch/ttscpp/src/general_neural_audio_codec.cpp ggml.o ggml-cpu.o ggml-ops.o ggml-vec.o ggml-binops.o ggml-unops.o llama.o console.o llavaclip_default.o llava.o ggml-backend_default.o ggml-backend-reg_default.o ggml-repack.o $(OBJS_FULL) $(OBJS)
$(CXX) $(CXXFLAGS) $(filter-out %.h,$^) -o $@ $(LDFLAGS)
qwen3tts: otherarch/qwen3tts/main.cpp otherarch/qwen3tts/qwen3_tts.cpp otherarch/qwen3tts/text_tokenizer.cpp otherarch/qwen3tts/gguf_loader.cpp otherarch/qwen3tts/tts_transformer.cpp otherarch/qwen3tts/audio_tokenizer_decoder.cpp otherarch/qwen3tts/audio_tokenizer_encoder.cpp otherarch/qwen3tts/coreml_code_predictor_stub.cpp ggml.o ggml-cpu.o ggml-ops.o ggml-vec.o ggml-binops.o ggml-unops.o llama.o console.o llavaclip_default.o llava.o ggml-backend_default.o ggml-backend-reg_default.o ggml-repack.o $(OBJS_FULL) $(OBJS)
$(CXX) $(CXXFLAGS) $(filter-out %.h,$^) -o $@ $(LDFLAGS)
ggml/src/ggml-vulkan-shaders.cpp:
ifdef VULKAN_BUILD

View file

@ -184,6 +184,7 @@ and it will install everything required. Alternatively, you can download the abo
- Llama.cpp source repo is at https://github.com/ggml-org/llama.cpp (MIT)
- Stable-diffusion.cpp source repo is at https://github.com/leejet/stable-diffusion.cpp (MIT)
- TTS.cpp source repo is at https://github.com/mmwillet/TTS.cpp (MIT)
- Qwen3TTS source repo is at https://github.com/predict-woo/qwen3-tts.cpp (MIT)
- KoboldCpp source repo is at https://github.com/LostRuins/koboldcpp (AGPL)
- KoboldAI Lite source repo is at https://github.com/LostRuins/lite.koboldai.net (AGPL)
- For any further enquiries, contact @concedo on discord, or LostRuins on github.

View file

@ -0,0 +1,24 @@
The original Qwen3TTS.cpp is made by predict-woo, repo can be found at https://github.com/predict-woo/qwen3-tts.cpp
MIT License was granted at https://github.com/predict-woo/qwen3-tts.cpp/issues/4
MIT License
Copyright (c) 2023-2026 The ggml authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,893 @@
#include "audio_tokenizer_decoder.h"
#include "gguf_loader.h"
#include "ggml-cpu.h"
#include <cmath>
#include <cstring>
#include <algorithm>
#include <numeric>
#define QWEN3_TTS_DEC_MAX_NODES 32768
namespace qwen3_tts {
AudioTokenizerDecoder::AudioTokenizerDecoder() = default;
AudioTokenizerDecoder::~AudioTokenizerDecoder() {
unload_model();
}
void AudioTokenizerDecoder::unload_model() {
free_audio_decoder_model(model_);
if (state_.sched) {
ggml_backend_sched_free(state_.sched);
state_.sched = nullptr;
}
if (state_.backend) {
release_preferred_backend(state_.backend);
state_.backend = nullptr;
}
if (state_.backend_cpu) {
ggml_backend_free(state_.backend_cpu);
state_.backend_cpu = nullptr;
}
state_.compute_meta.clear();
codes_buf_.clear();
}
void AudioTokenizerDecoder::normalize_codebooks() {
const float epsilon = 1e-5f;
auto normalize_codebook = [epsilon](struct ggml_tensor * codebook, struct ggml_tensor * usage, const char *) {
if (!codebook || !usage || !codebook->data || !usage->data) return;
int64_t codebook_dim = codebook->ne[0];
int64_t codebook_size = codebook->ne[1];
ggml_fp16_t * cb_data = (ggml_fp16_t *)codebook->data;
float * usage_data = (float *)usage->data;
for (int64_t emb_idx = 0; emb_idx < codebook_size; ++emb_idx) {
float u = usage_data[emb_idx];
if (u < epsilon) u = epsilon;
float inv_u = 1.0f / u;
for (int64_t dim_idx = 0; dim_idx < codebook_dim; ++dim_idx) {
int64_t mem_idx = dim_idx + emb_idx * codebook_dim;
float val = ggml_fp16_to_fp32(cb_data[mem_idx]);
cb_data[mem_idx] = ggml_fp32_to_fp16(val * inv_u);
}
}
};
normalize_codebook(model_.vq_first_codebook, model_.vq_first_usage, "first");
for (int i = 0; i < 15; ++i) {
char name[16];
snprintf(name, sizeof(name), "rest%d", i);
normalize_codebook(model_.vq_rest_codebook[i], model_.vq_rest_usage[i], name);
}
}
bool AudioTokenizerDecoder::load_model(const std::string & model_path) {
unload_model();
GGUFLoader loader;
if (!loader.open(model_path)) {
error_msg_ = loader.get_error();
return false;
}
model_.config.sample_rate = loader.get_u32("qwen3-tts.tokenizer.sample_rate", 24000);
model_.config.n_codebooks = loader.get_u32("qwen3-tts.tokenizer.num_codebooks", 16);
model_.config.codebook_size = loader.get_u32("qwen3-tts.tokenizer.codebook_size", 2048);
int64_t n_tensors = loader.get_n_tensors();
int dec_tensor_count = 0;
for (int64_t i = 0; i < n_tensors; ++i) {
const char * name = loader.get_tensor_name(i);
if (name && strncmp(name, "tok_dec.", 8) == 0) {
dec_tensor_count++;
}
}
if (dec_tensor_count == 0) {
error_msg_ = "No decoder tensors found in model";
return false;
}
size_t ctx_size = ggml_tensor_overhead() * dec_tensor_count;
struct ggml_init_params params = {
/*.mem_size =*/ ctx_size,
/*.mem_buffer =*/ nullptr,
/*.no_alloc =*/ true,
};
model_.ctx = ggml_init(params);
if (!model_.ctx) {
error_msg_ = "Failed to initialize GGML context";
return false;
}
struct gguf_context * gguf_ctx = loader.get_ctx();
struct ggml_context * meta_ctx = loader.get_meta_ctx();
for (int64_t i = 0; i < n_tensors; ++i) {
const char * name = loader.get_tensor_name(i);
if (!name || strncmp(name, "tok_dec.", 8) != 0) {
continue;
}
struct ggml_tensor * meta_tensor = ggml_get_tensor(meta_ctx, name);
if (!meta_tensor) {
continue;
}
struct ggml_tensor * tensor = ggml_dup_tensor(model_.ctx, meta_tensor);
ggml_set_name(tensor, name);
model_.tensors[name] = tensor;
std::string sname(name);
if (sname == "tok_dec.vq_first.input_proj.weight") model_.vq_first_input_proj = tensor;
else if (sname == "tok_dec.vq_first.output_proj.weight") model_.vq_first_output_proj = tensor;
else if (sname == "tok_dec.vq_first.0.codebook") model_.vq_first_codebook = tensor;
else if (sname == "tok_dec.vq_first.0.usage") model_.vq_first_usage = tensor;
else if (sname == "tok_dec.vq_rest.input_proj.weight") model_.vq_rest_input_proj = tensor;
else if (sname == "tok_dec.vq_rest.output_proj.weight") model_.vq_rest_output_proj = tensor;
else if (sname == "tok_dec.pre_conv.weight") model_.pre_conv_w = tensor;
else if (sname == "tok_dec.pre_conv.bias") model_.pre_conv_b = tensor;
else if (sname == "tok_dec.pre_tfm.input_proj.weight") model_.pre_tfm_input_proj_w = tensor;
else if (sname == "tok_dec.pre_tfm.input_proj.bias") model_.pre_tfm_input_proj_b = tensor;
else if (sname == "tok_dec.pre_tfm.norm.weight") model_.pre_tfm_norm_w = tensor;
else if (sname == "tok_dec.pre_tfm.output_proj.weight") model_.pre_tfm_output_proj_w = tensor;
else if (sname == "tok_dec.pre_tfm.output_proj.bias") model_.pre_tfm_output_proj_b = tensor;
else if (sname == "tok_dec.dec.0.conv.weight") model_.dec0_conv_w = tensor;
else if (sname == "tok_dec.dec.0.conv.bias") model_.dec0_conv_b = tensor;
else if (sname == "tok_dec.dec.5.snake.alpha") model_.dec5_snake_alpha = tensor;
else if (sname == "tok_dec.dec.5.snake.beta") model_.dec5_snake_beta = tensor;
else if (sname == "tok_dec.dec.6.conv.weight") model_.dec6_conv_w = tensor;
else if (sname == "tok_dec.dec.6.conv.bias") model_.dec6_conv_b = tensor;
else if (sname.find("pre_tfm.blk.") != std::string::npos) {
int blk_idx;
if (sscanf(name, "tok_dec.pre_tfm.blk.%d.", &blk_idx) == 1 && blk_idx >= 0 && blk_idx < 8) {
if (sname.find(".attn_v.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].attn_v_w = tensor;
else if (sname.find(".ffn_gate.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].ffn_gate_w = tensor;
else if (sname.find(".attn_norm.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].attn_norm_w = tensor;
else if (sname.find(".attn_q.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].attn_q_w = tensor;
else if (sname.find(".attn_k.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].attn_k_w = tensor;
else if (sname.find(".attn_output.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].attn_output_w = tensor;
else if (sname.find(".attn_scale") != std::string::npos) model_.pre_tfm_layers[blk_idx].attn_scale = tensor;
else if (sname.find(".ffn_norm.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].ffn_norm_w = tensor;
else if (sname.find(".ffn_up.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].ffn_up_w = tensor;
else if (sname.find(".ffn_down.weight") != std::string::npos) model_.pre_tfm_layers[blk_idx].ffn_down_w = tensor;
else if (sname.find(".ffn_scale") != std::string::npos) model_.pre_tfm_layers[blk_idx].ffn_scale = tensor;
}
}
else {
int blk_idx, res_idx, cb_idx, n = 0;
char suffix[64];
size_t name_len = strlen(name);
#define MATCH1(fmt, var) (sscanf(name, fmt "%n", &var, &n) == 1 && (size_t)n == name_len)
#define MATCH2(fmt, v1, v2) (sscanf(name, fmt "%n", &v1, &v2, &n) == 2 && (size_t)n == name_len)
#define MATCH1S(fmt, var, suf) (sscanf(name, fmt, &var, suf) == 2)
if (MATCH1("tok_dec.vq_rest.%d.codebook", cb_idx)) {
if (cb_idx >= 0 && cb_idx < 15) {
model_.vq_rest_codebook[cb_idx] = tensor;
}
}
else if (MATCH1("tok_dec.vq_rest.%d.usage", cb_idx)) {
if (cb_idx >= 0 && cb_idx < 15) {
model_.vq_rest_usage[cb_idx] = tensor;
}
}
else if (MATCH1S("tok_dec.upsample.%d.conv.%63s", blk_idx, suffix)) {
if (blk_idx >= 0 && blk_idx < 2) {
if (strcmp(suffix, "weight") == 0) model_.upsample[blk_idx].conv_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.upsample[blk_idx].conv_b = tensor;
}
}
else if (MATCH1S("tok_dec.upsample.%d.dwconv.%63s", blk_idx, suffix)) {
if (blk_idx >= 0 && blk_idx < 2) {
if (strcmp(suffix, "weight") == 0) model_.upsample[blk_idx].dwconv_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.upsample[blk_idx].dwconv_b = tensor;
}
}
else if (MATCH1S("tok_dec.upsample.%d.norm.%63s", blk_idx, suffix)) {
if (blk_idx >= 0 && blk_idx < 2) {
if (strcmp(suffix, "weight") == 0) model_.upsample[blk_idx].norm_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.upsample[blk_idx].norm_b = tensor;
}
}
else if (MATCH1S("tok_dec.upsample.%d.pwconv1.%63s", blk_idx, suffix)) {
if (blk_idx >= 0 && blk_idx < 2) {
if (strcmp(suffix, "weight") == 0) model_.upsample[blk_idx].pwconv1_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.upsample[blk_idx].pwconv1_b = tensor;
}
}
else if (MATCH1S("tok_dec.upsample.%d.pwconv2.%63s", blk_idx, suffix)) {
if (blk_idx >= 0 && blk_idx < 2) {
if (strcmp(suffix, "weight") == 0) model_.upsample[blk_idx].pwconv2_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.upsample[blk_idx].pwconv2_b = tensor;
}
}
else if (MATCH1("tok_dec.upsample.%d.gamma", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 2) model_.upsample[blk_idx].gamma = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.attn_norm.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].attn_norm_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.attn_q.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].attn_q_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.attn_k.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].attn_k_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.attn_v.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].attn_v_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.attn_output.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].attn_output_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.attn_scale", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].attn_scale = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.ffn_norm.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].ffn_norm_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.ffn_gate.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].ffn_gate_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.ffn_up.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].ffn_up_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.ffn_down.weight", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].ffn_down_w = tensor;
}
else if (MATCH1("tok_dec.pre_tfm.blk.%d.ffn_scale", blk_idx)) {
if (blk_idx >= 0 && blk_idx < 8) model_.pre_tfm_layers[blk_idx].ffn_scale = tensor;
}
else if (MATCH1("tok_dec.dec.%d.snake.alpha", blk_idx)) {
if (blk_idx >= 1 && blk_idx <= 4) model_.dec_blocks[blk_idx-1].snake_alpha = tensor;
}
else if (MATCH1("tok_dec.dec.%d.snake.beta", blk_idx)) {
if (blk_idx >= 1 && blk_idx <= 4) model_.dec_blocks[blk_idx-1].snake_beta = tensor;
}
else if (MATCH1("tok_dec.dec.%d.conv_t.weight", blk_idx)) {
if (blk_idx >= 1 && blk_idx <= 4) model_.dec_blocks[blk_idx-1].conv_t_w = tensor;
}
else if (MATCH1("tok_dec.dec.%d.conv_t.bias", blk_idx)) {
if (blk_idx >= 1 && blk_idx <= 4) model_.dec_blocks[blk_idx-1].conv_t_b = tensor;
}
else if (MATCH2("tok_dec.dec.%d.res.%d.act1.alpha", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].act1_alpha = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.act1.beta", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].act1_beta = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.conv1.weight", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].conv1_w = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.conv1.bias", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].conv1_b = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.act2.alpha", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].act2_alpha = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.act2.beta", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].act2_beta = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.conv2.weight", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].conv2_w = tensor;
}
}
else if (MATCH2("tok_dec.dec.%d.res.%d.conv2.bias", blk_idx, res_idx)) {
if (blk_idx >= 1 && blk_idx <= 4 && res_idx >= 2 && res_idx <= 4) {
model_.dec_blocks[blk_idx-1].res[res_idx-2].conv2_b = tensor;
}
}
#undef MATCH1
#undef MATCH2
#undef MATCH1S
}
}
if (!load_tensor_data_from_file(model_path, gguf_ctx, model_.ctx,
model_.tensors, model_.buffer, error_msg_,
GGML_BACKEND_DEVICE_TYPE_IGPU)) {
return false;
}
for (int i = 0; i < 4; ++i) {
model_.dec_blocks[i].res[0].dilation = 1;
model_.dec_blocks[i].res[1].dilation = 3;
model_.dec_blocks[i].res[2].dilation = 9;
}
normalize_codebooks();
// Codebooks are normalized in host memory; sync once to backend tensors.
auto upload_if_present = [](struct ggml_tensor * t) {
if (t && t->data) {
ggml_backend_tensor_set(t, t->data, 0, ggml_nbytes(t));
}
};
upload_if_present(model_.vq_first_codebook);
for (int i = 0; i < 15; ++i) {
upload_if_present(model_.vq_rest_codebook[i]);
}
state_.backend = init_preferred_backend("AudioTokenizerDecoder", &error_msg_);
if (!state_.backend) {
return false;
}
ggml_backend_dev_t device = ggml_backend_get_device(state_.backend);
const char * device_name = device ? ggml_backend_dev_name(device) : "Unknown";
fprintf(stderr, " AudioTokenizerDecoder backend: %s\n", device_name);
if (device && ggml_backend_dev_type(device) != GGML_BACKEND_DEVICE_TYPE_CPU) {
state_.backend_cpu = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);
if (!state_.backend_cpu) {
error_msg_ = "Failed to initialize CPU fallback backend for AudioTokenizerDecoder";
return false;
}
}
std::vector<ggml_backend_t> backends;
backends.push_back(state_.backend);
if (state_.backend_cpu) {
backends.push_back(state_.backend_cpu);
}
state_.sched = ggml_backend_sched_new(backends.data(), nullptr, (int)backends.size(), QWEN3_TTS_DEC_MAX_NODES, false, true);
if (!state_.sched) {
error_msg_ = "Failed to create backend scheduler";
return false;
}
state_.compute_meta.resize(ggml_tensor_overhead() * QWEN3_TTS_DEC_MAX_NODES + ggml_graph_overhead());
return true;
}
struct ggml_tensor * AudioTokenizerDecoder::apply_snake(struct ggml_context * ctx,
struct ggml_tensor * x,
struct ggml_tensor * alpha,
struct ggml_tensor * beta) {
int64_t seq_len = x->ne[0];
int64_t channels = x->ne[1];
int64_t batch = x->ne[2];
struct ggml_tensor * alpha_exp = ggml_exp(ctx, alpha);
struct ggml_tensor * alpha_3d = ggml_reshape_3d(ctx, alpha_exp, 1, channels, 1);
struct ggml_tensor * alpha_broad = ggml_repeat(ctx, alpha_3d,
ggml_new_tensor_3d(ctx, GGML_TYPE_F32, seq_len, channels, batch));
struct ggml_tensor * ax = ggml_mul(ctx, x, alpha_broad);
struct ggml_tensor * sin_ax = ggml_sin(ctx, ax);
struct ggml_tensor * sin_sq = ggml_sqr(ctx, sin_ax);
struct ggml_tensor * neg_beta = ggml_scale(ctx, beta, -1.0f);
struct ggml_tensor * inv_beta_exp = ggml_exp(ctx, neg_beta);
struct ggml_tensor * inv_beta_3d = ggml_reshape_3d(ctx, inv_beta_exp, 1, channels, 1);
struct ggml_tensor * inv_beta = ggml_repeat(ctx, inv_beta_3d,
ggml_new_tensor_3d(ctx, GGML_TYPE_F32, seq_len, channels, batch));
struct ggml_tensor * scaled_sin = ggml_mul(ctx, sin_sq, inv_beta);
return ggml_add(ctx, x, scaled_sin);
}
struct ggml_tensor * AudioTokenizerDecoder::apply_rms_norm(struct ggml_context * ctx,
struct ggml_tensor * x,
struct ggml_tensor * w,
float eps) {
struct ggml_tensor * normed = ggml_rms_norm(ctx, x, eps);
return ggml_mul(ctx, normed, w);
}
struct ggml_tensor * AudioTokenizerDecoder::apply_pre_tfm_layer(struct ggml_context * ctx,
struct ggml_tensor * x,
const pre_tfm_layer & layer,
int32_t n_frames,
struct ggml_tensor * positions) {
const auto & cfg = model_.config;
const int n_heads = cfg.n_heads;
const int qkv_dim = cfg.latent_dim;
const int head_dim = qkv_dim / n_heads;
if (!layer.attn_norm_w || !layer.attn_q_w || !layer.attn_k_w || !layer.attn_v_w ||
!layer.attn_output_w || !layer.ffn_norm_w || !layer.ffn_gate_w ||
!layer.ffn_up_w || !layer.ffn_down_w) {
return x;
}
struct ggml_tensor * residual = x;
struct ggml_tensor * normed = apply_rms_norm(ctx, x, layer.attn_norm_w, cfg.rms_norm_eps);
struct ggml_tensor * Qcur = ggml_mul_mat(ctx, layer.attn_q_w, normed);
struct ggml_tensor * Kcur = ggml_mul_mat(ctx, layer.attn_k_w, normed);
struct ggml_tensor * Vcur = ggml_mul_mat(ctx, layer.attn_v_w, normed);
Qcur = ggml_reshape_3d(ctx, Qcur, head_dim, n_heads, n_frames);
Kcur = ggml_reshape_3d(ctx, Kcur, head_dim, n_heads, n_frames);
Vcur = ggml_reshape_3d(ctx, Vcur, head_dim, n_heads, n_frames);
Qcur = ggml_rope_ext(ctx, Qcur, positions, nullptr,
head_dim, GGML_ROPE_TYPE_NEOX, 0,
cfg.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
Kcur = ggml_rope_ext(ctx, Kcur, positions, nullptr,
head_dim, GGML_ROPE_TYPE_NEOX, 0,
cfg.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
struct ggml_tensor * Q = ggml_permute(ctx, Qcur, 0, 2, 1, 3);
struct ggml_tensor * K = ggml_permute(ctx, Kcur, 0, 2, 1, 3);
struct ggml_tensor * V = ggml_permute(ctx, Vcur, 0, 2, 1, 3);
struct ggml_tensor * KQ = ggml_mul_mat(ctx, K, Q);
KQ = ggml_scale(ctx, KQ, 1.0f / sqrtf((float)head_dim));
// Apply causal mask (each position can only attend to itself and previous positions)
KQ = ggml_diag_mask_inf(ctx, KQ, 0);
KQ = ggml_soft_max(ctx, KQ);
V = ggml_cont(ctx, ggml_transpose(ctx, V));
struct ggml_tensor * KQV = ggml_mul_mat(ctx, V, KQ);
KQV = ggml_permute(ctx, KQV, 0, 2, 1, 3);
struct ggml_tensor * attn_out = ggml_cont_2d(ctx, KQV, n_heads * head_dim, n_frames);
attn_out = ggml_mul_mat(ctx, layer.attn_output_w, attn_out);
if (layer.attn_scale) {
attn_out = ggml_mul(ctx, attn_out, layer.attn_scale);
}
x = ggml_add(ctx, residual, attn_out);
residual = x;
normed = apply_rms_norm(ctx, x, layer.ffn_norm_w, cfg.rms_norm_eps);
struct ggml_tensor * gate = ggml_mul_mat(ctx, layer.ffn_gate_w, normed);
struct ggml_tensor * up = ggml_mul_mat(ctx, layer.ffn_up_w, normed);
gate = ggml_silu(ctx, gate);
struct ggml_tensor * ffn_out = ggml_mul(ctx, gate, up);
ffn_out = ggml_mul_mat(ctx, layer.ffn_down_w, ffn_out);
if (layer.ffn_scale) {
ffn_out = ggml_mul(ctx, ffn_out, layer.ffn_scale);
}
return ggml_add(ctx, residual, ffn_out);
}
struct ggml_tensor * AudioTokenizerDecoder::apply_upsample_block(struct ggml_context * ctx,
struct ggml_tensor * x,
const upsample_block & block,
int block_idx) {
int64_t seq_len = x->ne[0];
int64_t channels = x->ne[1];
struct ggml_tensor * x_2d = ggml_reshape_2d(ctx, x, seq_len, channels);
x_2d = ggml_conv_transpose_1d(ctx, block.conv_w, x_2d, 2, 0, 1);
int64_t new_seq_len = x_2d->ne[0];
x = ggml_reshape_3d(ctx, x_2d, new_seq_len, channels, 1);
if (block.conv_b) {
x = ggml_add(ctx, x, ggml_reshape_3d(ctx, block.conv_b, 1, channels, 1));
}
struct ggml_tensor * residual = x;
if (block.dwconv_w) {
// Causal padding: pad left with 6 zeros (kernel_size - 1 = 7 - 1 = 6)
x = ggml_pad_ext(ctx, x, 6, 0, 0, 0, 0, 0, 0, 0); // left pad only
x = ggml_conv_1d_dw(ctx, block.dwconv_w, x, 1, 0, 1); // no padding in conv
if (block.dwconv_b) {
x = ggml_add(ctx, x, ggml_reshape_3d(ctx, block.dwconv_b, 1, channels, 1));
}
}
x = ggml_permute(ctx, x, 1, 0, 2, 3);
x = ggml_cont(ctx, x);
if (block.norm_w && block.norm_b) {
x = ggml_norm(ctx, x, 1e-6f);
x = ggml_mul(ctx, x, block.norm_w);
x = ggml_add(ctx, x, block.norm_b);
}
x = ggml_mul_mat(ctx, block.pwconv1_w, x);
if (block.pwconv1_b) {
x = ggml_add(ctx, x, block.pwconv1_b);
}
x = ggml_gelu(ctx, x);
x = ggml_mul_mat(ctx, block.pwconv2_w, x);
if (block.pwconv2_b) {
x = ggml_add(ctx, x, block.pwconv2_b);
}
x = ggml_permute(ctx, x, 1, 0, 2, 3);
x = ggml_cont(ctx, x);
if (block.gamma) {
struct ggml_tensor * gamma_3d = ggml_reshape_3d(ctx, block.gamma, 1, channels, 1);
x = ggml_mul(ctx, x, ggml_repeat(ctx, gamma_3d,
ggml_new_tensor_3d(ctx, GGML_TYPE_F32, new_seq_len, channels, 1)));
}
return ggml_add(ctx, residual, x);
}
struct ggml_tensor * AudioTokenizerDecoder::apply_residual_block(struct ggml_context * ctx,
struct ggml_tensor * x,
const residual_block & block) {
struct ggml_tensor * residual = x;
if (block.act1_alpha) {
x = apply_snake(ctx, x, block.act1_alpha, block.act1_beta);
}
int64_t out_channels = block.conv1_w->ne[2];
int padding = 6 * block.dilation;
x = ggml_pad_ext(ctx, x, padding, 0, 0, 0, 0, 0, 0, 0);
x = ggml_conv_1d(ctx, block.conv1_w, x, 1, 0, block.dilation);
if (block.conv1_b) {
x = ggml_add(ctx, x, ggml_reshape_3d(ctx, block.conv1_b, 1, out_channels, 1));
}
if (block.act2_alpha) {
x = apply_snake(ctx, x, block.act2_alpha, block.act2_beta);
}
out_channels = block.conv2_w->ne[2];
x = ggml_conv_1d(ctx, block.conv2_w, x, 1, 0, 1);
if (block.conv2_b) {
x = ggml_add(ctx, x, ggml_reshape_3d(ctx, block.conv2_b, 1, out_channels, 1));
}
return ggml_add(ctx, residual, x);
}
struct ggml_tensor * AudioTokenizerDecoder::apply_decoder_block(struct ggml_context * ctx,
struct ggml_tensor * x,
const decoder_block & block,
int upsample_rate,
int block_idx) {
if (block.snake_alpha && block.snake_beta) {
x = apply_snake(ctx, x, block.snake_alpha, block.snake_beta);
}
int64_t seq_len = x->ne[0];
int64_t in_channels = x->ne[1];
int64_t out_channels = block.conv_t_w->ne[1];
int kernel_size = block.conv_t_w->ne[0];
struct ggml_tensor * x_2d = ggml_reshape_2d(ctx, x, seq_len, in_channels);
x_2d = ggml_conv_transpose_1d(ctx, block.conv_t_w, x_2d, upsample_rate, 0, 1);
int64_t new_seq_len = x_2d->ne[0];
x = ggml_reshape_3d(ctx, x_2d, new_seq_len, out_channels, 1);
// Python CausalTransConvNet: left_pad = right_pad = kernel_size - stride
int pad = kernel_size - upsample_rate;
int left_pad = pad;
int right_pad = pad;
int64_t out_seq_len = new_seq_len - left_pad - right_pad;
x = ggml_view_3d(ctx, x, out_seq_len, out_channels, 1,
x->nb[1], x->nb[2], left_pad * x->nb[0]);
x = ggml_cont(ctx, x);
if (block.conv_t_b) {
x = ggml_add(ctx, x, ggml_reshape_3d(ctx, block.conv_t_b, 1, out_channels, 1));
}
for (int i = 0; i < 3; ++i) {
x = apply_residual_block(ctx, x, block.res[i]);
}
return x;
}
struct ggml_cgraph * AudioTokenizerDecoder::build_graph(int32_t n_frames) {
const auto & cfg = model_.config;
struct ggml_init_params params = {
/*.mem_size =*/ state_.compute_meta.size(),
/*.mem_buffer =*/ state_.compute_meta.data(),
/*.no_alloc =*/ true,
};
struct ggml_context * ctx0 = ggml_init(params);
struct ggml_cgraph * gf = ggml_new_graph_custom(ctx0, QWEN3_TTS_DEC_MAX_NODES, false);
static const char * cb_names[16] = {
"codes_cb0", "codes_cb1", "codes_cb2", "codes_cb3",
"codes_cb4", "codes_cb5", "codes_cb6", "codes_cb7",
"codes_cb8", "codes_cb9", "codes_cb10", "codes_cb11",
"codes_cb12", "codes_cb13", "codes_cb14", "codes_cb15"
};
struct ggml_tensor * cb_codes_tensors[16];
for (int cb = 0; cb < 16; ++cb) {
cb_codes_tensors[cb] = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_frames);
ggml_set_name(cb_codes_tensors[cb], cb_names[cb]);
ggml_set_input(cb_codes_tensors[cb]);
}
struct ggml_tensor * first_codes = cb_codes_tensors[0];
struct ggml_tensor * first_emb = ggml_get_rows(ctx0, model_.vq_first_codebook, first_codes);
ggml_set_name(first_emb, "first_emb_raw");
struct ggml_tensor * rest_emb[15];
for (int cb = 0; cb < 15; ++cb) {
struct ggml_tensor * cb_codes = cb_codes_tensors[cb + 1];
rest_emb[cb] = ggml_get_rows(ctx0, model_.vq_rest_codebook[cb], cb_codes);
if (cb == 0) {
ggml_set_name(rest_emb[cb], "rest_cb0_emb_raw");
}
}
struct ggml_tensor * first_emb_2d = ggml_reshape_2d(ctx0, first_emb, cfg.codebook_dim, n_frames);
ggml_set_name(first_emb_2d, "first_emb_2d");
struct ggml_tensor * first_proj_weight_2d = ggml_reshape_2d(ctx0, model_.vq_first_output_proj,
cfg.codebook_dim, cfg.hidden_dim);
struct ggml_tensor * first_proj_2d = ggml_mul_mat(ctx0, first_proj_weight_2d, first_emb_2d);
ggml_set_name(first_proj_2d, "first_proj_2d");
struct ggml_tensor * rest_proj_weight_2d = ggml_reshape_2d(ctx0, model_.vq_rest_output_proj,
cfg.codebook_dim, cfg.hidden_dim);
struct ggml_tensor * rest_proj_2d = nullptr;
for (int cb = 0; cb < 15; ++cb) {
struct ggml_tensor * cb_emb_2d = ggml_reshape_2d(ctx0, rest_emb[cb], cfg.codebook_dim, n_frames);
if (cb == 0) {
ggml_set_name(cb_emb_2d, "rest_cb0_emb_2d");
}
struct ggml_tensor * cb_proj_2d = ggml_mul_mat(ctx0, rest_proj_weight_2d, cb_emb_2d);
if (rest_proj_2d == nullptr) {
rest_proj_2d = cb_proj_2d;
} else {
rest_proj_2d = ggml_add(ctx0, rest_proj_2d, cb_proj_2d);
}
}
ggml_set_name(rest_proj_2d, "rest_proj_2d");
struct ggml_tensor * latent_2d = ggml_add(ctx0, first_proj_2d, rest_proj_2d);
ggml_set_name(latent_2d, "latent_2d");
struct ggml_tensor * latent_t = ggml_transpose(ctx0, latent_2d);
ggml_set_name(latent_t, "latent_t");
struct ggml_tensor * latent_cont = ggml_cont(ctx0, latent_t);
ggml_set_name(latent_cont, "latent_cont");
struct ggml_tensor * latent = ggml_reshape_3d(ctx0, latent_cont, n_frames, cfg.hidden_dim, 1);
ggml_set_name(latent, "vq_output");
struct ggml_tensor * latent_for_conv = ggml_cont(ctx0, latent);
struct ggml_tensor * latent_padded = ggml_pad_ext(ctx0, latent_for_conv, 2, 0, 0, 0, 0, 0, 0, 0);
struct ggml_tensor * cur = ggml_conv_1d(ctx0, model_.pre_conv_w, latent_padded, 1, 0, 1);
if (model_.pre_conv_b) {
cur = ggml_add(ctx0, cur, ggml_reshape_3d(ctx0, model_.pre_conv_b, 1, cfg.latent_dim, 1));
}
ggml_set_name(cur, "pre_conv_output");
struct ggml_tensor * cur_2d = ggml_reshape_2d(ctx0, cur, n_frames, cfg.latent_dim);
struct ggml_tensor * cur_t = ggml_transpose(ctx0, cur_2d);
cur = ggml_cont(ctx0, cur_t);
ggml_set_name(cur, "pre_conv_reshaped");
cur = ggml_mul_mat(ctx0, model_.pre_tfm_input_proj_w, cur);
if (model_.pre_tfm_input_proj_b) {
cur = ggml_add(ctx0, cur, model_.pre_tfm_input_proj_b);
}
ggml_set_name(cur, "pre_tfm_input");
struct ggml_tensor * positions = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_frames);
ggml_set_name(positions, "positions");
ggml_set_input(positions);
for (int i = 0; i < cfg.n_pre_tfm_layers; ++i) {
cur = apply_pre_tfm_layer(ctx0, cur, model_.pre_tfm_layers[i], n_frames, positions);
}
if (model_.pre_tfm_norm_w) {
cur = apply_rms_norm(ctx0, cur, model_.pre_tfm_norm_w, cfg.rms_norm_eps);
}
cur = ggml_mul_mat(ctx0, model_.pre_tfm_output_proj_w, cur);
if (model_.pre_tfm_output_proj_b) {
cur = ggml_add(ctx0, cur, model_.pre_tfm_output_proj_b);
}
ggml_set_name(cur, "pre_tfm_output");
cur = ggml_permute(ctx0, cur, 1, 0, 2, 3);
cur = ggml_cont(ctx0, cur);
cur = ggml_reshape_3d(ctx0, cur, n_frames, cfg.latent_dim, 1);
ggml_set_name(cur, "pre_tfm_reshaped");
for (int i = 0; i < 2; ++i) {
cur = apply_upsample_block(ctx0, cur, model_.upsample[i], i);
}
ggml_set_name(cur, "upsample_output");
// Causal padding: left pad with 6 (kernel_size - 1 = 7 - 1 = 6)
cur = ggml_pad_ext(ctx0, cur, 6, 0, 0, 0, 0, 0, 0, 0);
cur = ggml_conv_1d(ctx0, model_.dec0_conv_w, cur, 1, 0, 1);
if (model_.dec0_conv_b) {
cur = ggml_add(ctx0, cur, ggml_reshape_3d(ctx0, model_.dec0_conv_b, 1, cfg.decoder_dim, 1));
}
ggml_set_name(cur, "dec0_output");
int upsample_rates[4] = {8, 5, 4, 3};
for (int i = 0; i < 4; ++i) {
cur = apply_decoder_block(ctx0, cur, model_.dec_blocks[i], upsample_rates[i], i);
char name[32];
snprintf(name, sizeof(name), "dec%d_output", i + 1);
ggml_set_name(cur, name);
}
if (model_.dec5_snake_alpha) {
cur = apply_snake(ctx0, cur, model_.dec5_snake_alpha, model_.dec5_snake_beta);
}
ggml_set_name(cur, "dec5_output");
// Causal padding: left pad with 6 (kernel_size - 1 = 7 - 1 = 6)
cur = ggml_pad_ext(ctx0, cur, 6, 0, 0, 0, 0, 0, 0, 0);
cur = ggml_conv_1d(ctx0, model_.dec6_conv_w, cur, 1, 0, 1);
if (model_.dec6_conv_b) {
cur = ggml_add(ctx0, cur, ggml_reshape_3d(ctx0, model_.dec6_conv_b, 1, 1, 1));
}
ggml_set_name(cur, "dec6_output");
cur = ggml_tanh(ctx0, cur);
cur = ggml_reshape_1d(ctx0, cur, cur->ne[0]);
ggml_set_name(cur, "audio");
ggml_set_output(cur);
ggml_build_forward_expand(gf, cur);
ggml_free(ctx0);
return gf;
}
bool AudioTokenizerDecoder::decode(const int32_t * codes, int32_t n_frames,
std::vector<float> & samples) {
if (!model_.ctx) {
error_msg_ = "Model not loaded";
return false;
}
const auto & cfg = model_.config;
codes_buf_.resize(n_frames * cfg.n_codebooks);
for (int f = 0; f < n_frames; ++f) {
for (int cb = 0; cb < cfg.n_codebooks; ++cb) {
codes_buf_[cb + f * cfg.n_codebooks] = codes[f * cfg.n_codebooks + cb];
}
}
struct ggml_cgraph * gf = build_graph(n_frames);
if (!ggml_backend_sched_alloc_graph(state_.sched, gf)) {
error_msg_ = "Failed to allocate graph";
return false;
}
std::vector<int32_t> cb_codes(n_frames);
for (int cb = 0; cb < 16; ++cb) {
char name[32];
snprintf(name, sizeof(name), "codes_cb%d", cb);
struct ggml_tensor * cb_tensor = ggml_graph_get_tensor(gf, name);
if (!cb_tensor) {
error_msg_ = "Failed to find codes tensor for codebook " + std::to_string(cb);
ggml_backend_sched_reset(state_.sched);
return false;
}
for (int f = 0; f < n_frames; ++f) {
cb_codes[f] = codes_buf_[f * cfg.n_codebooks + cb];
}
ggml_backend_tensor_set(cb_tensor, cb_codes.data(), 0, n_frames * sizeof(int32_t));
}
struct ggml_tensor * positions_tensor = ggml_graph_get_tensor(gf, "positions");
if (positions_tensor) {
std::vector<int32_t> positions(n_frames);
for (int i = 0; i < n_frames; ++i) {
positions[i] = i;
}
ggml_backend_tensor_set(positions_tensor, positions.data(), 0,
n_frames * sizeof(int32_t));
}
if (ggml_backend_sched_graph_compute(state_.sched, gf) != GGML_STATUS_SUCCESS) {
error_msg_ = "Failed to compute graph";
ggml_backend_sched_reset(state_.sched);
return false;
}
struct ggml_tensor * audio_tensor = ggml_graph_get_tensor(gf, "audio");
if (!audio_tensor) {
error_msg_ = "Failed to find audio tensor";
ggml_backend_sched_reset(state_.sched);
return false;
}
int64_t n_samples = audio_tensor->ne[0];
samples.resize(n_samples);
ggml_backend_tensor_get(audio_tensor, samples.data(), 0, n_samples * sizeof(float));
ggml_backend_sched_reset(state_.sched);
return true;
}
void free_audio_decoder_model(audio_decoder_model & model) {
if (model.buffer) {
ggml_backend_buffer_free(model.buffer);
model.buffer = nullptr;
}
if (model.ctx) {
ggml_free(model.ctx);
model.ctx = nullptr;
}
model.tensors.clear();
}
} // namespace qwen3_tts

View file

@ -0,0 +1,235 @@
#pragma once
#include "ggml.h"
#include "ggml-backend.h"
#include "gguf.h"
#include <string>
#include <map>
#include <vector>
#include <memory>
namespace qwen3_tts {
// Audio tokenizer decoder (vocoder) configuration
struct audio_decoder_config {
int32_t sample_rate = 24000;
int32_t n_codebooks = 16; // Total codebooks (1 first + 15 rest)
int32_t codebook_size = 2048; // Entries per codebook
int32_t codebook_dim = 256; // Embedding dimension per codebook
int32_t latent_dim = 1024; // Latent dimension after VQ
int32_t hidden_dim = 512; // Pre-transformer hidden dimension
int32_t n_pre_tfm_layers = 8; // Pre-transformer layers
int32_t n_heads = 16; // Attention heads in pre-transformer
int32_t ffn_dim = 1024; // FFN intermediate dimension
int32_t decoder_dim = 1536; // Initial decoder dimension
int32_t upsample_rates[4] = {8, 5, 4, 3}; // Total: 480x upsampling
float rms_norm_eps = 1e-5f;
float rope_theta = 10000.0f;
};
// Pre-transformer layer weights
struct pre_tfm_layer {
// Attention
struct ggml_tensor * attn_norm_w = nullptr;
struct ggml_tensor * attn_q_w = nullptr;
struct ggml_tensor * attn_k_w = nullptr;
struct ggml_tensor * attn_v_w = nullptr;
struct ggml_tensor * attn_output_w = nullptr;
struct ggml_tensor * attn_scale = nullptr; // layer_scale for attention
// FFN (SwiGLU)
struct ggml_tensor * ffn_norm_w = nullptr;
struct ggml_tensor * ffn_gate_w = nullptr;
struct ggml_tensor * ffn_up_w = nullptr;
struct ggml_tensor * ffn_down_w = nullptr;
struct ggml_tensor * ffn_scale = nullptr; // layer_scale for FFN
};
// Residual block weights (Snake + Conv + Snake + Conv)
struct residual_block {
int dilation = 1; // Dilation for conv1: [1, 3, 9] for res[0], res[1], res[2]
struct ggml_tensor * act1_alpha = nullptr;
struct ggml_tensor * act1_beta = nullptr;
struct ggml_tensor * conv1_w = nullptr;
struct ggml_tensor * conv1_b = nullptr;
struct ggml_tensor * act2_alpha = nullptr;
struct ggml_tensor * act2_beta = nullptr;
struct ggml_tensor * conv2_w = nullptr;
struct ggml_tensor * conv2_b = nullptr;
};
// Decoder block weights (Snake + ConvTranspose + Residual blocks)
struct decoder_block {
// Snake activation before conv transpose
struct ggml_tensor * snake_alpha = nullptr;
struct ggml_tensor * snake_beta = nullptr;
// Transposed convolution for upsampling
struct ggml_tensor * conv_t_w = nullptr;
struct ggml_tensor * conv_t_b = nullptr;
// Residual blocks (3 per decoder block)
residual_block res[3];
};
// Upsample block weights (ConvNeXt-style)
struct upsample_block {
struct ggml_tensor * conv_w = nullptr;
struct ggml_tensor * conv_b = nullptr;
struct ggml_tensor * dwconv_w = nullptr;
struct ggml_tensor * dwconv_b = nullptr;
struct ggml_tensor * norm_w = nullptr;
struct ggml_tensor * norm_b = nullptr;
struct ggml_tensor * pwconv1_w = nullptr;
struct ggml_tensor * pwconv1_b = nullptr;
struct ggml_tensor * pwconv2_w = nullptr;
struct ggml_tensor * pwconv2_b = nullptr;
struct ggml_tensor * gamma = nullptr;
};
// Audio tokenizer decoder model weights
struct audio_decoder_model {
audio_decoder_config config;
// VQ codebooks
// vq_first: 1 codebook for first code
struct ggml_tensor * vq_first_input_proj = nullptr; // [1, 512, 256]
struct ggml_tensor * vq_first_output_proj = nullptr; // [1, 256, 512]
struct ggml_tensor * vq_first_codebook = nullptr; // [256, 2048] embedding_sum
struct ggml_tensor * vq_first_usage = nullptr; // [2048] cluster_usage
// vq_rest: 15 codebooks for remaining codes
struct ggml_tensor * vq_rest_input_proj = nullptr; // [1, 512, 256]
struct ggml_tensor * vq_rest_output_proj = nullptr; // [1, 256, 512]
struct ggml_tensor * vq_rest_codebook[15] = {nullptr}; // [256, 2048] embedding_sum each
struct ggml_tensor * vq_rest_usage[15] = {nullptr}; // [2048] cluster_usage each
// Upsample blocks (2 ConvNeXt-style blocks)
upsample_block upsample[2];
// Pre-transformer
struct ggml_tensor * pre_tfm_input_proj_w = nullptr; // [1024, 512]
struct ggml_tensor * pre_tfm_input_proj_b = nullptr;
pre_tfm_layer pre_tfm_layers[8];
struct ggml_tensor * pre_tfm_norm_w = nullptr; // Final RMSNorm
struct ggml_tensor * pre_tfm_output_proj_w = nullptr; // [512, 1024]
struct ggml_tensor * pre_tfm_output_proj_b = nullptr;
// Pre-conv: [3, 512, 1024]
struct ggml_tensor * pre_conv_w = nullptr;
struct ggml_tensor * pre_conv_b = nullptr;
// Decoder blocks
// Block 0: Initial conv [7, 1024, 1536]
struct ggml_tensor * dec0_conv_w = nullptr;
struct ggml_tensor * dec0_conv_b = nullptr;
// Blocks 1-4: Snake + ConvTranspose + 3 residual blocks
decoder_block dec_blocks[4];
// Block 5: Final snake activation
struct ggml_tensor * dec5_snake_alpha = nullptr;
struct ggml_tensor * dec5_snake_beta = nullptr;
// Block 6: Output conv [7, 96, 1]
struct ggml_tensor * dec6_conv_w = nullptr;
struct ggml_tensor * dec6_conv_b = nullptr;
// GGML context for tensor metadata
struct ggml_context * ctx = nullptr;
// Backend buffer for weights
ggml_backend_buffer_t buffer = nullptr;
// Tensor name to tensor mapping
std::map<std::string, struct ggml_tensor *> tensors;
};
// Compute state for decoder
struct audio_decoder_state {
ggml_backend_t backend = nullptr;
ggml_backend_t backend_cpu = nullptr;
ggml_backend_sched_t sched = nullptr;
std::vector<uint8_t> compute_meta;
};
// Audio tokenizer decoder (vocoder) class
// Decodes discrete audio codes to waveform
class AudioTokenizerDecoder {
public:
AudioTokenizerDecoder();
~AudioTokenizerDecoder();
// Load model from GGUF file (tokenizer model)
bool load_model(const std::string & model_path);
// Release all model/runtime resources
void unload_model();
// Decode audio codes to waveform
// codes: audio codes [n_frames, n_codebooks] as int32_t (row-major)
// n_frames: number of frames
// Returns: audio samples normalized to [-1, 1] at 24kHz
bool decode(const int32_t * codes, int32_t n_frames,
std::vector<float> & samples);
const audio_decoder_config & get_config() const { return model_.config; }
const std::string & get_error() const { return error_msg_; }
private:
// Build computation graph for decoding
struct ggml_cgraph * build_graph(int32_t n_frames);
// Apply Snake activation: x + (1/alpha) * sin^2(alpha * x)
struct ggml_tensor * apply_snake(struct ggml_context * ctx,
struct ggml_tensor * x,
struct ggml_tensor * alpha,
struct ggml_tensor * beta);
// Apply RMSNorm
struct ggml_tensor * apply_rms_norm(struct ggml_context * ctx,
struct ggml_tensor * x,
struct ggml_tensor * w,
float eps);
// Apply pre-transformer layer
struct ggml_tensor * apply_pre_tfm_layer(struct ggml_context * ctx,
struct ggml_tensor * x,
const pre_tfm_layer & layer,
int32_t n_frames,
struct ggml_tensor * positions);
// Apply upsample block (ConvNeXt-style)
struct ggml_tensor * apply_upsample_block(struct ggml_context * ctx,
struct ggml_tensor * x,
const upsample_block & block,
int block_idx);
// Apply residual block
struct ggml_tensor * apply_residual_block(struct ggml_context * ctx,
struct ggml_tensor * x,
const residual_block & block);
// Apply decoder block (Snake + ConvTranspose + Residuals)
struct ggml_tensor * apply_decoder_block(struct ggml_context * ctx,
struct ggml_tensor * x,
const decoder_block & block,
int upsample_rate,
int block_idx);
void normalize_codebooks();
audio_decoder_model model_;
audio_decoder_state state_;
std::string error_msg_;
// Temporary storage for codes input
std::vector<int32_t> codes_buf_;
};
// Free model resources
void free_audio_decoder_model(audio_decoder_model & model);
} // namespace qwen3_tts

View file

@ -0,0 +1,771 @@
#include "audio_tokenizer_encoder.h"
#include "gguf_loader.h"
#include "ggml-cpu.h"
#include <cmath>
#include <cstring>
#include <algorithm>
#include <numeric>
#define QWEN3_TTS_MAX_NODES 16384
namespace qwen3_tts {
// Mel filterbank computation using librosa slaney normalization
// This matches librosa.filters.mel with norm='slaney'
static void compute_mel_filterbank_slaney(float * filterbank, int n_mels, int n_fft,
int sample_rate, float f_min, float f_max) {
// Slaney-style mel scale (used by librosa default)
auto hz_to_mel_slaney = [](float hz) -> float {
// Linear below 1000 Hz, logarithmic above
const float f_sp = 200.0f / 3.0f; // 66.67 Hz
const float min_log_hz = 1000.0f;
const float min_log_mel = (min_log_hz - 0.0f) / f_sp; // 15
const float logstep = logf(6.4f) / 27.0f; // log(6400/1000) / 27
if (hz < min_log_hz) {
return (hz - 0.0f) / f_sp;
} else {
return min_log_mel + logf(hz / min_log_hz) / logstep;
}
};
auto mel_to_hz_slaney = [](float mel) -> float {
const float f_sp = 200.0f / 3.0f;
const float min_log_hz = 1000.0f;
const float min_log_mel = (min_log_hz - 0.0f) / f_sp;
const float logstep = logf(6.4f) / 27.0f;
if (mel < min_log_mel) {
return 0.0f + f_sp * mel;
} else {
return min_log_hz * expf(logstep * (mel - min_log_mel));
}
};
float mel_min = hz_to_mel_slaney(f_min);
float mel_max = hz_to_mel_slaney(f_max);
int n_fft_bins = n_fft / 2 + 1;
// Compute mel center frequencies
std::vector<float> mel_points(n_mels + 2);
for (int i = 0; i < n_mels + 2; ++i) {
mel_points[i] = mel_min + (mel_max - mel_min) * i / (n_mels + 1);
}
// Convert to Hz and then to FFT bin indices
std::vector<float> hz_points(n_mels + 2);
std::vector<float> fft_freqs(n_fft_bins);
for (int i = 0; i < n_mels + 2; ++i) {
hz_points[i] = mel_to_hz_slaney(mel_points[i]);
}
for (int i = 0; i < n_fft_bins; ++i) {
fft_freqs[i] = (float)i * sample_rate / n_fft;
}
memset(filterbank, 0, n_mels * n_fft_bins * sizeof(float));
// Create triangular filters with slaney normalization
for (int m = 0; m < n_mels; ++m) {
float f_left = hz_points[m];
float f_center = hz_points[m + 1];
float f_right = hz_points[m + 2];
// Slaney normalization: divide by bandwidth (area normalization)
float enorm = 2.0f / (f_right - f_left);
for (int k = 0; k < n_fft_bins; ++k) {
float freq = fft_freqs[k];
if (freq >= f_left && freq <= f_center) {
if (f_center > f_left) {
filterbank[m * n_fft_bins + k] = enorm * (freq - f_left) / (f_center - f_left);
}
} else if (freq > f_center && freq <= f_right) {
if (f_right > f_center) {
filterbank[m * n_fft_bins + k] = enorm * (f_right - freq) / (f_right - f_center);
}
}
}
}
}
static void compute_dft(const float * input, float * real, float * imag, int n) {
for (int k = 0; k < n; ++k) {
real[k] = 0.0f;
imag[k] = 0.0f;
for (int t = 0; t < n; ++t) {
float angle = -2.0f * M_PI * k * t / n;
real[k] += input[t] * cosf(angle);
imag[k] += input[t] * sinf(angle);
}
}
}
// Periodic Hann window (matches torch.hann_window with periodic=True, which is default)
static void compute_hann_window(float * window, int n) {
for (int i = 0; i < n; ++i) {
window[i] = 0.5f * (1.0f - cosf(2.0f * M_PI * i / n));
}
}
// Compute centered window for STFT (PyTorch centers win_length window in n_fft frame)
static void compute_centered_window(float * window, int n_fft, int win_length) {
// Zero-initialize
memset(window, 0, n_fft * sizeof(float));
// Compute Hann window of win_length
int offset = (n_fft - win_length) / 2;
for (int i = 0; i < win_length; ++i) {
window[offset + i] = 0.5f * (1.0f - cosf(2.0f * M_PI * i / win_length));
}
}
AudioTokenizerEncoder::AudioTokenizerEncoder() = default;
AudioTokenizerEncoder::~AudioTokenizerEncoder() {
free_speaker_encoder_model(model_);
if (state_.sched) {
ggml_backend_sched_free(state_.sched);
state_.sched = nullptr;
}
if (state_.backend) {
release_preferred_backend(state_.backend);
state_.backend = nullptr;
}
if (state_.backend_cpu) {
ggml_backend_free(state_.backend_cpu);
state_.backend_cpu = nullptr;
}
}
bool AudioTokenizerEncoder::load_model(const std::string & model_path) {
GGUFLoader loader;
if (!loader.open(model_path)) {
error_msg_ = loader.get_error();
return false;
}
model_.config.sample_rate = loader.get_u32("qwen3-tts.speaker_encoder.sample_rate", 24000);
model_.config.embedding_dim = loader.get_u32("qwen3-tts.speaker_encoder.embedding_length", 1024);
int64_t n_tensors = loader.get_n_tensors();
int spk_tensor_count = 0;
for (int64_t i = 0; i < n_tensors; ++i) {
const char * name = loader.get_tensor_name(i);
if (name && strncmp(name, "spk_enc.", 8) == 0) {
spk_tensor_count++;
}
}
if (spk_tensor_count == 0) {
error_msg_ = "No speaker encoder tensors found in model";
return false;
}
size_t ctx_size = ggml_tensor_overhead() * spk_tensor_count;
struct ggml_init_params params = {
/*.mem_size =*/ ctx_size,
/*.mem_buffer =*/ nullptr,
/*.no_alloc =*/ true,
};
model_.ctx = ggml_init(params);
if (!model_.ctx) {
error_msg_ = "Failed to initialize GGML context";
return false;
}
struct gguf_context * gguf_ctx = loader.get_ctx();
struct ggml_context * meta_ctx = loader.get_meta_ctx();
for (int64_t i = 0; i < n_tensors; ++i) {
const char * name = loader.get_tensor_name(i);
if (!name || strncmp(name, "spk_enc.", 8) != 0) {
continue;
}
struct ggml_tensor * meta_tensor = ggml_get_tensor(meta_ctx, name);
if (!meta_tensor) {
continue;
}
struct ggml_tensor * tensor = ggml_dup_tensor(model_.ctx, meta_tensor);
ggml_set_name(tensor, name);
model_.tensors[name] = tensor;
std::string sname(name);
if (sname == "spk_enc.conv0.weight") model_.conv0_w = tensor;
else if (sname == "spk_enc.conv0.bias") model_.conv0_b = tensor;
else if (sname == "spk_enc.mfa.weight") model_.mfa_w = tensor;
else if (sname == "spk_enc.mfa.bias") model_.mfa_b = tensor;
else if (sname == "spk_enc.asp.conv.weight") model_.asp_conv_w = tensor;
else if (sname == "spk_enc.asp.conv.bias") model_.asp_conv_b = tensor;
else if (sname == "spk_enc.asp.tdnn.weight") model_.asp_tdnn_w = tensor;
else if (sname == "spk_enc.asp.tdnn.bias") model_.asp_tdnn_b = tensor;
else if (sname == "spk_enc.fc.weight") model_.fc_w = tensor;
else if (sname == "spk_enc.fc.bias") model_.fc_b = tensor;
else {
int blk_idx, res_idx;
char suffix[64];
if (sscanf(name, "spk_enc.blk.%d.tdnn1.%s", &blk_idx, suffix) == 2) {
if (blk_idx >= 1 && blk_idx <= 3) {
if (strcmp(suffix, "weight") == 0) model_.blocks[blk_idx-1].tdnn1_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.blocks[blk_idx-1].tdnn1_b = tensor;
}
}
else if (sscanf(name, "spk_enc.blk.%d.tdnn2.%s", &blk_idx, suffix) == 2) {
if (blk_idx >= 1 && blk_idx <= 3) {
if (strcmp(suffix, "weight") == 0) model_.blocks[blk_idx-1].tdnn2_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.blocks[blk_idx-1].tdnn2_b = tensor;
}
}
else if (sscanf(name, "spk_enc.blk.%d.res2net.%d.%s", &blk_idx, &res_idx, suffix) == 3) {
if (blk_idx >= 1 && blk_idx <= 3 && res_idx >= 0 && res_idx < 7) {
if (strcmp(suffix, "weight") == 0) model_.blocks[blk_idx-1].res2net_w[res_idx] = tensor;
else if (strcmp(suffix, "bias") == 0) model_.blocks[blk_idx-1].res2net_b[res_idx] = tensor;
}
}
else if (sscanf(name, "spk_enc.blk.%d.se.conv1.%s", &blk_idx, suffix) == 2) {
if (blk_idx >= 1 && blk_idx <= 3) {
if (strcmp(suffix, "weight") == 0) model_.blocks[blk_idx-1].se_conv1_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.blocks[blk_idx-1].se_conv1_b = tensor;
}
}
else if (sscanf(name, "spk_enc.blk.%d.se.conv2.%s", &blk_idx, suffix) == 2) {
if (blk_idx >= 1 && blk_idx <= 3) {
if (strcmp(suffix, "weight") == 0) model_.blocks[blk_idx-1].se_conv2_w = tensor;
else if (strcmp(suffix, "bias") == 0) model_.blocks[blk_idx-1].se_conv2_b = tensor;
}
}
}
}
if (!load_tensor_data_from_file(model_path, gguf_ctx, model_.ctx,
model_.tensors, model_.buffer, error_msg_)) {
return false;
}
state_.backend = init_preferred_backend("AudioTokenizerEncoder", &error_msg_);
if (!state_.backend) {
return false;
}
ggml_backend_dev_t device = ggml_backend_get_device(state_.backend);
const char * device_name = device ? ggml_backend_dev_name(device) : "Unknown";
fprintf(stderr, " AudioTokenizerEncoder backend: %s\n", device_name);
if (device && ggml_backend_dev_type(device) != GGML_BACKEND_DEVICE_TYPE_CPU) {
state_.backend_cpu = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);
if (!state_.backend_cpu) {
error_msg_ = "Failed to initialize CPU fallback backend for AudioTokenizerEncoder";
return false;
}
}
std::vector<ggml_backend_t> backends;
backends.push_back(state_.backend);
if (state_.backend_cpu) {
backends.push_back(state_.backend_cpu);
}
state_.sched = ggml_backend_sched_new(backends.data(), nullptr, (int)backends.size(), QWEN3_TTS_MAX_NODES, false, true);
if (!state_.sched) {
error_msg_ = "Failed to create backend scheduler";
return false;
}
state_.compute_meta.resize(ggml_tensor_overhead() * QWEN3_TTS_MAX_NODES + ggml_graph_overhead());
return true;
}
bool AudioTokenizerEncoder::compute_mel_spectrogram(const float * samples, int32_t n_samples,
std::vector<float> & mel, int32_t & n_frames) {
const auto & cfg = model_.config;
// Match PyTorch STFT padding: (n_fft - hop_size) // 2 on each side with reflect
int padding = (cfg.n_fft - cfg.hop_length) / 2;
int padded_length = n_samples + 2 * padding;
// Create padded signal with reflect padding
std::vector<float> padded(padded_length);
for (int i = 0; i < padded_length; ++i) {
int src_idx;
if (i < padding) {
// Reflect left: padding-1, padding-2, ..., 0 -> samples[padding-i], samples[padding-1-i], ...
src_idx = padding - i;
} else if (i >= padding + n_samples) {
// Reflect right
src_idx = 2 * n_samples - (i - padding) - 2;
} else {
src_idx = i - padding;
}
// Clamp to valid range
src_idx = std::max(0, std::min(n_samples - 1, src_idx));
padded[i] = samples[src_idx];
}
// With center=False, frames start at 0 and step by hop_length
n_frames = (padded_length - cfg.n_fft) / cfg.hop_length + 1;
if (n_frames <= 0) {
error_msg_ = "Audio too short for mel spectrogram";
return false;
}
int n_fft_bins = cfg.n_fft / 2 + 1;
std::vector<float> filterbank(cfg.n_mels * n_fft_bins);
compute_mel_filterbank_slaney(filterbank.data(), cfg.n_mels, cfg.n_fft,
cfg.sample_rate, cfg.f_min, cfg.f_max);
// PyTorch STFT with win_length < n_fft centers the window in the n_fft frame
// This is critical for matching PyTorch's output
std::vector<float> window(cfg.n_fft);
compute_centered_window(window.data(), cfg.n_fft, cfg.win_length);
// Output: [batch, n_mels, n_frames] but we store as [n_mels, n_frames] row-major
// which means mel[m * n_frames + f] = value at mel bin m, frame f
mel.resize(cfg.n_mels * n_frames);
std::vector<float> frame(cfg.n_fft, 0.0f);
std::vector<float> fft_real(cfg.n_fft);
std::vector<float> fft_imag(cfg.n_fft);
std::vector<float> magnitude(n_fft_bins);
for (int32_t f = 0; f < n_frames; ++f) {
int start = f * cfg.hop_length;
// Apply centered window to n_fft samples
for (int i = 0; i < cfg.n_fft; ++i) {
frame[i] = padded[start + i] * window[i];
}
compute_dft(frame.data(), fft_real.data(), fft_imag.data(), cfg.n_fft);
// Compute magnitude (not power) - matches torch.stft with return_complex=True then abs()
// spec = torch.sqrt(torch.view_as_real(spec).pow(2).sum(-1) + 1e-9)
for (int k = 0; k < n_fft_bins; ++k) {
magnitude[k] = sqrtf(fft_real[k] * fft_real[k] + fft_imag[k] * fft_imag[k] + 1e-9f);
}
// Apply mel filterbank and log compression
// mel_spec = torch.matmul(mel_basis, spec)
// mel_spec = dynamic_range_compression_torch(mel_spec) # log(clamp(x, min=1e-5) * 1)
for (int m = 0; m < cfg.n_mels; ++m) {
float sum = 0.0f;
for (int k = 0; k < n_fft_bins; ++k) {
sum += filterbank[m * n_fft_bins + k] * magnitude[k];
}
// dynamic_range_compression: log(clamp(x, min=1e-5))
mel[m * n_frames + f] = logf(std::max(sum, 1e-5f));
}
}
return true;
}
static struct ggml_tensor * apply_reflect_pad_1d(struct ggml_context * ctx,
struct ggml_tensor * x,
int pad) {
if (pad == 0) {
return x;
}
int64_t T = x->ne[0];
int64_t C = x->ne[1];
int64_t B = x->ne[2];
struct ggml_tensor * left_slices[16];
struct ggml_tensor * right_slices[16];
for (int i = 0; i < pad && i < 16; ++i) {
int left_src_idx = pad - i;
left_slices[i] = ggml_view_3d(ctx, x, 1, C, B,
x->nb[1], x->nb[2],
left_src_idx * x->nb[0]);
left_slices[i] = ggml_cont(ctx, left_slices[i]);
int right_src_idx = T - 2 - i;
right_slices[i] = ggml_view_3d(ctx, x, 1, C, B,
x->nb[1], x->nb[2],
right_src_idx * x->nb[0]);
right_slices[i] = ggml_cont(ctx, right_slices[i]);
}
struct ggml_tensor * left_pad = left_slices[0];
for (int i = 1; i < pad && i < 16; ++i) {
left_pad = ggml_concat(ctx, left_pad, left_slices[i], 0);
}
struct ggml_tensor * right_pad = right_slices[0];
for (int i = 1; i < pad && i < 16; ++i) {
right_pad = ggml_concat(ctx, right_pad, right_slices[i], 0);
}
struct ggml_tensor * padded = ggml_concat(ctx, left_pad, x, 0);
padded = ggml_concat(ctx, padded, right_pad, 0);
return padded;
}
static struct ggml_tensor * apply_conv1d(struct ggml_context * ctx,
struct ggml_tensor * w,
struct ggml_tensor * b,
struct ggml_tensor * x,
int stride, int pad, int dilation,
const char * debug_name = nullptr,
bool use_reflect_pad = true) {
struct ggml_tensor * input = x;
int actual_pad = pad;
if (use_reflect_pad && pad > 0) {
input = apply_reflect_pad_1d(ctx, x, pad);
actual_pad = 0;
}
struct ggml_tensor * y = ggml_conv_1d(ctx, w, input, stride, actual_pad, dilation);
if (debug_name) {
char name[64];
snprintf(name, sizeof(name), "%s_conv", debug_name);
ggml_set_name(y, name);
}
if (b) {
int64_t oc = y->ne[1];
y = ggml_add(ctx, y, ggml_reshape_3d(ctx, b, 1, oc, 1));
}
return y;
}
struct ggml_cgraph * AudioTokenizerEncoder::build_graph(int32_t n_frames) {
const auto & cfg = model_.config;
const int hidden_dim = cfg.hidden_dim; // 512
const int scale = cfg.res2net_scale; // 8
const int branch_dim = hidden_dim / scale; // 64
struct ggml_init_params params = {
/*.mem_size =*/ state_.compute_meta.size(),
/*.mem_buffer =*/ state_.compute_meta.data(),
/*.no_alloc =*/ true,
};
struct ggml_context * ctx0 = ggml_init(params);
struct ggml_cgraph * gf = ggml_new_graph_custom(ctx0, QWEN3_TTS_MAX_NODES, false);
// Input: mel spectrogram [n_mels, n_frames] - stored as [n_mels, n_frames] row-major
// GGML uses column-major, so this is [n_frames, n_mels] in GGML notation
struct ggml_tensor * mel = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_frames, cfg.n_mels);
ggml_set_name(mel, "mel");
ggml_set_input(mel);
// PyTorch: hidden_states = hidden_states.transpose(1, 2) # [B, T, C] -> [B, C, T]
// Our mel is [n_frames, n_mels] in GGML = [n_mels, n_frames] row-major
// For conv1d, we need [T, C, B] in GGML = [B, C, T] row-major
// So reshape to [n_frames, n_mels, 1]
struct ggml_tensor * cur = ggml_reshape_3d(ctx0, mel, n_frames, cfg.n_mels, 1);
ggml_set_name(cur, "mel_3d");
struct ggml_tensor * mel_padded = apply_reflect_pad_1d(ctx0, cur, 2);
ggml_set_name(mel_padded, "mel_padded");
cur = ggml_conv_1d(ctx0, model_.conv0_w, mel_padded, 1, 0, 1);
ggml_set_name(cur, "conv0_conv");
if (model_.conv0_b) {
int64_t oc = cur->ne[1];
cur = ggml_add(ctx0, cur, ggml_reshape_3d(ctx0, model_.conv0_b, 1, oc, 1));
}
ggml_set_name(cur, "conv0_pre_relu");
cur = ggml_relu(ctx0, cur);
ggml_set_name(cur, "conv0_out");
int64_t seq_len = cur->ne[0];
// Store block outputs for MFA (including block 0)
struct ggml_tensor * block_outputs[4];
block_outputs[0] = cur; // Initial TDNN output
// Blocks 1-3: SE-Res2Net blocks
// Dilations: block1=2, block2=3, block3=4
int dilations[3] = {2, 3, 4};
for (int blk = 0; blk < 3; ++blk) {
const auto & block = model_.blocks[blk];
int dilation = dilations[blk];
struct ggml_tensor * residual = cur;
cur = apply_conv1d(ctx0, block.tdnn1_w, block.tdnn1_b, cur, 1, 0, 1);
cur = ggml_relu(ctx0, cur);
if (blk == 0) {
ggml_set_name(cur, "blk1_tdnn1");
}
// Res2Net: Split into 8 branches of 64 channels each
// cur shape: [seq_len, 512, 1]
// Branch 0: identity (no conv)
// Branch i (1-7): conv(hidden_part + previous_output) for i >= 2, conv(hidden_part) for i == 1
// Split channels: view as [seq_len, 64, 8] then split
struct ggml_tensor * branches[8];
// Extract each branch using view operations
// cur is [seq_len, 512, 1], we want to split dim 1 into 8 parts of 64
for (int b = 0; b < scale; ++b) {
// View into the b-th chunk of 64 channels
// cur shape: [seq_len, 512, 1], we want [seq_len, 64, 1] starting at channel b*64
// nb1 = stride for dim 1 = cur->nb[1] (bytes to move from one channel to next)
// nb2 = stride for dim 2 = cur->nb[2] (bytes to move from one batch to next)
// offset = b * 64 * cur->nb[1] (skip b*64 channels)
branches[b] = ggml_view_3d(ctx0, cur,
seq_len, branch_dim, 1,
cur->nb[1], cur->nb[2],
b * branch_dim * cur->nb[1]);
branches[b] = ggml_cont(ctx0, branches[b]);
}
// Process branches according to Res2Net logic
struct ggml_tensor * outputs[8];
outputs[0] = branches[0]; // Branch 0: identity
for (int b = 1; b < scale; ++b) {
struct ggml_tensor * input;
if (b == 1) {
input = branches[b];
} else {
// Add previous output to current branch
input = ggml_add(ctx0, branches[b], outputs[b - 1]);
}
// Apply conv with dilation (kernel=3)
// Padding for kernel=3, dilation=d: pad = d * (3-1) / 2 = d
if (block.res2net_w[b - 1]) {
outputs[b] = apply_conv1d(ctx0, block.res2net_w[b - 1], block.res2net_b[b - 1],
input, 1, dilation, dilation);
outputs[b] = ggml_relu(ctx0, outputs[b]);
} else {
outputs[b] = input; // Fallback if weight missing
}
}
cur = outputs[0];
for (int b = 1; b < scale; ++b) {
cur = ggml_concat(ctx0, cur, outputs[b], 1);
}
if (blk == 0) {
ggml_set_name(cur, "blk1_res2net");
for (int b = 0; b < scale; ++b) {
char name[32];
snprintf(name, sizeof(name), "blk1_branch%d", b);
ggml_set_name(outputs[b], name);
}
}
cur = apply_conv1d(ctx0, block.tdnn2_w, block.tdnn2_b, cur, 1, 0, 1);
cur = ggml_relu(ctx0, cur);
if (blk == 0) {
ggml_set_name(cur, "blk1_tdnn2");
}
// SE (Squeeze-Excitation)
// Global average pooling over time: mean(dim=2, keepdim=True)
struct ggml_tensor * se = ggml_pool_1d(ctx0, cur, GGML_OP_POOL_AVG, seq_len, seq_len, 0);
se = ggml_reshape_3d(ctx0, se, 1, hidden_dim, 1);
// SE conv1: 512 -> 128 with ReLU
se = apply_conv1d(ctx0, block.se_conv1_w, block.se_conv1_b, se, 1, 0, 1);
se = ggml_relu(ctx0, se);
// SE conv2: 128 -> 512 with Sigmoid
se = apply_conv1d(ctx0, block.se_conv2_w, block.se_conv2_b, se, 1, 0, 1);
se = ggml_sigmoid(ctx0, se);
cur = ggml_mul(ctx0, cur, se);
if (blk == 0) {
ggml_set_name(cur, "blk1_se");
}
cur = ggml_add(ctx0, cur, residual);
char block_name[32];
snprintf(block_name, sizeof(block_name), "block_%d", blk + 1);
ggml_set_name(cur, block_name);
block_outputs[blk + 1] = cur;
}
// MFA: Concatenate block outputs [1:] (blocks 1, 2, 3 = indices 1, 2, 3)
// hidden_states = torch.cat(hidden_states_list[1:], dim=1)
// Each block output is [seq_len, 512, 1]
// Concatenated: [seq_len, 1536, 1]
struct ggml_tensor * mfa_input = ggml_concat(ctx0, block_outputs[1], block_outputs[2], 1);
mfa_input = ggml_concat(ctx0, mfa_input, block_outputs[3], 1);
ggml_set_name(mfa_input, "mfa_input");
// MFA conv: 1536 -> 1536 with ReLU
cur = apply_conv1d(ctx0, model_.mfa_w, model_.mfa_b, mfa_input, 1, 0, 1);
cur = ggml_relu(ctx0, cur);
ggml_set_name(cur, "mfa_out");
// ASP (Attentive Statistics Pooling)
// cur shape: [seq_len, 1536, 1]
// Step 1: Compute global mean and std over time
// mean = hidden_states.mean(dim=2, keepdim=True) # [1, 1536, 1]
struct ggml_tensor * global_mean = ggml_pool_1d(ctx0, cur, GGML_OP_POOL_AVG, seq_len, seq_len, 0);
global_mean = ggml_reshape_3d(ctx0, global_mean, 1, 1536, 1);
// std = sqrt(E[x^2] - E[x]^2)
struct ggml_tensor * sq = ggml_sqr(ctx0, cur);
struct ggml_tensor * mean_sq = ggml_pool_1d(ctx0, sq, GGML_OP_POOL_AVG, seq_len, seq_len, 0);
mean_sq = ggml_reshape_3d(ctx0, mean_sq, 1, 1536, 1);
struct ggml_tensor * var = ggml_sub(ctx0, mean_sq, ggml_sqr(ctx0, global_mean));
var = ggml_clamp(ctx0, var, 1e-12f, 1e10f);
struct ggml_tensor * global_std = ggml_sqrt(ctx0, var);
// Step 2: Expand mean and std to full sequence length and concatenate with hidden_states
// mean = mean.repeat(1, 1, seq_length) # [1, 1536, seq_len]
// std = std.repeat(1, 1, seq_length) # [1, 1536, seq_len]
// attention = torch.cat([hidden_states, mean, std], dim=1) # [1, 4608, seq_len]
struct ggml_tensor * mean_expanded = ggml_repeat(ctx0, global_mean,
ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, seq_len, 1536, 1));
struct ggml_tensor * std_expanded = ggml_repeat(ctx0, global_std,
ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, seq_len, 1536, 1));
struct ggml_tensor * attention = ggml_concat(ctx0, cur, mean_expanded, 1);
attention = ggml_concat(ctx0, attention, std_expanded, 1);
// attention shape: [seq_len, 4608, 1]
// Step 3: TDNN (4608 -> 128) with ReLU, then Tanh
// self.tdnn = TimeDelayNetBlock(channels * 3, attention_channels, 1, 1) # has ReLU
// attention = self.conv(self.tanh(self.tdnn(attention)))
attention = apply_conv1d(ctx0, model_.asp_tdnn_w, model_.asp_tdnn_b, attention, 1, 0, 1);
attention = ggml_relu(ctx0, attention); // TDNN has ReLU
ggml_set_name(attention, "asp_tdnn");
attention = ggml_tanh(ctx0, attention); // Then tanh is applied
// Step 4: Conv (128 -> 1536) for attention weights
// self.conv = nn.Conv1d(attention_channels, channels, kernel_size=1)
attention = apply_conv1d(ctx0, model_.asp_conv_w, model_.asp_conv_b, attention, 1, 0, 1);
ggml_set_name(attention, "asp_conv");
// attention shape: [seq_len, 1536, 1]
// Step 5: Softmax over time dimension
attention = ggml_soft_max(ctx0, attention);
ggml_set_name(attention, "asp_softmax");
// Step 6: Compute weighted mean and std
// mean, std = self._compute_statistics(hidden_states, attention)
// mean = (attention * hidden_states).sum(dim=2)
struct ggml_tensor * weighted = ggml_mul(ctx0, attention, cur);
struct ggml_tensor * weighted_mean = ggml_pool_1d(ctx0, weighted, GGML_OP_POOL_AVG, seq_len, seq_len, 0);
weighted_mean = ggml_scale(ctx0, weighted_mean, (float)seq_len); // Convert avg to sum
weighted_mean = ggml_reshape_3d(ctx0, weighted_mean, 1, 1536, 1);
// std = sqrt((attention * (hidden_states - mean)^2).sum(dim=2).clamp(eps))
struct ggml_tensor * mean_for_std = ggml_repeat(ctx0, weighted_mean,
ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, seq_len, 1536, 1));
struct ggml_tensor * diff = ggml_sub(ctx0, cur, mean_for_std);
struct ggml_tensor * diff_sq = ggml_sqr(ctx0, diff);
struct ggml_tensor * weighted_var = ggml_mul(ctx0, attention, diff_sq);
struct ggml_tensor * var_sum = ggml_pool_1d(ctx0, weighted_var, GGML_OP_POOL_AVG, seq_len, seq_len, 0);
var_sum = ggml_scale(ctx0, var_sum, (float)seq_len); // Convert avg to sum
var_sum = ggml_reshape_3d(ctx0, var_sum, 1, 1536, 1);
var_sum = ggml_clamp(ctx0, var_sum, 1e-12f, 1e10f);
struct ggml_tensor * weighted_std = ggml_sqrt(ctx0, var_sum);
// Step 7: Concatenate mean and std: [1, 3072, 1]
struct ggml_tensor * pooled = ggml_concat(ctx0, weighted_mean, weighted_std, 1);
ggml_set_name(pooled, "asp_pooled");
// FC: 3072 -> 1024
cur = apply_conv1d(ctx0, model_.fc_w, model_.fc_b, pooled, 1, 0, 1);
ggml_set_name(cur, "fc_out");
// Squeeze to 1D
cur = ggml_reshape_1d(ctx0, cur, cfg.embedding_dim);
ggml_set_name(cur, "embedding");
ggml_set_output(cur);
ggml_build_forward_expand(gf, cur);
ggml_free(ctx0);
return gf;
}
bool AudioTokenizerEncoder::encode(const float * samples, int32_t n_samples,
std::vector<float> & embedding) {
if (!model_.ctx) {
error_msg_ = "Model not loaded";
return false;
}
std::vector<float> mel;
int32_t n_frames;
if (!compute_mel_spectrogram(samples, n_samples, mel, n_frames)) {
return false;
}
struct ggml_cgraph * gf = build_graph(n_frames);
if (!ggml_backend_sched_alloc_graph(state_.sched, gf)) {
error_msg_ = "Failed to allocate graph";
return false;
}
struct ggml_tensor * mel_tensor = ggml_graph_get_tensor(gf, "mel");
if (!mel_tensor) {
error_msg_ = "Failed to find mel tensor";
ggml_backend_sched_reset(state_.sched);
return false;
}
// mel is stored as [n_mels, n_frames] row-major: mel[m * n_frames + f] = mel bin m at frame f
// GGML tensor is [n_frames, n_mels] column-major: element (f, m) at memory[f + m * n_frames]
// For GGML conv1d, we want input(t, c) = mel bin c at time t
// So GGML memory[t + c * n_frames] should equal mel[c * n_frames + t]
// Since the memory layout matches (both are contiguous in frame order for each mel bin),
// we can copy directly!
ggml_backend_tensor_set(mel_tensor, mel.data(), 0, mel.size() * sizeof(float));
if (ggml_backend_sched_graph_compute(state_.sched, gf) != GGML_STATUS_SUCCESS) {
error_msg_ = "Failed to compute graph";
ggml_backend_sched_reset(state_.sched);
return false;
}
struct ggml_tensor * emb_tensor = ggml_graph_get_tensor(gf, "embedding");
if (!emb_tensor) {
error_msg_ = "Failed to find embedding tensor";
ggml_backend_sched_reset(state_.sched);
return false;
}
embedding.resize(model_.config.embedding_dim);
ggml_backend_tensor_get(emb_tensor, embedding.data(), 0, embedding.size() * sizeof(float));
ggml_backend_sched_reset(state_.sched);
return true;
}
void free_speaker_encoder_model(speaker_encoder_model & model) {
if (model.buffer) {
ggml_backend_buffer_free(model.buffer);
model.buffer = nullptr;
}
if (model.ctx) {
ggml_free(model.ctx);
model.ctx = nullptr;
}
model.tensors.clear();
}
} // namespace qwen3_tts

View file

@ -0,0 +1,154 @@
#pragma once
#include "ggml.h"
#include "ggml-backend.h"
#include "gguf.h"
#include <string>
#include <map>
#include <vector>
#include <memory>
namespace qwen3_tts {
// Speaker encoder configuration (ECAPA-TDNN)
// Mel parameters MUST match extract_speaker_embedding() in modeling_qwen3_tts.py
struct speaker_encoder_config {
int32_t sample_rate = 24000;
int32_t n_mels = 128;
int32_t n_fft = 1024;
int32_t hop_length = 256;
int32_t win_length = 1024;
int32_t embedding_dim = 1024;
int32_t hidden_dim = 512;
int32_t n_res2net_blocks = 3;
int32_t res2net_scale = 8;
float f_min = 0.0f;
float f_max = 12000.0f;
};
// Res2Net block weights
struct res2net_block {
// TDNN1: 1x1 conv (512 -> 512)
struct ggml_tensor * tdnn1_w = nullptr;
struct ggml_tensor * tdnn1_b = nullptr;
// Res2Net branches: 7 conv layers (kernel=3, 64 -> 64)
struct ggml_tensor * res2net_w[7] = {nullptr};
struct ggml_tensor * res2net_b[7] = {nullptr};
// TDNN2: 1x1 conv (512 -> 512)
struct ggml_tensor * tdnn2_w = nullptr;
struct ggml_tensor * tdnn2_b = nullptr;
// SE (Squeeze-Excitation)
struct ggml_tensor * se_conv1_w = nullptr;
struct ggml_tensor * se_conv1_b = nullptr;
struct ggml_tensor * se_conv2_w = nullptr;
struct ggml_tensor * se_conv2_b = nullptr;
};
// Speaker encoder model weights
struct speaker_encoder_model {
speaker_encoder_config config;
// Initial conv: (5, 128, 512) - kernel 5, in 128 (mel), out 512
struct ggml_tensor * conv0_w = nullptr;
struct ggml_tensor * conv0_b = nullptr;
// Res2Net blocks (3 blocks)
res2net_block blocks[3];
// MFA (Multi-Frame Aggregation): 1x1 conv (1536 -> 1536)
struct ggml_tensor * mfa_w = nullptr;
struct ggml_tensor * mfa_b = nullptr;
// ASP (Attentive Statistics Pooling)
struct ggml_tensor * asp_conv_w = nullptr;
struct ggml_tensor * asp_conv_b = nullptr;
struct ggml_tensor * asp_tdnn_w = nullptr;
struct ggml_tensor * asp_tdnn_b = nullptr;
// Final FC: (3072 -> 1024)
struct ggml_tensor * fc_w = nullptr;
struct ggml_tensor * fc_b = nullptr;
// GGML context for tensor metadata
struct ggml_context * ctx = nullptr;
// Backend buffer for weights
ggml_backend_buffer_t buffer = nullptr;
// Tensor name to tensor mapping
std::map<std::string, struct ggml_tensor *> tensors;
};
// Compute state for speaker encoder
struct speaker_encoder_state {
ggml_backend_t backend = nullptr;
ggml_backend_t backend_cpu = nullptr;
ggml_backend_sched_t sched = nullptr;
std::vector<uint8_t> compute_meta;
};
// Speaker encoder class (ECAPA-TDNN)
// Extracts speaker embedding from audio waveform
class AudioTokenizerEncoder {
public:
AudioTokenizerEncoder();
~AudioTokenizerEncoder();
// Load model from GGUF file (main TTS model, not tokenizer)
bool load_model(const std::string & model_path);
// Encode audio samples to speaker embedding
// samples: audio samples normalized to [-1, 1], 24kHz
// n_samples: number of samples
// embedding: output speaker embedding [1024]
bool encode(const float * samples, int32_t n_samples,
std::vector<float> & embedding);
// Legacy interface for compatibility (not used for speaker encoding)
bool encode(const float * samples, int32_t n_samples,
std::vector<int32_t> & codes, int32_t & n_frames) {
(void)samples; (void)n_samples; (void)codes; (void)n_frames;
error_msg_ = "Use encode(samples, n_samples, embedding) for speaker encoding";
return false;
}
// Legacy interface (not used)
bool get_embeddings(const int32_t * codes, int32_t n_frames,
std::vector<float> & embeddings) {
(void)codes; (void)n_frames; (void)embeddings;
error_msg_ = "Use encode() for speaker embedding extraction";
return false;
}
const speaker_encoder_config & get_config() const { return model_.config; }
const std::string & get_error() const { return error_msg_; }
private:
// Compute mel spectrogram from waveform
bool compute_mel_spectrogram(const float * samples, int32_t n_samples,
std::vector<float> & mel, int32_t & n_frames);
// Build computation graph
struct ggml_cgraph * build_graph(int32_t n_frames);
speaker_encoder_model model_;
speaker_encoder_state state_;
std::string error_msg_;
};
// Free model resources
void free_speaker_encoder_model(speaker_encoder_model & model);
// Backward compatibility alias
using audio_encoder_config = speaker_encoder_config;
using audio_encoder_model = speaker_encoder_model;
inline void free_audio_encoder_model(audio_encoder_model & model) {
free_speaker_encoder_model(model);
}
} // namespace qwen3_tts

View file

@ -0,0 +1,35 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace qwen3_tts {
class CoreMLCodePredictor {
public:
CoreMLCodePredictor();
~CoreMLCodePredictor();
CoreMLCodePredictor(const CoreMLCodePredictor &) = delete;
CoreMLCodePredictor & operator=(const CoreMLCodePredictor &) = delete;
bool load(const std::string & model_dir, int32_t n_steps);
void unload();
bool is_loaded() const;
const std::string & get_error() const;
bool predict_step(int32_t step_idx,
const float * seq_embd,
int32_t seq_len,
int32_t hidden_size,
std::vector<float> & logits_out);
private:
struct Impl;
Impl * impl_ = nullptr;
};
} // namespace qwen3_tts

View file

@ -0,0 +1,31 @@
#include "coreml_code_predictor.h"
namespace qwen3_tts {
CoreMLCodePredictor::CoreMLCodePredictor() {}
CoreMLCodePredictor::~CoreMLCodePredictor() {}
bool CoreMLCodePredictor::load(const std::string &, int32_t) {
return false;
}
void CoreMLCodePredictor::unload() {}
bool CoreMLCodePredictor::is_loaded() const {
return false;
}
const std::string & CoreMLCodePredictor::get_error() const {
static const std::string err = "CoreML predictor only supported on Apple platforms";
return err;
}
bool CoreMLCodePredictor::predict_step(int32_t,
const float *,
int32_t,
int32_t,
std::vector<float> &) {
return false;
}
} // namespace qwen3_tts

View file

@ -0,0 +1,238 @@
#include "gguf_loader.h"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace qwen3_tts {
namespace {
struct shared_backend_state {
ggml_backend_t backend = nullptr;
int32_t ref_count = 0;
};
shared_backend_state & get_shared_backend_state() {
static shared_backend_state state;
return state;
}
}
GGUFLoader::GGUFLoader() = default;
GGUFLoader::~GGUFLoader() {
close();
}
ggml_backend_t init_preferred_backend(const char * component_name, std::string * error_msg) {
if (error_msg) error_msg->clear();
auto & shared = get_shared_backend_state();
if (shared.backend) {
shared.ref_count++;
return shared.backend;
}
ggml_backend_t backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_IGPU, nullptr);
if (!backend) {
backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_GPU, nullptr);
}
if (!backend) {
backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_ACCEL, nullptr);
}
if (!backend) {
backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);
}
if (!backend && error_msg) {
const char * name = component_name ? component_name : "component";
*error_msg = "Failed to initialize backend (IGPU/GPU/ACCEL/CPU) for " + std::string(name);
}
if (backend) {
shared.backend = backend;
shared.ref_count = 1;
}
return backend;
}
void release_preferred_backend(ggml_backend_t backend) {
if (!backend) {
return;
}
auto & shared = get_shared_backend_state();
if (shared.backend == backend) {
shared.ref_count--;
if (shared.ref_count <= 0) {
ggml_backend_free(shared.backend);
shared.backend = nullptr;
shared.ref_count = 0;
}
return;
}
ggml_backend_free(backend);
}
bool GGUFLoader::open(const std::string & path) {
close(); // Close any previously opened file
file_path_ = path;
struct gguf_init_params params = {
/*.no_alloc =*/ true,
/*.ctx =*/ &meta_ctx_,
};
ctx_ = gguf_init_from_file(path.c_str(), params);
if (!ctx_) {
error_msg_ = "Failed to open GGUF file: " + path;
return false;
}
return true;
}
void GGUFLoader::close() {
if (ctx_) {
gguf_free(ctx_);
ctx_ = nullptr;
}
if (meta_ctx_) {
ggml_free(meta_ctx_);
meta_ctx_ = nullptr;
}
file_path_.clear();
}
int64_t GGUFLoader::get_n_tensors() const {
if (!ctx_) return 0;
return gguf_get_n_tensors(ctx_);
}
const char * GGUFLoader::get_tensor_name(int64_t idx) const {
if (!ctx_) return nullptr;
return gguf_get_tensor_name(ctx_, idx);
}
enum ggml_type GGUFLoader::get_tensor_type(int64_t idx) const {
if (!ctx_) return GGML_TYPE_F32;
return gguf_get_tensor_type(ctx_, idx);
}
size_t GGUFLoader::get_tensor_offset(int64_t idx) const {
if (!ctx_) return 0;
return gguf_get_tensor_offset(ctx_, idx);
}
size_t GGUFLoader::get_tensor_size(int64_t idx) const {
if (!ctx_) return 0;
return gguf_get_tensor_size(ctx_, idx);
}
int32_t GGUFLoader::get_u32(const char * key, int32_t default_val) const {
if (!ctx_) return default_val;
int64_t idx = gguf_find_key(ctx_, key);
if (idx < 0) return default_val;
return (int32_t)gguf_get_val_u32(ctx_, idx);
}
float GGUFLoader::get_f32(const char * key, float default_val) const {
if (!ctx_) return default_val;
int64_t idx = gguf_find_key(ctx_, key);
if (idx < 0) return default_val;
return gguf_get_val_f32(ctx_, idx);
}
size_t GGUFLoader::get_data_offset() const {
if (!ctx_) return 0;
return gguf_get_data_offset(ctx_);
}
bool load_tensor_data_from_file(
const std::string & path,
struct gguf_context * ctx,
struct ggml_context * model_ctx,
const std::map<std::string, struct ggml_tensor *> & tensors,
ggml_backend_buffer_t & buffer,
std::string & error_msg,
enum ggml_backend_dev_type preferred_backend_type
) {
ggml_backend_t backend = ggml_backend_init_by_type(preferred_backend_type, nullptr);
if (!backend && preferred_backend_type != GGML_BACKEND_DEVICE_TYPE_CPU) {
backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);
}
if (!backend) {
error_msg = "Failed to initialize backend for GGUF tensor loader";
return false;
}
// Allocate buffer for all tensors
buffer = ggml_backend_alloc_ctx_tensors(model_ctx, backend);
if (!buffer) {
error_msg = "Failed to allocate tensor buffer";
ggml_backend_free(backend);
return false;
}
// Open file for reading tensor data
FILE * f = fopen(path.c_str(), "rb");
if (!f) {
error_msg = "Failed to open file for reading: " + path;
ggml_backend_free(backend);
return false;
}
const size_t data_offset = gguf_get_data_offset(ctx);
const int64_t n_tensors = gguf_get_n_tensors(ctx);
std::vector<uint8_t> read_buf;
for (int64_t i = 0; i < n_tensors; ++i) {
const char * name = gguf_get_tensor_name(ctx, i);
size_t offset = gguf_get_tensor_offset(ctx, i);
auto it = tensors.find(name);
if (it == tensors.end()) {
continue; // Skip tensors not in our map
}
struct ggml_tensor * tensor = it->second;
size_t nbytes = ggml_nbytes(tensor);
read_buf.resize(nbytes);
if (fseek(f, data_offset + offset, SEEK_SET) != 0) {
error_msg = "Failed to seek to tensor data: " + std::string(name);
fclose(f);
ggml_backend_free(backend);
return false;
}
if (fread(read_buf.data(), 1, nbytes, f) != nbytes) {
error_msg = "Failed to read tensor data: " + std::string(name);
fclose(f);
ggml_backend_free(backend);
return false;
}
ggml_backend_tensor_set(tensor, read_buf.data(), 0, nbytes);
}
fclose(f);
ggml_backend_free(backend);
return true;
}
void free_ggml_resources(struct ggml_context * ctx, ggml_backend_buffer_t buffer) {
if (buffer) {
ggml_backend_buffer_free(buffer);
}
if (ctx) {
ggml_free(ctx);
}
}
} // namespace qwen3_tts

View file

@ -0,0 +1,83 @@
#pragma once
#include "ggml.h"
#include "ggml-backend.h"
#include "gguf.h"
#include <string>
#include <map>
#include <vector>
#include <memory>
namespace qwen3_tts {
// Generic GGUF model loader class
// This is a simplified loader that can be extended for specific model types
class GGUFLoader {
public:
GGUFLoader();
~GGUFLoader();
// Open GGUF file and parse metadata
bool open(const std::string & path);
// Close file and free resources
void close();
// Get error message if operation failed
const std::string & get_error() const { return error_msg_; }
// Get number of tensors in file
int64_t get_n_tensors() const;
// Get tensor name by index
const char * get_tensor_name(int64_t idx) const;
// Get tensor type by index
enum ggml_type get_tensor_type(int64_t idx) const;
// Get tensor offset by index
size_t get_tensor_offset(int64_t idx) const;
// Get tensor size by index
size_t get_tensor_size(int64_t idx) const;
// Get metadata value (returns -1 if not found)
int32_t get_u32(const char * key, int32_t default_val = 0) const;
float get_f32(const char * key, float default_val = 0.0f) const;
// Get data offset (start of tensor data in file)
size_t get_data_offset() const;
// Get GGUF context (for advanced usage)
struct gguf_context * get_ctx() const { return ctx_; }
// Get metadata context
struct ggml_context * get_meta_ctx() const { return meta_ctx_; }
protected:
struct gguf_context * ctx_ = nullptr;
struct ggml_context * meta_ctx_ = nullptr;
std::string error_msg_;
std::string file_path_;
};
// Helper function to allocate and load tensor data from GGUF file
bool load_tensor_data_from_file(
const std::string & path,
struct gguf_context * ctx,
struct ggml_context * model_ctx,
const std::map<std::string, struct ggml_tensor *> & tensors,
ggml_backend_buffer_t & buffer,
std::string & error_msg,
enum ggml_backend_dev_type preferred_backend_type = GGML_BACKEND_DEVICE_TYPE_CPU
);
// Helper to initialize backend with GPU preference and CPU fallback
ggml_backend_t init_preferred_backend(const char * component_name, std::string * error_msg);
void release_preferred_backend(ggml_backend_t backend);
// Helper function to free model resources
void free_ggml_resources(struct ggml_context * ctx, ggml_backend_buffer_t buffer);
} // namespace qwen3_tts

178
otherarch/qwen3tts/main.cpp Normal file
View file

@ -0,0 +1,178 @@
#include "qwen3_tts.h"
#include <cstdio>
#include <cstring>
#include <string>
void print_usage(const char * program) {
fprintf(stderr, "Usage: %s [options] -m <model_dir> -t <text>\n", program);
fprintf(stderr, "\n");
fprintf(stderr, "Options:\n");
fprintf(stderr, " -m, --model <dir> Model directory (required)\n");
fprintf(stderr, " -t, --text <text> Text to synthesize (required)\n");
fprintf(stderr, " -o, --output <file> Output WAV file (default: output.wav)\n");
fprintf(stderr, " -r, --reference <file> Reference audio for voice cloning\n");
fprintf(stderr, " --temperature <val> Sampling temperature (default: 0.9, 0=greedy)\n");
fprintf(stderr, " --top-k <n> Top-k sampling (default: 50, 0=disabled)\n");
fprintf(stderr, " --top-p <val> Top-p sampling (default: 1.0)\n");
fprintf(stderr, " --max-tokens <n> Maximum audio tokens (default: 4096)\n");
fprintf(stderr, " --repetition-penalty <val> Repetition penalty (default: 1.05)\n");
fprintf(stderr, " -j, --threads <n> Number of threads (default: 4)\n");
fprintf(stderr, " -h, --help Show this help\n");
fprintf(stderr, "\n");
fprintf(stderr, "Example:\n");
fprintf(stderr, " %s -m ./models -t \"Hello, world!\" -o hello.wav\n", program);
fprintf(stderr, " %s -m ./models -t \"Hello!\" -r reference.wav -o cloned.wav\n", program);
}
int main(int argc, char ** argv) {
std::string model_dir;
std::string text;
std::string output_file = "output.wav";
std::string reference_audio;
qwen3_tts::tts_params params;
// Parse arguments
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
print_usage(argv[0]);
return 0;
} else if (arg == "-m" || arg == "--model") {
if (++i >= argc) {
fprintf(stderr, "Error: missing model directory\n");
return 1;
}
model_dir = argv[i];
} else if (arg == "-t" || arg == "--text") {
if (++i >= argc) {
fprintf(stderr, "Error: missing text\n");
return 1;
}
text = argv[i];
} else if (arg == "-o" || arg == "--output") {
if (++i >= argc) {
fprintf(stderr, "Error: missing output file\n");
return 1;
}
output_file = argv[i];
} else if (arg == "-r" || arg == "--reference") {
if (++i >= argc) {
fprintf(stderr, "Error: missing reference audio\n");
return 1;
}
reference_audio = argv[i];
} else if (arg == "--temperature") {
if (++i >= argc) {
fprintf(stderr, "Error: missing temperature value\n");
return 1;
}
params.temperature = std::stof(argv[i]);
} else if (arg == "--top-k") {
if (++i >= argc) {
fprintf(stderr, "Error: missing top-k value\n");
return 1;
}
params.top_k = std::stoi(argv[i]);
} else if (arg == "--top-p") {
if (++i >= argc) {
fprintf(stderr, "Error: missing top-p value\n");
return 1;
}
params.top_p = std::stof(argv[i]);
} else if (arg == "--max-tokens") {
if (++i >= argc) {
fprintf(stderr, "Error: missing max-tokens value\n");
return 1;
}
params.max_audio_tokens = std::stoi(argv[i]);
} else if (arg == "--repetition-penalty") {
if (++i >= argc) {
fprintf(stderr, "Error: missing repetition-penalty value\n");
return 1;
}
params.repetition_penalty = std::stof(argv[i]);
} else if (arg == "-j" || arg == "--threads") {
if (++i >= argc) {
fprintf(stderr, "Error: missing threads value\n");
return 1;
}
params.n_threads = std::stoi(argv[i]);
} else {
fprintf(stderr, "Error: unknown argument: %s\n", arg.c_str());
print_usage(argv[0]);
return 1;
}
}
// Validate required arguments
if (model_dir.empty()) {
fprintf(stderr, "Error: model directory is required\n");
print_usage(argv[0]);
return 1;
}
if (text.empty()) {
fprintf(stderr, "Error: text is required\n");
print_usage(argv[0]);
return 1;
}
// Initialize TTS
qwen3_tts::Qwen3TTS tts;
fprintf(stderr, "Loading models from: %s\n", model_dir.c_str());
if (!tts.load_models(model_dir)) {
fprintf(stderr, "Error: %s\n", tts.get_error().c_str());
return 1;
}
// Set progress callback
tts.set_progress_callback([](int tokens, int max_tokens) {
fprintf(stderr, "\rGenerating: %d/%d tokens", tokens, max_tokens);
});
// Generate speech
qwen3_tts::tts_result result;
if (reference_audio.empty()) {
fprintf(stderr, "Synthesizing: \"%s\"\n", text.c_str());
result = tts.synthesize(text, params);
} else {
fprintf(stderr, "Synthesizing with voice cloning: \"%s\"\n", text.c_str());
fprintf(stderr, "Reference audio: %s\n", reference_audio.c_str());
result = tts.synthesize_with_voice(text, reference_audio, params);
}
if (!result.success) {
fprintf(stderr, "\nError: %s\n", result.error_msg.c_str());
return 1;
}
fprintf(stderr, "\n");
// Save output
if (!qwen3_tts::save_audio_file(output_file, result.audio, result.sample_rate)) {
fprintf(stderr, "Error: failed to save output file: %s\n", output_file.c_str());
return 1;
}
fprintf(stderr, "Output saved to: %s\n", output_file.c_str());
fprintf(stderr, "Audio duration: %.2f seconds\n",
(float)result.audio.size() / result.sample_rate);
// Print timing
if (params.print_timing) {
fprintf(stderr, "\nTiming:\n");
fprintf(stderr, " Load: %6lld ms\n", (long long)result.t_load_ms);
fprintf(stderr, " Tokenize: %6lld ms\n", (long long)result.t_tokenize_ms);
fprintf(stderr, " Encode: %6lld ms\n", (long long)result.t_encode_ms);
fprintf(stderr, " Generate: %6lld ms\n", (long long)result.t_generate_ms);
fprintf(stderr, " Decode: %6lld ms\n", (long long)result.t_decode_ms);
fprintf(stderr, " Total: %6lld ms\n", (long long)result.t_total_ms);
}
return 0;
}

View file

@ -0,0 +1,585 @@
#include "qwen3_tts.h"
#include "gguf_loader.h"
#include <cstdio>
#include <cstring>
#include <chrono>
#include <cmath>
#include <fstream>
#include <cstdint>
#include <cstdlib>
namespace qwen3_tts {
static int64_t get_time_ms() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
struct process_memory_snapshot {
uint64_t rss_bytes = 0;
uint64_t phys_footprint_bytes = 0;
};
static bool get_process_memory_snapshot(process_memory_snapshot & out) {
return false;
}
static std::string format_bytes(uint64_t bytes) {
static const char * units[] = { "B", "KB", "MB", "GB", "TB" };
double val = (double) bytes;
int unit = 0;
while (val >= 1024.0 && unit < 4) {
val /= 1024.0;
++unit;
}
char buf[64];
snprintf(buf, sizeof(buf), "%.2f %s", val, units[unit]);
return std::string(buf);
}
static void log_memory_usage(const char * label) {
}
static void resample_linear(const float * input, int input_len, int input_rate,
std::vector<float> & output, int output_rate) {
double ratio = (double)input_rate / output_rate;
int output_len = (int)((double)input_len / ratio);
output.resize(output_len);
for (int i = 0; i < output_len; ++i) {
double src_idx = i * ratio;
int idx0 = (int)src_idx;
int idx1 = idx0 + 1;
double frac = src_idx - idx0;
if (idx1 >= input_len) {
output[i] = input[input_len - 1];
} else {
output[i] = (float)((1.0 - frac) * input[idx0] + frac * input[idx1]);
}
}
}
Qwen3TTS::Qwen3TTS() = default;
Qwen3TTS::~Qwen3TTS() = default;
bool Qwen3TTS::load_models(const std::string & model_dir) {
int64_t t_start = get_time_ms();
log_memory_usage("load/start");
transformer_.unload_model();
audio_decoder_.unload_model();
transformer_loaded_ = false;
decoder_loaded_ = false;
// Construct model paths
std::string tts_model_path = model_dir + "/qwen3-tts-0.6b-f16.gguf";
std::string tokenizer_model_path = model_dir + "/qwen3-tts-tokenizer-f16.gguf";
tts_model_path_ = tts_model_path;
decoder_model_path_ = tokenizer_model_path;
encoder_loaded_ = false;
transformer_loaded_ = false;
decoder_loaded_ = false;
const char * low_mem_env = std::getenv("QWEN3_TTS_LOW_MEM");
low_mem_mode_ = low_mem_env && low_mem_env[0] != '\0' && low_mem_env[0] != '0';
if (low_mem_mode_) {
fprintf(stderr, " Low-memory mode enabled (lazy decoder + component unloads)\n");
}
// Load TTS model (contains text tokenizer + transformer for generation)
fprintf(stderr, "Loading TTS model from %s...\n", tts_model_path.c_str());
// Load text tokenizer from TTS model
int64_t t_tokenizer_start = get_time_ms();
{
GGUFLoader loader;
if (!loader.open(tts_model_path)) {
error_msg_ = "Failed to open TTS model: " + loader.get_error();
return false;
}
if (!tokenizer_.load_from_gguf(loader.get_ctx())) {
error_msg_ = "Failed to load text tokenizer: " + tokenizer_.get_error();
return false;
}
fprintf(stderr, " Text tokenizer loaded: vocab_size=%d (%lld ms)\n",
tokenizer_.get_config().vocab_size,
(long long)(get_time_ms() - t_tokenizer_start));
}
log_memory_usage("load/after-tokenizer");
// Speaker encoder is loaded lazily on first voice cloning request.
fprintf(stderr, " Speaker encoder: deferred (lazy load)\n");
// Load TTS transformer from TTS model
int64_t t_transformer_start = get_time_ms();
if (!transformer_.load_model(tts_model_path)) {
error_msg_ = "Failed to load TTS transformer: " + transformer_.get_error();
return false;
}
transformer_loaded_ = true;
fprintf(stderr, " TTS transformer loaded: hidden_size=%d, n_layers=%d (%lld ms)\n",
transformer_.get_config().hidden_size, transformer_.get_config().n_layers,
(long long)(get_time_ms() - t_transformer_start));
log_memory_usage("load/after-transformer");
if (!low_mem_mode_) {
// Load vocoder (audio decoder) from tokenizer model
fprintf(stderr, "Loading vocoder from %s...\n", tokenizer_model_path.c_str());
int64_t t_decoder_start = get_time_ms();
if (!audio_decoder_.load_model(tokenizer_model_path)) {
error_msg_ = "Failed to load vocoder: " + audio_decoder_.get_error();
return false;
}
decoder_loaded_ = true;
fprintf(stderr, " Vocoder loaded: sample_rate=%d, n_codebooks=%d (%lld ms)\n",
audio_decoder_.get_config().sample_rate, audio_decoder_.get_config().n_codebooks,
(long long)(get_time_ms() - t_decoder_start));
log_memory_usage("load/after-vocoder");
} else {
fprintf(stderr, " Vocoder: deferred (lazy load)\n");
}
models_loaded_ = true;
int64_t t_end = get_time_ms();
fprintf(stderr, "All models loaded in %lld ms\n", (long long)(t_end - t_start));
log_memory_usage("load/end");
return true;
}
tts_result Qwen3TTS::synthesize(const std::string & text,
const tts_params & params) {
tts_result result;
if (!models_loaded_) {
result.error_msg = "Models not loaded";
return result;
}
// For basic synthesis without voice cloning, we use a zero speaker embedding
// This will use the model's default voice characteristics
std::vector<float> zero_embedding(transformer_.get_config().hidden_size, 0.0f);
return synthesize_internal(text, zero_embedding.data(), params, result);
}
tts_result Qwen3TTS::synthesize_with_voice(const std::string & text,
const std::string & reference_audio,
const tts_params & params) {
tts_result result;
std::vector<float> ref_samples;
int ref_sample_rate;
if (!load_audio_file(reference_audio, ref_samples, ref_sample_rate)) {
result.error_msg = "Failed to load reference audio: " + reference_audio;
return result;
}
const int target_rate = 24000;
if (ref_sample_rate != target_rate) {
fprintf(stderr, "Resampling audio from %d Hz to %d Hz...\n", ref_sample_rate, target_rate);
std::vector<float> resampled;
resample_linear(ref_samples.data(), (int)ref_samples.size(), ref_sample_rate, resampled, target_rate);
ref_samples = std::move(resampled);
}
return synthesize_with_voice(text, ref_samples.data(), (int32_t)ref_samples.size(), params);
}
tts_result Qwen3TTS::synthesize_with_voice(const std::string & text,
const float * ref_samples, int32_t n_ref_samples,
const tts_params & params) {
tts_result result;
if (!models_loaded_) {
result.error_msg = "Models not loaded";
return result;
}
if (!encoder_loaded_) {
if (tts_model_path_.empty()) {
result.error_msg = "Internal error: missing TTS model path for lazy encoder load";
return result;
}
int64_t t_encoder_load_start = get_time_ms();
if (!audio_encoder_.load_model(tts_model_path_)) {
result.error_msg = "Failed to load speaker encoder: " + audio_encoder_.get_error();
return result;
}
encoder_loaded_ = true;
if (params.print_timing) {
fprintf(stderr, " Speaker encoder lazy-loaded in %lld ms\n",
(long long)(get_time_ms() - t_encoder_load_start));
log_memory_usage("voice/after-encoder-load");
}
}
int64_t t_encode_start = get_time_ms();
std::vector<float> speaker_embedding;
if (!audio_encoder_.encode(ref_samples, n_ref_samples, speaker_embedding)) {
result.error_msg = "Failed to extract speaker embedding: " + audio_encoder_.get_error();
return result;
}
result.t_encode_ms = get_time_ms() - t_encode_start;
if (params.print_progress) {
fprintf(stderr, "Speaker embedding extracted: %zu floats\n", speaker_embedding.size());
}
return synthesize_internal(text, speaker_embedding.data(), params, result);
}
tts_result Qwen3TTS::synthesize_internal(const std::string & text,
const float * speaker_embedding,
const tts_params & params,
tts_result & result) {
int64_t t_total_start = get_time_ms();
auto sample_memory = [&](const char * stage) {
};
sample_memory("synth/start");
// Step 2: Tokenize input text
int64_t t_tokenize_start = get_time_ms();
std::vector<int32_t> text_tokens = tokenizer_.encode_for_tts(text);
result.t_tokenize_ms = get_time_ms() - t_tokenize_start;
sample_memory("synth/after-tokenize");
if (text_tokens.empty()) {
result.error_msg = "Failed to tokenize text";
return result;
}
if (params.print_progress) {
fprintf(stderr, "Text tokenized: %zu tokens\n", text_tokens.size());
fprintf(stderr, " Tokens: ");
for (size_t i = 0; i < std::min(text_tokens.size(), (size_t)10); ++i) {
fprintf(stderr, "%d ", text_tokens[i]);
}
if (text_tokens.size() > 10) fprintf(stderr, "...");
fprintf(stderr, "\n");
}
// Step 3: Generate speech codes using TTS transformer
int64_t t_generate_start = get_time_ms();
if (!transformer_loaded_) {
int64_t t_reload_start = get_time_ms();
if (!transformer_.load_model(tts_model_path_)) {
result.error_msg = "Failed to reload TTS transformer: " + transformer_.get_error();
return result;
}
transformer_loaded_ = true;
if (params.print_timing) {
fprintf(stderr, " Transformer reloaded in %lld ms\n",
(long long)(get_time_ms() - t_reload_start));
sample_memory("synth/after-transformer-reload");
}
}
transformer_.clear_kv_cache();
std::vector<int32_t> speech_codes;
if (!transformer_.generate(text_tokens.data(), (int32_t)text_tokens.size(),
speaker_embedding, params.max_audio_tokens, speech_codes,
2050, params.repetition_penalty,
params.temperature, params.top_k)) {
result.error_msg = "Failed to generate speech codes: " + transformer_.get_error();
return result;
}
result.t_generate_ms = get_time_ms() - t_generate_start;
sample_memory("synth/after-generate");
int n_codebooks = transformer_.get_config().n_codebooks;
int n_frames = (int)speech_codes.size() / n_codebooks;
if (params.print_progress) {
fprintf(stderr, "Speech codes generated: %d frames x %d codebooks\n", n_frames, n_codebooks);
}
if (n_frames == 0) {
result.error_msg = "No speech codes generated";
return result;
}
if (low_mem_mode_) {
transformer_.unload_model();
transformer_loaded_ = false;
sample_memory("synth/after-transformer-unload");
}
// Step 4: Decode speech codes to waveform using vocoder
int64_t t_decode_start = get_time_ms();
if (!decoder_loaded_) {
int64_t t_decoder_load_start = get_time_ms();
if (decoder_model_path_.empty()) {
result.error_msg = "Internal error: missing vocoder model path";
return result;
}
if (!audio_decoder_.load_model(decoder_model_path_)) {
result.error_msg = "Failed to load vocoder: " + audio_decoder_.get_error();
return result;
}
decoder_loaded_ = true;
if (params.print_timing) {
fprintf(stderr, " Vocoder lazy-loaded in %lld ms\n",
(long long)(get_time_ms() - t_decoder_load_start));
sample_memory("synth/after-vocoder-load");
}
}
if (!audio_decoder_.decode(speech_codes.data(), n_frames, result.audio)) {
result.error_msg = "Failed to decode speech codes: " + audio_decoder_.get_error();
return result;
}
result.t_decode_ms = get_time_ms() - t_decode_start;
sample_memory("synth/after-decode");
if (low_mem_mode_) {
audio_decoder_.unload_model();
decoder_loaded_ = false;
sample_memory("synth/after-vocoder-unload");
}
result.sample_rate = audio_decoder_.get_config().sample_rate;
result.success = true;
result.t_total_ms = get_time_ms() - t_total_start;
sample_memory("synth/end");
if (params.print_timing) {
const double audio_sec = result.sample_rate > 0
? (double) result.audio.size() / (double) result.sample_rate : 0.0;
const double wall_sec = (double) result.t_total_ms / 1000.0;
const double realtime_factor = audio_sec > 0.0 ? wall_sec / audio_sec : 0.0;
const double x_realtime = wall_sec > 0.0 ? audio_sec / wall_sec : 0.0;
fprintf(stderr, "\nTiming:\n");
fprintf(stderr, " Tokenization: %lld ms\n", (long long)result.t_tokenize_ms);
fprintf(stderr, " Speaker encode: %lld ms\n", (long long)result.t_encode_ms);
fprintf(stderr, " Code generation: %lld ms\n", (long long)result.t_generate_ms);
fprintf(stderr, " Vocoder decode: %lld ms\n", (long long)result.t_decode_ms);
fprintf(stderr, " Total: %lld ms\n", (long long)result.t_total_ms);
fprintf(stderr, " Audio duration: %.2f s\n", audio_sec);
fprintf(stderr, " Throughput: %.2fx realtime (RTF=%.3f)\n", x_realtime, realtime_factor);
fprintf(stderr, "\nMemory:\n");
fprintf(stderr, " RSS start/end: %s -> %s\n",
format_bytes(result.mem_rss_start_bytes).c_str(),
format_bytes(result.mem_rss_end_bytes).c_str());
fprintf(stderr, " RSS peak: %s\n",
format_bytes(result.mem_rss_peak_bytes).c_str());
fprintf(stderr, " Phys start/end: %s -> %s\n",
format_bytes(result.mem_phys_start_bytes).c_str(),
format_bytes(result.mem_phys_end_bytes).c_str());
fprintf(stderr, " Phys peak: %s\n",
format_bytes(result.mem_phys_peak_bytes).c_str());
}
return result;
}
void Qwen3TTS::set_progress_callback(tts_progress_callback_t callback) {
progress_callback_ = callback;
}
// WAV file loading (16-bit PCM or 32-bit float)
bool load_audio_file(const std::string & path, std::vector<float> & samples,
int & sample_rate) {
FILE * f = fopen(path.c_str(), "rb");
if (!f) {
fprintf(stderr, "ERROR: Cannot open WAV file: %s\n", path.c_str());
return false;
}
// Read RIFF header
char riff[4];
if (fread(riff, 1, 4, f) != 4 || strncmp(riff, "RIFF", 4) != 0) {
fprintf(stderr, "ERROR: Not a RIFF file\n");
fclose(f);
return false;
}
uint32_t file_size;
if (fread(&file_size, 4, 1, f) != 1) {
fclose(f);
return false;
}
char wave[4];
if (fread(wave, 1, 4, f) != 4 || strncmp(wave, "WAVE", 4) != 0) {
fprintf(stderr, "ERROR: Not a WAVE file\n");
fclose(f);
return false;
}
// Find fmt and data chunks
uint16_t audio_format = 0;
uint16_t num_channels = 0;
uint32_t sr = 0;
uint16_t bits_per_sample = 0;
while (!feof(f)) {
char chunk_id[4];
uint32_t chunk_size;
if (fread(chunk_id, 1, 4, f) != 4) break;
if (fread(&chunk_size, 4, 1, f) != 1) break;
if (strncmp(chunk_id, "fmt ", 4) == 0) {
if (fread(&audio_format, 2, 1, f) != 1) break;
if (fread(&num_channels, 2, 1, f) != 1) break;
if (fread(&sr, 4, 1, f) != 1) break;
fseek(f, 6, SEEK_CUR); // Skip byte rate and block align
if (fread(&bits_per_sample, 2, 1, f) != 1) break;
// Skip any extra format bytes
if (chunk_size > 16) {
fseek(f, chunk_size - 16, SEEK_CUR);
}
}
else if (strncmp(chunk_id, "data", 4) == 0) {
sample_rate = sr;
if (audio_format == 1) { // PCM
if (bits_per_sample == 16) {
int n_samples = chunk_size / (2 * num_channels);
samples.resize(n_samples);
std::vector<int16_t> raw(n_samples * num_channels);
if (fread(raw.data(), 2, n_samples * num_channels, f) != (size_t)(n_samples * num_channels)) {
fclose(f);
return false;
}
// Convert to mono float
for (int i = 0; i < n_samples; ++i) {
float sum = 0.0f;
for (int c = 0; c < num_channels; ++c) {
sum += raw[i * num_channels + c] / 32768.0f;
}
samples[i] = sum / num_channels;
}
}
else if (bits_per_sample == 32) {
int n_samples = chunk_size / (4 * num_channels);
samples.resize(n_samples);
std::vector<int32_t> raw(n_samples * num_channels);
if (fread(raw.data(), 4, n_samples * num_channels, f) != (size_t)(n_samples * num_channels)) {
fclose(f);
return false;
}
// Convert to mono float
for (int i = 0; i < n_samples; ++i) {
float sum = 0.0f;
for (int c = 0; c < num_channels; ++c) {
sum += raw[i * num_channels + c] / 2147483648.0f;
}
samples[i] = sum / num_channels;
}
}
else {
fprintf(stderr, "ERROR: Unsupported bits per sample: %d\n", bits_per_sample);
fclose(f);
return false;
}
}
else if (audio_format == 3) { // IEEE float
int n_samples = chunk_size / (4 * num_channels);
samples.resize(n_samples);
std::vector<float> raw(n_samples * num_channels);
if (fread(raw.data(), 4, n_samples * num_channels, f) != (size_t)(n_samples * num_channels)) {
fclose(f);
return false;
}
// Convert to mono
for (int i = 0; i < n_samples; ++i) {
float sum = 0.0f;
for (int c = 0; c < num_channels; ++c) {
sum += raw[i * num_channels + c];
}
samples[i] = sum / num_channels;
}
}
else {
fprintf(stderr, "ERROR: Unsupported audio format: %d\n", audio_format);
fclose(f);
return false;
}
fclose(f);
return true;
}
else {
// Skip unknown chunk
fseek(f, chunk_size, SEEK_CUR);
}
}
fprintf(stderr, "ERROR: No data chunk found\n");
fclose(f);
return false;
}
// WAV file saving (16-bit PCM at specified sample rate)
bool save_audio_file(const std::string & path, const std::vector<float> & samples,
int sample_rate) {
FILE * f = fopen(path.c_str(), "wb");
if (!f) {
fprintf(stderr, "ERROR: Cannot create WAV file: %s\n", path.c_str());
return false;
}
// WAV header parameters
uint16_t num_channels = 1;
uint16_t bits_per_sample = 16;
uint32_t byte_rate = sample_rate * num_channels * bits_per_sample / 8;
uint16_t block_align = num_channels * bits_per_sample / 8;
uint32_t data_size = samples.size() * block_align;
uint32_t file_size = 36 + data_size;
// Write RIFF header
fwrite("RIFF", 1, 4, f);
fwrite(&file_size, 4, 1, f);
fwrite("WAVE", 1, 4, f);
// Write fmt chunk
fwrite("fmt ", 1, 4, f);
uint32_t fmt_size = 16;
fwrite(&fmt_size, 4, 1, f);
uint16_t audio_format = 1; // PCM
fwrite(&audio_format, 2, 1, f);
fwrite(&num_channels, 2, 1, f);
uint32_t sr = sample_rate;
fwrite(&sr, 4, 1, f);
fwrite(&byte_rate, 4, 1, f);
fwrite(&block_align, 2, 1, f);
fwrite(&bits_per_sample, 2, 1, f);
// Write data chunk
fwrite("data", 1, 4, f);
fwrite(&data_size, 4, 1, f);
// Convert float samples to 16-bit PCM and write
for (size_t i = 0; i < samples.size(); ++i) {
// Clamp to [-1, 1] and convert to int16
float sample = samples[i];
if (sample > 1.0f) sample = 1.0f;
if (sample < -1.0f) sample = -1.0f;
int16_t pcm_sample = (int16_t)(sample * 32767.0f);
fwrite(&pcm_sample, 2, 1, f);
}
fclose(f);
return true;
}
} // namespace qwen3_tts

View file

@ -0,0 +1,150 @@
#pragma once
#include "text_tokenizer.h"
#include "tts_transformer.h"
#include "audio_tokenizer_encoder.h"
#include "audio_tokenizer_decoder.h"
#include <string>
#include <vector>
#include <functional>
#include <cstdint>
namespace qwen3_tts {
// TTS generation parameters
struct tts_params {
// Maximum number of audio tokens to generate
int32_t max_audio_tokens = 4096;
// Temperature for sampling (0 = greedy)
float temperature = 0.9f;
// Top-p sampling
float top_p = 1.0f;
// Top-k sampling (0 = disabled)
int32_t top_k = 50;
// Number of threads
int32_t n_threads = 4;
// Print progress during generation
bool print_progress = false;
// Print timing information
bool print_timing = true;
// Repetition penalty for CB0 token generation (HuggingFace style)
float repetition_penalty = 1.05f;
};
// TTS generation result
struct tts_result {
// Generated audio samples (24kHz, mono)
std::vector<float> audio;
// Sample rate
int32_t sample_rate = 24000;
// Success flag
bool success = false;
// Error message if failed
std::string error_msg;
// Timing info (in milliseconds)
int64_t t_load_ms = 0;
int64_t t_tokenize_ms = 0;
int64_t t_encode_ms = 0;
int64_t t_generate_ms = 0;
int64_t t_decode_ms = 0;
int64_t t_total_ms = 0;
// Process memory snapshots (bytes)
uint64_t mem_rss_start_bytes = 0;
uint64_t mem_rss_end_bytes = 0;
uint64_t mem_rss_peak_bytes = 0;
uint64_t mem_phys_start_bytes = 0;
uint64_t mem_phys_end_bytes = 0;
uint64_t mem_phys_peak_bytes = 0;
};
// Progress callback type
using tts_progress_callback_t = std::function<void(int tokens_generated, int max_tokens)>;
// Main TTS class that orchestrates the full pipeline
class Qwen3TTS {
public:
Qwen3TTS();
~Qwen3TTS();
// Load all models from directory
// model_dir should contain: transformer.gguf, tokenizer.gguf, vocoder.gguf
bool load_models(const std::string & model_dir);
// Generate speech from text
// text: input text to synthesize
// params: generation parameters
tts_result synthesize(const std::string & text,
const tts_params & params = tts_params());
// Generate speech with voice cloning
// text: input text to synthesize
// reference_audio: path to reference audio file (WAV, 24kHz)
// params: generation parameters
tts_result synthesize_with_voice(const std::string & text,
const std::string & reference_audio,
const tts_params & params = tts_params());
// Generate speech with voice cloning from samples
// text: input text to synthesize
// ref_samples: reference audio samples (24kHz, mono, normalized to [-1, 1])
// n_ref_samples: number of reference samples
// params: generation parameters
tts_result synthesize_with_voice(const std::string & text,
const float * ref_samples, int32_t n_ref_samples,
const tts_params & params = tts_params());
// Set progress callback
void set_progress_callback(tts_progress_callback_t callback);
// Get error message
const std::string & get_error() const { return error_msg_; }
// Check if models are loaded
bool is_loaded() const { return models_loaded_; }
private:
tts_result synthesize_internal(const std::string & text,
const float * speaker_embedding,
const tts_params & params,
tts_result & result);
TextTokenizer tokenizer_;
TTSTransformer transformer_;
AudioTokenizerEncoder audio_encoder_;
AudioTokenizerDecoder audio_decoder_;
bool models_loaded_ = false;
bool encoder_loaded_ = false;
bool transformer_loaded_ = false;
bool decoder_loaded_ = false;
bool low_mem_mode_ = false;
std::string error_msg_;
std::string tts_model_path_;
std::string decoder_model_path_;
tts_progress_callback_t progress_callback_;
};
// Utility: Load audio file (WAV format)
bool load_audio_file(const std::string & path, std::vector<float> & samples,
int & sample_rate);
// Utility: Save audio file (WAV format)
bool save_audio_file(const std::string & path, const std::vector<float> & samples,
int sample_rate);
} // namespace qwen3_tts

View file

@ -0,0 +1,351 @@
#include "text_tokenizer.h"
#include <algorithm>
#include <cstring>
#include <limits>
#include <sstream>
namespace qwen3_tts {
// GPT-2 byte-to-unicode mapping
// Maps bytes 0-255 to unicode characters to avoid control characters
static const char * BYTE_TO_UNICODE[256] = {
"Ā", "ā", "Ă", "ă", "Ą", "ą", "Ć", "ć", "Ĉ", "ĉ", "Ċ", "ċ", "Č", "č", "Ď", "ď",
"Đ", "đ", "Ē", "ē", "Ĕ", "ĕ", "Ė", "ė", "Ę", "ę", "Ě", "ě", "Ĝ", "ĝ", "Ğ", "ğ",
"Ġ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?",
"@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
"P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_",
"`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "ġ",
"Ģ", "ģ", "Ĥ", "ĥ", "Ħ", "ħ", "Ĩ", "ĩ", "Ī", "ī", "Ĭ", "ĭ", "Į", "į", "İ", "ı",
"IJ", "ij", "Ĵ", "ĵ", "Ķ", "ķ", "ĸ", "Ĺ", "ĺ", "Ļ", "ļ", "Ľ", "ľ", "Ŀ", "ŀ", "Ł",
"ł", "¡", "¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "®", "¯", "°",
"±", "²", "³", "´", "µ", "", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À",
"Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð",
"Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à",
"á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð",
"ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "Ń"
};
// Build reverse mapping at runtime
static std::unordered_map<std::string, uint8_t> build_unicode_to_byte() {
std::unordered_map<std::string, uint8_t> result;
for (int i = 0; i < 256; i++) {
result[BYTE_TO_UNICODE[i]] = (uint8_t)i;
}
return result;
}
static const std::unordered_map<std::string, uint8_t> UNICODE_TO_BYTE = build_unicode_to_byte();
TextTokenizer::TextTokenizer() = default;
TextTokenizer::~TextTokenizer() = default;
size_t TextTokenizer::utf8_len(char c) {
if ((c & 0x80) == 0) return 1;
if ((c & 0xE0) == 0xC0) return 2;
if ((c & 0xF0) == 0xE0) return 3;
if ((c & 0xF8) == 0xF0) return 4;
return 1; // Invalid UTF-8, treat as single byte
}
std::string TextTokenizer::bytes_to_unicode(const std::string & text) {
std::string result;
for (unsigned char c : text) {
result += BYTE_TO_UNICODE[c];
}
return result;
}
std::string TextTokenizer::unicode_to_bytes(const std::string & text) {
std::string result;
size_t i = 0;
while (i < text.size()) {
size_t len = utf8_len(text[i]);
std::string ch = text.substr(i, len);
auto it = UNICODE_TO_BYTE.find(ch);
if (it != UNICODE_TO_BYTE.end()) {
result += (char)it->second;
} else {
// Not in mapping, keep as-is (shouldn't happen for valid tokens)
result += ch;
}
i += len;
}
return result;
}
bool TextTokenizer::load_from_gguf(struct gguf_context * ctx) {
if (!ctx) {
error_msg_ = "GGUF context is null";
return false;
}
// Get vocabulary
int64_t tokens_key = gguf_find_key(ctx, "tokenizer.ggml.tokens");
if (tokens_key < 0) {
error_msg_ = "tokenizer.ggml.tokens not found in GGUF";
return false;
}
size_t n_vocab = gguf_get_arr_n(ctx, tokens_key);
if (n_vocab == 0) {
error_msg_ = "Empty vocabulary";
return false;
}
config_.vocab_size = (int32_t)n_vocab;
id_to_token_.resize(n_vocab);
for (size_t i = 0; i < n_vocab; i++) {
const char * token = gguf_get_arr_str(ctx, tokens_key, i);
if (token) {
id_to_token_[i] = token;
vocab_[token] = (int32_t)i;
}
}
// Get merges
int64_t merges_key = gguf_find_key(ctx, "tokenizer.ggml.merges");
if (merges_key >= 0) {
size_t n_merges = gguf_get_arr_n(ctx, merges_key);
for (size_t i = 0; i < n_merges; i++) {
const char * merge = gguf_get_arr_str(ctx, merges_key, i);
if (merge) {
std::string merge_str(merge);
// Parse "token1 token2" format
size_t space_pos = merge_str.find(' ');
if (space_pos != std::string::npos) {
std::string first = merge_str.substr(0, space_pos);
std::string second = merge_str.substr(space_pos + 1);
bpe_ranks_[{first, second}] = (int32_t)i;
}
}
}
}
// Get special token IDs (optional, use defaults if not found)
int64_t bos_key = gguf_find_key(ctx, "tokenizer.ggml.bos_token_id");
if (bos_key >= 0) {
config_.bos_token_id = (int32_t)gguf_get_val_u32(ctx, bos_key);
}
int64_t eos_key = gguf_find_key(ctx, "tokenizer.ggml.eos_token_id");
if (eos_key >= 0) {
config_.eos_token_id = (int32_t)gguf_get_val_u32(ctx, eos_key);
}
int64_t pad_key = gguf_find_key(ctx, "tokenizer.ggml.padding_token_id");
if (pad_key >= 0) {
config_.pad_token_id = (int32_t)gguf_get_val_u32(ctx, pad_key);
}
// Find special tokens by content
auto find_token = [this](const std::string & text) -> int32_t {
auto it = vocab_.find(text);
return (it != vocab_.end()) ? it->second : -1;
};
assistant_token_id_ = find_token("assistant");
if (assistant_token_id_ < 0) {
// Try with space prefix (GPT-2 style)
assistant_token_id_ = find_token("Ġassistant");
}
// Newline token
newline_token_id_ = find_token("Ċ"); // GPT-2 encoding for '\n'
if (newline_token_id_ < 0) {
newline_token_id_ = find_token("\n");
}
loaded_ = true;
return true;
}
std::pair<std::string, std::string> TextTokenizer::get_min_pair(
const std::vector<std::string> & word) const {
std::pair<std::string, std::string> min_pair;
int32_t min_rank = std::numeric_limits<int32_t>::max();
for (size_t i = 0; i + 1 < word.size(); i++) {
auto pair = std::make_pair(word[i], word[i + 1]);
auto it = bpe_ranks_.find(pair);
if (it != bpe_ranks_.end() && it->second < min_rank) {
min_rank = it->second;
min_pair = pair;
}
}
return min_pair;
}
std::vector<std::string> TextTokenizer::bpe(const std::string & token) const {
if (token.empty()) {
return {};
}
// Split into unicode characters
std::vector<std::string> word;
size_t i = 0;
while (i < token.size()) {
size_t len = utf8_len(token[i]);
word.push_back(token.substr(i, len));
i += len;
}
if (word.size() == 1) {
return word;
}
// Iteratively merge pairs
while (true) {
auto min_pair = get_min_pair(word);
if (min_pair.first.empty()) {
break; // No more merges possible
}
// Merge all occurrences of the pair
std::vector<std::string> new_word;
size_t j = 0;
while (j < word.size()) {
if (j + 1 < word.size() &&
word[j] == min_pair.first &&
word[j + 1] == min_pair.second) {
new_word.push_back(min_pair.first + min_pair.second);
j += 2;
} else {
new_word.push_back(word[j]);
j += 1;
}
}
word = std::move(new_word);
if (word.size() == 1) {
break;
}
}
return word;
}
std::vector<int32_t> TextTokenizer::encode(const std::string & text) const {
if (!loaded_) {
return {};
}
std::vector<int32_t> tokens;
// Convert text to GPT-2 unicode representation
std::string unicode_text = bytes_to_unicode(text);
// Simple word splitting (no regex pre-tokenization for now)
// Split on spaces but keep the space with the following word (GPT-2 style)
std::vector<std::string> words;
std::string current_word;
size_t i = 0;
while (i < unicode_text.size()) {
size_t len = utf8_len(unicode_text[i]);
std::string ch = unicode_text.substr(i, len);
// Check if this is a space (Ġ in GPT-2 encoding)
if (ch == "Ġ") {
if (!current_word.empty()) {
words.push_back(current_word);
current_word.clear();
}
current_word = ch; // Start new word with space
} else {
current_word += ch;
}
i += len;
}
if (!current_word.empty()) {
words.push_back(current_word);
}
// BPE encode each word
for (const auto & word : words) {
auto bpe_tokens = bpe(word);
for (const auto & tok : bpe_tokens) {
auto it = vocab_.find(tok);
if (it != vocab_.end()) {
tokens.push_back(it->second);
} else {
// Unknown token - encode as bytes
for (unsigned char c : tok) {
std::string byte_tok = BYTE_TO_UNICODE[c];
auto byte_it = vocab_.find(byte_tok);
if (byte_it != vocab_.end()) {
tokens.push_back(byte_it->second);
}
}
}
}
}
return tokens;
}
std::vector<int32_t> TextTokenizer::encode_for_tts(const std::string & text) const {
if (!loaded_) {
return {};
}
// Format: <|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n
std::vector<int32_t> tokens;
// <|im_start|>
tokens.push_back(config_.bos_token_id);
// assistant
tokens.push_back(assistant_token_id_);
// \n
tokens.push_back(newline_token_id_);
// Encode the text
auto text_tokens = encode(text);
tokens.insert(tokens.end(), text_tokens.begin(), text_tokens.end());
// <|im_end|>
tokens.push_back(config_.eos_token_id);
// \n
tokens.push_back(newline_token_id_);
// <|im_start|>
tokens.push_back(config_.bos_token_id);
// assistant
tokens.push_back(assistant_token_id_);
// \n
tokens.push_back(newline_token_id_);
return tokens;
}
std::string TextTokenizer::decode(const std::vector<int32_t> & tokens) const {
std::string result;
for (int32_t token : tokens) {
result += decode_token(token);
}
return result;
}
std::string TextTokenizer::decode_token(int32_t token_id) const {
if (token_id < 0 || token_id >= (int32_t)id_to_token_.size()) {
return "";
}
const std::string & token = id_to_token_[token_id];
// Convert from GPT-2 unicode back to bytes
return unicode_to_bytes(token);
}
} // namespace qwen3_tts

View file

@ -0,0 +1,88 @@
#pragma once
#include "gguf.h"
#include <string>
#include <vector>
#include <unordered_map>
#include <map>
namespace qwen3_tts {
// BPE tokenizer configuration
struct tokenizer_config {
int32_t vocab_size = 151936;
int32_t pad_token_id = 151643;
int32_t eos_token_id = 151645; // <|im_end|>
int32_t bos_token_id = 151644; // <|im_start|>
};
// Text tokenizer class (BPE-based, GPT-2 style with Qwen2 pre-tokenization)
class TextTokenizer {
public:
TextTokenizer();
~TextTokenizer();
// Load tokenizer from GGUF file
bool load_from_gguf(struct gguf_context * ctx);
// Encode text to token IDs
std::vector<int32_t> encode(const std::string & text) const;
// Encode with TTS format: <|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n
std::vector<int32_t> encode_for_tts(const std::string & text) const;
// Decode token IDs to text
std::string decode(const std::vector<int32_t> & tokens) const;
// Decode single token
std::string decode_token(int32_t token_id) const;
// Get configuration
const tokenizer_config & get_config() const { return config_; }
// Get error message
const std::string & get_error() const { return error_msg_; }
// Check if loaded
bool is_loaded() const { return loaded_; }
// Get special token IDs
int32_t bos_token_id() const { return config_.bos_token_id; }
int32_t eos_token_id() const { return config_.eos_token_id; }
int32_t pad_token_id() const { return config_.pad_token_id; }
private:
tokenizer_config config_;
std::string error_msg_;
bool loaded_ = false;
// Vocabulary: token string -> token ID
std::unordered_map<std::string, int32_t> vocab_;
// Reverse vocabulary: token ID -> token string
std::vector<std::string> id_to_token_;
// BPE merges: pair -> rank (lower rank = higher priority)
std::map<std::pair<std::string, std::string>, int32_t> bpe_ranks_;
// Special token for "assistant" and newline
int32_t assistant_token_id_ = 77091;
int32_t newline_token_id_ = 198; // '\n' encoded
// Helper: convert bytes to unicode (GPT-2 style byte encoding)
static std::string bytes_to_unicode(const std::string & text);
static std::string unicode_to_bytes(const std::string & text);
// Helper: get UTF-8 character length
static size_t utf8_len(char c);
// BPE encoding for a single word
std::vector<std::string> bpe(const std::string & token) const;
// Find the pair with lowest rank in a sequence
std::pair<std::string, std::string> get_min_pair(
const std::vector<std::string> & word) const;
};
} // namespace qwen3_tts

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,353 @@
#pragma once
#include "ggml.h"
#include "ggml-backend.h"
#include "gguf.h"
#include "coreml_code_predictor.h"
#include <string>
#include <map>
#include <vector>
#include <memory>
#include <random>
#ifdef QWEN3_TTS_TIMING
#include <chrono>
#endif
namespace qwen3_tts {
#ifdef QWEN3_TTS_TIMING
struct tts_timing {
// Prefill phase
double t_prefill_build_ms = 0; // build_prefill_graph (embedding lookups, text projection)
double t_prefill_forward_ms = 0; // forward_prefill total
double t_prefill_graph_build_ms = 0; // build_prefill_forward_graph
double t_prefill_graph_alloc_ms = 0; // sched_alloc_graph
double t_prefill_compute_ms = 0; // sched_graph_compute
double t_prefill_data_ms = 0; // tensor_set + tensor_get + reset
// Talker forward_step totals (accumulated across all frames)
double t_talker_forward_ms = 0; // total time in forward_step()
double t_talker_graph_build_ms = 0; // build_step_graph
double t_talker_graph_alloc_ms = 0; // sched_alloc_graph
double t_talker_compute_ms = 0; // sched_graph_compute
double t_talker_data_ms = 0; // tensor_set + tensor_get + reset
// Code predictor totals (accumulated across all frames)
double t_code_pred_ms = 0; // total predict_codes_autoregressive
double t_code_pred_init_ms = 0; // init/clear KV cache + CB0 embed lookup
double t_code_pred_prefill_ms = 0; // code pred prefill (2-token, per frame)
double t_code_pred_steps_ms = 0; // code pred autoregressive steps (14 steps, per frame)
double t_code_pred_graph_build_ms = 0; // graph build (prefill + steps combined)
double t_code_pred_graph_alloc_ms = 0; // sched_alloc_graph
double t_code_pred_compute_ms = 0; // sched_graph_compute
double t_code_pred_data_ms = 0; // tensor_set + tensor_get + reset
double t_code_pred_coreml_ms = 0; // CoreML predictor compute + I/O
// Embed lookups in generate() loop
double t_embed_lookup_ms = 0;
int32_t n_frames = 0;
double t_generate_total_ms = 0;
};
#endif
#define QWEN3_TTS_MAX_NODES 16384
// TTS Transformer configuration (Qwen2-based Talker)
struct tts_transformer_config {
// Text embedding
int32_t text_vocab_size = 151936;
int32_t text_embd_dim = 2048;
// Talker transformer
int32_t hidden_size = 1024;
int32_t n_layers = 28;
int32_t n_attention_heads = 16;
int32_t n_key_value_heads = 8;
int32_t intermediate_size = 3072;
int32_t head_dim = 128;
float rms_norm_eps = 1e-6f;
float rope_theta = 1000000.0f;
// M-RoPE sections [time, freq, channel] = [24, 20, 20]
int32_t mrope_section[3] = {24, 20, 20};
// Codec vocabulary
int32_t codec_vocab_size = 3072; // talker.codec_embd/codec_head
int32_t n_codebooks = 16;
// Code predictor
int32_t code_pred_layers = 5;
int32_t code_pred_vocab_size = 2048; // Per-codebook vocab
// Special codec tokens
int32_t codec_pad_id = 2148;
int32_t codec_bos_id = 2149;
int32_t codec_eos_id = 2150;
int32_t tts_bos_token_id = 151672;
int32_t tts_eos_token_id = 151673;
int32_t tts_pad_token_id = 151671;
int32_t codec_think_id = 2154;
int32_t codec_nothink_id = 2155;
int32_t codec_think_bos_id = 2156;
int32_t codec_think_eos_id = 2157;
int32_t english_language_id = 2050;
};
// Transformer layer weights
struct transformer_layer {
struct ggml_tensor * attn_norm = nullptr;
struct ggml_tensor * attn_q = nullptr;
struct ggml_tensor * attn_k = nullptr;
struct ggml_tensor * attn_v = nullptr;
struct ggml_tensor * attn_output = nullptr;
struct ggml_tensor * attn_q_norm = nullptr;
struct ggml_tensor * attn_k_norm = nullptr;
struct ggml_tensor * ffn_norm = nullptr;
struct ggml_tensor * ffn_gate = nullptr;
struct ggml_tensor * ffn_up = nullptr;
struct ggml_tensor * ffn_down = nullptr;
};
// TTS Transformer model weights
struct tts_transformer_model {
tts_transformer_config config;
// Text embedding and projection
struct ggml_tensor * text_embd = nullptr; // [text_embd_dim, text_vocab_size]
struct ggml_tensor * text_proj_fc1 = nullptr; // [text_embd_dim, text_embd_dim]
struct ggml_tensor * text_proj_fc1_bias = nullptr;
struct ggml_tensor * text_proj_fc2 = nullptr; // [text_embd_dim, hidden_size]
struct ggml_tensor * text_proj_fc2_bias = nullptr;
// Codec embedding (for autoregressive input)
struct ggml_tensor * codec_embd = nullptr; // [hidden_size, codec_vocab_size]
// Talker transformer layers
std::vector<transformer_layer> layers;
// Final RMSNorm
struct ggml_tensor * output_norm = nullptr; // [hidden_size]
// Codec head (for first codebook prediction)
struct ggml_tensor * codec_head = nullptr; // [hidden_size, codec_vocab_size]
// Code predictor layers
std::vector<transformer_layer> code_pred_layers;
// Code predictor output norm (final RMS norm before lm_head)
struct ggml_tensor * code_pred_output_norm = nullptr; // [hidden_size]
// Code predictor per-codebook embeddings and heads (15 codebooks, 0 uses talker output)
std::vector<struct ggml_tensor *> code_pred_embd; // [hidden_size, code_pred_vocab_size] x 15
std::vector<struct ggml_tensor *> code_pred_head; // [hidden_size, code_pred_vocab_size] x 15
// GGML context for tensor metadata
struct ggml_context * ctx = nullptr;
// Backend buffer for weights
ggml_backend_buffer_t buffer = nullptr;
// Tensor name to tensor mapping
std::map<std::string, struct ggml_tensor *> tensors;
};
// KV cache for autoregressive generation
struct tts_kv_cache {
std::vector<struct ggml_tensor *> k_cache;
std::vector<struct ggml_tensor *> v_cache;
struct ggml_context * ctx = nullptr;
ggml_backend_buffer_t buffer = nullptr;
int32_t n_ctx = 0;
int32_t n_used = 0;
int32_t head_dim = 128;
int32_t n_kv_heads = 8;
int32_t n_layers = 28;
};
// TTS Transformer state
struct tts_transformer_state {
ggml_backend_t backend = nullptr;
ggml_backend_t backend_cpu = nullptr;
ggml_backend_sched_t sched = nullptr;
std::vector<uint8_t> compute_meta;
tts_kv_cache cache; // Talker KV cache (28 layers)
tts_kv_cache code_pred_cache; // Code predictor KV cache (5 layers)
};
// TTS Transformer class
class TTSTransformer {
public:
TTSTransformer();
~TTSTransformer();
// Load model from GGUF file
bool load_model(const std::string & model_path);
// Release all model/runtime resources
void unload_model();
// Initialize KV cache
bool init_kv_cache(int32_t n_ctx);
// Clear KV cache
void clear_kv_cache();
// Initialize code predictor KV cache (5 layers, max 16 context)
bool init_code_pred_kv_cache(int32_t n_ctx);
// Clear code predictor KV cache
void clear_code_pred_kv_cache();
// Forward pass for text tokens (prefill phase)
// text_tokens: input text token IDs [n_tokens]
// speaker_embd: speaker embedding [hidden_size] (optional, can be nullptr)
// n_past: number of tokens already in KV cache
// output: hidden states [n_tokens, hidden_size]
bool forward_text(const int32_t * text_tokens, int32_t n_tokens,
const float * speaker_embd, int32_t n_past,
std::vector<float> & output);
bool forward_prefill(const float * prefill_embd, int32_t n_tokens,
int32_t n_past, std::vector<float> & output,
std::vector<float> * logits_out = nullptr);
// Forward pass for codec tokens (generation phase)
// codec_token: single codec token for first codebook
// n_past: number of tokens already in KV cache
// output: logits for next codec token [codec_vocab_size]
bool forward_codec(int32_t codec_token, int32_t n_past,
std::vector<float> & output);
bool forward_step(const float * step_embd, int32_t n_past,
std::vector<float> & output,
std::vector<float> * hidden_out = nullptr);
// Get hidden states from last forward pass (for code predictor)
bool get_hidden_states(std::vector<float> & hidden) const;
// Run code predictor to get all 16 codebook predictions
// hidden: hidden states from talker [hidden_size]
// prev_codes: previous codes for codebooks 1-15 (can be nullptr for first step)
// output: logits for all 16 codebooks [16, code_pred_vocab_size]
bool predict_codes(const float * hidden, const int32_t * prev_codes,
std::vector<float> & output);
// Run code predictor autoregressively to generate 15 codes (codebooks 1-15)
// hidden: hidden states from talker [hidden_size]
// codebook_0_token: the codebook 0 token (used to create 2-token prefill input)
// output: generated codes for codebooks 1-15 [15]
bool predict_codes_autoregressive(const float * hidden, int32_t codebook_0_token,
std::vector<int32_t> & output,
float temperature = 0.9f,
int32_t top_k = 50);
// Generate speech codes autoregressively
// text_tokens: input text token IDs [n_tokens]
// speaker_embd: speaker embedding [hidden_size]
// max_len: maximum number of frames to generate
// output: generated speech codes [n_frames, n_codebooks]
bool generate(const int32_t * text_tokens, int32_t n_tokens,
const float * speaker_embd, int32_t max_len,
std::vector<int32_t> & output,
int32_t language_id = 2050,
float repetition_penalty = 1.05f,
float temperature = 0.9f,
int32_t top_k = 50);
const tts_transformer_config & get_config() const { return model_.config; }
const std::string & get_error() const { return error_msg_; }
// Legacy interface for compatibility
bool forward(const int32_t * tokens, int32_t n_tokens, int32_t n_past,
std::vector<float> & output);
bool forward_with_audio(const int32_t * tokens, int32_t n_tokens,
const float * audio_embd, int32_t n_audio,
int32_t audio_start_pos, int32_t n_past,
std::vector<float> & output);
private:
bool try_init_coreml_code_predictor(const std::string & model_path);
bool predict_codes_autoregressive_coreml(const float * hidden, int32_t codebook_0_token,
std::vector<int32_t> & output,
float temperature,
int32_t top_k);
bool build_prefill_graph(const int32_t * text_tokens, int32_t n_tokens,
const float * speaker_embd, int32_t language_id,
std::vector<float> & prefill_embd,
std::vector<float> & trailing_text_hidden,
std::vector<float> & tts_pad_embed);
struct ggml_cgraph * build_prefill_forward_graph(int32_t n_tokens, int32_t n_past);
struct ggml_cgraph * build_step_graph(int32_t n_past);
bool project_text_tokens(const int32_t * text_tokens, int32_t n_tokens,
std::vector<float> & output);
bool lookup_embedding_rows(struct ggml_tensor * embedding, const int32_t * token_ids,
int32_t n_tokens, const char * input_name,
const char * output_name, std::vector<float> & output);
bool lookup_single_embedding_row(struct ggml_tensor * embedding, int32_t token_id,
float * out_row);
// Build computation graph for code predictor
struct ggml_cgraph * build_code_pred_graph(int32_t n_prev_codes);
// Build computation graph for single-step autoregressive code predictor
// n_past: number of tokens already in KV cache (0-14)
// generation_step: which codebook we're predicting (0-14)
struct ggml_cgraph * build_code_pred_step_graph(int32_t n_past, int32_t generation_step);
// Build computation graph for 2-token prefill of code predictor
// Processes [past_hidden, codec_embd(codebook_0_token)] together
struct ggml_cgraph * build_code_pred_prefill_graph();
// Parse hyperparameters from GGUF
bool parse_config(struct gguf_context * ctx);
// Create tensor structures
bool create_tensors(struct gguf_context * ctx);
// Load tensor data from file
bool load_tensor_data(const std::string & path, struct gguf_context * ctx);
tts_transformer_model model_;
tts_transformer_state state_;
std::string error_msg_;
// Cached hidden states from last forward pass
std::vector<float> last_hidden_;
std::vector<ggml_fp16_t> embd_row_fp16_scratch_;
std::mt19937 rng_{std::random_device{}()};
CoreMLCodePredictor coreml_code_predictor_;
bool use_coreml_code_predictor_ = false;
std::string coreml_code_predictor_path_;
bool skip_ggml_code_pred_layers_ = false;
#ifdef QWEN3_TTS_TIMING
tts_timing * timing_ = nullptr;
#endif
};
// Free model resources
void free_transformer_model(tts_transformer_model & model);
// Free KV cache resources
void free_tts_kv_cache(tts_kv_cache & cache);
} // namespace qwen3_tts