From ae226a5b01de271419ce0d2db207c3cfc71b24df Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 30 Sep 2022 10:50:28 +0200 Subject: [PATCH] Added `tts.mimic3` integration. Closes: #226 --- CHANGELOG.md | 1 + docs/source/platypush/plugins/tts.mimic3.rst | 5 + docs/source/plugins.rst | 1 + .../http/app/routes/plugins/tts/__init__.py | 0 .../http/app/routes/plugins/tts/mimic3.py | 46 +++++++ platypush/plugins/tts/mimic3/__init__.py | 119 ++++++++++++++++++ platypush/plugins/tts/mimic3/manifest.yaml | 6 + platypush/schemas/tts/mimic3.py | 51 ++++++++ 8 files changed, 229 insertions(+) create mode 100644 docs/source/platypush/plugins/tts.mimic3.rst create mode 100644 platypush/backend/http/app/routes/plugins/tts/__init__.py create mode 100644 platypush/backend/http/app/routes/plugins/tts/mimic3.py create mode 100644 platypush/plugins/tts/mimic3/__init__.py create mode 100644 platypush/plugins/tts/mimic3/manifest.yaml create mode 100644 platypush/schemas/tts/mimic3.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3804ba2b78..e2f4333fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ reported only starting from v0.20.2. ### Added - Added [Wallabag integration](https://git.platypush.tech/platypush/platypush/issues/224). +- Added [Mimic3 TTS integration](https://git.platypush.tech/platypush/platypush/issues/226). ## [0.23.6] - 2022-09-19 diff --git a/docs/source/platypush/plugins/tts.mimic3.rst b/docs/source/platypush/plugins/tts.mimic3.rst new file mode 100644 index 0000000000..d6e280b327 --- /dev/null +++ b/docs/source/platypush/plugins/tts.mimic3.rst @@ -0,0 +1,5 @@ +``tts.mimic3`` +============== + +.. automodule:: platypush.plugins.tts.mimic3 + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index ba4c0ace91..8c42418974 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -132,6 +132,7 @@ Plugins platypush/plugins/trello.rst platypush/plugins/tts.rst platypush/plugins/tts.google.rst + platypush/plugins/tts.mimic3.rst platypush/plugins/tv.samsung.ws.rst platypush/plugins/twilio.rst platypush/plugins/udp.rst diff --git a/platypush/backend/http/app/routes/plugins/tts/__init__.py b/platypush/backend/http/app/routes/plugins/tts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/platypush/backend/http/app/routes/plugins/tts/mimic3.py b/platypush/backend/http/app/routes/plugins/tts/mimic3.py new file mode 100644 index 0000000000..8bf37f6d42 --- /dev/null +++ b/platypush/backend/http/app/routes/plugins/tts/mimic3.py @@ -0,0 +1,46 @@ +import requests +from urllib.parse import urljoin + +from flask import abort, request, Blueprint + +from platypush.backend.http.app import template_folder + +mimic3 = Blueprint('mimic3', __name__, template_folder=template_folder) + +# Declare routes list +__routes__ = [ + mimic3, +] + + +@mimic3.route('/tts/mimic3/say', methods=['GET']) +def proxy_tts_request(): + """ + This route is used to proxy the POST request to the Mimic3 TTS server + through a GET, so it can be easily processed as a URL through a media + plugin. + """ + required_args = { + 'text', + 'server_url', + 'voice', + } + + missing_args = required_args.difference(set(request.args.keys())) + if missing_args: + abort(400, f'Missing parameters: {missing_args}') + + args = {arg: request.args[arg] for arg in required_args} + + rs = requests.post( + urljoin(args['server_url'], '/api/tts'), + data=args['text'], + params={ + 'voice': args['voice'], + }, + ) + + return rs.content + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/tts/mimic3/__init__.py b/platypush/plugins/tts/mimic3/__init__.py new file mode 100644 index 0000000000..6bcf801578 --- /dev/null +++ b/platypush/plugins/tts/mimic3/__init__.py @@ -0,0 +1,119 @@ +import requests +from typing import Optional +from urllib.parse import urljoin, urlencode +from platypush.backend.http.app.utils import get_local_base_url + +from platypush.context import get_backend +from platypush.plugins import action +from platypush.plugins.tts import TtsPlugin +from platypush.schemas.tts.mimic3 import Mimic3VoiceSchema + + +class TtsMimic3Plugin(TtsPlugin): + """ + TTS plugin that uses the `Mimic3 webserver + `_ provided by `Mycroft + `_ as a text-to-speech engine. + + The easiest way to deploy a Mimic3 instance is probably via Docker: + + .. code-block:: bash + + $ mkdir -p "$HOME/.local/share/mycroft/mimic3" + $ chmod a+rwx "$HOME/.local/share/mycroft/mimic3" + $ docker run --rm \ + -p 59125:59125 \ + -v "%h/.local/share/mycroft/mimic3:/home/mimic3/.local/share/mycroft/mimic3" \ + 'mycroftai/mimic3' + + Requires: + + * At least a *media plugin* (see + :class:`platypush.plugins.media.MediaPlugin`) enabled/configured - + used for speech playback. + * The ``http`` backend (:class:`platypush.backend.http.HttpBackend`) + enabled - used for proxying the API calls. + + """ + + def __init__( + self, + server_url: str, + voice: str = 'en_UK/apope_low', + media_plugin: Optional[str] = None, + player_args: Optional[dict] = None, + **kwargs + ): + """ + :param server_url: Base URL of the web server that runs the Mimic3 engine. + :param voice: Default voice to be used (default: ``en_UK/apope_low``). + You can get a full list of the voices available on the server + through :method:`.voices`. + :param media_plugin: Media plugin to be used for audio playback. Supported: + + - ``media.gstreamer`` + - ``media.omxplayer`` + - ``media.mplayer`` + - ``media.mpv`` + - ``media.vlc`` + + :param player_args: Optional arguments that should be passed to the player plugin's + :meth:`platypush.plugins.media.MediaPlugin.play` method. + """ + super().__init__(media_plugin=media_plugin, player_args=player_args, **kwargs) + + self.server_url = server_url + self.voice = voice + + @action + def say( + self, + text: str, + server_url: Optional[str] = None, + voice: Optional[str] = None, + player_args: Optional[dict] = None, + ): + """ + Say some text. + + :param text: Text to say. + :param server_url: Default ``server_url`` override. + :param voice: Default ``voice`` override. + :param player_args: Default ``player_args`` override. + """ + server_url = server_url or self.server_url + voice = voice or self.voice + player_args = player_args or self.player_args + http = get_backend('http') + assert http, 'http backend not configured' + assert self.media_plugin, 'No media plugin configured' + + url = ( + urljoin(get_local_base_url(), '/tts/mimic3/say') + + '?' + + urlencode( + { + 'text': text, + 'server_url': server_url, + 'voice': voice, + } + ) + ) + + self.media_plugin.play(url, **player_args) + + @action + def voices(self, server_url: Optional[str] = None): + """ + List the voices available on the server. + + :param server_url: Default ``server_url`` override. + :return: .. schema:: tts.mimi3.Mimic3VoiceSchema(many=True) + """ + server_url = server_url or self.server_url + rs = requests.get(urljoin(server_url, '/api/voices')) + rs.raise_for_status() + return Mimic3VoiceSchema().dump(rs.json(), many=True) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/tts/mimic3/manifest.yaml b/platypush/plugins/tts/mimic3/manifest.yaml new file mode 100644 index 0000000000..4119497ec7 --- /dev/null +++ b/platypush/plugins/tts/mimic3/manifest.yaml @@ -0,0 +1,6 @@ +manifest: + events: {} + install: + pip: [] + package: platypush.plugins.tts.mimic3 + type: plugin diff --git a/platypush/schemas/tts/mimic3.py b/platypush/schemas/tts/mimic3.py new file mode 100644 index 0000000000..a5819ff44c --- /dev/null +++ b/platypush/schemas/tts/mimic3.py @@ -0,0 +1,51 @@ +from marshmallow import Schema, fields + + +class Mimic3Schema(Schema): + pass + + +class Mimic3VoiceSchema(Mimic3Schema): + key = fields.String( + required=True, + dump_only=True, + metadata={ + 'description': 'Unique voice ID', + 'example': 'en_UK/apope_low', + }, + ) + + language = fields.String( + required=True, + dump_only=True, + metadata={ + 'example': 'en_UK', + }, + ) + + language_english = fields.String( + metadata={ + 'description': 'Name of the language (in English)', + } + ) + + language_native = fields.String( + metadata={ + 'description': 'Name of the language (in the native language)', + } + ) + + name = fields.String( + metadata={ + 'example': 'apope_low', + } + ) + + sample_text = fields.String( + metadata={ + 'example': 'Some text', + } + ) + + description = fields.String() + aliases = fields.List(fields.String)