Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ jobs:
python-version: "3.13"
architecture: ${{ matrix.arch }}

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
workspaces: src/server -> ../../build/cargo

- name: Cache model and voices
uses: actions/cache@v6
with:
Expand Down Expand Up @@ -78,3 +88,13 @@ jobs:

- name: Test
run: ctest --test-dir build -C Release --output-on-failure

- name: Lint kokoro-server
if: matrix.name == 'linux-x64'
working-directory: src/server
env:
KOKORO_LIB_DIR: ${{ github.workspace }}/build
CARGO_TARGET_DIR: ${{ github.workspace }}/build/cargo
run: |
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Build directories
build/
bin/
# cargo, when run directly in the kokoro-server crate
src/server/target/
# Built by `uv run bootstrap.py build-ort`
third_party/onnxruntime-src/
out/
Expand Down
15 changes: 15 additions & 0 deletions 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,6 +41,7 @@ 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 |
Expand Down Expand Up @@ -216,6 +218,19 @@ 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/`).** A Rust crate (axum + tokio) that links `libkokoro` through hand-written C API
declarations (`ffi.rs`) and serves an OpenAI-compatible API. CMake builds it with cargo (`KOKORO_BUILD_SERVER`) and
copies `kokoro-server` next to `libkokoro`; `build.rs` links against the library and sets the rpath.
- `Engine` (`engine.rs`) owns the one context; `Audio` frees the samples with `kokoro_audio_free` on drop.
- `AppState` (`routes.rs`) keeps the engine behind a `tokio::sync::Mutex`. A request waits for the lock without a
thread, then synthesizes in `spawn_blocking`, which also reads the thread-local `kokoro_last_error()`.
- `POST /v1/audio/speech` parses the JSON body (`speech_request.rs`), then encodes the float samples as 16-bit PCM
WAV or raw PCM (`audio_encoding.rs`). Other formats get an OpenAI-style 400 error.
- `GET /v1/audio/voices` lists the loaded voices; `GET /health` answers `{"status":"ok"}`.
- The listen socket (`listener.rs`) sets `SO_REUSEADDR` (`SO_EXCLUSIVEADDRUSE` on Windows) and never
`SO_REUSEPORT`, so a second server on the same port fails instead of sharing it.
- `options.rs` ports the CLI's option parsing (`src/cli/engine_options.h` values), messages and exit codes.

**.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
39 changes: 34 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,40 @@ elseif (UNIX)
set_target_properties(kokoro_cli PROPERTIES INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
endif()

if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
set(KOKORO_IS_TOP_LEVEL ON)
else()
set(KOKORO_IS_TOP_LEVEL OFF)
endif()
# HTTP server: a Rust crate in src/server, built by cargo against libkokoro and copied next to it.
option(KOKORO_BUILD_SERVER "Build kokoro-server (needs a Rust toolchain)" ${KOKORO_IS_TOP_LEVEL})
if (KOKORO_BUILD_SERVER)
find_program(CARGO cargo HINTS "$ENV{CARGO_HOME}/bin" "$ENV{HOME}/.cargo/bin" "$ENV{USERPROFILE}/.cargo/bin")
if (NOT CARGO)
message(FATAL_ERROR
"cargo not found: install Rust (https://rustup.rs) or configure with -DKOKORO_BUILD_SERVER=OFF")
endif()
set(KOKORO_CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/cargo")
set(KOKORO_CARGO_PROFILE "$<IF:$<CONFIG:Debug>,dev,release>")
set(KOKORO_CARGO_ENV
"CARGO_TARGET_DIR=${KOKORO_CARGO_TARGET_DIR}"
"KOKORO_LIB_DIR=$<TARGET_LINKER_FILE_DIR:kokoro>"
"KOKORO_INSTALL_LIBDIR=${CMAKE_INSTALL_LIBDIR}")
set(KOKORO_SERVER_EXE "kokoro-server${CMAKE_EXECUTABLE_SUFFIX}")
add_custom_target(kokoro_server ALL
COMMAND ${CMAKE_COMMAND} -E env ${KOKORO_CARGO_ENV}
"${CARGO}" build --locked --profile ${KOKORO_CARGO_PROFILE}
--manifest-path "${CMAKE_CURRENT_SOURCE_DIR}/src/server/Cargo.toml"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${KOKORO_CARGO_TARGET_DIR}/$<IF:$<CONFIG:Debug>,debug,release>/${KOKORO_SERVER_EXE}"
"$<TARGET_FILE_DIR:kokoro>/${KOKORO_SERVER_EXE}"
COMMENT "Building kokoro-server with cargo"
VERBATIM
)
add_dependencies(kokoro_server kokoro)
install(PROGRAMS "$<TARGET_FILE_DIR:kokoro>/${KOKORO_SERVER_EXE}" DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()

# Install: headers, libkokoro and a CMake package (find_package(kokoro) -> kokoro::kokoro).
install(TARGETS kokoro EXPORT kokoroTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
Expand All @@ -201,11 +235,6 @@ install(FILES
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/kokoro
)

if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
set(KOKORO_IS_TOP_LEVEL ON)
else()
set(KOKORO_IS_TOP_LEVEL OFF)
endif()
option(KOKORO_BUILD_TESTS "Build the kokoro tests" ${KOKORO_IS_TOP_LEVEL})
if (KOKORO_BUILD_TESTS)
enable_testing()
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A high-performance, lightweight C++ inference implementation of the [Kokoro](htt
- **CMake** (3.15+)
- **Ninja** (recommended; `uv run bootstrap.py build` uses it when it is in `PATH`)
- **C++ compiler** (C++17 support required)
- **Rust** (1.80+, via [rustup](https://rustup.rs)): builds `kokoro-server`; configure with `-DKOKORO_BUILD_SERVER=OFF` to skip it
- **[uv](https://docs.astral.sh/uv/)**: runs `bootstrap.py` and `voice_tool.py`, and installs their Python dependencies (Python 3.10+, click) into `.venv` on first use

## Setup
Expand Down Expand Up @@ -65,7 +66,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` runs `cargo test` for `kokoro-server` (request parsing, WAV/PCM encoding, the listen socket, and an end-to-end HTTP check against the model), and `synthesis` runs the real model from `models/` (model-dependent tests are 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 +136,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. It is written in Rust (`src/server/`, axum) on top of the C API, and `uv run bootstrap.py build` builds it with cargo next to `libkokoro`. 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 +235,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 (Rust crate).
- `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 Expand Up @@ -252,6 +270,7 @@ MIT
- **CMake** (3.15+)
- **Ninja**(推荐;`uv run bootstrap.py build` 在 `PATH` 中找到它时使用)
- **C++ 编译器** (需要支持 C++17)
- **Rust**(1.80+,通过 [rustup](https://rustup.rs) 安装):用于构建 `kokoro-server`;配置时加 `-DKOKORO_BUILD_SERVER=OFF` 可跳过
- **[uv](https://docs.astral.sh/uv/)**:用于运行 `bootstrap.py` 和 `voice_tool.py`,首次运行时会将其 Python 依赖(Python 3.10+、click)安装到 `.venv`

## 准备
Expand Down Expand Up @@ -477,6 +496,7 @@ Implementación ligera y de alto rendimiento en C++ de la inferencia del modelo
- **CMake** (3.15+)
- **Ninja** (recomendado; `uv run bootstrap.py build` lo usa si está en el `PATH`)
- **Compilador de C++** (con soporte de C++17)
- **Rust** (1.80+, con [rustup](https://rustup.rs)): compila `kokoro-server`; configura con `-DKOKORO_BUILD_SERVER=OFF` para omitirlo
- **[uv](https://docs.astral.sh/uv/)**: ejecuta `bootstrap.py` y `voice_tool.py`, e instala sus dependencias de Python (Python 3.10+, click) en `.venv` la primera vez

## Preparación
Expand Down Expand Up @@ -723,6 +743,7 @@ Eine schnelle, schlanke C++-Implementierung der Inferenz des TTS-Modells [Kokoro
- **CMake** (3.15+)
- **Ninja** (empfohlen; `uv run bootstrap.py build` verwendet es, wenn es im `PATH` liegt)
- **C++-Compiler** (mit C++17-Unterstützung)
- **Rust** (1.80+, über [rustup](https://rustup.rs)): baut `kokoro-server`; mit `-DKOKORO_BUILD_SERVER=OFF` konfigurieren, um es auszulassen
- **[uv](https://docs.astral.sh/uv/)**: führt `bootstrap.py` und `voice_tool.py` aus und installiert beim ersten Aufruf ihre Python-Abhängigkeiten (Python 3.10+, click) in `.venv`

## Einrichtung
Expand Down
15 changes: 15 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ 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

kokoro-server statically links Rust crates from crates.io; the exact versions are pinned in `src/server/Cargo.lock`
and each crate's license text ships in its source package (`cargo tree -e normal -f "{p} {l}"` lists them).

- MIT: axum, axum-core, bytes, http-body, http-body-util, hyper, hyper-util, mio, slab, tokio, tower, tower-layer,
tower-service, zmij
- MIT OR Apache-2.0: atomic-waker, futures-core, futures-task, futures-util, http, httparse, httpdate, itoa, mime,
percent-encoding, pin-project-lite, serde_core, serde_json, smallvec, socket2, windows-link, windows-sys (Windows)
- MIT AND BSD-3-Clause: matchit
- Unlicense OR MIT: memchr
- Apache-2.0: sync_wrapper
- Build-time only (proc macros): proc-macro2, quote, syn (MIT OR Apache-2.0), tokio-macros (MIT), unicode-ident
((MIT OR Apache-2.0) AND Unicode-3.0)

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

### CMU Pronouncing Dictionary
Expand Down
2 changes: 1 addition & 1 deletion include/kokoro/kokoro.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ KOKORO_API size_t kokoro_voice_count(const kokoro_ctx* ctx);
KOKORO_API const char* kokoro_voice_name(const kokoro_ctx* ctx, size_t index);

/*
* Synthesizes `text` with `voice` at `speed` (1.0 = normal, must be > 0).
* Synthesizes `text` with `voice` at `speed` (1.0 = normal, must be > 0 and < 2^31).
* flags: 0 or KOKORO_INPUT_PHONEMES.
* On success fills *out_audio; the caller must release it with kokoro_audio_free().
* On failure *out_audio is zeroed.
Expand Down
2 changes: 1 addition & 1 deletion packaging/build-natives.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function Invoke-NativeBuild([hashtable]$Row) {
throw 'ninja not found on PATH (install the VS "C++ CMake tools" component or Ninja)'
}
Invoke-Checked cmake @('-S', $root, '-B', $buildDir, '-G', 'Ninja', '-DCMAKE_BUILD_TYPE=Release',
"-DONNXRUNTIME_ROOT=$ortRoot", '-DKOKORO_BUILD_EVAL=OFF')
"-DONNXRUNTIME_ROOT=$ortRoot", '-DKOKORO_BUILD_EVAL=OFF', '-DKOKORO_BUILD_SERVER=OFF')
Invoke-Checked cmake @('--build', $buildDir, '--parallel')
}
}
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 of the kokoro front end (kokoro-server ports them in src/server/options.rs).
#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
Loading
Loading