Repository: MycroftAI/mycroft-core

Issue:
Cannot get Porcupine wakeword detection working with Raspberry-pi
Trying to use Porcupine for wake word detection, and not having any success:  As there is no way to create a custom wakeword as optimizer is no longer part of the porcupine package, I do see there is a way with the picovoice console tool to create custom wake word, but the free version only allows creation of wake for for x86 not ARM based machines....so trying to work with the existing models that are included with Procupine package.  I have set mycroft.conf as this:

```
"listener": {
  "wake_word": "blueberry"
},

"hotwords": {
  "blueberry": {
    "module": "porcupine",
    "porcupine_path": "~/.mycroft/Porcupine",
    "keyword_file_path": "~/.mycroft/Porcupine/resources/keyword_files/raspberry-pi/blueberry_raspberry-pi.ppn"
    }
}
```

Porcupine wake word appears to be attempint to load, but having this error:

```
2020-10-13 18:03:22.296 | INFO     |  6577 | mycroft.client.speech.listener:create_wake_word_recognizer:328 | Creating wake word engine
2020-10-13 18:03:22.299 | INFO     |  6577 | mycroft.client.speech.listener:create_wake_word_recognizer:351 | Using hotword entry for blueberry
2020-10-13 18:03:22.302 | WARNING  |  6577 | mycroft.client.speech.listener:create_wake_word_recognizer:353 | Phonemes are missing falling back to listeners configuration
2020-10-13 18:03:22.305 | WARNING  |  6577 | mycroft.client.speech.listener:create_wake_word_recognizer:357 | Threshold is missing falling back to listeners configuration
2020-10-13 18:03:22.310 | INFO     |  6577 | mycroft.client.speech.hotword_factory:load_module:403 | Loading "blueberry" wake word via porcupine
2020-10-13 18:03:22.315 | INFO     |  6577 | mycroft.client.speech.hotword_factory:__init__:331 | Adding /home/pi/.mycroft/Porcupine/binding/python to Python path
2020-10-13 18:03:22.338 | INFO     |  6577 | mycroft.client.speech.hotword_factory:__init__:356 | Loading Porcupine using library path /home/pi/.mycroft/Porcupine/lib/linux/armv7l/libpv_porcupine.so and keyword paths ['/home/pi/.mycroft/Porcupine/resources/keyword_files/raspberry-pi/blueberry_raspberry-pi.ppn']
2020-10-13 18:03:22.341 | ERROR    |  6577 | mycroft.client.speech.hotword_factory:initialize:423 | Could not create hotword. Falling back to default.
Traceback (most recent call last):
  File "/home/pi/mycroft-core/mycroft/client/speech/hotword_factory.py", line 411, in initialize
    instance = clazz(hotword, config, lang=lang)
  File "/home/pi/mycroft-core/mycroft/client/speech/hotword_factory.py", line 361, in __init__
    sensitivities=sensitivities)
TypeError: __init__() got an unexpected keyword argument 'model_file_path'
2020-10-13 18:03:22.345 | INFO     |  6577 | mycroft.client.speech.hotword_factory:load_module:403 | Loading "blueberry" wake word via pocketsphinx
2020-10-13 18:03:22.475 | INFO     |  6577 | mycroft.client.speech.listener:create_wakeup_recognizer:365 | creating stand up word engine
2020-10-13 18:03:22.478 | INFO     |  6577 | mycroft.client.speech.hotword_factory:load_module:403 | Loading "wake up" wake word via pocketsphinx
2020-10-13 18:03:22.602 | INFO     |  6577 | __main__:on_ready:175 | Speech client is ready.
```

I saw the Phenome and Threshold warnings, so tried to set those in mycroft.conf, but it did not make any difference, I got exactly the same errors. 

Interestingly enough, I ask mycroft what the listener is and it tells me it's "Porcupine"   although any sound seems to trigger it, and it is not recogizing speech commands after trigger. 

I"m noting that the library that is loaded is "Loading Porcupine using library path /home/pi/.mycroft/Porcupine/lib/linux/armv7l/libpv_porcupine.so "   which could be an issue, as this is raspberry-pi and the Procupine package has a specific library for raspberry-pi.    I tried adjusting the code in "hotword_factory.py" to use the raspberry pi library.   

```
 library_path = join(
            porcupine_path, 'lib/raspberry-pi/arm11/libpv_porcupine.so')
        model_file_path = join(
            porcupine_path, 'lib/common/porcupine_params.pv')

```

but I get the same error.   So, how to go about getting this setup on raspberry-pi using the included raspberry-pi wakewords?

_Originally posted by @butteryak in https://github.com/MycroftAI/mycroft-core/pull/2139#issuecomment-707893646_


Discussion:
There are a bunch of changes in the porcupine repo since the hotword engine was added which will require some fixes.

I did some additional testing of this and with the latest version on x86_64 (which previously worked) constantly detects wakewords even after fixing the basic issues with missmatched arguments.

Resolving patch:
diff --git a/mycroft/client/speech/hotword_factory.py b/mycroft/client/speech/hotword_factory.py
--- a/mycroft/client/speech/hotword_factory.py
+++ b/mycroft/client/speech/hotword_factory.py
@@ -14,29 +14,27 @@
 #
 """Factory functions for loading hotword engines - both internal and plugins.
 """
-from time import time, sleep
+from contextlib import suppress
+from glob import glob
 import os
+from os.path import dirname, exists, join, abspath, expanduser, isfile, isdir
 import platform
 import posixpath
+from shutil import rmtree
 import struct
-import sys
 import tempfile
-import requests
-from contextlib import suppress
-from glob import glob
-from os.path import dirname, exists, join, abspath, expanduser, isfile, isdir
-from shutil import rmtree
 from threading import Timer, Thread
+from time import time, sleep
 from urllib.error import HTTPError
 
 from petact import install_package
+import requests
 
 from mycroft.configuration import Configuration, LocalConf, USER_CONFIG
-from mycroft.util.log import LOG
 from mycroft.util.monotonic_event import MonotonicEvent
+from mycroft.util.log import LOG
 from mycroft.util.plugins import load_plugin
 
-
 RECOGNIZER_DIR = join(abspath(dirname(__file__)), "recognizer")
 INIT_TIMEOUT = 10  # In seconds
 
@@ -357,29 +355,27 @@ def found_wake_word(self, frame_data):
 
 
 class PorcupineHotWord(HotWordEngine):
+    """Hotword engine using picovoice's Porcupine hot word engine.
+
+    TODO: Remove in 21.02
+    """
     def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
-        super(PorcupineHotWord, self).__init__(key_phrase, config, lang)
-        porcupine_path = expanduser(self.config.get(
-            "porcupine_path", join('~', '.mycroft', 'Porcupine')))
+        super().__init__(key_phrase, config, lang)
         keyword_file_paths = [expanduser(x.strip()) for x in self.config.get(
             "keyword_file_path", "hey_mycroft.ppn").split(',')]
         sensitivities = self.config.get("sensitivities", 0.5)
-        bindings_path = join(porcupine_path, 'binding/python')
-        LOG.info('Adding %s to Python path' % bindings_path)
-        sys.path.append(bindings_path)
+
         try:
-            from porcupine import Porcupine
-        except ImportError:
+            from pvporcupine.porcupine import Porcupine
+            from pvporcupine.util import (pv_library_path,
+                                          pv_model_path)
+        except ImportError as err:
             raise Exception(
                 "Python bindings for Porcupine not found. "
-                "Please use --porcupine-path to set Porcupine base path")
-
-        system = platform.system()
-        machine = platform.machine()
-        library_path = join(
-            porcupine_path, 'lib/linux/%s/libpv_porcupine.so' % machine)
-        model_file_path = join(
-            porcupine_path, 'lib/common/porcupine_params.pv')
+                "Please run \"mycroft-pip install pvporcupine\"") from err
+
+        library_path = pv_library_path('')
+        model_file_path = pv_model_path('')
         if isinstance(sensitivities, float):
             sensitivities = [sensitivities] * len(keyword_file_paths)
         else:
@@ -388,41 +384,58 @@ def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
         self.audio_buffer = []
         self.has_found = False
         self.num_keywords = len(keyword_file_paths)
+
+        LOG.warning('The Porcupine wakeword engine shipped with '
+                    'Mycroft-core is deprecated and will be removed in '
+                    'mycroft-core 21.02. Use the mycroft-porcupine-plugin '
+                    'instead.')
         LOG.info(
             'Loading Porcupine using library path {} and keyword paths {}'
             .format(library_path, keyword_file_paths))
         self.porcupine = Porcupine(
             library_path=library_path,
-            model_file_path=model_file_path,
-            keyword_file_paths=keyword_file_paths,
+            model_path=model_file_path,
+            keyword_paths=keyword_file_paths,
             sensitivities=sensitivities)
 
         LOG.info('Loaded Porcupine')
 
     def update(self, chunk):
+        """Update detection state from a chunk of audio data.
+
+        Arguments:
+            chunk (bytes): Audio data to parse
+        """
         pcm = struct.unpack_from("h" * (len(chunk)//2), chunk)
         self.audio_buffer += pcm
         while True:
             if len(self.audio_buffer) >= self.porcupine.frame_length:
                 result = self.porcupine.process(
                     self.audio_buffer[0:self.porcupine.frame_length])
-                # result could be boolean (if there is one keword)
-                # or int (if more than one keyword)
-                self.has_found |= (
-                    (self.num_keywords == 1 and result) |
-                    (self.num_keywords > 1 and result >= 0))
+                # result will be the index of the found keyword or -1 if
+                # nothing has been found.
+                self.has_found |= result >= 0
                 self.audio_buffer = self.audio_buffer[
                     self.porcupine.frame_length:]
             else:
                 return
 
     def found_wake_word(self, frame_data):
+        """Check if wakeword has been found.
+
+        Returns:
+            (bool) True if wakeword was found otherwise False.
+        """
         if self.has_found:
             self.has_found = False
             return True
         return False
 
     def stop(self):
+        """Stop the hotword engine.
+
+        Clean up Porcupine library.
+        """
         if self.porcupine is not None:
             self.porcupine.delete()
 


Test patch: