├── .travis.yml
├── LICENSE
├── README.md
├── clean.sh
├── client
├── __init__.py
├── app_utils.py
├── brain.py
├── conversation.py
├── diagnose.py
├── g2p.py
├── jasperpath.py
├── local_mic.py
├── main.py
├── mic.py
├── modules
│ ├── Unclear.py
│ └── __init__.py
├── populate.py
├── stt.py
├── test_mic.py
├── tts.py
└── vocabcompiler.py
├── conf
└── profile.yml
├── jasper.py
├── requirements.txt
├── start.sh
├── static
├── audio
│ ├── beep_hi.wav
│ ├── beep_lo.wav
│ ├── jasper.wav
│ ├── say.wav
│ ├── time.wav
│ └── weather_zh.wav
├── dictionary_persona.dic
├── keyword_phrases
├── languagemodel_persona.lm
└── text
│ └── JOKES.txt
└── tests
├── __init__.py
├── test_brain.py
├── test_diagnose.py
└── test_modules.py
/.travis.yml:
--------------------------------------------------------------------------------
1 | env:
2 | - ARCH=x86
3 | language: python
4 | sudo: false
5 | python:
6 | - "3.4"
7 | cache:
8 | directories:
9 | - "$HOME/.pip-cache/"
10 | - "/home/travis/virtualenv/python3.4"
11 | install:
12 | - "pip install -r requirements.txt"
13 | - "pip install python-coveralls"
14 | - "pip install coverage"
15 | - "pip install flake8"
16 | before_script:
17 | - "flake8 jasper.py client tests"
18 | script:
19 | - "coverage run -m unittest discover"
20 | after_success:
21 | - "coverage report"
22 | - "coveralls"
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Junjie Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 来宝人工智能:基于树莓派的语音对话机器人
2 | =================
3 |
4 | [](https://travis-ci.org/jjwang/laibot-client) [](https://www.python.org)
6 |
7 | 如果你喜欢此项目,请给我打星。
8 |
9 | ## 1. 必读
10 | - [演示视频](http://v.youku.com/v_show/id_XMzIzNDUyNjQ5Mg==.html?spm=a2h3j.8428770.3416059.1)
11 | - [项目Wiki](https://github.com/jjwang/laibot-client/wiki)
12 |
13 | ## 2. 由来
14 |
15 | 来宝项目的由来是因为2017年智能音箱太火了!就智能音箱这个方向来说,众多巨头涌入的是一个2亿的小市场,小市场的由来是因为用户接受度不高(画外音:什么?天猫双十一卖了一百万台?OMG,我可什么都没说)。归根结底,就是现在的汉语普通话智能音箱方案用户体验不佳,说是智能、经常情况下是智障。如果方案在用户体验上没有任何优势,集成了再多的服务实际上用处并不大。
16 |
17 | 说了这么多,那么来宝想干嘛?其实很简单,来宝就是想试一下基于当前可用的开源软硬件和免费语音服务,能打造的语音助理最好能到什么样子?好吧,这就是来宝的由来!能做到什么程度,说实在的,我也不知道,走走看吧!来宝基于Jasper。
18 |
19 | ## 3. 规矩
20 |
21 | - 确保通过单元测试。
22 |
23 | python3 -m unittest discover
24 | - Python 代码需符合 PEP 8 编程规范,检查工具使用 flake8。
25 |
26 |
--------------------------------------------------------------------------------
/clean.sh:
--------------------------------------------------------------------------------
1 | find . -name "*.pyc" | xargs rm -rf
2 | find . -name "__pycache__" | xargs rm -rf
3 | rm -r -f conf/vocabularies
4 |
--------------------------------------------------------------------------------
/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/client/__init__.py
--------------------------------------------------------------------------------
/client/app_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import smtplib
3 | from email.MIMEText import MIMEText
4 | import urllib2
5 | import re
6 | from pytz import timezone
7 |
8 |
9 | def sendEmail(SUBJECT, BODY, TO, FROM, SENDER, PASSWORD, SMTP_SERVER):
10 | """Sends an HTML email."""
11 | for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8':
12 | try:
13 | BODY.encode(body_charset)
14 | except UnicodeError:
15 | pass
16 | else:
17 | break
18 | msg = MIMEText(BODY.encode(body_charset), 'html', body_charset)
19 | msg['From'] = SENDER
20 | msg['To'] = TO
21 | msg['Subject'] = SUBJECT
22 |
23 | SMTP_PORT = 587
24 | session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
25 | session.starttls()
26 | session.login(FROM, PASSWORD)
27 | session.sendmail(SENDER, TO, msg.as_string())
28 | session.quit()
29 |
30 |
31 | def emailUser(profile, SUBJECT="", BODY=""):
32 | """
33 | sends an email.
34 |
35 | Arguments:
36 | profile -- contains information related to the user (e.g., email
37 | address)
38 | SUBJECT -- subject line of the email
39 | BODY -- body text of the email
40 | """
41 | def generateSMSEmail(profile):
42 | """
43 | Generates an email from a user's phone number based on their carrier.
44 | """
45 | if profile['carrier'] is None or not profile['phone_number']:
46 | return None
47 |
48 | return str(profile['phone_number']) + "@" + profile['carrier']
49 |
50 | if profile['prefers_email'] and profile['gmail_address']:
51 | # add footer
52 | if BODY:
53 | BODY = profile['first_name'] + \
54 | ",
Here are your top headlines:" + BODY
55 | BODY += "
Sent from your Jasper"
56 |
57 | recipient = profile['gmail_address']
58 | if profile['first_name'] and profile['last_name']:
59 | recipient = profile['first_name'] + " " + \
60 | profile['last_name'] + " <%s>" % recipient
61 | else:
62 | recipient = generateSMSEmail(profile)
63 |
64 | if not recipient:
65 | return False
66 |
67 | try:
68 | if 'mailgun' in profile:
69 | user = profile['mailgun']['username']
70 | password = profile['mailgun']['password']
71 | server = 'smtp.mailgun.org'
72 | else:
73 | user = profile['gmail_address']
74 | password = profile['gmail_password']
75 | server = 'smtp.gmail.com'
76 | sendEmail(SUBJECT, BODY, recipient, user,
77 | "Jasper ", password, server)
78 |
79 | return True
80 | except Exception:
81 | return False
82 |
83 |
84 | def getTimezone(profile):
85 | """
86 | Returns the pytz timezone for a given profile.
87 |
88 | Arguments:
89 | profile -- contains information related to the user (e.g., email
90 | address)
91 | """
92 | try:
93 | return timezone(profile['timezone'])
94 | except Exception:
95 | return None
96 |
97 |
98 | def generateTinyURL(URL):
99 | """
100 | Generates a compressed URL.
101 |
102 | Arguments:
103 | URL -- the original URL to-be compressed
104 | """
105 | target = "http://tinyurl.com/api-create.php?url=" + URL
106 | response = urllib2.urlopen(target)
107 | return response.read()
108 |
109 |
110 | def isNegative(phrase):
111 | """
112 | Returns True if the input phrase has a negative sentiment.
113 |
114 | Arguments:
115 | phrase -- the input phrase to-be evaluated
116 | """
117 | return bool(re.search(r'\b(no(t)?|don\'t|stop|end)\b', phrase,
118 | re.IGNORECASE))
119 |
120 |
121 | def isPositive(phrase):
122 | """
123 | Returns True if the input phrase has a positive sentiment.
124 |
125 | Arguments:
126 | phrase -- the input phrase to-be evaluated
127 | """
128 | return bool(re.search(r'\b(sure|yes|yeah|go)\b', phrase, re.IGNORECASE))
129 |
--------------------------------------------------------------------------------
/client/brain.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import logging
3 | import pkgutil
4 | import client.jasperpath as jasperpath
5 |
6 |
7 | class Brain(object):
8 |
9 | def __init__(self, mic, profile):
10 | """
11 | Instantiates a new Brain object, which cross-references user
12 | input with a list of modules. Note that the order of brain.modules
13 | matters, as the Brain will cease execution on the first module
14 | that accepts a given input.
15 |
16 | Arguments:
17 | mic -- used to interact with the user (for both input and output)
18 | profile -- contains information related to the user (e.g., phone
19 | number)
20 | """
21 |
22 | self.mic = mic
23 | self.profile = profile
24 | self.modules = self.get_modules()
25 | self._logger = logging.getLogger(__name__)
26 |
27 | @classmethod
28 | def get_modules(cls):
29 | """
30 | Dynamically loads all the modules in the modules folder and sorts
31 | them by the PRIORITY key. If no PRIORITY is defined for a given
32 | module, a priority of 0 is assumed.
33 | """
34 |
35 | logger = logging.getLogger(__name__)
36 | locations = [jasperpath.PLUGIN_PATH]
37 | logger.debug("Looking for modules in: %s",
38 | ', '.join(["'%s'" % location for location in locations]))
39 | modules = []
40 | for finder, name, ispkg in pkgutil.walk_packages(locations):
41 | try:
42 | loader = finder.find_module(name)
43 | mod = loader.load_module(name)
44 | except Exception:
45 | logger.warning("Skipped module '%s' due to an error.", name,
46 | exc_info=True)
47 | else:
48 | if hasattr(mod, 'WORDS'):
49 | logger.debug("Found module '%s' with words: %r", name,
50 | mod.WORDS)
51 | modules.append(mod)
52 | else:
53 | logger.warning("Skipped module '%s' because it misses " +
54 | "the WORDS constant.", name)
55 | modules.sort(key=lambda mod: mod.PRIORITY if hasattr(mod, 'PRIORITY')
56 | else 0, reverse=True)
57 | return modules
58 |
59 | def query(self, texts):
60 | """
61 | Passes user input to the appropriate module, testing it against
62 | each candidate module's isValid function.
63 |
64 | Arguments:
65 | text -- user input, typically speech, to be parsed by a module
66 | """
67 | for module in self.modules:
68 | for text in texts:
69 | if module.isValid(text):
70 | self._logger.debug("'%s' is a valid phrase for module " +
71 | "'%s'", text, module.__name__)
72 | try:
73 | module.handle(text, self.mic, self.profile)
74 | except Exception:
75 | self._logger.error('Failed to execute module',
76 | exc_info=True)
77 | self.mic.say("I'm sorry. I had some trouble with " +
78 | "that operation. Please try again later.")
79 | else:
80 | self._logger.debug("Handling of phrase '%s' by " +
81 | "module '%s' completed", text,
82 | module.__name__)
83 | finally:
84 | return
85 | self._logger.debug("No module was able to handle any of these " +
86 | "phrases: %r", texts)
87 |
--------------------------------------------------------------------------------
/client/conversation.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import logging
3 | from client.brain import Brain
4 |
5 |
6 | class Conversation(object):
7 |
8 | def __init__(self, persona, mic, profile):
9 | self._logger = logging.getLogger(__name__)
10 | self.persona = persona
11 | self.mic = mic
12 | self.profile = profile
13 | self.brain = Brain(mic, profile)
14 |
15 | def handleForever(self):
16 | """
17 | Delegates user input to the handling function when activated.
18 | """
19 | self._logger.info("Starting to handle conversation with keyword '%s'.",
20 | self.persona)
21 | while True:
22 | self._logger.debug("Started listening for keyword '%s'",
23 | self.persona)
24 | threshold, transcribed = self.mic.passiveListen(self.persona)
25 | self._logger.debug("Stopped listening for keyword '%s'",
26 | self.persona)
27 |
28 | if not transcribed or not threshold:
29 | self._logger.info("Nothing has been said or transcribed.")
30 | continue
31 | self._logger.info("Keyword '%s' has been said!", self.persona)
32 |
33 | self._logger.debug("Started to listen actively with threshold: %r",
34 | threshold)
35 | input = self.mic.activeListen()
36 | self._logger.debug("Stopped to listen actively with threshold: %r",
37 | threshold)
38 |
39 | if input:
40 | self.brain.query(input)
41 | else:
42 | self.mic.say("请重说")
43 |
--------------------------------------------------------------------------------
/client/diagnose.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import os
3 | import sys
4 | import time
5 | import socket
6 | import subprocess
7 | import pkgutil
8 | import logging
9 | import pip.req
10 | from client import jasperpath
11 | if sys.version_info < (3, 3):
12 | from distutils.spawn import find_executable
13 | else:
14 | from shutil import which as find_executable
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | def check_network_connection(server="www.baidu.com"):
20 | """
21 | Checks if jasper can connect a network server.
22 |
23 | Arguments:
24 | server -- (optional) the server to connect with (Default:
25 | "www.google.com")
26 |
27 | Returns:
28 | True or False
29 | """
30 | logger = logging.getLogger(__name__)
31 | logger.debug("Checking network connection to server '%s'...", server)
32 | try:
33 | # see if we can resolve the host name -- tells us if there is
34 | # a DNS listening
35 | host = socket.gethostbyname(server)
36 | # connect to the host -- tells us if the host is actually
37 | # reachable
38 | socket.create_connection((host, 80), 2)
39 | except Exception:
40 | logger.debug("Network connection not working")
41 | return False
42 | else:
43 | logger.debug("Network connection working")
44 | return True
45 |
46 |
47 | def check_executable(executable):
48 | """
49 | Checks if an executable exists in $PATH.
50 |
51 | Arguments:
52 | executable -- the name of the executable (e.g. "echo")
53 |
54 | Returns:
55 | True or False
56 | """
57 | logger = logging.getLogger(__name__)
58 | logger.debug("Checking executable '%s'...", executable)
59 | executable_path = find_executable(executable)
60 | found = executable_path is not None
61 | if found:
62 | logger.debug("Executable '%s' found: '%s'", executable,
63 | executable_path)
64 | else:
65 | logger.debug("Executable '%s' not found", executable)
66 | return found
67 |
68 |
69 | def check_python_import(package_or_module):
70 | """
71 | Checks if a python package or module is importable.
72 |
73 | Arguments:
74 | package_or_module -- the package or module name to check
75 |
76 | Returns:
77 | True or False
78 | """
79 | logger = logging.getLogger(__name__)
80 | logger.debug("Checking python import '%s'...", package_or_module)
81 | loader = pkgutil.get_loader(package_or_module)
82 | found = loader is not None
83 | if found:
84 | logger.debug("Python %s '%s' found: %r",
85 | "package" if loader.is_package(package_or_module)
86 | else "module", package_or_module, loader.get_filename())
87 | else:
88 | logger.debug("Python import '%s' not found", package_or_module)
89 | return found
90 |
91 |
92 | def get_pip_requirements(fname=os.path.join(jasperpath.LIB_PATH,
93 | 'requirements.txt')):
94 | """
95 | Gets the PIP requirements from a text file. If the files does not exists
96 | or is not readable, it returns None
97 |
98 | Arguments:
99 | fname -- (optional) the requirement text file (Default:
100 | "client/requirements.txt")
101 |
102 | Returns:
103 | A list of pip requirement objects or None
104 | """
105 | logger = logging.getLogger(__name__)
106 | if os.access(fname, os.R_OK):
107 | reqs = list(pip.req.parse_requirements(fname))
108 | logger.debug("Found %d PIP requirements in file '%s'", len(reqs),
109 | fname)
110 | return reqs
111 | else:
112 | logger.debug("PIP requirements file '%s' not found or not readable",
113 | fname)
114 |
115 |
116 | def get_git_revision():
117 | """
118 | Gets the current git revision hash as hex string. If the git executable is
119 | missing or git is unable to get the revision, None is returned
120 |
121 | Returns:
122 | A hex string or None
123 | """
124 | logger = logging.getLogger(__name__)
125 | if not check_executable('git'):
126 | logger.warning("'git' command not found, git revision not detectable")
127 | return None
128 | output = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()
129 | if not output:
130 | logger.warning("Couldn't detect git revision (not a git repository?)")
131 | return None
132 | return output
133 |
134 |
135 | def run():
136 | """
137 | Performs a series of checks against the system and writes the results to
138 | the logging system.
139 |
140 | Returns:
141 | The number of failed checks as integer
142 | """
143 | logger = logging.getLogger(__name__)
144 |
145 | # Set loglevel of this module least to info
146 | loglvl = logger.getEffectiveLevel()
147 | if loglvl == logging.NOTSET or loglvl > logging.INFO:
148 | logger.setLevel(logging.INFO)
149 |
150 | logger.info("Starting jasper diagnostic at %s" % time.strftime("%c"))
151 | logger.info("Git revision: %r", get_git_revision())
152 |
153 | failed_checks = 0
154 |
155 | if not check_network_connection():
156 | failed_checks += 1
157 |
158 | for executable in ['phonetisaurus-g2p', 'espeak', 'say']:
159 | if not check_executable(executable):
160 | logger.warning("Executable '%s' is missing in $PATH", executable)
161 | failed_checks += 1
162 |
163 | for req in get_pip_requirements():
164 | logger.debug("Checking PIP package '%s'...", req.name)
165 | if not req.check_if_exists():
166 | logger.warning("PIP package '%s' is missing", req.name)
167 | failed_checks += 1
168 | else:
169 | logger.debug("PIP package '%s' found", req.name)
170 |
171 | for fname in [os.path.join(jasperpath.APP_PATH, os.pardir, "phonetisaurus",
172 | "g014b2b.fst")]:
173 | logger.debug("Checking file '%s'...", fname)
174 | if not os.access(fname, os.R_OK):
175 | logger.warning("File '%s' is missing", fname)
176 | failed_checks += 1
177 | else:
178 | logger.debug("File '%s' found", fname)
179 |
180 | if not failed_checks:
181 | logger.info("All checks passed")
182 | else:
183 | logger.info("%d checks failed" % failed_checks)
184 |
185 | return failed_checks
186 |
187 |
188 | if __name__ == '__main__':
189 | logging.basicConfig(stream=sys.stdout)
190 | logger = logging.getLogger()
191 | if '--debug' in sys.argv:
192 | logger.setLevel(logging.DEBUG)
193 | run()
194 |
--------------------------------------------------------------------------------
/client/g2p.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import os
3 | import re
4 | import subprocess
5 | import tempfile
6 | import logging
7 |
8 | import yaml
9 |
10 | import client.diagnose as diagnose
11 | import client.jasperpath as jasperpath
12 |
13 |
14 | class PhonetisaurusG2P(object):
15 | PATTERN = re.compile(r'^(?P.+)\t(?P\d+\.\d+)\t ' +
16 | r'(?P.*) ', re.MULTILINE)
17 |
18 | @classmethod
19 | def execute(cls, fst_model, input, is_file=False, nbest=None):
20 | logger = logging.getLogger(__name__)
21 |
22 | cmd = ['phonetisaurus-g2p',
23 | '--model=%s' % fst_model,
24 | '--input=%s' % input,
25 | '--words']
26 |
27 | if is_file:
28 | cmd.append('--isfile')
29 |
30 | if nbest is not None:
31 | cmd.extend(['--nbest=%d' % nbest])
32 |
33 | cmd = [str(x) for x in cmd]
34 | try:
35 | # FIXME: We can't just use subprocess.call and redirect stdout
36 | # and stderr, because it looks like Phonetisaurus can't open
37 | # an already opened file descriptor a second time. This is why
38 | # we have to use this somehow hacky subprocess.Popen approach.
39 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
40 | stderr=subprocess.PIPE)
41 | stdoutdata, stderrdata = proc.communicate()
42 | except OSError:
43 | logger.error("Error occured while executing command '%s'",
44 | ' '.join(cmd), exc_info=True)
45 | raise
46 |
47 | if stderrdata:
48 | for line in stderrdata.splitlines():
49 | message = line.strip()
50 | if message:
51 | logger.debug(message)
52 |
53 | if proc.returncode != 0:
54 | logger.error("Command '%s' return with exit status %d",
55 | ' '.join(cmd), proc.returncode)
56 | raise OSError("Command execution failed")
57 |
58 | result = {}
59 | if stdoutdata is not None:
60 | for word, precision, pronounc in \
61 | cls.PATTERN.findall(stdoutdata.decode('utf-8')):
62 | if word not in result:
63 | result[word] = []
64 | result[word].append(pronounc)
65 | return result
66 |
67 | @classmethod
68 | def get_config(cls):
69 | # FIXME: Replace this as soon as pull request
70 | # jasperproject/jasper-client#128 has been merged
71 |
72 | conf = {'fst_model': os.path.join(jasperpath.APP_PATH, os.pardir,
73 | 'phonetisaurus', 'g014b2b.fst')}
74 | # Try to get fst_model from config
75 | profile_path = jasperpath.config('profile.yml')
76 | if os.path.exists(profile_path):
77 | with open(profile_path, 'r') as f:
78 | profile = yaml.safe_load(f)
79 | if 'pocketsphinx' in profile:
80 | if 'fst_model' in profile['pocketsphinx']:
81 | conf['fst_model'] = \
82 | profile['pocketsphinx']['fst_model']
83 | if 'nbest' in profile['pocketsphinx']:
84 | conf['nbest'] = int(profile['pocketsphinx']['nbest'])
85 | return conf
86 |
87 | def __new__(cls, fst_model=None, *args, **kwargs):
88 | if not diagnose.check_executable('phonetisaurus-g2p'):
89 | raise OSError("Can't find command 'phonetisaurus-g2p'! Please " +
90 | "check if Phonetisaurus is installed and in your " +
91 | "$PATH.")
92 | if fst_model is None or not os.access(fst_model, os.R_OK):
93 | raise OSError(("FST model '%r' does not exist! Can't create " +
94 | "instance.") % fst_model)
95 | inst = object.__new__(cls, *args, **kwargs)
96 | return inst
97 |
98 | def __init__(self, fst_model=None, nbest=None):
99 | self._logger = logging.getLogger(__name__)
100 |
101 | self.fst_model = os.path.abspath(fst_model)
102 | self._logger.debug("Using FST model: '%s'", self.fst_model)
103 |
104 | self.nbest = nbest
105 | if self.nbest is not None:
106 | self._logger.debug("Will use the %d best results.", self.nbest)
107 |
108 | def _translate_word(self, word):
109 | return self.execute(self.fst_model, word, nbest=self.nbest)
110 |
111 | def _translate_words(self, words):
112 | with tempfile.NamedTemporaryFile(suffix='.g2p', delete=False) as f:
113 | # The 'delete=False' kwarg is kind of a hack, but Phonetisaurus
114 | # won't work if we remove it, because it seems that I can't open
115 | # a file descriptor a second time.
116 | for word in words:
117 | f.write(('%s\n' % word).encode('utf-8'))
118 | tmp_fname = f.name
119 | output = self.execute(self.fst_model, tmp_fname, is_file=True,
120 | nbest=self.nbest)
121 | os.remove(tmp_fname)
122 | return output
123 |
124 | def translate(self, words):
125 | if type(words) is str or len(words) == 1:
126 | self._logger.debug('Converting single word to phonemes')
127 | output = self._translate_word(words if type(words) is str
128 | else words[0])
129 | else:
130 | self._logger.debug('Converting %d words to phonemes', len(words))
131 | output = self._translate_words(words)
132 | self._logger.debug('G2P conversion returned phonemes for %d words',
133 | len(output))
134 | return output
135 |
136 |
137 | if __name__ == "__main__":
138 | import pprint
139 | import argparse
140 | parser = argparse.ArgumentParser(description='Phonetisaurus G2P module')
141 | parser.add_argument('fst_model', action='store',
142 | help='Path to the FST Model')
143 | parser.add_argument('--debug', action='store_true',
144 | help='Show debug messages')
145 | args = parser.parse_args()
146 |
147 | logging.basicConfig()
148 | logger = logging.getLogger()
149 | if args.debug:
150 | logger.setLevel(logging.DEBUG)
151 |
152 | words = ['THIS', 'IS', 'A', 'TEST']
153 |
154 | g2pconv = PhonetisaurusG2P(args.fst_model, nbest=3)
155 | output = g2pconv.translate(words)
156 |
157 | pp = pprint.PrettyPrinter(indent=2)
158 | pp.pprint(output)
159 |
--------------------------------------------------------------------------------
/client/jasperpath.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import os
3 |
4 | # Jasper main directory
5 | APP_PATH = os.path.normpath(os.path.join(
6 | os.path.dirname(os.path.abspath(__file__)), os.pardir))
7 |
8 | DATA_PATH = os.path.join(APP_PATH, "static")
9 | LIB_PATH = os.path.join(APP_PATH, "client")
10 | TOOLS_PATH = os.path.join(APP_PATH, "tools")
11 |
12 | PLUGIN_PATH = os.path.join(LIB_PATH, "modules")
13 | CONFIG_PATH = os.path.join(APP_PATH, 'conf')
14 | TJBOT_PATH = os.path.join(APP_PATH, '../tjbot/bootstrap/tests/')
15 |
16 |
17 | def config(*fname):
18 | return os.path.join(CONFIG_PATH, *fname)
19 |
20 |
21 | def data(*fname):
22 | return os.path.join(DATA_PATH, *fname)
23 |
24 |
25 | def tjbot(*fname):
26 | return os.path.join(TJBOT_PATH, *fname)
27 |
28 |
29 | def hotword():
30 | return 'OKEY TOMMY'
31 |
--------------------------------------------------------------------------------
/client/local_mic.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | """
3 | A drop-in replacement for the Mic class that allows for all I/O to occur
4 | over the terminal. Useful for debugging. Unlike with the typical Mic
5 | implementation, Jasper is always active listening with local_mic.
6 | """
7 |
8 |
9 | class Mic:
10 | prev = None
11 |
12 | def __init__(self, speaker, passive_stt_engine, active_stt_engine):
13 | return
14 |
15 | def passiveListen(self, PERSONA):
16 | return True, "JASPER"
17 |
18 | def activeListenToAllOptions(self, THRESHOLD=None, LISTEN=True,
19 | MUSIC=False):
20 | return [self.activeListen(THRESHOLD=THRESHOLD, LISTEN=LISTEN,
21 | MUSIC=MUSIC)]
22 |
23 | def activeListen(self, THRESHOLD=None, LISTEN=True, MUSIC=False):
24 | if not LISTEN:
25 | return self.prev
26 |
27 | input = input("YOU: ")
28 | self.prev = input
29 | return input
30 |
31 | def say(self, phrase, OPTIONS=None):
32 | print("JASPER: %s" % phrase)
33 |
--------------------------------------------------------------------------------
/client/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8-*-
3 | # This file exists for backwards compatibility with older versions of jasper.
4 | # It might be removed in future versions.
5 | import os
6 | import sys
7 | import runpy
8 | script_path = os.path.join(os.path.dirname(__file__), os.pardir, "jasper.py")
9 | sys.path.insert(0, os.path.dirname(script_path))
10 | runpy.run_path(script_path, run_name="__main__")
11 |
--------------------------------------------------------------------------------
/client/mic.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | """
3 | The Mic class handles all interactions with the microphone and speaker.
4 | """
5 | import logging
6 | import tempfile
7 | import wave
8 | import os
9 | import pyaudio
10 | import client.jasperpath as jasperpath
11 | import sys
12 |
13 | import webrtcvad
14 | import collections
15 | import signal
16 | from array import array
17 | import time
18 |
19 | RATE = 16000
20 |
21 | gotOneSentence = False
22 | leaveRecord = False
23 |
24 |
25 | def handle_int(sig, chunk):
26 | global leaveRecord, gotOneSentence
27 | leaveRecord = True
28 | gotOneSentence = True
29 |
30 |
31 | def normalize(snd_data):
32 | "Average the volume out"
33 | MAXIMUM = 32767 # 16384
34 | times = float(MAXIMUM) / max(abs(i) for i in snd_data)
35 | r = array('h')
36 | for i in snd_data:
37 | r.append(int(i * times))
38 | return r
39 |
40 |
41 | class Mic:
42 |
43 | speechRec = None
44 | speechRec_persona = None
45 |
46 | def __init__(self, speaker, passive_stt_engine, active_stt_engine):
47 | """
48 | Initiates the pocketsphinx instance.
49 |
50 | Arguments:
51 | speaker -- handles platform-independent audio output
52 | passive_stt_engine -- performs STT while Jasper is in passive listen
53 | mode
54 | acive_stt_engine -- performs STT while Jasper is in active listen mode
55 | """
56 | self._logger = logging.getLogger(__name__)
57 | self.speaker = speaker
58 | self.passive_stt_engine = passive_stt_engine
59 | self.active_stt_engine = active_stt_engine
60 | self._logger.info("Initializing PyAudio. ALSA/Jack error messages " +
61 | "that pop up during this process are normal and " +
62 | "can usually be safely ignored.")
63 | self._audio = pyaudio.PyAudio()
64 | self._logger.info("Initialization of PyAudio completed.")
65 |
66 | def __del__(self):
67 | self._audio.terminate()
68 |
69 | def passiveListen(self, PERSONA):
70 | """
71 | Listens for PERSONA in everyday sound. Times out after LISTEN_TIME, so
72 | needs to be restarted.
73 | """
74 | transcribed = self.listenVoice(False, PERSONA)
75 | if any(PERSONA in phrase for phrase in transcribed):
76 | return (True, PERSONA)
77 |
78 | return (False, transcribed)
79 |
80 | def activeListen(self):
81 | """
82 | Records until a second of silence or times out after 12 seconds
83 |
84 | Returns the first matching string or None
85 | """
86 | return self.listenVoice(True)
87 |
88 | def listenVoice(self, ACTIVE=True, PERSONA=''):
89 | FORMAT = pyaudio.paInt16
90 | CHANNELS = 1
91 | CHUNK_DURATION_MS = 30 # supports 10, 20 and 30 (ms)
92 | PADDING_DURATION_MS = 1500 # 1 sec jugement
93 | CHUNK_SIZE = int(RATE * CHUNK_DURATION_MS / 1000) # chunk to read
94 | NUM_PADDING_CHUNKS = int(PADDING_DURATION_MS / CHUNK_DURATION_MS)
95 | NUM_WINDOW_CHUNKS = int(400 / CHUNK_DURATION_MS) # 400ms/30ms
96 | NUM_WINDOW_CHUNKS_END = NUM_WINDOW_CHUNKS * 2
97 |
98 | global leaveRecord, gotOneSentence
99 | if leaveRecord:
100 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
101 | os.system("node " + jasperpath.tjbot('shine.led.js') + " off")
102 | return None
103 |
104 | # prepare recording stream
105 | stream = self._audio.open(format=FORMAT,
106 | channels=CHANNELS,
107 | rate=RATE,
108 | input=True,
109 | start=False,
110 | frames_per_buffer=CHUNK_SIZE)
111 | vad = webrtcvad.Vad(1)
112 | signal.signal(signal.SIGINT, handle_int)
113 | if not ACTIVE:
114 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
115 | os.system("node " + jasperpath.tjbot('shine.led.js') +
116 | " white")
117 | stream.start_stream()
118 | raw_data = array('h')
119 | start_point = 0
120 | end_point = 0
121 |
122 | # loop for passive listening
123 | while not leaveRecord:
124 | gotOneSentence = False
125 |
126 | if ACTIVE:
127 | self.speaker.play(jasperpath.data('audio', 'beep_hi.wav'))
128 | else:
129 | self.passive_stt_engine.utt_start()
130 | # Process buffered voice data
131 | if end_point > 0:
132 | raw_data.reverse()
133 | for index in range(end_point - CHUNK_SIZE * 20):
134 | raw_data.pop()
135 | raw_data.reverse()
136 | print("* process buffered voice data...")
137 | transcribed = self.passive_stt_engine.utt_transcribe(
138 | raw_data)
139 | # if voice trigger is included in results, return directly
140 | if any(PERSONA in phrase for phrase in transcribed):
141 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
142 | os.system("node " +
143 | jasperpath.tjbot('shine.led.js') +
144 | " off")
145 | self.passive_stt_engine.utt_end()
146 | stream.stop_stream()
147 | stream.close()
148 | return transcribed
149 |
150 | ring_buffer = collections.deque(maxlen=NUM_PADDING_CHUNKS)
151 | triggered = False
152 | ring_buffer_flags = [0] * NUM_WINDOW_CHUNKS
153 | ring_buffer_index = 0
154 |
155 | ring_buffer_flags_end = [0] * NUM_WINDOW_CHUNKS_END
156 | ring_buffer_index_end = 0
157 | index = 0
158 | start_point = 0
159 | end_point = 0
160 | StartTime = time.time()
161 | print("* recording: ")
162 | raw_data = array('h')
163 | if ACTIVE:
164 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
165 | os.system("node " + jasperpath.tjbot('shine.led.js') +
166 | " blue")
167 | stream.start_stream()
168 |
169 | # stop recording when EOS is detected
170 | while not gotOneSentence and not leaveRecord:
171 | chunk = stream.read(CHUNK_SIZE, exception_on_overflow=False)
172 | if not ACTIVE:
173 | transcribed = self.passive_stt_engine.utt_transcribe(chunk)
174 | if any(PERSONA in phrase for phrase in transcribed):
175 | triggered = False
176 | gotOneSentence = True
177 | end_point = index
178 | break
179 | # add WangS
180 | raw_data.extend(array('h', chunk))
181 | index += CHUNK_SIZE
182 | TimeUse = time.time() - StartTime
183 |
184 | active = vad.is_speech(chunk, RATE)
185 |
186 | if ACTIVE:
187 | sys.stdout.write('I' if active else '_')
188 | ring_buffer_flags[ring_buffer_index] = 1 if active else 0
189 | ring_buffer_index += 1
190 | ring_buffer_index %= NUM_WINDOW_CHUNKS
191 |
192 | ring_buffer_flags_end[ring_buffer_index_end] = \
193 | 1 if active else 0
194 | ring_buffer_index_end += 1
195 | ring_buffer_index_end %= NUM_WINDOW_CHUNKS_END
196 |
197 | # start point detection
198 | if not triggered:
199 | ring_buffer.append(chunk)
200 | num_voiced = sum(ring_buffer_flags)
201 | if num_voiced > 0.8 * NUM_WINDOW_CHUNKS:
202 | if ACTIVE:
203 | sys.stdout.write('[OPEN]')
204 | triggered = True
205 | start_point = index - CHUNK_SIZE * 20 # start point
206 | # voiced_frames.extend(ring_buffer)
207 | ring_buffer.clear()
208 | # end point detection
209 | else:
210 | # voiced_frames.append(chunk)
211 | ring_buffer.append(chunk)
212 | num_unvoiced = NUM_WINDOW_CHUNKS_END \
213 | - sum(ring_buffer_flags_end)
214 | if num_unvoiced > 0.90 * NUM_WINDOW_CHUNKS_END \
215 | or TimeUse > 10:
216 | if ACTIVE:
217 | sys.stdout.write('[CLOSE]')
218 | triggered = False
219 | gotOneSentence = True
220 | end_point = index
221 | sys.stdout.flush()
222 |
223 | sys.stdout.write('\n')
224 |
225 | # result processing for passive and active listening respectively
226 | print("* done recording")
227 | if leaveRecord:
228 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
229 | os.system("node " +
230 | jasperpath.tjbot('shine.led.js') +
231 | " off")
232 | break
233 | if ACTIVE:
234 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
235 | os.system("node " + jasperpath.tjbot('shine.led.js') +
236 | " off")
237 | self.speaker.play(jasperpath.data('audio', 'beep_lo.wav'))
238 | # write to file
239 | raw_data.reverse()
240 | for index in range(start_point):
241 | raw_data.pop()
242 | raw_data.reverse()
243 | raw_data = normalize(raw_data)
244 |
245 | stream.stop_stream()
246 | stream.close()
247 |
248 | # save the audio data
249 | with tempfile.SpooledTemporaryFile(mode='w+b') as f:
250 | wav_fp = wave.open(f, 'wb')
251 | wav_fp.setnchannels(1)
252 | wav_fp.setsampwidth(
253 | pyaudio.get_sample_size(pyaudio.paInt16))
254 | wav_fp.setframerate(RATE)
255 | wav_fp.writeframes(raw_data)
256 | wav_fp.close()
257 | f.seek(0)
258 | return self.active_stt_engine.transcribe(f)
259 | else:
260 | # read one more chunks in EOS
261 | chunk = stream.read(CHUNK_SIZE, exception_on_overflow=False)
262 | transcribed = self.passive_stt_engine.utt_transcribe(chunk)
263 | self.passive_stt_engine.utt_end()
264 | # if voice trigger is included in results, return directly
265 | if any(PERSONA in phrase for phrase in transcribed):
266 | if os.path.exists(jasperpath.tjbot('shine.led.js')):
267 | os.system("node " +
268 | jasperpath.tjbot('shine.led.js') +
269 | " off")
270 | stream.stop_stream()
271 | stream.close()
272 | return transcribed
273 | # if voice trigger is not included in results, start another
274 | # cycle
275 |
276 | # exit
277 | if ACTIVE:
278 | stream.close()
279 | return None
280 |
281 | def say(self, phrase,
282 | OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"):
283 | # alter phrase before speaking
284 | self.speaker.say(phrase)
285 |
--------------------------------------------------------------------------------
/client/modules/Unclear.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import random
3 | import sys # Laibot
4 | import urllib
5 | import re
6 | import os
7 | import client.jasperpath as jasperpath
8 |
9 | WORDS = []
10 |
11 | PRIORITY = -(sys.maxsize + 1)
12 |
13 |
14 | def handle(text, mic, profile):
15 | """
16 | Reports that the user has unclear or unusable input.
17 |
18 | Arguments:
19 | text -- user-input, typically transcribed speech
20 | mic -- used to interact with the user (for both input and output)
21 | profile -- contains information related to the user (e.g., phone
22 | number)
23 | """
24 |
25 | try:
26 | url = 'http://laibot.applinzi.com/chat?'
27 | pat = '[\s+\.\!\/_,$%^*(+\"\']+|[+——!,。?、~@]'
28 | req = re.sub(pat, '', text)
29 | url = url + urllib.parse.urlencode({'req': req})
30 | http_response = urllib.request.urlopen(url)
31 | ans = http_response.read().decode('utf-8')
32 | if ans.find('//shakehand') >= 0:
33 | ans = ans.replace('//shakehand', '')
34 | if os.path.exists(jasperpath.tjbot('shakehand.servo.js')):
35 | os.system("node " + jasperpath.tjbot('shakehand.servo.js'))
36 | os.system("node " + jasperpath.tjbot('shakehand.servo.js'))
37 | mic.say(ans)
38 | except Exception as err:
39 | print(str(err))
40 |
41 | messages = ["我没有听清楚",
42 | "请再说一遍"]
43 | message = random.choice(messages)
44 | mic.say(message)
45 |
46 |
47 | def isValid(text):
48 | return True
49 |
--------------------------------------------------------------------------------
/client/modules/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/client/modules/__init__.py
--------------------------------------------------------------------------------
/client/populate.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | import os
3 | import re
4 | from getpass import getpass
5 | import yaml
6 | from pytz import timezone
7 | import feedparser
8 | import jasperpath
9 |
10 |
11 | def run():
12 | profile = {}
13 |
14 | print("Welcome to the profile populator. If, at any step, you'd prefer " +
15 | "not to enter the requested information, just hit 'Enter' with a " +
16 | "blank field to continue.")
17 |
18 | def simple_request(var, cleanVar, cleanInput=None):
19 | input = input(cleanVar + ": ")
20 | if input:
21 | if cleanInput:
22 | input = cleanInput(input)
23 | profile[var] = input
24 |
25 | # name
26 | simple_request('first_name', 'First name')
27 | simple_request('last_name', 'Last name')
28 |
29 | # gmail
30 | print("\nJasper uses your Gmail to send notifications. Alternatively, " +
31 | "you can skip this step (or just fill in the email address if you " +
32 | "want to receive email notifications) and setup a Mailgun " +
33 | "account, as at http://jasperproject.github.io/documentation/" +
34 | "software/#mailgun.\n")
35 | simple_request('gmail_address', 'Gmail address')
36 | profile['gmail_password'] = getpass()
37 |
38 | # phone number
39 | def clean_number(s):
40 | return re.sub(r'[^0-9]', '', s)
41 |
42 | phone_number = clean_number(input("\nPhone number (no country " +
43 | "code). Any dashes or spaces will " +
44 | "be removed for you: "))
45 | profile['phone_number'] = phone_number
46 |
47 | # carrier
48 | print("\nPhone carrier (for sending text notifications).")
49 | print("If you have a US phone number, you can enter one of the " +
50 | "following: 'AT&T', 'Verizon', 'T-Mobile' (without the quotes). " +
51 | "If your carrier isn't listed or you have an international " +
52 | "number, go to http://www.emailtextmessages.com and enter the " +
53 | "email suffix for your carrier (e.g., for Virgin Mobile, enter " +
54 | "'vmobl.com'; for T-Mobile Germany, enter 't-d1-sms.de').")
55 | carrier = input('Carrier: ')
56 | if carrier == 'AT&T':
57 | profile['carrier'] = 'txt.att.net'
58 | elif carrier == 'Verizon':
59 | profile['carrier'] = 'vtext.com'
60 | elif carrier == 'T-Mobile':
61 | profile['carrier'] = 'tmomail.net'
62 | else:
63 | profile['carrier'] = carrier
64 |
65 | # location
66 | def verifyLocation(place):
67 | feed = feedparser.parse('http://rss.wunderground.com/auto/rss_full/' +
68 | place)
69 | numEntries = len(feed['entries'])
70 | if numEntries == 0:
71 | return False
72 | else:
73 | print("Location saved as " + feed['feed']['description'][33:])
74 | return True
75 |
76 | print("\nLocation should be a 5-digit US zipcode (e.g., 08544). If you " +
77 | "are outside the US, insert the name of your nearest big " +
78 | "town/city. For weather requests.")
79 | location = input("Location: ")
80 | while location and not verifyLocation(location):
81 | print("Weather not found. Please try another location.")
82 | location = input("Location: ")
83 | if location:
84 | profile['location'] = location
85 |
86 | # timezone
87 | print("\nPlease enter a timezone from the list located in the TZ* " +
88 | "column at http://en.wikipedia.org/wiki/" +
89 | "List_of_tz_database_time_zones, or none at all.")
90 | tz = input("Timezone: ")
91 | while tz:
92 | try:
93 | timezone(tz)
94 | profile['timezone'] = tz
95 | break
96 | except Exception:
97 | print("Not a valid timezone. Try again.")
98 | tz = input("Timezone: ")
99 |
100 | response = input("\nWould you prefer to have notifications sent by " +
101 | "email (E) or text message (T)? ")
102 | while not response or (response != 'E' and response != 'T'):
103 | response = input("Please choose email (E) or text message (T): ")
104 | profile['prefers_email'] = (response == 'E')
105 |
106 | stt_engines = {
107 | "sphinx": None,
108 | "google": "GOOGLE_SPEECH"
109 | }
110 |
111 | response = input("\nIf you would like to choose a specific STT " +
112 | "engine, please specify which.\nAvailable " +
113 | "implementations: %s. (Press Enter to default " +
114 | "to PocketSphinx): " % stt_engines.keys())
115 | if (response in stt_engines):
116 | profile["stt_engine"] = response
117 | api_key_name = stt_engines[response]
118 | if api_key_name:
119 | key = input("\nPlease enter your API key: ")
120 | profile["keys"] = {api_key_name: key}
121 | else:
122 | print("Unrecognized STT engine. Available implementations: %s"
123 | % stt_engines.keys())
124 | profile["stt_engine"] = "sphinx"
125 |
126 | if response == "google":
127 | response = input("\nChoosing Google means every sound " +
128 | "makes a request online. " +
129 | "\nWould you like to process the wake up word " +
130 | "locally with PocketSphinx? (Y) or (N)?")
131 | while not response or (response != 'Y' and response != 'N'):
132 | response = input("Please choose PocketSphinx (Y) " +
133 | "or keep just Google (N): ")
134 | if response == 'Y':
135 | profile['stt_passive_engine'] = "sphinx"
136 |
137 | # write to profile
138 | print("Writing to profile...")
139 | if not os.path.exists(jasperpath.CONFIG_PATH):
140 | os.makedirs(jasperpath.CONFIG_PATH)
141 | outputFile = open(jasperpath.config("profile.yml"), "w")
142 | yaml.dump(profile, outputFile, default_flow_style=False)
143 | print("Done.")
144 |
145 |
146 | if __name__ == "__main__":
147 | run()
148 |
--------------------------------------------------------------------------------
/client/stt.py:
--------------------------------------------------------------------------------
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 | from abc import ABCMeta, abstractmethod
10 | import requests
11 | import yaml
12 | import client.jasperpath as jasperpath
13 | import client.diagnose as diagnose
14 | import client.vocabcompiler as vocabcompiler
15 |
16 | import base64 # Baidu TTS
17 | from uuid import getnode as get_mac
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 | @abstractmethod
65 | def utt_start(self):
66 | pass
67 |
68 | @abstractmethod
69 | def utt_end(self):
70 | pass
71 |
72 | @abstractmethod
73 | def utt_transcribe(self, data):
74 | pass
75 |
76 |
77 | class BaiduSTT(AbstractSTTEngine):
78 | """
79 | Baidu's STT engine
80 | To leverage this engile, please register a developer account at
81 | yuyin.baidu.com. Then new an application, get API Key and Secret
82 | Key in management console. Fill them in profile.yml.
83 | ...
84 | baidu_yuyin:
85 | api_key: 'xxx............................................'
86 | secret_key: 'xxx.........................................'
87 | ...
88 | """
89 |
90 | SLUG = "baidu-stt"
91 |
92 | def __init__(self, api_key='', secret_key=''):
93 | self._logger = logging.getLogger(__name__)
94 | self.api_key = api_key
95 | self.secret_key = secret_key
96 | self.token = ''
97 |
98 | @classmethod
99 | def get_config(cls):
100 | # FIXME: Replace this as soon as we have a config module
101 | config = {}
102 | # Try to get baidu_yuyin config from config
103 | profile_path = jasperpath.config('profile.yml')
104 | if os.path.exists(profile_path):
105 | with open(profile_path, 'r') as f:
106 | profile = yaml.safe_load(f)
107 | if 'baidu_yuyin' in profile:
108 | if 'api_key' in profile['baidu_yuyin']:
109 | config['api_key'] = \
110 | profile['baidu_yuyin']['api_key']
111 | if 'secret_key' in profile['baidu_yuyin']:
112 | config['secret_key'] = \
113 | profile['baidu_yuyin']['secret_key']
114 | return config
115 |
116 | def get_token(self):
117 | URL = 'http://openapi.baidu.com/oauth/2.0/token'
118 | params = urllib.parse.urlencode({'grant_type': 'client_credentials',
119 | 'client_id': self.api_key,
120 | 'client_secret': self.secret_key})
121 | r = requests.get(URL, params=params)
122 | try:
123 | r.raise_for_status()
124 | token = r.json()['access_token']
125 | return token
126 | except requests.exceptions.HTTPError:
127 | self._logger.critical('Token request failed with response: %r',
128 | r.text,
129 | exc_info=True)
130 | return ''
131 |
132 | def transcribe(self, fp):
133 | try:
134 | wav_file = wave.open(fp, 'rb')
135 | except IOError:
136 | self._logger.critical('wav file not found: %s',
137 | fp,
138 | exc_info=True)
139 | return []
140 | n_frames = wav_file.getnframes()
141 | frame_rate = wav_file.getframerate()
142 | audio = wav_file.readframes(n_frames)
143 | base_data = base64.b64encode(audio)
144 | if self.token == '':
145 | self.token = self.get_token()
146 | data = {"format": "wav",
147 | "token": self.token,
148 | "len": len(audio),
149 | "rate": frame_rate,
150 | "speech": base_data.decode(encoding="utf-8"),
151 | "cuid": str(get_mac())[:32],
152 | "channel": 1}
153 | data = json.dumps(data)
154 | r = requests.post('http://vop.baidu.com/server_api',
155 | data=data,
156 | headers={'content-type': 'application/json'})
157 | try:
158 | r.raise_for_status()
159 | text = ''
160 | if 'result' in r.json():
161 | text = r.json()['result'][0]
162 | except requests.exceptions.HTTPError:
163 | self._logger.critical('Request failed with response: %r',
164 | r.text,
165 | exc_info=True)
166 | return []
167 | except requests.exceptions.RequestException:
168 | self._logger.critical('Request failed.', exc_info=True)
169 | return []
170 | except ValueError as e:
171 | self._logger.critical('Cannot parse response: %s',
172 | e.args[0])
173 | return []
174 | except KeyError:
175 | self._logger.critical('Cannot parse response.',
176 | exc_info=True)
177 | return []
178 | else:
179 | transcribed = []
180 | if text:
181 | transcribed.append(text.upper())
182 | self._logger.info(u'Baidu STT outputs: %s' % text)
183 | return transcribed
184 |
185 | @classmethod
186 | def is_available(cls):
187 | return diagnose.check_network_connection()
188 |
189 | def utt_start(self):
190 | return True
191 |
192 | def utt_end(self):
193 | return True
194 |
195 | def utt_transcribe(self, data):
196 | return ''
197 |
198 |
199 | class PocketSphinxSTT(AbstractSTTEngine):
200 | """
201 | The default Speech-to-Text implementation which relies on PocketSphinx.
202 | """
203 |
204 | SLUG = 'sphinx'
205 | VOCABULARY_TYPE = vocabcompiler.PocketsphinxVocabulary
206 |
207 | _pocketsphinx_v5 = False
208 | _previous_decoding_output = ''
209 |
210 | def __init__(self, vocabulary, hmm_dir="/usr/local/share/" +
211 | "pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k"):
212 |
213 | """
214 | Initiates the pocketsphinx instance.
215 |
216 | Arguments:
217 | vocabulary -- a PocketsphinxVocabulary instance
218 | hmm_dir -- the path of the Hidden Markov Model (HMM)
219 | """
220 |
221 | self._logger = logging.getLogger(__name__)
222 |
223 | # quirky bug where first import doesn't work
224 | try:
225 | import pocketsphinx as ps
226 | except Exception:
227 | import pocketsphinx as ps
228 |
229 | with tempfile.NamedTemporaryFile(prefix='psdecoder_',
230 | suffix='.log', delete=False) as f:
231 | self._logfile = f.name
232 |
233 | self._logger.debug("Initializing PocketSphinx Decoder with hmm_dir " +
234 | "'%s'", hmm_dir)
235 |
236 | # Perform some checks on the hmm_dir so that we can display more
237 | # meaningful error messages if neccessary
238 | if not os.path.exists(hmm_dir):
239 | msg = ("hmm_dir '%s' does not exist! Please make sure that you " +
240 | "have set the correct hmm_dir in your profile.") % hmm_dir
241 | self._logger.error(msg)
242 | raise RuntimeError(msg)
243 | # Lets check if all required files are there. Refer to:
244 | # http://cmusphinx.sourceforge.net/wiki/acousticmodelformat
245 | # for details
246 | missing_hmm_files = []
247 | for fname in ('mdef', 'feat.params', 'means', 'noisedict',
248 | 'transition_matrices', 'variances'):
249 | if not os.path.exists(os.path.join(hmm_dir, fname)):
250 | missing_hmm_files.append(fname)
251 | mixweights = os.path.exists(os.path.join(hmm_dir, 'mixture_weights'))
252 | sendump = os.path.exists(os.path.join(hmm_dir, 'sendump'))
253 | if not mixweights and not sendump:
254 | # We only need mixture_weights OR sendump
255 | missing_hmm_files.append('mixture_weights or sendump')
256 | if missing_hmm_files:
257 | self._logger.warning("hmm_dir '%s' is missing files: %s. Please " +
258 | "make sure that you have set the correct " +
259 | "hmm_dir in your profile.",
260 | hmm_dir, ', '.join(missing_hmm_files))
261 | self._pocketsphinx_v5 = hasattr(ps.Decoder, 'default_config')
262 | if self._pocketsphinx_v5:
263 | # Pocketsphinx v5
264 | config = ps.Decoder.default_config()
265 | config.set_string('-hmm', hmm_dir)
266 | config.set_string('-lm', vocabulary.languagemodel_file)
267 | config.set_string('-dict', vocabulary.dictionary_file)
268 | config.set_string('-logfn', self._logfile)
269 | self._decoder = ps.Decoder(config)
270 | else:
271 | self._decoder = ps.Decoder(hmm=hmm_dir, logfn=self._logfile,
272 | lm=vocabulary.languagemodel_file,
273 | dict=vocabulary.dictionary_file)
274 |
275 | def __del__(self):
276 | os.remove(self._logfile)
277 |
278 | @classmethod
279 | def get_config(cls):
280 | # FIXME: Replace this as soon as we have a config module
281 | config = {}
282 | # HMM dir
283 | # Try to get hmm_dir from config
284 | profile_path = jasperpath.config('profile.yml')
285 |
286 | if os.path.exists(profile_path):
287 | with open(profile_path, 'r') as f:
288 | profile = yaml.safe_load(f)
289 | try:
290 | config['hmm_dir'] = profile['pocketsphinx']['hmm_dir']
291 | except KeyError:
292 | pass
293 |
294 | return config
295 |
296 | def transcribe(self, fp):
297 | """
298 | Performs STT, transcribing an audio file and returning the result.
299 |
300 | Arguments:
301 | fp -- a file object containing audio data
302 | """
303 |
304 | fp.seek(44)
305 |
306 | # FIXME: Can't use the Decoder.decode_raw() here, because
307 | # pocketsphinx segfaults with tempfile.SpooledTemporaryFile()
308 | data = fp.read()
309 | self._decoder.start_utt()
310 | self._decoder.process_raw(data, False, True)
311 | self._decoder.end_utt()
312 |
313 | if self._pocketsphinx_v5:
314 | hyp = self._decoder.hyp()
315 | result = hyp.hypstr if hyp is not None else ''
316 | else:
317 | result = self._decoder.get_hyp()[0]
318 | if self._logfile is not None:
319 | with open(self._logfile, 'r+') as f:
320 | for line in f:
321 | self._logger.debug(line.strip())
322 | f.truncate()
323 |
324 | transcribed = [result] if result != '' else []
325 | self._logger.info('Transcribed: %r', transcribed)
326 | return transcribed
327 |
328 | @classmethod
329 | def is_available(cls):
330 | return diagnose.check_python_import('pocketsphinx')
331 |
332 | def utt_start(self):
333 | self._decoder.start_utt()
334 | return True
335 |
336 | def utt_end(self):
337 | self._decoder.end_utt()
338 | return True
339 |
340 | def utt_transcribe(self, data):
341 | self._decoder.process_raw(data, False, False)
342 | if self._pocketsphinx_v5:
343 | hyp = self._decoder.hyp()
344 | result = hyp.hypstr if hyp is not None else ''
345 | else:
346 | result = self._decoder.get_hyp()[0]
347 | transcribed = [result] if result != '' else []
348 | if self._previous_decoding_output != transcribed:
349 | self._logger.info('Partial: %r', transcribed)
350 | self._previous_decoding_output = transcribed
351 | return transcribed
352 |
353 |
354 | def get_engine_by_slug(slug=None):
355 | """
356 | Returns:
357 | An STT Engine implementation available on the current platform
358 |
359 | Raises:
360 | ValueError if no speaker implementation is supported on this platform
361 | """
362 |
363 | if not slug or type(slug) is not str:
364 | raise TypeError("Invalid slug '%s'", slug)
365 |
366 | selected_filter = filter(lambda engine: hasattr(engine, "SLUG") and
367 | engine.SLUG == slug, get_engines())
368 | selected_engines = [engine for engine in selected_filter]
369 | if len(selected_engines) == 0:
370 | raise ValueError("No STT engine found for slug '%s'" % slug)
371 | else:
372 | if len(selected_engines) > 1:
373 | print(("WARNING: Multiple STT engines found for slug '%s'. " +
374 | "This is most certainly a bug.") % slug)
375 | engine = selected_engines[0]
376 | if not engine.is_available():
377 | raise ValueError(("STT engine '%s' is not available (due to " +
378 | "missing dependencies, missing " +
379 | "dependencies, etc.)") % slug)
380 | return engine
381 |
382 |
383 | def get_engines():
384 | def get_subclasses(cls):
385 | subclasses = set()
386 | for subclass in cls.__subclasses__():
387 | subclasses.add(subclass)
388 | subclasses.update(get_subclasses(subclass))
389 | return subclasses
390 | return [stt_engine for stt_engine in
391 | list(get_subclasses(AbstractSTTEngine))
392 | if hasattr(stt_engine, 'SLUG') and stt_engine.SLUG]
393 |
--------------------------------------------------------------------------------
/client/test_mic.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | """
3 | A drop-in replacement for the Mic class used during unit testing.
4 | Designed to take pre-arranged inputs as an argument and store any
5 | outputs for inspection. Requires a populated profile (profile.yml).
6 | """
7 |
8 |
9 | class Mic:
10 |
11 | def __init__(self, inputs):
12 | self.inputs = inputs
13 | self.idx = 0
14 | self.outputs = []
15 |
16 | def passiveListen(self, PERSONA):
17 | return True, "JASPER"
18 |
19 | def activeListenToAllOptions(self, THRESHOLD=None, LISTEN=True,
20 | MUSIC=False):
21 | return [self.activeListen(THRESHOLD=THRESHOLD, LISTEN=LISTEN,
22 | MUSIC=MUSIC)]
23 |
24 | def activeListen(self, THRESHOLD=None, LISTEN=True, MUSIC=False):
25 | if not LISTEN:
26 | return self.inputs[self.idx - 1]
27 |
28 | input = self.inputs[self.idx]
29 | self.idx += 1
30 | return input
31 |
32 | def say(self, phrase, OPTIONS=None):
33 | self.outputs.append(phrase)
34 |
--------------------------------------------------------------------------------
/client/tts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | """
3 | A Speaker handles audio output from Jasper to the user
4 |
5 | Speaker methods:
6 | say - output 'phrase' as speech
7 | play - play the audio in 'filename'
8 | is_available - returns True if the platform supports this implementation
9 | """
10 | import os
11 | import platform
12 | import tempfile
13 | import subprocess
14 | import pipes
15 | import logging
16 | import urllib
17 | import requests
18 | from abc import ABCMeta, abstractmethod
19 | from uuid import getnode as get_mac # Import for Baidu TTS
20 |
21 | import argparse
22 | import yaml
23 |
24 | from client import diagnose
25 | from client import jasperpath
26 |
27 |
28 | class AbstractTTSEngine(object):
29 | """
30 | Generic parent class for all speakers
31 | """
32 | __metaclass__ = ABCMeta
33 |
34 | @classmethod
35 | def get_config(cls):
36 | return {}
37 |
38 | @classmethod
39 | def get_instance(cls):
40 | config = cls.get_config()
41 | instance = cls(**config)
42 | return instance
43 |
44 | @classmethod
45 | @abstractmethod
46 | def is_available(cls):
47 | return diagnose.check_executable('aplay')
48 |
49 | def __init__(self, **kwargs):
50 | self._logger = logging.getLogger(__name__)
51 |
52 | @abstractmethod
53 | def say(self, phrase, *args):
54 | pass
55 |
56 | def play(self, filename):
57 | # FIXME: Use platform-independent audio-output here
58 | # See issue jasperproject/jasper-client#188
59 | cmd = ['aplay', '-D', 'plughw:1,0', str(filename)]
60 | self._logger.debug('Executing %s', ' '.join([pipes.quote(arg)
61 | for arg in cmd]))
62 | with tempfile.TemporaryFile() as f:
63 | subprocess.call(cmd, stdout=f, stderr=f)
64 | f.seek(0)
65 | output = f.read()
66 | if output:
67 | self._logger.debug("Output was: '%s'", output)
68 |
69 |
70 | class AbstractMp3TTSEngine(AbstractTTSEngine):
71 | """
72 | Generic class that implements the 'play' method for mp3 files
73 | """
74 | @classmethod
75 | def is_available(cls):
76 | return (super(AbstractMp3TTSEngine, cls).is_available() and
77 | diagnose.check_python_import('mad'))
78 |
79 | def play_mp3(self, filename):
80 | with tempfile.NamedTemporaryFile(suffix='.wav') as f:
81 | os.system('sox "%s" "%s"' % (filename, f.name))
82 | self.play(f.name)
83 |
84 |
85 | class DummyTTS(AbstractTTSEngine):
86 | """
87 | Dummy TTS engine that logs phrases with INFO level instead of synthesizing
88 | speech.
89 | """
90 |
91 | SLUG = "dummy-tts"
92 |
93 | @classmethod
94 | def is_available(cls):
95 | return True
96 |
97 | def say(self, phrase):
98 | self._logger.info(phrase)
99 |
100 | def play(self, filename):
101 | self._logger.debug("Playback of file '%s' requested")
102 | pass
103 |
104 |
105 | class BaiduTTS(AbstractMp3TTSEngine):
106 | SLUG = "baidu-tts"
107 |
108 | def __init__(self, api_key='', secret_key='', per=0):
109 | self._logger = logging.getLogger(__name__)
110 | self.api_key = api_key
111 | self.secret_key = secret_key
112 | self.per = per
113 | self.token = ''
114 |
115 | @classmethod
116 | def get_config(cls):
117 | # FIXME: Replace this as soon as we have a config module
118 | config = {}
119 | # Try to get baidu_yuyin config from config
120 | profile_path = jasperpath.config('profile.yml')
121 | if os.path.exists(profile_path):
122 | with open(profile_path, 'r') as f:
123 | profile = yaml.safe_load(f)
124 | if 'baidu_yuyin' in profile:
125 | if 'api_key' in profile['baidu_yuyin']:
126 | config['api_key'] = \
127 | profile['baidu_yuyin']['api_key']
128 | if 'secret_key' in profile['baidu_yuyin']:
129 | config['secret_key'] = \
130 | profile['baidu_yuyin']['secret_key']
131 | if 'per' in profile['baidu_yuyin']:
132 | config['per'] = \
133 | profile['baidu_yuyin']['per']
134 | else:
135 | print("Cannot find config file - %s\n" % profile_path)
136 | return config
137 |
138 | @classmethod
139 | def is_available(cls):
140 | return (super(cls, cls).is_available() and
141 | diagnose.check_network_connection())
142 |
143 | def get_token(self):
144 | URL = 'http://openapi.baidu.com/oauth/2.0/token'
145 | params = urllib.parse.urlencode({'grant_type': 'client_credentials',
146 | 'client_id': self.api_key,
147 | 'client_secret': self.secret_key})
148 | r = requests.get(URL, params=params)
149 | try:
150 | r.raise_for_status()
151 | token = r.json()['access_token']
152 | return token
153 | except requests.exceptions.HTTPError:
154 | self._logger.critical('Token request failed with response: %r',
155 | r.text,
156 | exc_info=True)
157 | return ''
158 |
159 | def split_sentences(self, text):
160 | punctuations = ['.', '。', ';', ';', '\n']
161 | for i in punctuations:
162 | text = text.replace(i, '@@@')
163 | return text.split('@@@')
164 |
165 | def get_speech(self, phrase):
166 | if self.token == '':
167 | self.token = self.get_token()
168 | if self.token == '': # Try once more
169 | self.token = self.get_token()
170 | if self.token == '': # Try again
171 | self.token = self.get_token()
172 | query = {'tex': phrase,
173 | 'lan': 'zh',
174 | 'tok': self.token,
175 | 'ctp': 1,
176 | 'cuid': str(get_mac())[:32],
177 | 'per': self.per
178 | }
179 | r = requests.post('http://tsn.baidu.com/text2audio',
180 | data=query,
181 | headers={'content-type': 'application/json'})
182 | try:
183 | r.raise_for_status()
184 | if r.json()['err_msg'] is not None:
185 | self._logger.critical('Baidu TTS failed with response: %r',
186 | r.json()['err_msg'],
187 | exc_info=True)
188 | return None
189 | except Exception:
190 | pass
191 | with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
192 | f.write(r.content)
193 | tmpfile = f.name
194 | return tmpfile
195 |
196 | def say(self, phrase):
197 | self._logger.debug(u"Saying '%s' with '%s'", phrase, self.SLUG)
198 | tmpfile = self.get_speech(phrase)
199 | if tmpfile is not None:
200 | self.play_mp3(tmpfile)
201 | os.remove(tmpfile)
202 |
203 |
204 | def get_default_engine_slug():
205 | return 'osx-tts' if platform.system().lower() == 'darwin' else 'espeak-tts'
206 |
207 |
208 | def get_engine_by_slug(slug=None):
209 | """
210 | Returns:
211 | A speaker implementation available on the current platform
212 |
213 | Raises:
214 | ValueError if no speaker implementation is supported on this platform
215 | """
216 |
217 | if not slug or type(slug) is not str:
218 | raise TypeError("Invalid slug '%s'", slug)
219 |
220 | selected_filter = filter(lambda engine: hasattr(engine, "SLUG") and
221 | engine.SLUG == slug, get_engines())
222 | selected_engines = [engine for engine in selected_filter]
223 | if len(selected_engines) == 0:
224 | raise ValueError("No TTS engine found for slug '%s'" % slug)
225 | else:
226 | if len(selected_engines) > 1:
227 | print("WARNING: Multiple TTS engines found for slug '%s'. " +
228 | "This is most certainly a bug." % slug)
229 | engine = selected_engines[0]
230 | if not engine.is_available():
231 | raise ValueError(("TTS engine '%s' is not available (due to " +
232 | "missing dependencies, etc.)") % slug)
233 | return engine
234 |
235 |
236 | def get_engines():
237 | def get_subclasses(cls):
238 | subclasses = set()
239 | for subclass in cls.__subclasses__():
240 | subclasses.add(subclass)
241 | subclasses.update(get_subclasses(subclass))
242 | return subclasses
243 | return [tts_engine for tts_engine in
244 | list(get_subclasses(AbstractTTSEngine))
245 | if hasattr(tts_engine, 'SLUG') and tts_engine.SLUG]
246 |
247 |
248 | if __name__ == '__main__':
249 | parser = argparse.ArgumentParser(description='Jasper TTS module')
250 | parser.add_argument('--debug', action='store_true',
251 | help='Show debug messages')
252 | args = parser.parse_args()
253 |
254 | logging.basicConfig()
255 | if args.debug:
256 | logger = logging.getLogger(__name__)
257 | logger.setLevel(logging.DEBUG)
258 |
259 | engines = get_engines()
260 | available_engines = []
261 | for engine in get_engines():
262 | if engine.is_available():
263 | available_engines.append(engine)
264 | disabled_engines = list(set(engines).difference(set(available_engines)))
265 | print("Available TTS engines:")
266 | for i, engine in enumerate(available_engines, start=1):
267 | print("%d. %s" % (i, engine.SLUG))
268 |
269 | print("")
270 | print("Disabled TTS engines:")
271 |
272 | for i, engine in enumerate(disabled_engines, start=1):
273 | print("%d. %s" % (i, engine.SLUG))
274 |
275 | print("")
276 | for i, engine in enumerate(available_engines, start=1):
277 | print("%d. Testing engine '%s'..." % (i, engine.SLUG))
278 | engine.get_instance().say("This is a test.")
279 |
280 | print("Done.")
281 |
--------------------------------------------------------------------------------
/client/vocabcompiler.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8-*-
2 | """
3 | Iterates over all the WORDS variables in the modules and creates a
4 | vocabulary for the respective stt_engine if needed.
5 | """
6 |
7 | import os
8 | import tempfile
9 | import logging
10 | import hashlib
11 | import shutil
12 | from abc import ABCMeta, abstractmethod, abstractproperty
13 |
14 | import client.brain as brain
15 | import client.jasperpath as jasperpath
16 |
17 | from client.g2p import PhonetisaurusG2P
18 | try:
19 | import cmuclmtk
20 | except ImportError:
21 | logging.getLogger(__name__).error("Error importing CMUCLMTK module. " +
22 | "PocketsphinxVocabulary will not work " +
23 | "correctly.", exc_info=True)
24 |
25 |
26 | class AbstractVocabulary(object):
27 | """
28 | Abstract base class for Vocabulary classes.
29 |
30 | Please note that subclasses have to implement the compile_vocabulary()
31 | method and set a string as the PATH_PREFIX class attribute.
32 | """
33 | __metaclass__ = ABCMeta
34 |
35 | @classmethod
36 | def phrases_to_revision(cls, phrases):
37 | """
38 | Calculates a revision from phrases by using the SHA1 hash function.
39 |
40 | Arguments:
41 | phrases -- a list of phrases
42 |
43 | Returns:
44 | A revision string for given phrases.
45 | """
46 | sorted_phrases = sorted(phrases)
47 | joined_phrases = '\n'.join(sorted_phrases)
48 | sha1 = hashlib.sha1()
49 | sha1.update(joined_phrases.encode('ascii'))
50 | return sha1.hexdigest()
51 |
52 | def __init__(self, name='default', path='.'):
53 | """
54 | Initializes a new Vocabulary instance.
55 |
56 | Optional Arguments:
57 | name -- (optional) the name of the vocabulary (Default: 'default')
58 | path -- (optional) the path in which the vocabulary exists or will
59 | be created (Default: '.')
60 | """
61 | self.name = name
62 | self.path = os.path.abspath(os.path.join(path, self.PATH_PREFIX, name))
63 | self._logger = logging.getLogger(__name__)
64 |
65 | @property
66 | def revision_file(self):
67 | """
68 | Returns:
69 | The path of the the revision file as string
70 | """
71 | return os.path.join(self.path, 'revision')
72 |
73 | @abstractproperty
74 | def is_compiled(self):
75 | """
76 | Checks if the vocabulary is compiled by checking if the revision file
77 | is readable. This method should be overridden by subclasses to check
78 | for class-specific additional files, too.
79 |
80 | Returns:
81 | True if the dictionary is compiled, else False
82 | """
83 | return os.access(self.revision_file, os.R_OK)
84 |
85 | @property
86 | def compiled_revision(self):
87 | """
88 | Reads the compiled revision from the revision file.
89 |
90 | Returns:
91 | the revision of this vocabulary (i.e. the string
92 | inside the revision file), or None if is_compiled
93 | if False
94 | """
95 | if not self.is_compiled:
96 | return None
97 | with open(self.revision_file, 'r') as f:
98 | revision = f.read().strip()
99 | self._logger.debug("compiled_revision is '%s'", revision)
100 | return revision
101 |
102 | def matches_phrases(self, phrases):
103 | """
104 | Convenience method to check if this vocabulary exactly contains the
105 | phrases passed to this method.
106 |
107 | Arguments:
108 | phrases -- a list of phrases
109 |
110 | Returns:
111 | True if phrases exactly matches the phrases inside this
112 | vocabulary.
113 |
114 | """
115 | return (self.compiled_revision == self.phrases_to_revision(phrases))
116 |
117 | def compile(self, phrases, force=False):
118 | """
119 | Compiles this vocabulary. If the force argument is True, compilation
120 | will be forced regardless of necessity (which means that the
121 | preliminary check if the current revision already equals the
122 | revision after compilation will be skipped).
123 | This method is not meant to be overridden by subclasses - use the
124 | _compile_vocabulary()-method instead.
125 |
126 | Arguments:
127 | phrases -- a list of phrases that this vocabulary will contain
128 | force -- (optional) forces compilation (Default: False)
129 |
130 | Returns:
131 | The revision of the compiled vocabulary
132 | """
133 | revision = self.phrases_to_revision(phrases)
134 | if not force and self.compiled_revision == revision:
135 | self._logger.debug('Compilation not neccessary, compiled ' +
136 | 'version matches phrases.')
137 | return revision
138 |
139 | if not os.path.exists(self.path):
140 | self._logger.debug("Vocabulary dir '%s' does not exist, " +
141 | "creating...", self.path)
142 | try:
143 | os.makedirs(self.path)
144 | except OSError:
145 | self._logger.error("Couldn't create vocabulary dir '%s'",
146 | self.path, exc_info=True)
147 | raise
148 | try:
149 | with open(self.revision_file, 'w') as f:
150 | f.write(revision)
151 | except (OSError, IOError):
152 | self._logger.error("Couldn't write revision file in '%s'",
153 | self.revision_file, exc_info=True)
154 | raise
155 | else:
156 | self._logger.info('Starting compilation...')
157 | try:
158 | self._compile_vocabulary(phrases)
159 | except Exception as e:
160 | self._logger.error("Fatal compilation Error occured, " +
161 | "cleaning up...", exc_info=True)
162 | try:
163 | os.remove(self.revision_file)
164 | except OSError:
165 | pass
166 | raise e
167 | else:
168 | self._logger.info('Compilation done.')
169 | return revision
170 |
171 | @abstractmethod
172 | def _compile_vocabulary(self, phrases):
173 | """
174 | Abstract method that should be overridden in subclasses with custom
175 | compilation code.
176 |
177 | Arguments:
178 | phrases -- a list of phrases that this vocabulary will contain
179 | """
180 |
181 |
182 | class DummyVocabulary(AbstractVocabulary):
183 |
184 | PATH_PREFIX = 'dummy-vocabulary'
185 |
186 | @property
187 | def is_compiled(self):
188 | """
189 | Checks if the vocabulary is compiled by checking if the revision
190 | file is readable.
191 |
192 | Returns:
193 | True if this vocabulary has been compiled, else False
194 | """
195 | return super(self.__class__, self).is_compiled
196 |
197 | def _compile_vocabulary(self, phrases):
198 | """
199 | Does nothing (because this is a dummy class for testing purposes).
200 | """
201 | pass
202 |
203 |
204 | class PocketsphinxVocabulary(AbstractVocabulary):
205 |
206 | PATH_PREFIX = 'pocketsphinx-vocabulary'
207 |
208 | @property
209 | def languagemodel_file(self):
210 | """
211 | Returns:
212 | The path of the the pocketsphinx languagemodel file as string
213 | """
214 | return os.path.join(self.path, 'languagemodel')
215 |
216 | @property
217 | def dictionary_file(self):
218 | """
219 | Returns:
220 | The path of the pocketsphinx dictionary file as string
221 | """
222 | return os.path.join(self.path, 'dictionary')
223 |
224 | @property
225 | def is_compiled(self):
226 | """
227 | Checks if the vocabulary is compiled by checking if the revision,
228 | languagemodel and dictionary files are readable.
229 |
230 | Returns:
231 | True if this vocabulary has been compiled, else False
232 | """
233 | return (super(self.__class__, self).is_compiled and
234 | os.access(self.languagemodel_file, os.R_OK) and
235 | os.access(self.dictionary_file, os.R_OK))
236 |
237 | @property
238 | def decoder_kwargs(self):
239 | """
240 | Convenience property to use this Vocabulary with the __init__() method
241 | of the pocketsphinx.Decoder class.
242 |
243 | Returns:
244 | A dict containing kwargs for the pocketsphinx.Decoder.__init__()
245 | method.
246 |
247 | Example:
248 | decoder = pocketsphinx.Decoder(**vocab_instance.decoder_kwargs,
249 | hmm='/path/to/hmm')
250 |
251 | """
252 | return {'lm': self.languagemodel_file, 'dict': self.dictionary_file}
253 |
254 | def _compile_vocabulary(self, phrases):
255 | """
256 | Compiles the vocabulary to the Pocketsphinx format by creating a
257 | languagemodel and a dictionary.
258 |
259 | Arguments:
260 | phrases -- a list of phrases that this vocabulary will contain
261 | """
262 | text = " ".join([(" %s " % phrase) for phrase in phrases])
263 | self._logger.debug('Compiling languagemodel...')
264 | vocabulary = self._compile_languagemodel(text, self.languagemodel_file)
265 | self._logger.debug('Starting dictionary...')
266 | self._compile_dictionary(vocabulary, self.dictionary_file)
267 |
268 | def _compile_languagemodel(self, text, output_file):
269 | """
270 | Compiles the languagemodel from a text.
271 |
272 | Arguments:
273 | text -- the text the languagemodel will be generated from
274 | output_file -- the path of the file this languagemodel will
275 | be written to
276 |
277 | Returns:
278 | A list of all unique words this vocabulary contains.
279 | """
280 | with tempfile.NamedTemporaryFile(suffix='.vocab', delete=False) as f:
281 | vocab_file = f.name
282 |
283 | # Create vocab file from text
284 | self._logger.debug("Creating vocab file: '%s'", vocab_file)
285 | cmuclmtk.text2vocab(text, vocab_file)
286 |
287 | # Create language model from text
288 | self._logger.debug("Creating languagemodel file: '%s'", output_file)
289 | cmuclmtk.text2lm(text, output_file, vocab_file=vocab_file)
290 |
291 | # Get words from vocab file
292 | self._logger.debug("Getting words from vocab file and removing it " +
293 | "afterwards...")
294 | words = []
295 | with open(vocab_file, 'r') as f:
296 | for line in f:
297 | line = line.strip()
298 | if not line.startswith('#') and line not in ('', ''):
299 | words.append(line)
300 | os.remove(vocab_file)
301 |
302 | return words
303 |
304 | def _compile_dictionary(self, words, output_file):
305 | """
306 | Compiles the dictionary from a list of words.
307 |
308 | Arguments:
309 | words -- a list of all unique words this vocabulary contains
310 | output_file -- the path of the file this dictionary will
311 | be written to
312 | """
313 | # create the dictionary
314 | self._logger.debug("Getting phonemes for %d words...", len(words))
315 | g2pconverter = PhonetisaurusG2P(**PhonetisaurusG2P.get_config())
316 | phonemes = g2pconverter.translate(words)
317 |
318 | self._logger.debug("Creating dict file: '%s'", output_file)
319 | with open(output_file, "w") as f:
320 | for word, pronounciations in phonemes.items():
321 | for i, pronounciation in enumerate(pronounciations, start=1):
322 | if i == 1:
323 | line = "%s\t%s\n" % (word, pronounciation)
324 | else:
325 | line = "%s(%d)\t%s\n" % (word, i, pronounciation)
326 | f.write(line)
327 |
328 |
329 | def get_phrases_from_module(module):
330 | """
331 | Gets phrases from a module.
332 |
333 | Arguments:
334 | module -- a module reference
335 |
336 | Returns:
337 | The list of phrases in this module.
338 | """
339 | return module.WORDS if hasattr(module, 'WORDS') else []
340 |
341 |
342 | def get_keyword_phrases():
343 | """
344 | Gets the keyword phrases from the keywords file in the jasper data dir.
345 |
346 | Returns:
347 | A list of keyword phrases.
348 | """
349 | phrases = []
350 |
351 | with open(jasperpath.data('keyword_phrases'), mode="r") as f:
352 | for line in f:
353 | phrase = line.strip()
354 | if phrase:
355 | phrases.append(phrase)
356 |
357 | return phrases
358 |
359 |
360 | def get_all_phrases():
361 | """
362 | Gets phrases from all modules.
363 |
364 | Returns:
365 | A list of phrases in all modules plus additional phrases passed to this
366 | function.
367 | """
368 | phrases = []
369 |
370 | modules = brain.Brain.get_modules()
371 | for module in modules:
372 | phrases.extend(get_phrases_from_module(module))
373 |
374 | return sorted(list(set(phrases)))
375 |
376 |
377 | if __name__ == '__main__':
378 | import argparse
379 |
380 | parser = argparse.ArgumentParser(description='Vocabcompiler Demo')
381 | parser.add_argument('--base-dir', action='store',
382 | help='the directory in which the vocabulary will be ' +
383 | 'compiled.')
384 | parser.add_argument('--debug', action='store_true',
385 | help='show debug messages')
386 | args = parser.parse_args()
387 |
388 | logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
389 | base_dir = args.base_dir if args.base_dir else tempfile.mkdtemp()
390 |
391 | phrases = get_all_phrases()
392 | print("Module phrases: %r" % phrases)
393 |
394 | for subclass in AbstractVocabulary.__subclasses__():
395 | if hasattr(subclass, 'PATH_PREFIX'):
396 | vocab = subclass(path=base_dir)
397 | print("Vocabulary in: %s" % vocab.path)
398 | print("Revision file: %s" % vocab.revision_file)
399 | print("Compiled revision: %s" % vocab.compiled_revision)
400 | print("Is compiled: %r" % vocab.is_compiled)
401 | print("Matches phrases: %r" % vocab.matches_phrases(phrases))
402 | if not vocab.is_compiled or not vocab.matches_phrases(phrases):
403 | print("Compiling...")
404 | vocab.compile(phrases)
405 | print("")
406 | print("Vocabulary in: %s" % vocab.path)
407 | print("Revision file: %s" % vocab.revision_file)
408 | print("Compiled revision: %s" % vocab.compiled_revision)
409 | print("Is compiled: %r" % vocab.is_compiled)
410 | print("Matches phrases: %r" % vocab.matches_phrases(phrases))
411 | print("")
412 | if not args.base_dir:
413 | print("Removing temporary directory '%s'..." % base_dir)
414 | shutil.rmtree(base_dir)
415 |
--------------------------------------------------------------------------------
/conf/profile.yml:
--------------------------------------------------------------------------------
1 | carrier: ''
2 | first_name: Tommy
3 | gmail_password: ''
4 | last_name: Wang
5 | location: Beijing
6 | phone_number: ''
7 | prefers_email: false
8 |
9 | stt_engine: baidu-stt
10 | tts_engine: baidu-tts
11 | stt_passive_engine: sphinx
12 |
13 | pocketsphinx:
14 | fst_model: '/home/pi/g014b2b.fst'
15 | hmm_dir: '/usr/local/share/pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k'
16 |
17 | baidu_yuyin:
18 | api_key: D4C4GcF1fwqqhPaGPuXSMLC8
19 | secret_key: 95ea4357542d01a490559500b85a1eff
20 |
--------------------------------------------------------------------------------
/jasper.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8-*-
3 |
4 | import os
5 | import shutil
6 | import logging
7 |
8 | import yaml
9 | import argparse
10 |
11 | from client import tts
12 | from client import stt
13 | from client import jasperpath
14 | from client import diagnose
15 | from client.conversation import Conversation
16 | import sys
17 |
18 | # Add jasperpath.LIB_PATH to sys.path
19 | sys.path.append(jasperpath.LIB_PATH)
20 |
21 | parser = argparse.ArgumentParser(description='Jasper Voice Control Center')
22 | parser.add_argument('--local', action='store_true',
23 | help='Use text input instead of a real microphone')
24 | parser.add_argument('--no-network-check', action='store_true',
25 | help='Disable the network connection check')
26 | parser.add_argument('--diagnose', action='store_true',
27 | help='Run diagnose and exit')
28 | parser.add_argument('--debug', action='store_true', help='Show debug messages')
29 | args = parser.parse_args()
30 |
31 | if args.local:
32 | from client.local_mic import Mic
33 | else:
34 | from client.mic import Mic
35 |
36 |
37 | class Jasper(object):
38 | def __init__(self):
39 | self._logger = logging.getLogger(__name__)
40 |
41 | # Create config dir if it does not exist yet
42 | if not os.path.exists(jasperpath.CONFIG_PATH):
43 | try:
44 | os.makedirs(jasperpath.CONFIG_PATH)
45 | except OSError:
46 | self._logger.error("Could not create config dir: '%s'",
47 | jasperpath.CONFIG_PATH, exc_info=True)
48 | raise
49 |
50 | # Check if config dir is writable
51 | if not os.access(jasperpath.CONFIG_PATH, os.W_OK):
52 | self._logger.critical("Config dir %s is not writable. Jasper " +
53 | "won't work correctly.",
54 | jasperpath.CONFIG_PATH)
55 |
56 | # FIXME: For backwards compatibility, move old config file to newly
57 | # created config dir
58 | old_configfile = os.path.join(jasperpath.LIB_PATH, 'profile.yml')
59 | new_configfile = jasperpath.config('profile.yml')
60 | if os.path.exists(old_configfile):
61 | if os.path.exists(new_configfile):
62 | self._logger.warning("Deprecated profile file found: '%s'. " +
63 | "Please remove it.", old_configfile)
64 | else:
65 | self._logger.warning("Deprecated profile file found: '%s'. " +
66 | "Trying to copy it to new location '%s'.",
67 | old_configfile, new_configfile)
68 | try:
69 | shutil.copy2(old_configfile, new_configfile)
70 | except shutil.Error:
71 | self._logger.error("Unable to copy config file. " +
72 | "Please copy it manually.",
73 | exc_info=True)
74 | raise
75 |
76 | # Read config
77 | self._logger.debug("Trying to read config file: '%s'", new_configfile)
78 | try:
79 | with open(new_configfile, "r") as f:
80 | self.config = yaml.safe_load(f)
81 | except OSError:
82 | self._logger.error("Can't open config file: '%s'", new_configfile)
83 | raise
84 |
85 | try:
86 | stt_engine_slug = self.config['stt_engine']
87 | except KeyError:
88 | stt_engine_slug = 'sphinx'
89 | logger.warning("stt_engine not specified in profile, defaulting " +
90 | "to '%s'", stt_engine_slug)
91 | stt_engine_class = stt.get_engine_by_slug(stt_engine_slug)
92 |
93 | try:
94 | slug = self.config['stt_passive_engine']
95 | stt_passive_engine_class = stt.get_engine_by_slug(slug)
96 | except KeyError:
97 | stt_passive_engine_class = stt_engine_class
98 |
99 | try:
100 | tts_engine_slug = self.config['tts_engine']
101 | except KeyError:
102 | tts_engine_slug = tts.get_default_engine_slug()
103 | logger.warning("tts_engine not specified in profile, defaulting " +
104 | "to '%s'", tts_engine_slug)
105 | tts_engine_class = tts.get_engine_by_slug(tts_engine_slug)
106 |
107 | # Initialize Mic
108 | self.mic = Mic(tts_engine_class.get_instance(),
109 | stt_passive_engine_class.get_passive_instance(),
110 | stt_engine_class.get_active_instance())
111 |
112 | def run(self):
113 | if 'first_name' in self.config:
114 | salutation = ("%s 很高兴为你服务"
115 | % self.config["first_name"])
116 | else:
117 | salutation = "很高兴为你服务"
118 | if os.path.exists(jasperpath.tjbot('shakehand.servo.js')):
119 | os.system("node " + jasperpath.tjbot('shakehand.servo.js'))
120 | self.mic.say(salutation)
121 |
122 | conversation = Conversation(jasperpath.hotword(),
123 | self.mic, self.config)
124 | conversation.handleForever()
125 |
126 |
127 | if __name__ == "__main__":
128 |
129 | print("*******************************************************")
130 | print("* LAIBOT - THE TALKING COMPUTER *")
131 | print("* (c) 2017 Thomas J.J Wang *")
132 | print("* (c) 2015 Shubhro Saha, Charlie Marsh & Jan Holthuis *")
133 | print("*******************************************************")
134 |
135 | logging.basicConfig()
136 | logger = logging.getLogger()
137 | logger.getChild("client.stt").setLevel(logging.INFO)
138 |
139 | if args.debug:
140 | logger.setLevel(logging.DEBUG)
141 |
142 | if not args.no_network_check and not diagnose.check_network_connection():
143 | logger.warning("Network not connected. This may prevent Jasper from " +
144 | "running properly.")
145 |
146 | if args.diagnose:
147 | failed_checks = diagnose.run()
148 | sys.exit(0 if not failed_checks else 1)
149 |
150 | try:
151 | app = Jasper()
152 | except Exception:
153 | logger.error("Error occured!", exc_info=True)
154 | sys.exit(1)
155 |
156 | app.run()
157 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytz==2017.3
2 | mock==2.0.0
3 | cmuclmtk==0.1.5
4 | feedparser==5.2.1
5 | requests==2.4.3
6 | pip==1.5.6
7 | gtts==1.2.2
8 | mad==0.2.2
9 | pyvona==1.2.1
10 | PyYAML==3.12
11 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | BASE_DIR=$(cd `dirname $0`; pwd)
3 | TJBOT_DIR=$BASE_DIR/../tjbot
4 |
5 | if [ -x $TJBOT_DIR ]; then
6 | cd $TJBOT_DIR/bootstrap/tests
7 | echo "Installing support libraries for TJBot. This may take few minutes."
8 | npm install > install.log 2>&1
9 |
10 | cd $BASE_DIR
11 | fi
12 |
13 | python3 jasper.py --debug
14 |
--------------------------------------------------------------------------------
/static/audio/beep_hi.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/static/audio/beep_hi.wav
--------------------------------------------------------------------------------
/static/audio/beep_lo.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/static/audio/beep_lo.wav
--------------------------------------------------------------------------------
/static/audio/jasper.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/static/audio/jasper.wav
--------------------------------------------------------------------------------
/static/audio/say.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/static/audio/say.wav
--------------------------------------------------------------------------------
/static/audio/time.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/static/audio/time.wav
--------------------------------------------------------------------------------
/static/audio/weather_zh.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/static/audio/weather_zh.wav
--------------------------------------------------------------------------------
/static/dictionary_persona.dic:
--------------------------------------------------------------------------------
1 | BE B IY
2 | DID D IH D
3 | FIRST F ER S T
4 | IN IH N
5 | JASPER JH AE S P ER
6 | NOW N AW
7 | OKEY OW K IY
8 | RIGHT R AY T
9 | SAY S EY
10 | TOMMY T AA M IY
11 | WHAT W AH T
12 | WHAT(2) HH W AH T
13 |
14 |
--------------------------------------------------------------------------------
/static/keyword_phrases:
--------------------------------------------------------------------------------
1 | BE
2 | DID
3 | FIRST
4 | IN
5 | OKEY
6 | TOMMY
7 | JASPER
8 | NOW
9 | RIGHT
10 | SAY
11 | WHAT
12 | OKEY TOMMY
13 | OKEY TOMMY
14 | OKEY TOMMY
15 | OKEY TOMMY
16 | OKEY TOMMY
17 | OKEY TOMMY
18 | OKEY TOMMY
19 | OKEY TOMMY
20 | OKEY TOMMY
21 | OKEY TOMMY
22 |
23 |
--------------------------------------------------------------------------------
/static/languagemodel_persona.lm:
--------------------------------------------------------------------------------
1 | Language model created by QuickLM on Sun Nov 19 06:58:16 EST 2017
2 | Copyright (c) 1996-2010 Carnegie Mellon University and Alexander I. Rudnicky
3 |
4 | The model is in standard ARPA format, designed by Doug Paul while he was at MITRE.
5 |
6 | The code that was used to produce this language model is available in Open Source.
7 | Please visit http://www.speech.cs.cmu.edu/tools/ for more information
8 |
9 | The (fixed) discount mass is 0.5. The backoffs are computed using the ratio method.
10 | This model based on a corpus of 21 sentences and 13 words
11 |
12 | \data\
13 | ngram 1=13
14 | ngram 2=23
15 | ngram 3=13
16 |
17 | \1-grams:
18 | -0.8421 -0.3010
19 | -0.8421 -0.1974
20 | -2.1644 BE -0.2336
21 | -2.1644 DID -0.2336
22 | -2.1644 FIRST -0.2336
23 | -2.1644 IN -0.2336
24 | -2.1644 JASPER -0.2336
25 | -2.1644 NOW -0.2336
26 | -1.1230 OKEY -0.1936
27 | -2.1644 RIGHT -0.2336
28 | -2.1644 SAY -0.2336
29 | -1.1230 TOMMY -0.2336
30 | -2.1644 WHAT -0.2336
31 |
32 | \2-grams:
33 | -1.6232 BE 0.0000
34 | -1.6232 DID 0.0000
35 | -1.6232 FIRST 0.0000
36 | -1.6232 IN 0.0000
37 | -1.6232 JASPER 0.0000
38 | -1.6232 NOW 0.0000
39 | -0.5819 OKEY 0.0000
40 | -1.6232 RIGHT 0.0000
41 | -1.6232 SAY 0.0000
42 | -1.6232 TOMMY 0.0000
43 | -1.6232 WHAT 0.0000
44 | -0.3010 BE -0.3010
45 | -0.3010 DID -0.3010
46 | -0.3010 FIRST -0.3010
47 | -0.3010 IN -0.3010
48 | -0.3010 JASPER -0.3010
49 | -0.3010 NOW -0.3010
50 | -1.3424 OKEY -0.3010
51 | -0.3424 OKEY TOMMY 0.0000
52 | -0.3010 RIGHT -0.3010
53 | -0.3010 SAY -0.3010
54 | -0.3010 TOMMY -0.3010
55 | -0.3010 WHAT -0.3010
56 |
57 | \3-grams:
58 | -0.3010 BE
59 | -0.3010 DID
60 | -0.3010 FIRST
61 | -0.3010 IN
62 | -0.3010 JASPER
63 | -0.3010 NOW
64 | -1.3424 OKEY
65 | -0.3424 OKEY TOMMY
66 | -0.3010 RIGHT
67 | -0.3010 SAY
68 | -0.3010 TOMMY
69 | -0.3010 WHAT
70 | -0.3010 OKEY TOMMY
71 |
72 | \end\
73 |
74 |
--------------------------------------------------------------------------------
/static/text/JOKES.txt:
--------------------------------------------------------------------------------
1 | little old lady
2 | wow... I didn't know you could yodel... get it... because it sounded like you were yodeling
3 |
4 | oink oink
5 | make up your mind... are you a pig or an owl... get it... because an owl goes who but a pig goes oink... oink
6 |
7 | cows go
8 | no... cows go moo... didn't you know
9 |
10 | jarvis
11 | jarvis... it's me you idiot
12 |
13 | me
14 | no... seriously... it's just me... I was telling a knock knock joke ha ha ha
15 |
16 | madam
17 | my damn foot got stuck in the door, so open it he he he... but actually... I think I hurt my foot
18 |
19 | doris
20 | door is locked... that's why I'm knocking, you idiot
21 |
22 | cash
23 | no thanks... but I would like a peanut instead... get it? because you said cashew... ha ha ha
24 |
25 | orange
26 | orange you glad I am your friend
27 |
28 | alex
29 | I'll ask the questions around here, thank you
30 |
31 | viper
32 | viper nose... it's running
33 |
34 | canoe
35 | can you scratch my back... it itches
36 |
37 | pete
38 | pizza delivery guy... you idiot
39 |
40 | doctor
41 | that is the best show ever
42 |
43 | arizona
44 | arizona room for only one of us in this room
45 |
46 | spider
47 | in spider what everyone says, I still feel like a human
48 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jjwang/laibot-client/cc4a07617dd5316f32d391eba7e2e51ad01925bd/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_brain.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8-*-
3 | import unittest
4 | import mock
5 | from client import brain, test_mic
6 |
7 |
8 | DEFAULT_PROFILE = {
9 | 'prefers_email': False,
10 | 'location': 'Cape Town',
11 | 'timezone': 'US/Eastern',
12 | 'phone_number': '012344321'
13 | }
14 |
15 |
16 | class TestBrain(unittest.TestCase):
17 |
18 | @staticmethod
19 | def _emptyBrain():
20 | mic = test_mic.Mic([])
21 | profile = DEFAULT_PROFILE
22 | return brain.Brain(mic, profile)
23 |
24 | def testLog(self):
25 | """Does Brain correctly log errors when raised by modules?"""
26 | my_brain = TestBrain._emptyBrain()
27 | unclear = my_brain.modules[-1]
28 | with mock.patch.object(unclear, 'handle') as mocked_handle:
29 | with mock.patch.object(my_brain._logger, 'error') as mocked_log:
30 | mocked_handle.side_effect = KeyError('foo')
31 | my_brain.query("zzz gibberish zzz")
32 | self.assertTrue(mocked_log.called)
33 |
--------------------------------------------------------------------------------
/tests/test_diagnose.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8-*-
3 | import unittest
4 | from client import diagnose
5 |
6 |
7 | class TestDiagnose(unittest.TestCase):
8 | def testPythonImportCheck(self):
9 | # This a python stdlib module that definitely exists
10 | self.assertTrue(diagnose.check_python_import("os"))
11 | # I sincerly hope nobody will ever create a package with that name
12 | self.assertFalse(diagnose.check_python_import("nonexistant_package"))
13 |
--------------------------------------------------------------------------------
/tests/test_modules.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8-*-
3 | import unittest
4 | from client import test_mic
5 | from client.modules import Unclear
6 |
7 | DEFAULT_PROFILE = {
8 | 'prefers_email': False,
9 | 'location': 'Cape Town',
10 | 'timezone': 'US/Eastern',
11 | 'phone_number': '012344321'
12 | }
13 |
14 |
15 | class TestModules(unittest.TestCase):
16 |
17 | def setUp(self):
18 | self.profile = DEFAULT_PROFILE
19 | self.send = False
20 |
21 | def runConversation(self, query, inputs, module):
22 | """Generic method for spoofing conversation.
23 |
24 | Arguments:
25 | query -- The initial input to the server.
26 | inputs -- Additional input, if conversation is extended.
27 |
28 | Returns:
29 | The server's responses, in a list.
30 | """
31 | self.assertTrue(module.isValid(query))
32 | mic = test_mic.Mic(inputs)
33 | module.handle(query, mic, self.profile)
34 | return mic.outputs
35 |
36 | def testUnclear(self):
37 | query = "What time is it?"
38 | inputs = []
39 | self.runConversation(query, inputs, Unclear)
40 |
--------------------------------------------------------------------------------