Skip to content
This repository has been archived by the owner on Dec 28, 2021. It is now read-only.

Commit

Permalink
add demo scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
richtier committed Jan 4, 2019
1 parent fb24bc9 commit 7f8ee2e
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ test:
--cov-config=.coveragerc \
--capture=no \
--last-failed \
--verbose
-vv

.PHONY: build publish_test publish test_requirements test
80 changes: 38 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
pip install avs_client
```

## Usage ##
or if you want to run the demos:

```sh
pip install avs_client[demo]
```

## Usage

### File audio ###
```py
Expand All @@ -32,11 +38,12 @@ with open('./tests/resources/alexa_what_time_is_it.wav', 'rb') as f:
f.write(directive.audio_attachment)
```

Now listen to `output.wav` and Alexa should tell you the time.
Now listen to `output_0.wav` and Alexa should tell you the time.

### Microphone audio

```py
from io import BytesIO
import io

from avs_client import AlexaVoiceServiceClient
import pyaudio
Expand All @@ -61,7 +68,7 @@ alexa_client = AlexaVoiceServiceClient(
refresh_token='my-refresh-token',
)

buffer = BytesIO()
buffer = io.BytesIO()
try:
stream.start_stream()
print('listening. Press CTRL + C to exit.')
Expand All @@ -80,47 +87,34 @@ finally:

An Alexa command may relate to a previous command e.g,

[you] "Alexa, open the magic door"
[Alexa] "...do you want to go to the dark forest or turn back?"
[you] "Go to the dark forest"
[Alexa] "...it's creepy. Keep going or turn back?"
[you] "Keep going"
[Alexa] "...it's getting darker."
[you] "Alexa, play twenty questions"
[Alexa] "Is it a animal, mineral, or vegetable?"
[you] "Mineral"
[Alexa] "Is it valuable"
[you] "No"
[Alexa] "is it..."

This request lifecycle can be managed by passing in a dialog request id:
This can be achieved by passing the same dialog request ID to multiple `send_audio_file` calls:

```py
import io

from avs_client import AlexaVoiceServiceClient
from avs_client.avs_client import helpers
from pydub import AudioSegment
from pydub.playback import play


alexa_client = AlexaVoiceServiceClient(
client_id='my-client-id',
secret='my-secret',
refresh_token='my-refresh-token',
)
alexa_client.connect() # authenticate and other handshaking steps

input_file_names = [
'./tests/resources/alexa_open_magic_door.wav',
'./tests/resources/alexa_go_to_dark_forest.wav',
'./tests/resources/alexa_keep_going.wav',
]

dialog_request_id = helpers.generate_unique_id()
for input_file_name in input_file_names:
with open(input_file_name, 'rb') as f:
for directive in alexa_client.send_audio_file(f, dialog_request_id=dialog_request_id):
if directive.name in ['Speak', 'Play']:
track = AudioSegment.from_mp3(io.BytesIO(directive.audio_attachment))
play(track)
directives_one = alexa_client.send_audio_file(audio_one, dialog_request_id=dialog_request_id)
directives_two = alexa_client.send_audio_file(audio_two, dialog_request_id=dialog_request_id)
directives_three = alexa_client.send_audio_file(audio_three, dialog_request_id=dialog_request_id)

```

Run the streaming microphone audio demo to use this feature:

```sh
pip install avs_client[demo]
python -m avs_client.demo.streaming_microphone \
--client-id="{enter-client-id-here}" \
--client-secret="{enter-client-secret-here"} \
--refresh-token="{enter-refresh-token-here}"
```

## Authentication

Expand All @@ -140,8 +134,8 @@ You will need to login to Amazon via a web browser to get your refresh token.

To enable this first go [here](https://developer.amazon.com/avs/home.html#/avs/home) and click on your product to set some security settings under `Security Profile`:

| setting | value |
| ------------------- | ---------------------------------|
| setting | value |
| ------------------- | --------------------------------|
| Allowed Origins | http://localhost:9000 |
| Allowed Return URLs | http://localhost:9000/callback/ |

Expand All @@ -150,10 +144,10 @@ Note what you entered for Product ID under Product Information, as this will be
Then run:

```sh
python ./avs_client/refreshtoken/serve.py \
--device-type-id=enter-device-type-id-here \
--client-id=enter-client-id-here \
--client-secret=enter-client-secret-here
python -m avs_client.refreshtoken.serve \
--device-type-id="{enter-device-type-id-here}" \
--client-id="{enter-client-id-here}" \
--client-secret="{enter-client-secret-here}"
```

Follow the on-screen instructions shown at `http://localhost:9000` in your web browser.
Expand Down Expand Up @@ -187,6 +181,8 @@ You will only need this if you intend to run the process for more than five minu
To run the unit tests, call the following commands:

```sh
git clone git@github.com:richtier/alexa-voice-service-client.git
pip install -e .[test]
make test_requirements
make test
```
Expand Down
1 change: 1 addition & 0 deletions avs_client/avs_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
authentication, connection, device, helpers, ping
)


class AlexaVoiceServiceClient:
authentication_manager_class = (
authentication.AlexaVoiceServiceTokenAuthenticator
Expand Down
7 changes: 5 additions & 2 deletions avs_client/avs_client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def send_audio_file(
'header': {
'namespace': 'SpeechRecognizer',
'name': 'Recognize',
'messageId': helpers.generate_unique_id(),
'messageId': self.generate_message_id(),
'dialogRequestId': dialog_request_id,
},
'payload': {
Expand Down Expand Up @@ -148,7 +148,10 @@ def ping(self, authentication_headers):
def parse_response(response):
if response.status == http.client.NO_CONTENT:
return None
yield
if not response.status == http.client.OK:
raise HTTPError(response=response)
return helpers.AVSMultipartDecoder(response).directives

@staticmethod
def generate_message_id():
return helpers.generate_unique_id()
Empty file added avs_client/demo/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions avs_client/demo/streaming_microphone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import argparse
import io
import time

from pydub import AudioSegment
from pydub.playback import play
import pyaudio

from avs_client import AlexaVoiceServiceClient
from avs_client.avs_client import helpers


def main(client_id, secret, refresh_token):
alexa_client = AlexaVoiceServiceClient(
client_id=client_id,
secret=secret,
refresh_token=refresh_token,
)

p = pyaudio.PyAudio()

def callback(in_data, frame_count, time_info, status):
input_buffer.write(in_data)
return (in_data, pyaudio.paContinue)

stream = p.open(
rate=16000,
channels=1,
format=pyaudio.paInt16,
input=True,
stream_callback=callback,
frames_per_buffer=128,
start=False
)

dialog_request_id = helpers.generate_unique_id()

try:
print('listening. Press CTRL + C to exit.')
input_buffer = io.BytesIO()
stream.start_stream()
print('Say something to Alexa.')
alexa_client.connect()
while True:
directives = alexa_client.send_audio_file(
input_buffer,
dialog_request_id=dialog_request_id
)
stream.stop_stream()
if directives:
print('Alexa\'s turn.')
for directive in directives:
if directive.name in ['Speak', 'Play']:
output_buffer = io.BytesIO(directive.audio_attachment)
track = AudioSegment.from_mp3(output_buffer)
play(track)
input_buffer = io.BytesIO()
stream.start_stream()
print('Your turn. Say something.')
time.sleep(1)
finally:
stream.stop_stream()
stream.close()
p.terminate()


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'-c', '--client-id', help='AVS client ID', required=True
)
parser.add_argument(
'-s', '--client-secret', help='AVS client secret', required=True
)
parser.add_argument(
'-r', '--refresh-token', help='AVS refresh token', required=True
)
parsed = parser.parse_args()
main(
client_id=parsed.client_id,
secret=parsed.client_secret,
refresh_token=parsed.refresh_token,
)
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
'twine>=1.11.0,<2.0.0',
'wheel>=0.31.0,<1.0.0',
'setuptools>=38.6.0,<39.0.0',
],
'demo': [
'pydub>=0.23.0,<1.0.0',
'pyaudio>=0.2.11,<1.0.0',
]
},
classifiers=[
Expand Down
6 changes: 5 additions & 1 deletion tests/avs_client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,16 @@ def test_client_send_audio_file(client):
client.device_manager.get_device_state.return_value = {'device': 'state'}

audio_file = BytesIO(b'things')
client.send_audio_file(audio_file)
client.send_audio_file(
audio_file,
dialog_request_id='dialog-id'
)

assert client.connection_manager.send_audio_file.call_args == call(
audio_file=audio_file,
device_state={'device': 'state'},
authentication_headers={'auth': 'value'},
dialog_request_id='dialog-id',
)
assert client.ping_manager.update_ping_deadline.call_count == 1

Expand Down
9 changes: 5 additions & 4 deletions tests/avs_client/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@


class TestConnectionManager(TestConnectionMixin, connection.ConnectionManager):
@staticmethod
def generate_dialogue_id():
# override existing non-deterministic method
return 'dialogue-id'

@staticmethod
def generate_message_id():
Expand Down Expand Up @@ -143,6 +139,7 @@ def test_send_audio_file(
device_state=device_state,
authentication_headers=authentication_headers,
audio_file=audio_file,
dialog_request_id='dialogue-id',
)

headers = dict(list(manager.connection.recent_stream.headers.items()))
Expand Down Expand Up @@ -208,6 +205,7 @@ def test_speak_and_play_response_200(
audio_file=audio_file,
device_state=device_state,
authentication_headers=authentication_headers,
dialog_request_id='dialogue-id',
))
assert len(directives) == 3

Expand Down Expand Up @@ -253,6 +251,7 @@ def test_parse_speak_response_200(
audio_file=audio_file,
device_state=device_state,
authentication_headers=authentication_headers,
dialog_request_id='dialogue-id',
)
for directive in directives:
assert directive.get_content_id(directive.directive) == (
Expand All @@ -273,6 +272,7 @@ def test_send_audio_204_response(
device_state=device_state,
authentication_headers=authentication_headers,
audio_file=audio_file,
dialog_request_id='dialogue-id',
)

assert response is None
Expand All @@ -292,6 +292,7 @@ def test_send_audio_non_200_response(
device_state=device_state,
authentication_headers=authentication_headers,
audio_file=audio_file,
dialog_request_id='dialogue-id',
)


Expand Down

0 comments on commit 7f8ee2e

Please sign in to comment.