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/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 = 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 },