Initial release
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user