Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ __pycache__/
!/webui/logs/.gitkeep
/webui/third_party/
/webui/llm_api_key.txt
/webui/native/node_modules/
/webui/native/.svelte-kit/
/webui/native/dist/*
!/webui/native/dist/index.html
# written by the in-UI language picker; per-machine, not a project setting
/webui/configs/ui_language.json
# personal voice recording — stays local, repo is public
Expand Down
30 changes: 30 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1229,19 +1229,37 @@ if (ENGINE_ENABLE_OPENMP)
target_link_libraries(audiocpp_cli PRIVATE OpenMP::OpenMP_CXX)
endif()

set(AUDIOCPP_UI_DIST "${CMAKE_CURRENT_SOURCE_DIR}/webui/native/dist/index.html")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${AUDIOCPP_UI_DIST}")
if (EXISTS "${AUDIOCPP_UI_DIST}")
file(READ "${AUDIOCPP_UI_DIST}" AUDIOCPP_UI_HEX HEX)
else()
string(HEX "<!doctype html><title>audio.cpp</title><p>Embedded WebUI assets are unavailable.</p>" AUDIOCPP_UI_HEX)
endif()
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," AUDIOCPP_UI_BYTES "${AUDIOCPP_UI_HEX}")
string(REPLACE "," ",\n" AUDIOCPP_UI_BYTES "${AUDIOCPP_UI_BYTES}")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/app/server/audiocpp_ui_asset.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/generated/audiocpp_ui_asset.h"
@ONLY
)

add_executable(audiocpp_server
app/server/main.cpp
app/server/config.cpp
app/server/http.cpp
app/server/model_installer.cpp
app/server/multipart.cpp
app/server/runtime.cpp
app/server/ui_assets.cpp
app/cli/args.cpp
app/cli/request.cpp
app/streaming/pcm_source.cpp
app/streaming/streaming.cpp
)

target_link_libraries(audiocpp_server PRIVATE engine_runtime ggml)
target_include_directories(audiocpp_server PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
if (WIN32)
target_link_libraries(audiocpp_server PRIVATE ws2_32)
endif()
Expand Down Expand Up @@ -2028,6 +2046,18 @@ if (ENGINE_BUILD_TESTS)
if (ENGINE_ENABLE_OPENMP)
target_link_libraries(parakeet_parity_dump PRIVATE OpenMP::OpenMP_CXX)
endif()

add_executable(server_model_installer_test
tests/unittests/test_server_model_installer.cpp
app/server/model_installer.cpp
)
target_include_directories(server_model_installer_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/app/server)
target_link_libraries(server_model_installer_test PRIVATE Threads::Threads)

add_test(
NAME server_model_installer_test
COMMAND server_model_installer_test
)
endif()

if (ENGINE_BUILD_EXAMPLES)
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,24 @@ package notes.
## WebUI
![Maintained by contributors](https://img.shields.io/badge/maintained%20by-contributors-brightgreen)

audio.cpp includes a Gradio WebUI for trying local models from the browser, managing downloads, and running common TTS/ASR/audio workflows without writing CLI commands.
`audiocpp_server` includes an embedded SvelteKit/TypeScript WebUI for running local TTS, cloning, ASR,
generation, conversion, separation, VAD, diarization, and alignment workflows. The production UI is compiled
into the server binary, so using it requires neither Python nor separate frontend files:

The WebUI lives in [webui/](webui/). See [webui/README.md](webui/README.md) for setup, launch commands, and model-download notes.
```bash
audiocpp_server --ui --backend cuda
```

Open `http://127.0.0.1:8080`. Starting with `--ui` and no server config enables on-demand model
load/unload and temporary browser uploads. Existing static server configurations also expose the UI by default;
add `--ui-management` when that instance should permit model switching.

The native UI also exposes background model download/preparation, long-text split-and-merge synthesis, a
browser-local saved voice library, microphone recording, and near-live ASR input. Some model preparation jobs invoke
the repository's Python model manager because those packages require Hugging Face download or checkpoint conversion;
model inference and the embedded UI remain Python-free. The previous Python/Gradio interface remains available for
compatibility. See [webui/README.md](webui/README.md) for native and legacy launch commands, model notes, and frontend
development instructions.

Huge thanks to [@kigner](https://github.com/kigner) for the original [audio.cpp-webui](https://github.com/kigner/audio.cpp-webui), and to [@patrickjchen](https://github.com/patrickjchen) for porting and integrating it into audio.cpp.

Expand Down
12 changes: 12 additions & 0 deletions app/server/audiocpp_ui_asset.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include <cstddef>

namespace minitts::server {

inline constexpr unsigned char kAudioCppUiHtml[] = {
@AUDIOCPP_UI_BYTES@
};
inline constexpr std::size_t kAudioCppUiHtmlSize = sizeof(kAudioCppUiHtml);

} // namespace minitts::server
9 changes: 7 additions & 2 deletions app/server/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ ServerConfig load_server_config(const std::filesystem::path & path) {
config.host = engine::io::json::optional_string(root, "host", config.host);
config.port = engine::io::json::optional_i32(root, "port", config.port);
config.cors_origins = engine::io::json::optional_string(root, "cors_origins", config.cors_origins);
config.ui_enabled = engine::io::json::optional_bool(root, "ui", config.ui_enabled);
config.ui_management = engine::io::json::optional_bool(root, "ui_management", config.ui_management);
config.backend = parse_server_backend(engine::io::json::optional_string(root, "backend", "cuda"));
config.device = engine::io::json::optional_i32(root, "device", config.device);
config.threads = engine::io::json::optional_i32(root, "threads", config.threads);
Expand All @@ -249,8 +251,11 @@ ServerConfig load_server_config(const std::filesystem::path & path) {
}

const auto * models = root.find("models");
if (models == nullptr || !models->is_array() || models->as_array().empty()) {
throw std::runtime_error("server config requires a non-empty models array");
if (models == nullptr || !models->is_array()) {
throw std::runtime_error("server config requires a models array");
}
if (models->as_array().empty() && !config.ui_management) {
throw std::runtime_error("server config requires a non-empty models array unless ui_management is enabled");
}
for (const auto & item : models->as_array()) {
ServerModelConfig model;
Expand Down
2 changes: 2 additions & 0 deletions app/server/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ struct ServerConfig {
std::string host = "127.0.0.1";
int port = 8080;
std::string cors_origins = "";
bool ui_enabled = true;
bool ui_management = false;
engine::core::BackendType backend = engine::core::BackendType::Cuda;
int device = 0;
int threads = 1;
Expand Down
6 changes: 6 additions & 0 deletions app/server/http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,18 @@ const char * status_text(int status) noexcept {
switch (status) {
case 200:
return "OK";
case 204:
return "No Content";
case 400:
return "Bad Request";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 413:
return "Payload Too Large";
case 500:
return "Internal Server Error";
case 503:
Expand Down
60 changes: 55 additions & 5 deletions app/server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,50 @@ bool has_arg(int argc, char ** argv, const std::string & name) {
return false;
}

std::filesystem::path executable_directory(const char * argv0) {
if (argv0 == nullptr || *argv0 == '\0') {
return std::filesystem::current_path();
}
std::error_code ec;
auto path = std::filesystem::absolute(std::filesystem::path(argv0), ec);
if (ec) {
return std::filesystem::current_path();
}
path = path.lexically_normal();
if (std::filesystem::is_regular_file(path, ec)) {
return path.parent_path();
}
return std::filesystem::current_path();
}

void print_help() {
std::cout
<< "audiocpp_server --config <server.json> [--host <ip>] [--port <port>] [--backend <backend>]\n"
<< "audiocpp_server [--config <server.json>] [--ui] [--host <ip>] [--port <port>] [--backend <backend>]\n"
<< " [--device <id>] [--threads <n>] [--busy-timeout-ms <ms>]\n"
<< " [--model-spec-override <json-or-directory>]\n"
<< " [--log] [--log-file <path>]\n"
<< " [--cors-origins <origins>]\n"
<< " --ui serve the embedded WebUI; without --config, start\n"
<< " as a native model-management host\n"
<< " --no-ui disable the embedded WebUI\n"
<< " --ui-management allow WebUI model load/unload and temporary uploads\n"
<< " --backend cpu|cuda|hip|rocm|vulkan|metal default cuda (rocm is an alias for hip)\n"
<< " --busy-timeout-ms <ms> fail a request with 503 when the model has been\n"
<< " busy this long; default 300000, 0 disables\n"
<< " --cors-origins \"*\" experimental; disabled by default. Allows browser\n"
<< " requests from any origin for trusted local demos only\n"
<< "\n"
<< "Endpoints:\n"
<< " GET / embedded WebUI (enabled by default with a config)\n"
<< " GET /health\n"
<< " GET /v1/models\n"
<< " POST /v1/models/load available with --ui-management\n"
<< " POST /v1/models/unload available with --ui-management\n"
<< " POST /v1/ui/upload available with --ui-management\n"
<< " POST /v1/ui/models/install background package download/preparation\n"
<< " POST /v1/ui/models/delete remove one installed package precision\n"
<< " GET /v1/ui/models/install-status[?id=<package>]\n"
<< " GET /v1/ui/models/package-sizes package sizes from metadata-only checks\n"
<< " GET /v1/audio/voices?model=<id>\n"
<< " POST /v1/audio/speech\n"
<< " POST /v1/audio/transcriptions\n"
Expand All @@ -75,8 +103,9 @@ int main(int argc, char ** argv) {
return 0;
}
const auto config_path = arg_value(argc, argv, "--config");
if (!config_path.has_value()) {
throw std::runtime_error("missing required --config argument");
const bool ui_requested = has_arg(argc, argv, "--ui");
if (!config_path.has_value() && !ui_requested) {
throw std::runtime_error("missing required --config argument (or use --ui for the native WebUI)");
}
const auto log_file = arg_value(argc, argv, "--log-file");
engine::debug::configure_logging(engine::debug::LoggingConfig{
Expand All @@ -94,7 +123,22 @@ int main(int argc, char ** argv) {
std::signal(SIGPIPE, SIG_IGN);
#endif

auto config = minitts::server::load_server_config(*config_path);
auto config = config_path.has_value()
? minitts::server::load_server_config(*config_path)
: minitts::server::ServerConfig{};
if (!config_path.has_value()) {
config.ui_management = true;
config.lazy_load = true;
}
if (ui_requested) {
config.ui_enabled = true;
}
if (has_arg(argc, argv, "--no-ui")) {
config.ui_enabled = false;
}
if (has_arg(argc, argv, "--ui-management")) {
config.ui_management = true;
}
if (const auto host = arg_value(argc, argv, "--host")) {
config.host = *host;
}
Expand Down Expand Up @@ -129,7 +173,13 @@ int main(int argc, char ** argv) {
throw std::runtime_error("--busy-timeout-ms must be >= 0 (0 disables the guard)");
}

minitts::server::ServerState state(config, std::filesystem::current_path());
const auto ui_resource_anchor = !config_path.has_value()
? executable_directory(argc > 0 ? argv[0] : nullptr)
: std::filesystem::path{};
minitts::server::ServerState state(
config,
std::filesystem::current_path(),
ui_resource_anchor);
minitts::server::serve_http(config.host, config.port, state, shutdown_requested, config.max_request_body_bytes);
return 0;
} catch (const std::exception & ex) {
Expand Down
Loading
Loading