Skip to content

Omni legacy pipeline#4379

Open
dkalinowski wants to merge 32 commits into
mainfrom
omni
Open

Omni legacy pipeline#4379
dkalinowski wants to merge 32 commits into
mainfrom
omni

Conversation

@dkalinowski

@dkalinowski dkalinowski commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

CVS-191153

Copilot AI review requested due to automatic review settings July 16, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds initial “Omni” pipeline support to OVMS GenAI serving, enabling audio input (Chat Completions + Responses) and audio output generation/streaming integration, along with supporting utilities, docs, and demo clients.

Changes:

  • Introduces a new OMNI pipeline type (proto + C++ enums) and routes initialization to a new legacy Omni servable/executor/initializer.
  • Adds audio input decoding to the LLM input-processing pipeline and extends OpenAI-compatible request parsing for input_audio and output modalities/audio.
  • Refactors audio_utils into a dedicated namespace and provides non-resampling decode helper used by Omni audio input.

Reviewed changes

Copilot reviewed 34 out of 36 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/test/audio/text2speech_test.cpp Updates tests to use ovms::audio_utils namespace.
src/test/audio/audio_utils_test.cpp Updates tests to use ovms::audio_utils namespace.
src/llm/servable_initializer.hpp Adds PipelineType::OMNI.
src/llm/servable_initializer.cpp Detects OMNI models and initializes Omni legacy servable.
src/llm/omni_model/legacy/servable.hpp Declares Omni legacy servable/context/properties.
src/llm/omni_model/legacy/servable.cpp Implements Omni request lifecycle, audio response injection, streaming handling.
src/llm/omni_model/legacy/servable_initializer.hpp Declares Omni legacy servable initializer.
src/llm/omni_model/legacy/servable_initializer.cpp Builds ov::genai::OmniPipeline + tokenizer and config.
src/llm/omni_model/legacy/legacy_executor.hpp Declares Omni legacy executor + wrapper thread.
src/llm/omni_model/legacy/legacy_executor.cpp Implements Omni generation execution + speech streaming callback.
src/llm/llm_calculator.proto Adds OMNI to pipeline enum for graph options.
src/llm/io_processing/input_processors/image_decoding_processor.cpp Preserves multimodal part ordering when non-image parts exist (e.g., audio).
src/llm/io_processing/input_processors/audio_decoding_processor.hpp Adds new processor to extract input_audio into tensors.
src/llm/io_processing/input_processors/audio_decoding_processor.cpp Implements base64 decode + audio parsing to ov::Tensor.
src/llm/io_processing/input_processor.cpp Wires AudioDecodingProcessor into pipeline for Omni chat requests.
src/llm/io_processing/input_processing_config.hpp Adds isOmni config flag.
src/llm/BUILD Adds Omni + audio decoding sources and deps to Bazel targets.
src/llm/apis/openai_responses.cpp Supports input_audio items and serializes streamed audio delta events.
src/llm/apis/openai_request.hpp Adds audio output request fields to request model.
src/llm/apis/openai_completions.cpp Validates input_audio content parts for chat completions.
src/llm/apis/openai_api_handler.cpp Parses modalities + audio output config.
src/audio/text_to_speech/t2s_calculator.cc Qualifies audio utils calls with ovms::audio_utils.
src/audio/speech_to_text/s2t_calculator.cc Qualifies audio utils calls with ovms::audio_utils.
src/audio/audio_utils.hpp Moves audio helpers into ovms::audio_utils and adds readWithoutResample.
src/audio/audio_utils.cpp Implements readWithoutResample and wraps implementation in namespace.
OMNI_PLAN.md Documents design/plan and API mapping for Omni integration.
OMNI_AUDIO_OUTPUT.md Documents audio output implementation and streaming event format.
demos/omni/omni_voice_chat.py Adds a voice-to-voice demo client (record + stream + playback).
demos/omni/omni_stream_playback.py Adds streaming playback demo (text then audio deltas).
demos/omni/omni_responses_client.py Adds minimal Responses API audio-input demo client.
demos/omni/omni_full_client.py Adds full multimodal demo (audio+image → text+audio).
demos/omni/omni_client.py Adds minimal Chat Completions audio-input demo client.
demos/omni/omni_audio_output_client.py Adds Chat Completions audio-output demo client.
demos/omni/.gitignore Ignores generated WAVs in demos.

Comment thread src/llm/omni_model/legacy/legacy_executor.cpp
Comment thread src/llm/omni_model/legacy/legacy_executor.cpp
Comment thread src/llm/omni_model/legacy/legacy_executor.cpp
Comment thread src/llm/io_processing/input_processor.cpp Outdated
Comment thread src/audio/audio_utils.cpp Outdated
Comment thread src/audio/audio_utils.cpp Outdated
Comment thread src/llm/omni_model/legacy/servable.cpp Outdated
Comment on lines +257 to +265
if (responseDoc.HasMember("choices") && responseDoc["choices"].IsArray() && responseDoc["choices"].Size() > 0) {
// Chat Completions format: inject into choices[0].message.audio
auto& message = responseDoc["choices"][0]["message"];
rapidjson::Value audioObj(rapidjson::kObjectType);
audioObj.AddMember("data", rapidjson::Value(audioBase64.c_str(), alloc), alloc);
std::string transcript = message.HasMember("content") && message["content"].IsString() ? message["content"].GetString() : "";
audioObj.AddMember("transcript", rapidjson::Value(transcript.c_str(), alloc), alloc);
message.AddMember("audio", audioObj, alloc);
} else if (responseDoc.HasMember("output") && responseDoc["output"].IsArray()) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it will base on executionContext->apiHandler->getEndpoint()

Comment thread src/llm/servable_initializer.cpp Outdated
Comment on lines +458 to +462
// Existence of talker model indicates omni pipeline
bool isOmni = std::filesystem::exists(parsedModelsPathFs / "openvino_talker_model.xml");
// Existence of embeddings models indicates we are dealing with VLM pipeline
bool isVLM = (std::filesystem::exists(parsedModelsPathFs / "openvino_text_embeddings_model.xml") &&
std::filesystem::exists(parsedModelsPathFs / "openvino_vision_embeddings_model.bin"));
std::filesystem::exists(parsedModelsPathFs / "openvino_vision_embeddings_model.bin")) && !isOmni;
Comment thread src/audio/audio_utils.cpp
Comment thread src/llm/apis/openai_api_handler.cpp
const auto part = content[j];
const auto type = part["type"].as_string().value_or("");
if (type == "image_url") {
ov::genai::JsonContainer textEntry({{"type", "text"}, {"text", imageTagByPart[j]}});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it will work only for omni i think, needs to be changed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread demos/omni/chat_completions.py Outdated
Comment thread demos/omni/responses.py Outdated
Comment thread docs/omni.md Outdated
Comment thread docs/omni.md Outdated
Comment on lines +18 to +19
├── openvino_model.xml # thinker (VLM)
├── openvino_talker_model.xml # talker (speech)

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.

Why the comments? If we add comments to those two models, why not for other XMLs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I wanted to showcase whats new, it is not really user facing documentation yet I would say, updated to this for now:

image

but after we merge it we should add sphinx doc and actual demo similar to our other demos

Comment thread docs/omni.md Outdated
Comment on lines +186 to +190
// Copy audio output config from parsed request to execution context
const auto& req = omniExecutionContext->apiHandler->getRequest();
omniExecutionContext->audioOutputRequested = req.audioOutputRequested;
omniExecutionContext->audioVoice = req.audioVoice;
omniExecutionContext->audioFormat = req.audioFormat;

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.

Is that the only omni specific part in this method? If so, could we call base class parseRequest on this and then cast context to omni and fill remaining fields?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we should do it in next PR - it is related to VLMPipeline & OmniPipeline cleanup

Comment on lines +211 to +221
absl::Status OmniModelLegacyServable::readCompleteExecutionResults(std::shared_ptr<GenAiServableExecutionContext>& executionContext) {
auto omniExecutionContext = std::static_pointer_cast<OmniModelLegacyServableExecutionContext>(executionContext);
if (omniExecutionContext->payload.client->isDisconnected()) {
return absl::CancelledError();
}
omniExecutionContext->finished.wait();
if (!omniExecutionContext->success) {
return absl::InvalidArgumentError("Request processing failed, check its correctness.");
}
return absl::OkStatus();
}

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.

Do we need implementation of that? Looks kind of generic - wouldn't base class be able to handle this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we should do it in next PR - it is related to VLMPipeline & OmniPipeline cleanup

Comment on lines +302 to +305
absl::Status OmniModelLegacyServable::readPartialExecutionResults(std::shared_ptr<GenAiServableExecutionContext>& executionContext) {
executionContext->deltaChannel.waitForData();
return absl::OkStatus();
}

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.

Same, looks very generic, not sure if we need implementation here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we should do it in next PR - it is related to VLMPipeline & OmniPipeline cleanup


namespace ovms {

struct OmniModelLegacyServableExecutionContext : public GenAiServableExecutionContext {

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.

How about VLM servable execution context as base? Have you considered that?
Same for all other classes. Maybe it would allow us to reuse some more generic methods?

Comment thread new_class_diagram.png Outdated
Comment thread src/llm/omni_model/legacy/servable.cpp Outdated
std::string transcript = message.HasMember("content") && message["content"].IsString() ? message["content"].GetString() : "";
audioObj.AddMember("transcript", rapidjson::Value(transcript.c_str(), alloc), alloc);
message.AddMember("audio", audioObj, alloc);
} else if (responseDoc.HasMember("output") && responseDoc["output"].IsArray()) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

probably base on apihandler->api type or something like that

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

// Shape: {"delta":{...}} for content/reasoning/tool_calls, or an empty Document{}
// for finish-only chunks. Inspect the delta directly — no parsing needed here.
if (parsedDelta.HasMember("delta") && parsedDelta["delta"].IsObject()) {
// for finish-only chunks, or {"_audio_delta":"<base64>"} for audio chunks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

the comment should be moved below

@dkalinowski dkalinowski Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it still applies in general, so i extended it

Comment thread src/audio/audio_utils.hpp Outdated

namespace ovms::audio_utils {

static constexpr uint32_t PIPELINE_SUPPORTED_SAMPLE_RATE = 16000;

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.

Will read methods work with custom sampling rate?
This name is also quite generic - what pipeline?

Comment thread src/audio/audio_utils.cpp
Comment on lines +307 to +315
std::vector<float> readWithoutResample(const std::string_view& audioData, const std::string& format) {
if (format == "wav") {
return readWav(audioData, 0);
} else if (format == "mp3") {
return readMp3(audioData, 0);
} else {
throw std::runtime_error("Unsupported audio format: " + format);
}
}

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.

If 0 is a special value to disable resampling, maybe we should make it a const and somehow document (like explicitly in the comment)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is documented in code:

// Decode WAV data into mono float32 PCM samples.
// If targetSampleRate > 0, resamples to that rate. Otherwise returns at native sample rate.
std::vector<float> readWav(const std::string_view& wavData, uint32_t targetSampleRate = PIPELINE_SUPPORTED_SAMPLE_RATE);

// Decode MP3 data into mono float32 PCM samples.
// If targetSampleRate > 0, resamples to that rate. Otherwise returns at native sample rate.
std::vector<float> readMp3(const std::string_view& mp3Data, uint32_t targetSampleRate = PIPELINE_SUPPORTED_SAMPLE_RATE);

but I will make 0 a constexpr RESAMPLE_DISABLED

Comment thread src/audio/audio_utils.cpp
Comment thread src/llm/apis/openai_api_handler.cpp Outdated
}
}
request.audioOutputRequested = hasAudio;
// When modalities is explicitly provided and "text" is absent, suppress text in response

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.

Suggested change
// When modalities is explicitly provided and "text" is absent, suppress text in response
// When "modalities" is explicitly provided and "text" is absent, suppress text in response

Comment on lines +776 to +780
if (modality == "audio") {
hasAudio = true;
} else if (modality == "text") {
hasText = 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.

No images/video modalities?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

These modalities determine what model is capable of generating. We dont know any llm model that is capable of generating images. And it is not supported by OpenAI API anyway

Comment on lines 25 to +28
bool isVLM = false;
// True for Omni servables. Enables AudioDecodingProcessor in addition to
// ImageDecodingProcessor (implies isVLM-like behavior for images).
bool isOmni = false;

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.

I can see possibility that we will serve models that will accept audio input and will not be Omni. Like text, image, audio on the input and just text on the output. That's not Omni I suppose, but still requires audio processing. I'm thinking about renaming isVLM, isOmni to something explicit like supportsVisualInput, supportsAudioInput.
That naming would remove pipeline type awareness from the scope and focus on input data types.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is good direction, but lets do it once we have actual models that are capable of that

Comment on lines +53 to +57
if (context.config.isOmni) {
processors.emplace_back(std::make_unique<AudioDecodingProcessor>());
}

processors.emplace_back(std::make_unique<TextContentNormalizationProcessor>());

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.

We have a comment in input processor header I believe where we mention input processor ordering. Could you update it as well to reflect the new ordering?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment thread src/llm/llm_calculator.proto Outdated
Comment on lines +81 to +84
// Default option. If selected, pipeline type will be inferred by the serving
AUTO = 4;
// Omni model (text, image, audio input; text + speech output) based on OmniPipeline
OMNI = 5;

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.

I would prefer the default option to be first or last so it stands out among others. Would it be possible to for example move AUTO to the top with 0 identifier? Or would we experience some compatibility issues?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I dont think it will introduce any compatibility issues - I will change it

Comment thread src/llm/servable_initializer.cpp Outdated
std::filesystem::exists(parsedModelsPathFs / "openvino_vision_embeddings_model.bin"));
bool hasEmbeddingsModels = (std::filesystem::exists(parsedModelsPathFs / "openvino_text_embeddings_model.xml") &&
std::filesystem::exists(parsedModelsPathFs / "openvino_vision_embeddings_model.bin")) &&
!hasTalkerModel;

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.

Does talker model existence automatically means model cannot have embedding models?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, I will change the comment to:

// Existence of embeddings models indicates we are dealing with VLM pipeline
    // But if it has talker model, it means it is Omni pipeline which is built out of VLM Pipeline and Talker

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.

Still, the comment does not resolve the way hasEmbeddingsModels means. With that change it is dependent of hasTalkerModel which blurs the meaning.
Maybe we need another bool here? Or change conditions to explicitly if(hasEmbeddingsModels && !hasTalkerModel) then VLM

SPDLOG_LOGGER_ERROR(modelmanager_logger, "Error during LLM node resources initialization: {}", status.string());
return status;
}
} else if (pipelineType == PipelineType::OMNI) {

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.

Don't we need similar validation as for VLM to match behavior? For example if someone sets Omni explicitly, but there is no talker model in the catalog.

@dkalinowski dkalinowski Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

When someone forces --pipeline_type VLM but the model is LLM:

Models directory content indicates non-VLM pipeline, but pipeline type is set to VLM type.

When someone forces --pipeline_type OMNI but the model is VLM:

Models directory content indicates VLM pipeline, but pipeline type is set to non-VLM type.

When someone forces --pipeline_type VLM but the model is OMNI:

Models directory content indicates non-VLM pipeline, but pipeline type is set to VLM type.

I think it works as expected

@dkalinowski dkalinowski changed the title Omni Omni legacy pipeline Jul 23, 2026
Comment thread src/config.cpp Outdated
const auto& exportSettings = this->serverSettings.hfSettings.exportSettings;
auto textGenSettings = std::get<TextGenGraphSettingsImpl>(this->serverSettings.hfSettings.graphSettings);
std::vector allowedPipelineTypes = {"LM", "LM_CB", "VLM", "VLM_CB", "AUTO"};
std::vector allowedPipelineTypes = {"LM", "LM_CB", "VLM", "VLM_CB", "AUTO", "OMNI"};

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.

Can we keep the ordering as in the graph? So AUTO at the end?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

const auto status = processor.process(req);

EXPECT_TRUE(status.ok());
EXPECT_TRUE(req.inputAudios.empty());

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.

can we also check that processor did not remove anything, so the history is intact

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

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.

Have you considered a test with image, audio and normalization processor working together?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants