server: compatible with OpenAI API, add model alias

This commit is contained in:
karl
2026-07-17 21:13:57 +02:00
committed by Pascal
parent 19bbf94dcc
commit 8d556c9806
5 changed files with 25 additions and 18 deletions
+3 -2
View File
@@ -132,9 +132,10 @@ them by name :
``` ```
./build/tts-server \ ./build/tts-server \
--model models/qwen-talker-1.7b-base-Q8_0.gguf \ --model models/qwen-talker-1.7b-base-Q8_0.gguf \
--codec models/qwen-tokenizer-12hz-Q8_0.gguf --port 8080 --codec models/qwen-tokenizer-12hz-Q8_0.gguf \
--alias qwen3-tts-base --port 8080
curl -X POST localhost:8080/v1/voices -H "Content-Type: application/json" \ curl -X POST localhost:8080/v1/audio/voices -H "Content-Type: application/json" \
-d "{\"name\":\"freeman\",\"ref_text\":\"$(cat ref.txt)\", -d "{\"name\":\"freeman\",\"ref_text\":\"$(cat ref.txt)\",
\"spk_b64\":\"$(base64 -w0 ref.spk)\",\"rvq_b64\":\"$(base64 -w0 ref.rvq)\"}" \"spk_b64\":\"$(base64 -w0 ref.spk)\",\"rvq_b64\":\"$(base64 -w0 ref.rvq)\"}"
+5 -4
View File
@@ -617,6 +617,7 @@ Required:
--codec <gguf> Codec GGUF (qwen-tokenizer-*.gguf) --codec <gguf> Codec GGUF (qwen-tokenizer-*.gguf)
Optional: Optional:
--alias <name> Report this model id instead of the GGUF file name
--host <ip> Listen address (default: 127.0.0.1) --host <ip> Listen address (default: 127.0.0.1)
--port <n> Listen port (default: 8080) --port <n> Listen port (default: 8080)
--lang <n> Language label (default: auto) --lang <n> Language label (default: auto)
@@ -636,14 +637,14 @@ POST /v1/audio/speech OAI text-to-speech; response_format "pcm"
fields keep the engine defaults, temperature fields keep the engine defaults, temperature
0 selects greedy decoding, the subtalker 0 selects greedy decoding, the subtalker
mirrors the talker knobs mirrors the talker knobs
GET /v1/models single loaded model GET /v1/models single loaded model, using --alias when set
GET /v1/voices model speakers plus registered cloned voices GET /v1/audio/voices model speakers plus registered cloned voices
POST /v1/voices register a cloned voice: {name, ref_text, POST /v1/audio/voices register a cloned voice: {name, ref_text,
wav_b64} extracts server side through wav_b64} extracts server side through
qt_extract_voice_ref, {name, ref_text, qt_extract_voice_ref, {name, ref_text,
spk_b64, rvq_b64} takes the pre-extracted spk_b64, rvq_b64} takes the pre-extracted
latents verbatim latents verbatim
DELETE /v1/voices/{name} drop a registered voice DELETE /v1/audio/voices/{name} drop a registered voice
GET /health liveness probe GET /health liveness probe
``` ```
+1
View File
@@ -4,4 +4,5 @@
./build/tts-server \ ./build/tts-server \
--model models/qwen-talker-1.7b-base-Q8_0.gguf \ --model models/qwen-talker-1.7b-base-Q8_0.gguf \
--codec models/qwen-tokenizer-12hz-Q8_0.gguf \ --codec models/qwen-tokenizer-12hz-Q8_0.gguf \
--alias qwen3-tts-base \
--host 127.0.0.1 --port 8080 --lang auto --host 127.0.0.1 --port 8080 --lang auto
+10 -10
View File
@@ -10,12 +10,12 @@
// Endpoints: // Endpoints:
// POST /v1/audio/speech OAI text-to-speech // POST /v1/audio/speech OAI text-to-speech
// GET /v1/models single loaded model // GET /v1/models single loaded model
// GET /v1/voices model speakers plus registered cloned voices // GET /v1/audio/voices model speakers plus registered cloned voices
// POST /v1/voices register a cloned voice: {name, ref_text, // POST /v1/audio/voices register a cloned voice: {name, ref_text,
// wav_b64} extracts server side, {name, // wav_b64} extracts server side, {name,
// ref_text, spk_b64, rvq_b64} takes // ref_text, spk_b64, rvq_b64} takes
// pre-extracted latents verbatim // pre-extracted latents verbatim
// DELETE /v1/voices/{name} drop a registered voice // DELETE /v1/audio/voices/{name} drop a registered voice
// GET /health liveness probe // GET /health liveness probe
// //
// Audio out: response_format "pcm" streams s16le 24 kHz mono chunked as it // Audio out: response_format "pcm" streams s16le 24 kHz mono chunked as it
@@ -54,7 +54,7 @@ struct tts_request {
float repetition_penalty; // strictly positive float repetition_penalty; // strictly positive
}; };
// One voice registration parsed from the POST /v1/voices JSON body. // One voice registration parsed from the POST /v1/audio/voices JSON body.
// Exactly one payload form is present: wav holds decoded base64 WAV // Exactly one payload form is present: wav holds decoded base64 WAV
// bytes for server side extraction, or spk plus rvq hold the raw // bytes for server side extraction, or spk plus rvq hold the raw
// contents of pre-extracted .spk and .rvq files. ref_text carries the // contents of pre-extracted .spk and .rvq files. ref_text carries the
@@ -75,7 +75,7 @@ using tts_sink = std::function<bool(const float * samples, int n_samples)>;
// Adapter implemented by each project tool. // Adapter implemented by each project tool.
struct tts_backend { struct tts_backend {
std::string model_id; // reported by GET /v1/models std::string model_id; // reported by GET /v1/models
std::vector<std::string> voices; // reported by GET /v1/voices, may be empty std::vector<std::string> voices; // reported by GET /v1/audio/voices, may be empty
// Run synthesis. When the request streams, the adapter routes the ABI // Run synthesis. When the request streams, the adapter routes the ABI
// on_chunk to sink ; otherwise it pushes the whole buffer once. Returns // on_chunk to sink ; otherwise it pushes the whole buffer once. Returns
// the ABI status (0 on success), and fills err with the ABI message on // the ABI status (0 on success), and fills err with the ABI message on
@@ -84,7 +84,7 @@ struct tts_backend {
// Voice registry hooks, all optional: a null hook answers 501 on the // Voice registry hooks, all optional: a null hook answers 501 on the
// matching route. register_voice stores or replaces a cloned voice, // matching route. register_voice stores or replaces a cloned voice,
// remove_voice drops one (false when absent), registered_voices lists // remove_voice drops one (false when absent), registered_voices lists
// the current names for GET /v1/voices alongside the model speakers. // the current names for GET /v1/audio/voices alongside the model speakers.
std::function<bool(const tts_voice_upload & up, std::string & err)> register_voice; std::function<bool(const tts_voice_upload & up, std::string & err)> register_voice;
std::function<bool(const std::string & name)> remove_voice; std::function<bool(const std::string & name)> remove_voice;
std::function<std::vector<std::string>()> registered_voices; std::function<std::vector<std::string>()> registered_voices;
@@ -334,7 +334,7 @@ static bool tts_b64_decode(const std::string & in, std::string & out) {
return true; return true;
} }
// Parse the POST /v1/voices body: name plus either wav_b64 or the // Parse the POST /v1/audio/voices body: name plus either wav_b64 or the
// spk_b64 / rvq_b64 pair, ref_text optional (enables ICL clone mode). // spk_b64 / rvq_b64 pair, ref_text optional (enables ICL clone mode).
static bool tts_parse_voice_upload(const std::string & body, tts_voice_upload & up, std::string & err) { static bool tts_parse_voice_upload(const std::string & body, tts_voice_upload & up, std::string & err) {
yyjson_doc * doc = yyjson_read(body.c_str(), body.size(), 0); yyjson_doc * doc = yyjson_read(body.c_str(), body.size(), 0);
@@ -517,11 +517,11 @@ static int tts_server_run(const tts_backend & be, const server_config & cfg) {
[&be](const httplib::Request & req, httplib::Response & res) { tts_handle_speech(be, req, res); }); [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_speech(be, req, res); });
svr.Get("/v1/models", svr.Get("/v1/models",
[&be](const httplib::Request & req, httplib::Response & res) { tts_handle_models(be, req, res); }); [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_models(be, req, res); });
svr.Get("/v1/voices", svr.Get("/v1/audio/voices",
[&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voices(be, req, res); }); [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voices(be, req, res); });
svr.Post("/v1/voices", svr.Post("/v1/audio/voices",
[&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voice_register(be, req, res); }); [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voice_register(be, req, res); });
svr.Delete(R"(/v1/voices/(.+))", svr.Delete(R"(/v1/audio/voices/(.+))",
[&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voice_delete(be, req, res); }); [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voice_delete(be, req, res); });
svr.Get("/health", tts_handle_health); svr.Get("/health", tts_handle_health);
+6 -2
View File
@@ -42,6 +42,7 @@ static void print_usage(const char * prog) {
" --model <gguf> Talker LM GGUF (qwen-talker-*.gguf)\n" " --model <gguf> Talker LM GGUF (qwen-talker-*.gguf)\n"
" --codec <gguf> Codec GGUF (qwen-tokenizer-*.gguf)\n\n" " --codec <gguf> Codec GGUF (qwen-tokenizer-*.gguf)\n\n"
"Optional:\n" "Optional:\n"
" --alias <name> Report this model id instead of the GGUF file name\n"
" --host <ip> Listen address (default: 127.0.0.1)\n" " --host <ip> Listen address (default: 127.0.0.1)\n"
" --port <n> Listen port (default: 8080)\n" " --port <n> Listen port (default: 8080)\n"
" --lang <name> Language label (default: auto)\n" " --lang <name> Language label (default: auto)\n"
@@ -60,6 +61,7 @@ static std::string basename_of(const char * path) {
int main(int argc, char ** argv) { int main(int argc, char ** argv) {
const char * talker_path = NULL; const char * talker_path = NULL;
const char * codec_path = NULL; const char * codec_path = NULL;
std::string model_alias;
std::string lang = "auto"; std::string lang = "auto";
server_config cfg; server_config cfg;
bool use_fa = true; bool use_fa = true;
@@ -71,6 +73,8 @@ int main(int argc, char ** argv) {
talker_path = argv[++i]; talker_path = argv[++i];
} else if (!std::strcmp(arg, "--codec") && i + 1 < argc) { } else if (!std::strcmp(arg, "--codec") && i + 1 < argc) {
codec_path = argv[++i]; codec_path = argv[++i];
} else if (!std::strcmp(arg, "--alias") && i + 1 < argc) {
model_alias = argv[++i];
} else if (!std::strcmp(arg, "--host") && i + 1 < argc) { } else if (!std::strcmp(arg, "--host") && i + 1 < argc) {
cfg.host = argv[++i]; cfg.host = argv[++i];
} else if (!std::strcmp(arg, "--port") && i + 1 < argc) { } else if (!std::strcmp(arg, "--port") && i + 1 < argc) {
@@ -110,13 +114,13 @@ int main(int argc, char ** argv) {
} }
tts_backend be; tts_backend be;
be.model_id = basename_of(talker_path); be.model_id = model_alias.empty() ? basename_of(talker_path) : model_alias;
int n = qt_n_speakers(q); int n = qt_n_speakers(q);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
be.voices.push_back(qt_speaker_name(q, i)); be.voices.push_back(qt_speaker_name(q, i));
} }
// Voice registry: POST /v1/voices stores a cloned voice either from a // Voice registry: POST /v1/audio/voices stores a cloned voice either from a
// WAV (server side extraction through qt_extract_voice_ref) or from // WAV (server side extraction through qt_extract_voice_ref) or from
// pre-extracted .spk / .rvq payloads. Re-registering a name replaces // pre-extracted .spk / .rvq payloads. Re-registering a name replaces
// the previous entry. // the previous entry.