├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── avs ├── __init__.py ├── alexa.py ├── auth.py ├── check.py ├── config.py ├── interface │ ├── __init__.py │ ├── alerts.py │ ├── audio_player.py │ ├── notifications.py │ ├── playback_controller.py │ ├── speaker.py │ ├── speech_recognizer.py │ ├── speech_synthesizer.py │ └── system.py ├── main.py ├── mic │ ├── __init__.py │ ├── alsa_recorder.py │ └── pyaudio_recorder.py ├── player │ ├── __init__.py │ ├── gstreamer_player.py │ ├── mpg123_player.py │ └── mpv_player.py └── resources │ ├── README.md │ ├── alarm.mp3 │ └── web │ └── index.html ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_avs.py └── tox.ini /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Python Alexa Voice Service version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated and will overwrite each time you run travis_pypi_setup.py 2 | deploy: 3 | true: 4 | python: 2.7 5 | repo: respeaker/avs 6 | tags: true 7 | distributions: sdist bdist_wheel 8 | password: 9 | secure: F1qdw8lUDQJpXvoAH/8nP43exXx6wnUgJR0FPfmnDi30iJpyjK/sr05xKcHJ+RNHuyzs82p9c3oR+f8IT2OC2VmKb18F/9VDiGu5YHEi+mEzJoJKQmsnnaZYe0gWyi3DLuwwO3am4Pnia83UBfBStWpecWz00nCw69AMtXTljUb2xwpypCw3bYvkuKTp36umHLVsWDc8zitLgJnYWYHYralInunFZuwl+FV0nB0KBgaenHEhHd5R+pfBNDc6Qhe9JKEiPq8gunMNj3h86q1hksWN7peGNeJ6ynrFEsAZwrjeyJPzDeA/dx4DvsMbcdy6/JIj9RcoBXDsJfKb17El86JEbSGWA6LX+tZlc5Hdp0zurYs4Vb9T8uu5omp4IfHvhqNTlCMSYQRUKtriGoj/VBdB6ZG/RIoTkKAF2IOW9Ww/GDi5b507V7/ZuyVOnq9pnk4QeayPZvzCswRmsJL0BTXAJK3uN0JMNq/yhpjofWqAqZPG7FBrZJjwGgULk5oqkCgGmi2xqg2Kte5wpUgvo9tUOaC59pahyHulTwA3mgnPKld7pWJBixHyAxEvrDvLjitVNpuEXYfFq7sINqKMJURnW+C1Eb8Fs893aOak6DxRpB77Ox79/wA7/9ESZ5V3n/LMVNwEa8WAY0c7qDyh7rknXxj7VxN+7I7C3D2qsE0= 10 | provider: pypi 11 | user: yihui 12 | install: pip install -U tox-travis 13 | language: python 14 | python: 15 | - 3.5 16 | - 3.4 17 | - 2.7 18 | script: tox 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/respeaker/avs/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" 30 | and "help wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | Python Alexa Voice Service could always use more documentation, whether as part of the 42 | official Python Alexa Voice Service docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/respeaker/avs/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `avs` for local development. 61 | 62 | 1. Fork the `avs` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/avs.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv avs 70 | $ cd avs/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 80 | 81 | $ flake8 avs tests 82 | $ python setup.py test or py.test 83 | $ tox 84 | 85 | To get flake8 and tox, just pip install them into your virtualenv. 86 | 87 | 6. Commit your changes and push your branch to GitHub:: 88 | 89 | $ git add . 90 | $ git commit -m "Your detailed description of your changes." 91 | $ git push origin name-of-your-bugfix-or-feature 92 | 93 | 7. Submit a pull request through the GitHub website. 94 | 95 | Pull Request Guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. Put 102 | your new functionality into a function with a docstring, and add the 103 | feature to the list in README.rst. 104 | 3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check 105 | https://travis-ci.org/respeaker/avs/pull_requests 106 | and make sure that the tests pass for all supported Python versions. 107 | 108 | Tips 109 | ---- 110 | 111 | To run a subset of tests:: 112 | 113 | 114 | $ python -m unittest tests.test_avs 115 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 6 | 0.5.1 (2018-7-9) 7 | ------------------ 8 | 9 | * tornado 5.0.0+ compatible 10 | 11 | 0.5.0 (2018-6-12) 12 | ------------------ 13 | 14 | * python3 compatible 15 | 16 | 0.4.0 (2018-5-23) 17 | ------------------ 18 | 19 | * rewrite directive parser, fix an issue when EOL is in MP3 octet-stream 20 | * add a new recorder using ALSA arecord as lots of forks reported that they were confused by the log of pyaudio (portaudio) 21 | 22 | 0.3.0 (2018-5-21) 23 | ------------------ 24 | 25 | * bugfix 26 | 27 | 0.2.2 (2018-3-29) 28 | ------------------ 29 | 30 | * fix gstreamer doesn't release audio device after end-of-stream 31 | 32 | 0.2.1 (2018-3-28) 33 | ------------------ 34 | 35 | * use gstreamer as the default audio player as gstreamer works better that others on raspberry pi 36 | 37 | 0.2.0 (2018-3-28) 38 | ------------------ 39 | 40 | * support using mpg123 or mpv as player 41 | 42 | 0.1.1 (2017-12-29) 43 | ------------------ 44 | 45 | * bugfix 46 | 47 | 0.1.0 (2017-12-27) 48 | ------------------ 49 | 50 | * support TuneIn audio stream 51 | * add alexa-audio-check util 52 | 53 | 0.0.9 (2017-09-29) 54 | ------------------ 55 | 56 | * add LED light for respeaker core and respeaker usb mic array 57 | * add inactive time report event 58 | 59 | 0.0.8 (2017-09-25) 60 | ------------------ 61 | 62 | * limit audio queue length, fix string to invalid filename 63 | 64 | 0.0.7 (2017-08-31) 65 | ------------------ 66 | 67 | * able to handle a few kind of connection failures and do reconnection 68 | 69 | 0.0.6 (2017-08-30) 70 | ------------------ 71 | 72 | * fix dueros oauth invalid access token issue 73 | * fix dueros-auth 74 | 75 | 0.0.5 (2017-08-24) 76 | ------------------ 77 | 78 | * support alerts interface 79 | 80 | 0.0.4 (2017-07-13) 81 | ------------------ 82 | 83 | * add hands free alexa util using respeaker python library 84 | 85 | 0.0.3 (2017-07-11) 86 | ------------------ 87 | 88 | * use gstreamer to play audio stream 89 | * support dueros avs compatible service 90 | * many improments 91 | 92 | 0.0.2 (2017-07-05) 93 | ------------------ 94 | 95 | * rename alexa util to alexa-tap 96 | * add oauth util alexa-auth 97 | 98 | 0.0.1 (2017-07-04) 99 | ------------------ 100 | 101 | * First release on PyPI. 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Python implementation of Alexa Voice Service 6 | Copyright (C) 2017 Yihui Xiong 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Also add information on how to contact you by electronic and paper mail. 22 | 23 | You should also get your employer (if you work as a programmer) or school, 24 | if any, to sign a "copyright disclaimer" for the program, if necessary. 25 | For more information on this, and how to apply and follow the GNU GPL, see 26 | . 27 | 28 | The GNU General Public License does not permit incorporating your program 29 | into proprietary programs. If your program is a subroutine library, you 30 | may consider it more useful to permit linking proprietary applications with 31 | the library. If this is what you want to do, use the GNU Lesser General 32 | Public License instead of this License. But first, please read 33 | . 34 | 35 | 36 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.md 6 | 7 | recursive-include avs * 8 | recursive-include tests * 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | 12 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python Alexa Voice Service App 2 | ============================== 3 | 4 | [![](https://img.shields.io/pypi/v/avs.svg)](https://pypi.python.org/pypi/avs) 5 | [![](https://img.shields.io/travis/respeaker/avs.svg)](https://travis-ci.org/respeaker/avs) 6 | 7 | ### Features 8 | * Support Alexa Voice Service API v20160207 9 | * Support multiple audio players: gstreamer 1.0, mpv and mpg123 10 | * 支持[Baidu DuerOS](https://github.com/respeaker/avs/wiki/%E4%BD%BF%E7%94%A8DuerOS%E7%9A%84AVS%E5%85%BC%E5%AE%B9%E6%9C%8D%E5%8A%A1) 11 | 12 | 13 | ### Options 14 | 15 | 1. Player 16 | 17 | We have 3 players (`mpv`, `mpg123` and gstreamer) to use. 18 | `SpeechSynthesizer` and `Alerts` prefer `mpg123` which is more responsive. 19 | `AudioPlayer` likes gstreamer > `mpv` > `mpg123`. Gstreamer supports more audio format and works well on raspberry pi. We can also specify the player of `AudioPlayer` using the environment variable `PLAYER`. 20 | 21 | 2. Recorder 22 | 23 | 2 recorders (pyaudio & `arecord`) are available. We can use environment variable `RECORDER` to specify the recorder. For example, run `RECORDER=pyaudio alexa-tap` will use pyaudio as the recorder. By default, `arecord` is used as the recorder. 24 | 25 | 3. Keyword detector (optional) 26 | 27 | Use PocketSphinx or Snowboy. To use pocketsphinx, install respeaker python library and pocketsphinx. 28 | To use Snowboy, go to [Snowboy's Github](https://github.com/Kitt-AI/snowboy) to install it. 29 | 30 | >If you use raspberry pi and gstreamer, it is likely that gstreamer's default audio sink is GstOMXHdmiAudioSink. It ignores ALSA configurations and outputs audio to HDMI. If you don't want to use HDMI audio output, you should run `sudo apt remove gstreamer1.0-omx gstreamer1.0-omx-rpi` 31 | 32 | ### Requirements 33 | * For ReSpeaker Core (MT7688) 34 | 35 | gstreamer1.0, pyaudio and pocketsphinx and respeaker python library are already installed by default, just run `pip install avs` 36 | 37 | * For Debian/Ubuntu/Raspbian 38 | 39 | sudo apt-get install mpg123 mpv 40 | sudo apt-get install gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ 41 | gir1.2-gstreamer-1.0 python-gi python-gst-1.0 42 | sudo apt-get install python-pyaudio 43 | 44 | ### Get started 45 | 46 | 1. run `alexa-audio-check` to check if recording & playing is OK. If RMS is not zero, recording is OK, if you can hear alarm, playing is OK 47 | 48 | $alexa-audio-check 49 | RMS: 41 50 | RMS: 43 51 | 52 | 2. run `alexa-auth` to login Amazon, it will save authorization information to `~/.avs.json` 53 | 3. run `alexa-tap`, then press Enter to talk with alexa 54 | 55 | >If you want to use a specified player, use the environment variable `PLAYER` to specify it, such as `PLAYER=mpv alexa-tap` or `PLAYER=mpg123 alexa` or `PLAYER=gstreamer alexa` 56 | 57 | ### Hands-free Alexa 58 | #### Using PocketSphinx for Keyword Spotting 59 | 1. install respeaker and pocketsphinx python packages 60 | 61 | `sudo pip install respeaker pocketsphinx # pocketsphinx requires gcc toolchain and libpulse-dev` 62 | 63 | 2. run `alexa`, then use "alexa" to start a conversation with alexa, for example, "alexa, what time is it" 64 | 65 | #### Using Snowboy for Keyword Spotting 66 | 1. Install [Snowboy](https://github.com/Kitt-AI/snowboy) 67 | 68 | ``` 69 | git clone --depth 1 https://github.com/Kitt-AI/snowboy.git snowboy_github 70 | cd snowboy_github 71 | sudo apt install libatlas-base-dev swig 72 | python setup.py build 73 | sudo pip install . 74 | ``` 75 | 2. Install voice-engine python library 76 | 77 | `sudo pip install voice-engine` 78 | 79 | 3. run the following python script and use the keyword `alexa` to start a conversation with alexa 80 | 81 | ```python 82 | import time 83 | import signal 84 | from voice_engine.source import Source 85 | from voice_engine.kws import KWS 86 | from avs.alexa import Alexa 87 | import logging 88 | 89 | logging.basicConfig(level=logging.DEBUG) 90 | 91 | 92 | src = Source(rate=16000) 93 | kws = KWS(model='snowboy') 94 | alexa = Alexa() 95 | 96 | src.pipeline(kws, alexa) 97 | 98 | def on_detected(keyword): 99 | print('detected {}'.format(keyword)) 100 | alexa.listen() 101 | 102 | kws.set_callback(on_detected) 103 | 104 | is_quit = [] 105 | def signal_handler(signal, frame): 106 | print('Quit') 107 | is_quit.append(True) 108 | 109 | signal.signal(signal.SIGINT, signal_handler) 110 | 111 | src.pipeline_start() 112 | while not is_quit: 113 | time.sleep(1) 114 | src.pipeline_stop() 115 | ``` 116 | 117 | ### To do 118 | * Speaker interface 119 | * Notifications interface 120 | 121 | ### Change Alexa Voice Service client id and product id 122 | If you want to use your own client id and product id, try: 123 | 124 | 1. [register for an Amazon Developer Account](https://github.com/alexa/alexa-avs-raspberry-pi#61---register-your-product-and-create-a-security-profile) 125 | 126 | 2. create a file named config.json with your product_id, client_id and client_secret 127 | 128 | { 129 | "product_id": "x", 130 | "client_id": "y", 131 | "client_secret": "z" 132 | } 133 | 134 | 3. run `alexa-auth -c config.json` 135 | 136 | 4. run `alexa-tap` or `alexa` 137 | 138 | ### License 139 | GNU General Public License v3 140 | 141 | 142 | ### Credits 143 | This project is based on [nicholas-gh/python-alexa-client](https://github.com/nicholas-gh/python-alexa-client) 144 | 145 | This package was created with Cookiecutter_ and the [audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) project template. 146 | -------------------------------------------------------------------------------- /avs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for Python Alexa Voice Service.""" 4 | 5 | __author__ = """Yihui Xiong""" 6 | __email__ = 'yihui.xiong@hotmail.com' 7 | __version__ = '0.0.7' 8 | -------------------------------------------------------------------------------- /avs/alexa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import base64 5 | import cgi 6 | import json 7 | import logging 8 | import os 9 | import signal 10 | import sys 11 | import tempfile 12 | import threading 13 | import uuid 14 | 15 | if sys.version_info < (3, 0): 16 | import Queue as queue 17 | else: 18 | import queue 19 | 20 | import requests 21 | import datetime 22 | import hyper 23 | 24 | from avs.mic import Audio 25 | 26 | from avs.interface.alerts import Alerts 27 | from avs.interface.audio_player import AudioPlayer 28 | from avs.interface.speaker import Speaker 29 | from avs.interface.speech_recognizer import SpeechRecognizer 30 | from avs.interface.speech_synthesizer import SpeechSynthesizer 31 | from avs.interface.system import System 32 | import avs.config 33 | import avs.auth 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | class AlexaStateListener(object): 39 | def __init__(self): 40 | pass 41 | 42 | def on_ready(self): 43 | logger.info('on_ready') 44 | 45 | def on_disconnected(self): 46 | logger.info('on_disconnected') 47 | 48 | def on_listening(self): 49 | logger.info('on_listening') 50 | 51 | def on_thinking(self): 52 | logger.info('on_thinking') 53 | 54 | def on_speaking(self): 55 | logger.info('on_speaking') 56 | 57 | def on_finished(self): 58 | logger.info('on_finished') 59 | 60 | 61 | class Alexa(object): 62 | API_VERSION = 'v20160207' 63 | 64 | def __init__(self, config=None): 65 | self.event_queue = queue.Queue() 66 | self.SpeechRecognizer = SpeechRecognizer(self) 67 | self.SpeechSynthesizer = SpeechSynthesizer(self) 68 | self.AudioPlayer = AudioPlayer(self) 69 | self.Speaker = Speaker(self) 70 | self.Alerts = Alerts(self) 71 | self.System = System(self) 72 | 73 | self.state_listener = AlexaStateListener() 74 | 75 | # put() will send audio to speech recognizer 76 | self.put = self.SpeechRecognizer.put 77 | 78 | # listen() will trigger SpeechRecognizer's Recognize event 79 | self.listen = self.SpeechRecognizer.Recognize 80 | 81 | self.done = False 82 | 83 | self.requests = requests.Session() 84 | 85 | self._configfile = config 86 | self._config = avs.config.load(configfile=config) 87 | 88 | self.last_activity = datetime.datetime.utcnow() 89 | self._ping_time = None 90 | 91 | def set_state_listener(self, listner): 92 | self.state_listener = listner 93 | 94 | def start(self): 95 | self.done = False 96 | 97 | t = threading.Thread(target=self.run) 98 | t.daemon = True 99 | t.start() 100 | 101 | def stop(self): 102 | self.done = True 103 | 104 | def send_event(self, event, listener=None, attachment=None): 105 | self.event_queue.put((event, listener, attachment)) 106 | 107 | def run(self): 108 | while not self.done: 109 | try: 110 | self._run() 111 | except AttributeError as e: 112 | logger.exception(e) 113 | continue 114 | except hyper.http20.exceptions.StreamResetError as e: 115 | logger.exception(e) 116 | continue 117 | except ValueError as e: 118 | logging.exception(e) 119 | # failed to get an access token, exit 120 | sys.exit(1) 121 | except Exception as e: 122 | logging.exception(e) 123 | continue 124 | 125 | def _run(self): 126 | conn = hyper.HTTP20Connection('{}:443'.format( 127 | self._config['host_url']), force_proto='h2') 128 | 129 | headers = {'authorization': 'Bearer {}'.format(self.token)} 130 | if 'dueros-device-id' in self._config: 131 | headers['dueros-device-id'] = self._config['dueros-device-id'] 132 | 133 | downchannel_id = conn.request( 134 | 'GET', '/{}/directives'.format(self._config['api']), headers=headers) 135 | downchannel_response = conn.get_response(downchannel_id) 136 | if downchannel_response.status != 200: 137 | raise ValueError( 138 | "/directive requests returned {}".format(downchannel_response.status)) 139 | 140 | _, pdict = cgi.parse_header( 141 | downchannel_response.headers['content-type'][0].decode('utf-8')) 142 | downchannel_boundary = '--{}'.format(pdict['boundary']).encode('utf-8') 143 | downchannel = conn.streams[downchannel_id] 144 | downchannel_buffer = b'' 145 | eventchannel_boundary = 'seeed-voice-engine' 146 | 147 | # ping every 5 minutes (60 seconds early for latency) to maintain the connection 148 | self._ping_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=240) 149 | self.event_queue.queue.clear() 150 | self.System.SynchronizeState() 151 | while not self.done: 152 | try: 153 | event, listener, attachment = self.event_queue.get( 154 | timeout=0.25) 155 | except queue.Empty: 156 | event = None 157 | 158 | # we want to avoid blocking if the data wasn't for stream downchannel 159 | while conn._sock.can_read: 160 | conn._single_read() 161 | 162 | while downchannel.data: 163 | framebytes = downchannel._read_one_frame() 164 | downchannel_buffer = self._parse_response( 165 | framebytes, downchannel_boundary, downchannel_buffer 166 | ) 167 | 168 | if event is None: 169 | self._ping(conn) 170 | self.System.UserInactivityReport() 171 | continue 172 | 173 | headers = { 174 | ':method': 'POST', 175 | ':scheme': 'https', 176 | ':path': '/{}/events'.format(self._config['api']), 177 | 'authorization': 'Bearer {}'.format(self.token), 178 | 'content-type': 'multipart/form-data; boundary={}'.format(eventchannel_boundary) 179 | } 180 | if 'dueros-device-id' in self._config: 181 | headers['dueros-device-id'] = self._config['dueros-device-id'] 182 | 183 | stream_id = conn.putrequest(headers[':method'], headers[':path']) 184 | default_headers = (':method', ':scheme', ':authority', ':path') 185 | for name, value in headers.items(): 186 | is_default = name in default_headers 187 | conn.putheader(name, value, stream_id, replace=is_default) 188 | conn.endheaders(final=False, stream_id=stream_id) 189 | 190 | metadata = { 191 | 'context': self.context, 192 | 'event': event 193 | } 194 | # logger.info('metadata: {}'.format(json.dumps(metadata, indent=4))) 195 | 196 | json_part = '--{}\r\n'.format(eventchannel_boundary) 197 | json_part += 'Content-Disposition: form-data; name="metadata"\r\n' 198 | json_part += 'Content-Type: application/json; charset=UTF-8\r\n\r\n' 199 | json_part += json.dumps(metadata) 200 | 201 | conn.send(json_part.encode('utf-8'), 202 | final=False, stream_id=stream_id) 203 | 204 | if attachment: 205 | attachment_header = '\r\n--{}\r\n'.format( 206 | eventchannel_boundary) 207 | attachment_header += 'Content-Disposition: form-data; name="audio"\r\n' 208 | attachment_header += 'Content-Type: application/octet-stream\r\n\r\n' 209 | conn.send(attachment_header.encode('utf-8'), 210 | final=False, stream_id=stream_id) 211 | 212 | # AVS_AUDIO_CHUNK_PREFERENCE = 320 213 | for chunk in attachment: 214 | conn.send(chunk, final=False, stream_id=stream_id) 215 | 216 | # check if StopCapture directive is received 217 | while conn._sock.can_read: 218 | conn._single_read() 219 | 220 | while downchannel.data: 221 | framebytes = downchannel._read_one_frame() 222 | downchannel_buffer = self._parse_response( 223 | framebytes, downchannel_boundary, downchannel_buffer 224 | ) 225 | 226 | self.last_activity = datetime.datetime.utcnow() 227 | 228 | end_part = '\r\n--{}--'.format(eventchannel_boundary) 229 | conn.send(end_part.encode('utf-8'), 230 | final=True, stream_id=stream_id) 231 | 232 | logger.info("wait for response") 233 | response = conn.get_response(stream_id) 234 | logger.info("status code: %s", response.status) 235 | 236 | if response.status == 200: 237 | _, pdict = cgi.parse_header( 238 | response.headers['content-type'][0].decode('utf-8')) 239 | boundary = '--{}'.format(pdict['boundary']).encode('utf-8') 240 | self._parse_response(response.read(), boundary) 241 | elif response.status == 204: 242 | pass 243 | else: 244 | logger.warning(response.headers) 245 | logger.warning(response.read()) 246 | 247 | if listener and callable(listener): 248 | listener() 249 | 250 | def _parse_response(self, response, boundary, buffer=b''): 251 | directives = [] 252 | blen = len(boundary) 253 | response = buffer + response 254 | while response: 255 | pos = response.find(boundary) 256 | if pos < 0: 257 | break 258 | 259 | # skip small data block 260 | if pos > blen: 261 | # a blank line is between parts 262 | parts = response[:pos - 2].split(b'\r\n\r\n', 1) 263 | if parts[0].find(b'application/json') >= 0: 264 | metadata = json.loads(parts[1].decode('utf-8')) 265 | if 'directive' in metadata: 266 | directives.append(metadata['directive']) 267 | elif parts[0].find(b'application/octet-stream') >= 0: 268 | for line in parts[0].splitlines(): 269 | name, value = line.split(b':', 1) 270 | if name.lower() == b'content-id': 271 | content_id = value.strip()[1:-1] 272 | filename = base64.urlsafe_b64encode(content_id)[:8].decode('utf-8') 273 | with open(os.path.join(tempfile.gettempdir(), '{}.mp3'.format(filename)), 'wb') as f: 274 | f.write(parts[1]) 275 | logger.info('write audio to {}.mp3'.format(filename)) 276 | break 277 | 278 | response = response[pos + blen + 2:] 279 | 280 | for directive in directives: 281 | self._handle_directive(directive) 282 | 283 | return response 284 | 285 | def _handle_directive(self, directive): 286 | logger.info(json.dumps(directive, indent=4)) 287 | try: 288 | namespace = directive['header']['namespace'] 289 | name = directive['header']['name'] 290 | if hasattr(self, namespace): 291 | interface = getattr(self, namespace) 292 | directive_func = getattr(interface, name, None) 293 | if directive_func: 294 | directive_func(directive) 295 | else: 296 | logger.info( 297 | '{}.{} is not implemented yet'.format(namespace, name)) 298 | else: 299 | logger.info('{} is not implemented yet'.format(namespace)) 300 | 301 | except KeyError as e: 302 | logger.exception(e) 303 | except Exception as e: 304 | logger.exception(e) 305 | 306 | def _ping(self, connection): 307 | if datetime.datetime.utcnow() >= self._ping_time: 308 | connection.ping(uuid.uuid4().hex[:8]) 309 | 310 | logger.debug('ping at {}'.format( 311 | datetime.datetime.utcnow().strftime("%a %b %d %H:%M:%S %Y"))) 312 | 313 | # ping every 5 minutes (60 seconds early for latency) to maintain the connection 314 | self._ping_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=240) 315 | 316 | @property 317 | def context(self): 318 | # return [self.SpeechRecognizer.context, self.SpeechSynthesizer.context, 319 | # self.AudioPlayer.context, self.Speaker.context, self.Alerts.context] 320 | return [self.SpeechSynthesizer.context, self.Speaker.context, self.AudioPlayer.context, self.Alerts.context] 321 | 322 | @property 323 | def token(self): 324 | date_format = "%a %b %d %H:%M:%S %Y" 325 | 326 | if 'access_token' in self._config: 327 | if 'expiry' in self._config: 328 | expiry = datetime.datetime.strptime( 329 | self._config['expiry'], date_format) 330 | # refresh 60 seconds early to avoid chance of using expired access_token 331 | if (datetime.datetime.utcnow() - expiry) > datetime.timedelta(seconds=60): 332 | logger.info("Refreshing access_token") 333 | else: 334 | return self._config['access_token'] 335 | 336 | payload = { 337 | 'client_id': self._config['client_id'], 338 | 'client_secret': self._config['client_secret'], 339 | 'grant_type': 'refresh_token', 340 | 'refresh_token': self._config['refresh_token'] 341 | } 342 | 343 | response = None 344 | 345 | # try to request an access token 3 times 346 | for _ in range(3): 347 | try: 348 | response = self.requests.post( 349 | self._config['refresh_url'], data=payload) 350 | if response.status_code != 200: 351 | logger.warning(response.text) 352 | else: 353 | break 354 | except Exception as e: 355 | logger.exception(e) 356 | continue 357 | 358 | if (response is None) or (not hasattr(response, 'status_code')) or response.status_code != 200: 359 | raise ValueError( 360 | "refresh token request returned {}".format(response.status)) 361 | 362 | config = response.json() 363 | self._config['access_token'] = config['access_token'] 364 | 365 | expiry_time = datetime.datetime.utcnow( 366 | ) + datetime.timedelta(seconds=config['expires_in']) 367 | self._config['expiry'] = expiry_time.strftime(date_format) 368 | logger.debug(json.dumps(self._config, indent=4)) 369 | 370 | avs.config.save(self._config, configfile=self._configfile) 371 | 372 | return self._config['access_token'] 373 | 374 | def __enter__(self): 375 | self.start() 376 | return self 377 | 378 | def __exit__(self, exc_type, exc_val, exc_tb): 379 | self.stop() 380 | 381 | 382 | def main(): 383 | logging.basicConfig(level=logging.INFO) 384 | 385 | config = avs.config.DEFAULT_CONFIG_FILE if len(sys.argv) < 2 else sys.argv[1] 386 | 387 | if not os.path.isfile(config): 388 | print('Login amazon alexa or baidu dueros first') 389 | avs.auth.auth(None, config) 390 | 391 | audio = Audio() 392 | alexa = Alexa(config) 393 | 394 | audio.link(alexa) 395 | 396 | alexa.start() 397 | audio.start() 398 | 399 | is_quit = threading.Event() 400 | 401 | def signal_handler(sig, frame): 402 | print('Quit') 403 | is_quit.set() 404 | 405 | signal.signal(signal.SIGINT, signal_handler) 406 | 407 | while True: 408 | try: 409 | input('press ENTER to talk\n') 410 | except SyntaxError: 411 | pass 412 | except NameError: 413 | pass 414 | 415 | if is_quit.is_set(): 416 | break 417 | 418 | alexa.listen() 419 | 420 | alexa.stop() 421 | audio.stop() 422 | 423 | 424 | if __name__ == '__main__': 425 | main() 426 | -------------------------------------------------------------------------------- /avs/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | import time 5 | import uuid 6 | 7 | import click 8 | import requests 9 | import tornado.httpserver 10 | import tornado.ioloop 11 | import tornado.web 12 | 13 | import avs.config 14 | 15 | 16 | class MainHandler(tornado.web.RequestHandler): 17 | def initialize(self, config, output): 18 | self.config = config 19 | self.output = output 20 | 21 | @tornado.web.asynchronous 22 | def get(self): 23 | redirect_uri = self.request.protocol + "://" + self.request.host + "/authresponse" 24 | if self.request.path == '/authresponse': 25 | code = self.get_argument("code") 26 | payload = { 27 | "client_id": self.config['client_id'], 28 | "client_secret": self.config['client_secret'], 29 | "code": code, 30 | "grant_type": "authorization_code", 31 | "redirect_uri": redirect_uri 32 | } 33 | 34 | if self.config['host_url'] == 'dueros-h2.baidu.com': 35 | token_url = 'https://openapi.baidu.com/oauth/2.0/token' 36 | message = 'Succeed to login Baidu DuerOS' 37 | else: 38 | token_url = 'https://api.amazon.com/auth/o2/token' 39 | message = 'Succeed to login Amazon Alexa Voice Service' 40 | 41 | r = requests.post(token_url, data=payload) 42 | config = r.json() 43 | self.config['refresh_token'] = config['refresh_token'] 44 | 45 | if 'access_token' in config: 46 | date_format = "%a %b %d %H:%M:%S %Y" 47 | expiry_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=config['expires_in']) 48 | self.config['expiry'] = expiry_time.strftime(date_format) 49 | self.config['access_token'] = config['access_token'] 50 | 51 | print(json.dumps(self.config, indent=4)) 52 | avs.config.save(self.config, configfile=self.output) 53 | 54 | self.write(message) 55 | self.finish() 56 | tornado.ioloop.IOLoop.instance().stop() 57 | elif self.request.path == '/alexa': 58 | self.alexa_oauth() 59 | elif self.request.path == '/dueros': 60 | self.dueros_oauth() 61 | elif self.request.path == '/': 62 | index_html = os.path.realpath(os.path.join(os.path.dirname(__file__), 'resources/web/index.html')) 63 | with open(index_html) as f: 64 | self.write(f.read()) 65 | self.finish() 66 | 67 | def alexa_oauth(self): 68 | if 'client_secret' not in self.config: 69 | self.config.update(avs.config.alexa()) 70 | if 'dueros-device-id' in self.config: 71 | del self.config['dueros-device-id'] 72 | self.config.update(avs.config.alexa()) 73 | 74 | oauth_url = 'https://www.amazon.com/ap/oa' 75 | redirect_uri = self.request.protocol + "://" + self.request.host + "/authresponse" 76 | 77 | scope_data = json.dumps({ 78 | "alexa:all": { 79 | "productID": self.config['product_id'], 80 | "productInstanceAttributes": { 81 | "deviceSerialNumber": uuid.uuid4().hex 82 | } 83 | } 84 | }) 85 | payload = { 86 | "client_id": self.config['client_id'], 87 | "scope": "alexa:all", 88 | "scope_data": scope_data, 89 | "response_type": "code", 90 | "redirect_uri": redirect_uri 91 | } 92 | 93 | req = requests.Request('GET', oauth_url, params=payload) 94 | p = req.prepare() 95 | self.redirect(p.url) 96 | 97 | def dueros_oauth(self): 98 | if 'client_secret' not in self.config: 99 | self.config.update(avs.config.dueros()) 100 | 101 | oauth_url = 'https://openapi.baidu.com/oauth/2.0/authorize' 102 | redirect_uri = self.request.protocol + "://" + self.request.host + "/authresponse" 103 | 104 | payload = { 105 | "client_id": self.config["client_id"], 106 | "scope": "basic", 107 | "response_type": "code", 108 | "redirect_uri": redirect_uri 109 | } 110 | 111 | req = requests.Request('GET', oauth_url, params=payload) 112 | p = req.prepare() 113 | self.redirect(p.url) 114 | 115 | 116 | def open_webbrowser(): 117 | try: 118 | import webbrowser 119 | except ImportError: 120 | print('Go to http://{your device IP}:3000 to start') 121 | return 122 | 123 | time.sleep(0.1) 124 | print("A web page should is opened. If not, go to http://127.0.0.1:3000 to start") 125 | webbrowser.open('http://127.0.0.1:3000') 126 | 127 | 128 | def auth(config, output): 129 | import threading 130 | threading.Thread(target=open_webbrowser).start() 131 | 132 | config = avs.config.load(config) if config else {} 133 | 134 | application = tornado.web.Application([(r".*", MainHandler, dict(config=config, output=output))]) 135 | http_server = tornado.httpserver.HTTPServer(application) 136 | http_server.listen(3000) 137 | tornado.ioloop.IOLoop.instance().start() 138 | tornado.ioloop.IOLoop.instance().close() 139 | 140 | @click.command() 141 | @click.option('--config', '-c', help='configuration json file with product_id, client_id and client_secret') 142 | @click.option('--output', '-o', default=avs.config.DEFAULT_CONFIG_FILE, help='output json file with refresh token') 143 | def main(config, output): 144 | auth(config, output) 145 | 146 | 147 | if __name__ == '__main__': 148 | main() 149 | -------------------------------------------------------------------------------- /avs/check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Hands-free alexa with respeaker using pocketsphinx to search keyword 5 | 6 | It depends on respeaker python library (https://github.com/respeaker/respeaker_python_library) 7 | """ 8 | 9 | import audioop 10 | import os 11 | import signal 12 | import time 13 | 14 | from avs.mic import Audio 15 | from avs.player import Player 16 | 17 | 18 | class RMS(object): 19 | def __init__(self): 20 | pass 21 | 22 | def put(self, data): 23 | print('RMS: {}'.format(audioop.rms(data, 2))) 24 | 25 | 26 | def main(): 27 | audio = Audio(frames_size=1600) 28 | rms = RMS() 29 | 30 | audio.link(rms) 31 | 32 | audio.start() 33 | 34 | alarm = os.path.realpath(os.path.join(os.path.dirname(__file__), 'resources/alarm.mp3')) 35 | alarm_uri = 'file://{}'.format(alarm) 36 | 37 | player1 = Player() 38 | player2 = Player() 39 | 40 | is_quit = [] 41 | 42 | def signal_handler(signal, frame): 43 | print('Quit') 44 | is_quit.append(True) 45 | 46 | signal.signal(signal.SIGINT, signal_handler) 47 | 48 | while not is_quit: 49 | player1.play(alarm_uri) 50 | time.sleep(1) 51 | player1.pause() 52 | player2.play(alarm_uri) 53 | time.sleep(3) 54 | player2.pause() 55 | 56 | audio.stop() 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /avs/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import os 5 | import sys 6 | import uuid 7 | 8 | DEFAULT_CONFIG_FILE = os.path.join(os.path.expanduser('~'), '.avs.json') 9 | 10 | 11 | def load(configfile=DEFAULT_CONFIG_FILE): 12 | if configfile is None: 13 | configfile = DEFAULT_CONFIG_FILE 14 | 15 | if not os.path.isfile(configfile): 16 | raise RuntimeError('No configuration file') 17 | 18 | with open(configfile, 'r') as f: 19 | config = json.load(f) 20 | require_keys = ['product_id', 'client_id', 'client_secret'] 21 | for key in require_keys: 22 | if not ((key in config) and config[key]): 23 | raise RuntimeError('{} should include "{}"'.format(configfile, key)) 24 | 25 | if ('host_url' not in config) or (not config['host_url']): 26 | config['host_url'] = 'avs-alexa-na.amazon.com' 27 | 28 | if config['host_url'] == 'dueros-h2.baidu.com': 29 | config['api'] = 'dcs/avs-compatible-v20160207' 30 | config['refresh_url'] = 'https://openapi.baidu.com/oauth/2.0/token' 31 | else: 32 | config['api'] = 'v20160207' 33 | config['refresh_url'] = 'https://api.amazon.com/auth/o2/token' 34 | 35 | return config 36 | 37 | 38 | def save(config, configfile=None): 39 | if configfile is None: 40 | configfile = DEFAULT_CONFIG_FILE 41 | 42 | with open(configfile, 'w') as f: 43 | json.dump(config, f, indent=4) 44 | 45 | 46 | def dueros(): 47 | product_id = "xiaojing-" + uuid.uuid4().hex 48 | return { 49 | "dueros-device-id": product_id, 50 | "client_id": "lud6jnwdVFim4K4Zkoas3BkRVLvCO57Z", 51 | "host_url": "dueros-h2.baidu.com", 52 | "client_secret": "A017kke1GSSz7hp8Fj6ySoIWrnFraxf5", 53 | "product_id": product_id 54 | } 55 | 56 | def alexa(): 57 | return { 58 | "product_id": "ReSpeaker", 59 | "client_id": "amzn1.application-oa2-client.91b0cebd9074412cba1570a5dd03fc6e", 60 | "host_url": "avs-alexa-na.amazon.com", 61 | "client_secret": "fbd7a0e72953c1dd9a920670cf7f4115f694cd47c32a1513dc12a804c7f804e2" 62 | } -------------------------------------------------------------------------------- /avs/interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respeaker/avs/affcb7dd1b7ab7461ab812c4e0cd448cbc95f076/avs/interface/__init__.py -------------------------------------------------------------------------------- /avs/interface/alerts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/alerts""" 4 | 5 | import datetime 6 | import os 7 | import time 8 | import uuid 9 | from threading import Timer, Event 10 | 11 | import dateutil.parser 12 | 13 | # prefer mpg123 player as it is more responsive than mpv and gstreamer 14 | if os.system('which mpg123 >/dev/null') == 0: 15 | from avs.player.mpg123_player import Player 16 | else: 17 | from avs.player import Player 18 | 19 | 20 | class Alerts(object): 21 | STATES = {'IDLE', 'FOREGROUND', 'BACKGROUND'} 22 | 23 | def __init__(self, alexa): 24 | self.alexa = alexa 25 | self.player = Player() 26 | 27 | self.player.add_callback('eos', self._stop) 28 | self.player.add_callback('error', self._stop) 29 | 30 | alarm = os.path.realpath(os.path.join(os.path.dirname(__file__), '../resources/alarm.mp3')) 31 | self.alarm_uri = 'file://{}'.format(alarm) 32 | 33 | self.all_alerts = {} 34 | self.active_alerts = {} 35 | 36 | self.state = 'IDLE' 37 | self.end_event = Event() 38 | 39 | def _stop(self): 40 | """ 41 | Stop all active alerts 42 | """ 43 | for token in list(self.active_alerts.keys()): 44 | self.AlertStopped(token) 45 | 46 | self.active_alerts = {} 47 | self.state = 'IDLE' 48 | 49 | self.end_event.set() 50 | 51 | def stop(self): 52 | self.player.stop() 53 | self._stop() 54 | 55 | def enter_background(self): 56 | if self.state == 'FOREGROUND': 57 | self.state = 'BACKGROUND' 58 | self.player.pause() 59 | 60 | def enter_foreground(self): 61 | if self.state == 'BACKGROUND': 62 | self.state = 'FOREGROUND' 63 | self.player.resume() 64 | 65 | def _start_alert(self, token): 66 | if token in self.all_alerts: 67 | while self.alexa.SpeechRecognizer.conversation: 68 | time.sleep(1) 69 | 70 | if self.alexa.AudioPlayer.state == 'PLAYING': 71 | self.alexa.AudioPlayer.pause() 72 | 73 | self.AlertStarted(token) 74 | 75 | self.end_event.clear() 76 | 77 | # TODO: repeat play alarm until user stops it or timeout 78 | self.player.play(self.alarm_uri) 79 | 80 | if not self.end_event.wait(timeout=600): 81 | self.player.stop() 82 | 83 | if not self.alexa.SpeechRecognizer.conversation: 84 | if self.alexa.AudioPlayer.state == 'PAUSED': 85 | self.alexa.AudioPlayer.resume() 86 | 87 | # { 88 | # "directive": { 89 | # "header": { 90 | # "namespace": "Alerts", 91 | # "name": "SetAlert", 92 | # "messageId": "{{STRING}}", 93 | # "dialogRequestId": "{{STRING}}" 94 | # }, 95 | # "payload": { 96 | # "token": "{{STRING}}", 97 | # "type": "{{STRING}}", 98 | # "scheduledTime": "2017-08-07T09:02:58+0000", 99 | # } 100 | # } 101 | # } 102 | def SetAlert(self, directive): 103 | payload = directive['payload'] 104 | token = payload['token'] 105 | scheduled_time = dateutil.parser.parse(payload['scheduledTime']) 106 | 107 | # Update the alert 108 | if token in self.all_alerts: 109 | pass 110 | 111 | self.all_alerts[token] = payload 112 | 113 | interval = scheduled_time - datetime.datetime.now(scheduled_time.tzinfo) 114 | Timer(interval.seconds, self._start_alert, (token,)).start() 115 | 116 | self.SetAlertSucceeded(token) 117 | 118 | def SetAlertSucceeded(self, token): 119 | event = { 120 | "header": { 121 | "namespace": "Alerts", 122 | "name": "SetAlertSucceeded", 123 | "messageId": uuid.uuid4().hex 124 | }, 125 | "payload": { 126 | "token": token 127 | } 128 | } 129 | 130 | self.alexa.send_event(event) 131 | 132 | def SetAlertFailed(self, token): 133 | event = { 134 | "header": { 135 | "namespace": "Alerts", 136 | "name": "SetAlertFailed", 137 | "messageId": uuid.uuid4().hex 138 | }, 139 | "payload": { 140 | "token": token 141 | } 142 | } 143 | 144 | self.alexa.send_event(event) 145 | 146 | # { 147 | # "directive": { 148 | # "header": { 149 | # "namespace": "Alerts", 150 | # "name": "DeleteAlert", 151 | # "messageId": "{{STRING}}", 152 | # "dialogRequestId": "{{STRING}}" 153 | # }, 154 | # "payload": { 155 | # "token": "{{STRING}}" 156 | # } 157 | # } 158 | # } 159 | def DeleteAlert(self, directive): 160 | token = directive['payload']['token'] 161 | 162 | if token in self.active_alerts: 163 | self.AlertStopped(token) 164 | 165 | if token in self.all_alerts: 166 | del self.all_alerts[token] 167 | 168 | self.DeleteAlertSucceeded(token) 169 | 170 | def DeleteAlertSucceeded(self, token): 171 | event = { 172 | "header": { 173 | "namespace": "Alerts", 174 | "name": "DeleteAlertSucceeded", 175 | "messageId": uuid.uuid4().hex 176 | }, 177 | "payload": { 178 | "token": token 179 | } 180 | } 181 | 182 | self.alexa.send_event(event) 183 | 184 | def DeleteAlertFailed(self, token): 185 | event = { 186 | "header": { 187 | "namespace": "Alerts", 188 | "name": "DeleteAlertFailed", 189 | "messageId": uuid.uuid4().hex 190 | }, 191 | "payload": { 192 | "token": token 193 | } 194 | } 195 | 196 | self.alexa.send_event(event) 197 | 198 | def AlertStarted(self, token): 199 | if self.state == 'IDLE': 200 | self.state = 'FOREGROUND' 201 | 202 | self.active_alerts[token] = self.all_alerts[token] 203 | 204 | event = { 205 | "header": { 206 | "namespace": "Alerts", 207 | "name": "AlertStarted", 208 | "messageId": uuid.uuid4().hex 209 | }, 210 | "payload": { 211 | "token": token 212 | } 213 | } 214 | 215 | self.alexa.send_event(event) 216 | 217 | def AlertStopped(self, token): 218 | if token in self.active_alerts: 219 | del self.active_alerts[token] 220 | 221 | if token in self.all_alerts: 222 | del self.all_alerts[token] 223 | 224 | if not self.active_alerts: 225 | self.state = 'IDLE' 226 | 227 | event = { 228 | "header": { 229 | "namespace": "Alerts", 230 | "name": "AlertStopped", 231 | "messageId": "{STRING}" 232 | }, 233 | "payload": { 234 | "token": token 235 | } 236 | } 237 | 238 | self.alexa.send_event(event) 239 | 240 | def AlertEnteredForeground(self, token): 241 | event = { 242 | "header": { 243 | "namespace": "Alerts", 244 | "name": "AlertEnteredForeground", 245 | "messageId": uuid.uuid4().hex 246 | }, 247 | "payload": { 248 | "token": token 249 | } 250 | } 251 | 252 | self.alexa.send_event(event) 253 | 254 | def AlertEnteredBackground(self, token): 255 | event = { 256 | "header": { 257 | "namespace": "Alerts", 258 | "name": "AlertEnteredBackground", 259 | "messageId": uuid.uuid4().hex 260 | }, 261 | "payload": { 262 | "token": token 263 | } 264 | } 265 | 266 | self.alexa.send_event(event) 267 | 268 | @property 269 | def context(self): 270 | return { 271 | "header": { 272 | "namespace": "Alerts", 273 | "name": "AlertsState" 274 | }, 275 | "payload": { 276 | "allAlerts": list(self.all_alerts.values()), 277 | "activeAlerts": list(self.active_alerts.values()) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /avs/interface/audio_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/audioplayer""" 4 | 5 | import base64 6 | import hashlib 7 | import logging 8 | import os 9 | import tempfile 10 | import uuid 11 | from contextlib import closing 12 | 13 | import requests 14 | 15 | try: 16 | from urllib.parse import urlparse 17 | except ImportError: 18 | from urlparse import urlparse 19 | 20 | from avs.player import Player 21 | 22 | logger = logging.getLogger('AudioPlayer') 23 | 24 | 25 | # TODO: parse M3U8 and PLSv2 26 | # refer to https://github.com/alexa/avs-device-sdk/blob/master/PlaylistParser/src/PlaylistParser.cpp 27 | def get_audio_url(audio_url, timeout=3000): 28 | timeout = timeout / 1000. 29 | if audio_url.startswith('cid:'): 30 | filename = base64.urlsafe_b64encode(audio_url[4:]) 31 | filename = hashlib.md5(filename).hexdigest() 32 | mp3_file = os.path.join(tempfile.gettempdir(), filename + '.mp3') 33 | if os.path.isfile(mp3_file): 34 | return 'file://{}'.format(mp3_file) 35 | else: 36 | logger.warn('Unable to parse {}'.format(audio_url)) 37 | return None 38 | 39 | if audio_url.find('radiotime.com') >= 0: 40 | logger.debug('parse TuneIn audio stream: {}'.format(audio_url)) 41 | 42 | try: 43 | response = requests.get(audio_url, timeout=timeout) 44 | lines = response.content.decode().split('\n') 45 | # audio/x-mpegurl; charset=utf-8 46 | content_type = response.headers['Content-Type'] 47 | logger.debug(content_type) 48 | logger.debug(lines) 49 | if lines and lines[0]: 50 | audio_url = lines[0] 51 | logger.debug('Set audio_url to {}'.format(audio_url)) 52 | if content_type.find('audio/x-mpegurl'): 53 | return audio_url 54 | except Exception: 55 | pass 56 | 57 | extension = urlparse(audio_url).path[-4:] 58 | if extension in ['.mp3', '.wma']: 59 | logger.debug('Found audio stream {}'.format(audio_url)) 60 | return audio_url 61 | 62 | logger.debug('Parse stream: {}'.format(audio_url)) 63 | with closing(requests.get(audio_url, timeout=timeout, stream=True)) as response: 64 | content_type = response.headers['Content-Type'] or '' 65 | if content_type.find('pls') >= 0: 66 | try: 67 | logger.debug('parsing playlist: {}'.format(audio_url)) 68 | lines = response.content.decode().split('\n') 69 | logger.debug(lines) 70 | for line in lines: 71 | if line.find('File') == 0: 72 | audio_url = lines[2].split('=', 2)[1] 73 | logger.debug('Set audio_url to {}'.format(audio_url)) 74 | break 75 | except Exception: 76 | pass 77 | 78 | return audio_url 79 | 80 | 81 | class AudioPlayer(object): 82 | STATES = {'IDLE', 'PLAYING', 'STOPPED', 'PAUSED', 'BUFFER_UNDERRUN', 'FINISHED'} 83 | 84 | def __init__(self, alexa): 85 | self.alexa = alexa 86 | self.token = '' 87 | self.state = 'IDLE' 88 | 89 | self.player = Player() 90 | self.player.add_callback('eos', self.PlaybackFinished) 91 | self.player.add_callback('error', self.PlaybackFailed) 92 | 93 | # { 94 | # "directive": { 95 | # "header": { 96 | # "namespace": "AudioPlayer", 97 | # "name": "Play", 98 | # "messageId": "{{STRING}}", 99 | # "dialogRequestId": "{{STRING}}" 100 | # }, 101 | # "payload": { 102 | # "playBehavior": "{{STRING}}", 103 | # "audioItem": { 104 | # "audioItemId": "{{STRING}}", 105 | # "stream": { 106 | # "url": "{{STRING}}", 107 | # "streamFormat": "AUDIO_MPEG" 108 | # "offsetInMilliseconds": {{LONG}}, 109 | # "expiryTime": "{{STRING}}", 110 | # "progressReport": { 111 | # "progressReportDelayInMilliseconds": {{LONG}}, 112 | # "progressReportIntervalInMilliseconds": {{LONG}} 113 | # }, 114 | # "token": "{{STRING}}", 115 | # "expectedPreviousToken": "{{STRING}}" 116 | # } 117 | # } 118 | # } 119 | # } 120 | # } 121 | def Play(self, directive): 122 | if self.alexa.SpeechSynthesizer.state == 'PLAYING': 123 | self.alexa.SpeechSynthesizer.wait() 124 | 125 | behavior = directive['payload']['playBehavior'] 126 | self.token = directive['payload']['audioItem']['stream']['token'] 127 | audio_url = get_audio_url(directive['payload']['audioItem']['stream']['url']) 128 | 129 | self.player.play(audio_url) 130 | self.PlaybackStarted() 131 | 132 | logger.info('audio player is playing') 133 | 134 | def PlaybackStarted(self): 135 | self.state = 'PLAYING' 136 | 137 | event = { 138 | "header": { 139 | "namespace": "AudioPlayer", 140 | "name": "PlaybackStarted", 141 | "messageId": uuid.uuid4().hex 142 | }, 143 | "payload": { 144 | "token": self.token, 145 | "offsetInMilliseconds": self.player.position 146 | } 147 | } 148 | 149 | self.alexa.send_event(event) 150 | 151 | def PlaybackNearlyFinished(self): 152 | event = { 153 | "header": { 154 | "namespace": "AudioPlayer", 155 | "name": "PlaybackNearlyFinished", 156 | "messageId": uuid.uuid4().hex 157 | }, 158 | "payload": { 159 | "token": self.token, 160 | "offsetInMilliseconds": self.player.position 161 | } 162 | } 163 | self.alexa.send_event(event) 164 | 165 | def ProgressReportDelayElapsed(self): 166 | pass 167 | 168 | def ProgressReportIntervalElapsed(self): 169 | pass 170 | 171 | def PlaybackStutterStarted(self): 172 | pass 173 | 174 | def PlaybackStutterFinished(self): 175 | pass 176 | 177 | def PlaybackFinished(self): 178 | self.state = 'FINISHED' 179 | logger.info('playback is finished') 180 | 181 | event = { 182 | "header": { 183 | "namespace": "AudioPlayer", 184 | "name": "PlaybackFinished", 185 | "messageId": uuid.uuid4().hex 186 | }, 187 | "payload": { 188 | "token": self.token, 189 | "offsetInMilliseconds": self.player.position 190 | } 191 | } 192 | self.alexa.send_event(event) 193 | 194 | def PlaybackFailed(self): 195 | self.state = 'STOPPED' 196 | 197 | # { 198 | # "directive": { 199 | # "header": { 200 | # "namespace": "AudioPlayer", 201 | # "name": "Stop", 202 | # "messageId": "{{STRING}}", 203 | # "dialogRequestId": "{{STRING}}" 204 | # }, 205 | # "payload": { 206 | # } 207 | # } 208 | # } 209 | def Stop(self, directive): 210 | if self.state == 'PLAYING' or self.state == 'PAUSED': 211 | self.player.stop() 212 | self.PlaybackStopped() 213 | 214 | logger.info('audio player is stoped') 215 | 216 | def PlaybackStopped(self): 217 | self.state = 'STOPPED' 218 | event = { 219 | "header": { 220 | "namespace": "AudioPlayer", 221 | "name": "PlaybackStopped", 222 | "messageId": uuid.uuid4().hex 223 | }, 224 | "payload": { 225 | "token": self.token, 226 | "offsetInMilliseconds": self.player.position 227 | } 228 | } 229 | self.alexa.send_event(event) 230 | 231 | def pause(self): 232 | self.player.pause() 233 | self.PlaybackPaused() 234 | 235 | logger.info('audio player is paused') 236 | 237 | def PlaybackPaused(self): 238 | self.state = 'PAUSED' 239 | event = { 240 | "header": { 241 | "namespace": "AudioPlayer", 242 | "name": "PlaybackPaused", 243 | "messageId": uuid.uuid4().hex 244 | }, 245 | "payload": { 246 | "token": self.token, 247 | "offsetInMilliseconds": self.player.position 248 | } 249 | } 250 | self.alexa.send_event(event) 251 | 252 | def resume(self): 253 | self.player.resume() 254 | self.PlaybackResumed() 255 | 256 | logger.info('audio player is resumed') 257 | 258 | def PlaybackResumed(self): 259 | self.state = 'PLAYING' 260 | event = { 261 | "header": { 262 | "namespace": "AudioPlayer", 263 | "name": "PlaybackResumed", 264 | "messageId": uuid.uuid4().hex 265 | }, 266 | "payload": { 267 | "token": self.token, 268 | "offsetInMilliseconds": self.player.position 269 | } 270 | } 271 | self.alexa.send_event(event) 272 | 273 | # { 274 | # "directive": { 275 | # "header": { 276 | # "namespace": "AudioPlayer", 277 | # "name": "ClearQueue", 278 | # "messageId": "{{STRING}}", 279 | # "dialogRequestId": "{{STRING}}" 280 | # }, 281 | # "payload": { 282 | # "clearBehavior": "{{STRING}}" 283 | # } 284 | # } 285 | # } 286 | def ClearQueue(self, directive): 287 | self.PlaybackQueueCleared() 288 | behavior = directive['payload']['clearBehavior'] 289 | if behavior == 'CLEAR_ALL': 290 | self.player.stop() 291 | elif behavior == 'CLEAR_ENQUEUED': 292 | pass 293 | 294 | def PlaybackQueueCleared(self): 295 | event = { 296 | "header": { 297 | "namespace": "AudioPlayer", 298 | "name": "PlaybackQueueCleared", 299 | "messageId": uuid.uuid4().hex 300 | }, 301 | "payload": {} 302 | } 303 | self.alexa.send_event(event) 304 | 305 | def StreamMetadataExtracted(self): 306 | pass 307 | 308 | @property 309 | def context(self): 310 | if self.state != 'PLAYING': 311 | offset = 0 312 | else: 313 | offset = self.player.position 314 | 315 | return { 316 | "header": { 317 | "namespace": "AudioPlayer", 318 | "name": "PlaybackState" 319 | }, 320 | "payload": { 321 | "token": self.token, 322 | "offsetInMilliseconds": offset, 323 | "playerActivity": self.state 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /avs/interface/notifications.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respeaker/avs/affcb7dd1b7ab7461ab812c4e0cd448cbc95f076/avs/interface/notifications.py -------------------------------------------------------------------------------- /avs/interface/playback_controller.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respeaker/avs/affcb7dd1b7ab7461ab812c4e0cd448cbc95f076/avs/interface/playback_controller.py -------------------------------------------------------------------------------- /avs/interface/speaker.py: -------------------------------------------------------------------------------- 1 | class Speaker(object): 2 | def __init__(self, alexa): 3 | self.alexa = alexa 4 | self.volume = 50 5 | self.muted = False 6 | 7 | self.set_volume_cb = None 8 | self.get_volume_cb = None 9 | self.set_mute_cb = None 10 | 11 | def SetVolume(self, directive): 12 | vol = directive['payload']['volume'] 13 | if self.set_volume_cb: 14 | self.set_volume_cb(vol) 15 | self.volume = vol 16 | 17 | def CallbackSetVolume(self, func): 18 | self.set_volume_cb = func 19 | 20 | def AdjustVolume(self, directive): 21 | vol = directive['payload']['volume'] 22 | if self.get_volume_cb: 23 | self.volume = self.get_volume_cb() 24 | 25 | self.volume += vol 26 | if (self.volume > 100): 27 | self.volume = 100 28 | elif (self.volume < 0): 29 | self.volume = 0 30 | 31 | if self.set_volume_cb: 32 | self.set_volume_cb(self.volume) 33 | 34 | 35 | def CallbackGetVolume(self, func): 36 | self.get_volume_cb = func 37 | 38 | def VolumeChanged(self): 39 | event = { 40 | "event": { 41 | "header": { 42 | "namespace": "Speaker", 43 | "name": "VolumeChanged", 44 | "messageId": "{{STRING}}" 45 | }, 46 | "payload": { 47 | "volume": self.volume, 48 | "muted": self.muted 49 | } 50 | } 51 | } 52 | self.alexa.send_event(event) 53 | 54 | def SetMute(self, directive): 55 | muted = directive["payload"]["mute"] 56 | if self.set_mute_cb: 57 | self.set_mute_cb(muted) 58 | self.muted = muted 59 | 60 | 61 | def CallbackSetMute(self, func): 62 | self.set_mute_cb = func 63 | 64 | def MuteChanged(self): 65 | event = { 66 | "event": { 67 | "header": { 68 | "namespace": "Speaker", 69 | "name": "MuteChanged", 70 | "messageId": "{{STRING}}" 71 | }, 72 | "payload": { 73 | "volume": self.volume, 74 | "muted": self.muted 75 | } 76 | } 77 | } 78 | self.alexa.send_event(event) 79 | 80 | @property 81 | def context(self): 82 | return { 83 | "header": { 84 | "namespace": "Speaker", 85 | "name": "VolumeState" 86 | }, 87 | "payload": { 88 | "volume": self.volume, 89 | "muted": self.muted, 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /avs/interface/speech_recognizer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speechrecognizer""" 4 | 5 | import logging 6 | import sys 7 | import threading 8 | import time 9 | import uuid 10 | 11 | if sys.version_info < (3, 0): 12 | import Queue as queue 13 | else: 14 | import queue 15 | 16 | logger = logging.getLogger('SpeechRecognizer') 17 | 18 | 19 | class SpeechRecognizer(object): 20 | STATES = {'IDLE', 'RECOGNIZING', 'BUSY', 'EXPECTING SPEECH'} 21 | PROFILES = {'CLOSE_TALK', 'NEAR_FIELD', 'FAR_FIELD'} 22 | PRESS_AND_HOLD = {'type': 'PRESS_AND_HOLD', 'payload': {}} 23 | TAP = {'type': 'TAP', 'payload': {}} 24 | WAKEWORD = {'type': 'WAKEWORD', 'payload': {}} 25 | 26 | def __init__(self, alexa): 27 | self.alexa = alexa 28 | self.profile = 'FAR_FIELD' 29 | 30 | self.dialog_request_id = '' 31 | 32 | self.audio_queue = queue.Queue(maxsize=1000) 33 | self.listening = False 34 | self.conversation = 0 35 | self.lock = threading.RLock() 36 | 37 | def put(self, audio): 38 | """ 39 | Put audio into queue when listening 40 | :param audio: S16_LE format, sample rate 16000 bps audio data 41 | :return: None 42 | """ 43 | if self.listening: 44 | self.audio_queue.put(audio) 45 | 46 | def Recognize(self, dialog=None, initiator=None, timeout=10000): 47 | """ 48 | recognize 49 | :param dialog: 50 | :param initiator: 51 | :param timeout: 52 | :return: 53 | """ 54 | if self.listening: 55 | logger.debug('Already listening. Ignore') 56 | return 57 | 58 | logger.debug('Starting listening') 59 | 60 | self.audio_queue.queue.clear() 61 | self.listening = True 62 | 63 | with self.lock: 64 | self.conversation += 1 65 | 66 | def on_finished(): 67 | if self.alexa.SpeechSynthesizer.state == 'PLAYING': 68 | logger.info('wait until speech synthesizer is finished') 69 | self.alexa.SpeechSynthesizer.wait() 70 | logger.info('synthesizer is finished') 71 | 72 | with self.lock: 73 | self.conversation -= 1 74 | logger.info('conversation = {}'.format(self.conversation)) 75 | if not self.conversation: 76 | self.alexa.state_listener.on_finished() 77 | 78 | if self.alexa.AudioPlayer.state == 'PAUSED': 79 | self.alexa.AudioPlayer.resume() 80 | 81 | # Stop playing if Alexa is speaking or AudioPlayer is playing 82 | if self.alexa.SpeechSynthesizer.state == 'PLAYING': 83 | logger.info('stop speech synthesizer') 84 | self.alexa.SpeechSynthesizer.stop() 85 | elif self.alexa.Alerts.state == 'FOREGROUND': 86 | logger.info('stop alert(s)') 87 | self.alexa.Alerts.stop() 88 | elif self.alexa.AudioPlayer.state == 'PLAYING': 89 | logger.info('pause audio player') 90 | self.alexa.AudioPlayer.pause() 91 | 92 | self.alexa.state_listener.on_listening() 93 | 94 | self.dialog_request_id = dialog if dialog else uuid.uuid4().hex 95 | 96 | if initiator is None: 97 | initiator = self.WAKEWORD 98 | 99 | event = { 100 | "header": { 101 | "namespace": "SpeechRecognizer", 102 | "name": "Recognize", 103 | "messageId": uuid.uuid4().hex, 104 | "dialogRequestId": self.dialog_request_id 105 | }, 106 | "payload": { 107 | "profile": self.profile, 108 | "format": "AUDIO_L16_RATE_16000_CHANNELS_1", 109 | 'initiator': initiator 110 | } 111 | } 112 | 113 | def gen(): 114 | time_elapsed = 0 115 | while self.listening and time_elapsed <= timeout: 116 | try: 117 | chunk = self.audio_queue.get(timeout=1.0) 118 | except queue.Empty: 119 | break 120 | 121 | yield chunk 122 | time_elapsed += len(chunk) * 1000 / (2 * 16000) # 16000 fs, 2 bytes width 123 | logger.debug('Sending chunk, time_elapsed = {}'.format(time_elapsed)) 124 | 125 | self.listening = False 126 | self.alexa.state_listener.on_thinking() 127 | 128 | self.alexa.send_event(event, listener=on_finished, attachment=gen()) 129 | 130 | # { 131 | # "directive": { 132 | # "header": { 133 | # "namespace": "SpeechRecognizer", 134 | # "name": "StopCapture", 135 | # "messageId": "{{STRING}}", 136 | # "dialogRequestId": "{{STRING}}" 137 | # }, 138 | # "payload": { 139 | # } 140 | # } 141 | # } 142 | def StopCapture(self, directive): 143 | self.listening = False 144 | logger.info('StopCapture') 145 | 146 | # { 147 | # "directive": { 148 | # "header": { 149 | # "namespace": "SpeechRecognizer", 150 | # "name": "ExpectSpeech", 151 | # "messageId": "{{STRING}}", 152 | # "dialogRequestId": "{{STRING}}" 153 | # }, 154 | # "payload": { 155 | # "timeoutInMilliseconds": {{LONG}}, 156 | # "initiator": "{{STRING}}" 157 | # } 158 | # } 159 | # } 160 | def ExpectSpeech(self, directive): 161 | while self.alexa.SpeechSynthesizer.state == 'PLAYING': 162 | time.sleep(0.1) 163 | 164 | dialog = directive['header']['dialogRequestId'] 165 | timeout = directive['payload']['timeoutInMilliseconds'] 166 | 167 | initiator = None 168 | if 'initiator' in directive['payload']: 169 | initiator = directive['payload']['initiator'] 170 | 171 | self.Recognize(dialog=dialog, initiator=initiator, timeout=timeout) 172 | 173 | def ExpectSpeechTimedOut(self): 174 | event = { 175 | "header": { 176 | "namespace": "SpeechRecognizer", 177 | "name": "ExpectSpeechTimedOut", 178 | "messageId": uuid.uuid4().hex, 179 | }, 180 | "payload": {} 181 | } 182 | self.alexa.send_event(event) 183 | 184 | @property 185 | def context(self): 186 | return { 187 | "header": { 188 | "namespace": "SpeechRecognizer", 189 | "name": "RecognizerState" 190 | }, 191 | "payload": { 192 | "wakeword": "ALEXA" 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /avs/interface/speech_synthesizer.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import os 4 | import tempfile 5 | import threading 6 | import uuid 7 | 8 | # prefer mpg123 player as it is more responsive than mpv and gstreamer 9 | if os.system('which mpg123 >/dev/null') == 0: 10 | from avs.player.mpg123_player import Player 11 | else: 12 | from avs.player import Player 13 | 14 | logger = logging.getLogger('SpeechSynthesizer') 15 | 16 | 17 | class SpeechSynthesizer(object): 18 | STATES = {'PLAYING', 'FINISHED'} 19 | 20 | def __init__(self, alexa): 21 | self.alexa = alexa 22 | self.token = '' 23 | self._state = 'FINISHED' 24 | self.finished = threading.Event() 25 | 26 | self.player = Player() 27 | self.player.add_callback('eos', self.SpeechFinished) 28 | self.player.add_callback('error', self.SpeechFinished) 29 | self.mp3_file = None 30 | 31 | def stop(self): 32 | self.finished.set() 33 | self.player.stop() 34 | self._state = 'FINISHED' 35 | 36 | def wait(self): 37 | self.finished.wait() 38 | 39 | # { 40 | # "directive": { 41 | # "header": { 42 | # "namespace": "SpeechSynthesizer", 43 | # "name": "Speak", 44 | # "messageId": "{{STRING}}", 45 | # "dialogRequestId": "{{STRING}}" 46 | # }, 47 | # "payload": { 48 | # "url": "{{STRING}}", 49 | # "format": "AUDIO_MPEG", 50 | # "token": "{{STRING}}" 51 | # } 52 | # } 53 | # } 54 | # Content-Type: application/octet-stream 55 | # Content-ID: {{Audio Item CID}} 56 | # {{BINARY AUDIO ATTACHMENT}} 57 | def Speak(self, directive): 58 | # directive from dueros may not have the dialogRequestId 59 | if 'dialogRequestId' in directive['header']: 60 | dialog_request_id = directive['header']['dialogRequestId'] 61 | if self.alexa.SpeechRecognizer.dialog_request_id != dialog_request_id: 62 | return 63 | 64 | if self.alexa.Alerts.state == 'FOREGROUND': 65 | logger.info('stop alert(s)') 66 | self.alexa.Alerts.stop() 67 | elif self.alexa.AudioPlayer.state == 'PLAYING': 68 | logger.info('pause audio player') 69 | self.alexa.AudioPlayer.pause() 70 | 71 | self.token = directive['payload']['token'] 72 | url = directive['payload']['url'] 73 | if url.startswith('cid:'): 74 | filename = base64.urlsafe_b64encode(url[4:].encode('utf-8'))[:8].decode('utf-8') 75 | mp3_file = os.path.join(tempfile.gettempdir(), filename + '.mp3') 76 | if os.path.isfile(mp3_file): 77 | self.mp3_file = mp3_file 78 | 79 | self.finished.clear() 80 | self.SpeechStarted() 81 | # os.system('mpv "{}"'.format(mp3_file)) 82 | self.player.play('file://{}'.format(mp3_file)) 83 | 84 | logger.info('playing {}'.format(filename)) 85 | 86 | self.alexa.state_listener.on_speaking() 87 | 88 | # will be set at SpeechFinished() if the player reaches the End Of Stream or gets a error 89 | # self.finished.wait() 90 | else: 91 | logger.warning('not find {}'.format(mp3_file)) 92 | 93 | def SpeechStarted(self): 94 | self._state = 'PLAYING' 95 | event = { 96 | "header": { 97 | "namespace": "SpeechSynthesizer", 98 | "name": "SpeechStarted", 99 | "messageId": uuid.uuid4().hex 100 | }, 101 | "payload": { 102 | "token": self.token 103 | } 104 | } 105 | self.alexa.send_event(event) 106 | 107 | def SpeechFinished(self): 108 | if os.path.isfile(self.mp3_file): 109 | os.system('rm -rf "{}"'.format(self.mp3_file)) 110 | 111 | self.finished.set() 112 | self._state = 'FINISHED' 113 | 114 | # repeated operation, will cause bug when we try to listen() at 'on_speaking' state 115 | # self.alexa.state_listener.on_finished() 116 | 117 | if self.alexa.AudioPlayer.state == 'PAUSED': 118 | self.alexa.AudioPlayer.resume() 119 | 120 | event = { 121 | "header": { 122 | "namespace": "SpeechSynthesizer", 123 | "name": "SpeechFinished", 124 | "messageId": uuid.uuid4().hex 125 | }, 126 | "payload": { 127 | "token": self.token 128 | } 129 | } 130 | self.alexa.send_event(event) 131 | 132 | @property 133 | def state(self): 134 | if self._state == 'PLAYING' and self.player.state == 'PLAYING': 135 | s = 'PLAYING' 136 | else: 137 | s = 'FINISHED' 138 | 139 | # logger.debug('speech synthesizer is {}'.format(s)) 140 | return s 141 | 142 | @property 143 | def context(self): 144 | state = self.state 145 | offset = self.player.position if state == 'PLAYING' else 0 146 | 147 | return { 148 | "header": { 149 | "namespace": "SpeechSynthesizer", 150 | "name": "SpeechState" 151 | }, 152 | "payload": { 153 | "token": self.token, 154 | "offsetInMilliseconds": offset, 155 | "playerActivity": state 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /avs/interface/system.py: -------------------------------------------------------------------------------- 1 | """https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/system""" 2 | 3 | import datetime 4 | import uuid 5 | 6 | 7 | class System(object): 8 | def __init__(self, alexa): 9 | self.alexa = alexa 10 | self.last_inactive_report = datetime.datetime.utcnow() 11 | 12 | def SynchronizeState(self): 13 | event = { 14 | "header": { 15 | "namespace": "System", 16 | "name": "SynchronizeState", 17 | "messageId": uuid.uuid4().hex 18 | }, 19 | "payload": { 20 | } 21 | } 22 | 23 | def on_finished(): 24 | self.alexa.state_listener.on_ready() 25 | 26 | self.alexa.send_event(event, listener=on_finished) 27 | 28 | def UserInactivityReport(self): 29 | current = datetime.datetime.utcnow() 30 | dt = current - self.last_inactive_report 31 | 32 | # hourly report the amount of time elapsed since the last user activity 33 | if dt.seconds < (59 * 60): 34 | return 35 | 36 | self.last_inactive_report = current 37 | 38 | inactive_time = current - self.alexa.last_activity 39 | 40 | event = { 41 | "header": { 42 | "namespace": "System", 43 | "name": "UserInactivityReport", 44 | "messageId": uuid.uuid4().hex 45 | }, 46 | "payload": { 47 | "inactiveTimeInSeconds": inactive_time.seconds 48 | } 49 | 50 | } 51 | 52 | self.alexa.send_event(event) 53 | 54 | # { 55 | # "directive": { 56 | # "header": { 57 | # "namespace": "System", 58 | # "name": "ResetUserInactivity", 59 | # "messageId": "{{STRING}}" 60 | # }, 61 | # "payload": { 62 | # } 63 | # } 64 | # } 65 | def ResetUserInactivity(self, directive): 66 | self.alexa.last_activity = datetime.datetime.utcnow() 67 | 68 | # { 69 | # "directive": { 70 | # "header": { 71 | # "namespace": "System", 72 | # "name": "SetEndpoint", 73 | # "messageId": "{{STRING}}" 74 | # }, 75 | # "payload": { 76 | # "endpoint": "{{STRING}}" 77 | # } 78 | # } 79 | # } 80 | def SetEndpoint(self, directive): 81 | pass 82 | 83 | def ExceptionEncountered(self): 84 | event = { 85 | "header": { 86 | "namespace": "System", 87 | "name": "ExceptionEncountered", 88 | "messageId": "{{STRING}}" 89 | }, 90 | "payload": { 91 | "unparsedDirective": "{{STRING}}", 92 | "error": { 93 | "type": "{{STRING}}", 94 | "message": "{{STRING}}" 95 | } 96 | } 97 | } 98 | self.alexa.send_event(event) 99 | -------------------------------------------------------------------------------- /avs/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Hands-free alexa with respeaker using pocketsphinx to search keyword 5 | 6 | It depends on respeaker python library (https://github.com/respeaker/respeaker_python_library) 7 | """ 8 | 9 | import logging 10 | import signal 11 | import sys 12 | import threading 13 | import time 14 | 15 | if sys.version_info < (3, 0): 16 | import Queue as queue 17 | else: 18 | import queue 19 | 20 | from avs.alexa import Alexa 21 | from avs.mic import Audio 22 | from respeaker.pixel_ring import pixel_ring 23 | 24 | logger = logging.getLogger(__file__) 25 | logging.basicConfig(level=logging.INFO) 26 | 27 | 28 | class KWS(object): 29 | def __init__(self): 30 | self.queue = queue.Queue() 31 | 32 | self.sinks = [] 33 | self._callback = None 34 | 35 | self.done = False 36 | 37 | def put(self, data): 38 | self.queue.put(data) 39 | 40 | def start(self): 41 | self.done = False 42 | thread = threading.Thread(target=self.run) 43 | thread.daemon = True 44 | thread.start() 45 | 46 | def stop(self): 47 | self.done = True 48 | 49 | def link(self, sink): 50 | if hasattr(sink, 'put') and callable(sink.put): 51 | self.sinks.append(sink) 52 | else: 53 | raise ValueError('Not implement put() method') 54 | 55 | def unlink(self, sink): 56 | self.sinks.remove(sink) 57 | 58 | def set_callback(self, callback): 59 | self._callback = callback 60 | 61 | def run(self): 62 | from respeaker.microphone import Microphone 63 | 64 | decoder = Microphone.create_decoder() 65 | decoder.start_utt() 66 | 67 | while not self.done: 68 | chunk = self.queue.get() 69 | decoder.process_raw(chunk, False, False) 70 | hypothesis = decoder.hyp() 71 | if hypothesis: 72 | keyword = hypothesis.hypstr 73 | logger.info('Detected {}'.format(keyword)) 74 | 75 | if callable(self._callback): 76 | self._callback(keyword) 77 | 78 | decoder.end_utt() 79 | decoder.start_utt() 80 | 81 | for sink in self.sinks: 82 | sink.put(chunk) 83 | 84 | 85 | def main(): 86 | config = None if len(sys.argv) < 2 else sys.argv[1] 87 | 88 | audio = Audio(frames_size=1600) 89 | kws = KWS() 90 | alexa = Alexa(config) 91 | 92 | def speak(): 93 | pixel_ring.speak(10, 0) 94 | 95 | alexa.state_listener.on_listening = pixel_ring.listen 96 | alexa.state_listener.on_thinking = pixel_ring.wait 97 | alexa.state_listener.on_speaking = speak 98 | alexa.state_listener.on_finished = pixel_ring.off 99 | 100 | audio.link(kws) 101 | kws.link(alexa) 102 | 103 | def wakeup(keyword): 104 | if keyword.find('alexa') >= 0: 105 | alexa.listen() 106 | 107 | kws.set_callback(wakeup) 108 | 109 | alexa.start() 110 | kws.start() 111 | audio.start() 112 | 113 | is_quit = threading.Event() 114 | 115 | def signal_handler(signal, frame): 116 | print('Quit') 117 | is_quit.set() 118 | 119 | signal.signal(signal.SIGINT, signal_handler) 120 | 121 | while not is_quit.is_set(): 122 | time.sleep(1) 123 | 124 | alexa.stop() 125 | kws.stop() 126 | audio.stop() 127 | 128 | 129 | if __name__ == '__main__': 130 | main() 131 | -------------------------------------------------------------------------------- /avs/mic/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | recorder_option = os.getenv('RECORDER', 'default').lower() 6 | 7 | if recorder_option.find('pyaudio') >= 0 or os.system('which arecord >/dev/null') != 0: 8 | from .pyaudio_recorder import Audio 9 | else: 10 | from .alsa_recorder import Audio 11 | 12 | __all__ = ['Audio'] 13 | -------------------------------------------------------------------------------- /avs/mic/alsa_recorder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import subprocess 4 | import threading 5 | 6 | 7 | class Audio(object): 8 | def __init__(self, rate=16000, frames_size=160, channels=1, device_name='default'): 9 | self.rate = rate 10 | self.frames_size = frames_size 11 | self.channels = channels 12 | self.device_name = device_name 13 | self.done = False 14 | self.thread = None 15 | self.sinks = [] 16 | 17 | def run(self): 18 | cmd = [ 19 | 'arecord', 20 | '-t', 'raw', 21 | '-f', 'S16_LE', 22 | '-c', str(self.channels), 23 | '-r', str(self.rate), 24 | '-D', self.device_name, 25 | '-q' 26 | ] 27 | process = subprocess.Popen(cmd, stdout=subprocess.PIPE) 28 | 29 | frames_bytes = int(self.frames_size * self.channels * 2) 30 | while not self.done: 31 | audio = process.stdout.read(frames_bytes) 32 | for sink in self.sinks: 33 | sink.put(audio) 34 | 35 | process.kill() 36 | 37 | def start(self): 38 | self.done = False 39 | self.thread = threading.Thread(target=self.run) 40 | self.thread.daemon = True 41 | self.thread.start() 42 | 43 | def stop(self): 44 | self.done = True 45 | if self.thread and self.thread.is_alive(): 46 | self.thread.join(timeout=3) 47 | 48 | def link(self, sink): 49 | if hasattr(sink, 'put') and callable(sink.put): 50 | self.sinks.append(sink) 51 | else: 52 | raise ValueError('Not implement put() method') 53 | 54 | def unlink(self, sink): 55 | self.sinks.remove(sink) 56 | -------------------------------------------------------------------------------- /avs/mic/pyaudio_recorder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import logging 5 | import pyaudio 6 | 7 | logger = logging.getLogger(__file__) 8 | 9 | 10 | class Audio(object): 11 | def __init__(self, rate=16000, frames_size=None, channels=None, device_index=None): 12 | self.sample_rate = rate 13 | self.frames_size = frames_size if frames_size else rate / 100 14 | self.channels = channels if channels else 1 15 | 16 | self.pyaudio_instance = pyaudio.PyAudio() 17 | 18 | if device_index is None: 19 | if channels: 20 | for i in range(self.pyaudio_instance.get_device_count()): 21 | dev = self.pyaudio_instance.get_device_info_by_index(i) 22 | name = dev['name'].encode('utf-8') 23 | logger.info('{}:{} with {} input channels'.format(i, name, dev['maxInputChannels'])) 24 | if dev['maxInputChannels'] == channels: 25 | logger.info('Use {}'.format(name)) 26 | device_index = i 27 | break 28 | else: 29 | device_index = self.pyaudio_instance.get_default_input_device_info()['index'] 30 | 31 | if device_index is None: 32 | raise Exception('Can not find an input device with {} channel(s)'.format(channels)) 33 | 34 | self.stream = self.pyaudio_instance.open( 35 | start=False, 36 | format=pyaudio.paInt16, 37 | input_device_index=device_index, 38 | channels=self.channels, 39 | rate=int(self.sample_rate), 40 | frames_per_buffer=int(self.frames_size), 41 | stream_callback=self._callback, 42 | input=True 43 | ) 44 | 45 | self.sinks = [] 46 | 47 | def _callback(self, in_data, frame_count, time_info, status): 48 | for sink in self.sinks: 49 | sink.put(in_data) 50 | return None, pyaudio.paContinue 51 | 52 | def start(self): 53 | self.stream.start_stream() 54 | 55 | def stop(self): 56 | self.stream.stop_stream() 57 | 58 | def link(self, sink): 59 | if hasattr(sink, 'put') and callable(sink.put): 60 | self.sinks.append(sink) 61 | else: 62 | raise ValueError('Not implement put() method') 63 | 64 | def unlink(self, sink): 65 | self.sinks.remove(sink) 66 | -------------------------------------------------------------------------------- /avs/player/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Player 4 | support mpv, mpg123 and gstreamer 1.0 5 | It likes gstreamer 1.0 > mpv > mpg123 6 | We can specify a player using environment variable PLAYER (mpv, mpg123, gstreamer) 7 | """ 8 | 9 | import os 10 | 11 | player_option = os.getenv('PLAYER', 'default').lower() 12 | 13 | if player_option.find('mpv') >= 0: 14 | from .mpv_player import Player 15 | elif player_option.find('mpg123') >= 0: 16 | from .mpg123_player import Player 17 | elif player_option.find('gstreamer') >= 0: 18 | from .gstreamer_player import Player 19 | else: 20 | try: 21 | from .gstreamer_player import Player 22 | except ImportError: 23 | if os.system('which mpv >/dev/null') == 0: 24 | from .mpv_player import Player 25 | elif os.system('which mpg123 >/dev/null') == 0: 26 | from .mpg123_player import Player 27 | else: 28 | raise ImportError('No player available, install one of the players: gstreamer, mpv and mpg123 first') 29 | 30 | __all__ = ['Player'] 31 | -------------------------------------------------------------------------------- /avs/player/gstreamer_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Player using gstreamer.""" 4 | 5 | import gi 6 | import threading 7 | import time 8 | 9 | gi.require_version('Gst', '1.0') 10 | from gi.repository import Gst, GLib, GObject 11 | 12 | 13 | def setup_message_handler(): 14 | GObject.threads_init() 15 | Gst.init(None) 16 | loop = GLib.MainLoop() 17 | 18 | t = threading.Thread(target=loop.run) 19 | t.daemon = True 20 | t.start() 21 | 22 | return t 23 | 24 | 25 | class Player(object): 26 | message_handler = None 27 | 28 | def __init__(self): 29 | if Player.message_handler is None: 30 | Player.message_handler = setup_message_handler() 31 | 32 | self.callbacks = {} 33 | self.player = Gst.ElementFactory.make("playbin", "player") 34 | bus = self.player.get_bus() 35 | bus.add_signal_watch() 36 | bus.connect('message', self.on_message) 37 | 38 | # bus.enable_sync_message_emission() 39 | # bus.connect('sync-message::eos', self.on_eos) 40 | 41 | def play(self, uri): 42 | self.player.set_state(Gst.State.NULL) 43 | self.player.set_property('uri', uri) 44 | self.player.set_state(Gst.State.PLAYING) 45 | 46 | def stop(self): 47 | self.player.set_state(Gst.State.NULL) 48 | 49 | def pause(self): 50 | self.player.set_state(Gst.State.PAUSED) 51 | 52 | def resume(self): 53 | self.player.set_state(Gst.State.PLAYING) 54 | 55 | # name: {eos, error, ...} 56 | def add_callback(self, name, callback): 57 | if not callable(callback): 58 | return 59 | 60 | self.callbacks[name] = callback 61 | 62 | def on_message(self, bus, message): 63 | if message.type == Gst.MessageType.EOS: 64 | self.player.set_state(Gst.State.NULL) 65 | if 'eos' in self.callbacks: 66 | self.callbacks['eos']() 67 | elif message.type == Gst.MessageType.ERROR: 68 | self.player.set_state(Gst.State.NULL) 69 | if 'error' in self.callbacks: 70 | self.callbacks['error']() 71 | # else: 72 | # print(message.type) 73 | 74 | @property 75 | def duration(self): 76 | for _ in range(10): 77 | success, duration = self.player.query_duration(Gst.Format.TIME) 78 | if success: 79 | break 80 | time.sleep(0.1) 81 | 82 | return int(duration / Gst.MSECOND) 83 | 84 | @property 85 | def position(self): 86 | for _ in range(10): 87 | success, position = self.player.query_position(Gst.Format.TIME) 88 | if success: 89 | break 90 | time.sleep(0.1) 91 | 92 | return int(position / Gst.MSECOND) 93 | 94 | @property 95 | def state(self): 96 | # GST_STATE_VOID_PENDING no pending state. 97 | # GST_STATE_NULL the NULL state or initial state of an element. 98 | # GST_STATE_READY the element is ready to go to PAUSED. 99 | # GST_STATE_PAUSED the element is PAUSED, it is ready to accept and process data. 100 | # Sink elements however only accept one buffer and then block. 101 | # GST_STATE_PLAYING the element is PLAYING, the GstClock is running and the data is flowing. 102 | _, state, _ = self.player.get_state(Gst.SECOND) 103 | return 'FINISHED' if state != Gst.State.PLAYING else 'PLAYING' 104 | -------------------------------------------------------------------------------- /avs/player/mpg123_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Player using mpg123""" 4 | 5 | import os 6 | import subprocess 7 | import threading 8 | 9 | if os.system('which mpg123 >/dev/null') != 0: 10 | ImportError('mpg123 not found, install it first') 11 | 12 | 13 | class Player(object): 14 | def __init__(self): 15 | self.callbacks = {} 16 | self.process = None 17 | self.state = 'NULL' 18 | self.audio = None 19 | self.tty = None 20 | 21 | self.event = threading.Event() 22 | t = threading.Thread(target=self._run) 23 | t.daemon = True 24 | t.start() 25 | 26 | def _run(self): 27 | while True: 28 | self.event.wait() 29 | self.event.clear() 30 | print('Playing {}'.format(self.audio)) 31 | 32 | master, slave = os.openpty() 33 | self.process = subprocess.Popen(['mpg123', '-q', '-C', self.audio], stdin=master) 34 | self.tty = slave 35 | 36 | self.process.wait() 37 | print('Finished {}'.format(self.audio)) 38 | 39 | if not self.event.is_set(): 40 | self.on_eos() 41 | 42 | def play(self, uri): 43 | if uri.startswith('file://'): 44 | uri = uri[7:] 45 | 46 | self.audio = uri 47 | self.event.set() 48 | 49 | if self.process and self.process.poll() == None: 50 | os.write(self.tty, b'q') 51 | 52 | self.state = 'PLAYING' 53 | 54 | def stop(self): 55 | if self.process and self.process.poll() == None: 56 | os.write(self.tty, b'q') 57 | self.state = 'NULL' 58 | 59 | def pause(self): 60 | if self.state == 'PLAYING': 61 | self.state = 'PAUSED' 62 | os.write(self.tty, b's') 63 | 64 | print('pause()') 65 | 66 | def resume(self): 67 | if self.state == 'PAUSED': 68 | self.state = 'PLAYING' 69 | os.write(self.tty, b's') 70 | 71 | # name: {eos, ...} 72 | def add_callback(self, name, callback): 73 | if not callable(callback): 74 | return 75 | 76 | self.callbacks[name] = callback 77 | 78 | def on_eos(self): 79 | self.state = 'NULL' 80 | if 'eos' in self.callbacks: 81 | self.callbacks['eos']() 82 | 83 | @property 84 | def duration(self): 85 | return 0 86 | 87 | @property 88 | def position(self): 89 | return 0 90 | -------------------------------------------------------------------------------- /avs/player/mpv_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Player using MPV""" 4 | 5 | import os 6 | import subprocess 7 | import threading 8 | 9 | if os.system('which mpv >/dev/null') != 0: 10 | raise ImportError('mpv not found, install it first') 11 | 12 | 13 | class Player(object): 14 | def __init__(self): 15 | self.callbacks = {} 16 | self.process = None 17 | self.state = 'NULL' 18 | self.audio = None 19 | self.tty = None 20 | 21 | self.event = threading.Event() 22 | t = threading.Thread(target=self._run) 23 | t.daemon = True 24 | t.start() 25 | 26 | def _run(self): 27 | while True: 28 | self.event.wait() 29 | self.event.clear() 30 | print('Playing {}'.format(self.audio)) 31 | 32 | master, slave = os.openpty() 33 | self.process = subprocess.Popen(['mpv', '--no-video', self.audio], stdin=master) 34 | self.tty = slave 35 | 36 | self.process.wait() 37 | print('Finished {}'.format(self.audio)) 38 | 39 | if not self.event.is_set(): 40 | self.on_eos() 41 | 42 | def play(self, uri): 43 | self.audio = uri 44 | self.event.set() 45 | 46 | if self.process and self.process.poll() == None: 47 | os.write(self.tty, b'q') 48 | 49 | self.state = 'PLAYING' 50 | 51 | def stop(self): 52 | if self.process and self.process.poll() == None: 53 | os.write(self.tty, b'q') 54 | self.state = 'NULL' 55 | 56 | def pause(self): 57 | if self.state == 'PLAYING': 58 | self.state = 'PAUSED' 59 | os.write(self.tty, ' ') 60 | 61 | print('pause()') 62 | 63 | def resume(self): 64 | if self.state == 'PAUSED': 65 | self.state = 'PLAYING' 66 | os.write(self.tty, b' ') 67 | 68 | # name: {eos, ...} 69 | def add_callback(self, name, callback): 70 | if not callable(callback): 71 | return 72 | 73 | self.callbacks[name] = callback 74 | 75 | def on_eos(self): 76 | self.state = 'NULL' 77 | if 'eos' in self.callbacks: 78 | self.callbacks['eos']() 79 | 80 | @property 81 | def duration(self): 82 | return 0 83 | 84 | @property 85 | def position(self): 86 | return 0 87 | -------------------------------------------------------------------------------- /avs/resources/README.md: -------------------------------------------------------------------------------- 1 | 2 | Resources from https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/content/alexa-voice-service-ux-design-guidelines 3 | -------------------------------------------------------------------------------- /avs/resources/alarm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respeaker/avs/affcb7dd1b7ab7461ab812c4e0cd448cbc95f076/avs/resources/alarm.mp3 -------------------------------------------------------------------------------- /avs/resources/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 48 | 49 | 50 | 53 | 56 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:avs/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | # Define setup.py command aliases here 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import setup, find_packages 7 | 8 | 9 | with open('README.md') as f: 10 | long_description = f.read() 11 | 12 | 13 | requirements = [ 14 | 'click', 15 | 'hyper', 16 | 'tornado==5.1.1', 17 | 'requests', 18 | 'python-dateutil' 19 | ] 20 | 21 | setup_requirements = [ 22 | 'wheel' 23 | ] 24 | 25 | test_requirements = [ 26 | 'pytest' 27 | ] 28 | 29 | setup( 30 | name='avs', 31 | version='0.5.5', 32 | description="Alexa Voice Service Python SDK", 33 | long_description=long_description, 34 | long_description_content_type='text/markdown', 35 | author="Yihui Xiong", 36 | author_email='yihui.xiong@hotmail.com', 37 | url='https://github.com/respeaker/avs', 38 | packages=find_packages(include=['avs']), 39 | include_package_data=True, 40 | install_requires=requirements, 41 | entry_points={ 42 | 'console_scripts': [ 43 | 'alexa=avs.main:main', 44 | 'alexa-tap=avs.alexa:main', 45 | 'alexa-auth=avs.auth:main', 46 | 'dueros-auth=avs.auth:main', 47 | 'alexa-audio-check=avs.check:main' 48 | ], 49 | }, 50 | license="GNU General Public License v3", 51 | zip_safe=False, 52 | keywords='alexa voice service', 53 | classifiers=[ 54 | 'Development Status :: 2 - Pre-Alpha', 55 | 'Intended Audience :: Developers', 56 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 57 | 'Natural Language :: English', 58 | "Programming Language :: Python :: 2", 59 | 'Programming Language :: Python :: 2.7', 60 | 'Programming Language :: Python :: 3', 61 | 'Programming Language :: Python :: 3.4', 62 | 'Programming Language :: Python :: 3.5', 63 | ], 64 | test_suite='tests', 65 | tests_require=test_requirements, 66 | setup_requires=setup_requirements, 67 | ) 68 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for avs.""" 4 | -------------------------------------------------------------------------------- /tests/test_avs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `avs` package.""" 5 | 6 | 7 | import unittest 8 | 9 | 10 | class TestAlexa(unittest.TestCase): 11 | """Tests for `avs` package.""" 12 | 13 | def setUp(self): 14 | """Set up test fixtures, if any.""" 15 | 16 | def tearDown(self): 17 | """Tear down test fixtures, if any.""" 18 | 19 | def test_000_something(self): 20 | """Test something.""" 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, flake8 3 | 4 | [travis] 5 | python = 6 | 3.5: py35 7 | 3.4: py34 8 | 2.7: py27 9 | 10 | [testenv:flake8] 11 | basepython=python 12 | deps=flake8 13 | commands=flake8 avs 14 | 15 | [testenv] 16 | setenv = 17 | PYTHONPATH = {toxinidir} 18 | 19 | commands = python setup.py test 20 | 21 | ; If you want to make tox run the tests with the same versions, create a 22 | ; requirements.txt with the pinned versions and uncomment the following lines: 23 | ; deps = 24 | ; -r{toxinidir}/requirements.txt 25 | --------------------------------------------------------------------------------