Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion examples/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,16 @@ curl http://127.0.0.1:8080/v1/audio/speech \

The only required parameter is `input` otherwise generation configuration will be determined by the defaults set on server initialization, and the `response_format` will use `wav`. The `response_format` field currently supports only `wav` and `aiff` audio formats.

#### Voices

For models that support voices a complete json list of supported voices can be queried vis the voices endpoint, `/v1/audio/voices`:

```commandline
Comment thread
danielzgtg marked this conversation as resolved.
curl http://127.0.0.1:8080/v1/audio/voices
```

### Future Work

Future work will include:
* Support for token authentication and permissioning
* Multiple model support
* Streaming audio, for longform audio generation.
78 changes: 62 additions & 16 deletions examples/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ enum error_type {
enum task_type {
TTS,
CONDITIONAL_PROMPT,
VOICES,
};

using json = nlohmann::ordered_json;
Expand Down Expand Up @@ -87,8 +88,8 @@ static void log_server_request(const httplib::Request & req, const httplib::Resp
fprintf(stdout, "request: %s %s %s %d\n", req.method.c_str(), req.path.c_str(), req.remote_addr.c_str(), res.status);
}

struct simple_text_prompt_task {
simple_text_prompt_task(task_type task, std::string prompt): task(task), prompt(prompt) {
struct simple_server_task {
simple_server_task(task_type task, std::string prompt = ""): task(task), prompt(prompt) {
id = rand();
time = std::chrono::steady_clock::now();
}
Expand All @@ -114,11 +115,11 @@ struct simple_text_prompt_task {
struct simple_task_queue {
std::mutex rw_mutex;
std::condition_variable condition;
std::deque<simple_text_prompt_task*> queue;
std::deque<simple_server_task*> queue;
bool running = true;

struct simple_text_prompt_task * get_next() {
struct simple_text_prompt_task * resp;
struct simple_server_task * get_next() {
struct simple_server_task * resp;
std::unique_lock<std::mutex> lock(rw_mutex);
condition.wait(lock, [&]{
return !queue.empty() || !running;
Expand All @@ -138,7 +139,7 @@ struct simple_task_queue {
condition.notify_all();
}

void push(struct simple_text_prompt_task * task) {
void push(struct simple_server_task * task) {
std::lock_guard<std::mutex> lock(rw_mutex);
queue.push_back(task);
condition.notify_one();
Expand All @@ -152,7 +153,7 @@ struct simple_response_map {
std::atomic<bool> running = true;
std::thread * cleanup_thread;

std::map<int, simple_text_prompt_task*> completed;
std::map<int, simple_server_task*> completed;

void cleanup_routine() {
std::unique_lock<std::mutex> lock(rw_mutex);
Expand Down Expand Up @@ -182,16 +183,16 @@ struct simple_response_map {
updated.notify_all();
}

void push(struct simple_text_prompt_task * task) {
void push(struct simple_server_task * task) {
std::unique_lock<std::mutex> lock(rw_mutex);
completed[task->id] = task;
lock.unlock();
updated.notify_all();
}

struct simple_text_prompt_task * get(int id) {
struct simple_server_task * get(int id) {
std::unique_lock<std::mutex> lock(rw_mutex);
struct simple_text_prompt_task * resp = nullptr;
struct simple_server_task * resp = nullptr;
try {
return completed.at(id);
} catch (const std::out_of_range& e) {
Expand Down Expand Up @@ -231,14 +232,14 @@ struct worker {

void loop() {
while (running) {
struct simple_text_prompt_task * task = task_queue->get_next();
struct simple_server_task * task = task_queue->get_next();
if (task) {
process_task(task);
}
}
}

void process_task(struct simple_text_prompt_task * task) {
void process_task(struct simple_server_task * task) {
if (task->timed_out(task_timeout)) {
return;
}
Expand All @@ -264,6 +265,21 @@ struct worker {
task->success = true;
response_map->push(task);
break;
case VOICES:
if (!runner->supports_voices) {
task->message = "Voices are not supported for architecture '" + runner->arch_name() + "'.";
response_map->push(task);
break;
}
for (auto voice : list_voices(runner)) {
if (!task->message.empty()) {
task->message += ",";

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really shouldn't use the message output for passing data back from the workers.

}
task->message += voice;
}
task->success = true;
response_map->push(task);
break;
}
}
};
Expand Down Expand Up @@ -432,6 +448,15 @@ int main(int argc, const char ** argv) {
res.status = 200;
};

auto res_ok_voices = [](httplib::Response & res, const std::vector<std::string> & voices) {
json json_voices = json::array();
for (auto voice : voices) {
json_voices.push_back(voice);
}
res.set_content(safe_json_to_str(json_voices), MIMETYPE_JSON);
res.status = 200;
};

svr->set_exception_handler([&res_error](const httplib::Request &, httplib::Response & res, const std::exception_ptr & ep) {
std::string message;
try {
Expand Down Expand Up @@ -516,7 +541,7 @@ int main(int argc, const char ** argv) {
res_error(res, formatted_error);
return;
}
struct simple_text_prompt_task * task = new simple_text_prompt_task(TTS, prompt);
struct simple_server_task * task = new simple_server_task(TTS, prompt);
int id = task->id;
generation_configuration * conf = new generation_configuration();
std::memcpy(conf, default_generation_config, sizeof(generation_configuration));
Expand Down Expand Up @@ -544,7 +569,7 @@ int main(int argc, const char ** argv) {

task->gen_config = conf;
tqueue->push(task);
struct simple_text_prompt_task * rtask = rmap->get(id);
struct simple_server_task * rtask = rmap->get(id);
if (!rtask->success) {
json formatted_error = format_error_response(rtask->message, ERROR_TYPE_SERVER);
res_error(res, formatted_error);
Expand Down Expand Up @@ -586,10 +611,10 @@ int main(int argc, const char ** argv) {
return;
}
std::string prompt = data.at("input").get<std::string>();
struct simple_text_prompt_task * task = new simple_text_prompt_task(CONDITIONAL_PROMPT, prompt);
struct simple_server_task * task = new simple_server_task(CONDITIONAL_PROMPT, prompt);
int id = task->id;
tqueue->push(task);
struct simple_text_prompt_task * rtask = rmap->get(id);
struct simple_server_task * rtask = rmap->get(id);
if (!rtask->success) {
json formatted_error = format_error_response(rtask->message, ERROR_TYPE_SERVER);
res_error(res, formatted_error);
Expand All @@ -599,10 +624,31 @@ int main(int argc, const char ** argv) {
res_ok_json(res, health);
};

const auto handle_voices = [&args, &tqueue, &rmap, &res_error, &res_ok_voices](const httplib::Request & req, httplib::Response & res) {
struct simple_server_task * task = new simple_server_task(VOICES);
int id = task->id;
tqueue->push(task);
struct simple_server_task * rtask = rmap->get(id);
if (!rtask->success) {
json formatted_error;
if (has_prefix(rtask->message, "Voices are not supported")) {
formatted_error = format_error_response(rtask->message, ERROR_TYPE_NOT_SUPPORTED);
} else {
formatted_error = format_error_response(rtask->message, ERROR_TYPE_SERVER);
}
res_error(res, formatted_error);
return;
}
std::vector<std::string> voices = split(rtask->message, ",");

@mmwillet mmwillet May 1, 2025

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very inefficient and a more generic response pattern should be preferred (rather than parsing a string list from the message).

res_ok_voices(res, voices);
};

// register API routes
svr->Get("/health", handle_health);
svr->Post("/v1/audio/speech", handle_tts);
svr->Post("/v1/audio/conditional-prompt", handle_conditional);
svr->Get("/v1/audio/voices", handle_voices);


// Start the server
svr->new_task_queue = [&args] {
Expand Down
10 changes: 10 additions & 0 deletions include/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const std::map<std::string, tts_arch> SUPPORTED_ARCHITECTURES = {
{ "kokoro", KOKORO_ARCH },
};

const std::map<tts_arch, std::string> ARCHITECTURE_NAMES = {
{ PARLER_TTS_ARCH, "parler-tts" },
{ KOKORO_ARCH, "kokoro" },
};

struct generation_configuration {
generation_configuration(
std::string voice = "",
Expand All @@ -47,6 +52,11 @@ struct tts_runner {
tts_arch arch;
struct ggml_context * ctx = nullptr;
float sampling_rate = 44100.0f;
bool supports_voices = false;

std::string arch_name() {
return ARCHITECTURE_NAMES.at(arch);
}

void init_build(std::vector<uint8_t>* buf_compute_meta);
void free_build();
Expand Down
1 change: 1 addition & 0 deletions include/tts.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct tts_runner * kokoro_from_file(gguf_context * meta_ctx, ggml_context * wei
struct tts_runner * runner_from_file(const std::string & fname, int n_threads, generation_configuration * config, bool cpu_only = true);
int generate(tts_runner * runner, std::string sentence, struct tts_response * response, generation_configuration * config);
void update_conditional_prompt(tts_runner * runner, const std::string file_path, const std::string prompt, bool cpu_only = true);
std::vector<std::string> list_voices(tts_runner * runner);

struct quantization_params {
quantization_params(uint32_t n_threads, enum ggml_type quantize_type, void * imatrix = nullptr): n_threads(n_threads), quantize_type(quantize_type), imatrix(imatrix) {};
Expand Down
9 changes: 9 additions & 0 deletions src/kokoro_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,15 @@ int kokoro_runner::generate(std::string prompt, struct tts_response * response,
return 0;
}

std::vector<std::string> kokoro_runner::list_voices() {
std::vector<std::string> voices;
voices.reserve(model->voices.size());
for (auto voice : model->voices) {
voices.push_back(voice.first);
}
return voices;
}


std::string get_espeak_id_from_kokoro_voice(std::string voice) {
return !voice.empty() && KOKORO_LANG_TO_ESPEAK_ID.find(voice[0]) != KOKORO_LANG_TO_ESPEAK_ID.end() ? KOKORO_LANG_TO_ESPEAK_ID[voice[0]] : "gmw/en-US";
Expand Down
3 changes: 2 additions & 1 deletion src/kokoro_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ struct kokoro_context * build_new_kokoro_context(struct kokoro_model * model, in
struct kokoro_runner : tts_runner {
kokoro_runner(kokoro_model * model, kokoro_context * context, single_pass_tokenizer * tokenizer, kokoro_duration_runner * drunner, phonemizer * phmzr): model(model), kctx(context), tokenizer(tokenizer), drunner(drunner), phmzr(phmzr) {
tts_runner::sampling_rate = 24000.0f;
tts_runner::supports_voices = true;
};
~kokoro_runner() {
if (ctx) {
Expand All @@ -452,8 +453,8 @@ struct kokoro_runner : tts_runner {
void init_build() {
tts_runner::init_build(&kctx->buf_compute_meta);
}


std::vector<std::string> list_voices();
std::vector<std::vector<uint32_t>> tokenize_chunks(std::vector<std::string> clauses);
void assign_weight(std::string name, ggml_tensor * tensor);
void prepare_post_load();
Expand Down
9 changes: 9 additions & 0 deletions src/tts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ int generate(tts_runner * runner, std::string sentence, struct tts_response * re
}
}

std::vector<std::string> list_voices(tts_runner * runner) {
switch(runner->arch) {
case KOKORO_ARCH:
return ((kokoro_runner*)runner)->list_voices();
default:
TTS_ABORT("%s failed. The architecture '%d' does not support #list_voices supported.", __func__, runner->arch);
Comment thread
danielzgtg marked this conversation as resolved.
}
}

void update_conditional_prompt(tts_runner * runner, const std::string file_path, const std::string prompt, bool cpu_only) {
int n_threads = ((parler_tts_runner*)runner)->pctx->n_threads;
((parler_tts_runner*)runner)->update_conditional_prompt(file_path, prompt, n_threads, cpu_only);
Expand Down