Initial release

This commit is contained in:
Pascal
2026-05-10 15:57:15 +02:00
commit add3f940a0
77 changed files with 12367 additions and 0 deletions
+473
View File
@@ -0,0 +1,473 @@
// quantize.cpp : GGUF requantizer for qwen
// Reads BF16 GGUF, writes quantized GGUF with mixed-precision K-quant policy.
// Policy mirrors llama-quantize: important tensors (v_proj, down_proj) get
// bumped in S/M variants, embed_tokens always Q6_K, norms promoted to F32.
// Streaming write: one tensor at a time, low memory footprint for small configs.
//
// Usage: quantize <input.gguf> <output.gguf> <type>
// Types: Q2_K Q3_K_S Q3_K_M Q3_K_L Q4_K_S Q4_K_M Q5_K_S Q5_K_M Q6_K Q8_0
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#ifdef _WIN32
# define NOMINMAX
# include <windows.h>
# define strcasecmp _stricmp
#else
# include <fcntl.h>
# include <sys/mman.h>
# include <sys/stat.h>
# include <unistd.h>
#endif
#include "ggml.h"
#include "gguf.h"
#include "version.h"
// Quant variant: base type + optional bump rules for important tensors
struct QuantVariant {
const char * name;
enum ggml_type base;
enum ggml_type bump; // type for "important" tensors (or COUNT = no bump)
enum ggml_type embed; // type for embed_tokens (or COUNT = same as base)
// bump_mode: 0=none, 1=first N layers, 2=first+last+every 3rd, 3=all important
int bump_mode;
int bump_n; // for mode 1: number of layers to bump
};
static const QuantVariant VARIANTS[] = {
// name base bump embed mode n
{ "BF16", GGML_TYPE_BF16, GGML_TYPE_COUNT, GGML_TYPE_BF16, 0, 0 },
{ "Q2_K", GGML_TYPE_Q2_K, GGML_TYPE_Q4_K, GGML_TYPE_Q6_K, 1, 4 },
{ "Q3_K_S", GGML_TYPE_Q3_K, GGML_TYPE_COUNT, GGML_TYPE_Q6_K, 0, 0 },
{ "Q3_K_M", GGML_TYPE_Q3_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, 2, 0 },
{ "Q3_K_L", GGML_TYPE_Q3_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, 3, 0 },
{ "Q4_K_S", GGML_TYPE_Q4_K, GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, 1, 4 },
{ "Q4_K_M", GGML_TYPE_Q4_K, GGML_TYPE_Q6_K, GGML_TYPE_Q6_K, 2, 0 },
{ "Q5_K_S", GGML_TYPE_Q5_K, GGML_TYPE_COUNT, GGML_TYPE_Q6_K, 0, 0 },
{ "Q5_K_M", GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, GGML_TYPE_Q6_K, 2, 0 },
{ "Q6_K", GGML_TYPE_Q6_K, GGML_TYPE_COUNT, GGML_TYPE_Q6_K, 0, 0 },
{ "Q8_0", GGML_TYPE_Q8_0, GGML_TYPE_COUNT, GGML_TYPE_Q8_0, 0, 0 },
};
static const QuantVariant * find_variant(const char * s) {
for (const auto & v : VARIANTS) {
if (strcasecmp(s, v.name) == 0) {
return &v;
}
}
return nullptr;
}
// Extract layer index from HF tensor name: model.layers.N.xxx -> N, else -1
static int extract_layer(const char * name) {
const char * p = strstr(name, "layers.");
if (!p) {
return -1;
}
return atoi(p + 7);
}
// Important tensors for S/M: v_proj + down_proj
static bool is_important_sm(const char * name) {
return (strstr(name, "v_proj.weight") != nullptr) || (strstr(name, "down_proj.weight") != nullptr);
}
// Important tensors for L: v_proj + down_proj + o_proj
static bool is_important_l(const char * name) {
return is_important_sm(name) || (strstr(name, "o_proj.weight") != nullptr);
}
// Tensors accessed via ggml_get_rows (text token embeddings, audio token
// embeddings, codebook lookups). These must use a type the CUDA get_rows
// kernel supports : F32, F16, BF16, Q4_0, Q4_1, Q5_0, Q5_1, Q8_0. K-quants
// are NOT supported.
//
// In the qwen3-tts naming convention :
// token_embd / output LM standard names
// talker.codec_embd / talker.text_embd TTS specific embeddings
// code_pred.codec_embd.{i} per codebook embeddings on the predictor
// code_pred.lm_head.{i} per codebook output heads
// tok_enc.vq_*.{i}.codebook RVQ codebook tables, nearest neighbor lookup
// tok_dec.vq_*.{i}.codebook RVQ codebook tables, F.embedding lookup
static bool is_embed(const char * name) {
return strstr(name, "token_embd") != nullptr || strstr(name, "embed_tokens.weight") != nullptr ||
strstr(name, "audio_embeddings.weight") != nullptr || strstr(name, ".codec_embd") != nullptr ||
strstr(name, ".text_embd") != nullptr;
}
// Should this tensor be quantized at all?
//
// Single source of truth for the quantization policy. Applies to EVERY
// variant (BF16, Q8_0, Q6_K, Q5_K_M, Q4_K_M, ...) : tensors that return
// false here keep their source dtype (F32) regardless of the requested
// type. Conv weights pass through the main loop and fall back to F16 when
// the row width does not divide the variant block size (kernel K=7,3,1,...).
// gf_load_conv_f16 then memcpys F16 source straight to the F16 backend
// tensor (ARM im2col strict requirement, see src/gguf-weights.h).
//
// Sensitive tensors that MUST stay in full precision :
// tok_enc.vq_*.{i}.codebook RVQ codebook tables, encoder side
// tok_dec.vq_*.{i}.codebook RVQ codebook tables, decoder side
// tok_enc.vq_*.input_proj linear wrapping the RVQ encode loop
// tok_enc.vq_*.output_proj linear wrapping the RVQ encode loop
// tok_dec.vq_*.output_proj linear wrapping the RVQ decode loop
// Nearest-neighbor lookup is sensitive to per-row quantization noise ;
// even BF16 destroys the mantissa enough to mis-select codes and break
// voice cloning. Same philosophy as acestep.cpp keeping VAE-critical
// paths in full precision.
static bool should_quantize(const char * name, int n_dims, const char * arch) {
if (strstr(arch, "vae")) {
return false;
}
if (n_dims < 2) {
return false;
}
if (strstr(arch, "text-enc") && strstr(name, "embed_tokens")) {
return false;
}
if (strstr(name, "silence_latent")) {
return false;
}
if (strstr(name, "scale_shift_table")) {
return false;
}
if (strstr(name, "null_condition_emb")) {
return false;
}
// Snake activation parameters : stored as per-channel floats, are
// activation parameters, not weights. The DAC loaders widen them to
// F32 on the backend with a reciprocal transform, no other dtype
// path. Keep them source-dtype in every variant. Both the legacy
// snake1/snake2 names from the old layout and the standard tok_dec
// snake naming are matched.
if (strstr(name, ".snake.alpha") || strstr(name, ".snake.beta") || strstr(name, ".act1.alpha") ||
strstr(name, ".act1.beta") || strstr(name, ".act2.alpha") || strstr(name, ".act2.beta") ||
strstr(name, ".snake1.alpha") || strstr(name, ".snake2.alpha")) {
return false;
}
// RVQ codebooks and the linear projections wrapping them : nearest
// neighbor lookup is sensitive to per-row quantization noise. Q8_0
// and K-quants break reference audio encoding and tank voice cloning ;
// BF16 already loses enough mantissa to drift codes. Keep at F32 in
// every variant.
if (strstr(name, "tok_enc.vq_") || strstr(name, "tok_dec.vq_")) {
return false;
}
// Speaker encoder final FC, semantically equivalent to the old
// top level fc.weight / fc2.weight in earlier layouts.
if (strstr(name, "spk_enc.fc.weight")) {
return false;
}
return true;
}
// Decide target type for a single tensor given the variant + layer info
static enum ggml_type pick_type(const char * name,
int n_dims,
const char * arch,
const QuantVariant & v,
int n_layers) {
if (!should_quantize(name, n_dims, arch)) {
return GGML_TYPE_COUNT;
}
// embed_tokens in LM: use embed type
if (is_embed(name) && !strstr(arch, "text-enc")) {
return (v.embed != GGML_TYPE_COUNT) ? v.embed : v.base;
}
// Important tensor bump logic
bool important = (v.bump_mode == 3) ? is_important_l(name) : is_important_sm(name);
if (important && v.bump != GGML_TYPE_COUNT) {
int layer = extract_layer(name);
bool bumped = false;
switch (v.bump_mode) {
case 1: // first N layers only
bumped = (layer >= 0 && layer < v.bump_n);
break;
case 2:
{ // M variant: first few + last few + every 3rd
int ql = n_layers;
bumped = (layer >= 0) && (layer < ql / 9 || layer >= ql - ql / 7 || layer % 3 == 0);
break;
}
case 3: // L variant: all important tensors (v+down+o_proj)
bumped = true;
break;
}
if (bumped) {
return v.bump;
}
}
return v.base;
}
// Convert source data to F32
static bool to_f32(const void * src, float * dst, int64_t n, enum ggml_type type) {
switch (type) {
case GGML_TYPE_BF16:
ggml_bf16_to_fp32_row((const ggml_bf16_t *) src, dst, n);
return true;
case GGML_TYPE_F16:
ggml_fp16_to_fp32_row((const ggml_fp16_t *) src, dst, n);
return true;
case GGML_TYPE_F32:
memcpy(dst, src, (size_t) n * sizeof(float));
return true;
default:
return false;
}
}
int main(int argc, char ** argv) {
if (argc != 4) {
fprintf(stderr, "qwentts.cpp %s\n\n", QWEN_VERSION);
fprintf(stderr, "Usage: %s <input.gguf> <output.gguf> <type>\n", argv[0]);
fprintf(stderr, "Types:");
for (const auto & v : VARIANTS) {
fprintf(stderr, " %s", v.name);
}
fprintf(stderr, "\n");
return 1;
}
const char * inp_path = argv[1];
const char * out_path = argv[2];
const QuantVariant * variant = find_variant(argv[3]);
if (!variant) {
fprintf(stderr, "[Quantize] Unknown type: %s\n", argv[3]);
return 1;
}
fprintf(stderr, "[Quantize] %s -> %s (%s)\n", inp_path, out_path, variant->name);
// Mmap input file
#ifdef _WIN32
HANDLE fh = CreateFileA(inp_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (fh == INVALID_HANDLE_VALUE) {
fprintf(stderr, "[Quantize] Failed to open %s\n", inp_path);
return 1;
}
HANDLE mh = CreateFileMappingA(fh, NULL, PAGE_READONLY, 0, 0, NULL);
if (!mh) {
fprintf(stderr, "[Quantize] CreateFileMapping failed %s\n", inp_path);
CloseHandle(fh);
return 1;
}
void * mapping = MapViewOfFile(mh, FILE_MAP_READ, 0, 0, 0);
if (!mapping) {
fprintf(stderr, "[Quantize] MapViewOfFile failed %s\n", inp_path);
CloseHandle(mh);
CloseHandle(fh);
return 1;
}
#else
int fd = open(inp_path, O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
struct stat st;
fstat(fd, &st);
size_t file_size = (size_t) st.st_size;
void * mapping = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapping == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
#endif
// Parse input GGUF
struct gguf_init_params params = { /*no_alloc=*/true, /*ctx=*/nullptr };
struct ggml_context * meta = nullptr;
params.ctx = &meta;
struct gguf_context * inp = gguf_init_from_file(inp_path, params);
if (!inp) {
fprintf(stderr, "[Quantize] Failed to read %s\n", inp_path);
#ifdef _WIN32
UnmapViewOfFile(mapping);
CloseHandle(mh);
CloseHandle(fh);
#else
munmap(mapping, file_size);
close(fd);
#endif
return 1;
}
const size_t data_off = gguf_get_data_offset(inp);
const int n_tensors = (int) gguf_get_n_tensors(inp);
// Read architecture
char arch[64] = "unknown";
{
int64_t idx = gguf_find_key(inp, "general.architecture");
if (idx >= 0) {
const char * s = gguf_get_val_str(inp, (int) idx);
snprintf(arch, sizeof(arch), "%s", s);
}
}
// Read block count for bump policy. Standard archs publish
// {arch}.block_count, but multi LM archs like qwen3-tts namespace
// it under a sub-component (talker) so we try a small list of
// fallbacks before giving up.
int n_layers = 0;
{
const char * candidates[] = {
".block_count",
".talker.block_count",
".decoder.num_hidden_layers",
};
for (const char * suffix : candidates) {
char key[160];
snprintf(key, sizeof(key), "%s%s", arch, suffix);
int64_t idx = gguf_find_key(inp, key);
if (idx >= 0) {
n_layers = (int) gguf_get_val_u32(inp, (int) idx);
break;
}
}
}
fprintf(stderr, "[Quantize] Arch=%s Layers=%d\n", arch, n_layers);
// Create output GGUF: copy KV metadata
struct gguf_context * out = gguf_init_empty();
gguf_set_kv(out, inp);
gguf_set_val_u32(out, "general.quantization_version", 2);
gguf_set_val_str(out, "general.file_type", variant->name);
// Plan: for each tensor, decide target type
struct TensorPlan {
enum ggml_type target;
bool quantize;
};
std::vector<TensorPlan> plans((size_t) n_tensors);
for (int i = 0; i < n_tensors; i++) {
const char * name = gguf_get_tensor_name(inp, i);
struct ggml_tensor * t = ggml_get_tensor(meta, name);
const int n_dims = ggml_n_dims(t);
gguf_add_tensor(out, t);
plans[(size_t) i] = { GGML_TYPE_COUNT, false };
enum ggml_type target = pick_type(name, n_dims, arch, *variant, n_layers);
if (target == GGML_TYPE_COUNT) {
continue;
}
bool can_convert = (t->type == GGML_TYPE_BF16 || t->type == GGML_TYPE_F16 || t->type == GGML_TYPE_F32);
bool aligned = (t->ne[0] % ggml_blck_size(target) == 0);
// Conv kernels (K=7,3,1,...) cannot fit a block-quant row : fall back
// to F16. F16 has no block size, 10-bit mantissa beats BF16 (7) and
// Q* effective on these weights, and gf_load_conv_f16 memcpys F16
// source straight to the F16 backend tensor at load time.
if (can_convert && !aligned) {
target = GGML_TYPE_F16;
aligned = true;
}
if (can_convert && aligned) {
gguf_set_tensor_type(out, name, target);
plans[(size_t) i] = { target, true };
}
}
// Write metadata only (header + tensor info, no data)
bool ok = gguf_write_to_file(out, out_path, true);
if (!ok) {
fprintf(stderr, "[Quantize] Failed to write metadata %s\n", out_path);
return 1;
}
// Stream tensor data one at a time (low memory)
FILE * fout = fopen(out_path, "ab");
if (!fout) {
fprintf(stderr, "[Quantize] Failed to open %s for append\n", out_path);
return 1;
}
const size_t alignment = gguf_get_alignment(out);
int n_quantized = 0;
int64_t bytes_in = 0, bytes_out = 0;
size_t data_pos = 0;
for (int i = 0; i < n_tensors; i++) {
const char * name = gguf_get_tensor_name(inp, i);
struct ggml_tensor * t = ggml_get_tensor(meta, name);
const int64_t nel = ggml_nelements(t);
const size_t src_size = ggml_nbytes(t);
const size_t t_off = gguf_get_tensor_offset(inp, i);
const void * src = (const uint8_t *) mapping + data_off + t_off;
bytes_in += (int64_t) src_size;
// Pad to alignment boundary
size_t pad = (alignment - (data_pos % alignment)) % alignment;
if (pad > 0) {
uint8_t zeros[64] = {};
fwrite(zeros, 1, pad, fout);
data_pos += pad;
}
const TensorPlan & plan = plans[(size_t) i];
if (plan.quantize) {
// Quantize: src -> f32 -> target
std::vector<float> f32((size_t) nel);
to_f32(src, f32.data(), nel, t->type);
const int64_t n_per_row = t->ne[0];
const int64_t nrows = nel / n_per_row;
const size_t qsize = ggml_row_size(plan.target, n_per_row) * (size_t) nrows;
std::vector<uint8_t> qbuf(qsize);
ggml_quantize_chunk(plan.target, f32.data(), qbuf.data(), 0, nrows, n_per_row, nullptr);
fwrite(qbuf.data(), 1, qsize, fout);
data_pos += qsize;
bytes_out += (int64_t) qsize;
n_quantized++;
} else {
// Keep as-is
fwrite(src, 1, src_size, fout);
data_pos += src_size;
bytes_out += (int64_t) src_size;
}
}
fclose(fout);
fprintf(stderr, "[Quantize] Quantized %d/%d tensors\n", n_quantized, n_tensors);
fprintf(stderr, "[Quantize] %.1f GB -> %.1f GB (%.1fx)\n", (double) bytes_in / 1e9, (double) bytes_out / 1e9,
bytes_out > 0 ? (double) bytes_in / (double) bytes_out : 0.0);
fprintf(stderr, "[Quantize] Wrote %s\n", out_path);
gguf_free(out);
gguf_free(inp);
ggml_free(meta);
#ifdef _WIN32
UnmapViewOfFile(mapping);
CloseHandle(mh);
CloseHandle(fh);
#else
munmap(mapping, file_size);
close(fd);
#endif
return 0;
}
+283
View File
@@ -0,0 +1,283 @@
// qwen-codec.cpp: codec CLI for Qwen3-TTS.
//
// Encode a 24 kHz mono WAV into RVQ codes (.rvq), or decode RVQ codes
// back into a 24 kHz mono float32 WAV. Mode is inferred from the input
// file extension: .wav in -> encode, .rvq in -> decode. Output is
// auto-named next to the input file by swapping the extension.
//
// File format (.rvq): flat code stream packed at 11 bits per code,
// LSB-first, no header. Layout is [K, T] row-major. K is fixed by the
// codec config in the GGUF (16 codebooks for the 12Hz tokenizer,
// codebook_size = 2048). T is the frame count derived from filesize.
#include "audio-io.h"
#include "backend.h"
#include "pipeline-codec.h"
#include "version.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
static const uint32_t QWEN_RVQ_CODE_MASK = (1u << QWEN_TOKENIZER_CODE_BITS) - 1u;
static void print_usage(const char * prog) {
fprintf(stderr, "qwentts.cpp %s\n\n", QWEN_VERSION);
fprintf(stderr,
"Usage: %s --model <gguf> [-i <input>] [--format <fmt>]\n\n"
"Required:\n"
" --model <gguf> Codec GGUF (qwen-tokenizer-12hz-*.gguf)\n\n"
"Optional:\n"
" -i <path> Input. WAV -> encode, .rvq -> decode\n"
" --format <fmt> WAV output format: wav16, wav24, wav32 (default: wav16)\n\n"
"Output is auto-named next to input : clip.wav -> clip.rvq, clip.rvq -> clip.wav.\n"
"When -i is omitted, runs a load self-test of the codec GGUF.\n",
prog);
}
// Symmetric unpack: reads N codes from packed bytes (11 bits LSB-first).
static std::vector<int32_t> unpack_codes(const std::vector<uint8_t> & in, size_t n_codes) {
std::vector<int32_t> out(n_codes);
uint64_t acc = 0;
int bits_in_acc = 0;
size_t in_pos = 0;
for (size_t i = 0; i < n_codes; i++) {
while (bits_in_acc < QWEN_TOKENIZER_CODE_BITS && in_pos < in.size()) {
acc |= ((uint64_t) in[in_pos++]) << bits_in_acc;
bits_in_acc += 8;
}
out[i] = (int32_t) (acc & QWEN_RVQ_CODE_MASK);
acc >>= QWEN_TOKENIZER_CODE_BITS;
bits_in_acc -= QWEN_TOKENIZER_CODE_BITS;
}
return out;
}
// Pack flat int32 codes into 11-bit LSB-first packed bytes. Output size is
// ceil(N * 11 / 8) bytes.
static std::vector<uint8_t> pack_codes(const std::vector<int32_t> & codes) {
const size_t total_bits = codes.size() * (size_t) QWEN_TOKENIZER_CODE_BITS;
std::vector<uint8_t> out((total_bits + 7) / 8, 0);
uint64_t acc = 0;
int bits_in_acc = 0;
size_t out_pos = 0;
for (size_t i = 0; i < codes.size(); i++) {
acc |= ((uint64_t) ((uint32_t) codes[i] & QWEN_RVQ_CODE_MASK)) << bits_in_acc;
bits_in_acc += QWEN_TOKENIZER_CODE_BITS;
while (bits_in_acc >= 8) {
out[out_pos++] = (uint8_t) (acc & 0xFF);
acc >>= 8;
bits_in_acc -= 8;
}
}
if (bits_in_acc > 0) {
out[out_pos++] = (uint8_t) (acc & 0xFF);
}
return out;
}
// Read a .rvq file and unpack it into K*T codes. T is inferred from the
// file size: T = (filesize * 8) / (K * QWEN_TOKENIZER_CODE_BITS).
static bool read_rvq(const char * path, int K, std::vector<int32_t> & codes, int * n_frames) {
FILE * f = fopen(path, "rb");
if (!f) {
fprintf(stderr, "[Codec] FATAL: cannot open %s\n", path);
return false;
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0) {
fprintf(stderr, "[Codec] FATAL: %s is empty\n", path);
fclose(f);
return false;
}
std::vector<uint8_t> buf((size_t) sz);
if (fread(buf.data(), 1, buf.size(), f) != buf.size()) {
fprintf(stderr, "[Codec] FATAL: short read on %s\n", path);
fclose(f);
return false;
}
fclose(f);
const size_t total_bits = (size_t) sz * 8;
const size_t n_codes = total_bits / (size_t) QWEN_TOKENIZER_CODE_BITS;
if (n_codes == 0 || (n_codes % (size_t) K) != 0) {
fprintf(stderr, "[Codec] FATAL: %s yields %zu codes, not a multiple of K=%d\n", path, n_codes, K);
return false;
}
codes = unpack_codes(buf, n_codes);
*n_frames = (int) (n_codes / (size_t) K);
return true;
}
// Pack and write a .rvq file.
static bool write_rvq(const char * path, const std::vector<int32_t> & codes) {
std::vector<uint8_t> packed = pack_codes(codes);
FILE * f = fopen(path, "wb");
if (!f) {
fprintf(stderr, "[Codec] FATAL: cannot open %s for write\n", path);
return false;
}
if (fwrite(packed.data(), 1, packed.size(), f) != packed.size()) {
fprintf(stderr, "[Codec] FATAL: short write on %s\n", path);
fclose(f);
return false;
}
fclose(f);
return true;
}
// Replace or append extension on a path string.
static std::string swap_ext(const std::string & path, const char * ext) {
size_t dot = path.find_last_of('.');
size_t sep = path.find_last_of("/\\");
if (dot != std::string::npos && (sep == std::string::npos || dot > sep)) {
return path.substr(0, dot) + ext;
}
return path + ext;
}
// 0: unsupported, 1: encode (.wav in), 2: decode (.rvq in).
static int infer_mode(const char * path) {
size_t n = strlen(path);
if (n >= 4 && strcmp(path + n - 4, ".wav") == 0) {
return 1;
}
if (n >= 4 && strcmp(path + n - 4, ".rvq") == 0) {
return 2;
}
return 0;
}
int main(int argc, char ** argv) {
if (argc <= 1) {
print_usage(argv[0]);
return 0;
}
const char * model_path = NULL;
const char * input_path = NULL;
WavFormat wav_fmt = WAV_S16;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--model") == 0 && i + 1 < argc) {
model_path = argv[++i];
} else if (strcmp(argv[i], "-i") == 0 && i + 1 < argc) {
input_path = argv[++i];
} else if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) {
if (!audio_parse_format(argv[++i], wav_fmt)) {
fprintf(stderr, "[CLI] ERROR: unknown format: %s\n", argv[i]);
print_usage(argv[0]);
return 1;
}
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
return 0;
} else {
fprintf(stderr, "[CLI] ERROR: unknown arg: %s\n", argv[i]);
print_usage(argv[0]);
return 1;
}
}
if (!model_path) {
print_usage(argv[0]);
return 1;
}
int mode = 0;
if (input_path) {
mode = infer_mode(input_path);
if (mode == 0) {
fprintf(stderr, "[CLI] ERROR: %s: unsupported extension (expect .wav or .rvq)\n", input_path);
return 1;
}
}
BackendPair bp = backend_init("Codec");
if (!bp.backend) {
fprintf(stderr, "[Codec] FATAL: backend init failed\n");
return 1;
}
PipelineCodec pc = {};
if (!pipeline_codec_load(&pc, model_path, bp)) {
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
int rc = 0;
if (!input_path) {
fprintf(stderr, "[Codec] Load self-test passed\n");
} else if (mode == 1) {
// Encode .wav -> .rvq
const std::string out_str = swap_ext(input_path, ".rvq");
int T_in = 0;
float * audio_in = audio_read_mono(input_path, QWEN_TOKENIZER_SAMPLE_RATE, &T_in);
if (!audio_in || T_in <= 0) {
fprintf(stderr, "[Codec] FATAL: cannot read %s\n", input_path);
free(audio_in);
rc = 1;
} else {
// Pad to a multiple of HOP_LENGTH so the RVQ frame count is integral.
int hop = QWEN_TOKENIZER_HOP_LENGTH;
int T_padded = ((T_in + hop - 1) / hop) * hop;
int T_frames = T_padded / hop;
std::vector<float> audio_buf((size_t) T_padded, 0.0f);
memcpy(audio_buf.data(), audio_in, (size_t) T_in * sizeof(float));
free(audio_in);
fprintf(stderr, "[Codec] Encode: %s, %d samples @ %d Hz, padded to %d (%d frames @ 12.5 Hz, %.2f s)\n",
input_path, T_in, QWEN_TOKENIZER_SAMPLE_RATE, T_padded, T_frames,
(double) T_padded / (double) QWEN_TOKENIZER_SAMPLE_RATE);
std::vector<int32_t> codes = pipeline_codec_encode(&pc, audio_buf.data(), T_padded);
if (codes.empty()) {
fprintf(stderr, "[Codec] FATAL: encode failed\n");
rc = 1;
} else if (!write_rvq(out_str.c_str(), codes)) {
rc = 1;
} else {
fprintf(stderr, "[Codec] Wrote %s: K=%d T=%d, %zu codes -> %zu packed bytes\n", out_str.c_str(),
QWEN_TOKENIZER_NUM_CODEBOOKS, T_frames, codes.size(),
(codes.size() * (size_t) QWEN_TOKENIZER_CODE_BITS + 7) / 8);
}
}
} else {
// Decode .rvq -> .wav
const std::string out_str = swap_ext(input_path, ".wav");
std::vector<int32_t> codes;
int T = 0;
if (!read_rvq(input_path, QWEN_TOKENIZER_NUM_CODEBOOKS, codes, &T)) {
rc = 1;
} else {
fprintf(stderr, "[Codec] Decode: %s, K=%d T=%d (%.2f s)\n", input_path, QWEN_TOKENIZER_NUM_CODEBOOKS, T,
(double) (T * QWEN_TOKENIZER_HOP_LENGTH) / (double) QWEN_TOKENIZER_SAMPLE_RATE);
std::vector<float> audio = pipeline_codec_decode(&pc, codes.data(), QWEN_TOKENIZER_NUM_CODEBOOKS, T);
if (audio.empty()) {
fprintf(stderr, "[Codec] FATAL: decode failed\n");
rc = 1;
} else if (!audio_write_wav(out_str.c_str(), audio.data(), (int) audio.size(), QWEN_TOKENIZER_SAMPLE_RATE,
wav_fmt)) {
fprintf(stderr, "[Codec] FATAL: cannot write %s\n", out_str.c_str());
rc = 1;
} else {
fprintf(stderr, "[Codec] Wrote %s: %d samples @ %d Hz, %.2f s\n", out_str.c_str(), (int) audio.size(),
QWEN_TOKENIZER_SAMPLE_RATE, (double) audio.size() / (double) QWEN_TOKENIZER_SAMPLE_RATE);
}
}
}
pipeline_codec_free(&pc);
backend_release(bp.backend, bp.cpu_backend);
return rc;
}
+340
View File
@@ -0,0 +1,340 @@
// qwen-tts.cpp : thin CLI wrapper around the Qwen3-TTS synthesis
// pipeline. Parses arguments, loads the talker + codec GGUFs, hands
// off to pipeline_tts_synthesize and writes the resulting waveform as
// a WAV file. All heavy lifting lives in src/pipeline-tts.cpp.
//
// Talker variants : 0.6B-Base / 0.6B-CustomVoice / 1.7B-Base /
// 1.7B-CustomVoice / 1.7B-VoiceDesign. The decoder path is selected
// from GGUF metadata at load time. The CLI surface mirrors the
// omnivoice.cpp tooling : kebab-case flags, --format wav16/wav24/wav32,
// -o '-' streams to stdout, --seed -1 means non deterministic, the
// utterance text comes from --text or stdin if --text is absent.
#include "audio-io.h"
#include "backend.h"
#include "bpe.h"
#include "pipeline-tts.h"
#include "version.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <random>
#include <sstream>
#include <stdexcept>
#include <string>
static void print_usage(const char * prog) {
fprintf(stderr, "qwentts.cpp %s\n\n", QWEN_VERSION);
fprintf(stderr,
"Usage: %s --model <gguf> --codec <gguf> [options] -o <out.wav>\n\n"
"Required:\n"
" --model <gguf> Talker LM GGUF (qwen-talker-*.gguf)\n"
" --codec <gguf> Tokenizer GGUF (qwen-tokenizer-*.gguf)\n"
" -o <path> Output WAV. '-' streams to stdout (pipe friendly).\n\n"
"Input:\n"
" --text <s> Utterance text. If absent, stdin is read fully.\n\n"
"Synthesis options:\n"
" --lang <name> Language (auto, english, chinese, ...) (default: english)\n"
" --instruct <s> Style instruction. Required for VoiceDesign, optional\n"
" for CustomVoice. Rejected for Base.\n"
" --speaker <name> Speaker name. Only valid for CustomVoice.\n"
" --ref-audio <wav> Reference WAV path for voice clone (Base only). Mutually\n"
" exclusive with --speaker. Mode A (x_vector_only) extracts\n"
" a speaker embedding via the ECAPA-TDNN encoder.\n"
" --ref-text <s> Reference transcript for voice clone ICL mode (Base only,\n"
" requires --ref-audio). Switches the prompt to ICL mode B\n"
" where the talker conditions on the reference codec codes.\n"
" --max-new <n> Max new audio frames (default: 2048)\n"
" --format <fmt> WAV output format: wav16, wav24, wav32 (default: wav16)\n\n"
"Sampling options:\n"
" --seed <n> Sampling seed, -1 for random (default: -1)\n"
" --greedy Disable stochastic sampling on both stacks\n"
" --temp <f> Talker temperature (default: 0.9)\n"
" --top-k <n> Talker top-k (default: 50, 0 = disabled)\n"
" --top-p <f> Talker top-p (default: 1.0)\n"
" --rep-pen <f> Talker repetition penalty (default: 1.05)\n"
" --sub-temp <f> Sub-talker temperature (default: 0.9)\n"
" --sub-top-k <n> Sub-talker top-k (default: 50)\n"
" --sub-top-p <f> Sub-talker top-p (default: 1.0)\n\n"
"Debug:\n"
" --dump <dir> Dump intermediate tensors for cossim debug\n",
prog);
}
struct Args {
const char * model;
const char * codec;
const char * text;
const char * lang;
const char * instruct;
const char * speaker;
const char * ref_audio;
const char * ref_text;
const char * dump_dir;
const char * out_wav;
const char * format;
int max_new_tokens;
int64_t seed;
bool do_sample;
float temperature;
int top_k;
float top_p;
float repetition_penalty;
int subtalker_top_k;
float subtalker_top_p;
float subtalker_temperature;
bool subtalker_do_sample;
};
// Read all of stdin into a string. Trims trailing newlines so a piped
// text file behaves like a clean --text argument.
static std::string read_stdin_text() {
std::ostringstream ss;
ss << std::cin.rdbuf();
std::string s = ss.str();
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) {
s.pop_back();
}
return s;
}
static bool parse_args(int argc, char ** argv, Args & a) {
a = {};
a.lang = "english";
a.format = "wav16";
a.max_new_tokens = 2048;
a.seed = -1;
a.do_sample = true;
a.temperature = 0.9f;
a.top_k = 50;
a.top_p = 1.0f;
a.repetition_penalty = 1.05f;
a.subtalker_do_sample = true;
a.subtalker_top_k = 50;
a.subtalker_top_p = 1.0f;
a.subtalker_temperature = 0.9f;
for (int i = 1; i < argc; i++) {
const char * arg = argv[i];
if (std::strcmp(arg, "-h") == 0 || std::strcmp(arg, "--help") == 0) {
return false;
}
if (std::strcmp(arg, "--model") == 0 && i + 1 < argc) {
a.model = argv[++i];
} else if (std::strcmp(arg, "--codec") == 0 && i + 1 < argc) {
a.codec = argv[++i];
} else if (std::strcmp(arg, "--text") == 0 && i + 1 < argc) {
a.text = argv[++i];
} else if (std::strcmp(arg, "--lang") == 0 && i + 1 < argc) {
a.lang = argv[++i];
} else if (std::strcmp(arg, "--instruct") == 0 && i + 1 < argc) {
a.instruct = argv[++i];
} else if (std::strcmp(arg, "--speaker") == 0 && i + 1 < argc) {
a.speaker = argv[++i];
} else if (std::strcmp(arg, "--ref-audio") == 0 && i + 1 < argc) {
a.ref_audio = argv[++i];
} else if (std::strcmp(arg, "--ref-text") == 0 && i + 1 < argc) {
a.ref_text = argv[++i];
} else if (std::strcmp(arg, "--format") == 0 && i + 1 < argc) {
a.format = argv[++i];
} else if (std::strcmp(arg, "--dump") == 0 && i + 1 < argc) {
a.dump_dir = argv[++i];
} else if (std::strcmp(arg, "--max-new") == 0 && i + 1 < argc) {
a.max_new_tokens = std::atoi(argv[++i]);
} else if (std::strcmp(arg, "--seed") == 0 && i + 1 < argc) {
a.seed = (int64_t) std::atoll(argv[++i]);
} else if (std::strcmp(arg, "--greedy") == 0) {
// Greedy mode : argmax sampling on both stacks. The sampling
// fast path in sampling.h uses temperature <= 0 to short
// circuit to argmax, bypassing rep penalty and top-k/p
// truncation, which exactly mirrors the Python reference
// greedy behaviour used by tests/debug-tts-cossim.py.
a.do_sample = false;
a.subtalker_do_sample = false;
} else if (std::strcmp(arg, "--temp") == 0 && i + 1 < argc) {
a.temperature = (float) std::atof(argv[++i]);
} else if (std::strcmp(arg, "--top-k") == 0 && i + 1 < argc) {
a.top_k = std::atoi(argv[++i]);
} else if (std::strcmp(arg, "--top-p") == 0 && i + 1 < argc) {
a.top_p = (float) std::atof(argv[++i]);
} else if (std::strcmp(arg, "--rep-pen") == 0 && i + 1 < argc) {
a.repetition_penalty = (float) std::atof(argv[++i]);
} else if (std::strcmp(arg, "--sub-temp") == 0 && i + 1 < argc) {
a.subtalker_temperature = (float) std::atof(argv[++i]);
} else if (std::strcmp(arg, "--sub-top-k") == 0 && i + 1 < argc) {
a.subtalker_top_k = std::atoi(argv[++i]);
} else if (std::strcmp(arg, "--sub-top-p") == 0 && i + 1 < argc) {
a.subtalker_top_p = (float) std::atof(argv[++i]);
} else if (std::strcmp(arg, "-o") == 0 && i + 1 < argc) {
a.out_wav = argv[++i];
} else {
fprintf(stderr, "[CLI] ERROR: unknown or incomplete argument: %s\n", arg);
return false;
}
}
return a.model && a.codec;
}
static int run(const Args & a) {
BackendPair bp = backend_init("Talker");
PipelineTTS pt;
if (!pipeline_tts_load(&pt, a.model, a.codec, bp)) {
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
// Validate mode flag combination against the loaded model_type. The
// upstream Python raises ValueError when generate_voice_design is
// called on a non voice_design model and the same shape applies to
// generate_custom_voice. We mirror that here, explicit and KISS, so
// the user never gets a silently wrong synthesis.
const std::string mt = pt.model_type;
if (a.speaker && mt != "custom_voice") {
fprintf(stderr, "[CLI] ERROR: --speaker is only valid for custom_voice models (loaded: %s)\n", mt.c_str());
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (a.instruct && mt == "base") {
fprintf(stderr, "[CLI] ERROR: --instruct is not supported for base models\n");
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (mt == "custom_voice" && !a.speaker) {
fprintf(stderr, "[CLI] ERROR: custom_voice models require --speaker\n");
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (mt == "voice_design" && (!a.instruct || a.instruct[0] == '\0')) {
fprintf(stderr, "[CLI] ERROR: voice_design models require --instruct\n");
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (a.ref_audio && mt != "base") {
fprintf(stderr, "[CLI] ERROR: --ref-audio is only valid for base models (loaded: %s)\n", mt.c_str());
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (a.speaker && a.ref_audio) {
fprintf(stderr, "[CLI] ERROR: --speaker and --ref-audio are mutually exclusive\n");
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (a.ref_text && !a.ref_audio) {
fprintf(stderr, "[CLI] ERROR: --ref-text requires --ref-audio\n");
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
// Resolve output WAV format string : wav16 / wav24 / wav32. Default
// wav16 mirrors the omnivoice.cpp default.
WavFormat wav_fmt;
if (!audio_parse_format(a.format, wav_fmt)) {
fprintf(stderr, "[CLI] ERROR: invalid --format '%s' (expected wav16, wav24, wav32)\n", a.format);
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
// Resolve utterance text : explicit --text wins, otherwise read stdin
// fully. Empty stdin combined with no --text triggers a clean error.
std::string text_buf;
const char * text = a.text;
if (!text) {
text_buf = read_stdin_text();
if (text_buf.empty()) {
fprintf(stderr, "[CLI] ERROR: no --text and stdin is empty\n");
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
text = text_buf.c_str();
}
// Resolve seed : -1 means non deterministic, sample from a hardware
// random_device. Anything else is taken verbatim, including negative
// values reaching int64 range, so reproducibility is one --seed away.
int64_t seed = a.seed;
if (seed < 0) {
std::random_device rd;
seed = (int64_t) (((uint64_t) rd() << 32) ^ (uint64_t) rd());
}
BPETokenizer tok = {};
if (!load_bpe_from_gguf(&tok, a.model)) {
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
const char * specials_keys[] = {
"qwen3-tts.text.im_start_id", "qwen3-tts.text.im_end_id", "qwen3-tts.text.tts_pad_id",
"qwen3-tts.text.tts_bos_id", "qwen3-tts.text.tts_eos_id",
};
bpe_load_specials_from_keys(&tok, a.model, specials_keys, 5);
PipelineTTSSynthesizeParams p = {};
p.text = text;
p.lang = a.lang;
p.instruct = a.instruct;
p.speaker = a.speaker;
p.ref_audio = a.ref_audio;
p.ref_text = a.ref_text;
p.seed = seed;
p.max_new_tokens = a.max_new_tokens;
p.do_sample = a.do_sample;
p.temperature = a.temperature;
p.top_k = a.top_k;
p.top_p = a.top_p;
p.repetition_penalty = a.repetition_penalty;
p.subtalker_do_sample = a.subtalker_do_sample;
p.subtalker_temperature = a.subtalker_temperature;
p.subtalker_top_k = a.subtalker_top_k;
p.subtalker_top_p = a.subtalker_top_p;
p.dump_dir = a.dump_dir;
PipelineTTSSynthesizeOutput out;
if (!pipeline_tts_synthesize(&pt, &tok, p, &out)) {
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
if (!out.audio.empty()) {
const char * out_path = a.out_wav ? a.out_wav : "out.wav";
if (!audio_write_wav(out_path, out.audio.data(), (int) out.audio.size(), out.sample_rate, wav_fmt)) {
fprintf(stderr, "[Pipeline] FATAL: WAV write failed for %s\n", out_path);
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 1;
}
qt_log(QT_LOG_INFO, "[Pipeline] Wrote %zu samples (%.2f s) -> %s", out.audio.size(),
(double) out.audio.size() / (double) out.sample_rate, out_path);
}
pipeline_tts_free(&pt);
backend_release(bp.backend, bp.cpu_backend);
return 0;
}
int main(int argc, char ** argv) {
Args a;
if (!parse_args(argc, argv, a)) {
print_usage(argv[0]);
return 1;
}
try {
return run(a);
} catch (const std::runtime_error & e) {
qt_set_error("%s", e.what());
qt_log(QT_LOG_ERROR, "%s", e.what());
return 1;
}
}
+38
View File
@@ -0,0 +1,38 @@
# Generate version.h with the current git commit hash and date.
# Only rewrites the file if the content changed (avoids rebuild cascade).
# Usage: cmake -DSRC_DIR=... -DOUTPUT=... -P version.cmake
execute_process(
COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY "${SRC_DIR}"
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
RESULT_VARIABLE GIT_RESULT
)
if(NOT GIT_RESULT EQUAL 0)
set(GIT_HASH "unknown")
endif()
execute_process(
COMMAND git show -s --format=%cs HEAD
WORKING_DIRECTORY "${SRC_DIR}"
OUTPUT_VARIABLE GIT_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
RESULT_VARIABLE DATE_RESULT
)
if(NOT DATE_RESULT EQUAL 0)
set(GIT_DATE "unknown")
endif()
set(CONTENT "#pragma once\n#define QWEN_VERSION \"${GIT_HASH} (${GIT_DATE})\"\n")
if(EXISTS "${OUTPUT}")
file(READ "${OUTPUT}" EXISTING)
if("${EXISTING}" STREQUAL "${CONTENT}")
return()
endif()
endif()
file(WRITE "${OUTPUT}" "${CONTENT}")