Merge branch 'dev' into feature/improve-concurrent-ci
commit
424382afca
|
@ -1,12 +1,24 @@
|
||||||
.. Mycroft documentation master file
|
.. Mycroft documentation master file
|
||||||
|
Mycroft-core technical documentation
|
||||||
|
====================================
|
||||||
|
|
||||||
Mycroft Skills API
|
Mycroft Skills API
|
||||||
==================
|
------------------
|
||||||
|
|
||||||
*Reference for the Mycroft Skills API*
|
*Reference for use during Skill creation*
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
source/mycroft
|
source/mycroft
|
||||||
|
|
||||||
|
Mycroft plugin API
|
||||||
|
------------------
|
||||||
|
*Reference for use during Plugin creation*
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
source/plugins
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
Mycroft plugins
|
||||||
|
===============
|
||||||
|
Mycroft is extendable by plugins. These plugins can add support for new Speech To Text engines, Text To Speech engines, wake word engines and add new audio playback options.
|
||||||
|
|
||||||
|
TTS - Base for TTS plugins
|
||||||
|
--------------------------
|
||||||
|
.. autoclass:: mycroft.tts.TTS
|
||||||
|
:members:
|
||||||
|
|
||||||
|
STT - base for STT plugins
|
||||||
|
--------------------------
|
||||||
|
.. autoclass:: mycroft.stt.STT
|
||||||
|
:members:
|
||||||
|
|
|
||||||
|
|
|
||||||
|
.. autoclass:: mycroft.stt.StreamingSTT
|
||||||
|
:members:
|
||||||
|
|
|
||||||
|
|
|
||||||
|
.. autoclass:: mycroft.stt.StreamThread
|
||||||
|
:members:
|
||||||
|
|
||||||
|
HotWordEngine - Base for Hotword engine plugins
|
||||||
|
-----------------------------------------------
|
||||||
|
.. autoclass:: mycroft.client.speech.hotword_factory.HotWordEngine
|
||||||
|
:members:
|
||||||
|
|
||||||
|
AudioBackend - Base for audioservice backend plugins
|
||||||
|
------------------
|
||||||
|
.. autoclass:: mycroft.audio.services.AudioBackend
|
||||||
|
:members:
|
||||||
|
|
|
||||||
|
|
|
||||||
|
.. autoclass:: mycroft.audio.services.RemoteAudioBackend
|
||||||
|
:members:
|
||||||
|
|
|
@ -12,132 +12,129 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
"""Definition of the audio service backends base classes.
|
||||||
|
|
||||||
|
These classes can be used to create an Audioservice plugin extending
|
||||||
|
Mycroft's media playback options.
|
||||||
|
"""
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class AudioBackend(metaclass=ABCMeta):
|
class AudioBackend(metaclass=ABCMeta):
|
||||||
"""
|
"""Base class for all audio backend implementations.
|
||||||
Base class for all audio backend implementations.
|
|
||||||
|
|
||||||
Args:
|
Arguments:
|
||||||
config: configuration dict for the instance
|
config (dict): configuration dict for the instance
|
||||||
bus: Mycroft messagebus emitter
|
bus (MessageBusClient): Mycroft messagebus emitter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config, bus):
|
def __init__(self, config, bus):
|
||||||
self._track_start_callback = None
|
self._track_start_callback = None
|
||||||
self.supports_mime_hints = False
|
self.supports_mime_hints = False
|
||||||
|
self.config = config
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def supported_uris(self):
|
def supported_uris(self):
|
||||||
|
"""List of supported uri types.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Supported uri's
|
||||||
"""
|
"""
|
||||||
Returns: list of supported uri types.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def clear_list(self):
|
def clear_list(self):
|
||||||
"""
|
"""Clear playlist."""
|
||||||
Clear playlist
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def add_list(self, tracks):
|
def add_list(self, tracks):
|
||||||
"""
|
"""Add tracks to backend's playlist.
|
||||||
Add tracks to backend's playlist.
|
|
||||||
|
|
||||||
Args:
|
Arguments:
|
||||||
tracks: list of tracks.
|
tracks (list): list of tracks.
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def play(self, repeat=False):
|
def play(self, repeat=False):
|
||||||
"""
|
"""Start playback.
|
||||||
Start playback.
|
|
||||||
|
|
||||||
Args:
|
Starts playing the first track in the playlist and will contiune
|
||||||
repeat: Repeat playlist, defaults to False
|
until all tracks have been played.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
repeat (bool): Repeat playlist, defaults to False
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""Stop playback.
|
||||||
Stop playback.
|
|
||||||
|
|
||||||
Returns: (bool) True if playback was stopped, otherwise False
|
Stops the current playback.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if playback was stopped, otherwise False
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
def set_track_start_callback(self, callback_func):
|
def set_track_start_callback(self, callback_func):
|
||||||
"""
|
"""Register callback on track start.
|
||||||
Register callback on track start, should be called as each track
|
|
||||||
in a playlist is started.
|
This method should be called as each track in a playlist is started.
|
||||||
"""
|
"""
|
||||||
self._track_start_callback = callback_func
|
self._track_start_callback = callback_func
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
|
"""Pause playback.
|
||||||
|
|
||||||
|
Stops playback but may be resumed at the exact position the pause
|
||||||
|
occured.
|
||||||
"""
|
"""
|
||||||
Pause playback.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
|
"""Resume paused playback.
|
||||||
|
|
||||||
|
Resumes playback after being paused.
|
||||||
"""
|
"""
|
||||||
Resume paused playback.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
"""
|
"""Skip to next track in playlist."""
|
||||||
Skip to next track in playlist.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def previous(self):
|
def previous(self):
|
||||||
"""
|
"""Skip to previous track in playlist."""
|
||||||
Skip to previous track in playlist.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def lower_volume(self):
|
def lower_volume(self):
|
||||||
|
"""Lower volume.
|
||||||
|
|
||||||
|
This method is used to implement audio ducking. It will be called when
|
||||||
|
Mycroft is listening or speaking to make sure the media playing isn't
|
||||||
|
interfering.
|
||||||
"""
|
"""
|
||||||
Lower volume.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def restore_volume(self):
|
def restore_volume(self):
|
||||||
|
"""Restore normal volume.
|
||||||
|
|
||||||
|
Called when to restore the playback volume to previous level after
|
||||||
|
Mycroft has lowered it using lower_volume().
|
||||||
"""
|
"""
|
||||||
Restore normal volume.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def seek_forward(self, seconds=1):
|
def seek_forward(self, seconds=1):
|
||||||
"""
|
"""Skip X seconds.
|
||||||
Skip X seconds
|
|
||||||
|
|
||||||
Args:
|
Arguments:
|
||||||
seconds (int): number of seconds to seek, if negative rewind
|
seconds (int): number of seconds to seek, if negative rewind
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
def seek_backward(self, seconds=1):
|
def seek_backward(self, seconds=1):
|
||||||
"""
|
"""Rewind X seconds.
|
||||||
Rewind X seconds
|
|
||||||
|
|
||||||
Args:
|
Arguments:
|
||||||
seconds (int): number of seconds to seek, if negative rewind
|
seconds (int): number of seconds to seek, if negative jump forward.
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
def track_info(self):
|
def track_info(self):
|
||||||
"""
|
"""Get info about current playing track.
|
||||||
Fetch info about current playing track.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with track info.
|
dict: Track info containing atleast the keys artist and album.
|
||||||
"""
|
"""
|
||||||
ret = {}
|
ret = {}
|
||||||
ret['artist'] = ''
|
ret['artist'] = ''
|
||||||
|
@ -145,13 +142,19 @@ class AudioBackend(metaclass=ABCMeta):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
""" Perform clean shutdown """
|
"""Perform clean shutdown.
|
||||||
|
|
||||||
|
Implements any audio backend specific shutdown procedures.
|
||||||
|
"""
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
class RemoteAudioBackend(AudioBackend):
|
class RemoteAudioBackend(AudioBackend):
|
||||||
""" Base class for remote audio backends.
|
"""Base class for remote audio backends.
|
||||||
|
|
||||||
These may be things like Chromecasts, mopidy servers, etc.
|
RemoteAudioBackends will always be checked after the normal
|
||||||
|
AudioBackends to make playback start locally by default.
|
||||||
|
|
||||||
|
An example of a RemoteAudioBackend would be things like Chromecasts,
|
||||||
|
mopidy servers, etc.
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
"""Factory functions for loading hotword engines - both internal and plugins.
|
||||||
|
"""
|
||||||
from time import time, sleep
|
from time import time, sleep
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
@ -54,12 +56,19 @@ def msec_to_sec(msecs):
|
||||||
msecs: milliseconds
|
msecs: milliseconds
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
input converted from milliseconds to seconds
|
int: input converted from milliseconds to seconds
|
||||||
"""
|
"""
|
||||||
return msecs / 1000
|
return msecs / 1000
|
||||||
|
|
||||||
|
|
||||||
class HotWordEngine:
|
class HotWordEngine:
|
||||||
|
"""Hotword/Wakeword base class to be implemented by all wake word plugins.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
key_phrase (str): string representation of the wake word
|
||||||
|
config (dict): Configuration block for the specific wake word
|
||||||
|
lang (str): language code (BCP-47)
|
||||||
|
"""
|
||||||
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
|
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
|
||||||
self.key_phrase = str(key_phrase).lower()
|
self.key_phrase = str(key_phrase).lower()
|
||||||
|
|
||||||
|
@ -77,25 +86,43 @@ class HotWordEngine:
|
||||||
self.lang = str(self.config.get("lang", lang)).lower()
|
self.lang = str(self.config.get("lang", lang)).lower()
|
||||||
|
|
||||||
def found_wake_word(self, frame_data):
|
def found_wake_word(self, frame_data):
|
||||||
|
"""Check if wake word has been found.
|
||||||
|
|
||||||
|
Checks if the wake word has been found. Should reset any internal
|
||||||
|
tracking of the wake word state.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
frame_data (binary data): Deprecated. Audio data for large chunk
|
||||||
|
of audio to be processed. This should not
|
||||||
|
be used to detect audio data instead
|
||||||
|
use update() to incrementaly update audio
|
||||||
|
Returns:
|
||||||
|
bool: True if a wake word was detected, else False
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update(self, chunk):
|
def update(self, chunk):
|
||||||
pass
|
"""Updates the hotword engine with new audio data.
|
||||||
|
|
||||||
|
The engine should process the data and update internal trigger state.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
chunk (bytes): Chunk of audio data to process
|
||||||
|
"""
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Perform any actions needed to shut down the hot word engine.
|
"""Perform any actions needed to shut down the wake word engine.
|
||||||
|
|
||||||
This may include things such as unload loaded data or shutdown
|
This may include things such as unloading data or shutdown
|
||||||
external processess.
|
external processess.
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PocketsphinxHotWord(HotWordEngine):
|
class PocketsphinxHotWord(HotWordEngine):
|
||||||
"""Hotword engine using PocketSphinx.
|
"""Wake word engine using PocketSphinx.
|
||||||
|
|
||||||
PocketSphinx is very general purpose but has a somewhat high error rate.
|
PocketSphinx is very general purpose but has a somewhat high error rate.
|
||||||
The key advantage is to be able to specify the wakeword with phonemes.
|
The key advantage is to be able to specify the wake word with phonemes.
|
||||||
"""
|
"""
|
||||||
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
|
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
|
||||||
super().__init__(key_phrase, config, lang)
|
super().__init__(key_phrase, config, lang)
|
||||||
|
@ -154,7 +181,7 @@ class PocketsphinxHotWord(HotWordEngine):
|
||||||
|
|
||||||
|
|
||||||
class PreciseHotword(HotWordEngine):
|
class PreciseHotword(HotWordEngine):
|
||||||
"""Precice is the default wakeword engine for mycroft.
|
"""Precise is the default wake word engine for Mycroft.
|
||||||
|
|
||||||
Precise is developed by Mycroft AI and produces quite good wake word
|
Precise is developed by Mycroft AI and produces quite good wake word
|
||||||
spotting when trained on a decent dataset.
|
spotting when trained on a decent dataset.
|
||||||
|
@ -301,7 +328,7 @@ class PreciseHotword(HotWordEngine):
|
||||||
|
|
||||||
|
|
||||||
class SnowboyHotWord(HotWordEngine):
|
class SnowboyHotWord(HotWordEngine):
|
||||||
"""Snowboy is a thirdparty hotword engine providing an easy training and
|
"""Snowboy is a thirdparty wake word engine providing an easy training and
|
||||||
testing interface.
|
testing interface.
|
||||||
"""
|
"""
|
||||||
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
|
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
|
||||||
|
@ -410,6 +437,11 @@ def load_wake_word_plugin(module_name):
|
||||||
|
|
||||||
|
|
||||||
class HotWordFactory:
|
class HotWordFactory:
|
||||||
|
"""Factory class instantiating the configured Hotword engine.
|
||||||
|
|
||||||
|
The factory can select between a range of built-in Hotword engines and also
|
||||||
|
from Hotword engine plugins.
|
||||||
|
"""
|
||||||
CLASSES = {
|
CLASSES = {
|
||||||
"pocketsphinx": PocketsphinxHotWord,
|
"pocketsphinx": PocketsphinxHotWord,
|
||||||
"precise": PreciseHotword,
|
"precise": PreciseHotword,
|
||||||
|
|
|
@ -126,7 +126,8 @@
|
||||||
"url": "https://api.mycroft.ai",
|
"url": "https://api.mycroft.ai",
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
"update": true,
|
"update": true,
|
||||||
"metrics": false
|
"metrics": false,
|
||||||
|
"sync_skill_settings": true
|
||||||
},
|
},
|
||||||
|
|
||||||
// The mycroft-core messagebus websocket
|
// The mycroft-core messagebus websocket
|
||||||
|
|
|
@ -115,7 +115,11 @@ class SkillGUI:
|
||||||
return self.__session_data.__contains__(key)
|
return self.__session_data.__contains__(key)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Reset the value dictionary, and remove namespace from GUI."""
|
"""Reset the value dictionary, and remove namespace from GUI.
|
||||||
|
|
||||||
|
This method does not close the GUI for a Skill. For this purpose see
|
||||||
|
the `release` method.
|
||||||
|
"""
|
||||||
self.__session_data = {}
|
self.__session_data = {}
|
||||||
self.page = None
|
self.page = None
|
||||||
self.skill.bus.emit(Message("gui.clear.namespace",
|
self.skill.bus.emit(Message("gui.clear.namespace",
|
||||||
|
@ -349,6 +353,15 @@ class SkillGUI:
|
||||||
self.show_page("SYSTEM_UrlFrame.qml", override_idle,
|
self.show_page("SYSTEM_UrlFrame.qml", override_idle,
|
||||||
override_animations)
|
override_animations)
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
"""Signal that this skill is no longer using the GUI,
|
||||||
|
allow different platforms to properly handle this event.
|
||||||
|
Also calls self.clear() to reset the state variables
|
||||||
|
Platforms can close the window or go back to previous page"""
|
||||||
|
self.clear()
|
||||||
|
self.skill.bus.emit(Message("mycroft.gui.screen.close",
|
||||||
|
{"skill_id": self.skill.skill_id}))
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Shutdown gui interface.
|
"""Shutdown gui interface.
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,11 @@ from .base import IntentMatch
|
||||||
class AdaptIntent(IntentBuilder):
|
class AdaptIntent(IntentBuilder):
|
||||||
"""Wrapper for IntentBuilder setting a blank name.
|
"""Wrapper for IntentBuilder setting a blank name.
|
||||||
|
|
||||||
This is mainly here for backwards compatibility, adapt now support
|
Arguments:
|
||||||
automatically named IntentBulders.
|
name (str): Optional name of intent
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, name=''):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
|
||||||
def _strip_result(context_features):
|
def _strip_result(context_features):
|
||||||
|
|
|
@ -139,6 +139,12 @@ class SettingsMetaUploader:
|
||||||
self.settings_meta = {}
|
self.settings_meta = {}
|
||||||
self.api = None
|
self.api = None
|
||||||
self.upload_timer = None
|
self.upload_timer = None
|
||||||
|
self.sync_enabled = self.config["server"].get("sync_skill_settings",
|
||||||
|
False)
|
||||||
|
if not self.sync_enabled:
|
||||||
|
LOG.info("Skill settings sync is disabled, settingsmeta will "
|
||||||
|
"not be uploaded")
|
||||||
|
|
||||||
self._stopped = None
|
self._stopped = None
|
||||||
|
|
||||||
# Property placeholders
|
# Property placeholders
|
||||||
|
@ -218,6 +224,8 @@ class SettingsMetaUploader:
|
||||||
The settingsmeta file does not change often, if at all. Only perform
|
The settingsmeta file does not change often, if at all. Only perform
|
||||||
the upload if a change in the file is detected.
|
the upload if a change in the file is detected.
|
||||||
"""
|
"""
|
||||||
|
if not self.sync_enabled:
|
||||||
|
return
|
||||||
synced = False
|
synced = False
|
||||||
if is_paired():
|
if is_paired():
|
||||||
self.api = DeviceApi()
|
self.api = DeviceApi()
|
||||||
|
@ -315,6 +323,11 @@ class SkillSettingsDownloader:
|
||||||
self.remote_settings = None
|
self.remote_settings = None
|
||||||
self.api = DeviceApi()
|
self.api = DeviceApi()
|
||||||
self.download_timer = None
|
self.download_timer = None
|
||||||
|
self.sync_enabled = Configuration.get()["server"]\
|
||||||
|
.get("sync_skill_settings", False)
|
||||||
|
if not self.sync_enabled:
|
||||||
|
LOG.info("Skill settings sync is disabled, backend settings will "
|
||||||
|
"not be downloaded")
|
||||||
|
|
||||||
def stop_downloading(self):
|
def stop_downloading(self):
|
||||||
"""Stop synchronizing backend and core."""
|
"""Stop synchronizing backend and core."""
|
||||||
|
@ -328,6 +341,8 @@ class SkillSettingsDownloader:
|
||||||
|
|
||||||
When used as a messagebus handler a message is passed but not used.
|
When used as a messagebus handler a message is passed but not used.
|
||||||
"""
|
"""
|
||||||
|
if not self.sync_enabled:
|
||||||
|
return
|
||||||
if is_paired():
|
if is_paired():
|
||||||
remote_settings = self._get_remote_settings()
|
remote_settings = self._get_remote_settings()
|
||||||
if remote_settings:
|
if remote_settings:
|
||||||
|
|
|
@ -27,7 +27,7 @@ from mycroft.util.plugins import load_plugin
|
||||||
|
|
||||||
|
|
||||||
class STT(metaclass=ABCMeta):
|
class STT(metaclass=ABCMeta):
|
||||||
""" STT Base class, all STT backends derives from this one. """
|
"""STT Base class, all STT backends derive from this one. """
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
config_core = Configuration.get()
|
config_core = Configuration.get()
|
||||||
self.lang = str(self.init_language(config_core))
|
self.lang = str(self.init_language(config_core))
|
||||||
|
@ -39,6 +39,7 @@ class STT(metaclass=ABCMeta):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def init_language(config_core):
|
def init_language(config_core):
|
||||||
|
"""Helper method to get language code from Mycroft config."""
|
||||||
lang = config_core.get("lang", "en-US")
|
lang = config_core.get("lang", "en-US")
|
||||||
langs = lang.split("-")
|
langs = lang.split("-")
|
||||||
if len(langs) == 2:
|
if len(langs) == 2:
|
||||||
|
@ -47,7 +48,21 @@ class STT(metaclass=ABCMeta):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def execute(self, audio, language=None):
|
def execute(self, audio, language=None):
|
||||||
pass
|
"""Implementation of STT functionallity.
|
||||||
|
|
||||||
|
This method needs to be implemented by the derived class to implement
|
||||||
|
the specific STT engine connection.
|
||||||
|
|
||||||
|
The method gets passed audio and optionally a language code and is
|
||||||
|
expected to return a text string.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
audio (AudioData): audio recorded by mycroft.
|
||||||
|
language (str): optional language code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: parsed text
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TokenSTT(STT, metaclass=ABCMeta):
|
class TokenSTT(STT, metaclass=ABCMeta):
|
||||||
|
@ -322,8 +337,14 @@ class DeepSpeechServerSTT(STT):
|
||||||
|
|
||||||
|
|
||||||
class StreamThread(Thread, metaclass=ABCMeta):
|
class StreamThread(Thread, metaclass=ABCMeta):
|
||||||
"""
|
"""ABC class to be used with StreamingSTT class implementations.
|
||||||
ABC class to be used with StreamingSTT class implementations.
|
|
||||||
|
This class reads audio chunks from a queue and sends it to a parsing
|
||||||
|
STT engine.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
queue (Queue): Input Queue
|
||||||
|
language (str): language code for the current language.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, queue, language):
|
def __init__(self, queue, language):
|
||||||
|
@ -333,6 +354,7 @@ class StreamThread(Thread, metaclass=ABCMeta):
|
||||||
self.text = None
|
self.text = None
|
||||||
|
|
||||||
def _get_data(self):
|
def _get_data(self):
|
||||||
|
"""Generator reading audio data from queue."""
|
||||||
while True:
|
while True:
|
||||||
d = self.queue.get()
|
d = self.queue.get()
|
||||||
if d is None:
|
if d is None:
|
||||||
|
@ -341,23 +363,38 @@ class StreamThread(Thread, metaclass=ABCMeta):
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Thread entry point."""
|
||||||
return self.handle_audio_stream(self._get_data(), self.language)
|
return self.handle_audio_stream(self._get_data(), self.language)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def handle_audio_stream(self, audio, language):
|
def handle_audio_stream(self, audio, language):
|
||||||
pass
|
"""Handling of audio stream.
|
||||||
|
|
||||||
|
Needs to be implemented by derived class to process audio data and
|
||||||
|
optionally update `self.text` with the current hypothesis.
|
||||||
|
|
||||||
|
Argumens:
|
||||||
|
audio (bytes): raw audio data.
|
||||||
|
language (str): language code for the current session.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class StreamingSTT(STT, metaclass=ABCMeta):
|
class StreamingSTT(STT, metaclass=ABCMeta):
|
||||||
"""
|
"""ABC class for threaded streaming STT implemenations."""
|
||||||
ABC class for threaded streaming STT implemenations.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.stream = None
|
self.stream = None
|
||||||
self.can_stream = True
|
self.can_stream = True
|
||||||
|
|
||||||
def stream_start(self, language=None):
|
def stream_start(self, language=None):
|
||||||
|
"""Indicate start of new audio stream.
|
||||||
|
|
||||||
|
This creates a new thread for handling the incomming audio stream as
|
||||||
|
it's collected by Mycroft.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
language (str): optional language code for the new stream.
|
||||||
|
"""
|
||||||
self.stream_stop()
|
self.stream_stop()
|
||||||
language = language or self.lang
|
language = language or self.lang
|
||||||
self.queue = Queue()
|
self.queue = Queue()
|
||||||
|
@ -365,9 +402,21 @@ class StreamingSTT(STT, metaclass=ABCMeta):
|
||||||
self.stream.start()
|
self.stream.start()
|
||||||
|
|
||||||
def stream_data(self, data):
|
def stream_data(self, data):
|
||||||
|
"""Receiver of audio data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
data (bytes): raw audio data.
|
||||||
|
"""
|
||||||
self.queue.put(data)
|
self.queue.put(data)
|
||||||
|
|
||||||
def stream_stop(self):
|
def stream_stop(self):
|
||||||
|
"""Indicate that the audio stream has ended.
|
||||||
|
|
||||||
|
This will tear down the processing thread and collect the result
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: parsed text
|
||||||
|
"""
|
||||||
if self.stream is not None:
|
if self.stream is not None:
|
||||||
self.queue.put(None)
|
self.queue.put(None)
|
||||||
self.stream.join()
|
self.stream.join()
|
||||||
|
@ -380,11 +429,20 @@ class StreamingSTT(STT, metaclass=ABCMeta):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def execute(self, audio, language=None):
|
def execute(self, audio, language=None):
|
||||||
|
"""End the parsing thread and collect data."""
|
||||||
return self.stream_stop()
|
return self.stream_stop()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create_streaming_thread(self):
|
def create_streaming_thread(self):
|
||||||
pass
|
"""Create thread for parsing audio chunks.
|
||||||
|
|
||||||
|
This method should be implemented by the derived class to return an
|
||||||
|
instance derived from StreamThread to handle the audio stream and
|
||||||
|
send it to the STT engine.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StreamThread: Thread to handle audio data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class DeepSpeechStreamThread(StreamThread):
|
class DeepSpeechStreamThread(StreamThread):
|
||||||
|
@ -550,7 +608,9 @@ def load_stt_plugin(module_name):
|
||||||
"""Wrapper function for loading stt plugin.
|
"""Wrapper function for loading stt plugin.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
(str) Mycroft stt module name from config
|
module_name (str): Mycroft stt module name from config
|
||||||
|
Returns:
|
||||||
|
class: STT plugin class
|
||||||
"""
|
"""
|
||||||
return load_plugin('mycroft.plugin.stt', module_name)
|
return load_plugin('mycroft.plugin.stt', module_name)
|
||||||
|
|
||||||
|
|
|
@ -128,10 +128,10 @@ class PlaybackThread(Thread):
|
||||||
"""Send viseme data to enclosure
|
"""Send viseme data to enclosure
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
pairs(list): Visime and timing pair
|
pairs (list): Visime and timing pair
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if button has been pressed.
|
bool: True if button has been pressed.
|
||||||
"""
|
"""
|
||||||
if self.enclosure:
|
if self.enclosure:
|
||||||
self.enclosure.mouth_viseme(time(), pairs)
|
self.enclosure.mouth_viseme(time(), pairs)
|
||||||
|
@ -187,7 +187,7 @@ class TTS(metaclass=ABCMeta):
|
||||||
self.tts_name = type(self).__name__
|
self.tts_name = type(self).__name__
|
||||||
|
|
||||||
def load_spellings(self):
|
def load_spellings(self):
|
||||||
"""Load phonetic spellings of words as dictionary"""
|
"""Load phonetic spellings of words as dictionary."""
|
||||||
path = join('text', self.lang.lower(), 'phonetic_spellings.txt')
|
path = join('text', self.lang.lower(), 'phonetic_spellings.txt')
|
||||||
spellings_file = resolve_resource_file(path)
|
spellings_file = resolve_resource_file(path)
|
||||||
if not spellings_file:
|
if not spellings_file:
|
||||||
|
@ -202,7 +202,7 @@ class TTS(metaclass=ABCMeta):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def begin_audio(self):
|
def begin_audio(self):
|
||||||
"""Helper function for child classes to call in execute()"""
|
"""Helper function for child classes to call in execute()."""
|
||||||
# Create signals informing start of speech
|
# Create signals informing start of speech
|
||||||
self.bus.emit(Message("recognizer_loop:audio_output_start"))
|
self.bus.emit(Message("recognizer_loop:audio_output_start"))
|
||||||
|
|
||||||
|
@ -254,11 +254,23 @@ class TTS(metaclass=ABCMeta):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def modify_tag(self, tag):
|
def modify_tag(self, tag):
|
||||||
"""Override to modify each supported ssml tag"""
|
"""Override to modify each supported ssml tag.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
tag (str): SSML tag to check and possibly transform.
|
||||||
|
"""
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_ssml(text):
|
def remove_ssml(text):
|
||||||
|
"""Removes SSML tags from a string.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
text (str): input string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: input string stripped from tags.
|
||||||
|
"""
|
||||||
return re.sub('<[^>]*>', '', text).replace(' ', ' ')
|
return re.sub('<[^>]*>', '', text).replace(' ', ' ')
|
||||||
|
|
||||||
def validate_ssml(self, utterance):
|
def validate_ssml(self, utterance):
|
||||||
|
@ -267,10 +279,10 @@ class TTS(metaclass=ABCMeta):
|
||||||
Remove unsupported / invalid tags
|
Remove unsupported / invalid tags
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
utterance(str): Sentence to validate
|
utterance (str): Sentence to validate
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
validated_sentence (str)
|
str: validated_sentence
|
||||||
"""
|
"""
|
||||||
# if ssml is not supported by TTS engine remove all tags
|
# if ssml is not supported by TTS engine remove all tags
|
||||||
if not self.ssml_tags:
|
if not self.ssml_tags:
|
||||||
|
@ -306,14 +318,14 @@ class TTS(metaclass=ABCMeta):
|
||||||
def execute(self, sentence, ident=None, listen=False):
|
def execute(self, sentence, ident=None, listen=False):
|
||||||
"""Convert sentence to speech, preprocessing out unsupported ssml
|
"""Convert sentence to speech, preprocessing out unsupported ssml
|
||||||
|
|
||||||
The method caches results if possible using the hash of the
|
The method caches results if possible using the hash of the
|
||||||
sentence.
|
sentence.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
sentence: Sentence to be spoken
|
sentence: (str) Sentence to be spoken
|
||||||
ident: Id reference to current interaction
|
ident: (str) Id reference to current interaction
|
||||||
listen: True if listen should be triggered at the end
|
listen: (bool) True if listen should be triggered at the end
|
||||||
of the utterance.
|
of the utterance.
|
||||||
"""
|
"""
|
||||||
sentence = self.validate_ssml(sentence)
|
sentence = self.validate_ssml(sentence)
|
||||||
|
|
||||||
|
@ -357,11 +369,16 @@ class TTS(metaclass=ABCMeta):
|
||||||
self.queue.put((self.audio_ext, wav_file, vis, ident, l))
|
self.queue.put((self.audio_ext, wav_file, vis, ident, l))
|
||||||
|
|
||||||
def viseme(self, phonemes):
|
def viseme(self, phonemes):
|
||||||
"""Create visemes from phonemes. Needs to be implemented for all
|
"""Create visemes from phonemes.
|
||||||
tts backends.
|
|
||||||
|
|
||||||
Arguments:
|
May be implemented to convert TTS phonemes into Mycroft mouth
|
||||||
phonemes(str): String with phoneme data
|
visuals.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
phonemes (str): String with phoneme data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: visemes
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -384,8 +401,8 @@ class TTS(metaclass=ABCMeta):
|
||||||
"""Cache phonemes
|
"""Cache phonemes
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
key: Hash key for the sentence
|
key (str): Hash key for the sentence
|
||||||
phonemes: phoneme string to save
|
phonemes (str): phoneme string to save
|
||||||
"""
|
"""
|
||||||
cache_dir = mycroft.util.get_cache_directory("tts/" + self.tts_name)
|
cache_dir = mycroft.util.get_cache_directory("tts/" + self.tts_name)
|
||||||
pho_file = os.path.join(cache_dir, key + ".pho")
|
pho_file = os.path.join(cache_dir, key + ".pho")
|
||||||
|
@ -400,7 +417,7 @@ class TTS(metaclass=ABCMeta):
|
||||||
"""Load phonemes from cache file.
|
"""Load phonemes from cache file.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
Key: Key identifying phoneme cache
|
key (str): Key identifying phoneme cache
|
||||||
"""
|
"""
|
||||||
pho_file = os.path.join(
|
pho_file = os.path.join(
|
||||||
mycroft.util.get_cache_directory("tts/" + self.tts_name),
|
mycroft.util.get_cache_directory("tts/" + self.tts_name),
|
||||||
|
@ -436,6 +453,7 @@ class TTSValidator(metaclass=ABCMeta):
|
||||||
self.validate_connection()
|
self.validate_connection()
|
||||||
|
|
||||||
def validate_dependencies(self):
|
def validate_dependencies(self):
|
||||||
|
"""Determine if all the TTS's external dependencies are satisfied."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def validate_instance(self):
|
def validate_instance(self):
|
||||||
|
@ -454,15 +472,19 @@ class TTSValidator(metaclass=ABCMeta):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def validate_lang(self):
|
def validate_lang(self):
|
||||||
pass
|
"""Ensure the TTS supports current language."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def validate_connection(self):
|
def validate_connection(self):
|
||||||
pass
|
"""Ensure the TTS can connect to it's backend.
|
||||||
|
|
||||||
|
This can mean for example being able to launch the correct executable
|
||||||
|
or contact a webserver.
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_tts_class(self):
|
def get_tts_class(self):
|
||||||
pass
|
"""Return TTS class that this validator is for."""
|
||||||
|
|
||||||
|
|
||||||
def load_tts_plugin(module_name):
|
def load_tts_plugin(module_name):
|
||||||
|
@ -470,11 +492,18 @@ def load_tts_plugin(module_name):
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
(str) Mycroft tts module name from config
|
(str) Mycroft tts module name from config
|
||||||
|
Returns:
|
||||||
|
class: found tts plugin class
|
||||||
"""
|
"""
|
||||||
return load_plugin('mycroft.plugin.tts', module_name)
|
return load_plugin('mycroft.plugin.tts', module_name)
|
||||||
|
|
||||||
|
|
||||||
class TTSFactory:
|
class TTSFactory:
|
||||||
|
"""Factory class instantiating the configured TTS engine.
|
||||||
|
|
||||||
|
The factory can select between a range of built-in TTS engines and also
|
||||||
|
from TTS engine plugins.
|
||||||
|
"""
|
||||||
from mycroft.tts.festival_tts import Festival
|
from mycroft.tts.festival_tts import Festival
|
||||||
from mycroft.tts.espeak_tts import ESpeak
|
from mycroft.tts.espeak_tts import ESpeak
|
||||||
from mycroft.tts.fa_tts import FATTS
|
from mycroft.tts.fa_tts import FATTS
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
six==1.13.0
|
six==1.13.0
|
||||||
requests==2.20.0
|
requests==2.20.0
|
||||||
gTTS==2.1.1
|
gTTS==2.2.0
|
||||||
PyAudio==0.2.11
|
PyAudio==0.2.11
|
||||||
pyee==8.1.0
|
pyee==8.1.0
|
||||||
SpeechRecognition==3.8.1
|
SpeechRecognition==3.8.1
|
||||||
|
|
|
@ -12,11 +12,28 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
from copy import deepcopy
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from msm import MycroftSkillsManager
|
from msm import MycroftSkillsManager
|
||||||
from msm.skill_repo import SkillRepo
|
from msm.skill_repo import SkillRepo
|
||||||
|
|
||||||
|
from mycroft.configuration.config import LocalConf, DEFAULT_CONFIG
|
||||||
|
|
||||||
|
__CONFIG = LocalConf(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def base_config():
|
||||||
|
"""Base config used when mocking.
|
||||||
|
|
||||||
|
Preload to skip hitting the disk each creation time but make a copy
|
||||||
|
so modifications don't mutate it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(dict) Mycroft default configuration
|
||||||
|
"""
|
||||||
|
return deepcopy(__CONFIG)
|
||||||
|
|
||||||
|
|
||||||
def mock_msm(temp_dir):
|
def mock_msm(temp_dir):
|
||||||
"""Mock the MycroftSkillsManager because it reaches out to the internet."""
|
"""Mock the MycroftSkillsManager because it reaches out to the internet."""
|
||||||
|
@ -50,27 +67,13 @@ def mock_msm(temp_dir):
|
||||||
def mock_config(temp_dir):
|
def mock_config(temp_dir):
|
||||||
"""Supply a reliable return value for the Configuration.get() method."""
|
"""Supply a reliable return value for the Configuration.get() method."""
|
||||||
get_config_mock = Mock()
|
get_config_mock = Mock()
|
||||||
get_config_mock.return_value = dict(
|
config = base_config()
|
||||||
skills=dict(
|
config['skills']['priority_skills'] = ['foobar']
|
||||||
msm=dict(
|
config['data_dir'] = str(temp_dir)
|
||||||
directory='skills',
|
config['server']['metrics'] = False
|
||||||
versioned=True,
|
config['enclosure'] = {}
|
||||||
repo=dict(
|
|
||||||
cache='.skills-repo',
|
|
||||||
url='https://github.com/MycroftAI/mycroft-skills',
|
|
||||||
branch='19.02'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
update_interval=1.0,
|
|
||||||
auto_update=True,
|
|
||||||
blacklisted_skills=[],
|
|
||||||
priority_skills=['foobar'],
|
|
||||||
upload_skill_manifest=True
|
|
||||||
),
|
|
||||||
data_dir=str(temp_dir),
|
|
||||||
enclosure=dict()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
get_config_mock.return_value = config
|
||||||
return get_config_mock
|
return get_config_mock
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"server": {
|
|
||||||
"metrics": false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ from adapt.intent import IntentBuilder
|
||||||
from mycroft.configuration import Configuration
|
from mycroft.configuration import Configuration
|
||||||
from mycroft.messagebus import Message
|
from mycroft.messagebus import Message
|
||||||
from mycroft.skills.intent_service import (ContextManager, IntentService,
|
from mycroft.skills.intent_service import (ContextManager, IntentService,
|
||||||
_get_message_lang)
|
_get_message_lang, AdaptIntent)
|
||||||
|
|
||||||
from test.util import base_config
|
from test.util import base_config
|
||||||
|
|
||||||
|
@ -327,3 +327,14 @@ class TestIntentServiceApi(TestCase):
|
||||||
self.intent_service.handle_get_adapt(msg)
|
self.intent_service.handle_get_adapt(msg)
|
||||||
reply = get_last_message(self.intent_service.bus)
|
reply = get_last_message(self.intent_service.bus)
|
||||||
self.assertEqual(reply.data['intent'], None)
|
self.assertEqual(reply.data['intent'], None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdaptIntent(TestCase):
|
||||||
|
"""Test the AdaptIntent wrapper."""
|
||||||
|
def test_named_intent(self):
|
||||||
|
intent = AdaptIntent("CallEaglesIntent")
|
||||||
|
self.assertEqual(intent.name, "CallEaglesIntent")
|
||||||
|
|
||||||
|
def test_unnamed_intent(self):
|
||||||
|
intent = AdaptIntent()
|
||||||
|
self.assertEqual(intent.name, "")
|
||||||
|
|
10
test/util.py
10
test/util.py
|
@ -1,12 +1,4 @@
|
||||||
from mycroft.configuration.config import LocalConf, DEFAULT_CONFIG
|
from test.unittests.mocks import base_config # For backwards compatibility
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
__config = LocalConf(DEFAULT_CONFIG)
|
|
||||||
|
|
||||||
|
|
||||||
# Base config to use when mocking
|
|
||||||
def base_config():
|
|
||||||
return deepcopy(__config)
|
|
||||||
|
|
||||||
|
|
||||||
class Anything:
|
class Anything:
|
||||||
|
|
Loading…
Reference in New Issue