Skip to content

Commit

Permalink
feat/multiple wakewords
Browse files Browse the repository at this point in the history
Multiple hotwords! Yes, its MycroftAI#1233 again for the 9087th time

hotwords can do any of the following:
- play a sound
- start listening
- trigger "wake up"
- emit a bus event
- emit an utterance
- select a STT lang

sample config
```json
  "hotwords": {
    "hey_mycroft": {
        "module": "ovos-precise-lite",
        "model": "/home/user/precise_stuff/training/hey_mycroft.tflite",
        "listen": true,
        "sound": "snd/start_listening.wav"
        },
    "wake up": {
        "module": "pocketsphinx",
        "phonemes": "W EY K . AH P",
        "threshold": 1e-20,
        "lang": "en-us",
        "wakeup": true
        },
     "thank you": {
        "module": "pocketsphinx",
        "phonemes": "TH AE NG K . Y UW",
        "threshold": 0.1,
        "event": "mycroft.stop",
        "utterance": "thank you",
        "lang": "en-us"
    }
  },
```
  • Loading branch information
JarbasAl committed Oct 28, 2021
1 parent 47d8a1f commit a1acf43
Show file tree
Hide file tree
Showing 6 changed files with 471 additions and 442 deletions.
236 changes: 10 additions & 226 deletions mycroft/client/speech/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,239 +12,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from threading import Lock

from mycroft import dialog
from mycroft.enclosure.api import EnclosureAPI
from mycroft.client.speech.listener import RecognizerLoop
from mycroft.configuration import Configuration, setup_locale
from mycroft.identity import IdentityManager
from mycroft.client.speech.service import SpeechClient
from mycroft.configuration import setup_locale
from mycroft.lock import Lock as PIDLock # Create/Support PID locking file
from mycroft.messagebus.message import Message
from mycroft.util import (
create_daemon,
reset_sigint_handler,
start_message_bus_client,
wait_for_exit_signal
)
from mycroft.util.log import LOG
from mycroft.util.process_utils import ProcessStatus, StatusCallbackMap

bus = None # Mycroft messagebus connection
lock = Lock()
loop = None
config = None


def handle_record_begin():
"""Forward internal bus message to external bus."""
LOG.info("Begin Recording...")
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('recognizer_loop:record_begin', context=context))


def handle_record_end():
"""Forward internal bus message to external bus."""
LOG.info("End Recording...")
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('recognizer_loop:record_end', context=context))


def handle_no_internet():
LOG.debug("Notifying enclosure of no internet connection")
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('enclosure.notify.no_internet', context=context))


def handle_awoken():
"""Forward mycroft.awoken to the messagebus."""
LOG.info("Listener is now Awake: ")
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('mycroft.awoken', context=context))


def handle_wakeword(event):
LOG.info("Wakeword Detected: " + event['utterance'])
bus.emit(Message('recognizer_loop:wakeword', event))


def handle_utterance(event):
LOG.info("Utterance: " + str(event['utterances']))
context = {'client_name': 'mycroft_listener',
'source': 'audio',
'destination': ["skills"]}
if 'ident' in event:
ident = event.pop('ident')
context['ident'] = ident
bus.emit(Message('recognizer_loop:utterance', event, context))


def handle_unknown():
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('mycroft.speech.recognition.unknown', context=context))


def handle_speak(event):
"""
Forward speak message to message bus.
"""
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('speak', event, context))


def handle_complete_intent_failure(event):
"""Extreme backup for answering completely unhandled intent requests."""
LOG.info("Failed to find intent.")
data = {'utterance': dialog.get('not.loaded')}
context = {'client_name': 'mycroft_listener',
'source': 'audio'}
bus.emit(Message('speak', data, context))


def handle_sleep(event):
"""Put the recognizer loop to sleep."""
loop.sleep()


def handle_wake_up(event):
"""Wake up the the recognize loop."""
loop.awaken()


def handle_mic_mute(event):
"""Mute the listener system."""
loop.mute()


def handle_mic_unmute(event):
"""Unmute the listener system."""
loop.unmute()


def handle_mic_listen(_):
"""Handler for mycroft.mic.listen.
Starts listening as if wakeword was spoken.
"""
loop.responsive_recognizer.trigger_listen()


def handle_mic_get_status(event):
"""Query microphone mute status."""
data = {'muted': loop.is_muted()}
bus.emit(event.response(data))


def handle_paired(event):
"""Update identity information with pairing data.
This is done here to make sure it's only done in a single place.
TODO: Is there a reason this isn't done directly in the pairing skill?
"""
IdentityManager.update(event.data)


def handle_audio_start(event):
"""Mute recognizer loop."""
if config.get("listener").get("mute_during_output"):
loop.mute()


def handle_audio_end(event):
"""Request unmute, if more sources have requested the mic to be muted
it will remain muted.
"""
if config.get("listener").get("mute_during_output"):
loop.unmute() # restore


def handle_stop(event):
"""Handler for mycroft.stop, i.e. button press."""
loop.force_unmute()


def handle_open():
# TODO: Move this into the Enclosure (not speech client)
# Reset the UI to indicate ready for speech processing
EnclosureAPI(bus).reset()


def on_ready():
LOG.info('Speech client is ready.')


def on_stopping():
LOG.info('Speech service is shutting down...')


def on_error(e='Unknown'):
LOG.error('Audio service failed to launch ({}).'.format(repr(e)))


def connect_loop_events(loop):
loop.on('recognizer_loop:utterance', handle_utterance)
loop.on('recognizer_loop:speech.recognition.unknown', handle_unknown)
loop.on('speak', handle_speak)
loop.on('recognizer_loop:record_begin', handle_record_begin)
loop.on('recognizer_loop:awoken', handle_awoken)
loop.on('recognizer_loop:wakeword', handle_wakeword)
loop.on('recognizer_loop:record_end', handle_record_end)
loop.on('recognizer_loop:no_internet', handle_no_internet)


def connect_bus_events(bus):
# Register handlers for events on main Mycroft messagebus
bus.on('open', handle_open)
bus.on('complete_intent_failure', handle_complete_intent_failure)
bus.on('recognizer_loop:sleep', handle_sleep)
bus.on('recognizer_loop:wake_up', handle_wake_up)
bus.on('mycroft.mic.mute', handle_mic_mute)
bus.on('mycroft.mic.unmute', handle_mic_unmute)
bus.on('mycroft.mic.get_status', handle_mic_get_status)
bus.on('mycroft.mic.listen', handle_mic_listen)
bus.on("mycroft.paired", handle_paired)
bus.on('recognizer_loop:audio_output_start', handle_audio_start)
bus.on('recognizer_loop:audio_output_end', handle_audio_end)
bus.on('mycroft.stop', handle_stop)


def main(ready_hook=on_ready, error_hook=on_error, stopping_hook=on_stopping,
watchdog=lambda: None):
global bus
global loop
global config

callbacks = StatusCallbackMap(on_ready=ready_hook, on_error=error_hook,
on_stopping=stopping_hook)
status = ProcessStatus('speech', callback_map=callbacks)
status.set_started()
try:
reset_sigint_handler()
PIDLock("voice")
config = Configuration.get()
bus = start_message_bus_client("VOICE")
connect_bus_events(bus)
status.bind(bus)

setup_locale()

# Register handlers on internal RecognizerLoop bus
loop = RecognizerLoop(watchdog)
connect_loop_events(loop)
create_daemon(loop.run)

except Exception as e:
error_hook(e)
else:
status.set_ready()
wait_for_exit_signal()
status.set_stopping()
def main():
reset_sigint_handler()
PIDLock("voice")
setup_locale()
service = SpeechClient()
service.setDaemon(True)
service.start()
wait_for_exit_signal()


if __name__ == "__main__":
Expand Down
3 changes: 3 additions & 0 deletions mycroft/client/speech/data_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def __init__(self, size, initial_data):
# Get at most size bytes from the end of the initial data
self._buffer = initial_data[-size:]

def clear(self):
self._buffer = b'\0' * self.size

def append(self, data):
"""Add new data to the buffer, and slide out data if the buffer is full
Expand Down
Loading

0 comments on commit a1acf43

Please sign in to comment.