Skip to content
Open
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
13 changes: 12 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ flowchart LR
scripts["scripts/"] -->|"vocab.txt, g2p_*.weights"| dict["dict/"]
core["kokoro_core (static)"] --> lib["kokoro (shared, C API)"]
lib --> cli["kokoro CLI"]
lib --> server["kokoro-server (HTTP)"]
lib --> dotnet["Kokoro.Net (P/Invoke)"] --> nuget["NuGet packages"]
core --> coretests["test_g2p / test_chunker / g2p_dump"]
lib --> libtests["test_c_api / test_synthesis*"]
Expand All @@ -40,12 +41,13 @@ flowchart LR
| `include/kokoro/` | public C header |
| `src/` | engine, G2P, C API implementation |
| `src/cli/` | CLI |
| `src/server/` | HTTP server |
| `src/chinese_english/`, `src/english/`, `src/spanish/`, `src/german/` | per-language G2P |
| `dotnet/` | wrapper, example, tests |
| `packaging/` | NuGet build scripts |
| `dict/` | runtime G2P data |
| `models/` | downloaded, not committed |
| `third_party/` | onnxruntime, cppjieba, doctest, miniaudio |
| `third_party/` | onnxruntime, cppjieba, doctest, miniaudio, cpp-httplib, nlohmann/json |
| `eval_bench/` | G2P bench |
| `tests/` | ctest and pytest suites |
| `scripts/` | one-off converters |
Expand Down Expand Up @@ -216,6 +218,15 @@ These are the touch points the German addition went through, in order:
opens lazily at the first clip's sample rate, and clips are never freed on the audio thread.
- Commands: `/voice`, `/speed`, `/voices`, `/help`, `/quit`.

**HTTP server (`src/server/`).** `main.cpp` reuses the CLI's engine-option parsers (`src/cli/engine_options.h`) and
serves an OpenAI-compatible API with cpp-httplib.
- `SpeechService` owns the one context and serializes `kokoro_synthesize` behind a mutex; HTTP worker threads share it.
- `POST /v1/audio/speech` (`http_routes.cpp`) parses the JSON body (`speech_request.cpp`), then encodes the float
samples as 16-bit PCM WAV or raw PCM (`audio_encoding.cpp`). Other formats get an OpenAI-style 400 error.
- `GET /v1/audio/voices` lists the loaded voices; `GET /health` answers `{"status":"ok"}`.
- The listen socket uses `SO_REUSEADDR` (`SO_EXCLUSIVEADDRUSE` on Windows) instead of httplib's default
`SO_REUSEPORT`, so a second server on the same port fails instead of sharing it.

**.NET (`dotnet/`).**
- `KokoroNative` calls the C API through `DllImport("kokoro")`, marshalling strings as `LPUTF8Str`. Strings owned by
the library stay `IntPtr`, so the marshaller never frees them.
Expand Down
32 changes: 32 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,38 @@ elseif (UNIX)
set_target_properties(kokoro_cli PROPERTIES INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
endif()

add_library(kokoro_server_lib STATIC
src/server/speech_request.cpp
src/server/audio_encoding.cpp
src/server/speech_service.cpp
src/server/http_routes.cpp
)
target_include_directories(kokoro_server_lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src/server")
target_include_directories(kokoro_server_lib SYSTEM PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/cpp-httplib"
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/nlohmann_json/include"
)
target_link_libraries(kokoro_server_lib PUBLIC kokoro Threads::Threads)
if (WIN32)
target_link_libraries(kokoro_server_lib PUBLIC ws2_32)
endif()

add_executable(kokoro_server src/server/main.cpp src/cli/console.cpp)
target_include_directories(kokoro_server PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/cli")
target_link_libraries(kokoro_server PRIVATE kokoro_server_lib)
set_target_properties(kokoro_server PROPERTIES OUTPUT_NAME kokoro-server)
if (WIN32)
target_link_libraries(kokoro_server PRIVATE Shell32)
endif()
if (MSVC)
set_target_properties(kokoro_server PROPERTIES PDB_NAME kokoro_server)
elseif (APPLE)
set_target_properties(kokoro_server PROPERTIES INSTALL_RPATH "@loader_path/../${CMAKE_INSTALL_LIBDIR}")
elseif (UNIX)
set_target_properties(kokoro_server PROPERTIES INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
endif()
install(TARGETS kokoro_server RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

# Install: headers, libkokoro and a CMake package (find_package(kokoro) -> kokoro::kokoro).
install(TARGETS kokoro EXPORT kokoroTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ At runtime the CUDA 12 and cuDNN 9 libraries must be loadable: on `PATH` on Wind
ctest --test-dir build -C Release --output-on-failure
```

Tests live in `tests/` (doctest, vendored in `third_party/doctest`): `c_api` checks the C API without a model, `g2p` pins the Chinese/English G2P output, and `synthesis` runs the real model from `models/` (skipped if the model or voices file is missing). Configure with `-DKOKORO_BUILD_TESTS=OFF` to skip building them.
Tests live in `tests/` (doctest, vendored in `third_party/doctest`): `c_api` checks the C API without a model, `g2p` pins the Chinese/English G2P output, `server` checks the HTTP server's request parsing and WAV/PCM encoding, and `synthesis` and `server_synthesis` run the real model from `models/` (skipped if the model or voices file is missing). Configure with `-DKOKORO_BUILD_TESTS=OFF` to skip building them.

The Python tools have their own tests in `tests/python/`: run `uv run pytest`. The `.pt` import tests are skipped unless PyTorch is installed; `uv run --group voice pytest` runs them too.

Expand Down Expand Up @@ -135,6 +135,22 @@ German (voices `df_eva`, `df_victoria`, `dm_bernd`, `dm_martin`; `df_*`/`dm_*` v

Output is a mono 32-bit float WAV at 24 kHz. Exit status is 0 on success, 1 on a library error, and 2 on a usage error.

### HTTP server

`kokoro-server` loads the model once and serves an OpenAI-compatible text-to-speech API. Run it from the project root (it takes the same `-m`, `--voices`, `-d`, `--lang`, `--language`, `--device` and `--gpu-id` options as `kokoro`):

```bash
./build/kokoro-server --host 127.0.0.1 --port 8880 -v af_maple
curl -o hello.wav -H 'Content-Type: application/json' \
-d '{"model":"kokoro","input":"Hello world","voice":"af_maple"}' http://127.0.0.1:8880/v1/audio/speech
```

- `POST /v1/audio/speech`: `input` (required), `voice` (default: `-v`), `speed` (> 0, default 1.0) and `response_format` (`wav`, the default, or `pcm`). `wav` is a 16-bit PCM mono WAV at 24 kHz; `pcm` is the same samples raw, signed 16-bit little-endian. Other formats, unknown voices and invalid fields get HTTP 400 with an OpenAI-style JSON error. `model` and `instructions` are ignored.
- `GET /v1/audio/voices`: `{"voices": [...]}` with the loaded voice names.
- `GET /health`: `{"status":"ok"}`.

Requests are synthesized one at a time on the shared model. The server has no authentication, so keep it on loopback. Exit status is 1 if the model cannot be loaded or the port is in use, and 2 on a usage error.

## Library

`libkokoro` exposes a C API in [`include/kokoro/kokoro.h`](include/kokoro/kokoro.h), so it can be used from C, C++ or any language with a C FFI (Python `ctypes`, C#, Rust, Go, ...):
Expand Down Expand Up @@ -218,6 +234,7 @@ accent on the stressed vowel; sounds Spanish spelling lacks are written as IPA (

- `include/kokoro/kokoro.h`, `src/kokoro_c.cpp`: public C API of `libkokoro`.
- `src/cli/main.cpp`: the `kokoro` command-line tool.
- `src/server/`: the `kokoro-server` OpenAI-compatible HTTP server.
- `src/Kokoro.cpp/h`: main TTS class (internal).
- `src/EnG2P.h`, `src/NeuralG2P.cpp/h`: English G2P (dictionary lookup and neural prediction).
- `src/SpanishG2P.cpp/h`: rule-based Spanish G2P (espeak-ng `es` conventions).
Expand Down
14 changes: 14 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ Each component remains under its own license; the full license texts are in the
- License: MIT
- License text: `third_party/cppjieba/deps/limonp/LICENSE`

## Compiled into kokoro-server

### cpp-httplib

- Source: https://github.com/yhirose/cpp-httplib (v0.63.0)
- License: MIT
- License text: `third_party/cpp-httplib/LICENSE`

### nlohmann/json

- Source: https://github.com/nlohmann/json (v3.12.0)
- License: MIT
- License text: `third_party/nlohmann_json/LICENSE.MIT`

## Redistributed data (`dict/`, shipped as `kokoro-dict/` in the Larroy.Kokoro NuGet package)

### CMU Pronouncing Dictionary
Expand Down
51 changes: 51 additions & 0 deletions src/cli/engine_options.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Value parsers for the engine options shared by the kokoro and kokoro-server front ends.
#ifndef KOKORO_CLI_ENGINE_OPTIONS_H
#define KOKORO_CLI_ENGINE_OPTIONS_H

#include <kokoro/kokoro.h>

#include <climits>
#include <cstdlib>
#include <string>

namespace kokoro_cli {

// Maps the --lang value; returns false for anything but "auto", "en", "zh", "es" or "de".
inline bool parse_number_language(const std::string& value, kokoro_number_language& language) {
if (value == "auto") { language = KOKORO_NUMBERS_AUTO; return true; }
if (value == "en") { language = KOKORO_NUMBERS_ENGLISH; return true; }
if (value == "zh") { language = KOKORO_NUMBERS_CHINESE; return true; }
if (value == "es") { language = KOKORO_NUMBERS_SPANISH; return true; }
if (value == "de") { language = KOKORO_NUMBERS_GERMAN; return true; }
return false;
}

// Maps the --language value; returns false for anything but "auto", "zh", "en", "es" or "de".
inline bool parse_language(const std::string& value, kokoro_language& language) {
if (value == "auto") { language = KOKORO_LANGUAGE_AUTO; return true; }
if (value == "zh") { language = KOKORO_LANGUAGE_CHINESE_ENGLISH; return true; }
if (value == "en") { language = KOKORO_LANGUAGE_ENGLISH; return true; }
if (value == "es") { language = KOKORO_LANGUAGE_SPANISH; return true; }
if (value == "de") { language = KOKORO_LANGUAGE_GERMAN; return true; }
return false;
}

inline bool parse_device(const std::string& value, kokoro_device& device) {
if (value == "auto") { device = KOKORO_DEVICE_AUTO; return true; }
if (value == "cpu") { device = KOKORO_DEVICE_CPU; return true; }
if (value == "cuda") { device = KOKORO_DEVICE_CUDA; return true; }
return false;
}

inline bool parse_gpu_id(const std::string& value, int& gpu_id) {
if (value.empty()) return false;
char* end = nullptr;
const long parsed = std::strtol(value.c_str(), &end, 10);
if (end != value.c_str() + value.size() || parsed < 0 || parsed > INT_MAX) return false;
gpu_id = static_cast<int>(parsed);
return true;
}

} // namespace kokoro_cli

#endif // KOKORO_CLI_ENGINE_OPTIONS_H
45 changes: 5 additions & 40 deletions src/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <kokoro/kokoro.h>

#include "console.h"
#include "engine_options.h"
#include "interactive.h"
#include "speed.h"

Expand Down Expand Up @@ -109,42 +110,6 @@ bool usage_error(const std::string& message) {
return false;
}

// Maps the --lang value; returns false for anything but "auto", "en", "zh", "es" or "de".
bool parse_number_language(const std::string& value, kokoro_number_language& language) {
if (value == "auto") { language = KOKORO_NUMBERS_AUTO; return true; }
if (value == "en") { language = KOKORO_NUMBERS_ENGLISH; return true; }
if (value == "zh") { language = KOKORO_NUMBERS_CHINESE; return true; }
if (value == "es") { language = KOKORO_NUMBERS_SPANISH; return true; }
if (value == "de") { language = KOKORO_NUMBERS_GERMAN; return true; }
return false;
}

// Maps the --language value; returns false for anything but "auto", "zh", "en", "es" or "de".
bool parse_language(const std::string& value, kokoro_language& language) {
if (value == "auto") { language = KOKORO_LANGUAGE_AUTO; return true; }
if (value == "zh") { language = KOKORO_LANGUAGE_CHINESE_ENGLISH; return true; }
if (value == "en") { language = KOKORO_LANGUAGE_ENGLISH; return true; }
if (value == "es") { language = KOKORO_LANGUAGE_SPANISH; return true; }
if (value == "de") { language = KOKORO_LANGUAGE_GERMAN; return true; }
return false;
}

bool parse_device(const std::string& value, kokoro_device& device) {
if (value == "auto") { device = KOKORO_DEVICE_AUTO; return true; }
if (value == "cpu") { device = KOKORO_DEVICE_CPU; return true; }
if (value == "cuda") { device = KOKORO_DEVICE_CUDA; return true; }
return false;
}

bool parse_gpu_id(const std::string& value, int& gpu_id) {
if (value.empty()) return false;
char* end = nullptr;
const long parsed = std::strtol(value.c_str(), &end, 10);
if (end != value.c_str() + value.size() || parsed < 0 || parsed > INT_MAX) return false;
gpu_id = static_cast<int>(parsed);
return true;
}

// Returns false after printing a usage error.
bool parse_args(const std::vector<std::string>& args, Options& opt) {
bool positional_only = false;
Expand Down Expand Up @@ -191,23 +156,23 @@ bool parse_args(const std::vector<std::string>& args, Options& opt) {
case Opt::Dict: opt.dict = value; break;
case Opt::Voice: opt.voice = value; break;
case Opt::Lang:
if (!parse_number_language(value, opt.number_language)) {
if (!kokoro_cli::parse_number_language(value, opt.number_language)) {
return usage_error("invalid number language '" + value + "' (must be auto, en, zh, es or de)");
}
break;
case Opt::Language:
if (!parse_language(value, opt.language)) {
if (!kokoro_cli::parse_language(value, opt.language)) {
return usage_error("invalid language '" + value + "' (must be auto, zh, en, es or de)");
}
break;
case Opt::Output: opt.output = value; break;
case Opt::Device:
if (!parse_device(value, opt.device)) {
if (!kokoro_cli::parse_device(value, opt.device)) {
return usage_error("invalid device '" + value + "' (expected auto, cpu or cuda)");
}
break;
case Opt::GpuId:
if (!parse_gpu_id(value, opt.gpu_id)) {
if (!kokoro_cli::parse_gpu_id(value, opt.gpu_id)) {
return usage_error("invalid GPU id '" + value + "' (must be an integer >= 0)");
}
break;
Expand Down
57 changes: 57 additions & 0 deletions src/server/audio_encoding.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include "audio_encoding.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <stdexcept>

namespace kokoro_server {
namespace {

void put_u16(std::string& out, uint16_t v) {
out.push_back(static_cast<char>(v & 0xFFu));
out.push_back(static_cast<char>((v >> 8) & 0xFFu));
}

void put_u32(std::string& out, uint32_t v) {
for (int shift = 0; shift < 32; shift += 8) out.push_back(static_cast<char>((v >> shift) & 0xFFu));
}

int16_t to_pcm16(float sample) {
return static_cast<int16_t>(std::lround(std::clamp(sample, -1.0f, 1.0f) * 32767.0f));
}

} // namespace

std::string encode_pcm16(const float* samples, size_t count) {
std::string out;
out.reserve(count * 2);
for (size_t i = 0; i < count; ++i) put_u16(out, static_cast<uint16_t>(to_pcm16(samples[i])));
return out;
}

std::string encode_wav_pcm16(const float* samples, size_t count, int sample_rate) {
if (count > (0xFFFFFFFFu - 36u) / 2u) throw std::length_error("audio too long for a WAV file");
const uint32_t data_size = static_cast<uint32_t>(count * 2);
const uint32_t rate = static_cast<uint32_t>(sample_rate);

std::string out;
out.reserve(44u + data_size);
out += "RIFF";
put_u32(out, 36u + data_size);
out += "WAVE";
out += "fmt ";
put_u32(out, 16); // fmt chunk size
put_u16(out, 1); // WAVE_FORMAT_PCM
put_u16(out, 1); // channels
put_u32(out, rate);
put_u32(out, rate * 2); // byte rate
put_u16(out, 2); // block align
put_u16(out, 16); // bits per sample
out += "data";
put_u32(out, data_size);
out += encode_pcm16(samples, count);
return out;
}

} // namespace kokoro_server
18 changes: 18 additions & 0 deletions src/server/audio_encoding.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 16-bit PCM encoders for kokoro's mono float audio.
#ifndef KOKORO_SERVER_AUDIO_ENCODING_H
#define KOKORO_SERVER_AUDIO_ENCODING_H

#include <cstddef>
#include <string>

namespace kokoro_server {

// Raw signed 16-bit little-endian mono samples.
std::string encode_pcm16(const float* samples, size_t count);

// 44-byte RIFF/WAVE header followed by encode_pcm16 data. Throws std::length_error past the 4 GiB WAV limit.
std::string encode_wav_pcm16(const float* samples, size_t count, int sample_rate);

} // namespace kokoro_server

#endif // KOKORO_SERVER_AUDIO_ENCODING_H
Loading
Loading