Repository: MycroftAI/mycroft-core

Issue:
pocketsphinx custom wake words broken (backend)
https://github.com/MycroftAI/mycroft-core/pull/2395 reverted this https://github.com/MycroftAI/mycroft-core/pull/1062#discussion_r137691072

backend seems to still be setting phonemes under listener, either backend needs update or that pr needs to be reverted

symptoms reported by users in chat:
https://chat.mycroft.ai/community/pl/3uayazg6tbb6pgf5ug6wdetwne
https://chat.mycroft.ai/community/pl/a8hfu5gx5jgcmee3u3rxc5ozza


Discussion:

Resolving patch:
diff --git a/mycroft/client/speech/listener.py b/mycroft/client/speech/listener.py
--- a/mycroft/client/speech/listener.py
+++ b/mycroft/client/speech/listener.py
@@ -309,32 +309,52 @@ def _load_config(self):
         self.state = RecognizerLoopState()
 
     def create_wake_word_recognizer(self):
-        # Create a local recognizer to hear the wakeup word, e.g. 'Hey Mycroft'
-        LOG.info("creating wake word engine")
-        word = self.config.get("wake_word", "hey mycroft")
+        """Create a local recognizer to hear the wakeup word
+
+        For example 'Hey Mycroft'.
+
+        The method uses the hotword entry for the selected wakeword, if
+        one is missing it will fall back to the old phoneme and threshold in
+        the listener entry in the config.
+
+        If the hotword entry doesn't include phoneme and threshold values these
+        will be patched in using the defaults from the config listnere entry.
+        """
+        LOG.info('Creating wake word engine')
+        word = self.config.get('wake_word', 'hey mycroft')
 
         # TODO remove this, only for server settings compatibility
-        phonemes = self.config.get("phonemes")
-        thresh = self.config.get("threshold")
+        phonemes = self.config.get('phonemes')
+        thresh = self.config.get('threshold')
 
         # Since we're editing it for server backwards compatibility
         # use a copy so we don't alter the hash of the config and
         # trigger a reload.
-        config = deepcopy(self.config_core.get("hotwords", {}))
+        config = deepcopy(self.config_core.get('hotwords', {}))
         if word not in config:
             # Fallback to using config from "listener" block
             LOG.warning('Wakeword doesn\'t have an entry falling back'
                         'to old listener config')
             config[word] = {'module': 'precise'}
             if phonemes:
-                config[word]["phonemes"] = phonemes
+                config[word]['phonemes'] = phonemes
             if thresh:
-                config[word]["threshold"] = thresh
+                config[word]['threshold'] = thresh
             if phonemes is None or thresh is None:
                 config = None
-        return HotWordFactory.create_hotword(
-            word, config, self.lang, loop=self
-        )
+        else:
+            LOG.info('Using hotword entry for {}'.format(word))
+            if 'phonemes' not in config[word]:
+                LOG.warning('Phonemes are missing falling back to listeners '
+                            'configuration')
+                config[word]['phonemes'] = phonemes
+            if 'threshold' not in config[word]:
+                LOG.warning('Threshold is missing falling back to listeners '
+                            'configuration')
+                config[word]['threshold'] = thresh
+
+        return HotWordFactory.create_hotword(word, config, self.lang,
+                                             loop=self)
 
     def create_wakeup_recognizer(self):
         LOG.info("creating stand up word engine")


Test patch:
diff --git a/test/unittests/client/test_local_recognizer.py b/test/unittests/client/test_local_recognizer.py
--- a/test/unittests/client/test_local_recognizer.py
+++ b/test/unittests/client/test_local_recognizer.py
@@ -51,7 +51,10 @@ def testRecognitionInLongerUtterance(self):
 
 class LocalRecognizerInitTest(unittest.TestCase):
     @patch.object(Configuration, 'get')
-    def testRecognizer(self, mock_config_get):
+    def testListenerConfig(self, mock_config_get):
+        """Ensure that the fallback method collecting phonemes etc.
+        from the listener config works.
+        """
         test_config = base_config()
         mock_config_get.return_value = test_config
 
@@ -71,3 +74,44 @@ def testRecognizer(self, mock_config_get):
         test_config['listener']['phonemes'] = 'ZZZZZZZZZZZZ'
         rl = RecognizerLoop()
         self.assertEqual(rl.wakeword_recognizer.key_phrase, "hey mycroft")
+
+    @patch.object(Configuration, 'get')
+    def testHotwordConfig(self, mock_config_get):
+        """Ensure that the fallback method collecting phonemes etc.
+        from the listener config works.
+        """
+        test_config = base_config()
+        mock_config_get.return_value = test_config
+
+        # Set fallback values
+        test_config['listener']['phonemes'] = 'HH EY . V IH K T AO R IY AH'
+        test_config['listener']['threshold'] = 1e-90
+
+        steve_conf = {
+            'model': 'pocketsphinx',
+            'phonemes': 'S T IY V .',
+            'threshold': 1e-42
+        }
+
+        test_config['hotwords']['steve'] = steve_conf
+        test_config['listener']['wake_word'] = 'steve'
+
+        rl = RecognizerLoop()
+        self.assertEqual(rl.wakeword_recognizer.key_phrase, 'steve')
+
+        # Ensure phonemes and threshold are poulated from listener config
+        # if they're missing
+
+        # Set fallback values
+        test_config['listener']['phonemes'] = 'S T IY V .'
+        test_config['listener']['threshold'] = 1e-90
+
+        steve_conf = {
+            'model': 'pocketsphinx'
+        }
+
+        test_config['hotwords']['steve'] = steve_conf
+        test_config['listener']['wake_word'] = 'steve'
+        rl = RecognizerLoop()
+        self.assertEqual(rl.wakeword_recognizer.key_phrase, 'steve')
+        self.assertEqual(rl.wakeword_recognizer.phonemes, 'S T IY V .')