From 3407c747a3b05bd412d320d23df0dbe5a5300af7 Mon Sep 17 00:00:00 2001 From: jasonchen31 Date: Thu, 3 Sep 2026 02:56:07 +0000 Subject: [PATCH 1/2] fix(voxcpm1): rename package id to remove dot for webui download Package id voxcpm1_0.5b_q8_0 contains '.' which is rejected by app/server/model_installer.cpp:valid_package_id() (only [A-Za-z0-9_-]). The WebUI install path (webui/native/src/lib/api.ts -> POST /v1/ui/models/install) forwards the id verbatim, so the native C++ PackageManager never receives the request and download from audio-cpp/audio.cpp-gguf appears to fail, while tools/model_manager_v2.py (no dot check) succeeds. Rename to voxcpm1_0_5b_q8_0 to match repo naming convention (qwen3_tts_0_6b, soprano_1_1_80m) and unblock validation: - model_specs/voxcpm1.json: recommended_package + packages[0].id - webui/configs/models_catalog.json: download_id for voxcpm1 - docs/community_models/voxcpm1.md: CLI example and package table Remote file name voxcpm-0.5b-q8_0-audiovae-f16.gguf unchanged; GGUF already embeds config/tokenizer sidecars, so single-file huggingface_snapshot install remains valid. --- docs/community_models/voxcpm1.md | 4 ++-- model_specs/voxcpm1.json | 4 ++-- webui/configs/models_catalog.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/community_models/voxcpm1.md b/docs/community_models/voxcpm1.md index e1c082780..3d2b7e227 100644 --- a/docs/community_models/voxcpm1.md +++ b/docs/community_models/voxcpm1.md @@ -21,7 +21,7 @@ VoxCPM1 is a **tokenizer-free TTS model** from [OpenBMB](https://github.com/Open ```bash # Via model manager (recommended) -python3 tools/model_manager_v2.py install voxcpm1_0.5b_q8_0 --models-root models +python3 tools/model_manager_v2.py install voxcpm1_0_5b_q8_0 --models-root models ``` This downloads the `voxcpm-0.5b-q8_0-audiovae-f16.gguf` package (~690 MB) to `models/VoxCPM1-GGUF/`. @@ -150,7 +150,7 @@ The default package is the standalone GGUF: | Package ID | Display Name | Format | Precision | Files | |---|---|---|---|---| -| `voxcpm1_0.5b_q8_0` | VoxCPM 0.5B Q8_0 GGUF | gguf | q8_0 (LLM) + f16 (AudioVAE) | `VoxCPM1-GGUF/voxcpm-0.5b-q8_0-audiovae-f16.gguf` | +| `voxcpm1_0_5b_q8_0` | VoxCPM 0.5B Q8_0 GGUF | gguf | q8_0 (LLM) + f16 (AudioVAE) | `VoxCPM1-GGUF/voxcpm-0.5b-q8_0-audiovae-f16.gguf` | The GGUF embeds: - Hybrid-quantized model (LLM Q8_0, AudioVAE F16) diff --git a/model_specs/voxcpm1.json b/model_specs/voxcpm1.json index 66376a33c..01d050c01 100644 --- a/model_specs/voxcpm1.json +++ b/model_specs/voxcpm1.json @@ -226,7 +226,7 @@ ] }, "ui": { - "recommended_package": "voxcpm1_0.5b_q8_0", + "recommended_package": "voxcpm1_0_5b_q8_0", "tags": [ "TTS", "Clone", @@ -248,7 +248,7 @@ }, "packages": [ { - "id": "voxcpm1_0.5b_q8_0", + "id": "voxcpm1_0_5b_q8_0", "display_name": "VoxCPM 0.5B Q8_0 GGUF", "default": true, "format": "gguf", diff --git a/webui/configs/models_catalog.json b/webui/configs/models_catalog.json index 36e8f4041..0d426b1bb 100644 --- a/webui/configs/models_catalog.json +++ b/webui/configs/models_catalog.json @@ -17,7 +17,7 @@ { "id": "miotts", "display_name": "MioTTS 1.7B (tts; needs MioCodec)", "family": "miotts", "path": "models/MioTTS-1.7B", "task": "tts", "mode": "offline", "download_id": "miotts_1_7b", "min_vram_gb": 8 }, { "id": "soprano-tts", "display_name": "Soprano TTS (tts)", "family": "soprano_tts", "path": "models/Soprano-1.1-80M-GGUF", "task": "tts", "mode": "offline", "download_id": "soprano_1_1_80m_q8_0", "min_vram_gb": 1 }, { "id": "voxcpm2", "display_name": "VoxCPM2 (tts)", "family": "voxcpm2", "path": "models/VoxCPM2", "task": "tts", "mode": "offline", "download_id": "voxcpm2", "session_options": { "voxcpm2.weight_type": "q8_0" }, "min_vram_gb": 6 }, - { "id": "voxcpm1", "display_name": "VoxCPM1 0.5B (tts + clone)", "family": "voxcpm1", "path": "models/VoxCPM1-GGUF", "task": "tts", "mode": "offline", "download_id": "voxcpm1_0.5b_q8_0", "min_vram_gb": 4 }, + { "id": "voxcpm1", "display_name": "VoxCPM1 0.5B (tts + clone)", "family": "voxcpm1", "path": "models/VoxCPM1-GGUF", "task": "tts", "mode": "offline", "download_id": "voxcpm1_0_5b_q8_0", "min_vram_gb": 4 }, { "id": "vibevoice", "display_name": "VibeVoice 1.5B/7B (tts, long-form/multi-speaker)", "family": "vibevoice", "path": "models/VibeVoice-1.5B", "task": "tts", "mode": "offline", "download_id": "vibevoice_1_5b", "min_vram_gb": 7 }, { "id": "vibevoice-7b", "display_name": "VibeVoice 7B (tts, long-form/multi-speaker)", "family": "vibevoice", "path": "models/VibeVoice-7B-GGUF", "task": "tts", "mode": "offline", "download_id": "vibevoice_7b_q8_0", "min_vram_gb": 16 }, { "id": "index-tts2", "display_name": "IndexTTS2 (tts 中英克隆+情感)", "display_name_en": "IndexTTS2 (tts, zh/en clone + emotion)", "family": "index_tts2", "path": "models/IndexTTS-2", "task": "tts", "mode": "offline", "download_id": "index_tts2", "min_vram_gb": 8 }, From bc36c6dcb517a9efd316f3723cbcc57290830e5e Mon Sep 17 00:00:00 2001 From: jasonchen31 Date: Thu, 3 Sep 2026 03:06:45 +0000 Subject: [PATCH 2/2] fix(voxcpm1): convert Traditional Chinese to Simplified unless Cantonese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port audio8_tts OpenCC fix (0eec2be) to voxcpm1 to avoid Cantonese mis-trigger on Traditional input without yue language. Uses shared engine::text::chinese_variant (TSCharacters 3222) and keeps Traditional only for yue/cantonese/zh-HK/zh-MO. - src/community_models/voxcpm1/session.cpp: add extract_request_language() (text_input.language -> voice.style.language -> language/lang option), maybe_convert_traditional_to_simplified_opt for prompt_text/reference_text and TTS text before chunking (offline) and before generate_streaming (streaming) - Mirrors src/community_models/audio8_tts/session.cpp:477,504 Fixes voxcpm1 rendering Traditional "發財" as Cantonese when no language is specified; yue/cantonese/zh-HK/zh-MO preserves Traditional. --- src/community_models/voxcpm1/session.cpp | 49 ++++++++++++++++++++---- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/community_models/voxcpm1/session.cpp b/src/community_models/voxcpm1/session.cpp index 3c86a705f..2ccfd5002 100644 --- a/src/community_models/voxcpm1/session.cpp +++ b/src/community_models/voxcpm1/session.cpp @@ -4,6 +4,7 @@ #include "engine/framework/model_spec/metadata.h" #include "engine/framework/model_spec/package.h" #include "engine/framework/runtime/options.h" +#include "engine/framework/text/chinese_variant.h" #include "engine/framework/text/chunking.h" #include @@ -147,6 +148,20 @@ int64_t product(const std::vector &values) { return out; } +std::optional extract_request_language(const runtime::TaskRequest &request) { + if (request.text_input.has_value() && !request.text_input->language.empty()) { + return request.text_input->language; + } + if (request.voice.has_value() && request.voice->style.has_value() && + request.voice->style->language.has_value()) { + return request.voice->style->language; + } + if (const auto lang = runtime::find_option(request.options, {"language", "lang"})) { + return *lang; + } + return std::nullopt; +} + } // namespace bool VoxCPM1SessionBase::EncodedPromptCacheKeyEqual::operator()( @@ -266,19 +281,31 @@ runtime::TaskResult VoxCPM1SessionBase::run_offline_request(const runtime::TaskR release_guard(this, release_runtime_memory); const auto wall_start = Clock::now(); + // Traditional -> Simplified conversion (OpenCC TSCharacters) unless language is Cantonese/Yue. + // Mirrors audio8_tts fix: keep Traditional only for yue/cantonese/zh-HK/zh-MO. + const auto language = extract_request_language(request); + auto maybe_convert = [&](std::string_view text) -> std::string { + return engine::text::maybe_convert_traditional_to_simplified_opt(text, language); + }; + const auto prompt_text_raw = + runtime::find_option(request.options, {"voxcpm1.prompt_text", + "voxcpm1.prompt_text", + "prompt_text", "reference_text"}) + .value_or(""); + const std::string prompt_text = maybe_convert(prompt_text_raw); + // Apply conversion to TTS text before chunking so word-budget chunking operates on converted text. + runtime::TaskRequest converted_request = request; + if (converted_request.text_input.has_value()) { + converted_request.text_input->text = maybe_convert(converted_request.text_input->text); + } const int64_t text_chunk_size = engine::text::parse_text_chunk_size_override(request.options).value_or(kDefaultTextChunkSize); const auto text_chunk_mode = engine::text::parse_text_chunk_mode_override(request.options) .value_or(engine::text::TextChunkMode::TagAware); const auto chunk_requests = - runtime::chunk_text_request(request, text_chunk_size, text_chunk_mode); + runtime::chunk_text_request(converted_request, text_chunk_size, text_chunk_mode); const auto generation_options = generation_options_from_request(request); - const auto prompt_text = - runtime::find_option(request.options, {"voxcpm1.prompt_text", - "voxcpm1.prompt_text", - "prompt_text", "reference_text"}) - .value_or(""); std::optional reference_audio; if (request.voice.has_value() && request.voice->speaker.has_value() && request.voice->speaker->audio.has_value()) { @@ -365,12 +392,17 @@ VoxCPM1SessionBase::run_streaming_request( release_guard(this, release_runtime_memory); const auto wall_start = Clock::now(); + const auto language = extract_request_language(request); + auto maybe_convert = [&](std::string_view text) -> std::string { + return engine::text::maybe_convert_traditional_to_simplified_opt(text, language); + }; auto generation_options = generation_options_from_request(request); - const auto prompt_text = + const auto prompt_text_raw = runtime::find_option(request.options, {"voxcpm1.prompt_text", "voxcpm1.prompt_text", "prompt_text", "reference_text"}) .value_or(""); + const std::string prompt_text = maybe_convert(prompt_text_raw); std::optional reference_audio; if (request.voice.has_value() && request.voice->speaker.has_value() && request.voice->speaker->audio.has_value()) { @@ -433,7 +465,8 @@ VoxCPM1SessionBase::run_streaming_request( }; const auto generator_start = Clock::now(); - (void)generator_->generate_streaming(request.text_input->text, prompt, + const std::string streaming_text = maybe_convert(request.text_input->text); + (void)generator_->generate_streaming(streaming_text, prompt, generation_options, emit_chunk); const auto generator_end = Clock::now(); const double generator_with_callbacks_ms =