refactor: ABI
This commit is contained in:
+45
-6
@@ -89,12 +89,51 @@ macro(link_ggml_backends target)
|
||||
add_dependencies(${target} version)
|
||||
endmacro()
|
||||
|
||||
# Core library shared between binaries. Holds the shared infrastructure
|
||||
# (error/log routing, future common helpers) that any binary linking the
|
||||
# pipeline needs. STATIC because we have actual sources now.
|
||||
add_library(qwen-core STATIC src/qt-error.cpp)
|
||||
# Core library always STATIC : the bundled CLI tools include
|
||||
# pipeline-tts.h / pipeline-codec.h / backend.h directly for the tests
|
||||
# paths that need every pipeline_* / backend_* symbol resolved without
|
||||
# going through the public ABI. QWENTTS_STATIC is propagated PUBLIC :
|
||||
# the lib's own .cpp files see it (so QWEN_API resolves to empty when
|
||||
# compiling qwen.cpp on Windows), and every consumer that links
|
||||
# qwen-core inherits it too (same effect on their side, no spurious
|
||||
# dllimport on a static archive). The shared library for ABI consumers
|
||||
# is a separate, opt-in target below.
|
||||
add_library(qwen-core STATIC
|
||||
src/qwen.cpp
|
||||
src/pipeline-tts.cpp
|
||||
src/pipeline-codec.cpp
|
||||
src/prompt-builder.cpp
|
||||
src/talker-forward.cpp
|
||||
src/code-predictor-forward.cpp
|
||||
)
|
||||
target_compile_definitions(qwen-core PUBLIC QWENTTS_STATIC)
|
||||
target_include_directories(qwen-core PUBLIC src)
|
||||
target_link_libraries(qwen-core PUBLIC ggml)
|
||||
link_ggml_backends(qwen-core)
|
||||
|
||||
# Public shared library for ABI consumers (Python ctypes, Rust bindgen,
|
||||
# Go cgo). Opt-in : -DQWENTTS_SHARED=ON at configure time. Exports only
|
||||
# the QWEN_API-marked symbols ; every internal pipeline_* / backend_*
|
||||
# stays hidden inside the .so. Intentionally a different target name
|
||||
# from qwen-core so the static path used by the tools is never affected.
|
||||
option(QWENTTS_SHARED "Build the shared qwen library for ABI consumers" OFF)
|
||||
if(QWENTTS_SHARED)
|
||||
add_library(qwen SHARED
|
||||
src/qwen.cpp
|
||||
src/pipeline-tts.cpp
|
||||
src/pipeline-codec.cpp
|
||||
src/prompt-builder.cpp
|
||||
src/talker-forward.cpp
|
||||
src/code-predictor-forward.cpp
|
||||
)
|
||||
target_compile_definitions(qwen PRIVATE QWENTTS_BUILD)
|
||||
set_target_properties(qwen PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN ON
|
||||
)
|
||||
link_ggml_backends(qwen)
|
||||
endif()
|
||||
|
||||
# quantize: GGUF requantizer (BF16 -> K-quants), shared policy with
|
||||
# omnivoice.cpp / acestep.cpp.
|
||||
@@ -102,11 +141,11 @@ add_executable(quantize tools/quantize.cpp)
|
||||
link_ggml_backends(quantize)
|
||||
|
||||
# qwen-codec : standalone codec CLI (codes <-> WAV via 12Hz tokenizer)
|
||||
add_executable(qwen-codec tools/qwen-codec.cpp src/pipeline-codec.cpp)
|
||||
add_executable(qwen-codec tools/qwen-codec.cpp)
|
||||
target_link_libraries(qwen-codec PRIVATE qwen-core)
|
||||
link_ggml_backends(qwen-codec)
|
||||
|
||||
# qwen-tts : full TTS pipeline (Talker LM + 12Hz tokenizer decoder).
|
||||
add_executable(qwen-tts tools/qwen-tts.cpp src/pipeline-tts.cpp src/pipeline-codec.cpp src/prompt-builder.cpp src/talker-forward.cpp src/code-predictor-forward.cpp)
|
||||
add_executable(qwen-tts tools/qwen-tts.cpp)
|
||||
target_link_libraries(qwen-tts PRIVATE qwen-core)
|
||||
link_ggml_backends(qwen-tts)
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
// qt-error.cpp : implementation of the qt_log / qt_set_error / qt_throw
|
||||
// helpers declared in qt-error.h. Storage is thread_local for the error
|
||||
// slot, atomic for the log callback so qt_log_set is wait-free.
|
||||
|
||||
#include "qt-error.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
// Thread-local backing store for qt_last_error(). std::string sized once
|
||||
// per thread, grows on demand, never freed across calls : the std runtime
|
||||
// reclaims it on thread exit. An empty string means "no error recorded
|
||||
// on this thread yet", which qt_last_error() exposes as "".
|
||||
static thread_local std::string g_last_error;
|
||||
|
||||
void qt_set_error_v(const char * fmt, va_list ap) {
|
||||
if (!fmt) {
|
||||
g_last_error.clear();
|
||||
return;
|
||||
}
|
||||
// Two-pass vsnprintf : first call sizes the buffer, second writes the
|
||||
// message. va_copy keeps the original ap valid for the second pass.
|
||||
va_list ap2;
|
||||
va_copy(ap2, ap);
|
||||
int needed = std::vsnprintf(nullptr, 0, fmt, ap2);
|
||||
va_end(ap2);
|
||||
if (needed < 0) {
|
||||
g_last_error = "qt_set_error : vsnprintf failed";
|
||||
return;
|
||||
}
|
||||
g_last_error.resize(static_cast<size_t>(needed));
|
||||
std::vsnprintf(g_last_error.data(), static_cast<size_t>(needed) + 1, fmt, ap);
|
||||
}
|
||||
|
||||
void qt_set_error(const char * fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
qt_set_error_v(fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
const char * qt_last_error(void) {
|
||||
return g_last_error.c_str();
|
||||
}
|
||||
|
||||
// Formats a message with printf semantics and throws std::runtime_error.
|
||||
// The catch site at the binary entry inspects the what() string and feeds
|
||||
// it into qt_set_error so the user-visible diagnostic is identical
|
||||
// whether the failure used the bool-return path or the throw path.
|
||||
void qt_throw(const char * fmt, ...) {
|
||||
char buf[1024];
|
||||
if (fmt) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
std::vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
throw std::runtime_error(buf);
|
||||
}
|
||||
|
||||
// Process-wide log callback. Atomic so qt_log_set can replace it without
|
||||
// locking : write happens with memory_order_release, every reader sees a
|
||||
// fully published callback pointer paired with its user_data slot.
|
||||
// std::atomic on a function pointer is lock-free on every platform we
|
||||
// target. user_data is a plain pointer because it is only ever published
|
||||
// alongside cb under the same release ordering.
|
||||
static std::atomic<qt_log_cb> g_log_cb{ nullptr };
|
||||
static void * g_log_cb_user = nullptr;
|
||||
|
||||
void qt_log_set(qt_log_cb cb, void * user_data) {
|
||||
g_log_cb_user = user_data;
|
||||
g_log_cb.store(cb, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Routes one log line to the installed callback or to stderr. Two-pass
|
||||
// vsnprintf sizes the heap buffer when the message exceeds the stack
|
||||
// scratchpad, which keeps the common case allocation-free.
|
||||
void qt_log(enum qt_log_level level, const char * fmt, ...) {
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
char stackbuf[512];
|
||||
char * buf = stackbuf;
|
||||
int needed = 0;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
{
|
||||
va_list ap2;
|
||||
va_copy(ap2, ap);
|
||||
needed = std::vsnprintf(stackbuf, sizeof(stackbuf), fmt, ap2);
|
||||
va_end(ap2);
|
||||
}
|
||||
if (needed < 0) {
|
||||
va_end(ap);
|
||||
return;
|
||||
}
|
||||
std::string heapbuf;
|
||||
if ((size_t) needed >= sizeof(stackbuf)) {
|
||||
heapbuf.resize((size_t) needed);
|
||||
std::vsnprintf(heapbuf.data(), (size_t) needed + 1, fmt, ap);
|
||||
buf = heapbuf.data();
|
||||
}
|
||||
va_end(ap);
|
||||
|
||||
qt_log_cb cb = g_log_cb.load(std::memory_order_acquire);
|
||||
if (cb) {
|
||||
cb(level, buf, g_log_cb_user);
|
||||
} else {
|
||||
std::fprintf(stderr, "%s\n", buf);
|
||||
}
|
||||
}
|
||||
+38
-29
@@ -1,34 +1,47 @@
|
||||
#pragma once
|
||||
// qt-error.h : internal helpers backing the (future) public qt_last_error
|
||||
// entry and the qt_log callback routing.
|
||||
// qt-error.h : internal helpers backing the public qwen_last_error
|
||||
// entry and the qwen_log callback routing.
|
||||
//
|
||||
// Storage is thread_local so concurrent synthesize calls on different
|
||||
// threads never race on each other's messages. The setter is variadic
|
||||
// with printf semantics ; messages longer than the internal buffer are
|
||||
// truncated, never split. Passing NULL as fmt clears the slot.
|
||||
// Not part of the public ABI. Translation units that emit user-facing
|
||||
// errors include this header to record a diagnostic on the calling
|
||||
// thread before they return a negative qwen_status (or false). The
|
||||
// actual storage and the public qwen_last_error() reader live in
|
||||
// qwen.cpp.
|
||||
//
|
||||
// qt_throw is the load-path counterpart : functions deep inside the GGUF
|
||||
// reader and the codec load chain cannot return false up dozens of call
|
||||
// sites without a massive cascade. They throw a std::runtime_error
|
||||
// instead, which the binary entry point (main, or a future ABI boundary)
|
||||
// catches and converts to qt_set_error plus a non-zero exit. Exceptions
|
||||
// never cross any future C ABI.
|
||||
// Storage is thread_local so concurrent qwen_synthesize calls on
|
||||
// different threads never race on each other's messages. The setter is
|
||||
// variadic with printf semantics ; messages longer than the internal
|
||||
// buffer are truncated, never split. Passing NULL as fmt clears the
|
||||
// slot.
|
||||
//
|
||||
// qt_log routes a formatted message to the user-installed qt_log_cb, or
|
||||
// to stderr when no callback is installed. Used by every translation
|
||||
// qt_throw is the load-path counterpart : functions deep inside the
|
||||
// GGUF reader and the codec load chain cannot return false up dozens
|
||||
// of call sites without a massive cascade. They throw a
|
||||
// std::runtime_error instead, which the ABI boundary entries
|
||||
// (qwen_init, qwen_synthesize) catch and convert into qt_set_error
|
||||
// plus a negative qwen_status. Exceptions never cross any future C ABI.
|
||||
//
|
||||
// qt_log routes a formatted message to the user-installed qwen_log_cb,
|
||||
// or to stderr when no callback is installed. Used by every translation
|
||||
// unit in the lib that wants its diagnostics to be redirectable from a
|
||||
// wrapper (Python logging, Rust tracing, ...).
|
||||
// wrapper (Python logging, Rust tracing, ...). The level enum is the
|
||||
// public qwen_log_level, re-exported here under the historic qt_log_level
|
||||
// name so existing call sites stay pixel perfect.
|
||||
|
||||
#include "qwen.h"
|
||||
|
||||
#include <cstdarg>
|
||||
|
||||
enum qt_log_level {
|
||||
QT_LOG_DEBUG = 0,
|
||||
QT_LOG_INFO = 1,
|
||||
QT_LOG_WARN = 2,
|
||||
QT_LOG_ERROR = 3,
|
||||
};
|
||||
// Internal log level alias. Same values, same layout as the public
|
||||
// qwen_log_level enum : a single underlying type means a single log
|
||||
// callback installed through qwen_log_set routes every diagnostic
|
||||
// without any cast or translation.
|
||||
typedef enum qwen_log_level qt_log_level;
|
||||
|
||||
typedef void (*qt_log_cb)(enum qt_log_level level, const char * msg, void * user_data);
|
||||
#define QT_LOG_DEBUG QWEN_LOG_DEBUG
|
||||
#define QT_LOG_INFO QWEN_LOG_INFO
|
||||
#define QT_LOG_WARN QWEN_LOG_WARN
|
||||
#define QT_LOG_ERROR QWEN_LOG_ERROR
|
||||
|
||||
void qt_set_error(const char * fmt, ...)
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
@@ -50,18 +63,14 @@ void qt_set_error_v(const char * fmt, va_list ap);
|
||||
;
|
||||
|
||||
// Routes a formatted message at the requested level to the installed
|
||||
// callback, or to stderr when none is set. The message is the full line
|
||||
// without trailing newline ; routing layers add their own framing.
|
||||
void qt_log(enum qt_log_level level, const char * fmt, ...)
|
||||
// callback, or to stderr when none is set. The message is the full
|
||||
// line without trailing newline ; routing layers add their own framing.
|
||||
void qt_log(qt_log_level level, const char * fmt, ...)
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__((format(printf, 2, 3)))
|
||||
#endif
|
||||
;
|
||||
|
||||
// Install a process-wide log callback. Pass NULL to revert to stderr.
|
||||
// user_data is opaque, forwarded as-is to every callback invocation.
|
||||
void qt_log_set(qt_log_cb cb, void * user_data);
|
||||
|
||||
// Returns the most recent error message recorded on the calling thread.
|
||||
// Returns "" if no error has been set on this thread. The pointer stays
|
||||
// valid until the next qt_set_error call on the same thread.
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
// qwen.cpp: public ABI implementation.
|
||||
//
|
||||
// Every entry declared in qwen.h lives here under one extern "C" block
|
||||
// so the symbols carry C linkage and are linkable from C, Rust, Go,
|
||||
// Python ctypes and any other binding generator. The struct
|
||||
// qwen_context opaque handle owns one BackendPair, one PipelineTTS
|
||||
// (which embeds its PipelineCodec) and one BPETokenizer. qwen_init
|
||||
// walks the load chain in dependency order and unwinds whatever it
|
||||
// already allocated when any step fails. qwen_free mirrors that order
|
||||
// in reverse.
|
||||
//
|
||||
// This translation unit also absorbs the internal qt_set_error /
|
||||
// qt_throw / qt_log helpers that the rest of the codebase calls. The
|
||||
// internal qt_log_level enum is a typedef of the public qwen_log_level
|
||||
// (same values, same layout) so a single log callback installed via
|
||||
// qwen_log_set routes every diagnostic, internal or public.
|
||||
|
||||
#include "qwen.h"
|
||||
|
||||
#include "backend.h"
|
||||
#include "bpe.h"
|
||||
#include "pipeline-tts.h"
|
||||
#include "qt-error.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
// Internal definition of the opaque handle. C++ types are fine here
|
||||
// because nothing in this struct ever crosses the public ABI boundary :
|
||||
// callers only ever see `struct qwen_context *`. PipelineTTS already
|
||||
// embeds the PipelineCodec, so no separate codec field is needed.
|
||||
struct qwen_context {
|
||||
BackendPair bp;
|
||||
PipelineTTS pt;
|
||||
BPETokenizer tok;
|
||||
};
|
||||
|
||||
// Thread-local backing store for qt_last_error(). std::string sized once
|
||||
// per thread, grows on demand, never freed across calls : the std runtime
|
||||
// reclaims it on thread exit. An empty string means "no error recorded
|
||||
// on this thread yet", which qt_last_error() exposes as "".
|
||||
static thread_local std::string g_last_error;
|
||||
|
||||
void qt_set_error_v(const char * fmt, va_list ap) {
|
||||
if (!fmt) {
|
||||
g_last_error.clear();
|
||||
return;
|
||||
}
|
||||
// Two-pass vsnprintf : first call sizes the buffer, second writes the
|
||||
// message. va_copy keeps the original ap valid for the second pass.
|
||||
va_list ap2;
|
||||
va_copy(ap2, ap);
|
||||
int needed = std::vsnprintf(nullptr, 0, fmt, ap2);
|
||||
va_end(ap2);
|
||||
if (needed < 0) {
|
||||
g_last_error = "qt_set_error : vsnprintf failed";
|
||||
return;
|
||||
}
|
||||
g_last_error.resize(static_cast<size_t>(needed));
|
||||
std::vsnprintf(g_last_error.data(), static_cast<size_t>(needed) + 1, fmt, ap);
|
||||
}
|
||||
|
||||
void qt_set_error(const char * fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
qt_set_error_v(fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
const char * qt_last_error(void) {
|
||||
return g_last_error.c_str();
|
||||
}
|
||||
|
||||
// Formats a message with printf semantics and throws std::runtime_error.
|
||||
// The catch site at the binary entry inspects the what() string and feeds
|
||||
// it into qt_set_error so the user-visible diagnostic is identical
|
||||
// whether the failure used the bool-return path or the throw path.
|
||||
void qt_throw(const char * fmt, ...) {
|
||||
char buf[1024];
|
||||
if (fmt) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
std::vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
throw std::runtime_error(buf);
|
||||
}
|
||||
|
||||
// Process-wide log callback. Atomic so qwen_log_set can replace it without
|
||||
// locking : write happens with memory_order_release, every reader sees a
|
||||
// fully published callback pointer paired with its user_data slot.
|
||||
// std::atomic on a function pointer is lock-free on every platform we
|
||||
// target. user_data is a plain pointer because it is only ever published
|
||||
// alongside cb under the same release ordering.
|
||||
static std::atomic<qwen_log_cb> g_log_cb{ nullptr };
|
||||
static void * g_log_cb_user = nullptr;
|
||||
|
||||
// Routes one log line to the installed callback or to stderr. Two-pass
|
||||
// vsnprintf sizes the heap buffer when the message exceeds the stack
|
||||
// scratchpad, which keeps the common case allocation-free.
|
||||
void qt_log(qt_log_level level, const char * fmt, ...) {
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
char stackbuf[512];
|
||||
char * buf = stackbuf;
|
||||
int needed = 0;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
{
|
||||
va_list ap2;
|
||||
va_copy(ap2, ap);
|
||||
needed = std::vsnprintf(stackbuf, sizeof(stackbuf), fmt, ap2);
|
||||
va_end(ap2);
|
||||
}
|
||||
if (needed < 0) {
|
||||
va_end(ap);
|
||||
return;
|
||||
}
|
||||
std::string heapbuf;
|
||||
if ((size_t) needed >= sizeof(stackbuf)) {
|
||||
heapbuf.resize((size_t) needed);
|
||||
std::vsnprintf(heapbuf.data(), (size_t) needed + 1, fmt, ap);
|
||||
buf = heapbuf.data();
|
||||
}
|
||||
va_end(ap);
|
||||
|
||||
qwen_log_cb cb = g_log_cb.load(std::memory_order_acquire);
|
||||
if (cb) {
|
||||
cb(level, buf, g_log_cb_user);
|
||||
} else {
|
||||
std::fprintf(stderr, "%s\n", buf);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
const char * qwen_version(void) {
|
||||
// QWEN_VERSION is a string literal injected by tools/version.cmake
|
||||
// ("<git-hash> (<date>)"), so its storage already has process
|
||||
// lifetime and no formatting wrapper is needed.
|
||||
return QWEN_VERSION;
|
||||
}
|
||||
|
||||
const char * qwen_last_error(void) {
|
||||
// c_str() on an empty std::string is guaranteed to point to a NUL
|
||||
// byte by C++11, so callers never have to NULL-check the result.
|
||||
return g_last_error.c_str();
|
||||
}
|
||||
|
||||
void qwen_audio_free(struct qwen_audio * a) {
|
||||
if (!a) {
|
||||
return;
|
||||
}
|
||||
if (a->samples) {
|
||||
std::free(a->samples);
|
||||
}
|
||||
a->samples = nullptr;
|
||||
a->n_samples = 0;
|
||||
a->sample_rate = 0;
|
||||
a->channels = 0;
|
||||
}
|
||||
|
||||
void qwen_log_set(qwen_log_cb cb, void * user_data) {
|
||||
g_log_cb_user = user_data;
|
||||
g_log_cb.store(cb, std::memory_order_release);
|
||||
}
|
||||
|
||||
void qwen_init_default_params(struct qwen_init_params * p) {
|
||||
p->abi_version = QWEN_ABI_VERSION;
|
||||
p->talker_path = nullptr;
|
||||
p->codec_path = nullptr;
|
||||
}
|
||||
|
||||
void qwen_tts_default_params(struct qwen_tts_params * p) {
|
||||
p->abi_version = QWEN_ABI_VERSION;
|
||||
p->text = nullptr;
|
||||
p->lang = "english";
|
||||
p->instruct = nullptr;
|
||||
p->speaker = nullptr;
|
||||
p->ref_audio_24k = nullptr;
|
||||
p->ref_n_samples = 0;
|
||||
p->ref_text = nullptr;
|
||||
p->seed = -1;
|
||||
p->max_new_tokens = 2048;
|
||||
p->do_sample = true;
|
||||
p->temperature = 0.9f;
|
||||
p->top_k = 50;
|
||||
p->top_p = 1.0f;
|
||||
p->repetition_penalty = 1.05f;
|
||||
p->subtalker_do_sample = true;
|
||||
p->subtalker_temperature = 0.9f;
|
||||
p->subtalker_top_k = 50;
|
||||
p->subtalker_top_p = 1.0f;
|
||||
p->dump_dir = nullptr;
|
||||
}
|
||||
|
||||
struct qwen_context * qwen_init(const struct qwen_init_params * params) {
|
||||
if (!params || !params->talker_path || !params->codec_path) {
|
||||
qt_set_error("qwen_init : params, talker_path or codec_path is NULL");
|
||||
qt_log(QT_LOG_ERROR, "[qwen] qwen_init requires talker_path and codec_path");
|
||||
return nullptr;
|
||||
}
|
||||
if (params->abi_version > QWEN_ABI_VERSION) {
|
||||
qt_set_error(
|
||||
"qwen_init : params->abi_version %d > QWEN_ABI_VERSION %d (binding compiled against a newer header)",
|
||||
params->abi_version, QWEN_ABI_VERSION);
|
||||
qt_log(QT_LOG_ERROR, "[qwen] qwen_init params struct is from a newer ABI (%d > %d)", params->abi_version,
|
||||
QWEN_ABI_VERSION);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
qt_log(QT_LOG_INFO, "[qwen] qwentts.cpp %s", qwen_version());
|
||||
|
||||
// new qwen_context() value-initialises every field: POD aggregates
|
||||
// (BackendPair, PipelineTTS) are zero-init, std containers in
|
||||
// BPETokenizer construct empty.
|
||||
qwen_context * q = new qwen_context();
|
||||
|
||||
// The load chain runs inside a try block. Any failure deep in the
|
||||
// GGUF reader, the codec load or the LM weight load throws via
|
||||
// qt_throw ; the catch funnels every variant into one cleanup via
|
||||
// qwen_free, which is idempotent on partial state (NULL-safe sched,
|
||||
// NULL GGUF handles, refcount-correct backend release).
|
||||
try {
|
||||
q->bp = backend_init("Talker");
|
||||
if (!q->bp.backend) {
|
||||
qt_throw("qwen_init : backend_init failed (no GGML backend available)");
|
||||
}
|
||||
|
||||
if (!pipeline_tts_load(&q->pt, params->talker_path, params->codec_path, q->bp)) {
|
||||
qt_throw("qwen_init : pipeline_tts_load failed for '%s' / '%s'", params->talker_path, params->codec_path);
|
||||
}
|
||||
|
||||
// BPE tokenizer payload lives inside the talker GGUF. Load the
|
||||
// base vocab + the qwen3-tts text specials in one shot. The
|
||||
// specials key list mirrors what the standalone CLI used to
|
||||
// do before the facade hoisted the load chain.
|
||||
if (!load_bpe_from_gguf(&q->tok, params->talker_path)) {
|
||||
qt_throw("qwen_init : load_bpe_from_gguf failed for '%s'", params->talker_path);
|
||||
}
|
||||
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(&q->tok, params->talker_path, specials_keys, 5);
|
||||
} catch (const std::exception & e) {
|
||||
qt_set_error("%s", e.what());
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", e.what());
|
||||
qwen_free(q);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
void qwen_free(struct qwen_context * q) {
|
||||
if (!q) {
|
||||
return;
|
||||
}
|
||||
pipeline_tts_free(&q->pt);
|
||||
backend_release(q->bp.backend, q->bp.cpu_backend);
|
||||
delete q;
|
||||
}
|
||||
|
||||
// Resolve a -1 seed to a hardware random 64-bit value. Anything else is
|
||||
// forwarded verbatim, so reproducibility is one explicit seed away.
|
||||
static int64_t qwen_resolve_seed(int64_t seed) {
|
||||
if (seed >= 0) {
|
||||
return seed;
|
||||
}
|
||||
std::random_device rd;
|
||||
return (int64_t) (((uint64_t) rd() << 32) ^ (uint64_t) rd());
|
||||
}
|
||||
|
||||
enum qwen_status qwen_synthesize(struct qwen_context * q,
|
||||
const struct qwen_tts_params * params,
|
||||
struct qwen_audio * out) {
|
||||
if (!q || !params || !out) {
|
||||
qt_set_error("qwen_synthesize : q, params or out is NULL");
|
||||
if (out) {
|
||||
qwen_audio_free(out);
|
||||
}
|
||||
return QWEN_STATUS_INVALID_PARAMS;
|
||||
}
|
||||
if (params->abi_version > QWEN_ABI_VERSION) {
|
||||
qt_set_error(
|
||||
"qwen_synthesize : params->abi_version %d > QWEN_ABI_VERSION %d (binding compiled against a newer header)",
|
||||
params->abi_version, QWEN_ABI_VERSION);
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_INVALID_PARAMS;
|
||||
}
|
||||
|
||||
// Mode validation. Mirrors the upstream Python which raises
|
||||
// ValueError when generate_voice_design is called on a non
|
||||
// voice_design model and the same shape applies to
|
||||
// generate_custom_voice. Explicit and KISS, so the caller never
|
||||
// gets a silently wrong synthesis. Messages preserved verbatim
|
||||
// from the previous CLI-side checks.
|
||||
const std::string & mt = q->pt.model_type;
|
||||
if (params->speaker && mt != "custom_voice") {
|
||||
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);
|
||||
return QWEN_STATUS_MODE_INVALID;
|
||||
}
|
||||
if (params->instruct && mt == "base") {
|
||||
qt_set_error("--instruct is not supported for base models");
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_MODE_INVALID;
|
||||
}
|
||||
if (mt == "custom_voice" && !params->speaker) {
|
||||
qt_set_error("custom_voice models require --speaker");
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_MODE_INVALID;
|
||||
}
|
||||
if (mt == "voice_design" && (!params->instruct || params->instruct[0] == '\0')) {
|
||||
qt_set_error("voice_design models require --instruct");
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_MODE_INVALID;
|
||||
}
|
||||
if (params->ref_audio_24k && mt != "base") {
|
||||
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);
|
||||
return QWEN_STATUS_MODE_INVALID;
|
||||
}
|
||||
if (params->speaker && params->ref_audio_24k) {
|
||||
qt_set_error("--speaker and --ref-audio are mutually exclusive");
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_INVALID_PARAMS;
|
||||
}
|
||||
if (params->ref_text && !params->ref_audio_24k) {
|
||||
qt_set_error("--ref-text requires --ref-audio");
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", qt_last_error());
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_INVALID_PARAMS;
|
||||
}
|
||||
|
||||
// Translate the public POD params into the internal C++ struct
|
||||
// expected by pipeline_tts_synthesize. Borrowed pointers are
|
||||
// forwarded verbatim ; the lifetime contract on the public side
|
||||
// (caller keeps strings alive for the duration of the call)
|
||||
// matches what the pipeline already requires.
|
||||
PipelineTTSSynthesizeParams p = {};
|
||||
p.text = params->text;
|
||||
p.lang = params->lang;
|
||||
p.instruct = params->instruct;
|
||||
p.speaker = params->speaker;
|
||||
p.ref_audio_24k = params->ref_audio_24k;
|
||||
p.ref_n_samples = params->ref_n_samples;
|
||||
p.ref_text = params->ref_text;
|
||||
p.seed = qwen_resolve_seed(params->seed);
|
||||
p.max_new_tokens = params->max_new_tokens;
|
||||
p.do_sample = params->do_sample;
|
||||
p.temperature = params->temperature;
|
||||
p.top_k = params->top_k;
|
||||
p.top_p = params->top_p;
|
||||
p.repetition_penalty = params->repetition_penalty;
|
||||
p.subtalker_do_sample = params->subtalker_do_sample;
|
||||
p.subtalker_temperature = params->subtalker_temperature;
|
||||
p.subtalker_top_k = params->subtalker_top_k;
|
||||
p.subtalker_top_p = params->subtalker_top_p;
|
||||
p.dump_dir = params->dump_dir;
|
||||
|
||||
// Defense in depth: the synthesis path normally reports failures
|
||||
// via bool return + qt_set_error. A future load-style throw or any
|
||||
// std::bad_alloc deep inside the GGML backend is caught here and
|
||||
// converted to QWEN_STATUS_GENERATE_FAILED so an exception never
|
||||
// crosses the extern "C" boundary.
|
||||
try {
|
||||
PipelineTTSSynthesizeOutput pout;
|
||||
if (!pipeline_tts_synthesize(&q->pt, &q->tok, p, &pout)) {
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_GENERATE_FAILED;
|
||||
}
|
||||
|
||||
// Copy the std::vector<float> into a malloc-backed buffer the
|
||||
// caller can free with std::free via qwen_audio_free. The
|
||||
// vector itself goes out of scope at function exit, releasing
|
||||
// its own storage independently.
|
||||
const size_t n = pout.audio.size();
|
||||
const size_t bytes = n * sizeof(float);
|
||||
float * buf = (float *) std::malloc(bytes > 0 ? bytes : 1);
|
||||
if (!buf) {
|
||||
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);
|
||||
return QWEN_STATUS_OOM;
|
||||
}
|
||||
if (n > 0) {
|
||||
std::memcpy(buf, pout.audio.data(), bytes);
|
||||
}
|
||||
out->samples = buf;
|
||||
out->n_samples = (int) n;
|
||||
out->sample_rate = pout.sample_rate;
|
||||
out->channels = 1;
|
||||
return QWEN_STATUS_OK;
|
||||
} catch (const std::exception & e) {
|
||||
qt_set_error("%s", e.what());
|
||||
qt_log(QT_LOG_ERROR, "[qwen] %s", e.what());
|
||||
qwen_audio_free(out);
|
||||
return QWEN_STATUS_GENERATE_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
#pragma once
|
||||
// qwen.h: public ABI for qwentts.cpp.
|
||||
//
|
||||
// Single-header public API. Pure C99, consumable from C and C++ alike.
|
||||
// Bindings (Python ctypes, Rust bindgen, Go cgo) parse this file directly.
|
||||
// Style follows whisper.h / llama.h / omnivoice.h: extern "C" linkage on
|
||||
// every entry, POD structs only, const char * UTF-8 strings, qwen_status
|
||||
// enum returns.
|
||||
//
|
||||
// The opaque qwen_context handle aggregates every module the synthesis
|
||||
// path needs (Talker LM weights, code predictor MTP head, optional
|
||||
// speaker encoder, 12 Hz audio tokenizer codec, BPE tokenizer, GGML
|
||||
// backend pair). One init, one free, one synthesize call covers the
|
||||
// full TTS path. The lower-level pipeline_tts_* / pipeline_codec_*
|
||||
// entries declared in pipeline-tts.h / pipeline-codec.h stay available
|
||||
// for tooling that needs partial init, but they are intentionally not
|
||||
// part of this public ABI.
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Symbol visibility. Three Windows cases: building the SHARED target
|
||||
// (QWENTTS_BUILD set, dllexport), consuming the SHARED target from
|
||||
// outside (nothing set, dllimport), consuming the STATIC archive
|
||||
// (QWENTTS_STATIC set by the static target's INTERFACE definitions,
|
||||
// empty so the linker resolves the symbol directly without dllimport).
|
||||
// On GCC/Clang the default-visibility attribute is harmless on static
|
||||
// builds and required on shared builds.
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
# if defined(QWENTTS_STATIC)
|
||||
# define QWEN_API
|
||||
# elif defined(QWENTTS_BUILD)
|
||||
# define QWEN_API __declspec(dllexport)
|
||||
# else
|
||||
# define QWEN_API __declspec(dllimport)
|
||||
# endif
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
# define QWEN_API __attribute__((visibility("default")))
|
||||
#else
|
||||
# define QWEN_API
|
||||
#endif
|
||||
|
||||
// Struct ABI version. Incremented every time a public POD struct grows a
|
||||
// new field at the end. Callers fill `.abi_version = QWEN_ABI_VERSION`
|
||||
// (or let qwen_*_default_params set it). Entries that consume those
|
||||
// structs reject inputs whose abi_version exceeds the build-time
|
||||
// constant: this guards a binary built against vN from receiving a
|
||||
// struct laid out for vN+1 by a freshly compiled binding. Adding fields
|
||||
// stays backward compat because the new tail is zero init in older
|
||||
// callers and the lib reads only what its abi_version permits.
|
||||
//
|
||||
// There is no separate semver triple. The runtime build identity is the
|
||||
// git short hash + commit date string returned by qwen_version() ; for
|
||||
// binding compat checks, QWEN_ABI_VERSION is the only number that
|
||||
// matters. Aligned on OV_ABI_VERSION = 2 for the omnivoice ABI cousin.
|
||||
#define QWEN_ABI_VERSION 2
|
||||
|
||||
// Returns a static string of the form "<git-hash> (<date>)" identifying
|
||||
// the exact commit this binary was built from. Safe to call from any
|
||||
// thread, no allocation. Pointer stays valid for the process lifetime.
|
||||
QWEN_API const char * qwen_version(void);
|
||||
|
||||
// Status code returned by every fallible entry. QWEN_STATUS_OK is always
|
||||
// zero so `if (rc)` reads as `if (rc != QWEN_STATUS_OK)`.
|
||||
enum qwen_status {
|
||||
QWEN_STATUS_OK = 0,
|
||||
QWEN_STATUS_INVALID_PARAMS = -1,
|
||||
QWEN_STATUS_MODE_INVALID = -2,
|
||||
QWEN_STATUS_GENERATE_FAILED = -3,
|
||||
QWEN_STATUS_OOM = -4,
|
||||
};
|
||||
|
||||
// Returns the last error message produced on the calling thread by any
|
||||
// qwen_* entry, as a NUL terminated UTF-8 string. errno-style semantics:
|
||||
// the pointer is only meaningful right after a failure (qwen_init
|
||||
// returning NULL, or any qwen_* entry returning a negative qwen_status) ;
|
||||
// calling it after a successful entry yields the previous message or an
|
||||
// empty string. Storage is thread local so two threads running
|
||||
// qwen_synthesize concurrently never race on each other's diagnostics.
|
||||
// The pointer stays valid until the next failing qwen_* entry on the
|
||||
// same thread.
|
||||
QWEN_API const char * qwen_last_error(void);
|
||||
|
||||
// Output audio buffer. Plain POD: the samples pointer is malloc
|
||||
// allocated by qwen_synthesize, owned by the struct, released by
|
||||
// qwen_audio_free. Do not free samples directly nor reassign without
|
||||
// freeing first. Zero initialise before the first use:
|
||||
// `struct qwen_audio a = {0};`.
|
||||
struct qwen_audio {
|
||||
float * samples; // mono PCM, malloc allocated
|
||||
int n_samples; // length in samples
|
||||
int sample_rate; // 24000 for the 12 Hz Qwen3-TTS tokenizer
|
||||
int channels; // 1 (mono)
|
||||
};
|
||||
|
||||
// Release the samples buffer and reset the struct to empty. Safe on a
|
||||
// zero initialised struct (no double free, no NULL deref).
|
||||
QWEN_API void qwen_audio_free(struct qwen_audio * a);
|
||||
|
||||
// Opaque handle. Definition lives in qwen.cpp. Use qwen_init / qwen_free.
|
||||
struct qwen_context;
|
||||
|
||||
// Initialisation parameters. Both GGUF paths are required: the talker
|
||||
// GGUF holds the LM weights, the code predictor MTP head and (for
|
||||
// custom_voice / voice_design checkpoints) the speaker encoder ; the
|
||||
// codec GGUF holds the 12 Hz audio tokenizer. abi_version stays first
|
||||
// so a future struct growth keeps reading the version field at offset
|
||||
// 0. No use_fa / clamp_fp16 yet : the current pipeline_tts_load picks
|
||||
// flash attention from backend capability without a user knob.
|
||||
struct qwen_init_params {
|
||||
int abi_version;
|
||||
const char * talker_path;
|
||||
const char * codec_path;
|
||||
};
|
||||
|
||||
// Initialise to the standard defaults: both paths NULL (caller must set
|
||||
// them before calling qwen_init), abi_version set to QWEN_ABI_VERSION.
|
||||
QWEN_API void qwen_init_default_params(struct qwen_init_params * p);
|
||||
|
||||
// Allocate every module described by params. Returns NULL on any
|
||||
// failure after releasing whatever it has allocated so far. The
|
||||
// returned handle owns its GGML backend pair and must be released with
|
||||
// qwen_free.
|
||||
QWEN_API struct qwen_context * qwen_init(const struct qwen_init_params * params);
|
||||
|
||||
// Release every module owned by the handle and free the handle itself.
|
||||
// Safe on NULL.
|
||||
QWEN_API void qwen_free(struct qwen_context * q);
|
||||
|
||||
// Log severity. Numerically ordered so a callback can filter with a
|
||||
// simple `if (level < threshold) return;`. ERROR is reserved for
|
||||
// failure reports that the lib also surfaces via qwen_status /
|
||||
// qwen_last_error ; WARN for recoverable surprises ; INFO for the
|
||||
// normal load and synthesis cadence ; DEBUG for tensor-level cossim
|
||||
// diagnostics.
|
||||
enum qwen_log_level {
|
||||
QWEN_LOG_DEBUG = 0,
|
||||
QWEN_LOG_INFO = 1,
|
||||
QWEN_LOG_WARN = 2,
|
||||
QWEN_LOG_ERROR = 3,
|
||||
};
|
||||
|
||||
// Logging callback. msg is a NUL terminated UTF-8 string already
|
||||
// formatted by the lib, with no trailing newline (the callback is free
|
||||
// to add one). user_data is forwarded verbatim from qwen_log_set.
|
||||
// Called from any thread the lib runs on: the callback must be
|
||||
// reentrant.
|
||||
typedef void (*qwen_log_cb)(enum qwen_log_level level, const char * msg, void * user_data);
|
||||
|
||||
// Install a global log callback. Passing cb == NULL restores the
|
||||
// default behaviour (write to stderr). Safe to call at any point ;
|
||||
// takes effect immediately on subsequent log emissions across every
|
||||
// thread. Storage is process wide, not per handle, matching
|
||||
// whisper_log_set / llama_log_set / ov_log_set.
|
||||
QWEN_API void qwen_log_set(qwen_log_cb cb, void * user_data);
|
||||
|
||||
// Synthesis parameters. Strings are NULL terminated UTF-8 ; NULL maps
|
||||
// to empty where the underlying pipeline accepts it. The selection
|
||||
// between base / custom_voice / voice_design synthesis mode is driven
|
||||
// by the model_type read from the talker GGUF at qwen_init time, not
|
||||
// by an explicit flag here ; the seven mode rules are enforced inside
|
||||
// qwen_synthesize and surface as QWEN_STATUS_MODE_INVALID with a
|
||||
// descriptive qwen_last_error(). abi_version stays first so the lib
|
||||
// can route on it before reading any field that may have shifted in a
|
||||
// future minor.
|
||||
struct qwen_tts_params {
|
||||
int abi_version;
|
||||
|
||||
// Input text and language hint. lang accepts the upstream
|
||||
// qwen3-tts language names ("english", "chinese", "auto", ...).
|
||||
// instruct is the style instruction string ; required for
|
||||
// voice_design, optional for custom_voice, rejected for base.
|
||||
// speaker is the named speaker for custom_voice models, rejected
|
||||
// for the other two modes.
|
||||
const char * text;
|
||||
const char * lang;
|
||||
const char * instruct;
|
||||
const char * speaker;
|
||||
|
||||
// Optional voice reference for base mode voice cloning. Mode A
|
||||
// (x_vector_only) sets ref_audio_24k only ; mode B (ICL) sets
|
||||
// both ref_audio_24k and ref_text. ref_audio_24k is a mono float
|
||||
// PCM buffer sampled at the codec sample rate (24 kHz). Mutually
|
||||
// exclusive with speaker. Rejected for custom_voice / voice_design.
|
||||
const float * ref_audio_24k;
|
||||
int ref_n_samples;
|
||||
const char * ref_text;
|
||||
|
||||
// Sampling configuration. seed == -1 is resolved by qwen_synthesize
|
||||
// to a hardware random seed via std::random_device, anything else
|
||||
// is forwarded verbatim for deterministic replay across runs.
|
||||
// Defaults match the upstream Python reference: do_sample true,
|
||||
// temperature 0.9, top_k 50, top_p 1.0, repetition_penalty 1.05,
|
||||
// subtalker mirrors talker, max_new_tokens 2048.
|
||||
int64_t seed;
|
||||
int max_new_tokens;
|
||||
bool do_sample;
|
||||
float temperature;
|
||||
int top_k;
|
||||
float top_p;
|
||||
float repetition_penalty;
|
||||
bool subtalker_do_sample;
|
||||
float subtalker_temperature;
|
||||
int subtalker_top_k;
|
||||
float subtalker_top_p;
|
||||
|
||||
// Intermediate tensor dump directory. NULL disables dumps. Debug
|
||||
// only, slows the run.
|
||||
const char * dump_dir;
|
||||
};
|
||||
|
||||
// Initialise to the standard defaults. Strings NULL, seed -1,
|
||||
// max_new_tokens 2048, do_sample true, temperature 0.9, top_k 50,
|
||||
// top_p 1.0, repetition_penalty 1.05, subtalker mirrors talker,
|
||||
// dump_dir NULL.
|
||||
QWEN_API void qwen_tts_default_params(struct qwen_tts_params * p);
|
||||
|
||||
// Run the full TTS synthesis. Validates the params against the loaded
|
||||
// model_type (the seven base / custom_voice / voice_design rules),
|
||||
// resolves the seed, hands off to pipeline_tts_synthesize and fills
|
||||
// `out` with mono float PCM at the codec sample rate. Returns
|
||||
// QWEN_STATUS_OK on success ; on any failure returns a negative
|
||||
// qwen_status describing the cause and leaves `out` empty.
|
||||
QWEN_API enum qwen_status qwen_synthesize(struct qwen_context * q,
|
||||
const struct qwen_tts_params * params,
|
||||
struct qwen_audio * out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user