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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ 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)
Expand Down Expand Up @@ -1777,6 +1778,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 +1791,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
19 changes: 16 additions & 3 deletions app/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ 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. |

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 @@ -319,7 +332,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 +357,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 +377,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
135 changes: 135 additions & 0 deletions app/server/frontend.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#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) {
#include "server_frontend_module_registrations.inc"
}

} // namespace minitts::server
83 changes: 83 additions & 0 deletions app/server/frontend.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#pragma once

#include "http.h"

#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string_view>
#include <vector>

namespace minitts::server {

class ServerFrontendContext {
public:
virtual ~ServerFrontendContext() = default;

virtual HttpResponse forward_to_core(const HttpRequest & request) = 0;
virtual std::filesystem::path resolve_request_path(const std::filesystem::path & path) const = 0;
virtual std::filesystem::path make_frontend_temp_path(std::string_view filename) = 0;
};

struct ServerFrontendRequest {
HttpRequest request;
std::optional<HttpResponse> response;
};

struct ServerFrontendResponse {
const HttpRequest & original_request;
const HttpRequest & core_request;
HttpResponse response;
};

struct FrontendPreContract {
std::string_view method;
std::string_view path;
std::string_view request_in;
std::string_view request_out;
};

struct FrontendPostContract {
std::string_view method;
std::string_view path;
std::string_view response_in;
std::string_view response_out;
};

namespace frontend_contracts {
inline constexpr std::string_view any = "*";
inline constexpr std::string_view client_encoded_audio_request = "client_encoded_audio_request";
inline constexpr std::string_view core_wav_audio_request = "core_wav_audio_request";
inline constexpr std::string_view client_mp3_speech_request = "client_mp3_speech_request";
inline constexpr std::string_view core_wav_speech_request = "core_wav_speech_request";
inline constexpr std::string_view core_wav_speech_response = "core_wav_speech_response";
inline constexpr std::string_view client_mp3_speech_response = "client_mp3_speech_response";
} // namespace frontend_contracts

class ServerFrontendModule {
public:
virtual ~ServerFrontendModule() = default;

virtual std::string_view name() const = 0;
virtual std::optional<FrontendPreContract> pre_contract() const;
virtual std::optional<FrontendPostContract> post_contract() const;
virtual void pre_process(ServerFrontendContext & context, ServerFrontendRequest & request);
virtual void post_process(ServerFrontendContext & context, ServerFrontendResponse & response);
};

using ServerFrontendModuleFactory = std::unique_ptr<ServerFrontendModule> (*)();

class ServerFrontendRegistry {
public:
void add(ServerFrontendModuleFactory factory);
bool empty() const;
HttpResponse handle(ServerFrontendContext & context, const HttpRequest & request) const;

private:
std::vector<std::unique_ptr<ServerFrontendModule>> modules_;
};

void register_static_server_frontends(ServerFrontendRegistry & registry);

} // namespace minitts::server
Loading
Loading