Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Dockerfile.redhat
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Dockerfile.ubuntu
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 4 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 \
Expand Down
3 changes: 1 addition & 2 deletions common_settings.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions create_package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add also a note in building from src doc with info that espeek is optional and can be delete from the package if not needed. explain the consequences - which language is supported without it

fi
# Resolve the packaged eSpeak shared object dynamically so version bumps
# do not require touching this script.
Expand Down
15 changes: 1 addition & 14 deletions demos/common/export_models/export_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Comment on lines 92 to 98
Expand Down Expand Up @@ -503,32 +503,19 @@ 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
<destination_path>/voices/<name>.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)
model_type = task_parameters.get('model_type', 'speecht5')

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/<name>.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'):
Expand Down
26 changes: 1 addition & 25 deletions distro.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

26 changes: 26 additions & 0 deletions docs/build_from_source.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment on lines +159 to +161

- 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).
Expand Down
14 changes: 7 additions & 7 deletions prepare_llm_models.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 0 additions & 6 deletions src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
54 changes: 54 additions & 0 deletions src/audio/text_to_speech/t2s_servable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
//*****************************************************************************

#include <memory>
#include <algorithm>
#include <string>
#include <fstream>
#include <sstream>
#include <limits>
#include <stdexcept>
#include <vector>
Comment on lines 20 to +24
Comment thread
Copilot marked this conversation as resolved.

#include "openvino/genai/speech_generation/text2speech_pipeline.hpp"
#include "src/audio/text_to_speech/t2s_calculator.pb.h"
Expand All @@ -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) {
Expand All @@ -42,6 +47,35 @@ static size_t getShapeElementsCount(const ov::Shape& shape) {
return elementsCount;
}

static std::vector<std::filesystem::path> getVoiceEmbeddingPaths(const std::filesystem::path& voicesDir) {
std::vector<std::filesystem::path> 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());
}
Comment on lines +50 to +56

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()) {
Expand Down Expand Up @@ -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()) {
Expand All @@ -108,13 +144,31 @@ TtsServable::TtsServable(const std::string& modelDir, const std::string& targetD
}
ttsPipeline = std::make_shared<ov::genai::Text2SpeechPipeline>(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());
}
Comment thread
Copilot marked this conversation as resolved.

for (const auto& voice : graphVoices) {
std::filesystem::path voicePath(voice.path());
if (voicePath.is_relative()) {
voicePath = graphDir / voicePath;
}
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);
}
}
Expand Down
31 changes: 0 additions & 31 deletions src/graph_export/graph_export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir>/voices/*.bin.
std::vector<std::string> 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"
Expand All @@ -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"(}
}
})";
Expand Down Expand Up @@ -795,5 +765,4 @@ std::variant<std::optional<std::string>, Status> GraphExport::createPluginString
return std::nullopt;
}
}

} // namespace ovms
8 changes: 1 addition & 7 deletions src/test/audio/graph_tts.pbtxt
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
]
}
}
}
Loading