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
23 changes: 23 additions & 0 deletions app/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,29 @@ match the config's, the model keeps the config's `default_request_options`,
`voice_presets`, `default_voice_preset`, `busy_timeout_ms` and `live_ingest`;
a load of another family or task under that `id` gets none of them.

### Loading an installed package by id

A server built with `AUDIOCPP_BUILD_NATIVE_MODEL_MANAGER=ON` also lets
`POST /v1/models/load` name an installed package with `package_id` instead of
`path`, so a client does not need to know where the models root is:

```bash
curl -X POST http://127.0.0.1:8080/v1/models/load \
-H 'Content-Type: application/json' \
-d '{"id":"sep","package_id":"htdemucs_q8_0","task":"sep"}'
```

The server reads the package's metadata from the package manager and asks the
family's loader, through `resolve_package_load`, for the path, family, load
options and session options, the same target the WebUI loads. The request then
goes through the existing load handler as if it had sent them. `family`
defaults to the package's family; a different `family` is rejected. A
`load_options` or `session_options` key the request sends wins over the
resolved one. Each precision is its own package id, such as `kokoro_82m_q8_0`
and `kokoro_82m_bf16`. Sending both `path` and `package_id` is rejected with
400, and an unknown or not-installed package returns 404. Requests with
`path`, and servers built without the native model manager, are unchanged.

### Paths and model specs

Relative model and configured voice-library paths resolve against the config
Expand Down
3 changes: 2 additions & 1 deletion app/server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ void print_help() {
<< " GET /health\n"
<< " GET /v1/models\n"
<< " GET /v1/recipes\n"
<< " POST /v1/models/load available with --ui-management\n"
<< " POST /v1/models/load available with --ui-management; path, or\n"
<< " package_id with the native model manager\n"
<< " POST /v1/models/unload available with --ui-management\n"
<< " POST /v1/ui/upload temporary browser upload for WebUI requests\n"
<< " POST /v1/ui/models/install background package download/preparation\n"
Expand Down
8 changes: 8 additions & 0 deletions app/server/model_installer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,14 @@ std::string ModelInstaller::clean_partial(const std::string & package_id) {
json_quote(message.empty() ? "Partial download files cleaned" : message) + "}";
}

std::optional<engine::package_manager::PackageInfo> ModelInstaller::package_info(
const std::string & package_id) const {
if (!valid_package_id(package_id)) {
return std::nullopt;
}
return state_->native_manager->package_info(package_id);
}

std::string ModelInstaller::remove(const std::string & package_id) {
if (!valid_package_id(package_id)) {
throw std::runtime_error("invalid model-manager package id");
Expand Down
4 changes: 4 additions & 0 deletions app/server/model_installer.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#pragma once

#include "engine/framework/package_manager/manager.h"

#include <filesystem>
#include <memory>
#include <optional>
#include <string>

namespace minitts::server {
Expand Down Expand Up @@ -30,6 +33,7 @@ class ModelInstaller {
std::string stop(const std::string & package_id);
std::string clean_partial(const std::string & package_id);
std::string remove(const std::string & package_id);
std::optional<engine::package_manager::PackageInfo> package_info(const std::string & package_id) const;
bool has_active_jobs() const;

private:
Expand Down
30 changes: 30 additions & 0 deletions app/server/parallel_runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,36 @@ HttpResponse ParallelServerState::handle_ui_upload(const HttpRequest & request)
}

#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
// Resolves a "package_id" to the installed package's load target before
// handing the request to handle_model_load, which only ever sees a path.
// Path-based loads never touch the installer or its mutex.
HttpResponse ParallelServerState::handle_package_model_load(const std::string & body_text) {
if (!config_.ui_management) {
return handle_model_load(body_text);
}
const auto body = engine::io::json::parse(body_text);
std::string package_id;
if (auto error = ui_model_load_package_id(body, package_id)) {
return *error;
}
if (package_id.empty()) {
return handle_model_load(body_text);
}
std::optional<engine::package_manager::PackageInfo> info;
{
std::lock_guard<std::mutex> lock(model_installer_mutex_);
if (!model_installer_) {
return error_response(403, "UI model management is disabled", "forbidden");
}
info = model_installer_->package_info(package_id);
}
std::string resolved_body;
if (auto error = resolve_ui_package_model_load(body, package_id, info, resolved_body)) {
return *error;
}
return handle_model_load(resolved_body);
}

HttpResponse ParallelServerState::handle_model_install(const std::string & body_text) {
if (!config_.ui_management) {
return error_response(403, "UI model installation is disabled", "forbidden");
Expand Down
1 change: 1 addition & 0 deletions app/server/parallel_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class ParallelServerState final : public IHttpHandler, public ServerFrontendCont
HttpResponse handle_path_status(const std::string & body_text) const;
HttpResponse handle_ui_upload(const HttpRequest & request);
#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
HttpResponse handle_package_model_load(const std::string & body_text);
HttpResponse handle_model_install(const std::string & body_text);
HttpResponse handle_model_install_stop(const std::string & body_text);
HttpResponse handle_model_clean_partial(const std::string & body_text);
Expand Down
4 changes: 4 additions & 0 deletions app/server/request_dispatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ struct RequestDispatcher {
response = state.handle_ui_voice_preview(request);
} else if (request.method == "POST" &&
request.path == "/v1/models/load") {
#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
response = state.handle_package_model_load(request.body);
#else
response = state.handle_model_load(request.body);
#endif
} else if (request.method == "POST" &&
request.path == "/v1/models/unload") {
response = state.handle_model_unload(request.body);
Expand Down
30 changes: 30 additions & 0 deletions app/server/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,36 @@ HttpResponse ServerState::handle_ui_upload(const HttpRequest & request) {
}

#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
// Resolves a "package_id" to the installed package's load target before
// handing the request to handle_model_load, which only ever sees a path.
// Path-based loads never touch the installer or its mutex.
HttpResponse ServerState::handle_package_model_load(const std::string & body_text) {
if (!config_.ui_management) {
return handle_model_load(body_text);
}
const auto body = engine::io::json::parse(body_text);
std::string package_id;
if (auto error = ui_model_load_package_id(body, package_id)) {
return *error;
}
if (package_id.empty()) {
return handle_model_load(body_text);
}
std::optional<engine::package_manager::PackageInfo> info;
{
std::lock_guard<std::mutex> lock(model_installer_mutex_);
if (!model_installer_) {
return error_response(403, "UI model management is disabled", "forbidden");
}
info = model_installer_->package_info(package_id);
}
std::string resolved_body;
if (auto error = resolve_ui_package_model_load(body, package_id, info, resolved_body)) {
return *error;
}
return handle_model_load(resolved_body);
}

HttpResponse ServerState::handle_model_install(const std::string & body_text) {
if (!config_.ui_management) {
return error_response(403, "UI model installation is disabled", "forbidden");
Expand Down
1 change: 1 addition & 0 deletions app/server/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class ServerState final : public IHttpHandler, public ServerFrontendContext {
HttpResponse handle_path_status(const std::string & body_text) const;
HttpResponse handle_ui_upload(const HttpRequest & request);
#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
HttpResponse handle_package_model_load(const std::string & body_text);
HttpResponse handle_model_install(const std::string & body_text);
HttpResponse handle_model_install_stop(const std::string & body_text);
HttpResponse handle_model_clean_partial(const std::string & body_text);
Expand Down
71 changes: 71 additions & 0 deletions app/server/server_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,77 @@ HttpResponse ui_model_package_sizes_response(ModelInstaller &installer) {
return json_response(json::stringify(json::Value::make_object(std::move(inventory))));
}

std::optional<HttpResponse> ui_model_load_package_id(
const engine::io::json::Value &body,
std::string &package_id) {
package_id.clear();
const auto *package_value = body.find("package_id");
if (package_value == nullptr) {
return std::nullopt;
}
if (!package_value->is_string() || package_value->as_string().empty()) {
return error_response(400, "package_id must be a non-empty string", "invalid_request_error");
}
if (body.find("path") != nullptr) {
return error_response(400, "send either path or package_id, not both", "invalid_request_error");
}
package_id = package_value->as_string();
return std::nullopt;
}

std::optional<HttpResponse> resolve_ui_package_model_load(
const engine::io::json::Value &body,
const std::string &package_id,
const std::optional<engine::package_manager::PackageInfo> &info,
std::string &resolved_body) {
namespace json = engine::io::json;
if (!info) {
return error_response(404, "unknown package id: " + package_id, "not_found");
}
if (!info->installed) {
return error_response(404, "package is not installed: " + package_id, "not_found");
}
engine::runtime::PackageLoadTarget target;
try {
target = engine::runtime::make_default_registry().resolve_package_load(info->family, info->package);
} catch (const std::exception &error) {
return error_response(400, "package " + package_id + " cannot be loaded: " + error.what(),
"invalid_request_error");
}
auto fields = body.as_object();
if (const auto found = fields.find("family"); found == fields.end()) {
fields["family"] = json::Value::make_string(target.family);
} else if (!found->second.is_string() || found->second.as_string() != target.family) {
return error_response(400,
"package " + package_id + " is for family " + target.family,
"invalid_request_error");
}
for (const char *key : {"load_options", "session_options"}) {
const auto *options = body.find(key);
if (options && !options->is_object() && !options->is_null()) {
return error_response(400, std::string(key) + " must be an object or null", "invalid_request_error");
}
}
// Resolved options fill in what the request leaves out; keys it sends win.
const auto merge_options = [&](const char *key, const std::unordered_map<std::string, std::string> &resolved) {
if (resolved.empty()) return;
json::Value::Object options;
if (const auto found = fields.find(key); found != fields.end() && found->second.is_object()) {
options = found->second.as_object();
}
for (const auto &option : resolved) {
options.emplace(option.first, json::Value::make_string(option.second));
}
fields[key] = json::Value::make_object(std::move(options));
};
merge_options("load_options", target.load_options);
merge_options("session_options", target.session_options);
fields.erase("package_id");
fields["path"] = json::Value::make_string(target.model_path.string());
resolved_body = json::stringify(json::Value::make_object(std::move(fields)));
return std::nullopt;
}

HttpResponse ui_models_root_response(
const std::filesystem::path &models_root,
const std::filesystem::path &default_models_root) {
Expand Down
19 changes: 19 additions & 0 deletions app/server/server_ui.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#pragma once

#include "http.h"
#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
#include "engine/framework/io/json.h"
#include "engine/framework/package_manager/manager.h"
#endif

#include <filesystem>
#include <functional>
Expand Down Expand Up @@ -58,6 +62,21 @@ HttpResponse ui_model_install_status_response(
const HttpRequest &request,
const ModelInstaller &installer);
HttpResponse ui_model_package_sizes_response(ModelInstaller &installer);
// POST /v1/models/load may name an installed package by "package_id" instead
// of "path". Sets package_id to the requested id, or leaves it empty for a
// path-based request; returns an error response for a malformed package_id.
std::optional<HttpResponse> ui_model_load_package_id(
const engine::io::json::Value &body,
std::string &package_id);
// Rewrites a package_id load into the path-based request the loader already
// takes, using the model-owned resolve_package_load for the path, family, load
// options and session options. Returns the error response instead when the
// package cannot be loaded by id.
std::optional<HttpResponse> resolve_ui_package_model_load(
const engine::io::json::Value &body,
const std::string &package_id,
const std::optional<engine::package_manager::PackageInfo> &info,
std::string &resolved_body);
HttpResponse ui_models_root_response(
const std::filesystem::path &models_root,
const std::filesystem::path &default_models_root);
Expand Down
Loading