Improve native WebUI model management (#199) · unslothai/audio.cpp@058d2be · GitHub
Skip to content

Commit 058d2be

Browse files
mirek1900xShug0
authored andcommitted
Improve native WebUI model management (0xShug0#199)
* Improve native WebUI model management Address the follow-up feedback from PR 0xShug0#185 with cancellable package downloads, partial-download cleanup, family-level variant grouping, and Hugging Face revision tracking. Add spec-driven request controls and quick-start voices, including a self-contained PocketTTS Alba package path, while correcting RVC and strict spec-v1 request behavior. Package bundled voice presets and document the revised model-management workflow. * Complete native WebUI model management follow-up * Embed native WebUI demo voices in server Compile four renamed demo WAV files and their matching reference transcripts into audiocpp_server so the native UI no longer depends on the repository-level webui/voice directory. Materialize the assets in the per-run temporary voice library, preserve explicit voice_dir overrides, and clean extracted files during normal server shutdown. Align the quick-start selector with the embedded voice identifiers and keep visible catalog and parameter text English-only. * Add multilingual native WebUI localization
1 parent 6d93f2e commit 058d2be

33 files changed

Lines changed: 2155 additions & 300 deletions

‎CMakeLists.txt‎

Lines changed: 28 additions & 0 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
5+
namespace minitts::server {
6+
7+
inline constexpr unsigned char kAudioCppDemo1ManWav[] = {
8+
@AUDIOCPP_DEMO_1_MAN_BYTES@
9+
};
10+
inline constexpr unsigned char kAudioCppDemo2ManWav[] = {
11+
@AUDIOCPP_DEMO_2_MAN_BYTES@
12+
};
13+
inline constexpr unsigned char kAudioCppDemo3WomanWav[] = {
14+
@AUDIOCPP_DEMO_3_WOMAN_BYTES@
15+
};
16+
inline constexpr unsigned char kAudioCppDemo4WomanWav[] = {
17+
@AUDIOCPP_DEMO_4_WOMAN_BYTES@
18+
};
19+
inline constexpr unsigned char kAudioCppDemoVoicePromptText[] = {
20+
@AUDIOCPP_DEMO_PROMPT_TEXT_BYTES@
21+
};
22+
23+
} // namespace minitts::server

‎app/server/main.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ void print_help() {
8282
<< " POST /v1/models/unload available with --ui-management\n"
8383
<< " POST /v1/ui/upload available with --ui-management\n"
8484
<< " POST /v1/ui/models/install background package download/preparation\n"
85+
<< " POST /v1/ui/models/install/stop stop one active package download\n"
86+
<< " POST /v1/ui/models/clean-partial remove abandoned package staging files\n"
8587
<< " POST /v1/ui/models/delete remove one installed package precision\n"
8688
<< " GET /v1/ui/models-root current and binary-local default models folders\n"
8789
<< " POST /v1/ui/models-root select a models folder (empty path restores default)\n"

‎app/server/model_installer.cpp‎

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ struct ModelInstaller::State {
203203
std::string state = "queued";
204204
std::string message;
205205
std::filesystem::path log_path;
206+
std::filesystem::path cancel_path;
207+
bool cancel_requested = false;
206208
int exit_code = -1;
207209
int64_t started_at_ms = 0;
208210
int64_t finished_at_ms = 0;
@@ -247,7 +249,7 @@ bool ModelInstaller::has_active_jobs() const {
247249
std::lock_guard<std::mutex> lock(state_->mutex);
248250
for (const auto & [id, job] : state_->jobs) {
249251
(void) id;
250-
if (job.state == "queued" || job.state == "running") {
252+
if (job.state == "queued" || job.state == "running" || job.state == "cancelling") {
251253
return true;
252254
}
253255
}
@@ -301,37 +303,46 @@ std::string ModelInstaller::start(
301303
const auto output_file_path = resolve_optional_path(output_file);
302304

303305
const auto log_path = state_->job_root / (package_id + ".log");
306+
const auto cancel_path = state_->job_root / (package_id + ".cancel");
307+
{
308+
std::error_code error;
309+
std::filesystem::remove(cancel_path, error);
310+
}
304311
{
305312
std::lock_guard<std::mutex> lock(state_->mutex);
306313
const auto existing = state_->jobs.find(package_id);
307314
if (existing != state_->jobs.end() &&
308-
(existing->second.state == "queued" || existing->second.state == "running")) {
315+
(existing->second.state == "queued" || existing->second.state == "running" ||
316+
existing->second.state == "cancelling")) {
309317
throw std::runtime_error("installation is already running for " + package_id);
310318
}
311319
State::Job job;
312320
job.package_id = package_id;
313321
job.message = "Waiting for model preparation worker";
314322
job.log_path = log_path;
323+
job.cancel_path = cancel_path;
315324
state_->jobs[package_id] = std::move(job);
316325
}
317326

318327
const auto shared = state_;
319328
std::thread([shared, package_id, source_file_path, output_file_path, source_path, variant, overwrite,
320-
legacy_conversion, script, log_path]() {
329+
legacy_conversion, script, log_path, cancel_path]() {
321330
try {
322331
{
323332
std::lock_guard<std::mutex> lock(shared->mutex);
324333
auto & job = shared->jobs.at(package_id);
325-
job.state = "running";
326-
job.message = "Downloading and preparing model files";
334+
if (!job.cancel_requested) {
335+
job.state = "running";
336+
job.message = "Downloading and preparing model files";
337+
}
327338
job.started_at_ms = now_ms();
328339
}
329340

330341
std::string command = python_command() + " -u " + shell_quote(script.string()) +
331342
" install " + shell_quote(package_id) +
332343
" --models-root " + shell_quote(shared->models_root.string());
333344
if (!legacy_conversion) {
334-
command += " --progress";
345+
command += " --progress --cancel-file " + shell_quote(cancel_path.string());
335346
}
336347
if (overwrite) {
337348
command += " --overwrite";
@@ -356,12 +367,14 @@ std::string ModelInstaller::start(
356367
auto & job = shared->jobs.at(package_id);
357368
job.exit_code = result;
358369
job.finished_at_ms = now_ms();
359-
job.state = result == 0 ? "complete" : "failed";
370+
const bool cancelled = job.cancel_requested || std::filesystem::is_regular_file(cancel_path);
371+
job.state = cancelled ? "cancelled" : result == 0 ? "complete" : "failed";
360372
job.downloaded_bytes = log.downloaded_bytes;
361373
job.total_bytes = log.total_bytes;
362374
job.message = !log.message.empty()
363375
? log.message
364-
: (result == 0 ? "Model installation completed" : "Model installation failed");
376+
: (cancelled ? "Download stopped; completed staging files were removed"
377+
: result == 0 ? "Model installation completed" : "Model installation failed");
365378
if (result == 0) {
366379
++shared->size_generation;
367380
shared->size_state = "idle";
@@ -370,8 +383,10 @@ std::string ModelInstaller::start(
370383
} catch (const std::exception & error) {
371384
std::lock_guard<std::mutex> lock(shared->mutex);
372385
auto & job = shared->jobs.at(package_id);
373-
job.state = "failed";
374-
job.message = error.what();
386+
job.state = job.cancel_requested ? "cancelled" : "failed";
387+
job.message = job.cancel_requested
388+
? "Download stopped; completed staging files were removed"
389+
: error.what();
375390
job.exit_code = -1;
376391
job.finished_at_ms = now_ms();
377392
}
@@ -386,7 +401,7 @@ std::string ModelInstaller::status(const std::string & package_id) const {
386401
std::string message = job.message;
387402
uint64_t downloaded_bytes = job.downloaded_bytes;
388403
uint64_t total_bytes = job.total_bytes;
389-
if (job.state == "running") {
404+
if (job.state == "running" || job.state == "cancelling") {
390405
const auto log = inspect_log(job.log_path);
391406
if (!log.message.empty()) {
392407
message = log.message;
@@ -438,6 +453,80 @@ std::string ModelInstaller::status(const std::string & package_id) const {
438453
return result + "]}";
439454
}
440455

456+
std::string ModelInstaller::stop(const std::string & package_id) {
457+
if (!valid_package_id(package_id)) {
458+
throw std::runtime_error("invalid model-manager package id");
459+
}
460+
{
461+
std::lock_guard<std::mutex> lock(state_->mutex);
462+
const auto found = state_->jobs.find(package_id);
463+
if (found == state_->jobs.end()) {
464+
throw std::runtime_error("no installation job exists for " + package_id);
465+
}
466+
auto & job = found->second;
467+
if (job.state != "queued" && job.state != "running" && job.state != "cancelling") {
468+
throw std::runtime_error("installation is not running for " + package_id);
469+
}
470+
std::ofstream marker(job.cancel_path, std::ios::binary | std::ios::trunc);
471+
if (!marker) {
472+
throw std::runtime_error("could not create download cancellation marker");
473+
}
474+
marker << "cancel\n";
475+
job.cancel_requested = true;
476+
job.state = "cancelling";
477+
job.message = "Stopping download and removing its staging files";
478+
}
479+
return status(package_id);
480+
}
481+
482+
std::string ModelInstaller::clean_partial(const std::string & package_id) {
483+
if (!valid_package_id(package_id)) {
484+
throw std::runtime_error("invalid model-manager package id");
485+
}
486+
const auto script = state_->repository_root / "tools" / "model_manager_v2.py";
487+
if (!std::filesystem::is_regular_file(script)) {
488+
throw std::runtime_error("model cleanup helper was not found at " + script.string());
489+
}
490+
std::filesystem::path cancel_path;
491+
{
492+
std::lock_guard<std::mutex> lock(state_->mutex);
493+
const auto found = state_->jobs.find(package_id);
494+
if (found != state_->jobs.end() &&
495+
(found->second.state == "queued" || found->second.state == "running" ||
496+
found->second.state == "cancelling")) {
497+
throw std::runtime_error("stop the active installation before cleaning partial files for " + package_id);
498+
}
499+
if (found != state_->jobs.end()) {
500+
cancel_path = found->second.cancel_path;
501+
}
502+
}
503+
504+
const auto log_path = state_->job_root / (package_id + "-clean-partial.log");
505+
std::string command = python_command() + " -u " + shell_quote(script.string()) +
506+
" clean-partial " + shell_quote(package_id) +
507+
" --models-root " + shell_quote(state_->models_root.string()) +
508+
" > " + shell_quote(log_path.string()) + " 2>&1";
509+
const int result = std::system(command.c_str());
510+
std::string message = read_log_tail_text(log_path);
511+
while (!message.empty() && std::isspace(static_cast<unsigned char>(message.back()))) {
512+
message.pop_back();
513+
}
514+
if (result != 0) {
515+
throw std::runtime_error(message.empty() ? "partial download cleanup failed" : message);
516+
}
517+
if (!cancel_path.empty()) {
518+
std::error_code error;
519+
std::filesystem::remove(cancel_path, error);
520+
}
521+
{
522+
std::lock_guard<std::mutex> lock(state_->mutex);
523+
state_->jobs.erase(package_id);
524+
}
525+
return "{\"id\":" + json_quote(package_id) +
526+
",\"cleaned\":true,\"message\":" +
527+
json_quote(message.empty() ? "Partial download files cleaned" : message) + "}";
528+
}
529+
441530
std::string ModelInstaller::remove(const std::string & package_id) {
442531
if (!valid_package_id(package_id)) {
443532
throw std::runtime_error("invalid model-manager package id");
@@ -450,7 +539,8 @@ std::string ModelInstaller::remove(const std::string & package_id) {
450539
std::lock_guard<std::mutex> lock(state_->mutex);
451540
const auto job = state_->jobs.find(package_id);
452541
if (job != state_->jobs.end() &&
453-
(job->second.state == "queued" || job->second.state == "running")) {
542+
(job->second.state == "queued" || job->second.state == "running" ||
543+
job->second.state == "cancelling")) {
454544
throw std::runtime_error("installation is still running for " + package_id);
455545
}
456546
}

‎app/server/model_installer.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class ModelInstaller {
2727
bool overwrite);
2828
std::string status(const std::string & package_id = {}) const;
2929
std::string package_sizes();
30+
std::string stop(const std::string & package_id);
31+
std::string clean_partial(const std::string & package_id);
3032
std::string remove(const std::string & package_id);
3133
bool has_active_jobs() const;
3234

‎app/server/runtime.cpp‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,31 @@ using engine::io::json::Value;
3636

3737
using Clock = std::chrono::steady_clock;
3838

39+
void materialize_embedded_demo_voices(const std::filesystem::path & voice_dir) {
40+
std::filesystem::create_directories(voice_dir);
41+
for (const auto & voice : embedded_demo_voices()) {
42+
std::ofstream wav(
43+
voice_dir / (std::string(voice.name) + ".wav"),
44+
std::ios::binary | std::ios::trunc);
45+
if (!wav) {
46+
throw std::runtime_error("failed to create embedded demo voice: " + std::string(voice.name));
47+
}
48+
wav.write(voice.wav_bytes.data(), static_cast<std::streamsize>(voice.wav_bytes.size()));
49+
if (!wav) {
50+
throw std::runtime_error("failed to write embedded demo voice: " + std::string(voice.name));
51+
}
52+
}
53+
const auto prompt_text = embedded_demo_voice_prompt_text();
54+
std::ofstream prompts(voice_dir / "prompt_text", std::ios::binary | std::ios::trunc);
55+
if (!prompts) {
56+
throw std::runtime_error("failed to create embedded demo voice prompt_text");
57+
}
58+
prompts.write(prompt_text.data(), static_cast<std::streamsize>(prompt_text.size()));
59+
if (!prompts) {
60+
throw std::runtime_error("failed to write embedded demo voice prompt_text");
61+
}
62+
}
63+
3964
// Per-request override for the busy timeout. Absent means "use the model's
4065
// configured ceiling"; a value is clamped to that ceiling by resolve_busy_timeout_ms
4166
// so a client can shorten its own wait but never weaken the guard.
@@ -786,6 +811,11 @@ ServerState::ServerState(
786811
std::chrono::duration_cast<std::chrono::microseconds>(
787812
std::chrono::system_clock::now().time_since_epoch()).count()));
788813
std::filesystem::create_directories(upload_root_);
814+
if (!config_.voice_dir.has_value()) {
815+
const auto embedded_voices = upload_root_ / "demo_voices";
816+
materialize_embedded_demo_voices(embedded_voices);
817+
config_.voice_dir = embedded_voices;
818+
}
789819
model_installer_ = std::make_unique<ModelInstaller>(
790820
repository_root_,
791821
models_root_);
@@ -863,6 +893,12 @@ HttpResponse ServerState::handle(const HttpRequest & request) {
863893
else if (request.method == "POST" && request.path == "/v1/ui/models/install") {
864894
response = handle_model_install(request.body);
865895
}
896+
else if (request.method == "POST" && request.path == "/v1/ui/models/install/stop") {
897+
response = handle_model_install_stop(request.body);
898+
}
899+
else if (request.method == "POST" && request.path == "/v1/ui/models/clean-partial") {
900+
response = handle_model_clean_partial(request.body);
901+
}
866902
else if (request.method == "POST" && request.path == "/v1/ui/models/delete") {
867903
response = handle_model_remove(request.body);
868904
}
@@ -1119,6 +1155,32 @@ HttpResponse ServerState::handle_model_remove(const std::string & body_text) {
11191155
return json_response(model_installer_->remove(package_id));
11201156
}
11211157

1158+
HttpResponse ServerState::handle_model_install_stop(const std::string & body_text) {
1159+
if (!config_.ui_management) {
1160+
return error_response(403, "UI model installation is disabled", "forbidden");
1161+
}
1162+
const auto body = engine::io::json::parse(body_text);
1163+
const std::string package_id = engine::io::json::require_string(body, "id");
1164+
std::lock_guard<std::mutex> lock(model_installer_mutex_);
1165+
if (!model_installer_) {
1166+
return error_response(403, "UI model installation is disabled", "forbidden");
1167+
}
1168+
return json_response(model_installer_->stop(package_id));
1169+
}
1170+
1171+
HttpResponse ServerState::handle_model_clean_partial(const std::string & body_text) {
1172+
if (!config_.ui_management) {
1173+
return error_response(403, "UI model cleanup is disabled", "forbidden");
1174+
}
1175+
const auto body = engine::io::json::parse(body_text);
1176+
const std::string package_id = engine::io::json::require_string(body, "id");
1177+
std::lock_guard<std::mutex> lock(model_installer_mutex_);
1178+
if (!model_installer_) {
1179+
return error_response(403, "UI model cleanup is disabled", "forbidden");
1180+
}
1181+
return json_response(model_installer_->clean_partial(package_id));
1182+
}
1183+
11221184
HttpResponse ServerState::handle_model_install_status(const HttpRequest & request) const {
11231185
if (!config_.ui_management) {
11241186
return error_response(403, "UI model installation is disabled", "forbidden");

‎app/server/runtime.h‎

Lines changed: 2 additions & 0 deletions

0 commit comments

Comments
 (0)