Repository: MycroftAI/mycroft-core

Issue:
IBM Watson STT not working
The IBM STT interface is not currently working as IBM have changed their authentication mechanism. 

The existing implementation uses the [SpeechRecognition Python package](https://github.com/Uberi/speech_recognition) which has not been updated to use the new API key. 

The simplest fix would be to implement a new StreamingSTT class within Mycroft and not use the SR package for this service.

IBM STT Docs:
https://cloud.ibm.com/apidocs/speech-to-text/speech-to-text



Discussion:
It would probably suffice with an even simpler non-streaming version as a stop-gap. wouldn't require many lines of code. (the streaming service which can be a bit tricky)

Granted, the streaming API would be optimal
Some guidance for anyone attempting the non-streaming version:
Change the base class to TokenSTT, the user will need to supply the url in the module config

```json
"stt" {
  "ibm": {
    "credential": {
      "token": "API_KEY_FROM_SERVICE"
    },
    "url": "URL_FROM_SERVICE"
  }
```
To authenticate using requests use HTTPBasicAuth('api' self.credential['token']) to authenticate with the service,

the request call becomes something like
```python
response = post(
    self.config['url'] + IBMSTT.ENDPOINT,
    headers={'Content-Type': 'audio/flac'},
    data=audio.get_flac_data(convert_rate=16000),
    auth=HTTPBasicAuth('apikey', self.token))

Then some additional parameters for language and/or model could be added as well.

Resolving patch:
diff --git a/mycroft/stt/__init__.py b/mycroft/stt/__init__.py
--- a/mycroft/stt/__init__.py
+++ b/mycroft/stt/__init__.py
@@ -108,14 +108,84 @@ def execute(self, audio, language=None):
         return self.recognizer.recognize_wit(audio, self.token)
 
 
-class IBMSTT(BasicSTT):
+class IBMSTT(TokenSTT):
+    """
+        IBM Speech to Text
+        Enables IBM Speech to Text access using API key. To use IBM as a
+        service provider, it must be configured locally in your config file. An
+        IBM Cloud account with Speech to Text enabled is required (limited free
+        tier may be available). STT config should match the following format:
+
+        "stt": {
+            "module": "ibm",
+            "ibm": {
+                "credential": {
+                    "token": "YOUR_API_KEY"
+                },
+                "url": "URL_FROM_SERVICE"
+            }
+        }
+    """
     def __init__(self):
         super(IBMSTT, self).__init__()
 
     def execute(self, audio, language=None):
+        if not self.token:
+            raise ValueError('API key (token) for IBM Cloud is not defined.')
+
+        url_base = self.config.get('url', '')
+        if not url_base:
+            raise ValueError('URL for IBM Cloud is not defined.')
+        url = url_base + '/v1/recognize'
+
         self.lang = language or self.lang
-        return self.recognizer.recognize_ibm(audio, self.username,
-                                             self.password, self.lang)
+        supported_languages = [
+            'ar-AR', 'pt-BR', 'zh-CN', 'nl-NL', 'en-GB', 'en-US', 'fr-FR',
+            'de-DE', 'it-IT', 'ja-JP', 'ko-KR', 'es-AR', 'es-ES', 'es-CL',
+            'es-CO', 'es-MX', 'es-PE'
+        ]
+        if self.lang not in supported_languages:
+            raise ValueError(
+                'Unsupported language "{}" for IBM STT.'.format(self.lang))
+
+        audio_model = 'BroadbandModel'
+        if audio.sample_rate < 16000 and not self.lang == 'ar-AR':
+            audio_model = 'NarrowbandModel'
+
+        params = {
+            'model': '{}_{}'.format(self.lang, audio_model),
+            'profanity_filter': 'false'
+        }
+        headers = {
+            'Content-Type': 'audio/x-flac',
+            'X-Watson-Learning-Opt-Out': 'true'
+        }
+
+        response = post(url, auth=('apikey', self.token), headers=headers,
+                        data=audio.get_flac_data(), params=params)
+
+        if response.status_code == 200:
+            result = json.loads(response.text)
+            if result.get('error_code') is None:
+                if ('results' not in result or len(result['results']) < 1 or
+                        'alternatives' not in result['results'][0]):
+                    raise Exception(
+                        'Transcription failed. Invalid or empty results.')
+                transcription = []
+                for utterance in result['results']:
+                    if 'alternatives' not in utterance:
+                        raise Exception(
+                            'Transcription failed. Invalid or empty results.')
+                    for hypothesis in utterance['alternatives']:
+                        if 'transcript' in hypothesis:
+                            transcription.append(hypothesis['transcript'])
+                return '\n'.join(transcription)
+        elif response.status_code == 401:  # Unauthorized
+            raise Exception('Invalid API key for IBM Cloud.')
+        else:
+            raise Exception(
+                'Request to IBM Cloud failed. Code: {} Body: {}'.format(
+                    response.status_code, response.text))
 
 
 class YandexSTT(STT):


Test patch:
diff --git a/test/unittests/stt/test_stt.py b/test/unittests/stt/test_stt.py
--- a/test/unittests/stt/test_stt.py
+++ b/test/unittests/stt/test_stt.py
@@ -35,13 +35,18 @@ def test_factory(self, mock_get):
                     'google': {'credential': {'token': 'FOOBAR'}},
                     'bing': {'credential': {'token': 'FOOBAR'}},
                     'houndify': {'credential': {'client_id': 'FOO',
-                                                "client_key": "BAR"}},
+                                                "client_key": 'BAR'}},
                     'google_cloud': {
                         'credential': {
                             'json': {}
                         }
                     },
-                    'ibm': {'credential': {'token': 'FOOBAR'}},
+                    'ibm': {
+                        'credential': {
+                            'token': 'FOOBAR'
+                        },
+                        'url': 'https://test.com/'
+                    },
                     'kaldi': {'uri': 'https://test.com'},
                     'mycroft': {'uri': 'https://test.com'}
                 },
@@ -164,26 +169,64 @@ def test_google_cloud_stt(self, mock_get):
         stt.execute(audio)
         self.assertTrue(stt.recognizer.recognize_google_cloud.called)
 
+    @patch('mycroft.stt.post')
     @patch.object(Configuration, 'get')
-    def test_ibm_stt(self, mock_get):
-        mycroft.stt.Recognizer = MagicMock
+    def test_ibm_stt(self, mock_get, mock_post):
+        import json
+
         config = base_config()
         config.merge(
             {
                 'stt': {
                     'module': 'ibm',
                     'ibm': {
-                        'credential': {'username': 'FOO', 'password': 'BAR'}
+                        'credential': {
+                            'token': 'FOOBAR'
+                        },
+                        'url': 'https://test.com'
                     },
                 },
                 'lang': 'en-US'
-            })
+            }
+        )
         mock_get.return_value = config
 
+        requests_object = MagicMock()
+        requests_object.status_code = 200
+        requests_object.text = json.dumps({
+            'results': [
+                {
+                    'alternatives': [
+                        {
+                            'confidence': 0.96,
+                            'transcript': 'sample response'
+                        }
+                    ],
+                    'final': True
+                }
+            ],
+            'result_index': 0
+        })
+        mock_post.return_value = requests_object
+
         audio = MagicMock()
+        audio.sample_rate = 16000
+
         stt = mycroft.stt.IBMSTT()
         stt.execute(audio)
-        self.assertTrue(stt.recognizer.recognize_ibm.called)
+
+        test_url_base = 'https://test.com/v1/recognize'
+        mock_post.assert_called_with(test_url_base,
+                                     auth=('apikey', 'FOOBAR'),
+                                     headers={
+                                         'Content-Type': 'audio/x-flac',
+                                         'X-Watson-Learning-Opt-Out': 'true'
+                                     },
+                                     data=audio.get_flac_data(),
+                                     params={
+                                         'model': 'en-US_BroadbandModel',
+                                         'profanity_filter': 'false'
+                                     })
 
     @patch.object(Configuration, 'get')
     def test_wit_stt(self, mock_get):