Advertisement
input54

jasper stt.py

Aug 24th, 2016
604
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 23.85 KB | None | 0 0
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8-*-
  3. import os
  4. import wave
  5. import json
  6. import tempfile
  7. import logging
  8. import urllib
  9. import urlparse
  10. import re
  11. import subprocess
  12. from abc import ABCMeta, abstractmethod
  13. import requests
  14. import yaml
  15. import jasperpath
  16. import diagnose
  17. import vocabcompiler
  18.  
  19.  
  20. class AbstractSTTEngine(object):
  21.     """
  22.    Generic parent class for all STT engines
  23.    """
  24.  
  25.     __metaclass__ = ABCMeta
  26.     VOCABULARY_TYPE = None
  27.  
  28.     @classmethod
  29.     def get_config(cls):
  30.         return {}
  31.  
  32.     @classmethod
  33.     def get_instance(cls, vocabulary_name, phrases):
  34.         config = cls.get_config()
  35.         if cls.VOCABULARY_TYPE:
  36.             vocabulary = cls.VOCABULARY_TYPE(vocabulary_name,
  37.                                              path=jasperpath.config(
  38.                                                  'vocabularies'))
  39.             if not vocabulary.matches_phrases(phrases):
  40.                 vocabulary.compile(phrases)
  41.             config['vocabulary'] = vocabulary
  42.         instance = cls(**config)
  43.         return instance
  44.  
  45.     @classmethod
  46.     def get_passive_instance(cls):
  47.         phrases = vocabcompiler.get_keyword_phrases()
  48.         return cls.get_instance('keyword', phrases)
  49.  
  50.     @classmethod
  51.     def get_active_instance(cls):
  52.         phrases = vocabcompiler.get_all_phrases()
  53.         return cls.get_instance('default', phrases)
  54.  
  55.     @classmethod
  56.     @abstractmethod
  57.     def is_available(cls):
  58.         return True
  59.  
  60.     @abstractmethod
  61.     def transcribe(self, fp):
  62.         pass
  63.  
  64.  
  65. class PocketSphinxSTT(AbstractSTTEngine):
  66.     """
  67.    The default Speech-to-Text implementation which relies on PocketSphinx.
  68.    """
  69.  
  70.     SLUG = 'sphinx'
  71.     VOCABULARY_TYPE = vocabcompiler.PocketsphinxVocabulary
  72.  
  73.     def __init__(self, vocabulary, hmm_dir="/usr/local/share/" +
  74.                  "pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k"):
  75.  
  76.         """
  77.        Initiates the pocketsphinx instance.
  78.  
  79.        Arguments:
  80.            vocabulary -- a PocketsphinxVocabulary instance
  81.            hmm_dir -- the path of the Hidden Markov Model (HMM)
  82.        """
  83.  
  84.         self._logger = logging.getLogger(__name__)
  85.  
  86.         # quirky bug where first import doesn't work
  87.         try:
  88.             import pocketsphinx as ps
  89.         except:
  90.             import pocketsphinx as ps
  91.  
  92.         with tempfile.NamedTemporaryFile(prefix='psdecoder_',
  93.                                          suffix='.log', delete=False) as f:
  94.             self._logfile = f.name
  95.  
  96.         self._logger.debug("Initializing PocketSphinx Decoder with hmm_dir " +
  97.                            "'%s'", hmm_dir)
  98.  
  99.         # Perform some checks on the hmm_dir so that we can display more
  100.         # meaningful error messages if neccessary
  101.         if not os.path.exists(hmm_dir):
  102.             msg = ("hmm_dir '%s' does not exist! Please make sure that you " +
  103.                    "have set the correct hmm_dir in your profile.") % hmm_dir
  104.             self._logger.error(msg)
  105.             raise RuntimeError(msg)
  106.         # Lets check if all required files are there. Refer to:
  107.         # http://cmusphinx.sourceforge.net/wiki/acousticmodelformat
  108.         # for details
  109.         missing_hmm_files = []
  110.         for fname in ('mdef', 'feat.params', 'means', 'noisedict',
  111.                       'transition_matrices', 'variances'):
  112.             if not os.path.exists(os.path.join(hmm_dir, fname)):
  113.                 missing_hmm_files.append(fname)
  114.         mixweights = os.path.exists(os.path.join(hmm_dir, 'mixture_weights'))
  115.         sendump = os.path.exists(os.path.join(hmm_dir, 'sendump'))
  116.         if not mixweights and not sendump:
  117.             # We only need mixture_weights OR sendump
  118.             missing_hmm_files.append('mixture_weights or sendump')
  119.         if missing_hmm_files:
  120.             self._logger.warning("hmm_dir '%s' is missing files: %s. Please " +
  121.                                  "make sure that you have set the correct " +
  122.                                  "hmm_dir in your profile.",
  123.                                  hmm_dir, ', '.join(missing_hmm_files))
  124.  
  125.         self._decoder = ps.Decoder(hmm=hmm_dir, logfn=self._logfile,
  126.                                    **vocabulary.decoder_kwargs)
  127.  
  128.  
  129.  
  130.  
  131.     def __del__(self):
  132.         os.remove(self._logfile)
  133.  
  134.     @classmethod
  135.     def get_config(cls):
  136.         # FIXME: Replace this as soon as we have a config module
  137.         config = {}
  138.         # HMM dir
  139.         # Try to get hmm_dir from config
  140.         profile_path = jasperpath.config('profile.yml')
  141.  
  142.         if os.path.exists(profile_path):
  143.             with open(profile_path, 'r') as f:
  144.                 profile = yaml.safe_load(f)
  145.                 try:
  146.                     config['hmm_dir'] = profile['pocketsphinx']['hmm_dir']
  147.                 except KeyError:
  148.                     pass
  149.  
  150.         return config
  151.  
  152.     def transcribe(self, fp):
  153.         """
  154.        Performs STT, transcribing an audio file and returning the result.
  155.  
  156.        Arguments:
  157.            fp -- a file object containing audio data
  158.        """
  159.  
  160.         fp.seek(44)
  161.  
  162.         # FIXME: Can't use the Decoder.decode_raw() here, because
  163.         # pocketsphinx segfaults with tempfile.SpooledTemporaryFile()
  164.         data = fp.read()
  165.         self._decoder.start_utt()
  166.         self._decoder.process_raw(data, False, True)
  167.         self._decoder.end_utt()
  168.  
  169.         result = self._decoder.get_hyp()
  170.         with open(self._logfile, 'r+') as f:
  171.             for line in f:
  172.                 self._logger.debug(line.strip())
  173.             f.truncate()
  174.  
  175.         transcribed = [result[0]]
  176.         self._logger.info('Transcribed: %r', transcribed)
  177.         return transcribed
  178.  
  179.     @classmethod
  180.     def is_available(cls):
  181.         return diagnose.check_python_import('pocketsphinx')
  182.  
  183.  
  184. class JuliusSTT(AbstractSTTEngine):
  185.     """
  186.    A very basic Speech-to-Text engine using Julius.
  187.    """
  188.  
  189.     SLUG = 'julius'
  190.     VOCABULARY_TYPE = vocabcompiler.JuliusVocabulary
  191.  
  192.     def __init__(self, vocabulary=None, hmmdefs="/usr/share/voxforge/julius/" +
  193.                  "acoustic_model_files/hmmdefs", tiedlist="/usr/share/" +
  194.                  "voxforge/julius/acoustic_model_files/tiedlist"):
  195.         self._logger = logging.getLogger(__name__)
  196.         self._vocabulary = vocabulary
  197.         self._hmmdefs = hmmdefs
  198.         self._tiedlist = tiedlist
  199.         self._pattern = re.compile(r'sentence(\d+): <s> (.+) </s>')
  200.  
  201.         # Inital test run: we run this command once to log errors/warnings
  202.         cmd = ['julius',
  203.                '-input', 'stdin',
  204.                '-dfa', self._vocabulary.dfa_file,
  205.                '-v', self._vocabulary.dict_file,
  206.                '-h', self._hmmdefs,
  207.                '-hlist', self._tiedlist,
  208.                '-forcedict']
  209.         cmd = [str(x) for x in cmd]
  210.         self._logger.debug('Executing: %r', cmd)
  211.         with tempfile.SpooledTemporaryFile() as out_f:
  212.             with tempfile.SpooledTemporaryFile() as f:
  213.                 with tempfile.SpooledTemporaryFile() as err_f:
  214.                     subprocess.call(cmd, stdin=f, stdout=out_f, stderr=err_f)
  215.             out_f.seek(0)
  216.             for line in out_f.read().splitlines():
  217.                 line = line.strip()
  218.                 if len(line) > 7 and line[:7].upper() == 'ERROR: ':
  219.                     if not line[7:].startswith('adin_'):
  220.                         self._logger.error(line[7:])
  221.                 elif len(line) > 9 and line[:9].upper() == 'WARNING: ':
  222.                     self._logger.warning(line[9:])
  223.                 elif len(line) > 6 and line[:6].upper() == 'STAT: ':
  224.                     self._logger.debug(line[6:])
  225.  
  226.     @classmethod
  227.     def get_config(cls):
  228.         # FIXME: Replace this as soon as we have a config module
  229.         config = {}
  230.         # HMM dir
  231.         # Try to get hmm_dir from config
  232.         profile_path = jasperpath.config('profile.yml')
  233.  
  234.         if os.path.exists(profile_path):
  235.             with open(profile_path, 'r') as f:
  236.                 profile = yaml.safe_load(f)
  237.                 if 'julius' in profile:
  238.                     if 'hmmdefs' in profile['julius']:
  239.                         config['hmmdefs'] = profile['julius']['hmmdefs']
  240.                     if 'tiedlist' in profile['julius']:
  241.                         config['tiedlist'] = profile['julius']['tiedlist']
  242.         return config
  243.  
  244.     def transcribe(self, fp, mode=None):
  245.         cmd = ['julius',
  246.                '-quiet',
  247.                '-nolog',
  248.                '-input', 'stdin',
  249.                '-dfa', self._vocabulary.dfa_file,
  250.                '-v', self._vocabulary.dict_file,
  251.                '-h', self._hmmdefs,
  252.                '-hlist', self._tiedlist,
  253.                '-forcedict']
  254.         cmd = [str(x) for x in cmd]
  255.         self._logger.debug('Executing: %r', cmd)
  256.         with tempfile.SpooledTemporaryFile() as out_f:
  257.             with tempfile.SpooledTemporaryFile() as err_f:
  258.                 subprocess.call(cmd, stdin=fp, stdout=out_f, stderr=err_f)
  259.             out_f.seek(0)
  260.             results = [(int(i), text) for i, text in
  261.                        self._pattern.findall(out_f.read())]
  262.         transcribed = [text for i, text in
  263.                        sorted(results, key=lambda x: x[0])
  264.                        if text]
  265.         if not transcribed:
  266.             transcribed.append('')
  267.         self._logger.info('Transcribed: %r', transcribed)
  268.         return transcribed
  269.  
  270.     @classmethod
  271.     def is_available(cls):
  272.         return diagnose.check_executable('julius')
  273.  
  274.  
  275. class GoogleSTT(AbstractSTTEngine):
  276.     """
  277.    Speech-To-Text implementation which relies on the Google Speech API.
  278.  
  279.    This implementation requires a Google API key to be present in profile.yml
  280.  
  281.    To obtain an API key:
  282.    1. Join the Chromium Dev group:
  283.       https://groups.google.com/a/chromium.org/forum/?fromgroups#!forum/chromium-dev
  284.    2. Create a project through the Google Developers console:
  285.       https://console.developers.google.com/project
  286.    3. Select your project. In the sidebar, navigate to "APIs & Auth." Activate
  287.       the Speech API.
  288.    4. Under "APIs & Auth," navigate to "Credentials." Create a new key for
  289.       public API access.
  290.    5. Add your credentials to your profile.yml. Add an entry to the 'keys'
  291.       section using the key name 'GOOGLE_SPEECH.' Sample configuration:
  292.    6. Set the value of the 'stt_engine' key in your profile.yml to 'google'
  293.  
  294.  
  295.    Excerpt from sample profile.yml:
  296.  
  297.        ...
  298.        timezone: US/Pacific
  299.        stt_engine: google
  300.        keys:
  301.            GOOGLE_SPEECH: $YOUR_KEY_HERE
  302.  
  303.    """
  304.  
  305.     SLUG = 'google'
  306.  
  307.     def __init__(self, api_key=None, language='en-us'):
  308.         # FIXME: get init args from config
  309.         """
  310.        Arguments:
  311.        api_key - the public api key which allows access to Google APIs
  312.        """
  313.         self._logger = logging.getLogger(__name__)
  314.         self._request_url = None
  315.         self._language = None
  316.         self._api_key = None
  317.         self._http = requests.Session()
  318.         self.language = language
  319.         self.api_key = api_key
  320.  
  321.     @property
  322.     def request_url(self):
  323.         return self._request_url
  324.  
  325.     @property
  326.     def language(self):
  327.         return self._language
  328.  
  329.     @language.setter
  330.     def language(self, value):
  331.         self._language = value
  332.         self._regenerate_request_url()
  333.  
  334.     @property
  335.     def api_key(self):
  336.         return self._api_key
  337.  
  338.     @api_key.setter
  339.     def api_key(self, value):
  340.         self._api_key = value
  341.         self._regenerate_request_url()
  342.  
  343.     def _regenerate_request_url(self):
  344.         if self.api_key and self.language:
  345.             query = urllib.urlencode({'output': 'json',
  346.                                       'client': 'chromium',
  347.                                       'key': self.api_key,
  348.                                       'lang': self.language,
  349.                                       'maxresults': 6,
  350.                                       'pfilter': 2})
  351.             self._request_url = urlparse.urlunparse(
  352.                 ('https', 'www.google.com', '/speech-api/v2/recognize', '',
  353.                  query, ''))
  354.         else:
  355.             self._request_url = None
  356.  
  357.     @classmethod
  358.     def get_config(cls):
  359.         # FIXME: Replace this as soon as we have a config module
  360.         config = {}
  361.         # HMM dir
  362.         # Try to get hmm_dir from config
  363.         profile_path = jasperpath.config('profile.yml')
  364.         if os.path.exists(profile_path):
  365.             with open(profile_path, 'r') as f:
  366.                 profile = yaml.safe_load(f)
  367.                 if 'keys' in profile and 'GOOGLE_SPEECH' in profile['keys']:
  368.                     config['api_key'] = profile['keys']['GOOGLE_SPEECH']
  369.         return config
  370.  
  371.     def transcribe(self, fp):
  372.         """
  373.        Performs STT via the Google Speech API, transcribing an audio file and
  374.        returning an English string.
  375.  
  376.        Arguments:
  377.        audio_file_path -- the path to the .wav file to be transcribed
  378.        """
  379.  
  380.         if not self.api_key:
  381.             self._logger.critical('API key missing, transcription request ' +
  382.                                   'aborted.')
  383.             return []
  384.         elif not self.language:
  385.             self._logger.critical('Language info missing, transcription ' +
  386.                                   'request aborted.')
  387.             return []
  388.  
  389.         wav = wave.open(fp, 'rb')
  390.         frame_rate = wav.getframerate()
  391.         wav.close()
  392.         data = fp.read()
  393.  
  394.         headers = {'content-type': 'audio/l16; rate=%s' % frame_rate}
  395.         r = self._http.post(self.request_url, data=data, headers=headers)
  396.         try:
  397.             r.raise_for_status()
  398.         except requests.exceptions.HTTPError:
  399.             self._logger.critical('Request failed with http status %d',
  400.                                   r.status_code)
  401.             if r.status_code == requests.codes['forbidden']:
  402.                 self._logger.warning('Status 403 is probably caused by an ' +
  403.                                      'invalid Google API key.')
  404.             return []
  405.         r.encoding = 'utf-8'
  406.         try:
  407.             # We cannot simply use r.json() because Google sends invalid json
  408.             # (i.e. multiple json objects, seperated by newlines. We only want
  409.             # the last one).
  410.             response = json.loads(list(r.text.strip().split('\n', 1))[-1])
  411.             if len(response['result']) == 0:
  412.                 # Response result is empty
  413.                 raise ValueError('Nothing has been transcribed.')
  414.             results = [alt['transcript'] for alt
  415.                        in response['result'][0]['alternative']]
  416.         except ValueError as e:
  417.             self._logger.warning('Empty response: %s', e.args[0])
  418.             results = []
  419.         except (KeyError, IndexError):
  420.             self._logger.warning('Cannot parse response.', exc_info=True)
  421.             results = []
  422.         else:
  423.             # Convert all results to uppercase
  424.             results = tuple(result.upper() for result in results)
  425.             self._logger.info('Transcribed: %r', results)
  426.         return results
  427.  
  428.     @classmethod
  429.     def is_available(cls):
  430.         return diagnose.check_network_connection()
  431.  
  432.  
  433. class AttSTT(AbstractSTTEngine):
  434.     """
  435.    Speech-To-Text implementation which relies on the AT&T Speech API.
  436.  
  437.    This implementation requires an AT&T app_key/app_secret to be present in
  438.    profile.yml. Please sign up at http://developer.att.com/apis/speech and
  439.    create a new app. You can then take the app_key/app_secret and put it into
  440.    your profile.yml:
  441.        ...
  442.        stt_engine: att
  443.        att-stt:
  444.          app_key:    4xxzd6abcdefghijklmnopqrstuvwxyz
  445.          app_secret: 6o5jgiabcdefghijklmnopqrstuvwxyz
  446.    """
  447.  
  448.     SLUG = "att"
  449.  
  450.     def __init__(self, app_key, app_secret):
  451.         self._logger = logging.getLogger(__name__)
  452.         self._token = None
  453.         self.app_key = app_key
  454.         self.app_secret = app_secret
  455.  
  456.     @classmethod
  457.     def get_config(cls):
  458.         # FIXME: Replace this as soon as we have a config module
  459.         config = {}
  460.         # Try to get AT&T app_key/app_secret from config
  461.         profile_path = jasperpath.config('profile.yml')
  462.         if os.path.exists(profile_path):
  463.             with open(profile_path, 'r') as f:
  464.                 profile = yaml.safe_load(f)
  465.                 if 'att-stt' in profile:
  466.                     if 'app_key' in profile['att-stt']:
  467.                         config['app_key'] = profile['att-stt']['app_key']
  468.                     if 'app_secret' in profile['att-stt']:
  469.                         config['app_secret'] = profile['att-stt']['app_secret']
  470.         return config
  471.  
  472.     @property
  473.     def token(self):
  474.         if not self._token:
  475.             headers = {'content-type': 'application/x-www-form-urlencoded',
  476.                        'accept': 'application/json'}
  477.             payload = {'client_id': self.app_key,
  478.                        'client_secret': self.app_secret,
  479.                        'scope': 'SPEECH',
  480.                        'grant_type': 'client_credentials'}
  481.             r = requests.post('https://api.att.com/oauth/v4/token',
  482.                               data=payload,
  483.                               headers=headers)
  484.             self._token = r.json()['access_token']
  485.         return self._token
  486.  
  487.     def transcribe(self, fp):
  488.         data = fp.read()
  489.         r = self._get_response(data)
  490.         if r.status_code == requests.codes['unauthorized']:
  491.             # Request token invalid, retry once with a new token
  492.             self._logger.warning('OAuth access token invalid, generating a ' +
  493.                                  'new one and retrying...')
  494.             self._token = None
  495.             r = self._get_response(data)
  496.         try:
  497.             r.raise_for_status()
  498.         except requests.exceptions.HTTPError:
  499.             self._logger.critical('Request failed with response: %r',
  500.                                   r.text,
  501.                                   exc_info=True)
  502.             return []
  503.         except requests.exceptions.RequestException:
  504.             self._logger.critical('Request failed.', exc_info=True)
  505.             return []
  506.         else:
  507.             try:
  508.                 recognition = r.json()['Recognition']
  509.                 if recognition['Status'] != 'OK':
  510.                     raise ValueError(recognition['Status'])
  511.                 results = [(x['Hypothesis'], x['Confidence'])
  512.                            for x in recognition['NBest']]
  513.             except ValueError as e:
  514.                 self._logger.debug('Recognition failed with status: %s',
  515.                                    e.args[0])
  516.                 return []
  517.             except KeyError:
  518.                 self._logger.critical('Cannot parse response.',
  519.                                       exc_info=True)
  520.                 return []
  521.             else:
  522.                 transcribed = [x[0].upper() for x in sorted(results,
  523.                                                             key=lambda x: x[1],
  524.                                                             reverse=True)]
  525.                 self._logger.info('Transcribed: %r', transcribed)
  526.                 return transcribed
  527.  
  528.     def _get_response(self, data):
  529.         headers = {'authorization': 'Bearer %s' % self.token,
  530.                    'accept': 'application/json',
  531.                    'content-type': 'audio/wav'}
  532.         return requests.post('https://api.att.com/speech/v3/speechToText',
  533.                              data=data,
  534.                              headers=headers)
  535.  
  536.     @classmethod
  537.     def is_available(cls):
  538.         return diagnose.check_network_connection()
  539.  
  540.  
  541. class WitAiSTT(AbstractSTTEngine):
  542.     """
  543.    Speech-To-Text implementation which relies on the Wit.ai Speech API.
  544.  
  545.    This implementation requires an Wit.ai Access Token to be present in
  546.    profile.yml. Please sign up at https://wit.ai and copy your instance
  547.    token, which can be found under Settings in the Wit console to your
  548.    profile.yml:
  549.        ...
  550.        stt_engine: witai
  551.        witai-stt:
  552.          access_token:    ERJKGE86SOMERANDOMTOKEN23471AB
  553.    """
  554.  
  555.     SLUG = "witai"
  556.  
  557.     def __init__(self, access_token):
  558.         self._logger = logging.getLogger(__name__)
  559.         self.token = access_token
  560.  
  561.     @classmethod
  562.     def get_config(cls):
  563.         # FIXME: Replace this as soon as we have a config module
  564.         config = {}
  565.         # Try to get wit.ai Auth token from config
  566.         profile_path = jasperpath.config('profile.yml')
  567.         if os.path.exists(profile_path):
  568.             with open(profile_path, 'r') as f:
  569.                 profile = yaml.safe_load(f)
  570.                 if 'witai-stt' in profile:
  571.                     if 'access_token' in profile['witai-stt']:
  572.                         config['access_token'] = \
  573.                             profile['witai-stt']['access_token']
  574.         return config
  575.  
  576.     @property
  577.     def token(self):
  578.         return self._token
  579.  
  580.     @token.setter
  581.     def token(self, value):
  582.         self._token = value
  583.         self._headers = {'Authorization': 'Bearer %s' % self.token,
  584.                          'accept': 'application/json',
  585.                          'Content-Type': 'audio/wav'}
  586.  
  587.     @property
  588.     def headers(self):
  589.         return self._headers
  590.  
  591.     def transcribe(self, fp):
  592.         data = fp.read()
  593.         r = requests.post('https://api.wit.ai/speech?v=20150101',
  594.                           data=data,
  595.                           headers=self.headers)
  596.         try:
  597.             r.raise_for_status()
  598.             text = r.json()['_text']
  599.         except requests.exceptions.HTTPError:
  600.             self._logger.critical('Request failed with response: %r',
  601.                                   r.text,
  602.                                   exc_info=True)
  603.             return []
  604.         except requests.exceptions.RequestException:
  605.             self._logger.critical('Request failed.', exc_info=True)
  606.             return []
  607.         except ValueError as e:
  608.             self._logger.critical('Cannot parse response: %s',
  609.                                   e.args[0])
  610.             return []
  611.         except KeyError:
  612.             self._logger.critical('Cannot parse response.',
  613.                                   exc_info=True)
  614.             return []
  615.         else:
  616.             transcribed = []
  617.             if text:
  618.                 transcribed.append(text.upper())
  619.             self._logger.info('Transcribed: %r', transcribed)
  620.             return transcribed
  621.  
  622.     @classmethod
  623.     def is_available(cls):
  624.         return diagnose.check_network_connection()
  625.  
  626.  
  627. def get_engine_by_slug(slug=None):
  628.     """
  629.    Returns:
  630.        An STT Engine implementation available on the current platform
  631.  
  632.    Raises:
  633.        ValueError if no speaker implementation is supported on this platform
  634.    """
  635.  
  636.     if not slug or type(slug) is not str:
  637.         raise TypeError("Invalid slug '%s'", slug)
  638.  
  639.     selected_engines = filter(lambda engine: hasattr(engine, "SLUG") and
  640.                               engine.SLUG == slug, get_engines())
  641.     if len(selected_engines) == 0:
  642.         raise ValueError("No STT engine found for slug '%s'" % slug)
  643.     else:
  644.         if len(selected_engines) > 1:
  645.             print(("WARNING: Multiple STT engines found for slug '%s'. " +
  646.                    "This is most certainly a bug.") % slug)
  647.         engine = selected_engines[0]
  648.         if not engine.is_available():
  649.             raise ValueError(("STT engine '%s' is not available (due to " +
  650.                               "missing dependencies, missing " +
  651.                               "dependencies, etc.)") % slug)
  652.         return engine
  653.  
  654.  
  655. def get_engines():
  656.     def get_subclasses(cls):
  657.         subclasses = set()
  658.         for subclass in cls.__subclasses__():
  659.             subclasses.add(subclass)
  660.             subclasses.update(get_subclasses(subclass))
  661.         return subclasses
  662.     return [tts_engine for tts_engine in
  663.             list(get_subclasses(AbstractSTTEngine))
  664.             if hasattr(tts_engine, 'SLUG') and tts_engine.SLUG]
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement