diff options
| author | schneefux <schneefux+commit@schneefux.xyz> | 2015-01-19 12:15:23 +0100 |
|---|---|---|
| committer | schneefux <schneefux+commit@schneefux.xyz> | 2015-01-19 12:15:23 +0100 |
| commit | 9fa55058c99fd9ef461b1a7b422eb53b5839783e (patch) | |
| tree | bfa85322d3744fca01732a7adb68f00ee4f30a3f | |
| parent | b7113990da5e446f7b371a846c314181fc961aa6 (diff) | |
| parent | f8ad3c9a982f8c4407931b513e6069289d561972 (diff) | |
| download | jasper-client-9fa55058c99fd9ef461b1a7b422eb53b5839783e.tar.gz jasper-client-9fa55058c99fd9ef461b1a7b422eb53b5839783e.zip | |
Merge pull request #285 from Holzhaus/julius-stt-engine
Added basic Julius STT engine and vocabcompiler
| -rw-r--r-- | client/stt.py | 93 | ||||
| -rw-r--r-- | client/vocabcompiler.py | 152 |
2 files changed, 244 insertions, 1 deletions
diff --git a/client/stt.py b/client/stt.py index 95ed487..c192e6a 100644 --- a/client/stt.py +++ b/client/stt.py @@ -7,6 +7,8 @@ import tempfile import logging import urllib import urlparse +import re +import subprocess from abc import ABCMeta, abstractmethod import requests import yaml @@ -176,6 +178,97 @@ class PocketSphinxSTT(AbstractSTTEngine): return diagnose.check_python_import('pocketsphinx') +class JuliusSTT(AbstractSTTEngine): + """ + A very basic Speech-to-Text engine using Julius. + """ + + SLUG = 'julius' + VOCABULARY_TYPE = vocabcompiler.JuliusVocabulary + + def __init__(self, vocabulary=None, hmmdefs="/usr/share/voxforge/julius/" + + "acoustic_model_files/hmmdefs", tiedlist="/usr/share/" + + "voxforge/julius/acoustic_model_files/tiedlist"): + self._logger = logging.getLogger(__name__) + self._vocabulary = vocabulary + self._hmmdefs = hmmdefs + self._tiedlist = tiedlist + self._pattern = re.compile(r'sentence(\d+): <s> (.+) </s>') + + # Inital test run: we run this command once to log errors/warnings + cmd = ['julius', + '-input', 'stdin', + '-dfa', self._vocabulary.dfa_file, + '-v', self._vocabulary.dict_file, + '-h', self._hmmdefs, + '-hlist', self._tiedlist, + '-forcedict'] + cmd = [str(x) for x in cmd] + self._logger.debug('Executing: %r', cmd) + with tempfile.SpooledTemporaryFile() as out_f: + with tempfile.SpooledTemporaryFile() as f: + with tempfile.SpooledTemporaryFile() as err_f: + subprocess.call(cmd, stdin=f, stdout=out_f, stderr=err_f) + out_f.seek(0) + for line in out_f.read().splitlines(): + line = line.strip() + if len(line) > 7 and line[:7].upper() == 'ERROR: ': + if not line[7:].startswith('adin_'): + self._logger.error(line[7:]) + elif len(line) > 9 and line[:9].upper() == 'WARNING: ': + self._logger.warning(line[9:]) + elif len(line) > 6 and line[:6].upper() == 'STAT: ': + self._logger.debug(line[6:]) + + @classmethod + def get_config(cls): + # FIXME: Replace this as soon as we have a config module + config = {} + # HMM dir + # Try to get hmm_dir from config + profile_path = jasperpath.config('profile.yml') + + if os.path.exists(profile_path): + with open(profile_path, 'r') as f: + profile = yaml.safe_load(f) + if 'julius' in profile: + if 'hmmdefs' in profile['julius']: + config['hmmdefs'] = profile['julius']['hmmdefs'] + if 'tiedlist' in profile['julius']: + config['tiedlist'] = profile['julius']['tiedlist'] + return config + + def transcribe(self, fp, mode=None): + cmd = ['julius', + '-quiet', + '-nolog', + '-input', 'stdin', + '-dfa', self._vocabulary.dfa_file, + '-v', self._vocabulary.dict_file, + '-h', self._hmmdefs, + '-hlist', self._tiedlist, + '-forcedict'] + cmd = [str(x) for x in cmd] + self._logger.debug('Executing: %r', cmd) + with tempfile.SpooledTemporaryFile() as out_f: + with tempfile.SpooledTemporaryFile() as err_f: + subprocess.call(cmd, stdin=fp, stdout=out_f, stderr=err_f) + out_f.seek(0) + results = [(int(i), text) for i, text in + self._pattern.findall(out_f.read())] + transcribed = [text for i, text in + sorted(results, key=lambda x: x[0]) + if text] + if not transcribed: + transcribed.append('') + self._logger.info('Transcribed: %r', transcribed) + return transcribed + + @classmethod + def is_available(cls): + return diagnose.check_executable('julius') + + class GoogleSTT(AbstractSTTEngine): """ Speech-To-Text implementation which relies on the Google Speech API. diff --git a/client/vocabcompiler.py b/client/vocabcompiler.py index ed9f425..0f8a648 100644 --- a/client/vocabcompiler.py +++ b/client/vocabcompiler.py @@ -8,7 +8,13 @@ import os import tempfile import logging import hashlib +import subprocess +import tarfile +import re +import contextlib +import shutil from abc import ABCMeta, abstractmethod, abstractproperty +import yaml import brain import jasperpath @@ -325,6 +331,151 @@ class PocketsphinxVocabulary(AbstractVocabulary): f.write(line) +class JuliusVocabulary(AbstractVocabulary): + class VoxForgeLexicon(object): + def __init__(self, fname, membername=None): + self._dict = {} + self.parse(fname, membername) + + @contextlib.contextmanager + def open_dict(self, fname, membername=None): + if tarfile.is_tarfile(fname): + if not membername: + raise ValueError('archive membername not set!') + tf = tarfile.open(fname) + f = tf.extractfile(membername) + yield f + f.close() + tf.close() + else: + with open(fname) as f: + yield f + + def parse(self, fname, membername=None): + pattern = re.compile(r'\[(.+)\]\W(.+)') + with self.open_dict(fname, membername=membername) as f: + for line in f: + matchobj = pattern.search(line) + if matchobj: + word, phoneme = [x.strip() for x in matchobj.groups()] + if word in self._dict: + self._dict[word].append(phoneme) + else: + self._dict[word] = [phoneme] + + def translate_word(self, word): + if word in self._dict: + return self._dict[word] + else: + return [] + + PATH_PREFIX = 'julius-vocabulary' + + @property + def dfa_file(self): + """ + Returns: + The path of the the julius dfa file as string + """ + return os.path.join(self.path, 'dfa') + + @property + def dict_file(self): + """ + Returns: + The path of the the julius dict file as string + """ + return os.path.join(self.path, 'dict') + + @property + def is_compiled(self): + return (super(self.__class__, self).is_compiled and + os.access(self.dfa_file, os.R_OK) and + os.access(self.dict_file, os.R_OK)) + + def _get_grammar(self, phrases): + return {'S': [['NS_B', 'WORD_LOOP', 'NS_E']], + 'WORD_LOOP': [['WORD_LOOP', 'WORD'], ['WORD']]} + + def _get_word_defs(self, lexicon, phrases): + word_defs = {'NS_B': [('<s>', 'sil')], + 'NS_E': [('</s>', 'sil')], + 'WORD': []} + + words = [] + for phrase in phrases: + if ' ' in phrase: + for word in phrase.split(' '): + words.append(word) + else: + words.append(phrase) + + for word in words: + for phoneme in lexicon.translate_word(word): + word_defs['WORD'].append((word, phoneme)) + return word_defs + + def _compile_vocabulary(self, phrases): + prefix = 'jasper' + tmpdir = tempfile.mkdtemp() + + lexicon_file = jasperpath.data('julius-stt', 'VoxForge.tgz') + lexicon_archive_member = 'VoxForge/VoxForgeDict' + profile_path = jasperpath.config('profile.yml') + if os.path.exists(profile_path): + with open(profile_path, 'r') as f: + profile = yaml.safe_load(f) + if 'julius' in profile: + if 'lexicon' in profile['julius']: + lexicon_file = profile['julius']['lexicon'] + if 'lexicon_archive_member' in profile['julius']: + lexicon_archive_member = \ + profile['julius']['lexicon_archive_member'] + + lexicon = JuliusVocabulary.VoxForgeLexicon(lexicon_file, + lexicon_archive_member) + + # Create grammar file + tmp_grammar_file = os.path.join(tmpdir, + os.extsep.join([prefix, 'grammar'])) + with open(tmp_grammar_file, 'w') as f: + grammar = self._get_grammar(phrases) + for definition in grammar.pop('S'): + f.write("%s: %s\n" % ('S', ' '.join(definition))) + for name, definitions in grammar.items(): + for definition in definitions: + f.write("%s: %s\n" % (name, ' '.join(definition))) + + # Create voca file + tmp_voca_file = os.path.join(tmpdir, os.extsep.join([prefix, 'voca'])) + with open(tmp_voca_file, 'w') as f: + for category, words in self._get_word_defs(lexicon, + phrases).items(): + f.write("%% %s\n" % category) + for word, phoneme in words: + f.write("%s\t\t\t%s\n" % (word, phoneme)) + + # mkdfa.pl + olddir = os.getcwd() + os.chdir(tmpdir) + cmd = ['mkdfa.pl', str(prefix)] + with tempfile.SpooledTemporaryFile() as out_f: + subprocess.call(cmd, stdout=out_f, stderr=out_f) + out_f.seek(0) + for line in out_f.read().splitlines(): + line = line.strip() + if line: + self._logger.debug(line) + os.chdir(olddir) + + tmp_dfa_file = os.path.join(tmpdir, os.extsep.join([prefix, 'dfa'])) + tmp_dict_file = os.path.join(tmpdir, os.extsep.join([prefix, 'dict'])) + shutil.move(tmp_dfa_file, self.dfa_file) + shutil.move(tmp_dict_file, self.dict_file) + + shutil.rmtree(tmpdir) + + def get_phrases_from_module(module): """ Gets phrases from a module. @@ -373,7 +524,6 @@ def get_all_phrases(): return sorted(list(set(phrases))) if __name__ == '__main__': - import shutil import argparse parser = argparse.ArgumentParser(description='Vocabcompiler Demo') |
