From c30292e5f2b38430f5c5f17a52ae20cc16131233 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 17 Sep 2014 18:54:51 +0200 Subject: Rewritten vocabcompiler and separated it from pocketsphinx logic The pocketsphinx part of vocabcompiler now uses the cmuclmtk wrapper libary for compilation of the languagemodel/dictionary. A revision check has been implemented, so that vocabulary won't get recompiled if there's no need. Proper integration into jasper.py, client/stt.py and client/test.py is still missing due to pending pull requests that change these modules. --- client/requirements.txt | 1 + client/vocabcompiler.py | 362 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 330 insertions(+), 33 deletions(-) (limited to 'client') diff --git a/client/requirements.txt b/client/requirements.txt index dd0e474..57b6bc6 100644 --- a/client/requirements.txt +++ b/client/requirements.txt @@ -10,3 +10,4 @@ python-mpd==0.3.0 pytz==2013b semantic==1.0.3 requests==2.1.0 +cmuclmtk==0.1.2 diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index 52b1f0a..0e498a2 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -1,56 +1,352 @@ # -*- coding: utf-8-*- """ Iterates over all the WORDS variables in the modules and creates a -dictionary for the client. +vocabulary for the respective stt_engine if needed. """ import os +import tempfile +import logging +import hashlib +from abc import ABCMeta, abstractmethod, abstractproperty + +import cmuclmtk import g2p -from brain import Brain +import brain + + +class AbstractVocabulary(object): + """ + Abstract base class for Vocabulary classes. + + Please note that subclasses have to implement the compile_vocabulary() + method and set a string as the PATH_PREFIX class attribute. + """ + __metaclass__ = ABCMeta + + @classmethod + def phrases_to_revision(self, phrases): + """ + Calculates a revision from phrases by using the SHA1 hash function. + + Arguments: + phrases -- a list of phrases + + Returns: + A revision string for given phrases. + """ + sorted_phrases = sorted(phrases) + joined_phrases = '\n'.join(sorted_phrases) + sha1 = hashlib.sha1() + sha1.update(joined_phrases) + return sha1.hexdigest() + + def __init__(self, name='default', path='.'): + """ + Initializes a new Vocabulary instance. + + Optional Arguments: + name -- (optional) the name of the vocabulary (Default: 'default') + path -- (optional) the path in which the vocabulary exists or will + be created (Default: '.') + """ + self.name = name + self.path = os.path.abspath(os.path.join(path, self.PATH_PREFIX, name)) + self._logger = logging.getLogger(__name__) + + @property + def revision_file(self): + """ + Returns: + The path of the the revision file as string + """ + return os.path.join(self.path, 'revision') + + @abstractproperty + def is_compiled(self): + """ + Checks if the vocabulary is compiled by checking if the revision file + is readable. This method should be overridden by subclasses to check + for class-specific additional files, too. + + Returns: + True if the dictionary is compiled, else False + """ + return os.access(self.revision_file, os.R_OK) + + @property + def compiled_revision(self): + """ + Reads the compiled revision from the revision file. + + Returns: + the revision of this vocabulary (i.e. the string + inside the revision file), or None if is_compiled + if False + """ + if not self.is_compiled: + return None + with open(self.revision_file, 'r') as f: + revision = f.read().strip() + self._logger.debug("compiled_revision is '%s'", revision) + return revision + + def matches_phrases(self, phrases): + """ + Convenience method to check if this vocabulary exactly contains the + phrases passed to this method. + + Arguments: + phrases -- a list of phrases + + Returns: + True if phrases exactly matches the phrases inside this + vocabulary. + + """ + return (self.compiled_revision == self.phrases_to_revision(phrases)) + + def compile(self, phrases, force=False): + """ + Compiles this vocabulary. If the force argument is True, compilation + will be forced regardless of necessity (which means that the + preliminary check if the current revision already equals the + revision after compilation will be skipped). + This method is not meant to be overridden by subclasses - use the + _compile_vocabulary()-method instead. + + Arguments: + phrases -- a list of phrases that this vocabulary will contain + force -- (optional) forces compilation (Default: False) + + Returns: + The revision of the compiled vocabulary + """ + revision = self.phrases_to_revision(phrases) + if not force and self.compiled_revision == revision: + self._logger.debug('Compilation not neccessary, compiled ' + + 'version matches phrases.') + return revision + + if not os.path.exists(self.path): + try: + os.makedirs(self.path) + except OSError: + self._logger.error("Couldn't create vocabulary dir '%s'", + self.path, exc_info=True) + raise + try: + with open(self.revision_file, 'w') as f: + f.write(revision) + except (OSError, IOError): + self._logger.error("Couldn't write revision file in '%s'", + self.revision_file, exc_info=True) + raise + else: + try: + self._logger.debug('Starting compilation...') + self._compile_vocabulary(phrases) + except Exception as e: + self._logger.error("Fatal compilation Error occured, " + + "cleaning up...", exc_info=True) + try: + os.remove(self.revision_file) + except OSError: + pass + raise e + return revision + + @abstractmethod + def _compile_vocabulary(self, phrases): + """ + Abstract method that should be overridden in subclasses with custom + compilation code. + + Arguments: + phrases -- a list of phrases that this vocabulary will contain + """ + pass + +class PocketsphinxVocabulary(AbstractVocabulary): -def text2lm(in_filename, out_filename): - """Wrapper around the language model compilation tools""" - def text2idngram(in_filename, out_filename): - cmd = "text2idngram -vocab %s < %s -idngram temp.idngram" % ( - out_filename, in_filename) - os.system(cmd) + PATH_PREFIX = 'pocketsphinx-vocabulary' - def idngram2lm(in_filename, out_filename): - cmd = "idngram2lm -idngram temp.idngram -vocab %s -arpa %s" % ( - in_filename, out_filename) - os.system(cmd) + @property + def languagemodel_file(self): + """ + Returns: + The path of the the pocketsphinx languagemodel file as string + """ + return os.path.join(self.path, 'languagemodel') - text2idngram(in_filename, in_filename) - idngram2lm(in_filename, out_filename) + @property + def dictionary_file(self): + """ + Returns: + The path of the pocketsphinx dictionary file as string + """ + return os.path.join(self.path, 'dictionary') + @property + def is_compiled(self): + """ + Checks if the vocabulary is compiled by checking if the revision, + languagemodel and dictionary files are readable. -def compile(sentences, dictionary, languagemodel): + Returns: + True if this vocabulary has been compiled, else False + """ + return (super(self.__class__, self).is_compiled and + os.access(self.languagemodel_file, os.R_OK) and + os.access(self.dictionary_file, os.R_OK)) + + @property + def decoder_kwargs(self): + """ + Convenience property to use this Vocabulary with the __init__() method + of the pocketsphinx.Decoder class. + + Returns: + A dict containing kwargs for the pocketsphinx.Decoder.__init__() + method. + + Example: + decoder = pocketsphinx.Decoder(**vocab_instance.decoder_kwargs, + hmm='/path/to/hmm') + + """ + return {'lm': self.languagemodel_file, 'dict': self.dictionary_file} + + def _compile_vocabulary(self, phrases): + """ + Compiles the vocabulary to the Pocketsphinx format by creating a + languagemodel and a dictionary. + + Arguments: + phrases -- a list of phrases that this vocabulary will contain + """ + text = " ".join([(" %s " % phrase) for phrase in phrases]) + vocabulary = self._compile_languagemodel(text, self.languagemodel_file) + self._compile_dictionary(vocabulary, self.dictionary_file) + + def _compile_languagemodel(self, text, output_file): + """ + Compiles the languagemodel from a text. + + Arguments: + text -- the text the languagemodel will be generated from + output_file -- the path of the file this languagemodel will + be written to + + Returns: + A list of all unique words this vocabulary contains. + """ + with tempfile.NamedTemporaryFile(suffix='.vocab', delete=False) as f: + vocab_file = f.name + + # Create vocab file from text + cmuclmtk.text2vocab(text, vocab_file) + + # Create language model from text + cmuclmtk.text2lm(text, output_file, vocab_file=vocab_file) + + # Get words from vocab file + words = [] + with open(vocab_file, 'r') as f: + for line in f: + line = line.strip() + if not line.startswith('#') and line not in ('', ''): + words.append(line) + + os.remove(vocab_file) + + return words + + def _compile_dictionary(self, words, output_file): + """ + Compiles the dictionary from a list of words. + + Arguments: + words -- a list of all unique words this vocabulary contains + output_file -- the path of the file this dictionary will + be written to + """ + # create the dictionary + pronounced = g2p.translateWords(words) + zipped = zip(words, pronounced) + lines = ["%s %s" % (x, y) for x, y in zipped] + + with open(output_file, "w") as f: + for line in lines: + f.write("%s\n" % line) + + +def get_phrases_from_module(module): """ - Gets the words and creates the dictionary + Gets phrases from a module. + + Arguments: + module -- a module reference + + Returns: + The list of phrases in this module. + """ + return module.WORDS if hasattr(module, 'WORDS') else [] + + +def get_all_phrases(): """ + Gets phrases from all modules. - modules = Brain.get_modules() + Returns: + A list of phrases in all modules plus additional phrases passed to this + function. + """ + phrases = [] - words = [] + modules = brain.Brain.get_modules() for module in modules: - words.extend(module.WORDS) + phrases.extend(get_phrases_from_module(module)) + + return sorted(list(set(phrases))) - words = list(set(words)) +if __name__ == '__main__': + import shutil + import argparse - # create the dictionary - pronounced = g2p.translateWords(words) - zipped = zip(words, pronounced) - lines = ["%s %s" % (x, y) for x, y in zipped] + parser = argparse.ArgumentParser(description='Vocabcompiler Demo') + parser.add_argument('--base-dir', action='store', + help='the directory in which the vocabulary will be ' + + 'compiled.') + parser.add_argument('--debug', action='store_true', + help='show debug messages') + args = parser.parse_args() - with open(dictionary, "w") as f: - f.write("\n".join(lines) + "\n") + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + base_dir = args.base_dir if args.base_dir else tempfile.mkdtemp() - # create the language model - with open(sentences, "w") as f: - f.write("\n".join(words) + "\n") - f.write(" \n \n") - f.close() + phrases = get_all_phrases() + print "Module phrases: %r" % phrases - # make language model - text2lm(sentences, languagemodel) + for subclass in AbstractVocabulary.__subclasses__(): + if hasattr(subclass, 'PATH_PREFIX'): + vocab = subclass(path=base_dir) + print("Vocabulary in: %s" % vocab.path) + print("Revision file: %s" % vocab.revision_file) + print("Compiled revision: %s" % vocab.compiled_revision) + print("Is compiled: %r" % vocab.is_compiled) + print("Matches phrases: %r" % vocab.matches_phrases(phrases)) + if not vocab.is_compiled or not vocab.matches_phrases(phrases): + print("Compiling...") + vocab.compile(phrases) + print("") + print("Vocabulary in: %s" % vocab.path) + print("Revision file: %s" % vocab.revision_file) + print("Compiled revision: %s" % vocab.compiled_revision) + print("Is compiled: %r" % vocab.is_compiled) + print("Matches phrases: %r" % vocab.matches_phrases(phrases)) + print("") + if not args.base_dir: + print("Removing temporary directory '%s'..." % base_dir) + shutil.rmtree(base_dir) -- cgit v1.3.1 From 9633fdeb0118f5e05c7e9eebd154a49f5ef85209 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 24 Sep 2014 19:10:43 +0200 Subject: Added DummyVocabulary class --- client/vocabcompiler.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'client') diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index 0e498a2..a7d2c4b 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -168,6 +168,28 @@ class AbstractVocabulary(object): pass +class DummyVocabulary(AbstractVocabulary): + + PATH_PREFIX = 'dummy-vocabulary' + + @property + def is_compiled(self): + """ + Checks if the vocabulary is compiled by checking if the revision + file is readable. + + Returns: + True if this vocabulary has been compiled, else False + """ + return super(self.__class__, self).is_compiled + + def _compile_vocabulary(self, phrases): + """ + Does nothing (because this is a dummy class for testing purposes). + """ + pass + + class PocketsphinxVocabulary(AbstractVocabulary): PATH_PREFIX = 'pocketsphinx-vocabulary' -- cgit v1.3.1 From b13fbec234292c46af2af37be0387d98a15ba2bf Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 24 Sep 2014 19:39:16 +0200 Subject: Changed vocabcompiler test cases --- client/test.py | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 6858700..9f9be8a 100644 --- a/client/test.py +++ b/client/test.py @@ -4,6 +4,8 @@ import os import sys import unittest import logging +import tempfile +import shutil import argparse from mock import patch, Mock @@ -32,31 +34,31 @@ class UnorderedList(list): class TestVocabCompiler(unittest.TestCase): - def testWordExtraction(self): - sentences = "temp_sentences.txt" - dictionary = "temp_dictionary.dic" - languagemodel = "temp_languagemodel.lm" - - words = [] + def testPhraseExtraction(self): + expected_phrases = ['MOCK'] mock_module = Mock() - mock_module.WORDS = [ - 'MOCK' - ] - - words.extend(mock_module.WORDS) - - with patch.object(g2p, 'translateWords') as translateWords: - with patch.object(vocabcompiler, 'text2lm') as text2lm: - with patch.object(brain.Brain, 'get_modules', - classmethod(lambda cls: [mock_module])): - vocabcompiler.compile(sentences, dictionary, languagemodel) - - translateWords.assert_called_once_with( - UnorderedList(words)) - self.assertTrue(text2lm.called) - os.remove(sentences) - os.remove(dictionary) + mock_module.WORDS = ['MOCK'] + + with patch.object(brain.Brain, 'get_modules', + classmethod(lambda cls: [mock_module])): + extracted_phrases = vocabcompiler.get_all_phrases() + self.assertEqual(expected_phrases, extracted_phrases) + + def testVocabulary(self): + phrases = ['THIS IS A TEST'] + + tempdir = tempfile.mkdtemp() + + vocab = vocabcompiler.DummyVocabulary(path=tempdir) + self.assertIsNone(vocab.compiled_revision) + self.assertFalse(vocab.is_compiled) + vocab.compile(phrases) + self.assertIsNotNone(vocab.compiled_revision) + self.assertTrue(vocab.is_compiled) + self.assertTrue(vocab.matches_phrases(phrases)) + + shutil.rmtree(tempdir) class TestMic(unittest.TestCase): -- cgit v1.3.1 From 0b681b489996ea8b9a9a5d3fc670af841994085d Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 25 Sep 2014 17:44:29 +0200 Subject: Add error handler for cmuclmtk import to vocabcompiler --- client/vocabcompiler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'client') diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index a7d2c4b..9481ef6 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -10,9 +10,12 @@ import logging import hashlib from abc import ABCMeta, abstractmethod, abstractproperty -import cmuclmtk import g2p import brain +try: + import cmuclmtk +except: + logging.getLogger(__name__).error("Error importing CMUCLMTK module. PocketsphinxVocabulary will not work correctly.", exc_info=True) class AbstractVocabulary(object): -- cgit v1.3.1 From 6e48647677f47ea7990d70757ec23eb2fea9e874 Mon Sep 17 00:00:00 2001 From: schneefux Date: Thu, 25 Sep 2014 22:24:20 +0200 Subject: Improve logging in vocabcompiler.py --- client/vocabcompiler.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'client') diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index 9481ef6..ae5dc7d 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -15,7 +15,9 @@ import brain try: import cmuclmtk except: - logging.getLogger(__name__).error("Error importing CMUCLMTK module. PocketsphinxVocabulary will not work correctly.", exc_info=True) + logging.getLogger(__name__).error("Error importing CMUCLMTK module. " + + "PocketsphinxVocabulary will not work " + + "correctly.", exc_info=True) class AbstractVocabulary(object): @@ -132,6 +134,8 @@ class AbstractVocabulary(object): return revision if not os.path.exists(self.path): + self._logger.debug("Vocabulary dir '%s' does not exist, " + + "creating...", self.path) try: os.makedirs(self.path) except OSError: @@ -146,8 +150,8 @@ class AbstractVocabulary(object): self.revision_file, exc_info=True) raise else: + self._logger.info('Starting compilation...') try: - self._logger.debug('Starting compilation...') self._compile_vocabulary(phrases) except Exception as e: self._logger.error("Fatal compilation Error occured, " + @@ -157,6 +161,8 @@ class AbstractVocabulary(object): except OSError: pass raise e + else: + self._logger.info('Compilation done.') return revision @abstractmethod @@ -252,7 +258,9 @@ class PocketsphinxVocabulary(AbstractVocabulary): phrases -- a list of phrases that this vocabulary will contain """ text = " ".join([(" %s " % phrase) for phrase in phrases]) + self._logger.debug('Compiling languagemodel...') vocabulary = self._compile_languagemodel(text, self.languagemodel_file) + self._logger.debug('Starting dictionary...') self._compile_dictionary(vocabulary, self.dictionary_file) def _compile_languagemodel(self, text, output_file): @@ -271,19 +279,22 @@ class PocketsphinxVocabulary(AbstractVocabulary): vocab_file = f.name # Create vocab file from text + self._logger.debug("Creating vocab file: '%s'", vocab_file) cmuclmtk.text2vocab(text, vocab_file) # Create language model from text + self._logger.debug("Creating languagemodel file: '%s'", output_file) cmuclmtk.text2lm(text, output_file, vocab_file=vocab_file) # Get words from vocab file + self._logger.debug("Getting words from vocab file and removing it " + + "afterwards...") words = [] with open(vocab_file, 'r') as f: for line in f: line = line.strip() if not line.startswith('#') and line not in ('', ''): words.append(line) - os.remove(vocab_file) return words @@ -298,10 +309,12 @@ class PocketsphinxVocabulary(AbstractVocabulary): be written to """ # create the dictionary + self._logger.debug("Getting phonemes for %d words...", len(words)) pronounced = g2p.translateWords(words) zipped = zip(words, pronounced) lines = ["%s %s" % (x, y) for x, y in zipped] + self._logger.debug("Creating dict file: '%s'", output_file) with open(output_file, "w") as f: for line in lines: f.write("%s\n" % line) -- cgit v1.3.1 From ae67577e9c66231b13d72822b407c5c699d0addc Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 26 Sep 2014 12:29:33 +0200 Subject: Fix assert statements in vocabcompiler unittests --- client/test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 9f9be8a..f91a06e 100644 --- a/client/test.py +++ b/client/test.py @@ -53,8 +53,9 @@ class TestVocabCompiler(unittest.TestCase): vocab = vocabcompiler.DummyVocabulary(path=tempdir) self.assertIsNone(vocab.compiled_revision) self.assertFalse(vocab.is_compiled) + self.assertFalse(vocab.matches_phrases(phrases)) vocab.compile(phrases) - self.assertIsNotNone(vocab.compiled_revision) + self.assertIsInstance(vocab.compiled_revision, str) self.assertTrue(vocab.is_compiled) self.assertTrue(vocab.matches_phrases(phrases)) -- cgit v1.3.1 From d282d1f7ea17feeb19cd311037d837e8f1c2a3d5 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 26 Sep 2014 12:30:39 +0200 Subject: Remove UnorderedList from unittests --- client/test.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index f91a06e..c08866f 100644 --- a/client/test.py +++ b/client/test.py @@ -26,12 +26,6 @@ DEFAULT_PROFILE = { } -class UnorderedList(list): - - def __eq__(self, other): - return sorted(self) == sorted(other) - - class TestVocabCompiler(unittest.TestCase): def testPhraseExtraction(self): -- cgit v1.3.1 From 382c21d2ec5ca18d6932d38db8c23432aeac33dd Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 26 Sep 2014 16:03:59 +0200 Subject: Use newer cmuclmtk libary version and catch ImportError --- client/requirements.txt | 2 +- client/vocabcompiler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'client') diff --git a/client/requirements.txt b/client/requirements.txt index 57b6bc6..f3508fe 100644 --- a/client/requirements.txt +++ b/client/requirements.txt @@ -10,4 +10,4 @@ python-mpd==0.3.0 pytz==2013b semantic==1.0.3 requests==2.1.0 -cmuclmtk==0.1.2 +cmuclmtk==0.1.5 diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index ae5dc7d..c59bd6e 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -14,7 +14,7 @@ import g2p import brain try: import cmuclmtk -except: +except ImportError: logging.getLogger(__name__).error("Error importing CMUCLMTK module. " + "PocketsphinxVocabulary will not work " + "correctly.", exc_info=True) -- cgit v1.3.1 From 0aacc7df0f106cbca991c2d7d0d86d5ec3abafad Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 27 Sep 2014 14:41:02 +0200 Subject: Add method to get keyword phrases to vocabcompiler This way they can be used by other STT engines as well --- client/vocabcompiler.py | 20 ++++++++++++++++++++ static/keyword_phrases | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 static/keyword_phrases (limited to 'client') diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index c59bd6e..8edee6f 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -12,6 +12,8 @@ from abc import ABCMeta, abstractmethod, abstractproperty import g2p import brain +import jasperpath + try: import cmuclmtk except ImportError: @@ -333,6 +335,24 @@ def get_phrases_from_module(module): return module.WORDS if hasattr(module, 'WORDS') else [] +def get_keyword_phrases(): + """ + Gets the keyword phrases from the keywords file in the jasper data dir. + + Returns: + A list of keyword phrases. + """ + phrases = [] + + with open(jasperpath.data('keyword_phrases'), mode="r") as f: + for line in f: + phrase = line.strip() + if phrase: + phrases.append(phrase) + + return phrases + + def get_all_phrases(): """ Gets phrases from all modules. diff --git a/static/keyword_phrases b/static/keyword_phrases new file mode 100644 index 0000000..d118b13 --- /dev/null +++ b/static/keyword_phrases @@ -0,0 +1,18 @@ +BE +BEING +BUT +DID +FIRST +IN +IS +IT +JASPER +NOW +OF +ON +RIGHT +SAY +WHAT +WHICH +WITH +WORK \ No newline at end of file -- cgit v1.3.1 From 46cc612c3b98beda5144c9278cad76fea06fbce8 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 27 Sep 2014 15:59:32 +0200 Subject: Integrate new vocabcompiler into jasper.py, client/stt.py and client/musicmode.py --- client/modules/MPDControl.py | 44 ++++++++-------------- client/stt.py | 88 ++++++++++++++++++++++++++------------------ jasper.py | 11 ++---- 3 files changed, 72 insertions(+), 71 deletions(-) (limited to 'client') diff --git a/client/modules/MPDControl.py b/client/modules/MPDControl.py index eefdcbd..f23fc7d 100644 --- a/client/modules/MPDControl.py +++ b/client/modules/MPDControl.py @@ -1,11 +1,11 @@ # -*- coding: utf-8-*- -import os import re import logging import difflib import mpd -import g2p import stt +import vocabcompiler +import jasperpath from mic import Mic # Standard module stuff @@ -73,34 +73,22 @@ class MusicMode(object): self.music = mpdwrapper # index spotify playlists into new dictionary and language models - original = ["STOP", "CLOSE", "PLAY", "PAUSE", "NEXT", "PREVIOUS", - "LOUDER", "SOFTER", "LOWER", "HIGHER", "VOLUME", - "PLAYLIST"] + self.music.get_soup_playlist() - pronounced = g2p.translateWords(original) - zipped = zip(original, pronounced) - lines = ["%s %s" % (x, y) for x, y in zipped] - - with open("dictionary_spotify.dic", "w") as f: - f.write("\n".join(lines) + "\n") - - with open("sentences_spotify.txt", "w") as f: - f.write("\n".join(original) + "\n") - f.write(" \n \n") - f.close() - - # make language model - os.system("text2idngram -vocab sentences_spotify.txt < " + - "sentences_spotify.txt -idngram spotify.idngram") - os.system("idngram2lm -idngram spotify.idngram -vocab " + - "sentences_spotify.txt -arpa languagemodel_spotify.lm") + phrases = ["STOP", "CLOSE", "PLAY", "PAUSE", "NEXT", "PREVIOUS", + "LOUDER", "SOFTER", "LOWER", "HIGHER", "VOLUME", + "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 - self.mic = Mic( - mic.speaker, - mic.passive_stt_engine, - stt.PocketSphinxSTT(lmd_music="languagemodel_spotify.lm", - dictd_music="dictionary_spotify.dic") - ) + config = stt.PocketSphinxSTT.get_config() + + self.mic = Mic(mic.speaker, + mic.passive_stt_engine, + stt.PocketSphinxSTT(vocabulary_music=vocabulary_music, + **config)) def delegateInput(self, input): diff --git a/client/stt.py b/client/stt.py index cb06117..9ac1a18 100644 --- a/client/stt.py +++ b/client/stt.py @@ -11,6 +11,7 @@ import requests import yaml import jasperpath import diagnose +import vocabcompiler class TranscriptionMode: @@ -45,23 +46,19 @@ class PocketSphinxSTT(AbstractSTTEngine): SLUG = 'sphinx' - def __init__(self, lmd=jasperpath.config("languagemodel.lm"), - dictd=jasperpath.config("dictionary.dic"), - lmd_persona=jasperpath.data("languagemodel_persona.lm"), - dictd_persona=jasperpath.data("dictionary_persona.dic"), - lmd_music=None, dictd_music=None, - hmm_dir="/usr/local/share/pocketsphinx/model/hmm/en_US/" + - "hub4wsj_sc_8k"): + def __init__(self, vocabulary=None, vocabulary_keyword=None, + vocabulary_music=None, hmm_dir="/usr/local/share/" + + "pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k"): + """ Initiates the pocketsphinx instance. Arguments: - speaker -- handles platform-independent audio output - lmd -- filename of the full language model - dictd -- filename of the full dictionary (.dic) - lmd_persona -- filename of the 'Persona' language model (containing, - e.g., 'Jasper') - dictd_persona -- filename of the 'Persona' dictionary (.dic) + 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) """ self._logger = logging.getLogger(__name__) @@ -84,16 +81,19 @@ class PocketSphinxSTT(AbstractSTTEngine): self._logfiles[TranscriptionMode.NORMAL] = f.name self._decoders = {} - if lmd_music and dictd_music: + if vocabulary_music is not None: self._decoders[TranscriptionMode.MUSIC] = \ - ps.Decoder(hmm=hmm_dir, lm=lmd_music, dict=dictd_music, - logfn=self._logfiles[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, lm=lmd_persona, dict=dictd_persona, - logfn=self._logfiles[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, lm=lmd, dict=dictd, - logfn=self._logfiles[TranscriptionMode.NORMAL]) + ps.Decoder(hmm=hmm_dir, + logfn=self._logfiles[TranscriptionMode.NORMAL], + **vocabulary.decoder_kwargs) def __del__(self): for filename in self._logfiles.values(): @@ -106,27 +106,45 @@ class PocketSphinxSTT(AbstractSTTEngine): # HMM dir # Try to get hmm_dir from config profile_path = os.path.join(os.path.dirname(__file__), '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 'lmd' in profile['pocketsphinx']: - config['lmd'] = profile['pocketsphinx']['lmd'] - if 'dictd' in profile['pocketsphinx']: - config['dictd'] = profile['pocketsphinx']['dictd'] - if 'lmd_persona' in profile['pocketsphinx']: - config['lmd_persona'] = \ - profile['pocketsphinx']['lmd_persona'] - if 'dictd_persona' in profile['pocketsphinx']: - config['dictd_persona'] = \ - profile['pocketsphinx']['dictd_persona'] - if 'lmd_music' in profile['pocketsphinx']: - config['lmd'] = profile['pocketsphinx']['lmd_music'] - if 'dictd_music' in profile['pocketsphinx']: - config['dictd_music'] = \ - profile['pocketsphinx']['dictd_music'] + + 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()) + return config def transcribe(self, fp, mode=TranscriptionMode.NORMAL): diff --git a/jasper.py b/jasper.py index d116ce9..24396cf 100755 --- a/jasper.py +++ b/jasper.py @@ -8,7 +8,7 @@ import logging import yaml import argparse -from client import vocabcompiler, tts, stt, jasperpath, diagnose +from client import tts, stt, jasperpath, diagnose # Add jasperpath.LIB_PATH to sys.path sys.path.append(jasperpath.LIB_PATH) @@ -99,14 +99,9 @@ class Jasper(object): "to '%s'", tts_engine_slug) tts_engine_class = tts.get_engine_by_slug(tts_engine_slug) - # Compile dictionary - sentences = jasperpath.config("sentences.txt") - dictionary = jasperpath.config("dictionary.dic") - languagemodel = jasperpath.config("languagemodel.lm") - vocabcompiler.compile(sentences, dictionary, languagemodel) - # Initialize Mic - self.mic = Mic(tts_engine_class(), stt.PocketSphinxSTT(), + self.mic = Mic(tts_engine_class(), + stt.PocketSphinxSTT(**stt.PocketSphinxSTT.get_config()), stt.newSTTEngine(stt_engine_type, api_key=api_key)) def run(self): -- cgit v1.3.1 From faa0df57224fd5c7374c831af67e01bedfd30e1b Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 28 Sep 2014 19:18:18 +0200 Subject: Rewritten G2P code --- client/g2p.py | 225 +++++++++++++++++++++++++++++++++++------------- client/vocabcompiler.py | 16 ++-- 2 files changed, 173 insertions(+), 68 deletions(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index 89f2282..8ce7997 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -1,69 +1,170 @@ # -*- coding: utf-8-*- import os -import tempfile -import subprocess +import sys import re -import yaml +import subprocess +import tempfile +import shutil +import logging +if sys.version_info < (3, 3): + import distutils.spawn import jasperpath +import yaml -PHONE_MATCH = re.compile(r' (.*) ') - -FST_MODEL = None - -# Try to get fst_model from config -profile_path = os.path.join(os.path.dirname(__file__), 'profile.yml') -if os.path.exists(profile_path): - with open(profile_path, 'r') as f: - profile = yaml.safe_load(f) - if ('pocketsphinx' in profile and - 'fst_model' in profile['pocketsphinx']): - FST_MODEL = profile['pocketsphinx']['fst_model'] - -if not FST_MODEL: - FST_MODEL = os.path.join(jasperpath.APP_PATH, os.pardir, 'phonetisaurus', - 'g014b2b.fst') - - -def parseLine(line): - return PHONE_MATCH.search(line).group(1) - - -def parseOutput(output): - return PHONE_MATCH.findall(output) - - -def translateWord(word): - out = subprocess.check_output( - ['phonetisaurus-g2p', '--model=%s' % FST_MODEL, '--input=%s' % word]) - return parseLine(out) - - -def translateWords(words): - full_text = '\n'.join(words) - - with tempfile.NamedTemporaryFile(suffix='.g2p', delete=False) as f: - temp_filename = f.name - f.write(full_text) - - output = translateFile(temp_filename) - os.remove(temp_filename) - - return output - - -def translateFile(input_filename, output_filename=None): - out = subprocess.check_output( - ['phonetisaurus-g2p', '--model=%s' % FST_MODEL, - '--input=%s' % input_filename, '--words', '--isfile']) - out = parseOutput(out) - - if output_filename: - out = '\n'.join(out) - - with open(output_filename, "wb") as f: - f.write(out) - - return None - return out +class PhonetisaurusG2P(object): + PATTERN = re.compile(r'^(?P.+)\t(?P\d+\.\d+)\t ' + + r'(?P.*) ', re.MULTILINE) + + @classmethod + def executable_found(cls): + if sys.version_info < (3, 3): + cmd_exists = distutils.spawn.find_executable + else: + cmd_exists = shutil.which + # Required binary for this class + cmd = 'phonetisaurus-g2p' + if not cmd_exists(cmd): + return False + return True + + @classmethod + def execute(cls, fst_model, input, is_file=False, nbest=None): + logger = logging.getLogger(__name__) + + cmd = ['phonetisaurus-g2p', + '--model=%s' % fst_model, + '--input=%s' % input, + '--words'] + + if is_file: + cmd.append('--isfile') + + if nbest is not None: + cmd.extend(['--nbest=%d' % nbest]) + + cmd = [str(x) for x in cmd] + with tempfile.SpooledTemporaryFile() as err_f: + try: + # FIXME: We can't just use subprocess.call and redirect stdout + # and stderr, because it looks like Phonetisaurus can't open + # an already opened file descriptor a second time. This is why + # we have to use this somehow hacky subprocess.Popen approach. + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdoutdata, stderrdata = proc.communicate() + returncode = proc.returncode + if returncode != 0: + logger.warning("Command '%s' return with exit status %d", + ' '.join(cmd), returncode) + except OSError: + logger.error("Error occured while executing command '%s'", + ' '.join(cmd), exc_info=True) + stdoutdata, stderrdata = None, None + if stderrdata is not None: + for line in stderrdata.splitlines(): + message = line.strip() + if message: + logger.debug(message) + + result = {} + if stdoutdata is not None: + for word, precision, pronounc in cls.PATTERN.findall(stdoutdata): + if word not in result: + result[word] = [] + result[word].append(pronounc) + return result + + @classmethod + def get_config(cls): + # FIXME: Replace this as soon as pull request + # jasperproject/jasper-client#128 has been merged + + conf = {'fst_model': os.path.join(jasperpath.APP_PATH, os.pardir, + 'phonetisaurus', 'g014b2b.fst')} + # Try to get fst_model from config + profile_path = os.path.join(os.path.dirname(__file__), 'profile.yml') + if os.path.exists(profile_path): + with open(profile_path, 'r') as f: + profile = yaml.safe_load(f) + if 'pocketsphinx' in profile: + if 'fst_model' in profile['pocketsphinx']: + conf['fst_model'] = \ + profile['pocketsphinx']['fst_model'] + if 'nbest' in profile['pocketsphinx']: + conf['nbest'] = int(profile['pocketsphinx']['nbest']) + print conf + return conf + + def __new__(cls, fst_model=None, *args, **kwargs): + if not cls.executable_found(): + raise OSError("Can't find command 'phonetisaurus-g2p'! Please " + + "check if Phonetisaurus is installed and in your " + + "$PATH.") + if fst_model is None or not os.access(fst_model, os.R_OK): + raise OSError("FST model '%r' does not exist! Can't create " + + "instance." % fst_model) + inst = object.__new__(cls, fst_model, *args, **kwargs) + return inst + + def __init__(self, fst_model=None, nbest=None): + self._logger = logging.getLogger(__name__) + + self.fst_model = os.path.abspath(fst_model) + self._logger.debug("Using FST model: '%s'", self.fst_model) + + self.nbest = nbest + if self.nbest is not None: + self._logger.debug("Will use the %d best results.", self.nbest) + + def _translate_word(self, word): + return self.execute(self.fst_model, word, nbest=self.nbest) + + def _translate_words(self, words): + with tempfile.NamedTemporaryFile(suffix='.g2p', delete=False) as f: + # The 'delete=False' kwarg is kind of a hack, but Phonetisaurus + # won't work if we remove it, because it seems that I can't open + # a file descriptor a second time. + for word in words: + f.write("%s\n" % word) + tmp_fname = f.name + output = self.execute(self.fst_model, tmp_fname, is_file=True, + nbest=self.nbest) + os.remove(tmp_fname) + return output + + def translate(self, words): + if type(words) is str or len(words) == 1: + self._logger.debug('Converting single word to phonemes') + output = self._translate_word(words if type(words) is str + else words[0]) + else: + self._logger.debug('Converting %d words to phonemes', len(words)) + output = self._translate_words(words) + self._logger.debug('G2P conversion returned phonemes for %d words', + len(output)) + return output + +if __name__ == "__main__": + import pprint + import argparse + parser = argparse.ArgumentParser(description='Phonetisaurus G2P module') + parser.add_argument('fst_model', action='store', + help='Path to the FST Model') + parser.add_argument('--debug', action='store_true', + help='Show debug messages') + args = parser.parse_args() + + logging.basicConfig() + logger = logging.getLogger() + if args.debug: + logger.setLevel(logging.DEBUG) + + words = ['THIS', 'IS', 'A', 'TEST'] + + g2pconv = PhonetisaurusG2P(args.fst_model, nbest=3) + output = g2pconv.translate(words) + + pp = pprint.PrettyPrinter(indent=2) + pp.pprint(output) diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index 8edee6f..1cfe15e 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -10,10 +10,10 @@ import logging import hashlib from abc import ABCMeta, abstractmethod, abstractproperty -import g2p import brain import jasperpath +from g2p import PhonetisaurusG2P try: import cmuclmtk except ImportError: @@ -312,14 +312,18 @@ class PocketsphinxVocabulary(AbstractVocabulary): """ # create the dictionary self._logger.debug("Getting phonemes for %d words...", len(words)) - pronounced = g2p.translateWords(words) - zipped = zip(words, pronounced) - lines = ["%s %s" % (x, y) for x, y in zipped] + g2pconverter = PhonetisaurusG2P(**PhonetisaurusG2P.get_config()) + phonemes = g2pconverter.translate(words) self._logger.debug("Creating dict file: '%s'", output_file) with open(output_file, "w") as f: - for line in lines: - f.write("%s\n" % line) + for word, pronounciations in phonemes.items(): + for i, pronounciation in enumerate(pronounciations, start=1): + if i == 1: + line = "%s\t%s\n" % (word, pronounciation) + else: + line = "%s(%d)\t%s\n" % (word, i, pronounciation) + f.write(line) def get_phrases_from_module(module): -- cgit v1.3.1 From 210327ebb4f58b84ff5f38541beda57a06ee5a69 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 1 Oct 2014 14:00:28 +0200 Subject: Minor style fix in phonetisaurus-g2p code --- client/g2p.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index 8ce7997..1e63d74 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -25,9 +25,7 @@ class PhonetisaurusG2P(object): cmd_exists = shutil.which # Required binary for this class cmd = 'phonetisaurus-g2p' - if not cmd_exists(cmd): - return False - return True + return cmd_exists(cmd) @classmethod def execute(cls, fst_model, input, is_file=False, nbest=None): -- cgit v1.3.1 From 30133ec0db993eee5bc9ada8b004f910d0a97c5a Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 1 Oct 2014 14:11:50 +0200 Subject: Fix G2P testcases, do not depend on fixed translation string --- client/test.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index c08866f..7f31f37 100644 --- a/client/test.py +++ b/client/test.py @@ -86,22 +86,17 @@ class TestMic(unittest.TestCase): class TestG2P(unittest.TestCase): def setUp(self): - self.translations = { - 'GOOD': 'G UH D', - 'BAD': 'B AE D', - 'UGLY': 'AH G L IY' - } + self.g2pconverter = g2p.PhonetisaurusG2P(**g2p.PhonetisaurusG2P.get_config()) + self.words = ['GOOD', 'BAD', 'UGLY'] def testTranslateWord(self): - for word in self.translations: - translation = self.translations[word] - self.assertEqual(g2p.translateWord(word), translation) + for word in self.words: + self.assertIn(word, self.g2pconverter.translate(word).keys()) def testTranslateWords(self): - words = self.translations.keys() - # preserve ordering - translations = [self.translations[w] for w in words] - self.assertEqual(g2p.translateWords(words), translations) + results = self.g2pconverter.translate(self.words).keys() + for word in self.words: + self.assertIn(word, results) class TestDiagnose(unittest.TestCase): -- cgit v1.3.1 From 550a133b7cc497c55c8c36ff75bf252860dc93aa Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 1 Oct 2014 14:12:30 +0200 Subject: Remove unneccessary print from g2p.py --- client/g2p.py | 1 - 1 file changed, 1 deletion(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index 1e63d74..741aa3f 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -92,7 +92,6 @@ class PhonetisaurusG2P(object): profile['pocketsphinx']['fst_model'] if 'nbest' in profile['pocketsphinx']: conf['nbest'] = int(profile['pocketsphinx']['nbest']) - print conf return conf def __new__(cls, fst_model=None, *args, **kwargs): -- cgit v1.3.1 From 897812b6659fdd7a68bceec9ccb008a16182ef9f Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 6 Oct 2014 18:10:03 +0200 Subject: Remove unused tempfile from client/g2p.py --- client/g2p.py | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index 741aa3f..b14ee98 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -43,28 +43,27 @@ class PhonetisaurusG2P(object): cmd.extend(['--nbest=%d' % nbest]) cmd = [str(x) for x in cmd] - with tempfile.SpooledTemporaryFile() as err_f: - try: - # FIXME: We can't just use subprocess.call and redirect stdout - # and stderr, because it looks like Phonetisaurus can't open - # an already opened file descriptor a second time. This is why - # we have to use this somehow hacky subprocess.Popen approach. - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdoutdata, stderrdata = proc.communicate() - returncode = proc.returncode - if returncode != 0: - logger.warning("Command '%s' return with exit status %d", - ' '.join(cmd), returncode) - except OSError: - logger.error("Error occured while executing command '%s'", - ' '.join(cmd), exc_info=True) - stdoutdata, stderrdata = None, None - if stderrdata is not None: - for line in stderrdata.splitlines(): - message = line.strip() - if message: - logger.debug(message) + try: + # FIXME: We can't just use subprocess.call and redirect stdout + # and stderr, because it looks like Phonetisaurus can't open + # an already opened file descriptor a second time. This is why + # we have to use this somehow hacky subprocess.Popen approach. + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdoutdata, stderrdata = proc.communicate() + returncode = proc.returncode + if returncode != 0: + logger.warning("Command '%s' return with exit status %d", + ' '.join(cmd), returncode) + except OSError: + logger.error("Error occured while executing command '%s'", + ' '.join(cmd), exc_info=True) + stdoutdata, stderrdata = None, None + if stderrdata is not None: + for line in stderrdata.splitlines(): + message = line.strip() + if message: + logger.debug(message) result = {} if stdoutdata is not None: -- cgit v1.3.1 From f9db756c18723abcacd54ad0667f69babca4c695 Mon Sep 17 00:00:00 2001 From: schneefux Date: Mon, 6 Oct 2014 18:11:32 +0200 Subject: PEP8 style fixes in test.py and vocabcompiler.py --- client/test.py | 3 ++- client/vocabcompiler.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 7f31f37..0034974 100644 --- a/client/test.py +++ b/client/test.py @@ -86,7 +86,8 @@ class TestMic(unittest.TestCase): class TestG2P(unittest.TestCase): def setUp(self): - self.g2pconverter = g2p.PhonetisaurusG2P(**g2p.PhonetisaurusG2P.get_config()) + self.g2pconverter = g2p.PhonetisaurusG2P( + **g2p.PhonetisaurusG2P.get_config()) self.words = ['GOOD', 'BAD', 'UGLY'] def testTranslateWord(self): diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index 1cfe15e..d0f0124 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -323,7 +323,7 @@ class PocketsphinxVocabulary(AbstractVocabulary): line = "%s\t%s\n" % (word, pronounciation) else: line = "%s(%d)\t%s\n" % (word, i, pronounciation) - f.write(line) + f.write(line) def get_phrases_from_module(module): -- cgit v1.3.1 From 63aa78e9029de4059c0306926bfd1943cf07492e Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 7 Oct 2014 14:24:19 +0200 Subject: Add unittests for G2P without phonetisaurus --- client/test.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 0034974..079ba92 100644 --- a/client/test.py +++ b/client/test.py @@ -100,6 +100,43 @@ class TestG2P(unittest.TestCase): self.assertIn(word, results) +class TestPatchedG2P(TestG2P): + class DummyProc(object): + def __init__(self, *args, **kwargs): + self.returncode = 0 + + def communicate(self): + return ("GOOD\t9.20477\t G UH D \n" + + "GOOD\t14.4036\t G UW D \n" + + "GOOD\t16.0258\t G UH D IY \n" + + "BAD\t0.7416\t B AE D \n" + + "BAD\t12.5495\t B AA D \n" + + "BAD\t13.6745\t B AH D \n" + + "UGLY\t12.572\t AH G L IY \n" + + "UGLY\t17.9278\t Y UW G L IY \n" + + "UGLY\t18.9617\t AH G L AY \n", "") + + def setUp(self): + with patch.object(g2p.PhonetisaurusG2P, 'executable_found', + classmethod(lambda cls: True)): + with tempfile.NamedTemporaryFile() as f: + conf = g2p.PhonetisaurusG2P.get_config().items() + with patch.object(g2p.PhonetisaurusG2P, 'get_config', + classmethod(lambda cls: dict( + conf + [('fst_model', f.name)]))): + super(self.__class__, self).setUp() + + def testTranslateWord(self): + with patch('subprocess.Popen', + return_value=TestPatchedG2P.DummyProc()): + super(self.__class__, self).testTranslateWord() + + def testTranslateWords(self): + with patch('subprocess.Popen', + return_value=TestPatchedG2P.DummyProc()): + super(self.__class__, self).testTranslateWords() + + class TestDiagnose(unittest.TestCase): def testPythonImportCheck(self): # This a python stdlib module that definitely exists @@ -270,7 +307,9 @@ if __name__ == '__main__': test_cases = [TestBrain, TestModules, TestVocabCompiler, TestTTS, TestDiagnose] - if not args.light: + if args.light: + test_cases.append(TestPatchedG2P) + else: test_cases.append(TestG2P) test_cases.append(TestMic) -- cgit v1.3.1 From b81e89ead31c8442a9f71f0bd7eabe19f5985842 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 11:57:20 +0200 Subject: Add testcases for (patched) PocketsphinxVocabulary --- client/test.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 13 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 079ba92..d4957b8 100644 --- a/client/test.py +++ b/client/test.py @@ -6,6 +6,7 @@ import unittest import logging import tempfile import shutil +import contextlib import argparse from mock import patch, Mock @@ -39,22 +40,60 @@ class TestVocabCompiler(unittest.TestCase): extracted_phrases = vocabcompiler.get_all_phrases() self.assertEqual(expected_phrases, extracted_phrases) - def testVocabulary(self): - phrases = ['THIS IS A TEST'] - tempdir = tempfile.mkdtemp() +class TestVocabulary(unittest.TestCase): - vocab = vocabcompiler.DummyVocabulary(path=tempdir) - self.assertIsNone(vocab.compiled_revision) - self.assertFalse(vocab.is_compiled) - self.assertFalse(vocab.matches_phrases(phrases)) - vocab.compile(phrases) - self.assertIsInstance(vocab.compiled_revision, str) - self.assertTrue(vocab.is_compiled) - self.assertTrue(vocab.matches_phrases(phrases)) + VOCABULARY = vocabcompiler.DummyVocabulary + @contextlib.contextmanager + def do_in_tempdir(self): + tempdir = tempfile.mkdtemp() + yield tempdir shutil.rmtree(tempdir) + def testVocabulary(self): + phrases = ['GOOD BAD UGLY'] + with self.do_in_tempdir() as tempdir: + vocab = self.VOCABULARY(path=tempdir) + self.assertIsNone(vocab.compiled_revision) + self.assertFalse(vocab.is_compiled) + self.assertFalse(vocab.matches_phrases(phrases)) + vocab.compile(phrases) + self.assertIsInstance(vocab.compiled_revision, str) + self.assertTrue(vocab.is_compiled) + self.assertTrue(vocab.matches_phrases(phrases)) + + +class TestPocketsphinxVocabulary(TestVocabulary): + + VOCABULARY = vocabcompiler.PocketsphinxVocabulary + + +class TestPatchedPocketsphinxVocabulary(TestPocketsphinxVocabulary): + + def testVocabulary(self): + + def write_test_vocab(text, output_file): + with open(output_file, "w") as f: + for word in text.split(' '): + f.write("%s\n" % word) + + def write_test_lm(text, output_file, **kwargs): + with open(output_file, "w") as f: + f.write("TEST") + + vocabcompiler.cmuclmtk = None + with patch('vocabcompiler.cmuclmtk') as mocked_cmuclmtk: + mocked_cmuclmtk.text2vocab = write_test_vocab + mocked_cmuclmtk.text2lm = write_test_lm + with patch.object(g2p.PhonetisaurusG2P, '__new__', + create=True) as mocked_g2p: + mocked_g2p.translate = (lambda *args, **kwargs: + {'GOOD': ['G UH D'], + 'BAD': ['B AE D'], + 'UGLY': ['AH G L IY']}) + super(self.__class__, self).testVocabulary() + class TestMic(unittest.TestCase): @@ -305,12 +344,14 @@ if __name__ == '__main__': # Change CWD to jasperpath.LIB_PATH os.chdir(jasperpath.LIB_PATH) - test_cases = [TestBrain, TestModules, TestVocabCompiler, TestTTS, - TestDiagnose] + test_cases = [TestBrain, TestModules, TestDiagnose, TestTTS, + TestVocabCompiler, TestVocabulary] if args.light: test_cases.append(TestPatchedG2P) + test_cases.append(TestPatchedPocketsphinxVocabulary) else: test_cases.append(TestG2P) + test_cases.append(TestPocketsphinxVocabulary) test_cases.append(TestMic) suite = unittest.TestSuite() -- cgit v1.3.1 From ffce99db9f6ade908a0b0f7c1fc847dad84b49bd Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 14:47:06 +0200 Subject: improve vocabulary unittest coverage --- client/test.py | 67 ++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 16 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index d4957b8..f661b89 100644 --- a/client/test.py +++ b/client/test.py @@ -42,7 +42,6 @@ class TestVocabCompiler(unittest.TestCase): class TestVocabulary(unittest.TestCase): - VOCABULARY = vocabcompiler.DummyVocabulary @contextlib.contextmanager @@ -54,20 +53,56 @@ class TestVocabulary(unittest.TestCase): def testVocabulary(self): phrases = ['GOOD BAD UGLY'] with self.do_in_tempdir() as tempdir: - vocab = self.VOCABULARY(path=tempdir) - self.assertIsNone(vocab.compiled_revision) - self.assertFalse(vocab.is_compiled) - self.assertFalse(vocab.matches_phrases(phrases)) - vocab.compile(phrases) - self.assertIsInstance(vocab.compiled_revision, str) - self.assertTrue(vocab.is_compiled) - self.assertTrue(vocab.matches_phrases(phrases)) + self.vocab = self.VOCABULARY(path=tempdir) + self.assertIsNone(self.vocab.compiled_revision) + self.assertFalse(self.vocab.is_compiled) + self.assertFalse(self.vocab.matches_phrases(phrases)) + + # We're now testing error handling. To avoid flooding the + # output with error messages that are catched anyway, + # we'll temporarly disable logging. Otherwise, error log + # messages and traceback would be printed so that someone + # might think that tests failed even though they succeeded. + logging.disable(logging.ERROR) + with self.assertRaises(OSError): + with patch('os.makedirs', side_effect=OSError('test')): + self.vocab.compile(phrases) + with self.assertRaises(OSError): + with patch('%s.open' % vocabcompiler.__name__, + create=True, + side_effect=OSError('test')): + self.vocab.compile(phrases) + + class StrangeCompilationError(Exception): + pass + with self.assertRaises(StrangeCompilationError): + with patch.object(self.vocab, '_compile_vocabulary', + side_effect=StrangeCompilationError('test')): + self.vocab.compile(phrases) + with patch('os.remove', + side_effect=OSError('test')): + self.vocab.compile(phrases) + # Re-enable logging again + logging.disable(logging.NOTSET) + + self.vocab.compile(phrases) + self.assertIsInstance(self.vocab.compiled_revision, str) + self.assertTrue(self.vocab.is_compiled) + self.assertTrue(self.vocab.matches_phrases(phrases)) + self.vocab.compile(phrases) + self.vocab.compile(phrases, force=True) class TestPocketsphinxVocabulary(TestVocabulary): VOCABULARY = vocabcompiler.PocketsphinxVocabulary + def testVocabulary(self): + super(TestPocketsphinxVocabulary, self).testVocabulary() + self.assertIsInstance(self.vocab.decoder_kwargs, dict) + self.assertIn('lm', self.vocab.decoder_kwargs) + self.assertIn('dict', self.vocab.decoder_kwargs) + class TestPatchedPocketsphinxVocabulary(TestPocketsphinxVocabulary): @@ -82,17 +117,17 @@ class TestPatchedPocketsphinxVocabulary(TestPocketsphinxVocabulary): with open(output_file, "w") as f: f.write("TEST") - vocabcompiler.cmuclmtk = None - with patch('vocabcompiler.cmuclmtk') as mocked_cmuclmtk: + with patch('vocabcompiler.cmuclmtk', + create=True) as mocked_cmuclmtk: mocked_cmuclmtk.text2vocab = write_test_vocab mocked_cmuclmtk.text2lm = write_test_lm with patch.object(g2p.PhonetisaurusG2P, '__new__', create=True) as mocked_g2p: - mocked_g2p.translate = (lambda *args, **kwargs: - {'GOOD': ['G UH D'], - 'BAD': ['B AE D'], - 'UGLY': ['AH G L IY']}) - super(self.__class__, self).testVocabulary() + mocked_g2p.translate.return_value = (lambda *args, **kwargs: + {'GOOD': ['G UH D'], + 'BAD': ['B AE D'], + 'UGLY': ['AH G L IY']}) + super(TestPatchedPocketsphinxVocabulary, self).testVocabulary() class TestMic(unittest.TestCase): -- cgit v1.3.1 From f8ead5d8fbaba38364702332a50a87471175b65e Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 16:41:20 +0200 Subject: Add test for keyword phrase extraction --- client/test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'client') diff --git a/client/test.py b/client/test.py index f661b89..0269d18 100644 --- a/client/test.py +++ b/client/test.py @@ -40,6 +40,19 @@ class TestVocabCompiler(unittest.TestCase): extracted_phrases = vocabcompiler.get_all_phrases() self.assertEqual(expected_phrases, extracted_phrases) + def testKeywordPhraseExtraction(self): + expected_phrases = ['MOCK'] + + with tempfile.TemporaryFile() as f: + # We can't use mock_open here, because it doesn't seem to work + # with the 'for line in f' syntax + f.write("MOCK\n") + f.seek(0) + with patch('%s.open' % vocabcompiler.__name__, + return_value=f, create=True): + extracted_phrases = vocabcompiler.get_keyword_phrases() + self.assertEqual(expected_phrases, extracted_phrases) + class TestVocabulary(unittest.TestCase): VOCABULARY = vocabcompiler.DummyVocabulary -- cgit v1.3.1 From 6d4028cb6b76f759ac38bd091cad8d6bd662bb93 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 16:42:20 +0200 Subject: Update TestMic testcase to work with Vocabulary --- client/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 0269d18..ac25b41 100644 --- a/client/test.py +++ b/client/test.py @@ -150,7 +150,7 @@ class TestMic(unittest.TestCase): self.time_clip = jasperpath.data('audio', 'time.wav') from stt import PocketSphinxSTT - self.stt = PocketSphinxSTT() + self.stt = PocketSphinxSTT(**PocketSphinxSTT.get_config()) def testTranscribeJasper(self): """ -- cgit v1.3.1 From baa4d68705def31614509bd4fdcb3438f175a985 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 16:50:56 +0200 Subject: Further improve unittest coverage --- client/test.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index ac25b41..7ab1761 100644 --- a/client/test.py +++ b/client/test.py @@ -88,10 +88,11 @@ class TestVocabulary(unittest.TestCase): class StrangeCompilationError(Exception): pass - with self.assertRaises(StrangeCompilationError): - with patch.object(self.vocab, '_compile_vocabulary', - side_effect=StrangeCompilationError('test')): + with patch.object(self.vocab, '_compile_vocabulary', + side_effect=StrangeCompilationError('test')): + with self.assertRaises(StrangeCompilationError): self.vocab.compile(phrases) + with self.assertRaises(StrangeCompilationError): with patch('os.remove', side_effect=OSError('test')): self.vocab.compile(phrases) @@ -130,17 +131,27 @@ class TestPatchedPocketsphinxVocabulary(TestPocketsphinxVocabulary): with open(output_file, "w") as f: f.write("TEST") + class DummyG2P(object): + def __init__(self, *args, **kwargs): + pass + + @classmethod + def get_config(self, *args, **kwargs): + return {} + + def translate(self, *args, **kwargs): + return {'GOOD': ['G UH D', + 'G UW D'], + 'BAD': ['B AE D'], + 'UGLY': ['AH G L IY']} + with patch('vocabcompiler.cmuclmtk', create=True) as mocked_cmuclmtk: mocked_cmuclmtk.text2vocab = write_test_vocab mocked_cmuclmtk.text2lm = write_test_lm - with patch.object(g2p.PhonetisaurusG2P, '__new__', - create=True) as mocked_g2p: - mocked_g2p.translate.return_value = (lambda *args, **kwargs: - {'GOOD': ['G UH D'], - 'BAD': ['B AE D'], - 'UGLY': ['AH G L IY']}) - super(TestPatchedPocketsphinxVocabulary, self).testVocabulary() + with patch('vocabcompiler.PhonetisaurusG2P', DummyG2P): + super(TestPatchedPocketsphinxVocabulary, + self).testVocabulary() class TestMic(unittest.TestCase): -- cgit v1.3.1 From f91ed45347ae10e4fed612ecb81037d421bb785c Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 19:55:31 +0200 Subject: Remove unneccessary pass statement from AbstractVocabulary class This should raise vocabcompiler test coverage to 100%. Whohooo! --- client/vocabcompiler.py | 1 - 1 file changed, 1 deletion(-) (limited to 'client') diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index d0f0124..7eef5c0 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -176,7 +176,6 @@ class AbstractVocabulary(object): Arguments: phrases -- a list of phrases that this vocabulary will contain """ - pass class DummyVocabulary(AbstractVocabulary): -- cgit v1.3.1 From a8809862e909e4c703228e4bd583bb25fabc3ae9 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 19:59:17 +0200 Subject: Use diagnose.check_executable for executable detection in g2p.py --- client/g2p.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index b14ee98..a2b3982 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -1,14 +1,11 @@ # -*- coding: utf-8-*- import os -import sys import re import subprocess import tempfile -import shutil import logging -if sys.version_info < (3, 3): - import distutils.spawn +import diagnose import jasperpath import yaml @@ -17,16 +14,6 @@ class PhonetisaurusG2P(object): PATTERN = re.compile(r'^(?P.+)\t(?P\d+\.\d+)\t ' + r'(?P.*) ', re.MULTILINE) - @classmethod - def executable_found(cls): - if sys.version_info < (3, 3): - cmd_exists = distutils.spawn.find_executable - else: - cmd_exists = shutil.which - # Required binary for this class - cmd = 'phonetisaurus-g2p' - return cmd_exists(cmd) - @classmethod def execute(cls, fst_model, input, is_file=False, nbest=None): logger = logging.getLogger(__name__) @@ -94,7 +81,7 @@ class PhonetisaurusG2P(object): return conf def __new__(cls, fst_model=None, *args, **kwargs): - if not cls.executable_found(): + if not diagnose.check_executable('phonetisaurus-g2p'): raise OSError("Can't find command 'phonetisaurus-g2p'! Please " + "check if Phonetisaurus is installed and in your " + "$PATH.") -- cgit v1.3.1 From d8e79ba4db37e4fcd1fdddb5e5554a762f97ed71 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 19:59:46 +0200 Subject: Reorder imports --- client/g2p.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index a2b3982..ec0b025 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -5,9 +5,10 @@ import subprocess import tempfile import logging +import yaml + import diagnose import jasperpath -import yaml class PhonetisaurusG2P(object): -- cgit v1.3.1 From 12a73debc9366aff44e32d119234cd8898205991 Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 20:01:41 +0200 Subject: Use configfile from jasper config dir in g2p.py (i.e. use ) --- client/g2p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index ec0b025..9e715c9 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -69,7 +69,7 @@ class PhonetisaurusG2P(object): conf = {'fst_model': os.path.join(jasperpath.APP_PATH, os.pardir, 'phonetisaurus', 'g014b2b.fst')} # Try to get fst_model from config - profile_path = os.path.join(os.path.dirname(__file__), 'profile.yml') + profile_path = jasperpath.config('profile.yml') if os.path.exists(profile_path): with open(profile_path, 'r') as f: profile = yaml.safe_load(f) -- cgit v1.3.1 From c763fb9760fb7bef5fd1a78af2fe32ff7a2b50da Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 20:07:05 +0200 Subject: remove redundant variable assignment --- client/g2p.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'client') diff --git a/client/g2p.py b/client/g2p.py index 9e715c9..7b03386 100644 --- a/client/g2p.py +++ b/client/g2p.py @@ -39,10 +39,9 @@ class PhonetisaurusG2P(object): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdoutdata, stderrdata = proc.communicate() - returncode = proc.returncode - if returncode != 0: + if proc.returncode != 0: logger.warning("Command '%s' return with exit status %d", - ' '.join(cmd), returncode) + ' '.join(cmd), proc.returncode) except OSError: logger.error("Error occured while executing command '%s'", ' '.join(cmd), exc_info=True) -- cgit v1.3.1 From ac0a6d731ad179d62bf2be6be28ee862a020bdab Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 8 Oct 2014 20:11:58 +0200 Subject: Fix G2P testcase --- client/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'client') diff --git a/client/test.py b/client/test.py index 7ab1761..0a414f4 100644 --- a/client/test.py +++ b/client/test.py @@ -215,8 +215,8 @@ class TestPatchedG2P(TestG2P): "UGLY\t18.9617\t AH G L AY \n", "") def setUp(self): - with patch.object(g2p.PhonetisaurusG2P, 'executable_found', - classmethod(lambda cls: True)): + with patch('g2p.diagnose.check_executable', + return_value=True): with tempfile.NamedTemporaryFile() as f: conf = g2p.PhonetisaurusG2P.get_config().items() with patch.object(g2p.PhonetisaurusG2P, 'get_config', -- cgit v1.3.1