diff --git a/Dockerfile.redhat b/Dockerfile.redhat index 04fe81cbea..561b732604 100644 --- a/Dockerfile.redhat +++ b/Dockerfile.redhat @@ -315,6 +315,12 @@ RUN rm -f /usr/lib64/cmake/OpenSSL/OpenSSLConfig.cmake # hadolint ignore=SC2046 RUN bazel build --jobs=$JOBS ${debug_bazel_flags} ${minitrace_flags} //src:ovms $(if [ "$OPTIMIZE_BUILDING_TESTS" == "1" ] ; then echo -n //src:ovms_test; fi) +# espeak-ng is built as a separate step, independent of the OVMS binary. +# Set ESPEAK=0 to skip the espeak build. +ARG ESPEAK=1 +# hadolint ignore=DL3059 +RUN if [ "$ESPEAK" == "1" ]; then bazel build --jobs=$JOBS ${debug_bazel_flags} //third_party:espeak_ng //third_party:espeak_ng_data; fi + # Tests execution COPY ci/check_coverage.bat /ovms/ ARG CHECK_COVERAGE=0 diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index ed38a3cb02..59a7fbdf23 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -331,6 +331,12 @@ ARG OPTIMIZE_BUILDING_TESTS=0 # hadolint ignore=SC2046 RUN if [ "$FUZZER_BUILD" == "0" ]; then bazel build --jobs=$JOBS ${debug_bazel_flags} ${minitrace_flags} //src:ovms $(if [ "${OPTIMIZE_BUILDING_TESTS}" == "1" ] ; then echo -n //src:ovms_test; fi); fi; +# espeak-ng is built as a separate step, independent of the OVMS binary. +# Set ESPEAK=0 to skip the espeak build. +ARG ESPEAK=1 +# hadolint ignore=DL3059 +RUN if [ "$ESPEAK" == "1" ]; then bazel build --jobs=$JOBS ${debug_bazel_flags} //third_party:espeak_ng //third_party:espeak_ng_data; fi + ARG RUN_TESTS=0 RUN if [ "$RUN_TESTS" == "1" ] ; then mkdir -p demos/common/export_models/ && mv export_model.py demos/common/export_models/ && ./prepare_llm_models.sh /ovms/src/test/llm_testing docker && ./run_unit_tests.sh ; fi diff --git a/Makefile b/Makefile index 654875576c..ff800383ca 100644 --- a/Makefile +++ b/Makefile @@ -148,13 +148,8 @@ else ifeq ($(findstring redhat,$(BASE_OS)),redhat) else $(error BASE_OS must be either ubuntu or redhat) endif -ifeq ($(ESPEAK),1) - ESPEAK_PARAMS = " --//:espeak=on" -else - ESPEAK_PARAMS = " --//:espeak=off" -endif -CAPI_FLAGS = "--strip=$(STRIP)"$(BAZEL_DEBUG_BUILD_FLAGS)" --config=mp_off_py_off"$(OV_TRACING_PARAMS)$(TARGET_DISTRO_PARAMS)$(ESPEAK_PARAMS) -BAZEL_DEBUG_FLAGS="--strip=$(STRIP)"$(BAZEL_DEBUG_BUILD_FLAGS)$(DISABLE_PARAMS)$(FUZZER_BUILD_PARAMS)$(OV_TRACING_PARAMS)$(TARGET_DISTRO_PARAMS)$(ESPEAK_PARAMS)$(REPO_ENV) +CAPI_FLAGS = "--strip=$(STRIP)"$(BAZEL_DEBUG_BUILD_FLAGS)" --config=mp_off_py_off"$(OV_TRACING_PARAMS)$(TARGET_DISTRO_PARAMS) +BAZEL_DEBUG_FLAGS="--strip=$(STRIP)"$(BAZEL_DEBUG_BUILD_FLAGS)$(DISABLE_PARAMS)$(FUZZER_BUILD_PARAMS)$(OV_TRACING_PARAMS)$(TARGET_DISTRO_PARAMS)$(REPO_ENV) # Option to Override release image. # Release image OS *must have* glibc version >= glibc version on BASE_OS: @@ -249,7 +244,8 @@ BUILD_ARGS = --build-arg http_proxy=$(HTTP_PROXY)\ --build-arg JOBS=$(JOBS)\ --build-arg CAPI_FLAGS=$(CAPI_FLAGS)\ --build-arg VERBOSE_LOGS=$(VERBOSE_LOGS)\ - --build-arg KONFLUX=$(KONFLUX) + --build-arg KONFLUX=$(KONFLUX)\ + --build-arg ESPEAK=$(ESPEAK) .PHONY: default docker_build \ diff --git a/common_settings.bzl b/common_settings.bzl index 8818ec83fe..b2f1101f2f 100644 --- a/common_settings.bzl +++ b/common_settings.bzl @@ -20,7 +20,7 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@mediapipe//mediapipe/framework:more_selects.bzl", "more_selects") load("@bazel_skylib//rules:common_settings.bzl", "string_flag") -load("//:distro.bzl", "distro_flag", "espeak_flag") +load("//:distro.bzl", "distro_flag") # cc_library rule wrapper that will accept the same arguments but if user will not provide # copts, linkopts, local_defines it will set them to the defaults @@ -58,7 +58,6 @@ def ovms_cc_library(**kwargs): def create_config_settings(): distro_flag() - espeak_flag() native.config_setting( name = "disable_mediapipe", define_values = { diff --git a/create_package.sh b/create_package.sh index 69812d7302..6220a93ec9 100755 --- a/create_package.sh +++ b/create_package.sh @@ -36,6 +36,7 @@ ESPEAK_DATA_SRC=$(find /ovms/bazel-out/k8-*/bin/external/espeak_ng -type d -name if [ -n "$ESPEAK_DATA_SRC" ] && [ -d "$ESPEAK_DATA_SRC" ] ; then mkdir -p /ovms_release/share cp -rL "$ESPEAK_DATA_SRC" /ovms_release/share/ ; + chmod -R u+w /ovms_release/share/espeak-ng-data 2>/dev/null || true ; fi # Resolve the packaged eSpeak shared object dynamically so version bumps # do not require touching this script. diff --git a/demos/common/export_models/export_model.py b/demos/common/export_models/export_model.py index 9192b91449..b5c1acb2d7 100644 --- a/demos/common/export_models/export_model.py +++ b/demos/common/export_models/export_model.py @@ -92,7 +92,7 @@ def add_common_arguments(parser): parser_text2speech = subparsers.add_parser('text2speech', help='export model for text2speech endpoint') add_common_arguments(parser_text2speech) parser_text2speech.add_argument('--num_streams', default=0, type=int, help='The number of parallel execution streams to use for the models in the pipeline.', dest='num_streams') -parser_text2speech.add_argument('--model_type', default='speecht5', choices=['speecht5', 'kokoro'], help='Type of the source TTS model. speecht5 uses optimum-cli; kokoro uses a dedicated PyTorch->OpenVINO conversion path.', dest='model_type') +parser_text2speech.add_argument('--model_type', default='kokoro', choices=['speecht5', 'kokoro'], help='Type of the source TTS model. speecht5 uses optimum-cli; kokoro uses a dedicated PyTorch->OpenVINO conversion path.', dest='model_type') parser_text2speech.add_argument('--vocoder', type=str, help='The vocoder model to use for speecht5. For example microsoft/speecht5_hifigan. Ignored for kokoro.', dest='vocoder') parser_text2speech.add_argument('--speaker_name', type=str, help='Name of the speaker (speecht5 only; for kokoro all voices from the HF repo are exported).', dest='speaker_name') parser_text2speech.add_argument('--speaker_path', type=str, help='Path to the speaker.bin file (speecht5 only; for kokoro all voices from the HF repo are exported).', dest='speaker_path') @@ -503,15 +503,6 @@ def export_embeddings_model_ov(model_repository_path, source_model, model_name, print("Created graph {}".format(os.path.join(model_repository_path, model_name, 'graph.pbtxt'))) add_servable_to_config(config_file_path, model_name, os.path.relpath(os.path.join(model_repository_path, model_name), os.path.dirname(config_file_path))) -def _list_kokoro_voices(destination_path): - """optimum-cli's Kokoro exporter writes per-voice speaker embeddings to - /voices/.bin. Return the sorted list of voice names.""" - voices_dir = os.path.join(destination_path, "voices") - if not os.path.isdir(voices_dir): - print("Warning: no voices/ directory found under", destination_path) - return [] - return sorted(Path(p).stem for p in Path(voices_dir).glob("*.bin")) - def export_text2speech_model(model_repository_path, source_model, model_name, precision, task_parameters, config_file_path): destination_path = os.path.join(model_repository_path, model_name) print("Exporting text2speech model to ",destination_path) @@ -519,16 +510,12 @@ def export_text2speech_model(model_repository_path, source_model, model_name, pr if model_type == 'kokoro': # optimum-intel registers Kokoro under library_name=kokoro / task=text-to-audio. - # The kokoro exporter also dumps each speaker embedding to voices/.bin. if not os.path.isfile(os.path.join(destination_path, 'openvino_model.xml')) or args['overwrite_models']: optimum_command = "optimum-cli export openvino --model {} --task text-to-audio --weight-format {} {} --trust-remote-code {}".format( source_model, precision, task_parameters['extra_quantization_params'], destination_path) print('Running command:', optimum_command) if os.system(optimum_command): raise ValueError("Failed to export kokoro model", source_model) - voice_names = _list_kokoro_voices(destination_path) - # Render the graph with every available voice (path is relative to graph.pbtxt). - task_parameters['voices'] = [{'name': n, 'path': f'./voices/{n}.bin'} for n in voice_names] else: if not os.path.isdir(destination_path) or args['overwrite_models']: if not task_parameters.get('vocoder'): diff --git a/distro.bzl b/distro.bzl index c01ad7bb56..a5f43dfd36 100644 --- a/distro.bzl +++ b/distro.bzl @@ -43,28 +43,4 @@ def distro_flag(): negate = ":ubuntu_build", ) -# Controls whether espeak-ng is built from source (via Bazel) and bundled -# into the OVMS release. When "off", no espeak-ng artifacts are produced -# and the runtime will not have phonemization fallback available. -def espeak_flag(): - string_flag( - name = "espeak", - values = ["on", "off"], - build_setting_default = "on", - ) - native.config_setting( - name = "espeak_on", - flag_values = { - "espeak": "on", - }, - ) - native.config_setting( - name = "espeak_off", - flag_values = { - "espeak": "off", - }, - ) - more_selects.config_setting_negation( - name = "not_espeak_on", - negate = ":espeak_on", - ) + diff --git a/docs/build_from_source.md b/docs/build_from_source.md index 1b3c9f8579..159792787b 100644 --- a/docs/build_from_source.md +++ b/docs/build_from_source.md @@ -156,6 +156,32 @@ dist/ubuntu22 └── ovms.tar.gz.sha256 ``` +### Optional `espeak-ng` for Speech Generation + +`espeak-ng` is optional in OVMS builds. It is used by Kokoro speech generation for: + +- non-English grapheme-to-phoneme conversion, +- fallback phonemization of some out-of-vocabulary (OOV) English words. + +If you do not need Kokoro non-English support and can accept reduced English OOV fallback behavior, you can: + +- skip building `espeak-ng` during image/package build: + +```bash +make targz_package ESPEAK=0 +``` + +- or remove it from an already prepared package by deleting: + - `libespeak-ng.so*` from the package `lib` directory + - `share/espeak-ng-data/` + +Consequences of removing or disabling `espeak-ng`: + +- Speech generation endpoint still works for Kokoro English usage. +- Non-English Kokoro text normalization/phonemization paths that depend on `espeak-ng` are not available. +- English OOV words no longer use `espeak-ng` fallback, so pronunciation quality/coverage for uncommon words may degrade. +- In practice, treat Kokoro as English-focused (for example `en-us` and `en-gb`) when `espeak-ng` is not present. + --- Read more details about building and testing changes in [developer guide](./developer_guide.md). diff --git a/prepare_llm_models.sh b/prepare_llm_models.sh index 0bb0257580..3d57f7b91e 100755 --- a/prepare_llm_models.sh +++ b/prepare_llm_models.sh @@ -27,7 +27,7 @@ LEGACY_MODEL_FILE="1/model.bin" EMBEDDING_MODEL="thenlper/gte-small" RERANK_MODEL="BAAI/bge-reranker-base" VLM_MODEL="OpenVINO/InternVL2-1B-int4-ov" -TTS_MODEL="microsoft/speecht5_tts" +TTS_MODEL="hexgrad/Kokoro-82M" STT_MODEL="openai/whisper-tiny" # Models for tools testing. Only tokenizers are downloaded. @@ -48,7 +48,7 @@ echo "Downloading LLM testing models to directory $1" export PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/cpu https://storage.openvinotoolkit.org/simple/wheels/nightly" if [ "$2" = "docker" ]; then export PATH=$PATH:/opt/intel/openvino/python/bin - python3 -m pip install "optimum-intel"@git+https://github.com/huggingface/optimum-intel.git nncf sentence_transformers==5.3.0 sentencepiece requests protobuf==7.35.0 + python3 -m pip install "optimum-intel"@git+https://github.com/huggingface/optimum-intel.git nncf sentence_transformers==5.3.0 sentencepiece requests protobuf==7.35.0 kokoro else python3 -m venv .venv . .venv/bin/activate @@ -83,13 +83,13 @@ if [ ! -f "$1/$FACEBOOK_MODEL/chat_template.jinja" ]; then cp src/test/llm/dummy_facebook_template.jinja "$1/$FACEBOOK_MODEL/chat_template.jinja" fi -if [ -f "$1/$TTS_MODEL/$TOKENIZER_FILE" ]; then - echo "Model file $1/$TTS_MODEL/$TOKENIZER_FILE exists. Skipping downloading models." +if [ -f "$1/$TTS_MODEL/openvino_model.xml" ]; then + echo "Model file $1/$TTS_MODEL/openvino_model.xml exists. Skipping downloading models." else - python3 demos/common/export_models/export_model.py text2speech --source_model "$TTS_MODEL" --weight-format int4 --model_repository_path $1 --vocoder microsoft/speecht5_hifigan + python3 demos/common/export_models/export_model.py text2speech --source_model "$TTS_MODEL" --model_type kokoro --weight-format int8 --model_repository_path $1 fi -if [ ! -f "$1/$TTS_MODEL/$TOKENIZER_FILE" ]; then - echo "[ERROR] Model file $1/$TTS_MODEL/$TOKENIZER_FILE does not exist." +if [ ! -f "$1/$TTS_MODEL/openvino_model.xml" ]; then + echo "[ERROR] Model file $1/$TTS_MODEL/openvino_model.xml does not exist." exit 1 fi diff --git a/src/BUILD b/src/BUILD index 2f271b28c2..2ef747b99e 100644 --- a/src/BUILD +++ b/src/BUILD @@ -2084,12 +2084,6 @@ cc_binary( "//src/python/binding:pyovms.so", ], "//:disable_python": [] - }) + select({ - "//:espeak_on": [ - "//third_party:espeak_ng", - "//third_party:espeak_ng_data", - ], - "//:espeak_off": [], }), # linkstatic = False, # Use for dynamic linking when necessary ) diff --git a/src/audio/text_to_speech/t2s_servable.cpp b/src/audio/text_to_speech/t2s_servable.cpp index a0af03b2cf..20d838a015 100644 --- a/src/audio/text_to_speech/t2s_servable.cpp +++ b/src/audio/text_to_speech/t2s_servable.cpp @@ -15,10 +15,13 @@ //***************************************************************************** #include +#include #include #include #include #include +#include +#include #include "openvino/genai/speech_generation/text2speech_pipeline.hpp" #include "src/audio/text_to_speech/t2s_calculator.pb.h" @@ -31,6 +34,8 @@ namespace ovms { +static constexpr const char* VOICES_DIR_NAME = "voices"; + static size_t getShapeElementsCount(const ov::Shape& shape) { size_t elementsCount = 1; for (const auto dim : shape) { @@ -42,6 +47,35 @@ static size_t getShapeElementsCount(const ov::Shape& shape) { return elementsCount; } +static std::vector getVoiceEmbeddingPaths(const std::filesystem::path& voicesDir) { + std::vector voicePaths; + std::error_code errorCode; + std::filesystem::directory_iterator directoryIt(voicesDir, errorCode); + if (errorCode) { + throw std::runtime_error("Failed to open voices directory '" + voicesDir.string() + "': " + errorCode.message()); + } + + const std::filesystem::directory_iterator directoryEnd; + for (; directoryIt != directoryEnd; directoryIt.increment(errorCode)) { + if (errorCode) { + throw std::runtime_error("Failed to iterate voices directory '" + voicesDir.string() + "': " + errorCode.message()); + } + const auto& entry = *directoryIt; + const bool isRegularFile = entry.is_regular_file(errorCode); + if (errorCode) { + throw std::runtime_error("Failed to inspect entry '" + entry.path().string() + "' in voices directory '" + voicesDir.string() + "': " + errorCode.message()); + } + if (!isRegularFile) { + continue; + } + if (entry.path().extension() == ".bin") { + voicePaths.emplace_back(entry.path()); + } + } + std::sort(voicePaths.begin(), voicePaths.end()); + return voicePaths; +} + static ov::Tensor readSpeakerEmbedding(const std::filesystem::path& filePath, const ov::Shape& expectedShape) { std::ifstream input(filePath, std::ios::binary); if (input.fail()) { @@ -100,6 +134,8 @@ TtsServable::TtsServable(const std::string& modelDir, const std::string& targetD device = recommendTargetDevice(); SPDLOG_INFO("No device specified for TTS model, using recommended device: {}", device); } + // Normalize the path to use OS-appropriate separators + parsedModelsPath = std::filesystem::absolute(parsedModelsPath); ov::AnyMap config; Status status = JsonParser::parsePluginConfig(pluginConfig, config); if (!status.ok()) { @@ -108,6 +144,21 @@ TtsServable::TtsServable(const std::string& modelDir, const std::string& targetD } ttsPipeline = std::make_shared(parsedModelsPath.string(), device, config); const ov::Shape speakerEmbeddingShape = ttsPipeline->get_speaker_embedding_shape(); + const std::filesystem::path voicesDir = parsedModelsPath / VOICES_DIR_NAME; + std::error_code ec; + if (std::filesystem::is_directory(voicesDir, ec)) { + try { + for (const auto& voicePath : getVoiceEmbeddingPaths(voicesDir)) { + const std::string voiceName = voicePath.stem().string(); + voices.insert_or_assign(voiceName, readSpeakerEmbedding(voicePath, speakerEmbeddingShape)); + } + } catch (const std::exception& e) { + SPDLOG_WARN("Failed to load voices from {}: {}", voicesDir.string(), e.what()); + } + } else { + SPDLOG_DEBUG("Voices directory not found: {}", voicesDir.string()); + } + for (const auto& voice : graphVoices) { std::filesystem::path voicePath(voice.path()); if (voicePath.is_relative()) { @@ -115,6 +166,9 @@ TtsServable::TtsServable(const std::string& modelDir, const std::string& targetD } if (!std::filesystem::exists(voicePath)) throw std::runtime_error{"Requested voice speaker embeddings file does not exist: " + voicePath.string()}; + if (voices.find(voice.name()) != voices.end()) { + SPDLOG_DEBUG("Voice '{}' is already configured and will be overwritten with tensor from: {}", voice.name(), voicePath.string()); + } voices[voice.name()] = readSpeakerEmbedding(voicePath, speakerEmbeddingShape); } } diff --git a/src/graph_export/graph_export.cpp b/src/graph_export/graph_export.cpp index d275a57588..1584c07387 100644 --- a/src/graph_export/graph_export.cpp +++ b/src/graph_export/graph_export.cpp @@ -350,23 +350,6 @@ static Status createTextToSpeechGraphTemplate(const std::string& directoryPath, SPDLOG_TRACE("modelsPath: {}, directoryPath: {}, ggufFilename: {}", modelsPath, directoryPath, ggufFilename.value_or("std::nullopt")); GET_PLUGIN_CONFIG_OPT_OR_FAIL_AND_RETURN(exportSettings); - // Enumerate kokoro speaker embeddings dumped by optimum-cli to /voices/*.bin. - std::vector voiceNames; - if (exportSettings.modelType == "kokoro") { - std::filesystem::path voicesDir = std::filesystem::path(directoryPath) / "voices"; - std::error_code ec; - if (std::filesystem::is_directory(voicesDir, ec)) { - for (const auto& entry : std::filesystem::directory_iterator(voicesDir, ec)) { - if (entry.is_regular_file(ec) && !ec && entry.path().extension() == ".bin") { - voiceNames.push_back(entry.path().stem().string()); - } - } - std::sort(voiceNames.begin(), voiceNames.end()); - } else { - SPDLOG_WARN("Kokoro voices directory not found at {}", voicesDir.string()); - } - } - // clang-format off oss << R"( input_stream: "HTTP_REQUEST_PAYLOAD:input" @@ -391,19 +374,6 @@ node { oss << R"(plugin_config: ')" << pluginConfigOpt.value() << R"(' )"; } - if (!voiceNames.empty()) { - oss << R"(voices: [)"; - for (size_t i = 0; i < voiceNames.size(); ++i) { - oss << R"( - { name: ")" << voiceNames[i] << R"(", path: "./voices/)" << voiceNames[i] << R"(.bin" })"; - if (i + 1 < voiceNames.size()) { - oss << ","; - } - } - oss << R"( - ] - )"; - } oss << R"(} } })"; @@ -795,5 +765,4 @@ std::variant, Status> GraphExport::createPluginString return std::nullopt; } } - } // namespace ovms diff --git a/src/test/audio/graph_tts.pbtxt b/src/test/audio/graph_tts.pbtxt index 021c525225..71c35b7d9c 100644 --- a/src/test/audio/graph_tts.pbtxt +++ b/src/test/audio/graph_tts.pbtxt @@ -24,15 +24,9 @@ node { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" plugin_config: '{"NUM_STREAMS": "1" }', target_device: "CPU" - voices: [ - { - name: "speaker1", - path: "/ovms/src/test/audio/speaker.bin", - } - ] } } } \ No newline at end of file diff --git a/src/test/audio/text2speech_test.cpp b/src/test/audio/text2speech_test.cpp index f4d5a88ac1..9313eab8ca 100644 --- a/src/test/audio/text2speech_test.cpp +++ b/src/test/audio/text2speech_test.cpp @@ -21,7 +21,6 @@ #include "../../audio/audio_utils.hpp" #include "../../http_rest_api_handler.hpp" #include "../../server.hpp" -#include "rapidjson/document.h" #include "../test_http_utils.hpp" #include "../test_utils.hpp" #include "../platform_utils.hpp" @@ -58,7 +57,8 @@ TEST_F(Text2SpeechHttpTest, simplePositive) { { "model": ")" + modelName + R"(", - "input": "The quick brown fox jumped over the lazy dog." + "input": "The quick brown fox jumped over the lazy dog.", + "voice": "af_alloy" } )"; ASSERT_EQ( @@ -74,7 +74,8 @@ TEST_F(Text2SpeechHttpTest, emptyInput) { { "model": ")" + modelName + R"(", - "input": "" + "input": "", + "voice": "af_alloy" } )"; ASSERT_EQ( @@ -103,7 +104,7 @@ TEST_F(Text2SpeechHttpTest, positiveWithVoice) { "model": ")" + modelName + R"(", "input": "The quick brown fox jumped over the lazy dog.", - "voice": "speaker1" + "voice": "af_alloy" } )"; ASSERT_EQ( @@ -130,6 +131,16 @@ TEST_F(Text2SpeechHttpTest, nonExistingVoiceRequested) { class Text2SpeechConfigTest : public ::testing::Test {}; +namespace { +ovms::Status validateText2SpeechGraphConfig(ConstructorEnabledModelManager& manager, std::string testPbtxt) { + adjustConfigForTargetPlatform(testPbtxt); + ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; + DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); + mediapipeDummy.inputConfig = testPbtxt; + return mediapipeDummy.validate(manager); +} +} // namespace + TEST_F(Text2SpeechConfigTest, NodeNameMissing) { ConstructorEnabledModelManager manager; std::string testPbtxt = R"( @@ -143,17 +154,14 @@ TEST_F(Text2SpeechConfigTest, NodeNameMissing) { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" target_device: "CPU" } } } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::LLM_NODE_MISSING_NAME); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::LLM_NODE_MISSING_NAME); } TEST_F(Text2SpeechConfigTest, SidePacketMissing) { @@ -169,17 +177,14 @@ TEST_F(Text2SpeechConfigTest, SidePacketMissing) { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" target_device: "CPU" } } } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::MEDIAPIPE_GRAPH_INITIALIZATION_ERROR); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::MEDIAPIPE_GRAPH_INITIALIZATION_ERROR); } TEST_F(Text2SpeechConfigTest, MissingModelsPath) { @@ -202,10 +207,7 @@ TEST_F(Text2SpeechConfigTest, MissingModelsPath) { } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); } TEST_F(Text2SpeechConfigTest, InvalidPluginConfig) { @@ -222,7 +224,7 @@ TEST_F(Text2SpeechConfigTest, InvalidPluginConfig) { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" plugin_config: 'INVALID', target_device: "CPU" } @@ -230,10 +232,32 @@ TEST_F(Text2SpeechConfigTest, InvalidPluginConfig) { } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); +} + +TEST_F(Text2SpeechConfigTest, MissingVoicesInGraphUsesModelVoicesDir) { + ConstructorEnabledModelManager manager; + std::string testPbtxt = R"( + input_stream: "HTTP_REQUEST_PAYLOAD:input" + output_stream: "HTTP_RESPONSE_PAYLOAD:output" + + node { + name: "ttsNode1" + input_side_packet: "TTS_NODE_RESOURCES:t2s_servable" + calculator: "T2sCalculator" + input_stream: "HTTP_REQUEST_PAYLOAD:input" + output_stream: "HTTP_RESPONSE_PAYLOAD:output" + node_options: { + [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" + plugin_config: '{"NUM_STREAMS": "1" }', + target_device: "CPU" + } + } + } + )"; + + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::OK); } TEST_F(Text2SpeechConfigTest, NonExistingVoicePath) { @@ -250,7 +274,7 @@ TEST_F(Text2SpeechConfigTest, NonExistingVoicePath) { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" plugin_config: '{"NUM_STREAMS": "1" }', target_device: "CPU" voices: [ @@ -264,10 +288,7 @@ TEST_F(Text2SpeechConfigTest, NonExistingVoicePath) { } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); } TEST_F(Text2SpeechConfigTest, VoiceMissingPath) { @@ -284,7 +305,7 @@ TEST_F(Text2SpeechConfigTest, VoiceMissingPath) { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" plugin_config: '{"NUM_STREAMS": "1" }', target_device: "CPU" voices: [ @@ -297,10 +318,7 @@ TEST_F(Text2SpeechConfigTest, VoiceMissingPath) { } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); } TEST_F(Text2SpeechConfigTest, VoiceInvalidFile) { @@ -317,7 +335,7 @@ TEST_F(Text2SpeechConfigTest, VoiceInvalidFile) { output_stream: "HTTP_RESPONSE_PAYLOAD:output" node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { - models_path: "/ovms/src/test/llm_testing/microsoft/speecht5_tts" + models_path: "/ovms/src/test/llm_testing/hexgrad/Kokoro-82M" plugin_config: '{"NUM_STREAMS": "1" }', target_device: "CPU" voices: [ @@ -331,8 +349,5 @@ TEST_F(Text2SpeechConfigTest, VoiceInvalidFile) { } )"; - ovms::MediapipeGraphConfig mgc{"mediaDummy", "", ""}; - DummyMediapipeGraphDefinition mediapipeDummy("mediaDummy", mgc, testPbtxt, nullptr); - mediapipeDummy.inputConfig = testPbtxt; - ASSERT_EQ(mediapipeDummy.validate(manager), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); + ASSERT_EQ(validateText2SpeechGraphConfig(manager, testPbtxt), StatusCode::MEDIAPIPE_GRAPH_CONFIG_FILE_INVALID); } diff --git a/src/test/graph_export_test.cpp b/src/test/graph_export_test.cpp index f0caa528fe..364765585e 100644 --- a/src/test/graph_export_test.cpp +++ b/src/test/graph_export_test.cpp @@ -420,10 +420,6 @@ node { node_options: { [type.googleapis.com / mediapipe.T2sCalculatorOptions]: { models_path: "./" - voices: [ - { name: "af_alloy", path: "./voices/af_alloy.bin" }, - { name: "am_adam", path: "./voices/am_adam.bin" } - ] } } } @@ -809,12 +805,6 @@ TEST_F(GraphCreationTest, textToSpeechPositiveDefault) { } TEST_F(GraphCreationTest, textToSpeechPositiveKokoro) { - // Pre-create the voices/ directory that optimum-cli would have populated for kokoro. - std::filesystem::path voicesDir = std::filesystem::path(this->directoryPath) / "voices"; - std::filesystem::create_directories(voicesDir); - { std::ofstream f(voicesDir / "af_alloy.bin"); } - { std::ofstream f(voicesDir / "am_adam.bin"); } - ovms::HFSettingsImpl hfSettings; hfSettings.task = ovms::TEXT_TO_SPEECH_GRAPH; hfSettings.exportSettings.modelName = "myModel"; diff --git a/third_party/BUILD b/third_party/BUILD index bce804d7a1..2622baa0a9 100644 --- a/third_party/BUILD +++ b/third_party/BUILD @@ -61,27 +61,16 @@ alias( ) # espeak-ng built from source via Bazel (rules_foreign_cc cmake). -# Selected on/off via the //:espeak build flag. When disabled this resolves -# to an empty cc_library so dependents can unconditionally list it. -cc_library( - name = "espeak_ng_empty", - visibility = ["//visibility:public"], -) - +# Built as a separate step in the Dockerfile (ARG ESPEAK=1/0), not as +# a dependency of the OVMS binary. alias( name = "espeak_ng", - actual = select({ - "//:espeak_on": "@espeak_ng//:espeak_ng", - "//:espeak_off": ":espeak_ng_empty", - }), + actual = "@espeak_ng//:espeak_ng", visibility = ["//visibility:public"], ) alias( name = "espeak_ng_data", - actual = select({ - "//:espeak_on": "@espeak_ng//:espeak_ng_data", - "//:espeak_off": ":espeak_ng_empty", - }), + actual = "@espeak_ng//:espeak_ng_data", visibility = ["//visibility:public"], ) \ No newline at end of file diff --git a/windows_build.bat b/windows_build.bat index eac54243e7..4c3f100075 100644 --- a/windows_build.bat +++ b/windows_build.bat @@ -49,18 +49,10 @@ IF "%~4"=="--integrity" ( set "buildWithIntegrity=" ) -:: Allow disabling espeak-ng (built from source via Bazel) by setting -:: ESPEAK=0 before invoking this script. Defaults to on. -IF "%ESPEAK%"=="0" ( - set "espeakArg=--//:espeak=off" -) ELSE ( - set "espeakArg=--//:espeak=on" -) - set "bazelStartupCmd=--output_user_root=!BAZEL_SHORT_PATH!" set "openvino_dir=!BAZEL_SHORT_PATH!/openvino/runtime/cmake" -set "buildCommand=bazel %bazelStartupCmd% build %buildWithIntegrity% %bazelBuildArgs% %espeakArg% --action_env OpenVINO_DIR=%openvino_dir% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures %buildTargets% 2>&1 | tee win_build.log" +set "buildCommand=bazel %bazelStartupCmd% build %buildWithIntegrity% %bazelBuildArgs% --action_env OpenVINO_DIR=%openvino_dir% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures %buildTargets% 2>&1 | tee win_build.log" set "setOvmsVersionCmd=python windows_set_ovms_version.py" :: Setting PATH environment variable based on default windows node settings: Added ovms_windows specific python settings and c:/opt and removed unused Nvidia and OCL specific tools. diff --git a/windows_prepare_llm_models.bat b/windows_prepare_llm_models.bat index 48f13e61b8..72cab81c98 100644 --- a/windows_prepare_llm_models.bat +++ b/windows_prepare_llm_models.bat @@ -33,7 +33,7 @@ set "RERANK_MODEL=BAAI/bge-reranker-base" set "TEXT_GENERATION_MODEL=HuggingFaceTB/SmolLM2-360M-Instruct" set "FACEBOOK_MODEL=facebook/opt-125m" set "VLM_MODEL=OpenVINO/InternVL2-1B-int4-ov" -set "TTS_MODEL=microsoft/speecht5_tts" +set "TTS_MODEL=hexgrad/Kokoro-82M" set "STT_MODEL=openai/whisper-tiny" :: Models for tools testing. Only tokenizers are downloaded. @@ -64,7 +64,7 @@ if not exist "%~1" mkdir "%~1" :: Export models -call :download_export_model_tts "%TTS_MODEL%" "text2speech" "--weight-format int4" "%~1" +call :download_export_model_tts "%TTS_MODEL%" "text2speech" "--model_type kokoro --weight-format int8" "%~1" call :download_export_model "%STT_MODEL%" "speech2text" "--weight-format int4" "%~1" call :download_openvino "%VLM_MODEL%" "%~1" OpenGVLab/InternVL2-1B call :download_export_model "%TEXT_GENERATION_MODEL%" "text_generation" "--weight-format int8" "%~1" @@ -113,11 +113,11 @@ set "model_type=%~2" set "export_args=%~3" set "repository=%~4" -if not exist "%repository%\%model%\openvino_tokenizer.bin" ( +if not exist "%repository%\%model%\openvino_model.xml" ( echo Downloading %model_type% model to %repository%\%model% directory. - python demos\common\export_models\export_model.py %model_type% --source_model "%model%" %export_args% --vocoder microsoft/speecht5_hifigan --model_repository_path %repository% + python demos\common\export_models\export_model.py %model_type% --source_model "%model%" %export_args% --model_repository_path %repository% ) else ( - echo Models file %repository%\%model%\openvino_tokenizer.bin exists. Skipping downloading models. + echo Models file %repository%\%model%\openvino_model.xml exists. Skipping downloading models. ) exit /b 0