facade, abi: thin CLI on top of qwen_* public ABI, lock contract with C99 probe
This commit is contained in:
@@ -149,3 +149,26 @@ link_ggml_backends(qwen-codec)
|
|||||||
add_executable(qwen-tts tools/qwen-tts.cpp)
|
add_executable(qwen-tts tools/qwen-tts.cpp)
|
||||||
target_link_libraries(qwen-tts PRIVATE qwen-core)
|
target_link_libraries(qwen-tts PRIVATE qwen-core)
|
||||||
link_ggml_backends(qwen-tts)
|
link_ggml_backends(qwen-tts)
|
||||||
|
|
||||||
|
# test-abi-c : pure C99 smoke test that locks in the public ABI contract.
|
||||||
|
# Compiles qwen.h with a C compiler under -Wall -Werror -pedantic and
|
||||||
|
# links against the static lib. The test never loads a model ; failure
|
||||||
|
# means the public API regressed. Built by default so a regression breaks
|
||||||
|
# the main build, not just an opt-in target.
|
||||||
|
add_executable(test-abi-c tests/abi-c.c)
|
||||||
|
set_target_properties(test-abi-c PROPERTIES
|
||||||
|
C_STANDARD 99
|
||||||
|
C_STANDARD_REQUIRED ON
|
||||||
|
C_EXTENSIONS OFF
|
||||||
|
)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(test-abi-c PRIVATE /W4 /WX)
|
||||||
|
else()
|
||||||
|
target_compile_options(test-abi-c PRIVATE -Wall -Werror -pedantic)
|
||||||
|
endif()
|
||||||
|
target_include_directories(test-abi-c PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_BINARY_DIR}
|
||||||
|
)
|
||||||
|
target_link_libraries(test-abi-c PRIVATE qwen-core)
|
||||||
|
link_ggml_backends(test-abi-c)
|
||||||
|
|||||||
@@ -309,43 +309,36 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
|
|||||||
const std::string & mt = q->pt.model_type;
|
const std::string & mt = q->pt.model_type;
|
||||||
if (params->speaker && mt != "custom_voice") {
|
if (params->speaker && mt != "custom_voice") {
|
||||||
qt_set_error("--speaker is only valid for custom_voice models (loaded: %s)", mt.c_str());
|
qt_set_error("--speaker is only valid for custom_voice models (loaded: %s)", mt.c_str());
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_MODE_INVALID;
|
return QWEN_STATUS_MODE_INVALID;
|
||||||
}
|
}
|
||||||
if (params->instruct && mt == "base") {
|
if (params->instruct && mt == "base") {
|
||||||
qt_set_error("--instruct is not supported for base models");
|
qt_set_error("--instruct is not supported for base models");
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_MODE_INVALID;
|
return QWEN_STATUS_MODE_INVALID;
|
||||||
}
|
}
|
||||||
if (mt == "custom_voice" && !params->speaker) {
|
if (mt == "custom_voice" && !params->speaker) {
|
||||||
qt_set_error("custom_voice models require --speaker");
|
qt_set_error("custom_voice models require --speaker");
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_MODE_INVALID;
|
return QWEN_STATUS_MODE_INVALID;
|
||||||
}
|
}
|
||||||
if (mt == "voice_design" && (!params->instruct || params->instruct[0] == '\0')) {
|
if (mt == "voice_design" && (!params->instruct || params->instruct[0] == '\0')) {
|
||||||
qt_set_error("voice_design models require --instruct");
|
qt_set_error("voice_design models require --instruct");
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_MODE_INVALID;
|
return QWEN_STATUS_MODE_INVALID;
|
||||||
}
|
}
|
||||||
if (params->ref_audio_24k && mt != "base") {
|
if (params->ref_audio_24k && mt != "base") {
|
||||||
qt_set_error("--ref-audio is only valid for base models (loaded: %s)", mt.c_str());
|
qt_set_error("--ref-audio is only valid for base models (loaded: %s)", mt.c_str());
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_MODE_INVALID;
|
return QWEN_STATUS_MODE_INVALID;
|
||||||
}
|
}
|
||||||
if (params->speaker && params->ref_audio_24k) {
|
if (params->speaker && params->ref_audio_24k) {
|
||||||
qt_set_error("--speaker and --ref-audio are mutually exclusive");
|
qt_set_error("--speaker and --ref-audio are mutually exclusive");
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_INVALID_PARAMS;
|
return QWEN_STATUS_INVALID_PARAMS;
|
||||||
}
|
}
|
||||||
if (params->ref_text && !params->ref_audio_24k) {
|
if (params->ref_text && !params->ref_audio_24k) {
|
||||||
qt_set_error("--ref-text requires --ref-audio");
|
qt_set_error("--ref-text requires --ref-audio");
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_INVALID_PARAMS;
|
return QWEN_STATUS_INVALID_PARAMS;
|
||||||
}
|
}
|
||||||
@@ -397,7 +390,6 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
|
|||||||
float * buf = (float *) std::malloc(bytes > 0 ? bytes : 1);
|
float * buf = (float *) std::malloc(bytes > 0 ? bytes : 1);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
qt_set_error("qwen_synthesize : malloc failed for %zu samples", n);
|
qt_set_error("qwen_synthesize : malloc failed for %zu samples", n);
|
||||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
|
||||||
qwen_audio_free(out);
|
qwen_audio_free(out);
|
||||||
return QWEN_STATUS_OOM;
|
return QWEN_STATUS_OOM;
|
||||||
}
|
}
|
||||||
|
|||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
/* tests/abi-c.c : link-only ABI smoke test for qwen.h.
|
||||||
|
*
|
||||||
|
* Compiled in pure C99 with -Wall -Werror -pedantic. The purpose of this
|
||||||
|
* test is NOT to run a full synthesis (no GGUF loaded, no model required) ;
|
||||||
|
* it is to guarantee at every build that :
|
||||||
|
*
|
||||||
|
* 1. qwen.h parses with a C compiler (no <cstdio>, no std::*, no
|
||||||
|
* C++-only forward declarations leak in).
|
||||||
|
* 2. Every public qwen_* symbol has C linkage and links from a C
|
||||||
|
* translation unit.
|
||||||
|
* 3. The structs are POD and zero-initialisable with `{0}` from C.
|
||||||
|
* 4. The qwen_log_set callback routes formatted messages from the lib
|
||||||
|
* to the user, and abi_version validation rejects future structs.
|
||||||
|
*
|
||||||
|
* If this test stops compiling or stops linking, the public ABI has
|
||||||
|
* regressed and the build breaks before anything else.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "qwen.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Counter incremented by the stub log callback. The probe checks that at
|
||||||
|
* least one log line was routed through the callback by triggering a
|
||||||
|
* qwen_init failure (which emits a [qwen] ERROR line via qt_log). */
|
||||||
|
static int g_log_lines = 0;
|
||||||
|
static enum qwen_log_level g_last_log_level = QWEN_LOG_DEBUG;
|
||||||
|
static char g_last_log_msg[512] = { 0 };
|
||||||
|
|
||||||
|
static void stub_log(enum qwen_log_level level, const char * msg, void * user_data) {
|
||||||
|
(void) user_data;
|
||||||
|
g_log_lines++;
|
||||||
|
g_last_log_level = level;
|
||||||
|
if (msg) {
|
||||||
|
size_t n = strlen(msg);
|
||||||
|
if (n >= sizeof(g_last_log_msg)) {
|
||||||
|
n = sizeof(g_last_log_msg) - 1;
|
||||||
|
}
|
||||||
|
memcpy(g_last_log_msg, msg, n);
|
||||||
|
g_last_log_msg[n] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
/* Static version string, always reachable. */
|
||||||
|
const char * version = qwen_version();
|
||||||
|
printf("qwen ABI probe : %s\n", version);
|
||||||
|
|
||||||
|
/* Default-initialise the public structs from C. */
|
||||||
|
struct qwen_init_params iparams;
|
||||||
|
qwen_init_default_params(&iparams);
|
||||||
|
|
||||||
|
struct qwen_tts_params params;
|
||||||
|
qwen_tts_default_params(¶ms);
|
||||||
|
|
||||||
|
/* Sanity-check a few default values, including the abi_version. */
|
||||||
|
if (params.max_new_tokens != 2048 || params.temperature != 0.9f) {
|
||||||
|
fprintf(stderr, "ABI probe : default values do not match\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (iparams.abi_version != QWEN_ABI_VERSION || params.abi_version != QWEN_ABI_VERSION) {
|
||||||
|
fprintf(stderr, "ABI probe : abi_version not set by qwen_*_default_params\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch every output struct field so the compiler validates the
|
||||||
|
* layout end-to-end without ever needing a model. */
|
||||||
|
struct qwen_audio audio = { 0 };
|
||||||
|
qwen_audio_free(&audio);
|
||||||
|
|
||||||
|
/* Install the log callback before the failing init so the [qwen]
|
||||||
|
* ERROR line lands on stub_log instead of stderr. */
|
||||||
|
qwen_log_set(stub_log, NULL);
|
||||||
|
|
||||||
|
/* Call every entry through its early-return path. qwen_init returns
|
||||||
|
* NULL on missing talker_path / codec_path, qwen_synthesize fails on
|
||||||
|
* NULL handle, qwen_free is safe on NULL. None of these load a model,
|
||||||
|
* but the linker must resolve every name to satisfy the call. */
|
||||||
|
struct qwen_context * dummy = qwen_init(NULL);
|
||||||
|
if (dummy != NULL) {
|
||||||
|
fprintf(stderr, "ABI probe : qwen_init(NULL) was supposed to return NULL\n");
|
||||||
|
qwen_free(dummy);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* qwen_init(NULL) just failed -> qwen_last_error() must point to a
|
||||||
|
* non-empty thread-local string. Pointer is always valid (c_str on
|
||||||
|
* an empty std::string still gives a NUL byte), so we only need to
|
||||||
|
* check the first byte to confirm an error was actually recorded. */
|
||||||
|
const char * err = qwen_last_error();
|
||||||
|
if (err == NULL || err[0] == '\0') {
|
||||||
|
fprintf(stderr, "ABI probe : qwen_last_error() empty after a known failure\n");
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The same failure must have surfaced through the log callback at
|
||||||
|
* ERROR level. */
|
||||||
|
if (g_log_lines == 0) {
|
||||||
|
fprintf(stderr, "ABI probe : qwen_log_set callback never invoked\n");
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
if (g_last_log_level != QWEN_LOG_ERROR) {
|
||||||
|
fprintf(stderr, "ABI probe : last log level was %d, expected %d\n", (int) g_last_log_level,
|
||||||
|
(int) QWEN_LOG_ERROR);
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
printf("qwen ABI probe : qwen_log_set routed %d line(s), last : '%s'\n", g_log_lines, g_last_log_msg);
|
||||||
|
printf("qwen ABI probe : qwen_last_error reads '%s'\n", err);
|
||||||
|
|
||||||
|
/* abi_version validation : a struct claiming a future ABI must be
|
||||||
|
* rejected up front, before any allocation. Both paths are filled
|
||||||
|
* with placeholders so the NULL guard does not short-circuit the
|
||||||
|
* abi_version branch. */
|
||||||
|
struct qwen_init_params future_iparams;
|
||||||
|
qwen_init_default_params(&future_iparams);
|
||||||
|
future_iparams.talker_path = "irrelevant.gguf";
|
||||||
|
future_iparams.codec_path = "irrelevant.gguf";
|
||||||
|
future_iparams.abi_version = QWEN_ABI_VERSION + 1;
|
||||||
|
struct qwen_context * rejected = qwen_init(&future_iparams);
|
||||||
|
if (rejected != NULL) {
|
||||||
|
fprintf(stderr, "ABI probe : qwen_init accepted a future abi_version\n");
|
||||||
|
qwen_free(rejected);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum qwen_status rc = qwen_synthesize(NULL, ¶ms, &audio);
|
||||||
|
if (rc != QWEN_STATUS_INVALID_PARAMS) {
|
||||||
|
fprintf(stderr, "ABI probe : qwen_synthesize(NULL) returned %d, expected %d\n", (int) rc,
|
||||||
|
(int) QWEN_STATUS_INVALID_PARAMS);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restore the default stderr fallback before exit so the trailing
|
||||||
|
* [qwen] log lines from the cleanup paths land where the user
|
||||||
|
* expects them. */
|
||||||
|
qwen_log_set(NULL, NULL);
|
||||||
|
|
||||||
|
qwen_free(NULL);
|
||||||
|
qwen_audio_free(&audio);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+78
-142
@@ -1,34 +1,36 @@
|
|||||||
// qwen-tts.cpp : thin CLI wrapper around the Qwen3-TTS synthesis
|
// qwen-tts.cpp : thin CLI wrapper around the qwentts.cpp public ABI.
|
||||||
// pipeline. Parses arguments, loads the talker + codec GGUFs, hands
|
// Parses arguments, reads the optional reference WAV plus transcript,
|
||||||
// off to pipeline_tts_synthesize and writes the resulting waveform as
|
// hands off to qwen_synthesize and writes the resulting waveform as a
|
||||||
// a WAV file. All heavy lifting lives in src/pipeline-tts.cpp.
|
// WAV file. All synthesis logic, mode validation and seed resolution
|
||||||
|
// live behind the qwen_* facade declared in qwen.h.
|
||||||
//
|
//
|
||||||
// Talker variants : 0.6B-Base / 0.6B-CustomVoice / 1.7B-Base /
|
// Talker variants : 0.6B-Base / 0.6B-CustomVoice / 1.7B-Base /
|
||||||
// 1.7B-CustomVoice / 1.7B-VoiceDesign. The decoder path is selected
|
// 1.7B-CustomVoice / 1.7B-VoiceDesign. The decoder path is selected
|
||||||
// from GGUF metadata at load time. The CLI surface mirrors the
|
// from GGUF metadata at qwen_init time. The CLI surface mirrors the
|
||||||
// omnivoice.cpp tooling : kebab-case flags, --format wav16/wav24/wav32,
|
// omnivoice.cpp tooling : kebab-case flags, --format wav16/wav24/wav32,
|
||||||
// -o '-' streams to stdout, --seed -1 means non deterministic, the
|
// -o '-' streams to stdout, --seed -1 means non deterministic
|
||||||
// utterance text comes from --text or stdin if --text is absent.
|
// (resolved inside qwen_synthesize), the utterance text comes from
|
||||||
|
// --text or stdin if --text is absent.
|
||||||
|
|
||||||
#include "audio-io.h"
|
#include "audio-io.h"
|
||||||
#include "backend.h"
|
#include "qwen.h"
|
||||||
#include "bpe.h"
|
|
||||||
#include "pipeline-tts.h"
|
|
||||||
#include "utf8.h"
|
|
||||||
#include "version.h"
|
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <random>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
// Tokenizer sample rate for the 12 Hz Qwen3-TTS codec : 24 kHz. Used
|
||||||
|
// by audio_read_mono to resample the optional --ref-audio file before
|
||||||
|
// handing it to the facade. The output sample rate is reported by
|
||||||
|
// qwen_audio.sample_rate after a successful synthesis.
|
||||||
|
static const int QWEN_TTS_SAMPLE_RATE = 24000;
|
||||||
|
|
||||||
static void print_usage(const char * prog) {
|
static void print_usage(const char * prog) {
|
||||||
fprintf(stderr, "qwentts.cpp %s\n\n", QWEN_VERSION);
|
fprintf(stderr, "qwentts.cpp %s\n\n", qwen_version());
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"Usage: %s --model <gguf> --codec <gguf> [options] -o <out.wav>\n\n"
|
"Usage: %s --model <gguf> --codec <gguf> [options] -o <out.wav>\n\n"
|
||||||
"Required:\n"
|
"Required:\n"
|
||||||
@@ -105,7 +107,7 @@ static std::string read_stdin_text() {
|
|||||||
|
|
||||||
// Read a small text file into a string. Trims trailing newlines.
|
// Read a small text file into a string. Trims trailing newlines.
|
||||||
static bool read_text_file(const char * path, std::string & out) {
|
static bool read_text_file(const char * path, std::string & out) {
|
||||||
FILE * f = utf8_fopen(path, "rb");
|
FILE * f = fopen(path, "rb");
|
||||||
if (!f) {
|
if (!f) {
|
||||||
fprintf(stderr, "[CLI] FATAL: cannot open '%s'\n", path);
|
fprintf(stderr, "[CLI] FATAL: cannot open '%s'\n", path);
|
||||||
return false;
|
return false;
|
||||||
@@ -208,60 +210,18 @@ static bool parse_args(int argc, char ** argv, Args & a) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int run(const Args & a) {
|
static int run(const Args & a) {
|
||||||
BackendPair bp = backend_init("Talker");
|
// Init the facade. The seven mode validations
|
||||||
|
// (base / custom_voice / voice_design rules) and the BPE tokenizer
|
||||||
|
// load live inside qwen_init / qwen_synthesize ; the CLI just hands
|
||||||
|
// off the two GGUF paths and reports qwen_last_error on failure.
|
||||||
|
qwen_init_params iparams;
|
||||||
|
qwen_init_default_params(&iparams);
|
||||||
|
iparams.talker_path = a.model;
|
||||||
|
iparams.codec_path = a.codec;
|
||||||
|
|
||||||
PipelineTTS pt = {};
|
qwen_context * q = qwen_init(&iparams);
|
||||||
if (!pipeline_tts_load(&pt, a.model, a.codec, bp)) {
|
if (!q) {
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
fprintf(stderr, "[CLI] ERROR: %s\n", qwen_last_error());
|
||||||
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_path && !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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,37 +231,33 @@ static int run(const Args & a) {
|
|||||||
const char * ref_text = NULL;
|
const char * ref_text = NULL;
|
||||||
if (a.ref_text_path) {
|
if (a.ref_text_path) {
|
||||||
if (!read_text_file(a.ref_text_path, ref_text_buf)) {
|
if (!read_text_file(a.ref_text_path, ref_text_buf)) {
|
||||||
pipeline_tts_free(&pt);
|
qwen_free(q);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (ref_text_buf.empty()) {
|
if (ref_text_buf.empty()) {
|
||||||
fprintf(stderr, "[CLI] ERROR: --ref-text file '%s' is empty\n", a.ref_text_path);
|
fprintf(stderr, "[CLI] ERROR: --ref-text file '%s' is empty\n", a.ref_text_path);
|
||||||
pipeline_tts_free(&pt);
|
qwen_free(q);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
ref_text = ref_text_buf.c_str();
|
ref_text = ref_text_buf.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the reference WAV once, mono at the codec sample rate. The
|
// Decode the reference WAV once, mono at the codec sample rate. The
|
||||||
// pipeline consumes the buffer directly so the WAV is read exactly
|
// facade consumes the buffer directly so the WAV is read exactly
|
||||||
// once regardless of how many encoders need the audio (speaker
|
// once regardless of how many encoders need the audio (speaker
|
||||||
// encoder embedding + codec encoder RVQ codes for ICL mode B).
|
// encoder embedding + codec encoder RVQ codes for ICL mode B).
|
||||||
std::vector<float> ref_audio_buf;
|
|
||||||
std::unique_ptr<float, void (*)(void *)> raw_holder(NULL, std::free);
|
std::unique_ptr<float, void (*)(void *)> raw_holder(NULL, std::free);
|
||||||
const float * ref_audio_24k = NULL;
|
const float * ref_audio_24k = NULL;
|
||||||
int ref_n_samples = 0;
|
int ref_n_samples = 0;
|
||||||
if (a.ref_audio) {
|
if (a.ref_audio) {
|
||||||
int T_in = 0;
|
int T_in = 0;
|
||||||
float * raw = audio_read_mono(a.ref_audio, QWEN_TOKENIZER_SAMPLE_RATE, &T_in);
|
float * raw = audio_read_mono(a.ref_audio, QWEN_TTS_SAMPLE_RATE, &T_in);
|
||||||
if (!raw || T_in <= 0) {
|
if (!raw || T_in <= 0) {
|
||||||
fprintf(stderr, "[CLI] ERROR: cannot read --ref-audio '%s'\n", a.ref_audio);
|
fprintf(stderr, "[CLI] ERROR: cannot read --ref-audio '%s'\n", a.ref_audio);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
std::free(raw);
|
std::free(raw);
|
||||||
}
|
}
|
||||||
pipeline_tts_free(&pt);
|
qwen_free(q);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
raw_holder.reset(raw);
|
raw_holder.reset(raw);
|
||||||
@@ -314,8 +270,7 @@ static int run(const Args & a) {
|
|||||||
WavFormat wav_fmt;
|
WavFormat wav_fmt;
|
||||||
if (!audio_parse_format(a.format, wav_fmt)) {
|
if (!audio_parse_format(a.format, wav_fmt)) {
|
||||||
fprintf(stderr, "[CLI] ERROR: invalid --format '%s' (expected wav16, wav24, wav32)\n", a.format);
|
fprintf(stderr, "[CLI] ERROR: invalid --format '%s' (expected wav16, wav24, wav32)\n", a.format);
|
||||||
pipeline_tts_free(&pt);
|
qwen_free(q);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,91 +282,72 @@ static int run(const Args & a) {
|
|||||||
text_buf = read_stdin_text();
|
text_buf = read_stdin_text();
|
||||||
if (text_buf.empty()) {
|
if (text_buf.empty()) {
|
||||||
fprintf(stderr, "[CLI] ERROR: no --text and stdin is empty\n");
|
fprintf(stderr, "[CLI] ERROR: no --text and stdin is empty\n");
|
||||||
pipeline_tts_free(&pt);
|
qwen_free(q);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
text = text_buf.c_str();
|
text = text_buf.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve seed : -1 means non deterministic, sample from a hardware
|
// Translate CLI args into the facade params. Seed -1 is forwarded
|
||||||
// random_device. Anything else is taken verbatim, including negative
|
// verbatim and resolved by qwen_synthesize via std::random_device.
|
||||||
// values reaching int64 range, so reproducibility is one --seed away.
|
qwen_tts_params params;
|
||||||
int64_t seed = a.seed;
|
qwen_tts_default_params(¶ms);
|
||||||
if (seed < 0) {
|
params.text = text;
|
||||||
std::random_device rd;
|
params.lang = a.lang;
|
||||||
seed = (int64_t) (((uint64_t) rd() << 32) ^ (uint64_t) rd());
|
params.instruct = a.instruct;
|
||||||
}
|
params.speaker = a.speaker;
|
||||||
|
params.ref_audio_24k = ref_audio_24k;
|
||||||
|
params.ref_n_samples = ref_n_samples;
|
||||||
|
params.ref_text = ref_text;
|
||||||
|
params.seed = a.seed;
|
||||||
|
params.max_new_tokens = a.max_new_tokens;
|
||||||
|
params.do_sample = a.do_sample;
|
||||||
|
params.temperature = a.temperature;
|
||||||
|
params.top_k = a.top_k;
|
||||||
|
params.top_p = a.top_p;
|
||||||
|
params.repetition_penalty = a.repetition_penalty;
|
||||||
|
params.subtalker_do_sample = a.subtalker_do_sample;
|
||||||
|
params.subtalker_temperature = a.subtalker_temperature;
|
||||||
|
params.subtalker_top_k = a.subtalker_top_k;
|
||||||
|
params.subtalker_top_p = a.subtalker_top_p;
|
||||||
|
params.dump_dir = a.dump_dir;
|
||||||
|
|
||||||
BPETokenizer tok = {};
|
qwen_audio audio = {};
|
||||||
if (!load_bpe_from_gguf(&tok, a.model)) {
|
qwen_status status = qwen_synthesize(q, ¶ms, &audio);
|
||||||
pipeline_tts_free(&pt);
|
if (status != QWEN_STATUS_OK) {
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
fprintf(stderr, "[CLI] ERROR: %s\n", qwen_last_error());
|
||||||
return 1;
|
qwen_audio_free(&audio);
|
||||||
}
|
qwen_free(q);
|
||||||
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_24k = ref_audio_24k;
|
|
||||||
p.ref_n_samples = ref_n_samples;
|
|
||||||
p.ref_text = 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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!out.audio.empty()) {
|
if (audio.n_samples > 0) {
|
||||||
const char * out_path = a.out_wav ? a.out_wav : "out.wav";
|
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)) {
|
if (!audio_write_wav(out_path, audio.samples, audio.n_samples, audio.sample_rate, wav_fmt)) {
|
||||||
fprintf(stderr, "[Pipeline] FATAL: WAV write failed for %s\n", out_path);
|
fprintf(stderr, "[Pipeline] FATAL: WAV write failed for %s\n", out_path);
|
||||||
pipeline_tts_free(&pt);
|
qwen_audio_free(&audio);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
qwen_free(q);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
qt_log(QT_LOG_INFO, "[Pipeline] Wrote %zu samples (%.2f s) -> %s", out.audio.size(),
|
fprintf(stderr, "[Pipeline] Wrote %d samples (%.2f s) -> %s\n", audio.n_samples,
|
||||||
(double) out.audio.size() / (double) out.sample_rate, out_path);
|
(double) audio.n_samples / (double) audio.sample_rate, out_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline_tts_free(&pt);
|
qwen_audio_free(&audio);
|
||||||
backend_release(bp.backend, bp.cpu_backend);
|
qwen_free(q);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char ** argv) {
|
int main(int argc, char ** argv) {
|
||||||
utf8_init(&argc, &argv);
|
|
||||||
Args a;
|
Args a;
|
||||||
if (!parse_args(argc, argv, a)) {
|
if (!parse_args(argc, argv, a)) {
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
try {
|
// The facade absorbs every std::exception thrown deep in the load
|
||||||
|
// and synthesis chains, converting them into qwen_status + a
|
||||||
|
// qwen_last_error message. No top-level try / catch needed here :
|
||||||
|
// the CLI just reads the status returned by qwen_init /
|
||||||
|
// qwen_synthesize and renders qwen_last_error to stderr.
|
||||||
return run(a);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user