server: add OpenAI compatible TTS server (chunked PCM streaming, WAV one-shot)

This commit is contained in:
Pascal
2026-06-06 12:47:10 +02:00
parent eda8b59092
commit f3cfa5cf47
16 changed files with 40571 additions and 2 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ static void print_usage(const char * prog) {
" --sub-top-p <f> Sub-talker top-p (default: 1.0)\n\n"
"Debug:\n"
" --no-fa Disable flash attention\n"
" --clamp-fp16 Clamp hidden states + V to FP16 range\n"
" --clamp-fp16 Clamp hidden states to FP16 range\n"
" --dump <dir> Dump intermediate tensors (f32) to <dir>\n",
prog);
}
+131
View File
@@ -0,0 +1,131 @@
// tts-server.cpp: OpenAI-compatible HTTP server backed by the qwentts
// ABI. Loads a talker + codec once, GPU resident, and serves synthesis over
// POST /v1/audio/speech. The shared core lives in src/tts-server.h ; this
// file only wires the qt_* ABI into the generic adapter.
#include "qwen.h"
#include "tts-server.h"
#include "version.h"
#include <cstdio>
#include <cstring>
#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]\n\n"
"Required:\n"
" --model <gguf> Talker LM GGUF (qwen-talker-*.gguf)\n"
" --codec <gguf> Codec GGUF (qwen-tokenizer-*.gguf)\n\n"
"Optional:\n"
" --host <ip> Listen address (default: 127.0.0.1)\n"
" --port <n> Listen port (default: 8080)\n"
" --lang <name> Language label (default: english)\n"
" --no-fa Disable flash attention\n"
" --clamp-fp16 Clamp hidden states to FP16 range\n",
prog);
}
// Trim a path down to its file name for the reported model id.
static std::string basename_of(const char * path) {
std::string s = path;
size_t p = s.find_last_of("/\\");
return p == std::string::npos ? s : s.substr(p + 1);
}
int main(int argc, char ** argv) {
const char * talker_path = NULL;
const char * codec_path = NULL;
std::string lang = "english";
server_config cfg;
bool use_fa = true;
bool clamp_fp16 = false;
for (int i = 1; i < argc; i++) {
const char * arg = argv[i];
if (!std::strcmp(arg, "--model") && i + 1 < argc) {
talker_path = argv[++i];
} else if (!std::strcmp(arg, "--codec") && i + 1 < argc) {
codec_path = argv[++i];
} else if (!std::strcmp(arg, "--host") && i + 1 < argc) {
cfg.host = argv[++i];
} else if (!std::strcmp(arg, "--port") && i + 1 < argc) {
cfg.port = std::atoi(argv[++i]);
} else if (!std::strcmp(arg, "--lang") && i + 1 < argc) {
lang = argv[++i];
} else if (!std::strcmp(arg, "--no-fa")) {
use_fa = false;
} else if (!std::strcmp(arg, "--clamp-fp16")) {
clamp_fp16 = true;
} else if (!std::strcmp(arg, "--help") || !std::strcmp(arg, "-h")) {
print_usage(argv[0]);
return 0;
} else {
fprintf(stderr, "[CLI] ERROR: unknown arg: %s\n", arg);
print_usage(argv[0]);
return 1;
}
}
if (!talker_path || !codec_path) {
print_usage(argv[0]);
return 0;
}
struct qt_init_params iparams;
qt_init_default_params(&iparams);
iparams.talker_path = talker_path;
iparams.codec_path = codec_path;
iparams.use_fa = use_fa;
iparams.clamp_fp16 = clamp_fp16;
struct qt_context * q = qt_init(&iparams);
if (!q) {
fprintf(stderr, "[Server] FATAL: %s\n", qt_last_error());
return 1;
}
tts_backend be;
be.model_id = basename_of(talker_path);
int n = qt_n_speakers(q);
for (int i = 0; i < n; i++) {
be.voices.push_back(qt_speaker_name(q, i));
}
// The adapter always drives the streaming pipeline : on_chunk routes to
// the shared sink, which either streams to the socket (pcm) or fills a
// one-shot buffer (wav). Either way the audio path is identical.
be.synthesize = [q, &lang](const tts_request & req, const tts_sink & sink, std::string & err) -> int {
struct qt_tts_params p;
qt_tts_default_params(&p);
p.text = req.input.c_str();
p.lang = lang.c_str();
if (!req.voice.empty() && qt_n_speakers(q) > 0) {
p.speaker = req.voice.c_str();
}
if (!req.instructions.empty()) {
p.instruct = req.instructions.c_str();
}
// Trampoline : the C ABI on_chunk forwards to the C++ sink.
const tts_sink * sink_ptr = &sink;
p.on_chunk = [](const float * s, int ns, void * u) -> bool {
return (*static_cast<const tts_sink *>(u))(s, ns);
};
p.on_chunk_user_data = (void *) sink_ptr;
struct qt_audio out = {};
enum qt_status rc = qt_synthesize(q, &p, &out);
qt_audio_free(&out);
if (rc != QT_STATUS_OK) {
err = qt_last_error();
return (int) rc;
}
return 0;
};
int rc = tts_server_run(be, cfg);
qt_free(q);
return rc;
}