Common Reading
An OVOS pipeline plugin that reads things aloud - fairy tales, articles, news, documents, reports, and whatever else a provider skill wants to offer - by orchestrating "read me something" across those provider skills, the same way OCP (ovos-common-play) orchestrates "play X" across media skills.
"If you want your children to be intelligent, read them fairy tales. If you want them to be more intelligent, read them more fairy tales." — Albert Einstein
What this is: a pipeline plugin for narrating text-based content via TTS - providers deliver plain text, and this plugin reads it aloud, sentence by sentence, with bookmarking and "continue" support.
What this is not: an audiobook or audio-file player. If you're looking for pre-recorded audiobooks, radio dramas, or narrated readings as audio files, that's already well covered by existing OCP media skills (e.g.
ovos-skill-librivox, or JarbasSkills'skill-golden-audiobooks/skill-hppodcraft/skill-epic-horror-theatre). This plugin solves a different problem: letting several text content providers coexist and be searched together, without any of them registering competing voice intents.
Install
pip install ovos-common-reading-pipeline-plugin
Add it to your pipeline in mycroft.conf right after your stop
matcher (stop_high/ovos-stop-pipeline-plugin-high) - roughly
where OCP sits, not before stop. "Stop" should always be the most
reliable, highest-priority command regardless of what skill is active;
putting anything ahead of it undermines that guarantee for every skill,
not just this one. An earlier version of this README recommended
putting this plugin first - that was wrong, and has been reverted; see
the note at the end of this section for why.
Don't copy-paste a whole pipeline block from here or anywhere else -
your existing list already has entries specific to your install (the
exact names/order vary: stop_high vs ovos-stop-pipeline-plugin-high,
whether persona/OCP/common-query plugins are present, etc). Find your
existing "pipeline" array and insert one line, right after your stop
matcher:
"pipeline": [
"stop_high",
+ "ovos-common-reading-pipeline-plugin",
"converse",
"ocp_high",
...
]
(a real, complete example from a live install - yours will look similar but isn't guaranteed to match exactly, which is exactly why editing your existing list beats copying a block):
"pipeline": [
"ovos-stop-pipeline-plugin-high",
"ovos-common-reading-pipeline-plugin",
"ovos-converse-pipeline-plugin",
"ovos-ocp-pipeline-plugin-high",
"ovos-persona-pipeline-plugin-high",
"ovos-padatious-pipeline-plugin-high",
"ovos-fallback-pipeline-plugin-high",
"ovos-adapt-pipeline-plugin-high",
"ovos-stop-pipeline-plugin-medium",
"ovos-adapt-pipeline-plugin-medium",
"ovos-common-query-pipeline-plugin",
"ovos-fallback-pipeline-plugin-medium",
"ovos-persona-pipeline-plugin-low",
"ovos-fallback-pipeline-plugin-low"
]
This plugin registers its own dedicated pause/continue intents and
correctly declines (None) for anything it doesn't recognize or isn't
currently relevant to, so this position - right after stop, ahead of
everything else - is enough for pause/continue to reliably reach
it without needing to jump the stop queue.
A separate, unresolved issue: "stop" itself may not reach this plugin at all
This is not a pipeline-ordering problem, and moving this plugin
around won't fix it. stop_high doesn't broadcast to every active
skill - it internally picks one specific skill to call .stop()
on. On at least one real install (confirmed live via
ovos-tui-client's activity
log), it picked a separate system skill (ovos-skill-stop, skill_id
stop.openvoiceos - which by its own description "doesn't do anything
directly" and exists for hardware/enclosure signaling) instead of
whichever skill was actually mid-session, so saying "stop" while a
story was playing did nothing at all - the reading continued right
through it. If you hit this, check whether ovos-skill-stop (or
similar) is installed and whether it needs to be blacklisted; this
looks like it may be a core stop_high skill-selection behavior worth
raising upstream rather than something fixable from this plugin's side.
You'll also want at least one provider skill installed, otherwise there's nothing to read:
- ovos-skill-andersen-tales - Hans Christian Andersen fairy tales
- ovos-skill-grimm-tales - Brothers Grimm fairy tales
- ovos-skill-andrew-lang-tales - Andrew Lang's Fairy Books (Project Gutenberg, English only, no translation)
- ovos-skill-bechstein-tales - Ludwig Bechstein's German fairy tales (Project Gutenberg, German only, no translation)
- ovos-skill-cosquin-tales - Emmanuel Cosquin's Lorraine folk tales (Project Gutenberg, French only, no translation)
- ovos-skill-ovosblog - the OpenVoiceOS blog, with machine-translation support
- ovos-skill-arxiv-papers - arXiv paper abstracts (
content_type: "paper") - ovos-skill-365tomorrows-stories - daily CC-licensed flash sci-fi, with machine-translation support
- ovos-skill-horoscope-readings - daily zodiac horoscopes (
content_type: "horoscope"), with machine-translation support
Building your own provider
See ovos-skill-common-reading-example - a template walking through two working patterns (RSS feeds and static-page scraping), the bus protocol, caching, and the judgment calls every provider has to make for itself (translate or not, what a human calls the source, what's worth reading aloud).
The ovos.common_reading.* bus protocol
Provider skills implement this to be usable by this plugin. It's a plain
messagebus convention (like ovos.common_play.*) - no shared package
dependency needed.
1. Search
Broadcast on a matched utterance:
ovos.common_reading.search
{
"phrase": "<what the user asked for, or null for 'surprise me'>",
"collection_hint": "<raw text like 'grimm' or 'h c andersen', or null>",
"content_type": "<raw hint like 'story', 'book', 'article', 'poem', or null>",
"requester": "<this plugin's id>"
}
collection_hint is set when the user names a specific source/collection
("read me a story from Grimm", "find Cinderella by Andersen"). It's
raw, unvalidated text - each provider fuzzy-matches it against its own
known friendly names and should only respond if it's a match, or if
collection_hint is null (in which case every provider competes as usual).
content_type is a similarly raw, optional hint. A provider that only
offers one kind of content can use it to stay silent when it clearly
doesn't apply, but should treat a null/missing content_type as "anyone
can compete".
phrase can also be null on its own - "read me a story from Grimm" with
no specific title named is a valid request for the hinted provider to
offer something of its own choosing.
Every provider that thinks it can help replies (within ~2s):
ovos.common_reading.search.response
{
"skill_id": "<provider skill id>",
"content_id": "<opaque id the provider will recognize later>",
"title": "<human-readable title>",
"author": "<author, optional>",
"collection": "<book/collection name, optional>",
"source": "<where the text comes from, e.g. 'grimmstories.com'>",
"confidence": 0.0-1.0,
"machine_translated": true/false (optional, defaults falsy)
}
The highest-confidence response wins (and, if it's below 0.8, this
plugin confirms with the user before continuing). If a provider sets
"machine_translated": true, that's disclosed as part of the
announcement right before reading starts.
2. Fetch
Once something is chosen (or resumed via "continue"), a targeted request goes to just that provider:
ovos.common_reading.fetch_content.<provider_skill_id>
{"content_id": "<from the search response>", "requester": "<this plugin's id>"}
The provider replies once with the full text, split into paragraphs:
ovos.common_reading.fetch_content.response
{"paragraphs": ["First paragraph...", "Second paragraph...", ...]}
Reading pacing, sentence splitting, and bookmark tracking all happen here - providers just deliver text.
Stop, pause, and continue
Reading checks a break condition between every sentence (not just between paragraphs), so an interruption takes effect within a sentence or so, not at the end of a whole paragraph. Two ways to interrupt:
- "stop" - handled via
stop(), the standard OVOS mechanism triggered by the platform's own global stop command/button. Not specific to this plugin's own vocabulary. - "pause" - a dedicated intent this plugin matches itself (see
locale/<lang>/pause.intent), rather than relying on "pause" being recognized as a synonym for "stop" at the OVOS core level, which isn't guaranteed. Functionally identical to stop (same bookmark preservation), just with a dialog that explicitly invites resuming ("say continue when you're ready") instead of sounding final.
Either way, progress is bookmarked automatically - saying "continue" later picks up from the same paragraph, even after a full restart, since the bookmark and the last-read content are both stored in persistent skill settings, not just in memory.
3. Ping (only used when a search comes up empty)
If a search returns zero candidates, this plugin needs to say something honest - but "I couldn't find that" and "you don't have any reading skills installed" are very different situations, and guessing which one applies (or worse, silently falling back to some other language, see #2) is worse than just asking. So it asks:
ovos.common_reading.ping
{"requester": "<this plugin's id>"}
Every provider that's loaded and listening should reply, cheaply, with no index lookup:
ovos.common_reading.pong
{"skill_id": "<provider skill id>", "collection": "<same collection name it uses in search responses>"}
This is only broadcast on the rare 0-candidates path, never on every search - a normal search/fetch round trip never triggers a ping. Based on the pongs:
- Zero pongs -> "you don't have any reading skills installed" (the most useful thing to tell the user, since it's actionable)
- At least one pong, but nothing matched a
collection_hint-> "I don't know a source called X" - At least one pong, but no phrase matched at all -> a generic "I couldn't find anything matching that"
This needs no language-awareness on the plugin's part: a provider that
refused to load for the device's language (the SUPPORTED_LANGUAGES
gate described below) never registers a ping handler either, so it
correctly stays silent here too - the same mechanism that keeps it out
of search results keeps it out of ping results, for free.
If you're building a provider, implementing this is required, not optional - a provider that never pongs looks, from the pipeline's perspective, exactly like a provider that isn't installed at all, which produces a misleading "nothing installed" message even when your skill is present and just didn't have a match. See ovos-skill-common-reading-example for the reference implementation.
Friendly names
Each provider should keep a small list of names it's willing to answer
to as collection_hint - not just its skill_id, but the natural
things a person might call it: ovos-skill-grimm-tales might match
"grimm", "the brothers grimm"; ovos-skill-andersen-tales matches
"andersen", "hans christian andersen", "h c andersen". Matching
should be fuzzy (e.g. via ovos_utils.parse.match_one against the
provider's own alias list) rather than exact string equality, since STT
transcription is never perfectly consistent.
If collection_hint doesn't clearly match a provider's own aliases,
that provider should simply not respond to the search at all - only
providers that actually answered are considered.
Tune your match threshold carefully. A threshold around 0.6 can produce false positives on short strings (e.g. "andrew lang" scoring 0.63 against "andersen" on plain character overlap, nothing to do with meaning) - 0.85 is a safer default. Verify empirically for your own alias list.
Language handling
Every provider in this family handles the device's language one of two ways:
- Fixed set of supported languages, no translation (
ovos-skill-andersen-tales,ovos-skill-grimm-tales,ovos-skill-andrew-lang-tales): checks aSUPPORTED_LANGUAGESset at the top ofinitialize(), before building any index or registering any bus events. On an unsupported device language, the provider logs why and stays completely inert - never loads an index, never listens forovos.common_reading.search- rather than loading fully and silently declining every search. - Machine translation (
ovos-skill-ovosblog,ovos-skill-arxiv-papers): always loads (it can't know in advance whether a translation plugin will be configured), matches search phrases against translated titles, and declines per-search rather than per-load if no translator is available.
Neither pattern ever silently serves the wrong language - the difference is only when the provider decides it can't help (once, at load time, if it has no way to translate; or per-search, if it might). See ovos-skill-common-reading-example's module docstring (decision points #1 and #4) for the full reasoning behind picking one over the other for your own provider.
Category
Entertainment
Tags
#reading #stories #articles #news #orchestrator #pipeline
Release files for ovos-common-reading-pipeline-plugin 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ovos_common_reading_pipeline_plugin-0.1.8.tar.gz | 55.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ovos_common_reading_pipeline_plugin-0.1.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 128.5 kB
Release files / ovos_common_reading_pipeline_plugin-0.1.8.tar.gz
| Download URL | ovos_common_reading_pipeline_plugin-0.1.8.tar.gz |
|---|---|
| Size | 55.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d08060046bea851e836f4562b5d6d3e82770736d411a98b6458a42db712fa66d
|
|
BLAKE2b-256 checksum How to use checksums |
b6d0244775b43ef3474798d2c7b0da7be18f5e0674c31b03b2dea494cd4a3982
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency logRelease files / ovos_common_reading_pipeline_plugin-0.1.8-py3-none-any.whl
| Download URL | ovos_common_reading_pipeline_plugin-0.1.8-py3-none-any.whl |
|---|---|
| Size | 73.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
84d7ad85017dca39c7204de5723e67cf541ae03a4e728ba262253fc45e9c8383
|
|
BLAKE2b-256 checksum How to use checksums |
61dbc6c84838f962aa3eb9029aab067f086e6dd2a0b5c3eff1bc74e88c2ceb0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency log