Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
strategy:
max-parallel: 2
matrix:
python-version: [ 3.7, 3.8, 3.9, "3.10" ]
python-version: [ 3.7, 3.8, 3.9]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
48 changes: 41 additions & 7 deletions mycroft/client/speech/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
import time
from queue import Queue, Empty
from threading import Thread

import pyaudio
from pyee import EventEmitter

from mycroft.client.speech.hotword_factory import HotWordFactory
from mycroft.client.speech.mic import MutableMicrophone, ResponsiveRecognizer
from mycroft.configuration import Configuration
from mycroft.metrics import Stopwatch, report_timing
from mycroft.session import SessionManager
from mycroft.stt import STTFactory
from mycroft.util.log import LOG
from mycroft.util import find_input_device
from queue import Queue, Empty
import json
from mycroft.util.log import LOG

MAX_MIC_RESTARTS = 20

Expand Down Expand Up @@ -195,7 +197,14 @@ def send_unknown_intent():

try:
# Invoke the STT engine on the audio clip
text = self.loop.stt.execute(audio, language=lang)
try:
text = self.loop.stt.execute(audio, language=lang)
except Exception as e:
if self.loop.fallback_stt:
LOG.warning(f"Using fallback STT, main plugin failed: {e}")
text = self.loop.fallback_stt.execute(audio, language=lang)
else:
raise e
if text is not None:
text = text.lower().strip()
LOG.debug("STT: " + text)
Expand Down Expand Up @@ -240,23 +249,25 @@ class RecognizerLoop(EventEmitter):
(optional, can be set later via self.bind )
"""

def __init__(self, bus, watchdog=None, stt=None):
def __init__(self, bus, watchdog=None, stt=None, fallback_stt=None):
super(RecognizerLoop, self).__init__()
self._watchdog = watchdog
self.mute_calls = 0
self.stt = stt
self.fallback_stt = fallback_stt
self.bus = bus
self.engines = {}
self.stt = None
self.queue = None
self.audio_consumer = None
self.audio_producer = None
self.responsive_recognizer = None

self._load_config()

def bind(self, stt):
def bind(self, stt, fallback_stt=None):
self.stt = stt
if fallback_stt:
self.fallback_stt = fallback_stt

def _load_config(self):
"""Load configuration parameters from configuration."""
Expand Down Expand Up @@ -320,11 +331,34 @@ def create_hotword_engines(self):
except Exception as e:
LOG.error("Failed to load hotword: " + word)

@staticmethod
def get_fallback_stt():
config_core = Configuration.get()
stt_config = config_core.get('stt', {})
engine = stt_config.get("fallback_module")
if not engine:
LOG.warning("No fallback STT configured")
else:
plugin_config = stt_config.get(engine) or {}
plugin_config["lang"] = plugin_config.get("lang") or \
config_core.get("lang", "en-us")
clazz = STTFactory.get_class({"module": engine,
engine: plugin_config})
if clazz:
return clazz
else:
LOG.warning(f"Could not find plugin: {engine}")
LOG.error(f"Failed to create fallback STT")

def start_async(self):
"""Start consumer and producer threads."""
self.state.running = True
if not self.stt:
self.stt = STTFactory.create()
if not self.fallback_stt:
clazz = self.get_fallback_stt()
self.fallback_stt = clazz()

self.queue = Queue()
self.audio_consumer = AudioConsumer(self)
self.audio_consumer.start()
Expand Down
15 changes: 2 additions & 13 deletions mycroft/configuration/mycroft.conf
Original file line number Diff line number Diff line change
Expand Up @@ -432,19 +432,8 @@
"stt": {
// Engine. Options: "mycroft", "google", "wit", "ibm", "kaldi", "bing",
// "houndify", "deepspeech_server", "govivace", "yandex"
"module": "mycroft"
// "deepspeech_server": {
// "uri": "http://localhost:8080/stt"
// },
// "kaldi": {
// "uri": "http://localhost:8080/client/dynamic/recognize"
// },
//"govivace": {
// "uri": "https://services.govivace.com:49149/telephony",
// "credential": {
// "token": "xxxxx"
// }
//}
"module": "mycroft",
"fallback_module": "ovos-stt-plugin-vosk"
},

// Text to Speech parameters
Expand Down
5 changes: 3 additions & 2 deletions mycroft/stt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ def execute(self, audio, language=None):

class STTFactory(OVOSSTTFactory):
@staticmethod
def create():
config = Configuration.get().get("stt", {})
def create(config=None):
config = config or Configuration.get().get("stt", {})
module = config.get("module", "mycroft")
LOG.info(f"Creating STT engine: {module}")
if module == "mycroft":
return MycroftSTT()
return OVOSSTTFactory.create(config)
1 change: 1 addition & 0 deletions requirements/extra-stt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ PyAudio~=0.2.11
ovos-ww-plugin-pocketsphinx>=0.1.2
ovos-ww-plugin-precise-lite>=0.1.1
ovos-ww-plugin-precise>=0.1.1
ovos-stt-plugin-vosk>=0.1.3a2
2 changes: 1 addition & 1 deletion requirements/minimal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ mycroft-messagebus-client~=0.9.1,!=0.9.2,!=0.9.3
psutil~=5.6.6
combo-lock~=0.2
ovos-utils~=0.0.18
ovos-plugin-manager~=0.0.10
ovos-plugin-manager~=0.0.11a1
3 changes: 2 additions & 1 deletion requirements/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ combo-lock~=0.2
PyYAML~=5.4

ovos-utils~=0.0.18
ovos-plugin-manager~=0.0.10
ovos-plugin-manager~=0.0.11a1
ovos-tts-plugin-mimic>=0.2.6
ovos-tts-plugin-mimic2>=0.1.4
ovos-tts-plugin-google-tx>=0.0.3
ovos-ww-plugin-pocketsphinx>=0.1.2
ovos-ww-plugin-precise-lite>=0.1.1
ovos-ww-plugin-precise>=0.1.1
ovos-stt-plugin-vosk>=0.1.3a2
ovos_workshop>=0.0.5
ovos_PHAL>=0.0.1

Expand Down
5 changes: 3 additions & 2 deletions test/license_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
'petact': 'MIT',
"sonopy": "Apache-2.0",
"precise-runner": "Apache-2.0",
'psutil': 'BSD3'
'psutil': 'BSD3',
"vosk": "Apache-2.0"
}
# explicitly allow these packages that would fail otherwise
whitelist = []
whitelist = ['ovos-skill-installer']

# validation flags
allow_nonfree = False
Expand Down
44 changes: 44 additions & 0 deletions test/unittests/base_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from ovos_plugin_manager.stt import find_stt_plugins
from ovos_plugin_manager.tts import find_tts_plugins
from ovos_plugin_manager.wakewords import find_wake_word_plugins
from ovos_plugin_manager.audio import find_audio_service_plugins

from unittest import TestCase, mock


class TestFindDefaults(TestCase):
def test_ww(self):
expected = ["ovos-ww-plugin-pocketsphinx",
"ovos-ww-plugin-precise",
"ovos-precise-lite" # TODO rename for convention
]
plugs = set(find_wake_word_plugins())
for plug in expected:
self.assertIn(plug, plugs)

def test_stt(self):
expected = ["ovos-stt-plugin-chromium",
"ovos-stt-plugin-vosk",
"ovos-stt-plugin-vosk-streaming"
]
plugs = set(find_stt_plugins())
for plug in expected:
self.assertIn(plug, plugs)

def test_tts(self):
expected = ["ovos-tts-plugin-mimic",
"ovos-tts-plugin-mimic2",
"ovos-tts-plugin-responsivevoice",
"ovos-tts-plugin-google-tx"
]
plugs = set(find_tts_plugins())
for plug in expected:
self.assertIn(plug, plugs)

def test_audio(self):
# TODO rename plugins for convention
expected = ['ovos_common_play',
'ovos_audio_simple']
plugs = set(find_audio_service_plugins())
for plug in expected:
self.assertIn(plug, plugs)
Empty file added test/unittests/stt/__init__.py
Empty file.
135 changes: 135 additions & 0 deletions test/unittests/stt/test_stt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright 2017 Mycroft AI Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import unittest
from io import StringIO
from unittest.mock import MagicMock, patch

import mycroft.configuration
import mycroft.stt
from mycroft.client.speech.listener import RecognizerLoop
from mycroft.util.log import LOG
from ovos_stt_plugin_vosk import VoskKaldiSTT
from test.util import base_config


class TestSTT(unittest.TestCase):
def test_factory(self):
config = {'module': 'mycroft',
'mycroft': {'uri': 'https://test.com'}}
stt = mycroft.stt.STTFactory.create(config)
self.assertEqual(type(stt), mycroft.stt.MycroftSTT)

config = {'stt': config}
stt = mycroft.stt.STTFactory.create(config)
self.assertEqual(type(stt), mycroft.stt.MycroftSTT)

@patch.object(mycroft.configuration.Configuration, 'get')
def test_factory_from_config(self, mock_get):
mycroft.stt.STTApi = MagicMock()
config = base_config()
config.merge(
{
'stt': {
'module': 'mycroft',
"fallback_module": "ovos-stt-plugin-vosk",
'mycroft': {'uri': 'https://test.com'}
},
'lang': 'en-US'
})
mock_get.return_value = config

stt = mycroft.stt.STTFactory.create()
self.assertEqual(type(stt), mycroft.stt.MycroftSTT)

@patch.object(mycroft.configuration.Configuration, 'get')
def test_mycroft_stt(self, mock_get):
mycroft.stt.STTApi = MagicMock()
config = base_config()
config.merge(
{
'stt': {
'module': 'mycroft',
'mycroft': {'uri': 'https://test.com'}
},
'lang': 'en-US'
})
mock_get.return_value = config

stt = mycroft.stt.MycroftSTT()
audio = MagicMock()
stt.execute(audio, 'en-us')
self.assertTrue(mycroft.stt.STTApi.called)

@patch.object(mycroft.configuration.Configuration, 'get')
def test_fallback_stt(self, mock_get):
config = base_config()
config.merge(
{
'stt': {
'module': 'mycroft',
"fallback_module": "ovos-stt-plugin-vosk",
'mycroft': {'uri': 'https://test.com'}
},
'lang': 'en-US'
})
mock_get.return_value = config

# check class matches
fallback_stt = RecognizerLoop.get_fallback_stt()
self.assertEqual(fallback_stt, VoskKaldiSTT)

@patch.object(mycroft.configuration.Configuration, 'get')
@patch.object(LOG, 'error')
@patch.object(LOG, 'warning')
def test_invalid_fallback_stt(self, mock_warn, mock_error, mock_get):
config = base_config()
config.merge(
{
'stt': {
'module': 'mycroft',
'fallback_module': 'invalid',
'mycroft': {'uri': 'https://test.com'}
},
'lang': 'en-US'
})
mock_get.return_value = config

fallback_stt = RecognizerLoop.get_fallback_stt()
self.assertIsNone(fallback_stt)
mock_warn.assert_called_with("Could not find plugin: invalid")
mock_error.assert_called_with("Failed to create fallback STT")

@patch.object(mycroft.configuration.Configuration, 'get')
@patch.object(LOG, 'error')
@patch.object(LOG, 'warning')
def test_fallback_stt_not_set(self, mock_warn, mock_error, mock_get):
config = base_config()
config.merge(
{
'stt': {
'module': 'mycroft',
'fallback_module': None,
'mycroft': {'uri': 'https://test.com'}
},
'lang': 'en-US'
})
mock_get.return_value = config

fallback_stt = RecognizerLoop.get_fallback_stt()
self.assertIsNone(fallback_stt)
mock_warn.assert_called_with("No fallback STT configured")
mock_error.assert_called_with("Failed to create fallback STT")