Skip to content

Composable Deployments: OVOS as a Library

In a nutshell

OVOS ships as a "batteries-included" assistant you can install with one command. Under the hood, it is a set of small, independent Python packages that agree on a common protocol, the messagebus. Because of that, nothing forces you to run "all of OVOS" as one blob.

You can start a single service on its own machine, load a single skill as its own process, or pip install a speech plugin into a separate Python project and call it directly, with no bus and no assistant around it at all. This page covers that second use case: OVOS the library, not OVOS the product.

The principle

Every OVOS service, including the bus, the voice pipeline, the audio player, the hardware abstraction layer, and the GUI, is an ordinary long-running Python process. They do not call each other's functions or share memory. They exchange JSON Message objects over a WebSocket.

Anything that can open that WebSocket and speak the same message types is a full participant, whether it is ovos-core itself, a HiveMind satellite, a shell script, or a Node.js prototype. This makes the two deployment styles below equally valid:

Style What you get Typical use
Batteries-included install ovos-core metapackage pulls in the bus, listener, audio, PHAL and GUI services and a process manager starts them all on one host A voice appliance, raspOVOS, a desktop assistant
À la carte library use You install and import only the pieces you need — one service, one skill, or a single plugin as a bare Python object Distributed/embedded deployments, custom apps, testing one component in isolation

Nothing in the code enforces the first style. It is a packaging convenience (ovos-core is a metapackage), not an architectural requirement. The Architecture Overview covers how these services cooperate at runtime. This page covers how to run them apart.

The standard split: one console script per service

Each core service is its own PyPI package with its own console-script entry point. Installing a package makes the script available on PATH. Running it starts that one process and nothing else.

Service Package Console script Role
messagebus ovos-messagebus ovos-messagebus The WebSocket hub every other process connects to
Core / orchestrator ovos-core ovos-core Pipeline matching, skill loading, intent dispatch
Voice pipeline ovos-dinkum-listener ovos-dinkum-listener Wake word, VAD, STT, produces recognizer_loop:utterance
Audio playback ovos-audio ovos-audio TTS synthesis and playback, speak handling
Hardware abstraction ovos-PHAL ovos_PHAL (underscore) Battery, network, LEDs, and other device-specific integrations
PHAL admin actions ovos-PHAL ovos_PHAL_admin Privileged system actions (shutdown/reboot) run as a separate process
GUI protocol server ovos-gui ovos-gui-service Serves the GUI protocol to display clients
Standalone intent matching ovos-core ovos-intent-service Pipeline matching without the rest of core's skill orchestration
Standalone skill installer ovos-core ovos-skill-installer Installs skills without a running ovos-core

Names are exact

The console scripts are not all named consistently with their package. Notably, ovos-PHAL installs ovos_PHAL and ovos_PHAL_admin with underscores, and the GUI service script is ovos-gui-service, not ovos-gui. Check pip show -f <package> if a script is not found.

Pointing services at a shared bus

For a step-by-step build of a server-plus-satellites deployment, see Satellites.

Every process reads the websocket block from its own configuration to find the bus:

{
  "websocket": {
    "host": "127.0.0.1",   // change to the bus host's LAN address/hostname
    "port": 8181,
    "route": "/core",
    "ssl": false
  }
}

The default is 127.0.0.1. Every service assumes the bus is local unless you say otherwise. To split services across hosts or containers, run ovos-messagebus on one host. Then set websocket.host to that host's address in the configuration of every other service. No other wiring is needed. A listener on one machine, ovos-core on a second, and ovos-audio on a third cooperate exactly as if they were on the same box, as long as each can reach the bus's host:port. The GUI service has its own analogous gui_websocket block, since display clients connect over a second WebSocket.

Loopback by default

websocket.host (bus) and most PHAL plugin bindings default to loopback or same-host assumptions. Distributing those services is a deliberate config change, not the out-of-the-box behavior. See Caveats below.

gui_websocket.host ships as 127.0.0.1, like the bus. Widen it to 0.0.0.0 only when a display client genuinely runs on another machine — doing so exposes the unauthenticated GUI protocol socket to the LAN.

Running one skill in its own process

A skill can run outside ovos-core's own process, in its own process on the same machine or a different one — but it still needs a reachable, running ovos-core, since it waits for that core's skill manager to answer mycroft.skills.is_ready before it loads. ovos-workshop ships a launcher for this:

ovos-skill-launcher {skill_id} [path/to/skill/directory]

This is the ovos-skill-launcher console script, backed by ovos_workshop.skill_launcher.SkillContainer. If you omit the directory, it searches the standard skill directories for a folder matching skill_id. If you pass one explicitly, it loads from there. This is convenient for developing a skill straight out of a git checkout.

from ovos_workshop.skill_launcher import SkillContainer

skill = SkillContainer(skill_id="my-skill.openvoiceos", skill_directory="./my-skill")
skill.run()  # connects to the bus, waits for ovos-core's skill manager to
              # report ready, then loads only this skill

Because this is a normal Python process that only needs a reachable bus and a running ovos-core, it enables:

  • Developing one skill against a remote device: run the skill on a laptop, point its websocket.host at a Raspberry Pi running the rest of the stack, and iterate without touching the device.
  • Resource isolation: a heavyweight skill (for example, one embedding a local LLM) runs in its own process with its own memory ceiling, instead of sharing ovos-core's process. The ovos-core process the skill waits for still runs somewhere, sized for everything else.
  • Per-skill containers: package a single skill and ovos-skill-launcher into a minimal container image, alongside (not instead of) a running ovos-core, so a skill can be deployed, scaled, or restarted independently of the rest of the assistant.

Library reuse outside OVOS

The pattern goes further than "one service, one process". The packages underneath each service are ordinary importable libraries with no hard dependency on a bus being present.

HiveMind satellites import audio/listener internals directly. hivemind-mic-satellite (a HiveMind client, not an OVOS-org package) depends on ovos-audio and ovos-plugin-manager and imports their classes directly instead of talking to a running ovos-audio process:

from ovos_audio.audio import AudioService
from ovos_audio.playback import PlaybackThread
from ovos_plugin_manager.microphone import OVOSMicrophoneFactory, Microphone
from ovos_plugin_manager.vad import OVOSVADFactory, VADEngine
from ovos_PHAL.service import PHAL

It reuses the audio-playback and microphone/VAD machinery as plain Python classes inside its own process, then relays results to the hive over its own protocol. No ovos-audio process is ever started.

OPM plugins run as plain libraries, with no bus at all. The OpenVoiceOS Plugin Manager (ovos-plugin-manager, "OPM") factories construct STT/TTS/VAD engines from configuration. Nothing about the returned object requires a bus:

from ovos_plugin_manager.vad import OVOSVADFactory
import numpy as np

vad = OVOSVADFactory.create({"module": "ovos-vad-plugin-noise"})
audio = (np.random.rand(1600) * 100).astype("int16").tobytes()
vad.is_silence(audio)  # -> False, plain function call, no messagebus involved

The same applies to OVOSSTTFactory.create(config) and OVOSTTSFactory.create(config). Both return an engine object you call directly (.execute(audio), .get_tts(text, path)) in a script, a notebook, or a separate application that has nothing to do with voice assistants.

ovos-bus-client and ovos-utils are building blocks in their own right. ovos-bus-client supplies MessageBusClient/Message (and the ovos-listen/ovos-speak/ovos-say-to CLI tools, see CLI Tools) for anything that wants to talk to an existing OVOS bus without being a skill or a service. ovos-utils supplies logging and audio I/O helpers, and also ovos_utils.fakebus.FakeBus, an in-memory stand-in used to exercise skill/plugin code in tests without any network socket at all. See Core Libraries for the full map of these packages and their upstream docs.

Containers: one service per box

ovos-docker mirrors the same split at the container level: one image per service, wired together with compose files. See Running OVOS in Containers for the compose layout, audio device passthrough, and the networking rules those containers run under.

The same idea shows up as standalone servers for individual speech components, packaged as their own containers/services rather than as OVOS services at all:

Server What it exposes
ovos-stt-server An HTTP/WebSocket wrapper around an STT plugin — POST audio, get text back
ovos-tts-server An HTTP wrapper around a TTS plugin — POST text, get audio back
ovos-translate-server An HTTP wrapper around a machine-translation plugin

These have no bus dependency and no notion of "skills" or "sessions". They are thin HTTP front-ends over the same OPM factories shown above, useful when another application (OVOS or not) just needs a speech primitive over the network.

Example topology

graph LR
    subgraph "Host A — bus"
        BUS[ovos-messagebus]
    end
    subgraph "Host B — voice pipeline"
        LISTENER[ovos-dinkum-listener<br/>+ STT plugin]
        AUDIO[ovos-audio<br/>+ TTS plugin]
    end
    subgraph "Host C — brain"
        CORE[ovos-core<br/>+ skills]
        SKILL[ovos-skill-launcher<br/>my-skill, isolated]
    end
    subgraph "Host D — device I/O"
        PHAL[ovos_PHAL]
        GUI[ovos-gui-service]
    end
    subgraph "Off-network"
        HIVE[hivemind-core]
        SAT[hivemind-mic-satellite<br/>imports ovos-audio directly]
    end

    LISTENER <-- websocket.host=A --> BUS
    AUDIO <-- websocket.host=A --> BUS
    CORE <-- websocket.host=A --> BUS
    SKILL <-- websocket.host=A --> BUS
    PHAL <-- websocket.host=A --> BUS
    GUI <-- websocket.host=A --> BUS
    HIVE <-- websocket.host=A --> BUS
    SAT -. HiveMind protocol .-> HIVE

Diagram: an example four-host topology where the listener, audio, core, skill, PHAL, GUI, and hivemind-core services on Hosts A-D all connect to one shared messagebus on Host A, while a remote hivemind-mic-satellite reaches hivemind-core over the separate HiveMind protocol.

Two instances of one service on one bus is not failover

ovos-core, ovos-audio, and ovos-dinkum-listener are each written assuming they are the only instance of that service talking to a given bus. The bus is pure fan-out with no leader election or ownership concept, so it cannot tell two ovos-core processes apart. Running two instances of the same service against one bus does not give you redundancy. It gives you duplicate handling of every message and double-emitted lifecycle events. See Services are implicit singletons per bus below. Scale horizontally with HiveMind satellites instead.

Version skew across a split deployment is a related risk: there is no central version negotiation, so mismatched major versions of ovos-bus-client, ovos-core, ovos-audio, and ovos-dinkum-listener can produce message shapes one side doesn't expect. See Version skew is a real risk below.

Multi-tenant hosting is not a supported pattern

One OVOS core serving several isolated households or accounts — separate skill sets, separate configs, separate data, one shared process — is not something the stack supports. Config, skill loading, and the "default" session are all singletons per core. The supported shape for serving many users is one core (or container stack) per tenant, with HiveMind satellites connecting each household's thin clients to its own core.

Trust boundary: the bus and HIVE links are localhost/LAN only

Every <-- websocket.host=A --> link in the diagram above (including the HIVE connection) is a direct connection to the raw messagebus and must stay on a trusted localhost/LAN network. It is never meant to be exposed to, or reachable from, the open internet. See Bus Service (see its "Security: the bus has no authentication" note) for why the bus itself is a trust boundary. The only link in this topology designed to cross an untrusted network is the dotted SAT -. HiveMind protocol .-> HIVE edge. The HiveMind satellite talks to hivemind-core over HiveMind's own authenticated protocol, not the raw bus. This is what makes it safe to run a satellite from a network you don't otherwise trust.

See Privacy & Security for the full trust model.

Caveats

Splitting services this way is fully supported, but it moves responsibilities that a single-host install hides for you. These are the most common sources of confusion; expand any one that bites you.

Plugins must be installed where they load

A plugin is only usable by the process that imports it. Installing an STT plugin next to ovos-core does nothing if ovos-dinkum-listener is the process that needs it.

Plugin type Loaded by Must be installed in
STT ovos-dinkum-listener The listener's environment/container
TTS ovos-audio The audio service's environment/container
VAD, wake word, microphone ovos-dinkum-listener The listener's environment/container
Pipeline (intent matching), skills ovos-core The core's environment/container
PHAL plugins ovos_PHAL The PHAL service's environment/container
GUI adapters ovos-gui-service The GUI service's environment/container
Transformer (opm.transformer.*) ovos-core (utterance/metadata/intent chains), ovos-dinkum-listener (audio chain), ovos-audio (dialog/tts chains) Whichever of those services' environments runs the chain that plugin belongs to

There is no cross-process plugin discovery. Each service resolves plugins from its own Python environment's entry points at startup.

Configuration is per-process, not shared

Each process reads its own mycroft.conf from its own XDG config path ($XDG_CONFIG_HOME/mycroft; inside a container, that is the container's filesystem, not the host's). Splitting services means keeping the relevant keys consistent by hand across every process's configuration. A websocket.host mismatch, or a listener that doesn't know which STT module ovos-core expects it to have already run, will silently misbehave rather than error loudly.

Version skew is a real risk

See the danger box above for the short version. Every process talks over the same bus protocol independently. There is no central version negotiation. Mismatched major versions across ovos-bus-client, ovos-core, ovos-audio, and ovos-dinkum-listener can produce message shapes one side doesn't expect. Keep versions aligned across a deployment, and check each package's changelog before upgrading only one service.

Latency and network reality

A single-host install exchanges messages over loopback, effectively free. Splitting services across hosts puts real network latency and reliability on the critical path of every utterance. Wake-word detection, STT, intent matching, and TTS all round-trip through the bus. A slow or lossy link between the listener and the bus is felt as sluggish or dropped voice interactions, not as an error message.

Services are implicit singletons per bus

See the danger box above for the short version. ovos-core, ovos-audio, and ovos-dinkum-listener are each written assuming they are the only instance of that service talking to a given bus. The bus itself is pure fan-out with no leader election or ownership concept. It has no way to tell two ovos-core processes apart or arbitrate between them.

Running two instances of the same service against one bus does not give you redundancy or failover. It gives you duplicate handling of every message and double-emitted lifecycle events (for example, two ovos.intent.handler.start/.complete pairs for one utterance), since both instances react to the same broadcast independently.

For a step-by-step build, see Satellites.

There is no supported skill-level or bus-level workaround for this. Filtering handlers by session_id inside a skill does not make it safe to run two instances of a singleton service against the same bus. The duplication happens at the message-dispatch level, before any skill code runs, so every subscriber on both instances still receives and reacts to every message.

Horizontal scaling for multiple devices or users is a distributed-deployment concern, not a duplicate-singleton one. It is done with HiveMind satellites talking to a single HiveMind server, not by running two copies of ovos-core, ovos-audio, or ovos-dinkum-listener against one bus.

Defaults assume localhost

websocket.host (127.0.0.1), and most PHAL device-integration plugins, assume everything they talk to is on the same machine. Treat every default as loopback-only until you have explicitly verified the config for a given deployment. The services start and look healthy on separate hosts with the defaults untouched, but they simply cannot reach each other.

For a step-by-step build, see Satellites.


Read next: Configuration Overview · Concepts Overview Related: Bus Service · Core Libraries · Remote Agents with HiveMind · Install raspOVOS