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
12 changes: 9 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ option(ENGINE_ENABLE_OPENMP "Build host code with OpenMP support" ON)
option(AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER
"Build native model-manager tools and server download/install support"
OFF)
include("${CMAKE_CURRENT_SOURCE_DIR}/app/server/frontends/server_frontends.cmake")
option(AUDIOCPP_USE_SYSTEM_OPENSSL
"Use system OpenSSL for native model management instead of bundled BoringSSL"
OFF)
if (AUDIOCPP_USE_SYSTEM_OPENSSL AND NOT AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER)
if (AUDIOCPP_USE_SYSTEM_OPENSSL AND NOT AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER AND NOT AUDIOCPP_SERVER_FRONTEND_HTTPS_ENABLED)
message(FATAL_ERROR
"AUDIOCPP_USE_SYSTEM_OPENSSL requires AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON")
"AUDIOCPP_USE_SYSTEM_OPENSSL requires AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON "
"or AUDIOCPP_BUILD_SERVER_FRONTENDS=ON with AUDIOCPP_SERVER_FRONTEND_MODULES=https")
endif()
option(ENGINE_ENABLE_CPU_ALL_VARIANTS
"Build CPU backends as dynamic libraries with per-ISA variants (for Docker/portable builds)"
Expand Down Expand Up @@ -1622,8 +1624,10 @@ target_include_directories(cjson_vendor PUBLIC
# builds do not configure an HTTP/TLS dependency. When enabled, package specs
# remain embedded so the standalone manager and managed WebUI do not require a
# source checkout or an external model_specs directory.
if (AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER)
if (AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER OR AUDIOCPP_SERVER_FRONTEND_HTTPS_ENABLED)
add_subdirectory(external/cpp-httplib)
endif()
if (AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER)
add_library(audiocpp_package_manager STATIC
src/framework/package_manager/manager.cpp
src/framework/io/json.cpp
Expand Down Expand Up @@ -1777,6 +1781,7 @@ add_executable(audiocpp_server
app/server/main.cpp
app/server/base64.cpp
app/server/config.cpp
app/server/frontend.cpp
app/server/http.cpp
app/server/model_memory.cpp
app/server/multipart.cpp
Expand All @@ -1789,6 +1794,7 @@ add_executable(audiocpp_server
)

target_link_libraries(audiocpp_server PRIVATE engine_runtime ggml)
audiocpp_configure_server_frontends(audiocpp_server)
if (AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER)
target_sources(audiocpp_server PRIVATE app/server/model_installer.cpp)
target_link_libraries(audiocpp_server PRIVATE audiocpp_package_manager)
Expand Down
25 changes: 22 additions & 3 deletions app/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,24 @@ Pick the mode that matches the behavior you want:
| Standalone deployed binary without local `model_specs/` | `-DAUDIOCPP_DEPLOYMENT_BUILD=ON` | `audiocpp_server --config server.json` | Binary carries compiled package specs for fallback model-spec lookup. |
| Offline/reproducible native-manager build | `-DAUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON -DAUDIOCPP_BORINGSSL_ARCHIVE=/path/to/boringssl.tar.gz` | `audiocpp_server --ui --ui-management --backend <backend>` | Configure does not fetch BoringSSL from the network. |
| Distro-packaged TLS instead of bundled BoringSSL | `-DAUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON -DAUDIOCPP_USE_SYSTEM_OPENSSL=ON` | `audiocpp_server --ui --ui-management --backend <backend>` | Uses system OpenSSL; useful for packagers. |
| Optional in-process frontend pipeline | `-DAUDIOCPP_BUILD_SERVER_FRONTENDS=ON -DAUDIOCPP_SERVER_FRONTEND_MODULES="audio_decode;mp3_encode"` | `audiocpp_server --config server.json` | Adds compiled-in pre/post processing modules around the stable core API. `audio_decode` accepts MP3/FLAC transcription input through miniaudio; `mp3_encode` returns `response_format=mp3` speech output through libmp3lame. The default server build includes none of these modules or dependencies. |
| Optional HTTPS frontend listener | `-DAUDIOCPP_BUILD_SERVER_FRONTENDS=ON -DAUDIOCPP_SERVER_FRONTEND_MODULES=https` | `audiocpp_server --config server.json --https-cert-file cert.pem --https-key-file key.pem` | Serves the same in-process server over HTTPS through the frontend layer. The default server build does not include this TLS dependency. |

Native model management uses bundled BoringSSL by default. Normal server builds
do not build or link that HTTP/TLS dependency.

Optional frontend modules are selected at configure time with the semicolon-separated
`AUDIOCPP_SERVER_FRONTEND_MODULES` list. The server runs selected modules as an
ordered pipeline: every module gets a pre-processing pass before the core handler,
then every module gets a post-processing pass after the core handler. A module that
does not need one side leaves that method empty. Each active side declares a simple
contract over the HTTP envelope state (`method`, `path`, `request_in/request_out`
for pre-processing, or `response_in/response_out` for post-processing), and module
registration rejects incompatible adjacent transforms on the same route. Add a new
module by implementing `ServerFrontendModule`, declaring its contract, registering it
from the generated CMake registration list, and appending its source and private
dependencies in `app/server/frontends/server_frontends.cmake`.

## Config

```bash
Expand Down Expand Up @@ -105,6 +119,11 @@ Set per-model `"default_request_options"` to apply request-option defaults to ev

Set top-level `"max_request_body_bytes"` to bound the largest HTTP request body buffered in host RAM before routing. This protects endpoints that accept JSON or audio uploads from unbounded `Content-Length` claims. The default is `2147483648` bytes (2 GiB). Raise or lower it to match the largest upload your deployment intends to accept. Values above `2^53 - 1` are rejected because this config parser stores JSON numbers as doubles.

Set top-level `"https_cert_file"` and `"https_key_file"` together to serve HTTPS
when the server was built with the optional `https` frontend capability. Relative
paths are resolved from the config file directory. The equivalent command-line
options are `--https-cert-file <pem>` and `--https-key-file <pem>`.

Set top-level `"log_request_body": true` and start the server with `--log` to print full JSON request bodies for debugging. This is off by default, and both switches are required so prompt text, paths, and request options are not logged accidentally. Audio bodies are not printed; multipart uploads log filename and byte count, while raw or live/chunked audio requests log only route, content type, query, and size/stream metadata.

### Experimental CORS
Expand Down Expand Up @@ -319,7 +338,7 @@ curl http://127.0.0.1:8080/v1/audio/speech \
}'
```

Set `"response_format": "json"` to receive base64 WAV in a JSON response.
Set `"response_format": "json"` to receive base64 WAV in a JSON response. In builds configured with `-DAUDIOCPP_BUILD_SERVER_FRONTENDS=ON -DAUDIOCPP_SERVER_FRONTEND_MODULES=mp3_encode`, `"response_format": "mp3"` returns `audio/mpeg` MP3 bytes for non-streaming speech requests.

For streaming-capable TTS models configured with `mode: "streaming"`, `stream_format` follows the OpenAI speech streaming shape:

Expand All @@ -344,7 +363,7 @@ The SSE stream emits `speech.audio.delta` events with base64 PCM chunks, then `s

### `POST /v1/audio/transcriptions`

JSON transcription request using a server-local audio path.
JSON transcription request using a server-local WAV audio path.

```bash
curl http://127.0.0.1:8080/v1/audio/transcriptions \
Expand All @@ -364,7 +383,7 @@ curl http://127.0.0.1:8080/v1/audio/transcriptions \
-F file=@/path/to/input.wav
```

`file` and `model` are required; `language` is optional. Uploaded WAV bytes are decoded in memory and are not written to a temporary file.
`file` and `model` are required; `language` is optional. Uploaded WAV bytes are decoded in memory and are not written to a temporary file. In builds configured with `-DAUDIOCPP_BUILD_SERVER_FRONTENDS=ON -DAUDIOCPP_SERVER_FRONTEND_MODULES=audio_decode`, the frontend also accepts MP3 and FLAC input for this route, decodes it to a temporary WAV, and forwards that normalized request to the same core transcription handler.

For streaming-capable ASR models configured with `mode: "streaming"`, pass `stream=true` to receive OpenAI-style transcription SSE:

Expand Down
17 changes: 17 additions & 0 deletions app/server/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,23 @@ ServerConfig load_server_config(const std::filesystem::path & path) {
}
config.voice_dir = resolve_path(base, value->as_string());
}
const auto * https_cert_file = root.find("https_cert_file");
const auto * https_key_file = root.find("https_key_file");
if (https_cert_file != nullptr || https_key_file != nullptr) {
if (https_cert_file == nullptr || https_key_file == nullptr) {
throw std::runtime_error("server https_cert_file and https_key_file must be set together");
}
if (!https_cert_file->is_string()) {
throw std::runtime_error("server https_cert_file must be a string");
}
if (!https_key_file->is_string()) {
throw std::runtime_error("server https_key_file must be a string");
}
config.https = ServerFrontendHttpsConfig{
resolve_path(base, https_cert_file->as_string()),
resolve_path(base, https_key_file->as_string()),
};
}
if (config.port <= 0 || config.port > 65535) {
throw std::runtime_error("server port must be in 1..65535");
}
Expand Down
2 changes: 2 additions & 0 deletions app/server/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "engine/framework/core/backend.h"

#include "frontend.h"
#include "http.h"

namespace minitts::server {
Expand Down Expand Up @@ -105,6 +106,7 @@ struct ServerConfig {
// Fleet-wide bounds for incrementally delivered request bodies. The defaults are
// in LiveIngestLimits; a model entry may override any subset of them.
LiveIngestLimits live_ingest;
std::optional<ServerFrontendHttpsConfig> https;
std::optional<std::filesystem::path> model_spec_override;
// Voice library shared across all TTS models: *.wav files plus a `prompt_text`
// mapping file (<basename>|<transcript>). A request `voice` name that is not a
Expand Down
156 changes: 156 additions & 0 deletions app/server/frontend.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#include "frontend.h"

#include <stdexcept>
#include <string>
#include <utility>

namespace minitts::server {

namespace {

bool contract_value_matches(std::string_view lhs, std::string_view rhs) {
return lhs == frontend_contracts::any || rhs == frontend_contracts::any || lhs == rhs;
}

bool contract_route_overlaps(std::string_view lhs, std::string_view rhs) {
return contract_value_matches(lhs, rhs);
}

bool validate_pre_contracts(
std::string_view previous_module,
const FrontendPreContract & previous,
std::string_view next_module,
const FrontendPreContract & next) {
if (!contract_route_overlaps(previous.method, next.method) ||
!contract_route_overlaps(previous.path, next.path)) {
return false;
}
if (!contract_value_matches(previous.request_out, next.request_in)) {
throw std::runtime_error(
"incompatible frontend pre-processing order: " + std::string(previous_module) +
" outputs request state '" + std::string(previous.request_out) +
"', but " + std::string(next_module) +
" expects '" + std::string(next.request_in) + "'");
}
return true;
}

bool validate_post_contracts(
std::string_view previous_module,
const FrontendPostContract & previous,
std::string_view next_module,
const FrontendPostContract & next) {
if (!contract_route_overlaps(previous.method, next.method) ||
!contract_route_overlaps(previous.path, next.path)) {
return false;
}
if (!contract_value_matches(previous.response_out, next.response_in)) {
throw std::runtime_error(
"incompatible frontend post-processing order: " + std::string(previous_module) +
" outputs response state '" + std::string(previous.response_out) +
"', but " + std::string(next_module) +
" expects '" + std::string(next.response_in) + "'");
}
return true;
}

} // namespace

#include "server_frontend_module_declarations.inc"

std::optional<FrontendPreContract> ServerFrontendModule::pre_contract() const {
return std::nullopt;
}

std::optional<FrontendPostContract> ServerFrontendModule::post_contract() const {
return std::nullopt;
}

void ServerFrontendModule::pre_process(ServerFrontendContext & context, ServerFrontendRequest & request) {
(void) context;
(void) request;
}

void ServerFrontendModule::post_process(ServerFrontendContext & context, ServerFrontendResponse & response) {
(void) context;
(void) response;
}

void ServerFrontendRegistry::add(ServerFrontendModuleFactory factory) {
if (factory == nullptr) {
throw std::runtime_error("server frontend registration requires a module factory");
}
auto module = factory();
if (!module) {
throw std::runtime_error("server frontend module factory returned null");
}

const auto pre = module->pre_contract();
const auto post = module->post_contract();
for (auto it = modules_.rbegin(); it != modules_.rend(); ++it) {
if (pre.has_value()) {
if (const auto previous = (*it)->pre_contract()) {
if (validate_pre_contracts((*it)->name(), *previous, module->name(), *pre)) {
break;
}
}
}
}
for (auto it = modules_.rbegin(); it != modules_.rend(); ++it) {
if (post.has_value()) {
if (const auto previous = (*it)->post_contract()) {
if (validate_post_contracts((*it)->name(), *previous, module->name(), *post)) {
break;
}
}
}
}

modules_.push_back(std::move(module));
}

bool ServerFrontendRegistry::empty() const { return modules_.empty(); }

HttpResponse ServerFrontendRegistry::handle(ServerFrontendContext & context, const HttpRequest & request) const {
ServerFrontendRequest frontend_request{request, std::nullopt};
for (const auto & module : modules_) {
module->pre_process(context, frontend_request);
if (frontend_request.response.has_value()) {
return std::move(*frontend_request.response);
}
}

auto core_response = context.forward_to_core(frontend_request.request);
ServerFrontendResponse frontend_response{request, frontend_request.request, std::move(core_response)};
for (const auto & module : modules_) {
module->post_process(context, frontend_response);
}
return std::move(frontend_response.response);
}

void register_static_server_frontends(ServerFrontendRegistry & registry) {
(void) registry;
#include "server_frontend_module_registrations.inc"
}

#if !defined(AUDIOCPP_SERVER_FRONTEND_HAS_HTTPS)
void serve_frontend_https(
const std::string & host,
int port,
IHttpHandler & handler,
ShutdownRequested shutdown_requested,
uint64_t max_request_body_bytes,
const ServerFrontendHttpsConfig & config) {
(void) host;
(void) port;
(void) handler;
(void) shutdown_requested;
(void) max_request_body_bytes;
(void) config;
throw std::runtime_error(
"HTTPS frontend support is not available in this build; configure with "
"-DAUDIOCPP_BUILD_SERVER_FRONTENDS=ON -DAUDIOCPP_SERVER_FRONTEND_MODULES=https");
}
#endif

} // namespace minitts::server
Loading
Loading