diff --git a/jasper/application.py b/jasper/application.py index 2b1dbdf3c..273d49c04 100644 --- a/jasper/application.py +++ b/jasper/application.py @@ -12,6 +12,7 @@ from . import conversation from . import mic from . import local_mic +from . import restapi class Jasper(object): @@ -221,6 +222,10 @@ def __init__(self, use_local_mic=False): self.conversation = conversation.Conversation( self.mic, self.brain, self.config) + # Initialize RESTful API + self.restapi = restapi.RestAPI(self.config, self.mic, + self.conversation) + def list_plugins(self): plugins = self.plugins.get_plugins() len_name = max(len(info.name) for info in plugins) diff --git a/jasper/conversation.py b/jasper/conversation.py index 42bd486d7..964b82821 100644 --- a/jasper/conversation.py +++ b/jasper/conversation.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import logging +from time import sleep from . import paths from . import i18n # from notifier import Notifier @@ -16,6 +17,7 @@ def __init__(self, mic, brain, profile): self.translations = { } + self.suspended = False # self.notifier = Notifier(profile) def greet(self): @@ -26,6 +28,28 @@ def greet(self): salutation = self.gettext("How can I be of service?") self.mic.say(salutation) + def handleInput(self, input): + if input: + plugin, text = self.brain.query(input) + if plugin and text: + try: + plugin.handle(text, self.mic) + except Exception: + self._logger.error('Failed to execute module', + exc_info=True) + self.mic.say(self.gettext( + "I'm sorry. I had some trouble with that " + + "operation. Please try again later.")) + else: + self._logger.debug("Handling of phrase '%s' by " + + "module '%s' completed", text, + plugin.info.name) + return True + else: + self.mic.say(self.gettext("Pardon?")) + + return False + def handleForever(self): """ Delegates user input to the handling function when activated. @@ -37,22 +61,26 @@ def handleForever(self): for notif in notifications: self._logger.info("Received notification: '%s'", str(notif))""" - input = self.mic.listen() - - if input: - plugin, text = self.brain.query(input) - if plugin and text: - try: - plugin.handle(text, self.mic) - except Exception: - self._logger.error('Failed to execute module', - exc_info=True) - self.mic.say(self.gettext( - "I'm sorry. I had some trouble with that " + - "operation. Please try again later.")) - else: - self._logger.debug("Handling of phrase '%s' by " + - "module '%s' completed", text, - plugin.info.name) - else: - self.mic.say(self.gettext("Pardon?")) + if not self.suspended: + input = self.mic.listen() + + if not self.suspended: + self.handleInput(input) + + if self.suspended: + sleep(0.25) + + def suspend(self): + """ + Suspends converstation handling + """ + self._logger.debug('Suspending handling conversation.') + self.suspended = True + self.mic.cancel_listen() + + def resume(self): + """ + Resumes converstation handling + """ + self._logger.debug('Resuming handling conversation.') + self.suspended = False diff --git a/jasper/mic.py b/jasper/mic.py index cb27d0408..4248e9938 100644 --- a/jasper/mic.py +++ b/jasper/mic.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from . import alteration +from . import paths import logging import tempfile import wave @@ -13,9 +15,6 @@ else: import queue -from . import alteration -from . import paths - def get_config_value(config, name, default): logger = logging.getLogger(__name__) @@ -74,6 +73,7 @@ def __init__(self, input_device, output_device, 'yes' if self._output_padding else 'no') self._threshold = 2.0**self._input_bits + self.cancelListen = False @contextlib.contextmanager def special_mode(self, name, phrases): @@ -150,7 +150,7 @@ def wait_for_keyword(self, keyword=None): self._input_rate): if keyword_uttered.is_set(): self._logger.info("Keyword %s has been uttered", keyword) - return + return True frames.append(frame) if not recording: snr = self._snr([frame]) @@ -185,10 +185,18 @@ def wait_for_keyword(self, keyword=None): frame_queue.put(tuple(recording_frames)) self._threshold = float( audioop.rms(b"".join(frames), 2)) + if self.cancelListen: + return False def listen(self): - self.wait_for_keyword(self._keyword) - return self.active_listen() + self.cancelListen = False + if self.wait_for_keyword(self._keyword): + return self.active_listen() + else: + return False + + def cancel_listen(self): + self.cancelListen = True def active_listen(self, timeout=3): # record until second of silence or double . diff --git a/jasper/pluginstore.py b/jasper/pluginstore.py index ed52e0e4d..4d8000258 100644 --- a/jasper/pluginstore.py +++ b/jasper/pluginstore.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from . import i18n +from . import plugin import os import logging import imp @@ -9,9 +11,6 @@ else: import configparser -from . import i18n -from . import plugin - MANDATORY_OPTIONS = ( ('Plugin', 'Name'), diff --git a/jasper/restapi.py b/jasper/restapi.py new file mode 100644 index 000000000..254b7792b --- /dev/null +++ b/jasper/restapi.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from flask import Flask, jsonify, request, Response, abort +import threading +from functools import wraps + + +class RestAPI(object): + + def __init__(self, profile, mic, conversation): + self.profile = profile + self.mic = mic + self.conversation = conversation + + try: + host = self.profile['restapi']['Host'] + except KeyError: + host = '127.0.0.1' + + try: + port = self.profile['restapi']['Port'] + except KeyError: + port = 5000 + + try: + password = self.profile['restapi']['Password'] + except KeyError: + password = None + + # create thread for http listener + t = threading.Thread(target=self.startRestAPI, + args=(host, port, password)) + t.daemon = True + t.start() + + def startRestAPI(self, host, port, password): + app = Flask(__name__) + + def requires_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if password and (not auth or auth.password != password): + return Response( + 'Authorization required.', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + return f(*args, **kwargs) + return decorated + + @app.route('/') + def index(): + return "Jasper restAPI: running" + + @app.route('/jasper/say', methods=['POST']) + @requires_auth + def say_task(): + if not request.json or 'text' not in request.json: + abort(400) + text = request.json['text'] + + self.conversation.suspend() + self.mic.say(text) + self.conversation.resume() + + return jsonify({'say': text}), 201 + + @app.route('/jasper/transcribe', methods=['GET']) + @requires_auth + def transcribe_task(): + self.conversation.suspend() + transcribed = self.mic.active_listen() + self.conversation.resume() + + return jsonify({'transcribed': transcribed}), 201 + + @app.route('/jasper/activate', methods=['GET']) + @requires_auth + def activate_task(): + self.conversation.suspend() + transcribed = self.mic.active_listen() + result = self.conversation.handleInput(transcribed) + self.conversation.resume() + + return jsonify({'transcribed': transcribed, 'result': result}), 201 + + @app.route('/jasper/handleinput', methods=['POST']) + @requires_auth + def handleinput_task(): + if not request.json or 'text' not in request.json: + abort(400) + text = request.json['text'] + + self.conversation.suspend() + result = self.conversation.handleInput([text]) + self.conversation.resume() + + return jsonify({'text': text, 'result': result}), 201 + + @app.route('/jasper/waitforkeyword', methods=['POST']) + @requires_auth + def waitforkeyword_task(): + if not request.json or 'keyword' not in request.json: + abort(400) + keyword = request.json['keyword'] + + self.conversation.suspend() + self.mic.wait_for_keyword(keyword) + self.conversation.resume() + + return jsonify({'keyword': keyword}), 201 + + @app.route('/jasper/playfile', methods=['POST']) + @requires_auth + def playfile_task(): + if not request.json or 'filename' not in request.json: + abort(400) + filename = request.json['filename'] + + self.conversation.suspend() + self.mic.play_file(filename) + self.conversation.resume() + + return jsonify({'filename': filename}), 201 + + # start http listener + app.run(host=host, port=port, debug=False) diff --git a/requirements.txt b/requirements.txt index 341da90fa..b7ea64a34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ python-slugify==0.1.0 pytz==2014.10 PyYAML==3.11 requests==2.5.0 +Flask==0.10.1 # Pocketsphinx STT engine cmuclmtk==0.1.5