├── .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://pypi.python.org/pypi/avs)
5 | [](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 |
--------------------------------------------------------------------------------