Migrated switches plugin

This commit is contained in:
Fabio Manganiello 2021-02-19 20:47:29 +01:00
parent 56f8d85feb
commit 6b5b50d186
19 changed files with 264 additions and 61 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0d6b06"],{"742e":function(e,n,t){"use strict";t.r(n);var a=t("7a23");function c(e,n,t,c,o,r){var s=Object(a["z"])("Panel");return Object(a["r"])(),Object(a["e"])(s,{"plugin-name":"tts.google"})}var o=t("3f9c"),r={name:"Tts",components:{Panel:o["a"]}};r.render=c;n["default"]=r}}]);
//# sourceMappingURL=chunk-2d0d6b06.c8766943.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/TtsGoogle/Index.vue","webpack:///./src/components/panels/TtsGoogle/Index.vue?cdc9"],"names":["plugin-name","name","components","Panel","render"],"mappings":"uNACE,eAAkC,GAA3BA,cAAY,e,gBAMN,GACbC,KAAM,MACNC,WAAY,CAACC,QAAA,OCNf,EAAOC,OAASA,EAED","file":"static/js/chunk-2d0d6b06.c8766943.js","sourcesContent":["<template>\n <Panel plugin-name=\"tts.google\" />\n</template>\n\n<script>\nimport Panel from \"@/components/panels/Tts/Panel\";\n\nexport default {\n name: \"Tts\",\n components: {Panel}\n}\n</script>\n","import { render } from \"./Index.vue?vue&type=template&id=5ae1fe52\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d22495e"],{e184:function(e,n,t){"use strict";t.r(n);var a=t("7a23");function c(e,n,t,c,r,o){var s=Object(a["z"])("Panel");return Object(a["r"])(),Object(a["e"])(s,{"plugin-name":"tts"})}var r=t("3f9c"),o={name:"Tts",components:{Panel:r["a"]}};o.render=c;n["default"]=o}}]);
//# sourceMappingURL=chunk-2d22495e.ff69ee49.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Tts/Index.vue","webpack:///./src/components/panels/Tts/Index.vue?f452"],"names":["plugin-name","name","components","Panel","render"],"mappings":"qNACE,eAA2B,GAApBA,cAAY,Q,gBAMN,GACbC,KAAM,MACNC,WAAY,CAACC,QAAA,OCNf,EAAOC,OAASA,EAED","file":"static/js/chunk-2d22495e.ff69ee49.js","sourcesContent":["<template>\n <Panel plugin-name=\"tts\" />\n</template>\n\n<script>\nimport Panel from \"@/components/panels/Tts/Panel\";\n\nexport default {\n name: \"Tts\",\n components: {Panel}\n}\n</script>\n","import { render } from \"./Index.vue?vue&type=template&id=4ab66a9e\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4d5b9580"],{"0f21":function(e,t,a){"use strict";a("2c22")},"2c22":function(e,t,a){},"3f9c":function(e,t,a){"use strict";var n=a("7a23"),c=Object(n["K"])("data-v-a248454a");Object(n["u"])("data-v-a248454a");var l={class:"tts-container"},i={class:"field text-container"},r={class:"field lang-container"},u={class:"field buttons"},s=Object(n["h"])("i",{class:"fa fa-volume-up"},null,-1);Object(n["s"])();var d=c((function(e,t,a,c,d,b){return Object(n["r"])(),Object(n["e"])("div",l,[Object(n["h"])("form",{onSubmit:t[1]||(t[1]=Object(n["J"])((function(){return b.talk.apply(b,arguments)}),["prevent"]))},[Object(n["h"])("div",i,[Object(n["h"])("label",null,[Object(n["h"])("input",{type:"text",name:"text",placeholder:"Text to say",disabled:d.talking},null,8,["disabled"])])]),Object(n["h"])("div",r,[Object(n["h"])("label",null,[Object(n["h"])("input",{type:"text",name:"language",placeholder:"Language code",disabled:d.talking},null,8,["disabled"])])]),Object(n["h"])("div",u,[Object(n["h"])("button",{type:"submit",disabled:d.talking},[s],8,["disabled"])])],32)])})),b=(a("13d5"),a("b0c0"),a("2909")),o=(a("96cf"),a("1da1")),p=a("3e54"),f={name:"Panel",mixins:[p["a"]],props:{pluginName:{type:String,required:!0}},data:function(){return{talking:!1}},methods:{talk:function(e){var t=this;return Object(o["a"])(regeneratorRuntime.mark((function a(){var n;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return n=Object(b["a"])(e.target.querySelectorAll("input")).reduce((function(e,t){return t.value.length&&(e[t.name]=t.value),e}),{}),t.talking=!0,a.prev=2,a.next=5,t.request("".concat(t.pluginName,".say"),n);case 5:return a.prev=5,t.talking=!1,a.finish(5);case 8:case"end":return a.stop()}}),a,null,[[2,,5,8]])})))()}}};a("0f21");f.render=d,f.__scopeId="data-v-a248454a";t["a"]=f}}]);
//# sourceMappingURL=chunk-4d5b9580.75a37b61.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/Tts/Panel.vue?0b35","webpack:///./src/components/panels/Tts/Panel.vue","webpack:///./src/components/panels/Tts/Panel.vue?858f"],"names":["class","talk","type","name","placeholder","disabled","talking","mixins","Utils","props","pluginName","String","required","data","methods","event","args","target","querySelectorAll","reduce","obj","el","value","length","request","render","__scopeId"],"mappings":"kHAAA,W,0JCCOA,MAAM,iB,GAEFA,MAAM,wB,GAKNA,MAAM,wB,GAKNA,MAAM,iB,EAEP,eAA+B,KAA5BA,MAAM,mBAAiB,S,wEAdlC,eAkBM,MAlBN,EAkBM,CAjBJ,eAgBO,QAhBA,SAAM,8CAAU,EAAAC,KAAA,qBAAI,e,CACzB,eAIM,MAJN,EAIM,CAHJ,eAEQ,cADN,eAA6E,SAAtEC,KAAK,OAAOC,KAAK,OAAOC,YAAY,cAAeC,SAAU,EAAAC,S,yBAGxE,eAIM,MAJN,EAIM,CAHJ,eAEQ,cADN,eAAmF,SAA5EJ,KAAK,OAAOC,KAAK,WAAWC,YAAY,gBAAiBC,SAAU,EAAAC,S,yBAG9E,eAIM,MAJN,EAIM,CAHJ,eAES,UAFDJ,KAAK,SAAUG,SAAU,EAAAC,S,CAC/B,G,kGAUK,GACbH,KAAM,QACNI,OAAQ,CAACC,EAAA,MAETC,MAAO,CACLC,WAAY,CACVR,KAAMS,OACNC,UAAU,IAIdC,KAXa,WAYX,MAAO,CACLP,SAAS,IAIbQ,QAAS,CACDb,KADC,SACIc,GAAO,qKACVC,EAAO,eAAID,EAAME,OAAOC,iBAAiB,UAAUC,QAAO,SAACC,EAAKC,GAGpE,OAFIA,EAAGC,MAAMC,SACXH,EAAIC,EAAGlB,MAAQkB,EAAGC,OACbF,IACN,IAEH,EAAKd,SAAU,EAPC,kBASR,EAAKkB,QAAL,UAAgB,EAAKd,WAArB,QAAuCM,GAT/B,uBAWd,EAAKV,SAAU,EAXD,4E,UCtCtB,EAAOmB,OAAS,EAChB,EAAOC,UAAY,kBAEJ","file":"static/js/chunk-4d5b9580.75a37b61.js","sourcesContent":["export * from \"-!../../../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--8-oneOf-1-0!../../../../node_modules/css-loader/dist/cjs.js??ref--8-oneOf-1-1!../../../../node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/stylePostLoader.js!../../../../node_modules/postcss-loader/src/index.js??ref--8-oneOf-1-2!../../../../node_modules/sass-loader/dist/cjs.js??ref--8-oneOf-1-3!../../../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../../../node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/index.js??ref--0-1!./Panel.vue?vue&type=style&index=0&id=a248454a&lang=scss&scoped=true\"","<template>\n <div class=\"tts-container\">\n <form @submit.prevent=\"talk\">\n <div class=\"field text-container\">\n <label>\n <input type=\"text\" name=\"text\" placeholder=\"Text to say\" :disabled=\"talking\">\n </label>\n </div>\n <div class=\"field lang-container\">\n <label>\n <input type=\"text\" name=\"language\" placeholder=\"Language code\" :disabled=\"talking\">\n </label>\n </div>\n <div class=\"field buttons\">\n <button type=\"submit\" :disabled=\"talking\">\n <i class=\"fa fa-volume-up\"></i>\n </button>\n </div>\n </form>\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\nexport default {\n name: \"Panel\",\n mixins: [Utils],\n\n props: {\n pluginName: {\n type: String,\n required: true,\n },\n },\n\n data() {\n return {\n talking: false,\n }\n },\n\n methods: {\n async talk(event) {\n const args = [...event.target.querySelectorAll('input')].reduce((obj, el) => {\n if (el.value.length)\n obj[el.name] = el.value\n return obj\n }, {})\n\n this.talking = true\n try {\n await this.request(`${this.pluginName}.say`, args)\n } finally {\n this.talking = false\n }\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.tts-container {\n height: max-content;\n background: $background-color;\n display: flex;\n justify-content: center;\n border: $default-border-3;\n box-shadow: $border-shadow-bottom-right;\n\n @media screen and (max-width: calc(#{$tablet - 1px})) {\n width: 100%;\n }\n\n @media screen and (min-width: $tablet) {\n width: 80%;\n border-radius: 1.5em;\n margin: 1.5em auto;\n }\n\n @media screen and (min-width: $desktop) {\n width: 30em;\n }\n\n form {\n width: 100%;\n border: none;\n box-shadow: none;\n padding: 1em .5em;\n margin: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n flex-direction: row;\n\n .field {\n margin: 0 .5em;\n }\n\n .text-container {\n width: 100%;\n margin-bottom: 1em;\n }\n\n input[type=text] {\n width: 100%;\n }\n\n button {\n border-radius: 1.5em;\n }\n\n input, button {\n &:hover {\n border-color: $default-hover-fg;\n }\n }\n }\n}\n</style>","import { render } from \"./Panel.vue?vue&type=template&id=a248454a&scoped=true\"\nimport script from \"./Panel.vue?vue&type=script&lang=js\"\nexport * from \"./Panel.vue?vue&type=script&lang=js\"\n\nimport \"./Panel.vue?vue&type=style&index=0&id=a248454a&lang=scss&scoped=true\"\nscript.render = render\nscript.__scopeId = \"data-v-a248454a\"\n\nexport default script"],"sourceRoot":""}

View file

@ -51,6 +51,12 @@
"sound": {
"class": "fa fa-microphone"
},
"tts": {
"class": "far fa-comment"
},
"tts.google": {
"class": "fas fa-comment"
},
"zigbee.mqtt": {
"imgUrl": "/icons/zigbee.svg"
},

View file

@ -0,0 +1,12 @@
<template>
<Panel plugin-name="tts" />
</template>
<script>
import Panel from "@/components/panels/Tts/Panel";
export default {
name: "Tts",
components: {Panel}
}
</script>

View file

@ -0,0 +1,120 @@
<template>
<div class="tts-container">
<form @submit.prevent="talk">
<div class="field text-container">
<label>
<input type="text" name="text" placeholder="Text to say" :disabled="talking">
</label>
</div>
<div class="field lang-container">
<label>
<input type="text" name="language" placeholder="Language code" :disabled="talking">
</label>
</div>
<div class="field buttons">
<button type="submit" :disabled="talking">
<i class="fa fa-volume-up"></i>
</button>
</div>
</form>
</div>
</template>
<script>
import Utils from "@/Utils";
export default {
name: "Panel",
mixins: [Utils],
props: {
pluginName: {
type: String,
required: true,
},
},
data() {
return {
talking: false,
}
},
methods: {
async talk(event) {
const args = [...event.target.querySelectorAll('input')].reduce((obj, el) => {
if (el.value.length)
obj[el.name] = el.value
return obj
}, {})
this.talking = true
try {
await this.request(`${this.pluginName}.say`, args)
} finally {
this.talking = false
}
},
},
}
</script>
<style lang="scss" scoped>
.tts-container {
height: max-content;
background: $background-color;
display: flex;
justify-content: center;
border: $default-border-3;
box-shadow: $border-shadow-bottom-right;
@media screen and (max-width: calc(#{$tablet - 1px})) {
width: 100%;
}
@media screen and (min-width: $tablet) {
width: 80%;
border-radius: 1.5em;
margin: 1.5em auto;
}
@media screen and (min-width: $desktop) {
width: 30em;
}
form {
width: 100%;
border: none;
box-shadow: none;
padding: 1em .5em;
margin: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
.field {
margin: 0 .5em;
}
.text-container {
width: 100%;
margin-bottom: 1em;
}
input[type=text] {
width: 100%;
}
button {
border-radius: 1.5em;
}
input, button {
&:hover {
border-color: $default-hover-fg;
}
}
}
}
</style>

View file

@ -0,0 +1,12 @@
<template>
<Panel plugin-name="tts.google" />
</template>
<script>
import Panel from "@/components/panels/Tts/Panel";
export default {
name: "Tts",
components: {Panel}
}
</script>

View file

@ -1,4 +1,5 @@
import os
import re
import select
import subprocess
import threading
@ -186,7 +187,8 @@ class MediaMplayerPlugin(MediaPlugin):
last_read_time = time.time()
if line.startswith('ANS_'):
k, v = tuple(line[4:].split('='))
m = re.match('^([^=]+)=(.*)$', line[4:])
k, v = m.group(1), m.group(2)
v = v.strip()
if v == 'yes':
v = True

View file

@ -1,8 +1,10 @@
import subprocess
import urllib.parse
from typing import Optional, List
from typing import Optional
from platypush.config import Config
from platypush.context import get_plugin
from platypush.plugins import Plugin, action
from platypush.plugins.media import MediaPlugin
class TtsPlugin(Plugin):
@ -11,46 +13,67 @@ class TtsPlugin(Plugin):
Requires:
* **mplayer** - see your distribution docs on how to install the mplayer package
* At least a *media plugin* (see :class:`platypush.plugins.media.MediaPlugin`) enabled/configured - used for
speech playback.
"""
def __init__(self, language='en-gb', player_args: Optional[List[str]] = None):
_supported_media_plugins = [
'media.omxplayer',
'media.gstreamer',
'media.mplayer',
'media.mpv',
'media.vlc',
]
def __init__(self, language='en-gb', media_plugin: Optional[str] = None, player_args: Optional[dict] = None):
"""
:param language: Language code (default: ``en-gb``).
:param player_args: Extra options to be passed to the audio player (default: ``mplayer``).
: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__()
self.language = language
self.player_args = player_args or []
self.player_args = player_args or {}
self.media_plugin = get_plugin(media_plugin) if media_plugin else self._get_media_plugin()
assert self.media_plugin, 'No media playback plugin configured. Supported plugins: [{}]'.format(
', '.join(self._supported_media_plugins))
@classmethod
def _get_media_plugin(cls) -> Optional[MediaPlugin]:
for plugin in cls._supported_media_plugins:
if plugin in Config.get():
return get_plugin(plugin)
@action
def say(self, text: str, language: Optional[str] = None, player_args: Optional[List[str]] = None):
def say(self, text: str, language: Optional[str] = None, player_args: Optional[dict] = None):
"""
Say some text.
:param text: Text to say.
:param language: Language code override.
:param player_args: ``player_args`` override.
:param player_args: Optional arguments that should be passed to the player plugin's
:meth:`platypush.plugins.media.MediaPlugin.play` method.
"""
language = language or self.language
player_args = player_args or self.player_args
cmd = [
'mplayer -ao alsa -really-quiet -noconsolecontrols ' +
' '.join(player_args) + ' ' +
'"http://translate.google.com/translate_tts?{}"'.format(
url = 'http://translate.google.com/translate_tts?{}'.format(
urllib.parse.urlencode({
'ie': 'UTF-8',
'client': 'tw-ob',
'tl': language,
'q': text,
})
)
]
}))
self.media_plugin.play(url, **player_args)
try:
return subprocess.check_output(
cmd, stderr=subprocess.STDOUT, shell=True).decode('utf-8')
except subprocess.CalledProcessError as e:
raise RuntimeError(e.output.decode('utf-8'))
# vim:sw=4:ts=4:et:

View file

@ -1,7 +1,6 @@
import os
import subprocess
import tempfile
from typing import Optional, List
from typing import Optional
from platypush.plugins import action
from platypush.plugins.tts import TtsPlugin
@ -17,6 +16,7 @@ class TtsGooglePlugin(TtsPlugin):
* **google-cloud-texttospeech** - ``pip install google-cloud-texttospeech``
* **mplayer** - see your distribution docs on how to install the mplayer package
"""
def __init__(self,
@ -24,24 +24,21 @@ class TtsGooglePlugin(TtsPlugin):
voice: Optional[str] = None,
gender: str = 'FEMALE',
credentials_file: str = '~/.credentials/platypush/google/platypush-tts.json',
player_args: Optional[List[str]] = None):
**kwargs):
"""
:param language: Language code, see https://cloud.google.com/text-to-speech/docs/basics for supported languages
:param voice: Voice type, see https://cloud.google.com/text-to-speech/docs/basics for supported voices
:param gender: Voice gender (MALE, FEMALE or NEUTRAL)
:param credentials_file: Where your GCloud credentials for TTS are stored, see https://cloud.google.com/text-to-speech/docs/basics
:param player_args: Extra options to be passed to the audio player (default: ``mplayer``).
:param kwargs: Extra arguments to be passed to the :class:`platypush.plugins.tts.TtsPlugin` constructor.
"""
from google.cloud import texttospeech
super().__init__()
super().__init__(**kwargs)
self.language = language
self.voice = voice
self.player_args = player_args or []
self.language = self._parse_language(language)
self.voice = self._parse_voice(self.language, voice)
self.gender = getattr(texttospeech.enums.SsmlVoiceGender, gender.upper())
self.gender = getattr(self._gender, gender.upper())
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.expanduser(credentials_file)
def _parse_language(self, language):
@ -66,13 +63,43 @@ class TtsGooglePlugin(TtsPlugin):
return language + '-Wavenet-C'
return language + '-Wavenet-A'
@property
def _gender(self):
from google.cloud import texttospeech
return texttospeech.enums.SsmlVoiceGender if hasattr(texttospeech, 'enums') else \
texttospeech.SsmlVoiceGender
@property
def _voice_selection_params(self):
from google.cloud import texttospeech
return texttospeech.types.VoiceSelectionParams if hasattr(texttospeech, 'types') else \
texttospeech.VoiceSelectionParams
@property
def _synthesis_input(self):
from google.cloud import texttospeech
return texttospeech.types.SynthesisInput if hasattr(texttospeech, 'types') else \
texttospeech.SynthesisInput
@property
def _audio_config(self):
from google.cloud import texttospeech
return texttospeech.types.AudioConfig if hasattr(texttospeech, 'types') else \
texttospeech.AudioConfig
@property
def _audio_encoding(self):
from google.cloud import texttospeech
return texttospeech.enums.AudioEncoding if hasattr(texttospeech, 'enums') else \
texttospeech.AudioEncoding
@action
def say(self,
text: str,
language: Optional[str] = None,
voice: Optional[str] = None,
gender: Optional[str] = None,
player_args: Optional[List[str]] = None):
player_args: Optional[dict] = None):
"""
Say a phrase.
@ -80,12 +107,14 @@ class TtsGooglePlugin(TtsPlugin):
:param language: Language code override.
:param voice: Voice type override.
:param gender: Gender override.
:param player_args: Player args override.
:param player_args: Optional arguments that should be passed to the player plugin's
:meth:`platypush.plugins.media.MediaPlugin.play` method.
"""
from google.cloud import texttospeech
client = texttospeech.TextToSpeechClient()
synthesis_input = texttospeech.types.SynthesisInput(text=text)
# noinspection PyTypeChecker
synthesis_input = self._synthesis_input(text=text)
language = self._parse_language(language)
voice = self._parse_voice(language, voice)
@ -93,28 +122,17 @@ class TtsGooglePlugin(TtsPlugin):
if gender is None:
gender = self.gender
else:
gender = getattr(texttospeech.enums.SsmlVoiceGender, gender.upper())
gender = getattr(self._gender, gender.upper())
player_args = player_args or self.player_args
voice = texttospeech.types.VoiceSelectionParams(
language_code=language, ssml_gender=gender,
name=voice)
audio_config = texttospeech.types.AudioConfig(
audio_encoding=texttospeech.enums.AudioEncoding.MP3)
response = client.synthesize_speech(synthesis_input, voice, audio_config)
voice = self._voice_selection_params(language_code=language, ssml_gender=gender, name=voice)
# noinspection PyTypeChecker
audio_config = self._audio_config(audio_encoding=self._audio_encoding.MP3)
response = client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config)
player_args = player_args or {}
with tempfile.NamedTemporaryFile() as f:
f.write(response.audio_content)
cmd = ['mplayer -ao alsa -really-quiet -noconsolecontrols {} "{}"'.format(
' '.join(player_args), f.name)]
try:
return subprocess.check_output(
cmd, stderr=subprocess.STDOUT, shell=True).decode('utf-8')
except subprocess.CalledProcessError as e:
raise RuntimeError(e.output.decode('utf-8'))
self.media_plugin.play(f.name, **player_args)
# vim:sw=4:ts=4:et: