voice_audio_multi/app/server/request_dispatch.h
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

199 lines
9.5 KiB
C++

#pragma once
#include "busy_guard.h"
#include "model_memory.h"
#include "server_protocol.h"
#include "engine/framework/debug/trace.h"
#include "engine/framework/runtime/errors.h"
#include <functional>
#include <optional>
#include <string>
#include <utility>
namespace minitts::server {
// Shares HTTP protocol dispatch without coupling the legacy and parallel
// runtime state or lifecycle implementations.
struct RequestDispatcher {
template <typename State>
static HttpResponse handle(State &state, const HttpRequest &request,
bool use_frontends,
const std::function<std::optional<HttpResponse>(const HttpRequest &)> &
additional_routes = {}) {
HttpResponse response;
const std::string allowed_origin = state.get_allowed_origin(request);
try {
log_request_body_if_enabled(state.config_, request);
if (request.method == "OPTIONS" &&
(!allowed_origin.empty() || state.config_.ui_enabled)) {
response.status = 204;
response.content_type = "text/plain";
response.headers["Access-Control-Allow-Headers"] = "*";
response.headers["Access-Control-Allow-Methods"] = "GET, POST";
} else if (use_frontends && !state.frontends_.empty()) {
response = state.frontends_.handle(state, request);
} else if (request.method == "GET" &&
(request.path == "/" || request.path == "/index.html")) {
response = state.handle_ui_asset();
} else if (request.method == "GET" && state.config_.ui_enabled &&
(request.path == "/sw.js" ||
request.path == "/service-worker.js")) {
response = ui_service_worker_retirement_response();
} else if (request.method == "GET" && request.path == "/favicon.ico") {
response.status = 204;
response.content_type = "image/x-icon";
} else if (request.method == "GET" && request.path == "/health") {
response = state.handle_health();
} else if (request.method == "GET" && request.path == "/v1/models") {
const auto include_session_options =
query_param(request.query, "include_session_options");
const auto include_params =
query_param(request.query, "include_params");
response = json_response(state.models_json(
include_session_options == "true" || include_session_options == "1",
include_params == "true" || include_params == "1"));
} else if (request.method == "GET" &&
request.path == "/v1/audio/voices") {
response = state.handle_voices(request);
} else if (request.method == "GET" &&
request.path == "/v1/ui/voice-preview") {
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);
} else if (request.method == "POST" &&
request.path == "/v1/ui/path-status") {
response = state.handle_path_status(request.body);
} else if (request.method == "POST" && request.path == "/v1/ui/upload") {
response = state.handle_ui_upload(request);
}
#if defined(AUDIOCPP_HAS_NATIVE_MODEL_MANAGER)
else if (request.method == "GET" &&
request.path == "/v1/ui/models-root") {
response = state.handle_models_root_get();
} else if (request.method == "POST" &&
request.path == "/v1/ui/models-root") {
response = state.handle_models_root_set(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/ui/browse-directories") {
response = state.handle_directory_browser(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/ui/models/install") {
response = state.handle_model_install(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/ui/models/install/stop") {
response = state.handle_model_install_stop(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/ui/models/clean-partial") {
response = state.handle_model_clean_partial(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/ui/models/delete") {
response = state.handle_model_remove(request.body);
} else if (request.method == "GET" &&
request.path == "/v1/ui/models/install-status") {
response = state.handle_model_install_status(request);
} else if (request.method == "GET" &&
request.path == "/v1/ui/models/package-sizes") {
response = state.handle_model_package_sizes();
}
#endif
else if (request.method == "POST" && request.path == "/v1/audio/speech") {
response = state.handle_speech(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/audio/speech/live") {
response = state.handle_speech_live(request);
} else if (request.method == "POST" &&
request.path == "/v1/audio/transcriptions") {
response = state.handle_transcription(request);
}
// Same request shape as the route above, opt-in richer response: the
// models that align words or separate speakers report them through fields
// the transcription response drops. A separate path rather than a flag
// keeps the existing response schema fixed for every client already built
// against it.
else if (request.method == "POST" &&
request.path == "/v1/audio/transcriptions/details") {
response = state.handle_transcription(request, /*detail=*/true);
} else if (request.method == "POST" &&
request.path == "/v1/batches/transcriptions") {
response = state.handle_batch_transcriptions(request);
} else if (request.method == "POST" &&
request.path == "/v1/audio/alignments") {
response = state.handle_alignment(request);
}
// Separate path rather than a flag on the endpoint above: the input
// transport differs (raw chunked PCM vs a complete upload), so keeping it
// distinct leaves every existing transcription client untouched.
else if (request.method == "POST" &&
request.path == "/v1/audio/transcriptions/live") {
response = state.handle_transcription_live(request);
} else if (request.method == "POST" && request.path == "/v1/tasks/run") {
response = state.handle_generic_run(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/tasks/batch") {
response = state.handle_generic_batch(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/tasks/stream") {
response = state.handle_generic_stream(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/tasks/unload_models") {
response = state.handle_unload_models(request.body);
} else if (request.method == "POST" &&
request.path == "/v1/tasks/unload_all_models") {
response = state.handle_unload_all_models();
} else {
auto additional = additional_routes ? additional_routes(request) : std::nullopt;
if (additional) {
response = std::move(*additional);
} else {
response = error_response(404, "unknown endpoint: " + request.path,
"not_found");
}
}
} catch (const engine::runtime::CapacityError &ex) {
// The request is too big for the device, which is the caller's to fix --
// reporting it as 500 sends them looking for a server fault that is not
// there. Checked before ServerBusyError only because both are
// runtime_error; the two conditions are disjoint.
response = error_response(400, ex.what(), "invalid_request_error");
} catch (const engine::runtime::InvalidRequestError &ex) {
// A request turned away as sent, such as an input the model does not
// take: the caller's to fix as well.
response = error_response(400, ex.what(), "invalid_request_error");
} catch (const InsufficientMemoryError &ex) {
response = error_response(503, ex.what(), "insufficient_memory");
} catch (const ServerBusyError &ex) {
// Non-streaming requests surface the busy state as 503 before any
// response is sent. (Streaming requests acquire the lock inside the
// stream body, after headers are sent, so there it becomes a stream error
// event instead.)
response = error_response(503, ex.what(), "server_busy");
} catch (const std::exception &ex) {
if (allowed_origin.empty()) {
throw;
}
// Keep the transport's error response, but attach CORS before returning
// it.
if (engine::debug::log_enabled()) {
engine::debug::log_message(
std::string("[SERVER_HTTP_DEBUG] http.error ") + ex.what());
}
response = error_response(500, ex.what(), "server_error");
}
if (!allowed_origin.empty()) {
response.headers["Access-Control-Allow-Origin"] = allowed_origin;
}
return response;
}
};
} // namespace minitts::server