From 25b5bd5370c4cf40c44a5b9fc078dc25f323b40e Mon Sep 17 00:00:00 2001 From: Nikhil Yogendra Murali Date: Fri, 19 Oct 2018 09:59:18 -0700 Subject: [PATCH] Initial release of audioplayer samples --- MultiStream/.ask/config | 32 + MultiStream/README.md | 115 +++ MultiStream/lambda/py/alexa/__init__.py | 0 MultiStream/lambda/py/alexa/data.py | 35 + MultiStream/lambda/py/alexa/util.py | 154 ++++ MultiStream/lambda/py/lambda_function.py | 733 ++++++++++++++++++ MultiStream/lambda/py/requirements.txt | 1 + MultiStream/models/en-GB.json | 80 ++ MultiStream/models/en-US.json | 79 ++ MultiStream/skill.json | 67 ++ README.md | 12 +- SingleStream/.ask/config | 53 ++ SingleStream/README.md | 123 +++ SingleStream/lambda/py/alexa/__init__.py | 0 SingleStream/lambda/py/alexa/data.py | 65 ++ SingleStream/lambda/py/alexa/util.py | 177 +++++ SingleStream/lambda/py/lambda_function.py | 497 ++++++++++++ SingleStream/lambda/py/locales/data.pot | 63 ++ .../py/locales/es-ES/LC_MESSAGES/data.mo | Bin 0 -> 1560 bytes .../py/locales/es-ES/LC_MESSAGES/data.po | 68 ++ .../py/locales/fr-FR/LC_MESSAGES/data.mo | Bin 0 -> 1524 bytes .../py/locales/fr-FR/LC_MESSAGES/data.po | 68 ++ .../py/locales/it-IT/LC_MESSAGES/data.mo | Bin 0 -> 1495 bytes .../py/locales/it-IT/LC_MESSAGES/data.po | 68 ++ SingleStream/lambda/py/requirements.txt | 2 + SingleStream/models/en-AU.json | 74 ++ SingleStream/models/en-CA.json | 74 ++ SingleStream/models/en-GB.json | 68 ++ SingleStream/models/en-IN.json | 70 ++ SingleStream/models/en-US.json | 70 ++ SingleStream/models/es-ES.json | 76 ++ SingleStream/models/es-MX.json | 76 ++ SingleStream/models/fr-FR.json | 73 ++ SingleStream/models/it-IT.json | 65 ++ SingleStream/skill.json | 201 +++++ 35 files changed, 3336 insertions(+), 3 deletions(-) create mode 100644 MultiStream/.ask/config create mode 100644 MultiStream/README.md create mode 100644 MultiStream/lambda/py/alexa/__init__.py create mode 100644 MultiStream/lambda/py/alexa/data.py create mode 100644 MultiStream/lambda/py/alexa/util.py create mode 100644 MultiStream/lambda/py/lambda_function.py create mode 100644 MultiStream/lambda/py/requirements.txt create mode 100644 MultiStream/models/en-GB.json create mode 100644 MultiStream/models/en-US.json create mode 100644 MultiStream/skill.json create mode 100644 SingleStream/.ask/config create mode 100644 SingleStream/README.md create mode 100644 SingleStream/lambda/py/alexa/__init__.py create mode 100644 SingleStream/lambda/py/alexa/data.py create mode 100644 SingleStream/lambda/py/alexa/util.py create mode 100644 SingleStream/lambda/py/lambda_function.py create mode 100644 SingleStream/lambda/py/locales/data.pot create mode 100644 SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.mo create mode 100644 SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.po create mode 100644 SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.mo create mode 100644 SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.po create mode 100644 SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.mo create mode 100644 SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.po create mode 100644 SingleStream/lambda/py/requirements.txt create mode 100644 SingleStream/models/en-AU.json create mode 100644 SingleStream/models/en-CA.json create mode 100644 SingleStream/models/en-GB.json create mode 100644 SingleStream/models/en-IN.json create mode 100644 SingleStream/models/en-US.json create mode 100644 SingleStream/models/es-ES.json create mode 100644 SingleStream/models/es-MX.json create mode 100644 SingleStream/models/fr-FR.json create mode 100644 SingleStream/models/it-IT.json create mode 100644 SingleStream/skill.json diff --git a/MultiStream/.ask/config b/MultiStream/.ask/config new file mode 100644 index 0000000..d755921 --- /dev/null +++ b/MultiStream/.ask/config @@ -0,0 +1,32 @@ +{ + "deploy_settings": { + "default": { + "skill_id": "amzn1.ask.skill.e5c21f3d-32a7-49b1-b341-d09b8f650fbe", + "was_cloned": false, + "resources": { + "manifest": { + "eTag": "1a9a9cec03b0583af5828d1db0d11ff9" + }, + "interactionModel": { + "en-US": { + "eTag": "8a62193c7f6432661809bc883985beb2" + }, + "en-GB": { + "eTag": "28bc1baebe415480fb571e1ac3d1c1c1" + } + } + }, + "merge": { + "manifest": { + "apis": { + "custom": { + "endpoint": { + "uri": "ask-custom-multistream-audioplayer-default" + } + } + } + } + } + } + } +} diff --git a/MultiStream/README.md b/MultiStream/README.md new file mode 100644 index 0000000..f9b661b --- /dev/null +++ b/MultiStream/README.md @@ -0,0 +1,115 @@ +# Multi Stream Audio Player Sample Skill (using ASK Python SDK) + +The Alexa Skills Kit now allows developers to build skills that play long-form audio content on Alexa devices. This sample project demonstrates how to use the new interfaces for triggering playback of audio and handling audio player input events. + +## How to Run the Sample + +You will need to comply to the prerequisites below and to change a few configuration files before creating the skill and upload the lambda code. + +### Pre-requisites + +0. This sample uses the [ASK Python SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/) packages for developing the Alexa skill. + + - If you already have the ASK Python SDK installed, then install the dependencies in the ``lambda/py/requirements.txt`` + using ``pip``. + - If you are starting fresh, follow the [Setting up the ASK SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html) + documentation, to get the ASK Python SDK installed on your machine. We recommend you to use the + [virtualenv approach](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html#option-1-set-up-the-sdk-in-a-virtual-environment). + Please run the following command in your virtualenv, to install the dependencies, before working on the skill code. + + `` + pip install -r lambda/py/requirements.txt + `` + +1. You need an [AWS account](https://aws.amazon.com) and an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. + + +### Code changes before deploying + +1. ```./skill.json``` + + Change the skill name, example phrase, icons, testing instructions etc ... + + Remember than most information is locale-specific and must be changed for each locale (en-GB and en-US) + + Please refer to https://developer.amazon.com/docs/smapi/skill-manifest.html for details about manifest values. + +2. ```./lambda/py/alexa/data.py``` + + Modify each value in the ```data.py``` file to provide your skill with the correct runtime values for ``AUDIO_DATA``, different responses by Alexa, DynamoDB table name. + + To learn more about Alexa App cards, see https://developer.amazon.com/docs/custom-skills/include-a-card-in-your-skills-response.html + +3. ```./models/*.json``` + + Change the model definition to replace the invocation name (it defaults to "audio player") and the sample phrases for each intent. + + Repeat the operation for each locale you are planning to support. + +4. DynamoDB table + +The dynamodb table is used to store the playback settings information of the user. If the table does not exist, the persistence code will create the table at the first invocation of the skill. + +You can manually create the DynamoDB table with the following command: + +```bash +aws dynamodb create-table --table-name Audio-Player-Multi-Stream --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 +``` + +To minimize latency, we recommend to create the DynamoDB table in the same region as the Lambda function. + +When using DynamoDB, you also must ensure your Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html) will have permissions to read and write to the DynamoDB table. Be sure [to add the following policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html) to the Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html): + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "sid123", + "Effect": "Allow", + "Action": [ + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:UpdateItem" + ], + "Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/Audio-Player-Multi-Stream" + } + ] +} +``` + +### Deployment + +For AWS Lambda to correctly execute the skill code, we need to zip the skill code along +with all dependencies and upload it. Follow the steps mentioned [here](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/DEVELOPING_YOUR_FIRST_SKILL.html#preparing-your-code-for-aws-lambda) +to get your skill code ready for uploading to AWS Lambda console. + +## On Device Tests + +To invoke the skill from your device, you need to login to the Alexa Developer Console, and enable the "Test" switch on your skill. + +See https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html#step-4-test-your-skill for more testing instructions. + +Then, just say : + +```text +Alexa, open audio player. +``` + +## How it Works + +Alexa Skills Kit now includes a set of output directives and input events that allow you to control the playback of audio files or streams. There are a few important concepts to get familiar with: + +* **AudioPlayer directives** are used by your skill to start and stop audio playback from content hosted at a publicly accessible secure URL. You send AudioPlayer directives in response to the intents you've configured for your skill, or new events you'll receive when a user controls their device with a dedicated controller (see PlaybackController events below). +* **PlaybackController events** are sent to your skill when a user selects play/next/prev/pause on dedicated hardware controls on the Alexa device, such as on the Amazon Tap or the Voice Remote for Amazon Echo and Echo Dot. Your skill receives these events if your skill is currently controlling audio on the device (i.e., you were the last to send an AudioPlayer directive). +* **AudioPlayer events** are sent to your skill at key changes in the status of audio playback, such as when audio has begun playing, been stopped or has finished. You can use them to track what's currently playing or queue up more content. Unlike intents, when you receive an AudioPlayer event, you may only respond with appropriate AudioPlayer directives to control playback. + +You can learn more about the new [AudioPlayer interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-audioplayer-interface-reference) and [PlaybackController interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-playbackcontroller-interface-reference). + +## Cleanup + +If you were deploying this skill just for learning purposes or for testing, do not forget to clean your AWS account to avoid recurring charges for your DynamoDB table. + +- delete the lambda function +- delete the IAM execution role +- delete the DynamoDB table (Audio-Player-Multi-Stream) diff --git a/MultiStream/lambda/py/alexa/__init__.py b/MultiStream/lambda/py/alexa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MultiStream/lambda/py/alexa/data.py b/MultiStream/lambda/py/alexa/data.py new file mode 100644 index 0000000..d51cda5 --- /dev/null +++ b/MultiStream/lambda/py/alexa/data.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +WELCOME_MSG = "Welcome to the Alexa Dev Chat Podcast. You can say, play the audio to begin the podcast." +WELCOME_REPROMPT_MSG = "You can say, play the audio, to begin." +WELCOME_PLAYBACK_MSG = "You were listening to {}. Would you like to resume?" +WELCOME_PLAYBACK_REPROMPT_MSG = "You can say yes to resume or no to play from the top" +DEVICE_NOT_SUPPORTED = "Sorry, this skill is not supported on this device" +LOOP_ON_MSG = "Loop turned on." +LOOP_OFF_MSG = "Loop turned off." +HELP_MSG = WELCOME_MSG +HELP_PLAYBACK_MSG = WELCOME_PLAYBACK_MSG +HELP_DURING_PLAY_MSG = "You are listening to the Alexa Dev Chat Podcast. You can say, Next or Previous to navigate through the playlist. At any time, you can say Pause to pause the audio and Resume to resume." +STOP_MSG = "Goodbye." +EXCEPTION_MSG = "Sorry, this is not a valid command. Please say help, to hear what you can say." +PLAYBACK_PLAY = "This is {}" +PLAYBACK_PLAY_CARD = "Playing {}" +PLAYBACK_NEXT_END = "You have reached the end of the playlist" +PLAYBACK_PREVIOUS_END = "You have reached the start of the playlist" + +DYNAMODB_TABLE_NAME = "Audio-Player-Multi-Stream" + +AUDIO_DATA = [ + { + "title": "Episode 22", + "url": "https://feeds.soundcloud.com/stream/459953355-user-652822799-episode-022-getting-started-with-alexa-for-business.mp3", + }, + { + "title": "Episode 23", + "url": "https://feeds.soundcloud.com/stream/476469807-user-652822799-episode-023-voicefirst-in-2018-where-are-we-now.mp3", + }, + { + "title": "Episode 24", + "url": "https://feeds.soundcloud.com/stream/496340574-user-652822799-episode-024-the-voice-generation-will-include-all-generations.mp3", + } +] \ No newline at end of file diff --git a/MultiStream/lambda/py/alexa/util.py b/MultiStream/lambda/py/alexa/util.py new file mode 100644 index 0000000..1096433 --- /dev/null +++ b/MultiStream/lambda/py/alexa/util.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +import random +from typing import List, Dict +from ask_sdk_model import IntentRequest, Response +from ask_sdk_model.ui import SimpleCard +from ask_sdk_model.interfaces.audioplayer import ( + PlayDirective, PlayBehavior, AudioItem, Stream, StopDirective) +from ask_sdk_core.handler_input import HandlerInput +from . import data + + +def get_playback_info(handler_input): + # type: (HandlerInput) -> Dict + persistence_attr = handler_input.attributes_manager.persistent_attributes + return persistence_attr.get('playback_info') + + +def can_throw_card(handler_input): + # type: (HandlerInput) -> bool + playback_info = get_playback_info(handler_input) + if (isinstance(handler_input.request_envelope.request, IntentRequest) + and playback_info.get('playback_index_changed')): + playback_info['playback_index_changed'] = False + return True + else: + return False + + +def get_token(handler_input): + """Extracting token received in the request.""" + # type: (HandlerInput) -> str + return handler_input.request_envelope.request.token + + +def get_index(handler_input): + """Extracting index from the token received in the request.""" + # type: (HandlerInput) -> int + token = int(get_token(handler_input)) + persistent_attr = handler_input.attributes_manager.persistent_attributes + + return persistent_attr.get("playback_info").get("play_order").index(token) + + +def get_offset_in_ms(handler_input): + """Extracting offset in milliseconds received in the request""" + # type: (HandlerInput) -> int + return handler_input.request_envelope.request.offset_in_milliseconds + + +def shuffle_order(): + # type: () -> List + podcast_indices = [l for l in range(0, len(data.AUDIO_DATA))] + random.shuffle(podcast_indices) + return podcast_indices + + +class Controller: + """Audioplayer and Playback Controller.""" + @staticmethod + def play(handler_input, is_playback=False): + # type: (HandlerInput) -> Response + playback_info = get_playback_info(handler_input) + response_builder = handler_input.response_builder + + play_order = playback_info.get("play_order") + offset_in_ms = playback_info.get("offset_in_ms") + index = playback_info.get("index") + + play_behavior = PlayBehavior.REPLACE_ALL + podcast = data.AUDIO_DATA[play_order[index]] + token = play_order[index] + playback_info['next_stream_enqueued'] = False + + response_builder.add_directive( + PlayDirective( + play_behavior=play_behavior, + audio_item=AudioItem( + stream=Stream( + token=token, + url=podcast.get("url"), + offset_in_milliseconds=offset_in_ms, + expected_previous_token=None), + metadata=None)) + ).set_should_end_session(True) + + if not is_playback: + # Add card and response only for events not triggered by + # Playback Controller + handler_input.response_builder.speak( + data.PLAYBACK_PLAY.format(podcast.get("title"))) + + if can_throw_card(handler_input): + response_builder.set_card(SimpleCard( + title=data.PLAYBACK_PLAY_CARD.format( + podcast.get("title")), + content=data.PLAYBACK_PLAY_CARD.format( + podcast.get("title")))) + + return response_builder.response + + @staticmethod + def stop(handler_input): + # type: (HandlerInput) -> Response + handler_input.response_builder.add_directive(StopDirective()) + return handler_input.response_builder.response + + @staticmethod + def play_next(handler_input, is_playback=False): + # type: (HandlerInput) -> Response + persistent_attr = handler_input.attributes_manager.persistent_attributes + + playback_info = persistent_attr.get("playback_info") + playback_setting = persistent_attr.get("playback_setting") + next_index = (playback_info.get("index") + 1) % len(data.AUDIO_DATA) + + if next_index == 0 and not playback_setting.get("loop"): + if not is_playback: + handler_input.response_builder.speak(data.PLAYBACK_NEXT_END) + + return handler_input.response_builder.add_directive( + StopDirective()).response + + playback_info["index"] = next_index + playback_info["offset_in_ms"] = 0 + playback_info["playback_index_changed"] = True + + return Controller.play(handler_input, is_playback) + + @staticmethod + def play_previous(handler_input, is_playback=False): + # type: (HandlerInput) -> Response + persistent_attr = handler_input.attributes_manager.persistent_attributes + + playback_info = persistent_attr.get("playback_info") + playback_setting = persistent_attr.get("playback_setting") + prev_index = playback_info.get("index") - 1 + + if prev_index == -1: + if playback_setting.get("loop"): + prev_index += len(data.AUDIO_DATA) + else: + if not is_playback: + handler_input.response_builder.speak( + data.PLAYBACK_PREVIOUS_END) + + return handler_input.response_builder.add_directive( + StopDirective()).response + + playback_info["index"] = prev_index + playback_info["offset_in_ms"] = 0 + playback_info["playback_index_changed"] = True + + return Controller.play(handler_input, is_playback) diff --git a/MultiStream/lambda/py/lambda_function.py b/MultiStream/lambda/py/lambda_function.py new file mode 100644 index 0000000..7ef3899 --- /dev/null +++ b/MultiStream/lambda/py/lambda_function.py @@ -0,0 +1,733 @@ +# -*- coding: utf-8 -*- + +import logging +from ask_sdk.standard import StandardSkillBuilder +from ask_sdk_core.dispatch_components import ( + AbstractRequestHandler, AbstractExceptionHandler, + AbstractRequestInterceptor, AbstractResponseInterceptor) +from ask_sdk_core.utils import is_request_type, is_intent_name +from ask_sdk_core.handler_input import HandlerInput +from ask_sdk_model import Response +from ask_sdk_model.interfaces.audioplayer import ( + PlayDirective, PlayBehavior, AudioItem, Stream) + +from alexa import data, util + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# ######################### INTENT HANDLERS ######################### +# This section contains handlers for the built-in intents and generic +# request handlers like launch, session end, skill events etc. + + +class CheckAudioInterfaceHandler(AbstractRequestHandler): + """Check if device supports audio play. + + This can be used as the first handler to be checked, before invoking + other handlers, thus making the skill respond to unsupported devices + without doing much processing. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + if handler_input.request_envelope.context.system.device: + # Since skill events won't have device information + return handler_input.request_envelope.context.system.device.supported_interfaces.audio_player is None + else: + return False + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In CheckAudioInterfaceHandler") + handler_input.response_builder.speak( + data.DEVICE_NOT_SUPPORTED).set_should_end_session(True) + return handler_input.response_builder.response + + +class LaunchRequestHandler(AbstractRequestHandler): + """Launch radio for skill launch or PlayAudio intent.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("LaunchRequest")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In LaunchRequestHandler") + + playback_info = util.get_playback_info(handler_input) + + if not playback_info.get('has_previous_playback_session'): + message = data.WELCOME_MSG + reprompt = data.WELCOME_REPROMPT_MSG + else: + playback_info['in_playback_session'] = False + message = data.WELCOME_PLAYBACK_MSG.format( + data.AUDIO_DATA[ + playback_info.get("play_order")[ + playback_info.get("index")]].get("title")) + reprompt = data.WELCOME_PLAYBACK_REPROMPT_MSG + + return handler_input.response_builder.speak(message).ask( + reprompt).response + + +class StartPlaybackHandler(AbstractRequestHandler): + """Handler for Playing audio on different events. + + Handles PlayAudio Intent, Resume Intent. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (is_intent_name("AMAZON.ResumeIntent")(handler_input) + or is_intent_name("PlayAudio")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In StartPlaybackHandler") + return util.Controller.play(handler_input) + + +class NextPlaybackHandler(AbstractRequestHandler): + """Handler for Playing next audio on different events. + + Handles Next Intent and NextCommandIssued event. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.NextIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In NextPlaybackHandler") + return util.Controller.play_next(handler_input) + + +class PreviousPlaybackHandler(AbstractRequestHandler): + """Handler for Playing previous audio on different events. + + Handles Previous Intent and PreviousCommandIssued event. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.PreviousIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PreviousPlaybackHandler") + return util.Controller.play_previous(handler_input) + + +class PausePlaybackHandler(AbstractRequestHandler): + """Handler for stopping audio. + + Handles Stop, Cancel and Pause Intents and PauseCommandIssued event. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and (is_intent_name("AMAZON.StopIntent")(handler_input) + or is_intent_name("AMAZON.CancelIntent")(handler_input) + or is_intent_name("AMAZON.PauseIntent")(handler_input))) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("PausePlaybackHandler") + return util.Controller.stop(handler_input) + + +class LoopOnHandler(AbstractRequestHandler): + """Handler for setting the audio loop on.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.LoopOnIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In LoopOnHandler") + persistent_attr = handler_input.attributes_manager.persistent_attributes + playback_setting = persistent_attr.get("playback_setting") + playback_setting["loop"] = True + + return handler_input.response_builder.speak(data.LOOP_ON_MSG).response + + +class LoopOffHandler(AbstractRequestHandler): + """Handler for setting the audio loop off.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.LoopOffIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In LoopOffHandler") + persistent_attr = handler_input.attributes_manager.persistent_attributes + playback_setting = persistent_attr.get("playback_setting") + playback_setting["loop"] = False + + return handler_input.response_builder.speak( + data.LOOP_OFF_MSG).response + + +class ShuffleOnHandler(AbstractRequestHandler): + """Handler for setting the audio shuffle on.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.ShuffleOnIntent")( + handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In ShuffleOnHandler") + persistent_attr = handler_input.attributes_manager.persistent_attributes + playback_setting = persistent_attr.get("playback_setting") + playback_info = persistent_attr.get("playback_info") + + playback_setting["shuffle"] = True + playback_info["play_order"] = util.shuffle_order() + playback_info["index"] = 0 + playback_info["offset_in_ms"] = 0 + playback_info["playback_index_changed"] = True + return util.Controller.play(handler_input) + + +class ShuffleOffHandler(AbstractRequestHandler): + """Handler for setting the audio shuffle off.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.ShuffleOffIntent")( + handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In ShuffleOffHandler") + persistent_attr = handler_input.attributes_manager.persistent_attributes + playback_setting = persistent_attr.get("playback_setting") + playback_info = persistent_attr.get("playback_info") + + playback_setting["shuffle"] = False + playback_info["index"] = playback_info["play_order"][ + playback_info["index"]] + playback_info["play_order"] = [l for l in range( + 0, len(data.AUDIO_DATA))] + return util.Controller.play(handler_input) + + +class StartOverHandler(AbstractRequestHandler): + """Handler for start over.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_intent_name("AMAZON.StartOverIntent")( + handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In StartOverHandler") + playback_info = util.get_playback_info(handler_input) + playback_info["offset_in_ms"] = 0 + + return util.Controller.play(handler_input) + + +class YesHandler(AbstractRequestHandler): + """Handler for Yes intent when audio is not playing.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (not playback_info.get("in_playback_session") + and is_intent_name("AMAZON.YesIntent")( + handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In YesHandler") + return util.Controller.play(handler_input) + + +class NoHandler(AbstractRequestHandler): + """Handler for No intent when audio is not playing.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (not playback_info.get("in_playback_session") + and is_intent_name("AMAZON.NoIntent")( + handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In NoHandler") + playback_info = util.get_playback_info(handler_input) + + playback_info["index"] = 0 + playback_info["offset_in_ms"] = 0 + playback_info["playback_index_changed"] = True + playback_info["has_previous_playback_session"] = False + + return util.Controller.play(handler_input) + + +class CancelOrStopIntentHandler(AbstractRequestHandler): + """Handler for cancel, stop intents when not playing an audio.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (not playback_info.get("in_playback_session") + and (is_intent_name("AMAZON.CancelIntent")(handler_input) + or is_intent_name("AMAZON.StopIntent")(handler_input))) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In CancelOrStopIntentHandler") + return handler_input.response_builder.speak(data.STOP_MSG).response + + +class SessionEndedRequestHandler(AbstractRequestHandler): + """Handler for session end.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("SessionEndedRequest")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In SessionEndedRequestHandler") + logger.info("Session ended with reason: {}".format( + handler_input.request_envelope.request.reason)) + return handler_input.response_builder.response + + +class HelpIntentHandler(AbstractRequestHandler): + """Handler for providing help information to user.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_intent_name("AMAZON.HelpIntent")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In HelpIntentHandler") + + playback_info = util.get_playback_info(handler_input) + + if not playback_info.get('has_previous_playback_session'): + message = data.HELP_MSG + elif not playback_info.get('in_playback_session'): + message = data.HELP_PLAYBACK_MSG + else: + message = data.HELP_DURING_PLAY_MSG + + return handler_input.response_builder.speak(message).ask( + message).response + + +class FallbackIntentHandler(AbstractRequestHandler): + """Handler for fallback intent, for unmatched utterances. + + 2018-July-12: AMAZON.FallbackIntent is currently available in all + English locales. This handler will not be triggered except in that + locale, so it can be safely deployed for any locale. More info + on the fallback intent can be found here: + https://developer.amazon.com/docs/custom-skills/standard-built-in-intents.html#fallback + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_intent_name("AMAZON.FallbackIntent")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In FallbackIntentHandler") + + handler_input.response_builder.speak( + data.EXCEPTION_MSG).ask(data.EXCEPTION_MSG) + return handler_input.response_builder.response + + +# ########## AUDIOPLAYER INTERFACE HANDLERS ######################### +# This section contains handlers related to Audioplayer interface + +class PlaybackStartedEventHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackStarted Directive received. + + Confirming that the requested audio file began playing. + Do not send any specific response. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackStarted")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackStartedHandler") + + playback_info = util.get_playback_info(handler_input) + + playback_info["token"] = util.get_token(handler_input) + playback_info["index"] = util.get_index(handler_input) + playback_info["in_playback_session"] = True + playback_info["has_previous_playback_session"] = True + + return handler_input.response_builder.response + +class PlaybackFinishedEventHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackFinished Directive received. + + Confirming that the requested audio file completed playing. + Do not send any specific response. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackFinished")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackFinishedHandler") + + playback_info = util.get_playback_info(handler_input) + + playback_info["in_playback_session"] = False + playback_info["has_previous_playback_session"] = False + playback_info["next_stream_enqueued"] = False + + return handler_input.response_builder.response + + +class PlaybackStoppedEventHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackStopped Directive received. + + Confirming that the requested audio file stopped playing. + Do not send any specific response. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackStopped")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackStoppedHandler") + + playback_info = util.get_playback_info(handler_input) + + playback_info["token"] = util.get_token(handler_input) + playback_info["index"] = util.get_index(handler_input) + playback_info["offset_in_ms"] = util.get_offset_in_ms( + handler_input) + + return handler_input.response_builder.response + + +class PlaybackNearlyFinishedEventHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackNearlyFinished Directive received. + + Replacing queue with the URL again. This should not happen on live streams. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackNearlyFinished")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackNearlyFinishedHandler") + + persistent_attr = handler_input.attributes_manager.persistent_attributes + playback_info = persistent_attr.get("playback_info") + playback_setting = persistent_attr.get("playback_setting") + + if playback_info.get("next_stream_enqueued"): + return handler_input.response_builder.response + + enqueue_index = (playback_info.get("index") + 1) % len(data.AUDIO_DATA) + if enqueue_index == 0 and not playback_setting.get("loop"): + return handler_input.response_builder.response + + playback_info["next_stream_enqueued"] = True + enqueue_token = playback_info.get("play_order")[enqueue_index] + play_behavior = PlayBehavior.ENQUEUE + podcast = data.AUDIO_DATA[enqueue_token] + expected_previous_token = playback_info.get("token") + offset_in_ms = 0 + + handler_input.response_builder.add_directive( + PlayDirective( + play_behavior=play_behavior, + audio_item=AudioItem( + stream=Stream( + token=enqueue_token, + url=podcast.get("url"), + offset_in_milliseconds=offset_in_ms, + expected_previous_token=expected_previous_token), + metadata=None))) + + return handler_input.response_builder.response + + +class PlaybackFailedEventHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackFailed Directive received. + + Logging the error and restarting playing with no output speech. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackFailed")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackFailedHandler") + + playback_info = util.get_playback_info(handler_input) + playback_info["in_playback_session"] = False + + logger.info("Playback Failed: {}".format( + handler_input.request_envelope.request.error)) + + return handler_input.response_builder.response + + +class ExceptionEncounteredHandler(AbstractRequestHandler): + """Handler to handle exceptions from responses sent by AudioPlayer + request. + """ + def can_handle(self, handler_input): + # type; (HandlerInput) -> bool + return is_request_type("System.ExceptionEncountered")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In ExceptionEncounteredHandler") + logger.info("System exception encountered: {}".format( + handler_input.request_envelope.request)) + return handler_input.response_builder.response + +# ################################################################### + +# ########## PLAYBACK CONTROLLER INTERFACE HANDLERS ################# +# This section contains handlers related to Playback Controller interface +# https://developer.amazon.com/docs/custom-skills/playback-controller-interface-reference.html#requests + +class PlayCommandHandler(AbstractRequestHandler): + """Handler for Play command from hardware buttons or touch control. + + This handler handles the play command sent through hardware buttons such + as remote control or the play control from Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type( + "PlaybackController.PlayCommandIssued")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlayCommandHandler") + return util.Controller.play(handler_input, is_playback=True) + + +class NextCommandHandler(AbstractRequestHandler): + """Handler for Next command from hardware buttons or touch + control. + + This handler handles the next command sent through hardware + buttons such as remote control or the next control from + Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_request_type( + "PlaybackController.NextCommandIssued")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In NextCommandHandler") + return util.Controller.play_next(handler_input, is_playback=True) + + +class PreviousCommandHandler(AbstractRequestHandler): + """Handler for Previous command from hardware buttons or touch + control. + + This handler handles the previous command sent through hardware + buttons such as remote control or the previous control from + Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + + return (playback_info.get("in_playback_session") + and is_request_type( + "PlaybackController.PreviousCommandIssued")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PreviousCommandHandler") + return util.Controller.play_previous(handler_input, is_playback=True) + + +class PauseCommandHandler(AbstractRequestHandler): + """Handler for Pause command from hardware buttons or touch control. + + This handler handles the pause command sent through hardware + buttons such as remote control or the pause control from + Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + playback_info = util.get_playback_info(handler_input) + return (playback_info.get("in_playback_session") + and is_request_type( + "PlaybackController.PauseCommandIssued")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PauseCommandHandler") + return util.Controller.stop(handler_input) + +# ################################################################### + +# ################## EXCEPTION HANDLERS ############################# +class CatchAllExceptionHandler(AbstractExceptionHandler): + """Catch all exception handler, log exception and + respond with custom message. + """ + def can_handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> bool + return True + + def handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> Response + logger.info("In CatchAllExceptionHandler") + logger.error(exception, exc_info=True) + handler_input.response_builder.speak(data.EXCEPTION_MSG).ask( + data.EXCEPTION_MSG) + + return handler_input.response_builder.response + +# ################################################################### + +# ############# REQUEST / RESPONSE INTERCEPTORS ##################### +class RequestLogger(AbstractRequestInterceptor): + """Log the alexa requests.""" + def process(self, handler_input): + # type: (HandlerInput) -> None + logger.debug("Alexa Request: {}".format( + handler_input.request_envelope.request)) + + +class LoadPersistenceAttributesRequestInterceptor(AbstractRequestInterceptor): + """Check if user is invoking skill for first time and initialize preset.""" + def process(self, handler_input): + # type: (HandlerInput) -> None + persistence_attr = handler_input.attributes_manager.persistent_attributes + + if len(persistence_attr) == 0: + # First time skill user + persistence_attr["playback_setting"] = { + "loop": False, + "shuffle": False + } + + persistence_attr["playback_info"] = { + "play_order": [l for l in range(0, len(data.AUDIO_DATA))], + "index": 0, + "offset_in_ms": 0, + "playback_index_changed": False, + "token": None, + "next_stream_enqueued": False, + "in_playback_session": False, + "has_previous_playback_session": False + } + else: + # Convert decimals to integers, because of AWS SDK DynamoDB issue + # https://github.com/boto/boto3/issues/369 + playback_info = persistence_attr.get("playback_info") + playback_info["index"] = int(playback_info.get("index")) + playback_info["offset_in_ms"] = int(playback_info.get( + "offset_in_ms")) + playback_info["play_order"] = [ + int(l) for l in playback_info.get("play_order")] + + +class ResponseLogger(AbstractResponseInterceptor): + """Log the alexa responses.""" + def process(self, handler_input, response): + # type: (HandlerInput, Response) -> None + logger.debug("Alexa Response: {}".format(response)) + + +class SavePersistenceAttributesResponseInterceptor(AbstractResponseInterceptor): + """Save persistence attributes before sending response to user.""" + def process(self, handler_input, response): + # type: (HandlerInput, Response) -> None + handler_input.attributes_manager.save_persistent_attributes() +# ################################################################### + + +sb = StandardSkillBuilder( + table_name=data.DYNAMODB_TABLE_NAME, auto_create_table=True) + +# ############# REGISTER HANDLERS ##################### +# Request Handlers +sb.add_request_handler(CheckAudioInterfaceHandler()) +sb.add_request_handler(LaunchRequestHandler()) +sb.add_request_handler(HelpIntentHandler()) +sb.add_request_handler(ExceptionEncounteredHandler()) +sb.add_request_handler(SessionEndedRequestHandler()) +sb.add_request_handler(YesHandler()) +sb.add_request_handler(NoHandler()) +sb.add_request_handler(StartPlaybackHandler()) +sb.add_request_handler(PlayCommandHandler()) +sb.add_request_handler(NextPlaybackHandler()) +sb.add_request_handler(NextCommandHandler()) +sb.add_request_handler(PreviousPlaybackHandler()) +sb.add_request_handler(PreviousCommandHandler()) +sb.add_request_handler(PausePlaybackHandler()) +sb.add_request_handler(PauseCommandHandler()) +sb.add_request_handler(LoopOnHandler()) +sb.add_request_handler(LoopOffHandler()) +sb.add_request_handler(ShuffleOnHandler()) +sb.add_request_handler(ShuffleOffHandler()) +sb.add_request_handler(StartOverHandler()) +sb.add_request_handler(CancelOrStopIntentHandler()) +sb.add_request_handler(PlaybackStartedEventHandler()) +sb.add_request_handler(PlaybackFinishedEventHandler()) +sb.add_request_handler(PlaybackStoppedEventHandler()) +sb.add_request_handler(PlaybackNearlyFinishedEventHandler()) +sb.add_request_handler(PlaybackStartedEventHandler()) +sb.add_request_handler(PlaybackFailedEventHandler()) + +# Exception handlers +sb.add_exception_handler(CatchAllExceptionHandler()) + +# Interceptors +sb.add_global_request_interceptor(RequestLogger()) +sb.add_global_request_interceptor(LoadPersistenceAttributesRequestInterceptor()) + +sb.add_global_response_interceptor(ResponseLogger()) +sb.add_global_response_interceptor(SavePersistenceAttributesResponseInterceptor()) + +# AWS Lambda handler +lambda_handler = sb.lambda_handler() diff --git a/MultiStream/lambda/py/requirements.txt b/MultiStream/lambda/py/requirements.txt new file mode 100644 index 0000000..7b15785 --- /dev/null +++ b/MultiStream/lambda/py/requirements.txt @@ -0,0 +1 @@ +ask-sdk diff --git a/MultiStream/models/en-GB.json b/MultiStream/models/en-GB.json new file mode 100644 index 0000000..188911b --- /dev/null +++ b/MultiStream/models/en-GB.json @@ -0,0 +1,80 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "audio player", + "intents": [{ + "name": "AMAZON.CancelIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "slots": [], + "samples": [] + }, + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "begin podcast", + "begin the podcast", + "begin playing the podcast", + "start podcast", + "start the podcast", + "start playing the podcast", + "play the podcast", + "to play", + "to begin podcast", + "to begin the podcast", + "to begin playing the podcast", + "to start podcast", + "to start the podcast", + "to start playing the podcast", + "to play the podcast" + ] + }, + { + "name": "AMAZON.PauseIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.PreviousIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.YesIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.NoIntent", + "slots": [], + "samples": [] + }, + { + "name": "AMAZON.NextIntent", + "slots": [], + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/MultiStream/models/en-US.json b/MultiStream/models/en-US.json new file mode 100644 index 0000000..2978528 --- /dev/null +++ b/MultiStream/models/en-US.json @@ -0,0 +1,79 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "audio player", + "intents": [ + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "begin podcast", + "begin the podcast", + "begin playing the podcast", + "start podcast", + "start the podcast", + "start playing the podcast", + "play the podcast", + "to play", + "to begin podcast", + "to begin the podcast", + "to begin playing the podcast", + "to start podcast", + "to start the podcast", + "to start playing the podcast", + "to play the podcast" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.PreviousIntent", + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "samples": [] + }, + { + "name": "AMAZON.YesIntent", + "samples": [] + }, + { + "name": "AMAZON.NoIntent", + "samples": [] + }, + { + "name": "AMAZON.NextIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/MultiStream/skill.json b/MultiStream/skill.json new file mode 100644 index 0000000..dbd905c --- /dev/null +++ b/MultiStream/skill.json @@ -0,0 +1,67 @@ +{ + "manifest": { + "publishingInformation": { + "locales": { + "en-US": { + "summary": "Very basic podcast sample", + "examplePhrases": [ + "Alexa, open audio player", + "Alexa, ask audio player to begin podcast", + "Alexa, ask audio player to stop" + ], + "keywords": [ + "podcast", + "streaming", + "example" + ], + "name": "Multi Stream Audio Player", + "description": "", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "en-GB": { + "summary": "Very basic podcast sample", + "examplePhrases": [ + "Alexa, open audio player", + "Alexa, ask audio player to begin podcast", + "Alexa, ask audio player to stop" + ], + "keywords": [ + "podcast", + "streaming", + "example" + ], + "name": "Multi Stream Audio Player", + "description": "", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + } + }, + "isAvailableWorldwide": true, + "testingInstructions": "Include your testing instruction (if any) here", + "category": "STREAMING_SERVICE", + "distributionCountries": [] + }, + "apis": { + "custom": { + "endpoint": { + "sourceDir": "lambda/py" + }, + "interfaces": [ + { + "type": "AUDIO_PLAYER" + } + ] + } + }, + "manifestVersion": "1.0", + "permissions": [], + "privacyAndCompliance": { + "allowsPurchases": false, + "isExportCompliant": true, + "containsAds": false, + "isChildDirected": false, + "usesPersonalInfo": false + } + } +} diff --git a/README.md b/README.md index b342540..8a02078 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ -## Skill Sample - Audio Player (Python) +# Skill Sample Python Audio Player -This project demonstrates the use of Alexa Audio Player for skills. +This project demonstrates the use of Alexa Audio Player for skills using ASK Python SDK. + +- Multiple-streams folder contains an example skill to play multiple, pre-recorded audio streams, such as a basic podcast skill. + +- Single-stream folder contains an example skill to play a single stream, such as a live radio skill. + +This code is using the [Alexa Skill Kit SDK for Python](https://github.com/alexa/alexa-skills-kit-sdk-for-python). ## License -This library is licensed under the Amazon Software License. +This library is licensed under the Amazon Software License. \ No newline at end of file diff --git a/SingleStream/.ask/config b/SingleStream/.ask/config new file mode 100644 index 0000000..bccb20f --- /dev/null +++ b/SingleStream/.ask/config @@ -0,0 +1,53 @@ +{ + "deploy_settings": { + "default": { + "skill_id": "amzn1.ask.skill.3164aad7-e102-4e35-a99f-46611012a94f", + "was_cloned": false, + "resources": { + "manifest": { + "eTag": "510fe2027ac0b0a6a619272d1a54d06c" + }, + "interactionModel": { + "it-IT": { + "eTag": "86a161a5ea0ca4fa6b588cba222b37c7" + }, + "en-US": { + "eTag": "03678dc7bbef2f2fc4103ce8cec83b5f" + }, + "en-CA": { + "eTag": "1f12b362510ff65ef93723526ca9335b" + }, + "en-IN": { + "eTag": "03678dc7bbef2f2fc4103ce8cec83b5f" + }, + "en-AU": { + "eTag": "d21de96898cf9ea3ba2507e4296941dd" + }, + "es-ES": { + "eTag": "7400ec1a2ebc4c3e6f444cbb29e65318" + }, + "fr-FR": { + "eTag": "49d15e4bae04319d3f7b3452c0725d09" + }, + "en-GB": { + "eTag": "cc497df5095e40681ee84fab3ba4155f" + }, + "es-MX": { + "eTag": "7400ec1a2ebc4c3e6f444cbb29e65318" + } + } + }, + "merge": { + "manifest": { + "apis": { + "custom": { + "endpoint": { + "uri": "ask-custom-singlestream-audioplayer-default" + } + } + } + } + } + } + } +} diff --git a/SingleStream/README.md b/SingleStream/README.md new file mode 100644 index 0000000..5701354 --- /dev/null +++ b/SingleStream/README.md @@ -0,0 +1,123 @@ +# Single Stream Audio Player Sample Skill (using ASK Python SDK) + +This skill demonstrates how to create a single stream audio skill. Single stream skills are typically used by radio stations to provide a convenient and quick access to their live stream. + +User interface is limited to Play and Stop use cases. + +## Usage + +```text +Alexa, play my radio + +Alexa, stop +``` + +## Installation + +You will need to comply to the prerequisites below and to change a few configuration files before creating the skill and upload the lambda code. + +### Pre-requisites + +0. This sample uses the [ASK Python SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/) packages for developing the Alexa skill. + + - If you already have the ASK Python SDK installed, then install the dependencies in the ``lambda/py/requirements.txt`` + using ``pip``. + - If you are starting fresh, follow the [Setting up the ASK SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html) + documentation, to get the ASK Python SDK installed on your machine. We recommend you to use the + [virtualenv approach](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html#option-1-set-up-the-sdk-in-a-virtual-environment). + Please run the following command in your virtualenv, to install the dependencies, before working on the skill code. + + `` + pip install -r lambda/py/requirements.txt + `` + +1. You need an [AWS account](https://aws.amazon.com) and an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. + + +### Code changes before deploying + +1. ```./skill.json``` + + Change the skill name, example phrase, icons, testing instructions etc ... + + Remember than most information is locale-specific and must be changed for each locale (en-GB, en-US etc.) + + Please refer to https://developer.amazon.com/docs/smapi/skill-manifest.html for details about manifest values. + +2. ```./lambda/py/alexa/data.py``` + + - Locale specific card data, radio URL, jingle URL has to be modified with correct runtime values. + ```start_jingle``` is an optional property defining a Jingle to be played before the live stream. + Be sure to modify the value for each language supported by your skill. + + - When playing a jingle before your stream, you can choose the name of the database table where the "last played" + information will be stored. If the table does not exist, the persistence code will create the table at the first + invocation of the skill. You can manually create the DynamoDB table with the following command: + + ```bash + aws dynamodb create-table --table-name my_radio --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 + ``` + + To minimize latency, we recommend to create the DynamoDB table in the same region as the Lambda function. + + When using DynamoDB, you also must ensure your Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html) will have permissions to read and write to the DynamoDB table. Be sure [to add the following policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html) to the Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html): + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "sid123", + "Effect": "Allow", + "Action": [ + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:UpdateItem" + ], + "Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/my_radio" + } + ] + } + ``` + +3. Localization + + We use ``babel`` and python standard i18n ``gettext`` libraries, to create locale specific alexa responses. + This [localization guide](https://github.com/alexa/skill-sample-python-fact/blob/master/instructions/localization.md) + explains in brief on how to localize your alexa responses. We have already localized this skill sample for multiple + locales. If you want to make changes to the strings, follow these steps: + + - The base strings (eg: the strings wrapped in ``_()``) are present in ``./lambda/py/alexa/data.py`` module. + - The message catalog ``data.pot`` is present in ``./lambda/py/alexa/locales`` directory. + - The language specific translations (eg: ``data.po``) are present in ``./lambda/py/alexa/locales`` directory + as subfolders. The corresponding ``mo`` byte code files are also present in the same subfolder. + - The localization interceptor has already been registered to the skill and can be checked in the + ``lambda/py/lambda_function.py`` module. + - If you want to make any changes in the base strings, remember to generate the message catalog and the locale specific + translations as mentioned in **Step 2** and **Step 3** of the guide. + - If you only want to change the translations, generate the ``mo`` files for the translated strings, following + the **Step 3** of the guide. + +4. ```./models/*.json``` + + Change the model definition to replace the invocation name (it defaults to "my radio") and the sample phrases for each intent. + + Repeat the operation for each locale you are planning to support. + + +### Deployment + +For AWS Lambda to correctly execute the skill code, we need to zip the skill code along +with all dependencies and upload it. Follow the steps mentioned [here](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/DEVELOPING_YOUR_FIRST_SKILL.html#preparing-your-code-for-aws-lambda) + +## On Device Tests + +To invoke the skill from your device, you need to login to the Alexa Developer Console, and enable the "Test" switch on your skill. + +See https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html#step-4-test-your-skill for more testing instructions. + +Then, just say : + +```text +Alexa, open my radio. +``` diff --git a/SingleStream/lambda/py/alexa/__init__.py b/SingleStream/lambda/py/alexa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SingleStream/lambda/py/alexa/data.py b/SingleStream/lambda/py/alexa/data.py new file mode 100644 index 0000000..b451c93 --- /dev/null +++ b/SingleStream/lambda/py/alexa/data.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import gettext + +_ = gettext.gettext + +WELCOME_MSG = _("Welcome to {}") +HELP_MSG = _("Welcome to {}. You can play, stop, resume listening. How can I help you ?") +UNHANDLED_MSG = _("Sorry, I could not understand what you've just said.") +CANNOT_SKIP_MSG = _("This is radio, you have to wait for previous or next track to play.") +RESUME_MSG = _("Resuming {}") +NOT_POSSIBLE_MSG = _("This is radio, you can not do that. You can ask me to stop or pause to stop listening.") +STOP_MSG = _("Goodbye.") +DEVICE_NOT_SUPPORTED = _("Sorry, this skill is not supported on this device") + +TEST = _("test english") +TEST_PARAMS = _("test with parameters {} and {}") + +jingle = { + "db_table": "my_radio", + "play_once_every": 1000*60*60*24 # 24 hours +} + +en = { + "card": { + "title": 'My Radio', + "text": 'Less bla bla bla, more la la la', + "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', + "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' + }, + "url": 'https://audio1.maxi80.com', + "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' +} + +fr = { + "card": { + "title": 'My Radio', + "text": 'Moins de bla bla bla, plus de la la la', + "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', + "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' + }, + "url": 'https://audio1.maxi80.com', + "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' +} + +it = { + "card": { + "title": 'La Mia Radio', + "text": 'Meno parlare, più musica', + "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', + "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' + }, + "url": 'https://audio1.maxi80.com', + "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' +} + +es = { + "card": { + "title": 'Mi Radio', + "text": 'Menos conversación, más música', + "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', + "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' + }, + "url": 'https://audio1.maxi80.com', + "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' +} \ No newline at end of file diff --git a/SingleStream/lambda/py/alexa/util.py b/SingleStream/lambda/py/alexa/util.py new file mode 100644 index 0000000..c8252b4 --- /dev/null +++ b/SingleStream/lambda/py/alexa/util.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +import datetime +from typing import Dict, Optional +from ask_sdk_model import Request, Response +from ask_sdk_model.ui import StandardCard, Image +from ask_sdk_model.interfaces.audioplayer import ( + PlayDirective, PlayBehavior, AudioItem, Stream, AudioItemMetadata, + StopDirective, ClearQueueDirective, ClearBehavior) +from ask_sdk_model.interfaces import display +from ask_sdk_core.response_helper import ResponseFactory +from ask_sdk_core.handler_input import HandlerInput +from . import data + +def audio_data(request): + # type: (Request) -> Dict + default_locale = "en-US" + if request.locale is None: + locale = default_locale + else: + locale = request.locale + + if locale.startswith("en"): + return data.en + elif locale.startswith("fr"): + return data.fr + elif locale.startswith("it"): + return data.it + elif locale.startswith("es"): + return data.es + else: + return {} + + +def play(url, offset, text, card_data, response_builder): + """Function to play audio. + + Using the function to begin playing audio when: + - Play Audio Intent is invoked. + - Resuming audio when stopped / paused. + - Next / Previous commands issues. + + https://developer.amazon.com/docs/custom-skills/audioplayer-interface-reference.html#play + REPLACE_ALL: Immediately begin playback of the specified stream, + and replace current and enqueued streams. + """ + # type: (str, int, str, Dict, ResponseFactory) -> Response + if card_data: + response_builder.set_card( + StandardCard( + title=card_data["title"], text=card_data["text"], + image=Image( + small_image_url=card_data["small_image_url"], + large_image_url=card_data["large_image_url"]) + ) + ) + + # Using URL as token as they are all unique + response_builder.add_directive( + PlayDirective( + play_behavior=PlayBehavior.REPLACE_ALL, + audio_item=AudioItem( + stream=Stream( + token=url, + url=url, + offset_in_milliseconds=offset, + expected_previous_token=None), + metadata=add_screen_background(card_data) if card_data else None + ) + ) + ).set_should_end_session(True) + + if text: + response_builder.speak(text) + + return response_builder.response + +def play_later(url, card_data, response_builder): + """Play the stream later. + + https://developer.amazon.com/docs/custom-skills/audioplayer-interface-reference.html#play + REPLACE_ENQUEUED: Replace all streams in the queue. This does not impact the currently playing stream. + """ + # type: (str, Dict, ResponseFactory) -> Response + if card_data: + # Using URL as token as they are all unique + response_builder.add_directive( + PlayDirective( + play_behavior=PlayBehavior.REPLACE_ENQUEUED, + audio_item=AudioItem( + stream=Stream( + token=url, + url=url, + offset_in_milliseconds=0, + expected_previous_token=None), + metadata=add_screen_background(card_data))) + ).set_should_end_session(True) + + return response_builder.response + +def stop(text, response_builder): + """Issue stop directive to stop the audio. + + Issuing AudioPlayer.Stop directive to stop the audio. + Attributes already stored when AudioPlayer.Stopped request received. + """ + # type: (str, ResponseFactory) -> Response + response_builder.add_directive(StopDirective()) + if text: + response_builder.speak(text) + + return response_builder.response + +def clear(response_builder): + """Clear the queue amd stop the player.""" + # type: (ResponseFactory) -> Response + response_builder.add_directive(ClearQueueDirective( + clear_behavior=ClearBehavior.CLEAR_ENQUEUED)) + return response_builder.response + +def add_screen_background(card_data): + # type: (Dict) -> Optional[AudioItemMetadata] + if card_data: + metadata = AudioItemMetadata( + title=card_data["title"], + subtitle=card_data["text"], + art=display.Image( + content_description=card_data["title"], + sources=[ + display.ImageInstance( + url="https://alexademo.ninja/skills/logo-512.png") + ] + ) + , background_image=display.Image( + content_description=card_data["title"], + sources=[ + display.ImageInstance( + url="https://alexademo.ninja/skills/logo-512.png") + ] + ) + ) + return metadata + else: + return None + + +def should_play_jingle(handler_input): + # type: (HandlerInput) -> bool + will_play_jingle = False + + jingle_data = audio_data(handler_input.request_envelope.request) + if jingle_data is None or "start_jingle" not in jingle_data: + return will_play_jingle + + attr = handler_input.attributes_manager.persistent_attributes + + if attr is None or not attr.keys(): + attr["last_played"] = "0001/01/01 00:00:00:000000" + attr["played_count"] = 0 + handler_input.attributes_manager.persistent_attributes = attr + + last_played_epoch = attr["last_played"] + now = datetime.datetime.now() + format = "%Y/%m/%d %H:%M:%S:%f" + delta = datetime.timedelta(milliseconds=data.jingle["play_once_every"]) + + # When last played is more than play_once_every milliseconds ago, play jingle + + will_play_jingle = (last_played_epoch == "0001/01/01 00:00:00:000000" + or datetime.datetime.strptime(last_played_epoch, format) + delta < now) + + if will_play_jingle: + attr["last_played"] = now.strftime(format) + attr["played_count"] += 1 + handler_input.attributes_manager.save_persistent_attributes() + + return will_play_jingle diff --git a/SingleStream/lambda/py/lambda_function.py b/SingleStream/lambda/py/lambda_function.py new file mode 100644 index 0000000..0305baf --- /dev/null +++ b/SingleStream/lambda/py/lambda_function.py @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- + +import logging +import gettext +from ask_sdk.standard import StandardSkillBuilder +from ask_sdk_core.dispatch_components import ( + AbstractRequestHandler, AbstractExceptionHandler, + AbstractRequestInterceptor, AbstractResponseInterceptor) +from ask_sdk_core.utils import is_request_type, is_intent_name +from ask_sdk_core.handler_input import HandlerInput +from ask_sdk_model import Response + +from alexa import data, util + +sb = StandardSkillBuilder( + table_name=data.jingle["db_table"], auto_create_table=True) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# ######################### INTENT HANDLERS ######################### +# This section contains handlers for the built-in intents and generic +# request handlers like launch, session end, skill events etc. + +class CheckAudioInterfaceHandler(AbstractRequestHandler): + """Check if device supports audio play. + + This can be used as the first handler to be checked, before invoking + other handlers, thus making the skill respond to unsupported devices + without doing much processing. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + if handler_input.request_envelope.context.system.device: + # Since skill events won't have device information + return handler_input.request_envelope.context.system.device.supported_interfaces.audio_player is None + else: + return False + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In CheckAudioInterfaceHandler") + _ = handler_input.attributes_manager.request_attributes["_"] + handler_input.response_builder.speak( + _(data.DEVICE_NOT_SUPPORTED)).set_should_end_session(True) + return handler_input.response_builder.response + + +class SkillEventHandler(AbstractRequestHandler): + """Close session for skill events or when session ends. + + Handler to handle session end or skill events (SkillEnabled, + SkillDisabled etc.) + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (handler_input.request_envelope.request.object_type.startswith( + "AlexaSkillEvent") or + is_request_type("SessionEndedRequest")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In SkillEventHandler") + return handler_input.response_builder.response + + +class LaunchRequestOrPlayAudioHandler(AbstractRequestHandler): + """Launch radio for skill launch or PlayAudio intent.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (is_request_type("LaunchRequest")(handler_input) or + is_intent_name("PlayAudio")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In LaunchRequestOrPlayAudioHandler") + + _ = handler_input.attributes_manager.request_attributes["_"] + request = handler_input.request_envelope.request + + if util.audio_data(request)["start_jingle"]: + if util.should_play_jingle(handler_input): + return util.play(url=util.audio_data(request)["start_jingle"], + offset=0, + text=_(data.WELCOME_MSG).format( + util.audio_data(request)["card"]["title"]), + card_data=util.audio_data(request)["card"], + response_builder=handler_input.response_builder) + + return util.play(url=util.audio_data(request)["url"], + offset=0, + text=_(data.WELCOME_MSG).format( + util.audio_data(request)["card"]["title"]), + card_data=util.audio_data(request)["card"], + response_builder=handler_input.response_builder) + + +class HelpIntentHandler(AbstractRequestHandler): + """Handler for providing help information to user.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_intent_name("AMAZON.HelpIntent")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In HelpIntentHandler") + _ = handler_input.attributes_manager.request_attributes["_"] + handler_input.response_builder.speak( + _(data.HELP_MSG).format( + util.audio_data( + handler_input.request_envelope.request)["card"]["title"]) + ).set_should_end_session(False) + return handler_input.response_builder.response + + +class UnhandledIntentHandler(AbstractRequestHandler): + """Handler for fallback intent, for unmatched utterances. + + 2018-July-12: AMAZON.FallbackIntent is currently available in all + English locales. This handler will not be triggered except in that + locale, so it can be safely deployed for any locale. More info + on the fallback intent can be found here: + https://developer.amazon.com/docs/custom-skills/standard-built-in-intents.html#fallback + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_intent_name("AMAZON.FallbackIntent")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In UnhandledIntentHandler") + _ = handler_input.attributes_manager.request_attributes["_"] + handler_input.response_builder.speak( + _(data.UNHANDLED_MSG)).set_should_end_session(True) + return handler_input.response_builder.response + + +class NextOrPreviousIntentHandler(AbstractRequestHandler): + """Handler for next or previous intents.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (is_intent_name("AMAZON.NextIntent")(handler_input) or + is_intent_name("AMAZON.PreviousIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In NextOrPreviousIntentHandler") + _ = handler_input.attributes_manager.request_attributes["_"] + handler_input.response_builder.speak( + _(data.CANNOT_SKIP_MSG)).set_should_end_session(True) + return handler_input.response_builder.response + + +class CancelOrStopIntentHandler(AbstractRequestHandler): + """Handler for cancel, stop or pause intents.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (is_intent_name("AMAZON.CancelIntent")(handler_input) or + is_intent_name("AMAZON.StopIntent")(handler_input) or + is_intent_name("AMAZON.PauseIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In CancelOrStopIntentHandler") + _ = handler_input.attributes_manager.request_attributes["_"] + return util.stop(_(data.STOP_MSG), handler_input.response_builder) + + +class ResumeIntentHandler(AbstractRequestHandler): + """Handler for resume intent.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_intent_name("AMAZON.ResumeIntent")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In ResumeIntentHandler") + request = handler_input.request_envelope.request + _ = handler_input.attributes_manager.request_attributes["_"] + speech = _(data.RESUME_MSG).format( + util.audio_data(request)["card"]["title"]) + return util.play( + url=util.audio_data(request)["url"], offset=0, + text=speech, card_data=util.audio_data(request)["card"], + response_builder=handler_input.response_builder) + + +class StartOverIntentHandler(AbstractRequestHandler): + """Handler for start over, loop on/off, shuffle on/off intent.""" + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (is_intent_name("AMAZON.StartOverIntent")(handler_input) or + is_intent_name("AMAZON.LoopOnIntent")(handler_input) or + is_intent_name("AMAZON.LoopOffIntent")(handler_input) or + is_intent_name("AMAZON.ShuffleOnIntent")(handler_input) or + is_intent_name("AMAZON.ShuffleOffIntent")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In StartOverIntentHandler") + + _ = handler_input.attributes_manager.request_attributes["_"] + speech = _(data.NOT_POSSIBLE_MSG) + return handler_input.response_builder.speak(speech).response + +# ################################################################### + +# ########## AUDIOPLAYER INTERFACE HANDLERS ######################### +# This section contains handlers related to Audioplayer interface + +class PlaybackStartedHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackStarted Directive received. + + Confirming that the requested audio file began playing. + Do not send any specific response. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackStarted")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackStartedHandler") + logger.info("Playback started") + return handler_input.response_builder.response + +class PlaybackFinishedHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackFinished Directive received. + + Confirming that the requested audio file completed playing. + Do not send any specific response. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackFinished")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackFinishedHandler") + logger.info("Playback finished") + return handler_input.response_builder.response + + +class PlaybackStoppedHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackStopped Directive received. + + Confirming that the requested audio file stopped playing. + Do not send any specific response. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackStopped")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackStoppedHandler") + logger.info("Playback stopped") + return handler_input.response_builder.response + + +class PlaybackNearlyFinishedHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackNearlyFinished Directive received. + + Replacing queue with the URL again. This should not happen on live streams. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackNearlyFinished")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackNearlyFinishedHandler") + logger.info("Playback nearly finished") + request = handler_input.request_envelope.request + return util.play_later( + url=util.audio_data(request)["url"], + card_data=util.audio_data(request)["card"], + response_builder=handler_input.response_builder) + + +class PlaybackFailedHandler(AbstractRequestHandler): + """AudioPlayer.PlaybackFailed Directive received. + + Logging the error and restarting playing with no output speech and card. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("AudioPlayer.PlaybackFailed")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlaybackFailedHandler") + request = handler_input.request_envelope.request + logger.info("Playback failed: {}".format(request.error)) + return util.play( + url=util.audio_data(request)["url"], offset=0, text=None, + card_data=None, + response_builder=handler_input.response_builder) + + +class ExceptionEncounteredHandler(AbstractRequestHandler): + """Handler to handle exceptions from responses sent by AudioPlayer + request. + """ + def can_handle(self, handler_input): + # type; (HandlerInput) -> bool + return is_request_type("System.ExceptionEncountered")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("\n**************** EXCEPTION *******************") + logger.info(handler_input.request_envelope) + return handler_input.response_builder.response + +# ################################################################### + +# ########## PLAYBACK CONTROLLER INTERFACE HANDLERS ################# +# This section contains handlers related to Playback Controller interface +# https://developer.amazon.com/docs/custom-skills/playback-controller-interface-reference.html#requests + +class PlayCommandHandler(AbstractRequestHandler): + """Handler for Play command from hardware buttons or touch control. + + This handler handles the play command sent through hardware buttons such + as remote control or the play control from Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type( + "PlaybackController.PlayCommandIssued")(handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PlayCommandHandler") + _ = handler_input.attributes_manager.request_attributes["_"] + request = handler_input.request_envelope.request + + if util.audio_data(request)["start_jingle"]: + if util.should_play_jingle(handler_input): + return util.play(url=util.audio_data(request)["start_jingle"], + offset=0, + text=None, + card_data=None, + response_builder=handler_input.response_builder) + + return util.play(url=util.audio_data(request)["url"], + offset=0, + text=None, + card_data=None, + response_builder=handler_input.response_builder) + + +class NextOrPreviousCommandHandler(AbstractRequestHandler): + """Handler for Next or Previous command from hardware buttons or touch + control. + + This handler handles the next/previous command sent through hardware + buttons such as remote control or the next/previous control from + Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return (is_request_type( + "PlaybackController.NextCommandIssued")(handler_input) or + is_request_type( + "PlaybackController.PreviousCommandIssued")(handler_input)) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In NextOrPreviousCommandHandler") + return handler_input.response_builder.response + + +class PauseCommandHandler(AbstractRequestHandler): + """Handler for Pause command from hardware buttons or touch control. + + This handler handles the pause command sent through hardware + buttons such as remote control or the pause control from + Alexa-devices with a screen. + """ + def can_handle(self, handler_input): + # type: (HandlerInput) -> bool + return is_request_type("PlaybackController.PauseCommandIssued")( + handler_input) + + def handle(self, handler_input): + # type: (HandlerInput) -> Response + logger.info("In PauseCommandHandler") + return util.stop(text=None, + response_builder=handler_input.response_builder) + +# ################################################################### + +# ################## EXCEPTION HANDLERS ############################# +class CatchAllExceptionHandler(AbstractExceptionHandler): + """Catch all exception handler, log exception and + respond with custom message. + """ + def can_handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> bool + return True + + def handle(self, handler_input, exception): + # type: (HandlerInput, Exception) -> Response + logger.info("In CatchAllExceptionHandler") + logger.error(exception, exc_info=True) + _ = handler_input.attributes_manager.request_attributes["_"] + handler_input.response_builder.speak(_(data.UNHANDLED_MSG)).ask( + _(data.HELP_MSG).format( + util.audio_data( + handler_input.request_envelope.request)["card"]["title"])) + + return handler_input.response_builder.response + +# ################################################################### + +# ############# REQUEST / RESPONSE INTERCEPTORS ##################### +class RequestLogger(AbstractRequestInterceptor): + """Log the alexa requests.""" + def process(self, handler_input): + # type: (HandlerInput) -> None + logger.debug("Alexa Request: {}".format( + handler_input.request_envelope.request)) + + +class LocalizationInterceptor(AbstractRequestInterceptor): + """Process the locale in request and load localized strings for response. + + This interceptors processes the locale in request, and loads the locale + specific localization strings for the function `_`, that is used during + responses. + """ + def process(self, handler_input): + # type: (HandlerInput) -> None + locale = getattr(handler_input.request_envelope.request, 'locale', None) + logger.info("Locale is {}".format(locale)) + if locale: + if locale.startswith("fr"): + locale_file_name = "fr-FR" + elif locale.startswith("it"): + locale_file_name = "it-IT" + elif locale.startswith("es"): + locale_file_name = "es-ES" + else: + locale_file_name = locale + + logger.info("Loading locale file: {}".format(locale_file_name)) + i18n = gettext.translation( + 'data', localedir='locales', languages=[locale_file_name], + fallback=True) + handler_input.attributes_manager.request_attributes[ + "_"] = i18n.gettext + else: + handler_input.attributes_manager.request_attributes[ + "_"] = gettext.gettext + + +class ResponseLogger(AbstractResponseInterceptor): + """Log the alexa responses.""" + def process(self, handler_input, response): + # type: (HandlerInput, Response) -> None + logger.debug("Alexa Response: {}".format(response)) + +# ################################################################### + + +# ############# REGISTER HANDLERS ##################### +# Request Handlers +sb.add_request_handler(CheckAudioInterfaceHandler()) +sb.add_request_handler(SkillEventHandler()) +sb.add_request_handler(LaunchRequestOrPlayAudioHandler()) +sb.add_request_handler(PlayCommandHandler()) +sb.add_request_handler(HelpIntentHandler()) +sb.add_request_handler(ExceptionEncounteredHandler()) +sb.add_request_handler(UnhandledIntentHandler()) +sb.add_request_handler(NextOrPreviousIntentHandler()) +sb.add_request_handler(NextOrPreviousCommandHandler()) +sb.add_request_handler(CancelOrStopIntentHandler()) +sb.add_request_handler(PauseCommandHandler()) +sb.add_request_handler(ResumeIntentHandler()) +sb.add_request_handler(StartOverIntentHandler()) +sb.add_request_handler(PlaybackStartedHandler()) +sb.add_request_handler(PlaybackFinishedHandler()) +sb.add_request_handler(PlaybackStoppedHandler()) +sb.add_request_handler(PlaybackNearlyFinishedHandler()) +sb.add_request_handler(PlaybackStartedHandler()) +sb.add_request_handler(PlaybackFailedHandler()) + +# Exception handlers +sb.add_exception_handler(CatchAllExceptionHandler()) + +# Interceptors +sb.add_global_request_interceptor(RequestLogger()) +sb.add_global_request_interceptor(LocalizationInterceptor()) +sb.add_global_response_interceptor(ResponseLogger()) + +# AWS Lambda handler +lambda_handler = sb.lambda_handler() diff --git a/SingleStream/lambda/py/locales/data.pot b/SingleStream/lambda/py/locales/data.pot new file mode 100644 index 0000000..a8f0017 --- /dev/null +++ b/SingleStream/lambda/py/locales/data.pot @@ -0,0 +1,63 @@ +# Translations template for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2018. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-09-15 23:34-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: alexa/data.py:6 +msgid "Welcome to {0}" +msgstr "" + +#: alexa/data.py:7 +msgid "" +"Welcome to {0}. You can play, stop, resume listening. How can I help you" +" ?" +msgstr "" + +#: alexa/data.py:8 +msgid "Sorry, I could not understand what you've just said." +msgstr "" + +#: alexa/data.py:9 +msgid "This is radio, you have to wait for next track to play." +msgstr "" + +#: alexa/data.py:10 +msgid "Resuming {0}" +msgstr "" + +#: alexa/data.py:11 +msgid "" +"This is radio, you can not do that. You can ask me to stop or pause to " +"stop listening." +msgstr "" + +#: alexa/data.py:12 +msgid "Goodbye." +msgstr "" + +#: alexa/data.py:13 +msgid "Sorry, this skill is not supported on this device" +msgstr "" + +#: alexa/data.py:15 +msgid "test english" +msgstr "" + +#: alexa/data.py:16 +msgid "test with parameters {0} and {1}" +msgstr "" + diff --git a/SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.mo b/SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.mo new file mode 100644 index 0000000000000000000000000000000000000000..5713d6c4dc0f8df2e7a51ae26d04de65babb00ee GIT binary patch literal 1560 zcmaJ>O>bL86dj;^$bdj>V1eMm0;uHoYA1z;*Cwr-xUGww1lzO%7S;H@i9N}9<}n{m zt*AeMHDbr6OBO6pe}cELWyu1mzW}jCoOy1NMyjwfI&Ws~$C-2Q&7W_b`;p=ID%QJL zK5m}+{f6&XuztsiHh*AU#QF=XiS_n#jNJuxfSbUFz}JC~fwzHw178DfJkQvBz%PJ5 z;A|iGGWP$zz}Wl1m(DS^3j7Gz0)7KL0{#TN2Yly6#@++DeDUtrc#gT=m8rTYv*<9P;P=MNE=T<7s5KvbwP&{?rCnq$1_3u!Fh6A7U{8* zpGZgUKq^J}h^{V_rLkTVWHeRRg_y})uwgXCpXG%#^%#@ov=<6q^+#S$V zkT>MKDalwWd2pu(N;)qz9*{mO?u3IZd=I(wv=^N%IGEsKAT92D@|NcZ(WF#-p0c|_ z1MO=1}?x9%K1b zcxa3QP?Q;IcdgkMxlcMpatAlk=!|yzJGa}LL%P%M4>~*BwZ4erCOtQnMRGliT{1Ko zwR^2j_gZUftKS|BYP&nbWYY@n5h2;)USzb=Xs#uV&y(h-w6dD5UQ8O78VwjGedrQ1 zJ=^Zm(yq&$PllFjr?@vZqZ?m!yR_Zvwdu;4)El)f*W$qi$tE+bu5TerS)8I_4Q>z`=)V4a%hYrRgdeJV^dZPYf6MiYSA zyhK8ex?Vtyq`pk~gj*;4dhmP6+NoW9!k(~6Tj!=g^JKI(lD>9RXknof6rE>uosWc~ zmGm=YV68$f9=ViRA+^jUDrHDCGtYFYNmL~ZW)C, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-09-15 23:34-0700\n" +"PO-Revision-Date: 2018-09-15 23:37-0700\n" +"Last-Translator: FULL NAME \n" +"Language: es_ES\n" +"Language-Team: es_ES \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: alexa/data.py:6 +msgid "Welcome to {}" +msgstr "Hola, esta es {}" + +#: alexa/data.py:7 +msgid "" +"Welcome to {}. You can play, stop, resume listening. How can I help you" +" ?" +msgstr "" +"Hola, con {}. Tu puedes reproducir, parar o volver a escuchar la radio. " +"Qué quieres hacer?" + +#: alexa/data.py:8 +msgid "Sorry, I could not understand what you've just said." +msgstr "Lo siento, No pude entender lo que acabas de decir." + +#: alexa/data.py:9 +msgid "This is radio, you have to wait for previous or next track to play." +msgstr "Recuerda que esto es la radio, tu debes esperar la anterior o siguiente reproducción en lista." + +#: alexa/data.py:10 +msgid "Resuming {}" +msgstr "reiniciando la reproduccion {}" + +#: alexa/data.py:11 +msgid "" +"This is radio, you can not do that. You can ask me to stop or pause to " +"stop listening." +msgstr "" +"Recuerda que esto es la radio, no puedo hacer eso. Sí quieres detener " +" la reproducción, me puedes decir: para o pausa, ." + +#: alexa/data.py:12 +msgid "Goodbye." +msgstr "Adiós." + +#: alexa/data.py:13 +msgid "Sorry, this skill is not supported on this device" +msgstr "Lo sentimos, esta habilidad no es compatible con este dispositivo" + +#: alexa/data.py:15 +msgid "test english" +msgstr "prueba español" + +#: alexa/data.py:16 +msgid "test with parameters {} and {}" +msgstr "prueba con los parámetros {} y {)" + diff --git a/SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.mo b/SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.mo new file mode 100644 index 0000000000000000000000000000000000000000..37d5339c993214f581ab5e69b70356ae60a9d196 GIT binary patch literal 1524 zcmZ`&&2A$_5FTKW!2AffAR&Z`Q$X?zPO>b)yUA|iIKjbAf@88;aX_7!vfa+Od*~kr zFXF-ja6sY#IDvT$OPt^cM-E6l0cRw>ww-vhS}d2o>F)ads_M_zZhRl%_X6Zihzo~D ze!t@TImmC2pz{ahCgd;3D&+O2qUb)b0jvRk0KNqL8+ZWx2lygz>**+Z3-~ecJKPO` z!Qa2nMA0k2=Wj&OI`9qPUEm?`8{oIVec-Fl&h;JuKf`$dd>Qx)F!=o)80~AFosBt( z+HCz$svdpyb#$VQ8JDR+srGqBN;~o@V`H6A8C~>+qp|iYBc_43j;xSba%trH(vm%w zc~1BUrq&mQHjXpWie}Flk7UYGC#d3YL?(4PxTI95nN6mlDO^JIU~wz#ISrYdChK%T z+E5|9U0&qUI#wtkd6M70K(esC5YkaM7#rlEy$y~Of9c2>k)8*YA{XN%y3cv4=kY+u zwNrAHQ&26>@+p%E{4p)lAo@sO%nE8ypYvi?-`&VD7-Q9gVt;-GesWSeKI+6Z!@|Ksyek}6X|1xl5m!EpSKp_#_3HY~xbi`z0>!uuUP7cN>)o2` zHHCF?#|UL};k2pJ-lxqb9d5Vkbmv;=tx{8{o)Dyd?2w3}9lxe`ER)j=bEtY6l7>s$!^o3^{g{i|hKBP># zco#VyoD5iqFqv_8MuDz2Om+1tr-oTF|QD9(L=1Ejr2?Ld3^2FsL%_0O3+J`V9 zhpv$DlL{=0l=X5X6sXvdtVXQBQ7999K7Ns0Oa791Q(mS8cZRXs vAs}k~B>2+4kFDp2@r;)_6N^3UVgRN;3Tfw?h_0mbO`QIOL}#lAvXRMu`y}0e literal 0 HcmV?d00001 diff --git a/SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.po b/SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.po new file mode 100644 index 0000000..cc1c084 --- /dev/null +++ b/SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.po @@ -0,0 +1,68 @@ +# French (France) translations for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-09-15 23:34-0700\n" +"PO-Revision-Date: 2018-09-15 23:36-0700\n" +"Last-Translator: FULL NAME \n" +"Language: fr_FR\n" +"Language-Team: fr_FR \n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: alexa/data.py:6 +msgid "Welcome to {}" +msgstr "Bienvenue sur {}" + +#: alexa/data.py:7 +msgid "" +"Welcome to {}. You can play, stop, resume listening. How can I help you" +" ?" +msgstr "" +"Bienvenue sur {}. Vous pouvez démarrer, arrêter ou reprendre. Que " +"souhaitez-vous faire ?" + +#: alexa/data.py:8 +msgid "Sorry, I could not understand what you've just said." +msgstr "Désolé, je n'ai pas compris ce que vous avez dit." + +#: alexa/data.py:9 +msgid "This is radio, you have to wait for previous or next track to play." +msgstr "C'est de la radio, vous devez attendre le titre précédent ou suivant." + +#: alexa/data.py:10 +msgid "Resuming {}" +msgstr "Je redémarre {}" + +#: alexa/data.py:11 +msgid "" +"This is radio, you can not do that. You can ask me to stop or pause to " +"stop listening." +msgstr "" +"C'est de la radio, vous ne pouvez pas faire ca. Vous pouvez me demander " +"d'arrêter ou de metre en pause pour arrêter la musique." + +#: alexa/data.py:12 +msgid "Goodbye." +msgstr "au revoir !" + +#: alexa/data.py:13 +msgid "Sorry, this skill is not supported on this device" +msgstr "Désolé, cette skill ne peut être utilisée sur cet appareil." + +#: alexa/data.py:15 +msgid "test english" +msgstr "test français" + +#: alexa/data.py:16 +msgid "test with parameters {} and {}" +msgstr "test avec paramètres {} et {}" + diff --git a/SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.mo b/SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.mo new file mode 100644 index 0000000000000000000000000000000000000000..5096ca255e8b1746e14ff3e4996e5fe2beec15d8 GIT binary patch literal 1495 zcmZux!Hye65N#kx$bgVIzy(lo3P_&8b^=+P-A#7APH?cZ#_J^F0xHkc+PypO9=dzH zScnhcjQ9XIfiD4fpH=ED;K#skv33HC z@&0*kjkPOEwcx)7yant51^5lH5Bv-G3UK>*r9K1pficgIz&C+E0o7e=^T|?DH4Y^Y# z#I3$6OY4HN`C9z#;bX0<1`U~))_KHaRcKc%@)u;ha+$vP^z z@|O#R_CX8|kUlN$j8QE1p0N&e95*}EU@IR38Tm3$aGae+N?GtSRga{|>^dJB@|c%i z)D)@pRXufbv429BWe|O2&sGQO(M*bRb-r6F2s$Qa3gT?-oar#bVI0pTpvSlXifu;P zp|dBFg`}4!5AjA@)289*@Lsnwrib0pQSb16Z6vX|$-qx_p6pjspNwsrx`Vx5|MuR& z!Kiz5R2v?Sla3P(h>#p`kT$g%&8?*IVbXk`TG!gwt|yHfjRuIx2z`l}p3b|m=JnZ! zWbD}Zf`fH!y7Qplr~7+@E^R-SdbieRGp%@vlsbIg8(;XzSa`l!r0stHcA-=2rn|La zQ8_M>JJ!v8n@qWK{Z8v9ZYI9jdB@P(JJfu)Hs}qymyKzrjatVV3;{}8mPiyj6ba4atAn#|@Ho;Gbwbg11GBMyB+N0)8d=ZO^5Np8C(mU8V&>vhlzt3Fw80(GOH%|;Nsp6Eg_)t8RrZ-JP~{U@qI!wJkDmnOkSIm(V<0XlDl6v1MIbH; zKrM4i6Li|9YG2GkOohfE6T0?4e^cf, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-09-15 23:34-0700\n" +"PO-Revision-Date: 2018-09-15 23:37-0700\n" +"Last-Translator: FULL NAME \n" +"Language: it_IT\n" +"Language-Team: it_IT \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: alexa/data.py:6 +msgid "Welcome to {}" +msgstr "Benvenuto in {}" + +#: alexa/data.py:7 +msgid "" +"Welcome to {}. You can play, stop, resume listening. How can I help you" +" ?" +msgstr "" +"Benvenuto in {}. Puoi ascoltare, mettere in pausa o riprendere l'ascolto." +" Come posso aiutarti?" + +#: alexa/data.py:8 +msgid "Sorry, I could not understand what you've just said." +msgstr "Scusami, non ho capito quello che hai appena detto" + +#: alexa/data.py:9 +msgid "This is radio, you have to wait for previous or next track to play." +msgstr "Questa è radio, devi attendere per passare al precedente o prossimo brano." + +#: alexa/data.py:10 +msgid "Resuming {}" +msgstr "Riprendo {}" + +#: alexa/data.py:11 +msgid "" +"This is radio, you can not do that. You can ask me to stop or pause to " +"stop listening." +msgstr "" +"Questa è radio, non è possibile procedere. Puoi chiedermi di terminare o " +"mettere in pausa l'ascolto" + +#: alexa/data.py:12 +msgid "Goodbye." +msgstr "Arrivederci" + +#: alexa/data.py:13 +msgid "Sorry, this skill is not supported on this device" +msgstr "Mi spiace, questa skill non è supportata da questo dispositivo" + +#: alexa/data.py:15 +msgid "test english" +msgstr "test italiano" + +#: alexa/data.py:16 +msgid "test with parameters {} and {}" +msgstr "testa con i parametri {} e {}" + diff --git a/SingleStream/lambda/py/requirements.txt b/SingleStream/lambda/py/requirements.txt new file mode 100644 index 0000000..1bea5d3 --- /dev/null +++ b/SingleStream/lambda/py/requirements.txt @@ -0,0 +1,2 @@ +ask-sdk +babel diff --git a/SingleStream/models/en-AU.json b/SingleStream/models/en-AU.json new file mode 100644 index 0000000..b407f39 --- /dev/null +++ b/SingleStream/models/en-AU.json @@ -0,0 +1,74 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "my radio", + "intents": [ + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "play", + "start", + "play my radio", + "play my radio please", + "start my radio", + "start my radio please", + "start the radio", + "start the radio please", + "start the audio", + "start the audio please", + "play the audio", + "play the audio please", + "start the music", + "start the music please", + "play the music", + "play the music please" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "help me please", + "help please", + "what should i do", + "what's next", + "how can I listen to my radio", + "tell me how to play", + "tell me how to stop", + "tell me how to resume", + "how to stop" + ] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/SingleStream/models/en-CA.json b/SingleStream/models/en-CA.json new file mode 100644 index 0000000..c6e12a4 --- /dev/null +++ b/SingleStream/models/en-CA.json @@ -0,0 +1,74 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "my radio", + "intents": [ + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "play", + "start", + "play my radio", + "play my radio please", + "start my radio", + "start my radio please", + "start the radio", + "start the radio please", + "start the audio", + "start the audio please", + "play the audio", + "play the audio please", + "start the music", + "start the music please", + "play the music", + "play the music please" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "help me please", + "help please", + "what should i do", + "what's next", + "how can I listen to my radio", + "tell me how to play", + "tell me how to stop", + "tell me how to resume", + "how to stop" + ] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "samples": [] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/SingleStream/models/en-GB.json b/SingleStream/models/en-GB.json new file mode 100644 index 0000000..9ef8c88 --- /dev/null +++ b/SingleStream/models/en-GB.json @@ -0,0 +1,68 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "my radio", + "intents": [ + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "play", + "start", + "play my radio", + "play my radio please", + "start my radio", + "start my radio please", + "start the audio", + "start the audio please", + "play the audio", + "play the audio please", + "start the music", + "start the music please", + "play the music", + "play the music please" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "help me please", + "help please", + "what should i do", + "what's next", + "how can I listen to my radio", + "tell me how to play", + "tell me how to stop", + "tell me how to resume", + "how to stop" + ] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/SingleStream/models/en-IN.json b/SingleStream/models/en-IN.json new file mode 100644 index 0000000..8327a60 --- /dev/null +++ b/SingleStream/models/en-IN.json @@ -0,0 +1,70 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "my radio", + "intents": [ + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "play", + "start", + "play my radio", + "play my radio please", + "start my radio", + "start my radio please", + "start the radio", + "start the radio please", + "start the audio", + "start the audio please", + "play the audio", + "play the audio please", + "start the music", + "start the music please", + "play the music", + "play the music please" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "help me please", + "help please", + "what should i do", + "what's next", + "how can I listen to my radio", + "tell me how to play", + "tell me how to stop", + "tell me how to resume", + "how to stop" + ] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/SingleStream/models/en-US.json b/SingleStream/models/en-US.json new file mode 100644 index 0000000..8327a60 --- /dev/null +++ b/SingleStream/models/en-US.json @@ -0,0 +1,70 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "my radio", + "intents": [ + { + "name": "PlayAudio", + "slots": [], + "samples": [ + "play", + "start", + "play my radio", + "play my radio please", + "start my radio", + "start my radio please", + "start the radio", + "start the radio please", + "start the audio", + "start the audio please", + "play the audio", + "play the audio please", + "start the music", + "start the music please", + "play the music", + "play the music please" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "help me please", + "help please", + "what should i do", + "what's next", + "how can I listen to my radio", + "tell me how to play", + "tell me how to stop", + "tell me how to resume", + "how to stop" + ] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.StartOverIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + } + ], + "types": [] + } + } +} \ No newline at end of file diff --git a/SingleStream/models/es-ES.json b/SingleStream/models/es-ES.json new file mode 100644 index 0000000..bd4dd4a --- /dev/null +++ b/SingleStream/models/es-ES.json @@ -0,0 +1,76 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "mi radio", + "intents": [ + { + "name": "PlayAudio", + "samples": [ + "jugar", + "poner", + "reproducir", + "reproducir mi radio", + "reproducir mi radio por favor", + "poner mi radio", + "poner mi radio por favor", + "pon mi radio", + "pon mi radio por favor", + "pone mi radio", + "pone mi radio por favor", + "pon la radio", + "pon la radio por favor", + "pone la radio", + "pone la radio por favor", + "poner la radio", + "poner la radio por favor", + "reproducir la radio", + "reproducir la radio por favor", + "poner el sonido", + "poner el sonido por favor", + "pone el sonido", + "pone el sonido por favor", + "pon el sonido", + "pon el sonido por favor", + "poner la música", + "poner la música por favor", + "pon la música", + "pon la música por favor", + "pone la música", + "pone la música por favor" + ] + }, + { + "name": "AMAZON.PauseIntent" + }, + { + "name": "AMAZON.ResumeIntent" + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "ayudame por favor", + "que debo hacer", + "y luego", + "como puedo escuchar mi radio", + "como puedo escuchar la música", + "como escuchar mi radio", + "como escuchar la música", + "dime cómo parar", + "còmo parar", + "cómo nos detenemos", + "cómo so detiene" + ] + }, + { + "name": "AMAZON.StopIntent" + }, + { + "name": "AMAZON.CancelIntent" + }, + { + "name": "AMAZON.StartOverIntent" + } + ] + } + } +} diff --git a/SingleStream/models/es-MX.json b/SingleStream/models/es-MX.json new file mode 100644 index 0000000..bd4dd4a --- /dev/null +++ b/SingleStream/models/es-MX.json @@ -0,0 +1,76 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "mi radio", + "intents": [ + { + "name": "PlayAudio", + "samples": [ + "jugar", + "poner", + "reproducir", + "reproducir mi radio", + "reproducir mi radio por favor", + "poner mi radio", + "poner mi radio por favor", + "pon mi radio", + "pon mi radio por favor", + "pone mi radio", + "pone mi radio por favor", + "pon la radio", + "pon la radio por favor", + "pone la radio", + "pone la radio por favor", + "poner la radio", + "poner la radio por favor", + "reproducir la radio", + "reproducir la radio por favor", + "poner el sonido", + "poner el sonido por favor", + "pone el sonido", + "pone el sonido por favor", + "pon el sonido", + "pon el sonido por favor", + "poner la música", + "poner la música por favor", + "pon la música", + "pon la música por favor", + "pone la música", + "pone la música por favor" + ] + }, + { + "name": "AMAZON.PauseIntent" + }, + { + "name": "AMAZON.ResumeIntent" + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "ayudame por favor", + "que debo hacer", + "y luego", + "como puedo escuchar mi radio", + "como puedo escuchar la música", + "como escuchar mi radio", + "como escuchar la música", + "dime cómo parar", + "còmo parar", + "cómo nos detenemos", + "cómo so detiene" + ] + }, + { + "name": "AMAZON.StopIntent" + }, + { + "name": "AMAZON.CancelIntent" + }, + { + "name": "AMAZON.StartOverIntent" + } + ] + } + } +} diff --git a/SingleStream/models/fr-FR.json b/SingleStream/models/fr-FR.json new file mode 100644 index 0000000..b4f09ad --- /dev/null +++ b/SingleStream/models/fr-FR.json @@ -0,0 +1,73 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "ma radio", + "intents": [ + { + "name": "PlayAudio", + "samples": [ + "joue", + "jouer", + "démarre", + "démarrer", + "joue le direct", + "jouer le direct", + "joue le direct s'il te plait", + "lance le direct", + "lancer le direct", + "lance le direct s'il te plait", + "lance la radio", + "lancer la radio", + "lance la radio s'il te plait", + "joue la radio", + "jouer la radio", + "joue la radio s'il te plait", + "lance le son", + "lancer le son", + "lance le son s'il te plait", + "joue le son", + "jouer le son", + "joue le son s'il te plait", + "lance la musique", + "lancer la musique", + "lance la musique s'il te plait", + "joue la musique", + "jouer la musique", + "joue la musique s'il te plait" + ] + }, + { + "name": "AMAZON.PauseIntent" + }, + { + "name": "AMAZON.ResumeIntent" + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "aide moi s'il te plait", + "que dois je faire", + "et ensuite", + "comment est ce que je peux écouter le direct", + "comment écouter la musique", + "dis moi comment arrêter", + "comment on arrête", + "comment arrêter", + "comment ca s'arrête", + "je ne comprends pas" + ] + }, + { + "name": "AMAZON.StopIntent" + }, + { + "name": "AMAZON.CancelIntent" + }, + { + "name": "AMAZON.StartOverIntent" + } + ] + } + } + } + \ No newline at end of file diff --git a/SingleStream/models/it-IT.json b/SingleStream/models/it-IT.json new file mode 100644 index 0000000..492deb3 --- /dev/null +++ b/SingleStream/models/it-IT.json @@ -0,0 +1,65 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "la mia radio", + "intents": [ + { + "name": "PlayAudio", + "samples": [ + "giocare", + "giocare per favore", + "avviare la radio", + "avviare la radio per favore", + "metti la radio", + "metti la radio per favore", + "mette la radio", + "mette la radio per favore", + "accendi la radio", + "accendi la radio per favore", + "metti la mia radio", + "metti la mia radio per favore", + "mette la mia radio", + "mette la mia radio per favore", + "accendi la mia radio", + "accendi la mia radio per favore", + "metti la musica", + "metti la musica per favore", + "mette la musica", + "mette la musica per favore", + "accendi la musica", + "accendi la musica per favore" + ] + }, + { + "name": "AMAZON.PauseIntent" + }, + { + "name": "AMAZON.ResumeIntent" + }, + { + "name": "AMAZON.HelpIntent", + "samples": [ + "come fermarsi", + "come posso smettere", + "aiutami", + "come ascoltare la radio", + "come ascoltare la mia radio", + "come ascoltare la musica" + ] + }, + { + "name": "AMAZON.StopIntent", + "samples" : [ + "puoi mettere in pausa" + ] + }, + { + "name": "AMAZON.CancelIntent" + }, + { + "name": "AMAZON.StartOverIntent" + } + ] + } + } +} diff --git a/SingleStream/skill.json b/SingleStream/skill.json new file mode 100644 index 0000000..7f48a22 --- /dev/null +++ b/SingleStream/skill.json @@ -0,0 +1,201 @@ +{ + "manifest": { + "publishingInformation": { + "locales": { + "en-US": { + "summary": "Listen to My Radio, less bla bla bla, more la la la.", + "examplePhrases": [ + "Alexa, open My Radio", + "Alexa, play My Radio", + "Alexa, ask My Radio to play" + ], + "keywords": [ + "music", + "streaming", + "radio" + ], + "name": "My Radio", + "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "en-GB": { + "summary": "Listen to My Radio, less bla bla bla, more la la la.", + "examplePhrases": [ + "Alexa, open My Radio", + "Alexa, play My Radio", + "Alexa, ask My Radio to play" + ], + "keywords": [ + "music", + "streaming", + "radio" + ], + "name": "My Radio", + "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "en-IN": { + "summary": "Listen to My Radio, less bla bla bla, more la la la.", + "examplePhrases": [ + "Alexa, open My Radio", + "Alexa, play My Radio", + "Alexa, ask My Radio to play" + ], + "keywords": [ + "music", + "streaming", + "radio" + ], + "name": "My Radio", + "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "en-CA": { + "summary": "Listen to My Radio, less bla bla bla, more la la la.", + "examplePhrases": [ + "Alexa, open My Radio", + "Alexa, play My Radio", + "Alexa, ask My Radio to play" + ], + "keywords": [ + "music", + "streaming", + "radio" + ], + "name": "My Radio", + "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "en-AU": { + "summary": "Listen to My Radio, less bla bla bla, more la la la.", + "examplePhrases": [ + "Alexa, open My Radio", + "Alexa, play My Radio", + "Alexa, ask My Radio to play" + ], + "keywords": [ + "music", + "streaming", + "radio" + ], + "name": "My Radio", + "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "fr-FR": { + "summary": "Ecoutez Ma Radio, moins de bla bla, plus de la la la.", + "examplePhrases": [ + "Alexa, ouvre Ma Radio", + "Alexa, lance Ma Radio", + "Alexa, demande à Ma Radio de démarrer" + ], + "keywords": [ + "musique", + "direct", + "radio" + ], + "name": "Ma Radio", + "description": "Ecoutez Ma Radio, moins de bla bla, plus de la la la\n\nMa Radio vous donne le meilleur son, 24h sur 24h\n\nPour démarrer, dites juste : \"Alexa, lance Ma Radio\" ou \"Alexa, démarre ma radio\"\".\n\nA n'importe quel moment, vous pouvez dire \"Alexa, stop\" pour arrêter.", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "fr-CA": { + "summary": "Ecoutez Ma Radio, moins de bla bla, plus de la la la.", + "examplePhrases": [ + "Alexa, ouvre Ma Radio", + "Alexa, lance Ma Radio", + "Alexa, demande à Ma Radio de démarrer" + ], + "keywords": [ + "musique", + "direct", + "radio" + ], + "name": "Ma Radio", + "description": "Ecoutez Ma Radio, moins de bla bla, plus de la la la\n\nMa Radio vous donne le meilleur son, 24h sur 24h\n\nPour démarrer, dites juste : \"Alexa, lance Ma Radio\" ou \"Alexa, démarre ma radio\"\".\n\nA n'importe quel moment, vous pouvez dire \"Alexa, stop\" pour arrêter.", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "es-ES": { + "summary": "Escucha Mi Radio", + "examplePhrases": [ + "Alexa, abre mi radio", + "Alexa, pide a mi radio poner la radio", + "Alexa, pide a mi radio reproducir" ], + "keywords": [ + "música", + "reproducir radio", + "radio" + ], + "name": "Mi Radio", + "description": "Escucha Mi Radio.\n\nPara empezar, solo tienes que decir \"Alexa, abre mi radio\" o \"Alexa, pide a mi radio poner la radio\".\n\nEn cualquier momento, tu puedes parar la radio diciendo \"Alexa, para\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "es-MX": { + "summary": "Escucha Mi Radio", + "examplePhrases": [ + "Alexa, abre mi radio", + "Alexa, pide a mi radio poner la radio", + "Alexa, pide a mi radio reproducir" ], + "keywords": [ + "música", + "reproducir radio", + "radio" + ], + "name": "Mi Radio", + "description": "Escucha Mi Radio.\n\nPara empezar, solo tienes que decir \"Alexa, abre mi radio\" o \"Alexa, pide a mi radio poner la radio\".\n\nEn cualquier momento, tu puedes parar la radio diciendo \"Alexa, para\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + }, + "it-IT": { + "summary": "Ascolta la mia radio.", + "examplePhrases": [ + "Alexa, apri la mia radio", + "Alexa, chiedi a la mia radio di avviare la radio", + "Alexa, chiedi a la mia radio di giocare" + ], + "keywords": [ + "musica", + "live", + "radio" + ], + "name": "La Mia Radio", + "description": "Ascolta La Mia Radio.\n\nPer iniziare, basta dire \"Alexa, apri La Mia Radio\" o \"Alexa, chiedi a la mia radio di avviare la radio\".\n\nIn qualsiasi momento, puoi interrompere la radio dicendo \"Alexa, basta\"", + "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", + "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" + } + }, + "isAvailableWorldwide": true, + "testingInstructions": "Include your testing instruction (if any) here", + "category": "STREAMING_SERVICE", + "distributionCountries": [] + }, + "apis": { + "custom": { + "endpoint": { + "sourceDir": "lambda/py" + }, + "interfaces": [ + { + "type": "AUDIO_PLAYER" + } + ] + } + }, + "manifestVersion": "1.0", + "permissions": [], + "privacyAndCompliance": { + "allowsPurchases": false, + "isExportCompliant": true, + "containsAds": false, + "isChildDirected": false, + "usesPersonalInfo": false + } + } +}