diff options
| author | schneefux <schneefux+commit@schneefux.xyz> | 2014-10-09 14:19:59 +0200 |
|---|---|---|
| committer | schneefux <schneefux+commit@schneefux.xyz> | 2014-11-05 14:37:11 +0100 |
| commit | c89514f6cd11afa0ebbd20842ab881fe42a20dba (patch) | |
| tree | b415fd6e88038e1a3d518e83f5493d0c068d5e29 | |
| parent | 533864c8814e9b02eceadd10e1bf20f46dbd1efe (diff) | |
| download | jasper-client-c89514f6cd11afa0ebbd20842ab881fe42a20dba.tar.gz jasper-client-c89514f6cd11afa0ebbd20842ab881fe42a20dba.zip | |
Remove TranscriptionMode and improve STT engine initialisation
| -rw-r--r-- | client/mic.py | 9 | ||||
| -rw-r--r-- | client/modules/MPDControl.py | 13 | ||||
| -rw-r--r-- | client/stt.py | 185 | ||||
| -rw-r--r-- | client/test.py | 13 | ||||
| -rwxr-xr-x | jasper.py | 18 |
5 files changed, 92 insertions, 146 deletions
diff --git a/client/mic.py b/client/mic.py index 2a6fe4a..f75539b 100644 --- a/client/mic.py +++ b/client/mic.py @@ -9,7 +9,6 @@ import audioop import pyaudio import alteration import jasperpath -from stt import TranscriptionMode class Mic: @@ -172,8 +171,7 @@ class Mic: wav_fp.close() f.seek(0) # check if PERSONA was said - transcribed = self.passive_stt_engine.transcribe( - f, mode=TranscriptionMode.KEYWORD) + transcribed = self.passive_stt_engine.transcribe(f) if any(PERSONA in phrase for phrase in transcribed): return (THRESHOLD, PERSONA) @@ -250,10 +248,7 @@ class Mic: wav_fp.writeframes(''.join(frames)) wav_fp.close() f.seek(0) - mode = (TranscriptionMode.MUSIC if MUSIC - else TranscriptionMode.NORMAL) - transcribed = self.active_stt_engine.transcribe(f, mode=mode) - return transcribed + return self.active_stt_engine.transcribe(f) def say(self, phrase, OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"): diff --git a/client/modules/MPDControl.py b/client/modules/MPDControl.py index f23fc7d..2548237 100644 --- a/client/modules/MPDControl.py +++ b/client/modules/MPDControl.py @@ -3,9 +3,6 @@ import re import logging import difflib import mpd -import stt -import vocabcompiler -import jasperpath from mic import Mic # Standard module stuff @@ -78,17 +75,11 @@ class MusicMode(object): "PLAYLIST"] phrases.extend(self.music.get_soup_playlist()) - vocabulary_music = vocabcompiler.PocketsphinxVocabulary( - name='music', path=jasperpath.config('vocabularies')) - vocabulary_music.compile(phrases) - - # create a new mic with the new music models - config = stt.PocketSphinxSTT.get_config() + music_stt_engine = mic.active_stt_engine.get_instance('music', phrases) self.mic = Mic(mic.speaker, mic.passive_stt_engine, - stt.PocketSphinxSTT(vocabulary_music=vocabulary_music, - **config)) + music_stt_engine) def delegateInput(self, input): diff --git a/client/stt.py b/client/stt.py index f5254ca..0ae7871 100644 --- a/client/stt.py +++ b/client/stt.py @@ -14,28 +14,48 @@ import diagnose import vocabcompiler -class TranscriptionMode: - NORMAL, KEYWORD, MUSIC = range(3) - - class AbstractSTTEngine(object): """ Generic parent class for all STT engines """ __metaclass__ = ABCMeta + VOCABULARY_TYPE = None @classmethod def get_config(cls): return {} @classmethod + def get_instance(cls, vocabulary_name, phrases): + config = cls.get_config() + if cls.VOCABULARY_TYPE: + vocabulary = cls.VOCABULARY_TYPE(vocabulary_name, + path=jasperpath.config( + 'vocabularies')) + if not vocabulary.matches_phrases(phrases): + vocabulary.compile(phrases) + config['vocabulary'] = vocabulary + instance = cls(**config) + return instance + + @classmethod + def get_passive_instance(cls): + phrases = vocabcompiler.get_keyword_phrases() + return cls.get_instance('keyword', phrases) + + @classmethod + def get_active_instance(cls): + phrases = vocabcompiler.get_all_phrases() + return cls.get_instance('default', phrases) + + @classmethod @abstractmethod def is_available(cls): return True @abstractmethod - def transcribe(self, fp, mode=TranscriptionMode.NORMAL): + def transcribe(self, fp): pass @@ -45,9 +65,9 @@ class PocketSphinxSTT(AbstractSTTEngine): """ SLUG = 'sphinx' + VOCABULARY_TYPE = vocabcompiler.PocketsphinxVocabulary - def __init__(self, vocabulary=None, vocabulary_keyword=None, - vocabulary_music=None, hmm_dir="/usr/local/share/" + + def __init__(self, vocabulary, hmm_dir="/usr/local/share/" + "pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k"): """ @@ -55,9 +75,6 @@ class PocketSphinxSTT(AbstractSTTEngine): Arguments: vocabulary -- a PocketsphinxVocabulary instance - vocabulary_keyword -- a PocketsphinxVocabulary instance - (containing, e.g., 'Jasper') - vocabulary_music -- (optional) a PocketsphinxVocabulary instance hmm_dir -- the path of the Hidden Markov Model (HMM) """ @@ -69,35 +86,15 @@ class PocketSphinxSTT(AbstractSTTEngine): except: import pocketsphinx as ps - self._logfiles = {} - with tempfile.NamedTemporaryFile(prefix='psdecoder_music_', - suffix='.log', delete=False) as f: - self._logfiles[TranscriptionMode.MUSIC] = f.name - with tempfile.NamedTemporaryFile(prefix='psdecoder_keyword_', - suffix='.log', delete=False) as f: - self._logfiles[TranscriptionMode.KEYWORD] = f.name - with tempfile.NamedTemporaryFile(prefix='psdecoder_normal_', + with tempfile.NamedTemporaryFile(prefix='psdecoder_', suffix='.log', delete=False) as f: - self._logfiles[TranscriptionMode.NORMAL] = f.name + self._logfile = f.name - self._decoders = {} - if vocabulary_music is not None: - self._decoders[TranscriptionMode.MUSIC] = \ - ps.Decoder(hmm=hmm_dir, - logfn=self._logfiles[TranscriptionMode.MUSIC], - **vocabulary_music.decoder_kwargs) - self._decoders[TranscriptionMode.KEYWORD] = \ - ps.Decoder(hmm=hmm_dir, - logfn=self._logfiles[TranscriptionMode.KEYWORD], - **vocabulary_keyword.decoder_kwargs) - self._decoders[TranscriptionMode.NORMAL] = \ - ps.Decoder(hmm=hmm_dir, - logfn=self._logfiles[TranscriptionMode.NORMAL], - **vocabulary.decoder_kwargs) + self._decoder = ps.Decoder(hmm=hmm_dir, logfn=self._logfile, + **vocabulary.decoder_kwargs) def __del__(self): - for filename in self._logfiles.values(): - os.remove(filename) + os.remove(self._logfile) @classmethod def get_config(cls): @@ -107,78 +104,38 @@ class PocketSphinxSTT(AbstractSTTEngine): # Try to get hmm_dir from config profile_path = jasperpath.config('profile.yml') - name_default = 'default' - path_default = jasperpath.config('vocabularies') - - name_keyword = 'keyword' - path_keyword = jasperpath.config('vocabularies') - if os.path.exists(profile_path): with open(profile_path, 'r') as f: profile = yaml.safe_load(f) - if 'pocketsphinx' in profile: - if 'hmm_dir' in profile['pocketsphinx']: - config['hmm_dir'] = profile['pocketsphinx']['hmm_dir'] - - if 'vocabulary_default_name' in profile['pocketsphinx']: - name_default = \ - profile['pocketsphinx']['vocabulary_default_name'] - - if 'vocabulary_default_path' in profile['pocketsphinx']: - path_default = \ - profile['pocketsphinx']['vocabulary_default_path'] - - if 'vocabulary_keyword_name' in profile['pocketsphinx']: - name_keyword = \ - profile['pocketsphinx']['vocabulary_keyword_name'] - - if 'vocabulary_keyword_path' in profile['pocketsphinx']: - path_keyword = \ - profile['pocketsphinx']['vocabulary_keyword_path'] - - config['vocabulary'] = vocabcompiler.PocketsphinxVocabulary( - name_default, path=path_default) - config['vocabulary_keyword'] = vocabcompiler.PocketsphinxVocabulary( - name_keyword, path=path_keyword) - - config['vocabulary'].compile(vocabcompiler.get_all_phrases()) - config['vocabulary_keyword'].compile( - vocabcompiler.get_keyword_phrases()) + try: + config['hmm_dir'] = profile['pocketsphinx']['hmm_dir'] + except KeyError: + pass return config - def transcribe(self, fp, mode=TranscriptionMode.NORMAL): + def transcribe(self, fp): """ Performs STT, transcribing an audio file and returning the result. Arguments: - audio_file_path -- the path to the audio file to-be transcribed - PERSONA_ONLY -- if True, uses the 'Persona' language model and - dictionary - MUSIC -- if True, uses the 'Music' language model and dictionary + fp -- a file object containing audio data """ - decoder = self._decoders[mode] fp.seek(44) # FIXME: Can't use the Decoder.decode_raw() here, because # pocketsphinx segfaults with tempfile.SpooledTemporaryFile() data = fp.read() - decoder.start_utt() - decoder.process_raw(data, False, True) - decoder.end_utt() + self._decoder.start_utt() + self._decoder.process_raw(data, False, True) + self._decoder.end_utt() - result = decoder.get_hyp() - with open(self._logfiles[mode], 'r+') as f: - if mode == TranscriptionMode.KEYWORD: - modename = "[KEYWORD]" - elif mode == TranscriptionMode.MUSIC: - modename = "[MUSIC]" - else: - modename = "[NORMAL]" - for line in f: - self._logger.debug("%s %s", modename, line.strip()) - f.truncate() + result = self._decoder.get_hyp() + with open(self._logfile, 'r+') as f: + for line in f: + self._logger.debug(line.strip()) + f.truncate() print "===================" print "JASPER: " + result[0] @@ -248,7 +205,7 @@ class GoogleSTT(AbstractSTTEngine): config['api_key'] = profile['keys']['GOOGLE_SPEECH'] return config - def transcribe(self, fp, mode=TranscriptionMode.NORMAL): + def transcribe(self, fp): """ Performs STT via the Google Speech API, transcribing an audio file and returning an English string. @@ -293,33 +250,41 @@ class GoogleSTT(AbstractSTTEngine): return diagnose.check_network_connection() -def get_engines(): - return [stt_engine for stt_engine in AbstractSTTEngine.__subclasses__() - if hasattr(stt_engine, 'SLUG') and stt_engine.SLUG] - +def get_engine_by_slug(slug=None): + """ + Returns: + An STT Engine implementation available on the current platform -def newSTTEngine(stt_engine, **kwargs): + Raises: + ValueError if no speaker implementation is supported on this platform """ - Returns a Speech-To-Text engine. - Currently, the supported implementations are the default Pocket Sphinx and - the Google Speech API + if not slug or type(slug) is not str: + raise TypeError("Invalid slug '%s'", slug) - Arguments: - engine_type -- one of "sphinx" or "google" - kwargs -- keyword arguments passed to the constructor of the STT engine - """ selected_engines = filter(lambda engine: hasattr(engine, "SLUG") and - engine.SLUG == stt_engine, get_engines()) + engine.SLUG == slug, get_engines()) if len(selected_engines) == 0: - raise ValueError("No STT engine found for slug '%s'" % stt_engine) + raise ValueError("No TTS engine found for slug '%s'" % slug) else: if len(selected_engines) > 1: - print(("WARNING: Multiple STT engines found for slug '%s'. This " + - "is most certainly a bug.") % stt_engine) + print("WARNING: Multiple TTS engines found for slug '%s'. " + + "This is most certainly a bug." % slug) engine = selected_engines[0] if not engine.is_available(): - raise ValueError(("STT engine '%s' is not available (due to " + - "missing dependencies, missing dependencies, " + - "etc.)") % stt_engine) - return engine(**engine.get_config()) + raise ValueError("TTS engine '%s' is not available (due to " + + "missing dependencies, missing " + + "dependencies, etc.)" % slug) + return engine + + +def get_engines(): + def get_subclasses(cls): + subclasses = set() + for subclass in cls.__subclasses__(): + subclasses.add(subclass) + subclasses.update(get_subclasses(subclass)) + return subclasses + return [tts_engine for tts_engine in + list(get_subclasses(AbstractSTTEngine)) + if hasattr(tts_engine, 'SLUG') and tts_engine.SLUG] diff --git a/client/test.py b/client/test.py index 0a414f4..5a5dc6b 100644 --- a/client/test.py +++ b/client/test.py @@ -17,7 +17,6 @@ import brain import jasperpath import tts import diagnose -from stt import TranscriptionMode DEFAULT_PROFILE = { 'prefers_email': False, @@ -154,22 +153,22 @@ class TestPatchedPocketsphinxVocabulary(TestPocketsphinxVocabulary): self).testVocabulary() -class TestMic(unittest.TestCase): +class TestSTT(unittest.TestCase): def setUp(self): self.jasper_clip = jasperpath.data('audio', 'jasper.wav') self.time_clip = jasperpath.data('audio', 'time.wav') from stt import PocketSphinxSTT - self.stt = PocketSphinxSTT(**PocketSphinxSTT.get_config()) + self.passive_stt_engine = PocketSphinxSTT.get_passive_instance() + self.active_stt_engine = PocketSphinxSTT.get_active_instance() def testTranscribeJasper(self): """ Does Jasper recognize his name (i.e., passive listen)? """ with open(self.jasper_clip, mode="rb") as f: - transcription = self.stt.transcribe(f, - mode=TranscriptionMode.KEYWORD) + transcription = self.passive_stt_engine.transcribe(f) self.assertIn("JASPER", transcription) def testTranscribe(self): @@ -177,7 +176,7 @@ class TestMic(unittest.TestCase): Does Jasper recognize 'time' (i.e., active listen)? """ with open(self.time_clip, mode="rb") as f: - transcription = self.stt.transcribe(f) + transcription = self.active_stt_engine.transcribe(f) self.assertIn("TIME", transcription) @@ -411,7 +410,7 @@ if __name__ == '__main__': else: test_cases.append(TestG2P) test_cases.append(TestPocketsphinxVocabulary) - test_cases.append(TestMic) + test_cases.append(TestSTT) suite = unittest.TestSuite() @@ -80,16 +80,12 @@ class Jasper(object): raise try: - api_key = self.config['keys']['GOOGLE_SPEECH'] + stt_engine_slug = self.config['stt_engine'] except KeyError: - api_key = None - - try: - stt_engine_type = self.config['stt_engine'] - except KeyError: - stt_engine_type = "sphinx" - self._logger.warning("stt_engine not specified in profile, " + - "defaulting to '%s'", stt_engine_type) + stt_engine_slug = 'sphinx' + logger.warning("stt_engine not specified in profile, defaulting " + + "to '%s'", stt_engine_slug) + stt_engine_class = stt.get_engine_by_slug(stt_engine_slug) try: tts_engine_slug = self.config['tts_engine'] @@ -101,8 +97,8 @@ class Jasper(object): # Initialize Mic self.mic = Mic(tts_engine_class.get_instance(), - stt.PocketSphinxSTT(**stt.PocketSphinxSTT.get_config()), - stt.newSTTEngine(stt_engine_type, api_key=api_key)) + stt_engine_class.get_passive_instance(), + stt_engine_class.get_active_instance()) def run(self): if 'first_name' in self.config: |
