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:
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.hostat 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. Theovos-coreprocess the skill waits for still runs somewhere, sized for everything else. - Per-skill containers: package a single skill and
ovos-skill-launcherinto a minimal container image, alongside (not instead of) a runningovos-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