Metadata-Version: 2.4
Name: ovos-common-reading-pipeline-plugin
Version: 0.1.1
Summary: OVOS pipeline plugin that orchestrates 'read me something' across content provider skills (fairy tales, articles, news, documents and more), similar in spirit to OCP for media skills
Home-page: https://github.com/andlo/ovos-common-reading-pipeline-plugin
Author: Andreas Lorensen
Author-email: andlo@outlook.dk
License: GPL-3.0-or-later
Project-URL: Source, https://github.com/andlo/ovos-common-reading-pipeline-plugin
Project-URL: Bug Tracker, https://github.com/andlo/ovos-common-reading-pipeline-plugin/issues
Keywords: ovos pipeline plugin voice assistant reading orchestrator
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Home Automation
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: padacioso
Requires-Dist: ovos-workshop
Requires-Dist: ovos-bus-client
Requires-Dist: ovos-plugin-manager
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# <img src='book-512.png' card_color='#40DBB0' width='50' height='50' style='vertical-align:bottom'/> 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](https://openvoiceos.github.io/ovos-technical-manual/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

[![Tests](https://github.com/andlo/ovos-common-reading-pipeline-plugin/actions/workflows/test.yml/badge.svg)](https://github.com/andlo/ovos-common-reading-pipeline-plugin/actions/workflows/test.yml)
[![PyPI version](https://img.shields.io/pypi/v/ovos-common-reading-pipeline-plugin.svg)](https://pypi.org/project/ovos-common-reading-pipeline-plugin/)

> **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
```bash
pip install ovos-common-reading-pipeline-plugin
```

Add it to your pipeline in `mycroft.conf`:

```json
{
  "intents": {
    "pipeline": [
      "stop_high",
      "ovos-common-reading-pipeline-plugin",
      "converse",
      "ocp_high",
      "padatious_high",
      "adapt_high",
      "..."
    ]
  }
}
```

You'll also want at least one *provider* skill installed, otherwise
there's nothing to read:

- [ovos-skill-andersen-tales](https://github.com/andlo/ovos-skill-andersen-tales) - Hans Christian Andersen fairy tales
- [ovos-skill-grimm-tales](https://github.com/andlo/ovos-skill-grimm-tales) - Brothers Grimm fairy tales
- [ovos-skill-andrew-lang-tales](https://github.com/andlo/ovos-skill-andrew-lang-tales) - Andrew Lang's Fairy Books (Project Gutenberg, English only, no translation)
- [ovos-skill-bechstein-tales](https://github.com/andlo/ovos-skill-bechstein-tales) - Ludwig Bechstein's German fairy tales (Project Gutenberg, German only, no translation)
- [ovos-skill-cosquin-tales](https://github.com/andlo/ovos-skill-cosquin-tales) - Emmanuel Cosquin's Lorraine folk tales (Project Gutenberg, French only, no translation)
- [ovos-skill-ovosblog](https://github.com/andlo/ovos-skill-ovosblog) - the OpenVoiceOS blog, with machine-translation support
- [ovos-skill-arxiv-papers](https://github.com/andlo/ovos-skill-arxiv-papers) - arXiv paper abstracts (`content_type: "paper"`)
- [ovos-skill-365tomorrows-stories](https://github.com/andlo/ovos-skill-365tomorrows-stories) - daily CC-licensed flash sci-fi, with machine-translation support

## Building your own provider

See [ovos-skill-common-reading-example](https://github.com/andlo/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.

### 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](https://github.com/andlo/ovos-common-reading-pipeline-plugin/issues/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](https://github.com/andlo/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 a
  `SUPPORTED_LANGUAGES` set at the *top of `initialize()`*, 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 for `ovos.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](https://github.com/andlo/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
