Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/fallback_stt #77

Merged
merged 9 commits into from Mar 3, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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
@@ -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

@@ -195,7 +197,14 @@

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)

Check warning on line 205 in mycroft/client/speech/listener.py

Codecov / codecov/patch

mycroft/client/speech/listener.py#L200-L205

Added lines #L200 - L205 were not covered by tests
else:
raise e

Check warning on line 207 in mycroft/client/speech/listener.py

Codecov / codecov/patch

mycroft/client/speech/listener.py#L207

Added line #L207 was not covered by tests
if text is not None:
text = text.lower().strip()
LOG.debug("STT: " + text)
@@ -240,23 +249,25 @@
(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

Check warning on line 257 in mycroft/client/speech/listener.py

Codecov / codecov/patch

mycroft/client/speech/listener.py#L257

Added line #L257 was not covered by tests
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
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

Check warning on line 270 in mycroft/client/speech/listener.py

Codecov / codecov/patch

mycroft/client/speech/listener.py#L269-L270

Added lines #L269 - L270 were not covered by tests

def _load_config(self):
"""Load configuration parameters from configuration."""
@@ -320,11 +331,34 @@
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()

Check warning on line 360 in mycroft/client/speech/listener.py

Codecov / codecov/patch

mycroft/client/speech/listener.py#L358-L360

Added lines #L358 - L360 were not covered by tests

self.queue = Queue()
self.audio_consumer = AudioConsumer(self)
self.audio_consumer.start()
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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

@@ -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
@@ -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.
@@ -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")