OCP Skills¶
In a nutshell
OCP (OVOS Common Playback) is the part of OVOS that handles playing media, like music, podcasts, or radio. An OCP skill does not listen for "play X" itself. Instead it acts as a source of media.
When someone asks to play something, OVOS asks every OCP skill "can you find this?" Each skill answers with whatever it can offer and how good a match it thinks it is. OVOS plays the best result. It works like asking several record shops for an album and going with whoever has the closest match. New terms are explained in the Glossary.
Just want to play music or radio? Install a ready-made skill instead of writing one. See What Can I Say? Music & Radio. This page is for developers writing a new OCP media skill.
What OCP means here
"OCP" names three different things in OVOS. Know which one a page is about:
- The OCP pipeline plugin: matches utterances like "play some jazz" to a media request. See OCP Pipeline.
- The OCP skill base class:
OVOSCommonPlaybackSkill; skills built on it provide or embody media for the pipeline to find. See OCP Skills. - The legacy OCP audio plugin:
ovos-plugin-common-play, the current default playback engine running insideovos-audio. See The OCP Audio Plugin.
OCP skills are giving way to MediaProvider plugins
OCP skills (media-provider skills built on OVOSCommonPlaybackSkill /
@ocp_search) still work and remain fully supported. The intended
successor is a dedicated MediaProvider plugin type (opm.media.provider). The
entry-point group is defined in ovos-plugin-manager, and the design is for the
ovos-media player to load such plugins in-process and call
search() on them directly, instead of broadcasting a query over the bus to skills.
That in-process loading is not wired up in ovos-media yet, so writing an OCP skill
remains the way to provide media today, and stays the simpler path for setups still on
the legacy audio service.
The split also changes what OCP skills are for. Once MediaProvider plugins own
catalog search, an OCP skill is for the case where the skill itself is the playable
media: a voice game (see ovos-skill-moon-game, a VoiceGamez title not currently public),
an ebook reader, any experience the player can start, pause, and resume like a track.
If your skill is only a searchable catalog of external media (a station list, a
podcast feed), plan to ship it as a MediaProvider plugin when that lands. If the
skill is the thing being played, it stays an OCP skill.
📐 Formal specification
OCP is specified by OVOS-OCP-1: OVOS Common Playback: the Virtual Media Player (a formal architecture spec). The spec defines a single per-session Virtual Media Player. It is one arbitration point that owns the session's now-playing track, queue, and transport state. It is addressed over the ovos.common_play.* bus surface with an MPRIS-style control set (play / search / pause / resume / next / previous / seek / stop) and a three-axis state model (PlayerState, MediaState, loop/shuffle). It can even be bridged to host-OS MPRIS players so voice controls media OVOS did not start.
This page covers the provider side: the @ocp_search skills that feed candidate media into that player. The player and its control surface are the spec's subject. Both this provider model and OVOS-OCP-1's player are current. The ovos-media refactor (see the note above) adds opm.media.provider plugins as an alternative to skill-based providers.
OCP (OVOS Common Playback) skills are built from the OVOSCommonPlaybackSkill class.
What / why (beginners): an OCP skill is a media provider. You do not write intents like "play X". OCP owns the "play music / play a podcast / play the radio" voice interaction. Your skill only answers the question "given this search phrase, what can you play?". You decorate one or more search methods with @ocp_search. Each one returns a list (or yields a stream) of result dicts with a confidence score. OCP picks the best match across every installed OCP skill and handles the actual playback, queueing and GUI.
from ovos_utils.ocp import MediaType, PlaybackType
from ovos_workshop.decorators.ocp import ocp_search, ocp_featured_media
from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill
MediaTypeandPlaybackTypeare imported fromovos_utils.ocp. The OCP decorators (ocp_search,ocp_featured_media,ocp_play,ocp_pause,ocp_resume,ocp_next,ocp_previous) live inovos_workshop.decorators.ocp.
Search Results¶
Search results are returned as a list of dicts. Skills can also use iterators to yield results one at a time as they become available.
Mandatory fields are:
uri: str # URL/URI of media, OCP will handle formatting and file handling
title: str
media_type: MediaType
playback: PlaybackType
match_confidence: int # 0-100
Other optional metadata includes artists, album, length and images for the GUI:
artist: str
album: str
image: str # uri/file path
bg_image: str # uri/file path
skill_icon: str # uri/file path
length: int # seconds, -1 for unknown/live streams (the MediaEntry search-result convention; the MediaBackend player-position API uses milliseconds instead)
The OCP search results GUI, with each
MediaEntry field labeled next to the part of the card it fills in.
The title and skill ID show as text. Duration shows as the elapsed-time readout. The image, skill icon, and background image show as the thumbnail, corner icon, and card background.
OCP Skill¶
General steps to create a skill:
-
subclass your skill from
OVOSCommonPlaybackSkill -
Pass
supported_mediatosuper().__init__()to indicate the media types you want to handle (MediaType/PlaybackTypelive inovos_utils.ocp) -
self.voc_match(phrase, "skill_name")to handle specific requests for your skill -
self.remove_voc(phrase, "skill_name")to remove matched phrases from the search request. For example,self.remove_voc("play some somafm radio", "somafm")strips thesomafmvocab match and returns"play some radio"(the removed word's surrounding whitespace stays, so a double space remains), and the rest of your matching logic scores against the cleaned-up phrase instead of the raw one -
Implement the
ocp_searchdecorator, as many as you want (within your skill they run sequentially, one at a time, in no guaranteed order; parallelism happens across different OCP skills answering on the bus, not across your own search methods) -
The decorated method can return a list or be an iterator of
result_dict(track or playlist) -
The search function can be entirely inline or call another Python library, like pandorinha or plexapi
-
self.extend_timeout()to delay OCP from selecting a result, requesting more time to search -
Implement a confidence score formula
-
Values are between 0 and 100
-
Results below the
min_scorethreshold are filtered out before OCP picks a winner across all responding skills. The plugin readsintents.ovos-ocp-pipeline-plugin.min_score, and the shippedmycroft.confsets exactly that key to 40 — so40is the threshold in force by default (the plugin's code default of 50 applies only if the key is absent, andintents.OCPis a shadowed back-compat fallback; see OCP Pipeline). No confidence value short-circuits the search or cancels other skills early. A higher score only makes your result more likely to win the cross-skill comparison -
ocp_featured_media: return a playlist for the OCP menu if selected from GUI (optional) -
Create a
requirements.txtfile with third-party package requirements
from os.path import join, dirname
import radiosoma
from ovos_utils import classproperty
from ovos_utils.ocp import MediaType, PlaybackType
from ovos_utils.parse import fuzzy_match
from ovos_workshop.decorators.ocp import ocp_search, ocp_featured_media
from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill
class SomaFMSkill(OVOSCommonPlaybackSkill):
def __init__(self, *args, **kwargs):
super().__init__(
supported_media=[MediaType.MUSIC, MediaType.RADIO],
skill_icon=join(dirname(__file__), "ui", "somafm.png"),
*args, **kwargs)
@ocp_featured_media()
def featured_media(self):
# playlist when selected from OCP skills menu
return [{
"match_confidence": 90,
"media_type": MediaType.RADIO,
"uri": ch.direct_stream,
"playback": PlaybackType.AUDIO,
"image": ch.image,
"bg_image": ch.image,
"skill_icon": self.skill_icon,
"title": ch.title,
"artist": "SomaFM",
"length": 0
} for ch in radiosoma.get_stations()]
@ocp_search()
def search_somafm(self, phrase, media_type):
# check if user asked for a known radio station
base_score = 0
if media_type == MediaType.RADIO:
base_score += 20
else:
base_score -= 30
if self.voc_match(phrase, "radio"):
base_score += 10
phrase = self.remove_voc(phrase, "radio")
if self.voc_match(phrase, "somafm"):
base_score += 30 # explicit request
phrase = self.remove_voc(phrase, "somafm")
for ch in radiosoma.get_stations():
score = round(base_score + fuzzy_match(ch.title.lower(),
phrase.lower()) * 100)
if score < 50:
continue
yield {
"match_confidence": min(100, score),
"media_type": MediaType.RADIO,
"uri": ch.direct_stream,
"playback": PlaybackType.AUDIO,
"image": ch.image,
"bg_image": ch.image,
"skill_icon": self.skill_icon,
"title": ch.title,
"artist": "SomaFM",
"length": 0
}
OCP Keywords¶
OCP skills often need to match hundreds or thousands of strings against the query string. self.voc_match can quickly become impractical to use in this scenario.
To help with this the OCP skill class provides efficient keyword matching.
def register_ocp_keyword(self, media_type: MediaType, label: str,
samples: List, langs: List[str] = None):
""" register strings as native OCP keywords (eg, movie_name, artist_name ...)
for a given media_type. ocp keywords can be efficiently matched with the
self.ocp_voc_match helper method that uses the Aho–Corasick algorithm
"""
def load_ocp_keyword_from_csv(self, csv_path: str, lang: str = None):
""" load entities from a .csv file for usage with self.ocp_voc_match
see the ocp_entities.csv datasets for example files built from wikidata SPARQL queries
examples contents of csv file
label,entity
film_genre,swashbuckler film
film_genre,neo-noir
film_genre,actual play film
film_genre,alternate history film
film_genre,spy film
...
"""
OCP Voc match¶
Uses the Aho–Corasick algorithm to match OCP keywords.
This efficiently matches many keywords against an utterance.
OCP keywords are registered via self.register_ocp_keyword.
ocp_voc_match needs ahocorasick_ner for local matching
pip install ahocorasick_ner to run keyword matching locally, inside the skill process
(e.g. to call ocp_voc_match directly, as in the example below). Without it, ocp_voc_match
still runs but returns {} — the registered keywords are still sent to OCP over the bus, so
matching still works end to end through the normal OCP pipeline, only the local call returns
empty. This ocp_voc_match integration is legacy OCP-skill-side keyword matching. It is not
expected to gain new features as OCP skills give way to MediaProvider plugins (see the
migration note above). The ahocorasick_ner package itself stays live: it also backs the
unrelated ovos-ahocorasick-ner-plugin intent transformer.
Wordlists can also be loaded from a .csv file. See the OCP dataset for a list of keywords gathered from wikidata with SPARQL queries.
OCP Database Skill¶
import json
from ovos_utils.fakebus import FakeBus
from ovos_utils.ocp import MediaType
from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill
class HorrorBabbleSkill(OVOSCommonPlaybackSkill):
def initialize(self):
# get file from
# https://github.com/JarbasSkills/skill-horrorbabble/blob/dev/bootstrap.json
with open("hb.json") as f:
db = json.load(f)
book_names = []
book_authors = []
for url, data in db.items():
t = data["title"].split("/")[0].strip()
if " by " in t:
title, author = t.split(" by ")
title = title.replace('"', "").strip()
author = author.split("(")[0].strip()
book_names.append(title)
book_authors.append(author)
if " " in author:
book_authors += author.split(" ")
elif t.startswith('"') and t.endswith('"'):
book_names.append(t[1:-1])
else:
book_names.append(t)
self.register_ocp_keyword(MediaType.AUDIOBOOK,
"book_author",
list(set(book_authors)))
self.register_ocp_keyword(MediaType.AUDIOBOOK,
"book_name",
list(set(book_names)))
self.register_ocp_keyword(MediaType.AUDIOBOOK,
"audiobook_streaming_provider",
["HorrorBabble", "Horror Babble"])
s = HorrorBabbleSkill(bus=FakeBus(), skill_id="demo.fake")
entities = s.ocp_voc_match("read The Call of Cthulhu by Lovecraft")
# {'book_author': 'Lovecraft', 'book_name': 'The Call of Cthulhu'}
print(entities)
entities = s.ocp_voc_match("play HorrorBabble")
# {'audiobook_streaming_provider': 'HorrorBabble'}
print(entities)
Playlist Results¶
Results can also be playlists, not only single tracks. For instance, a result can be a full album or a full season of a series.
When a playlist is selected from Search Results, it replaces the Now Playing list.
Playlist results look exactly the same as regular results, but instead of a uri they provide a playlist:
playlist: list # list of dicts, each dict is a regular search result
title: str
media_type: MediaType
playback: PlaybackType
match_confidence: int # 0-100
NOTE: nested playlists are a work in progress and not guaranteed to be functional. The
"playlist"dict key should not include other playlists.
Playlist Skill¶
class MyJamsSkill(OVOSCommonPlaybackSkill):
def __init__(self, *args, **kwargs):
super().__init__(
supported_media=[MediaType.MUSIC],
skill_icon=join(dirname(__file__), "ui", "myjams.png"),
*args, **kwargs)
@ocp_search()
def search_my_jams(self, phrase, media_type):
if self.voc_match(...):
results = [...] # regular result dicts, as in examples above
score = 70 # Match confidence
yield {
"match_confidence": min(100, score),
"media_type": MediaType.MUSIC,
"playlist": results, # replaces "uri"
"playback": PlaybackType.AUDIO,
"image": self.image,
"bg_image": self.image,
"skill_icon": self.skill_icon,
"title": "MyJams",
"length": sum([r["length"] for r in results]) # total playlist duration
}
Read next: UniversalSkill Related: Common Query Framework · OCP Pipeline · ovos-media · Media Plugins Reference