From 8d556c98066305f9e300902283ab34e13274cd68 Mon Sep 17 00:00:00 2001 From: karl Date: Thu, 9 Jul 2026 13:18:24 +0800 Subject: [PATCH] server: compatible with OpenAI API, add model alias --- README.md | 5 +++-- docs/ARCHITECTURE.md | 9 +++++---- examples/server.sh | 1 + src/tts-server.h | 20 ++++++++++---------- tools/tts-server.cpp | 8 ++++++-- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5a226b3..6984f35 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,10 @@ them by name : ``` ./build/tts-server \ --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)\", \"spk_b64\":\"$(base64 -w0 ref.spk)\",\"rvq_b64\":\"$(base64 -w0 ref.rvq)\"}" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f208ab4..864e3c2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -617,6 +617,7 @@ Required: --codec Codec GGUF (qwen-tokenizer-*.gguf) Optional: + --alias Report this model id instead of the GGUF file name --host Listen address (default: 127.0.0.1) --port Listen port (default: 8080) --lang 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 0 selects greedy decoding, the subtalker mirrors the talker knobs -GET /v1/models single loaded model -GET /v1/voices model speakers plus registered cloned voices -POST /v1/voices register a cloned voice: {name, ref_text, +GET /v1/models single loaded model, using --alias when set +GET /v1/audio/voices model speakers plus registered cloned voices +POST /v1/audio/voices register a cloned voice: {name, ref_text, wav_b64} extracts server side through qt_extract_voice_ref, {name, ref_text, spk_b64, rvq_b64} takes the 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 ``` diff --git a/examples/server.sh b/examples/server.sh index 9dce2f3..44b934f 100755 --- a/examples/server.sh +++ b/examples/server.sh @@ -4,4 +4,5 @@ ./build/tts-server \ --model models/qwen-talker-1.7b-base-Q8_0.gguf \ --codec models/qwen-tokenizer-12hz-Q8_0.gguf \ + --alias qwen3-tts-base \ --host 127.0.0.1 --port 8080 --lang auto diff --git a/src/tts-server.h b/src/tts-server.h index 68df91c..26b7dda 100644 --- a/src/tts-server.h +++ b/src/tts-server.h @@ -10,12 +10,12 @@ // Endpoints: // POST /v1/audio/speech OAI text-to-speech // GET /v1/models single loaded model -// GET /v1/voices model speakers plus registered cloned voices -// POST /v1/voices register a cloned voice: {name, ref_text, +// GET /v1/audio/voices model speakers plus registered cloned voices +// POST /v1/audio/voices register a cloned voice: {name, ref_text, // wav_b64} extracts server side, {name, // ref_text, spk_b64, rvq_b64} takes // 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 // // 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 }; -// 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 // bytes for server side extraction, or spk plus rvq hold the raw // contents of pre-extracted .spk and .rvq files. ref_text carries the @@ -75,7 +75,7 @@ using tts_sink = std::function; // Adapter implemented by each project tool. struct tts_backend { std::string model_id; // reported by GET /v1/models - std::vector voices; // reported by GET /v1/voices, may be empty + std::vector voices; // reported by GET /v1/audio/voices, may be empty // Run synthesis. When the request streams, the adapter routes the ABI // 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 @@ -84,7 +84,7 @@ struct tts_backend { // Voice registry hooks, all optional: a null hook answers 501 on the // matching route. register_voice stores or replaces a cloned voice, // 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 register_voice; std::function remove_voice; std::function()> registered_voices; @@ -334,7 +334,7 @@ static bool tts_b64_decode(const std::string & in, std::string & out) { 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). 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); @@ -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); }); svr.Get("/v1/models", [&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); }); - svr.Post("/v1/voices", + svr.Post("/v1/audio/voices", [&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); }); svr.Get("/health", tts_handle_health); diff --git a/tools/tts-server.cpp b/tools/tts-server.cpp index adaaadc..60004c7 100644 --- a/tools/tts-server.cpp +++ b/tools/tts-server.cpp @@ -42,6 +42,7 @@ static void print_usage(const char * prog) { " --model Talker LM GGUF (qwen-talker-*.gguf)\n" " --codec Codec GGUF (qwen-tokenizer-*.gguf)\n\n" "Optional:\n" + " --alias Report this model id instead of the GGUF file name\n" " --host Listen address (default: 127.0.0.1)\n" " --port Listen port (default: 8080)\n" " --lang Language label (default: auto)\n" @@ -60,6 +61,7 @@ static std::string basename_of(const char * path) { int main(int argc, char ** argv) { const char * talker_path = NULL; const char * codec_path = NULL; + std::string model_alias; std::string lang = "auto"; server_config cfg; bool use_fa = true; @@ -71,6 +73,8 @@ int main(int argc, char ** argv) { talker_path = argv[++i]; } else if (!std::strcmp(arg, "--codec") && i + 1 < argc) { 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) { cfg.host = argv[++i]; } else if (!std::strcmp(arg, "--port") && i + 1 < argc) { @@ -110,13 +114,13 @@ int main(int argc, char ** argv) { } 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); for (int i = 0; i < n; 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 // pre-extracted .spk / .rvq payloads. Re-registering a name replaces // the previous entry.