Converting text to speech with AWS Polly

I wanted to try my hand at using the AWS Polly text-to-speech service.  Polly offers several different voices and supports multiple languages, most of which sound pretty good, especially if you use SSML when passing text.  SSML is where the character of the speech (rate, tone, pitch, etc) come into play.  See here for more detail.

What I’ve done is created a script to interact with the AWS Polly API using PHP and store the output into an S3 bucket.  Click here to try it out.

Step 1: Creating the IAM User

This has been outlined in many prior posts so I won’t go into detail.  We’ll be using the AmazonS3FullAccess and AmazonPollyFullAccess permission policies (screenshot of my user summary here).  If you don’t plan on saving the results to S3, you don’t need the S3 policy attached (obviously).

Step 2:  Converting our text to speech

<?php
require '/home/bitnami/vendor/autoload.php'; 

//Prep the Polly client and plug in our IAM credentials
use Aws\Polly\PollyClient;
$clientPolly = new PollyClient([
'version' => 'latest',
'region' => 'us-west-2', //I have all of my AWS stuff in USW2 but it's merely preference given my location.
'credentials' => [
  'key' => '', //IAM user key
  'secret' => '', //IAM user secret
 ]]);

//Configure our inputs and desired output details
$resultPolly = $clientPolly->synthesizeSpeech([
'OutputFormat' => 'mp3',
'Text' => "<speak>This is my example.</speak>",
'TextType' => 'ssml',
'VoiceId' => 'Joey', //A list of voices is available here: https://docs.aws.amazon.com/polly/latest/dg/voicelist.html
]);

//Get our results
$resultPollyData = $resultPolly->get('AudioStream')->getContents();

?>

Step 3: Saving the MP3 file to S3

<?php
//Set timezone to use in the file name
date_default_timezone_set('America/Los_Angeles');

//Prep S3 client and plug in the IAM credentials
$clientS3 = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'us-west-2', //Region of your S3 bucket
'credentials' => [
  'key' => '', //Same IAM user as above
  'secret' => '',  //Same as above
 ]]);

//Put the Polly MP3 file to S3
$resultS3 = $clientS3->putObject([
  'Key'         => date("Y-m-d_H:i:s").'.mp3',
  'ACL'         => 'public-read',
  'Body'        => $resultPollyData,
  'Bucket'      => 'fkpolly',
  'ContentType' => 'audio/mpeg',
  'SampleRate'  => '22050'
]);

//Return a link to the file
echo "<a href=\"".$resultS3['ObjectURL']."\">Listen</a>";
?>

Try It Out

I’ve saved my example for ongoing tinkering.  Click here to try it out.

Using Natural Language Processing (AWS Comprehend) to Analyze Text (Cardi B Lyrics)

Humans spend a lot of time reading, analyzing, and responding through text (emails, chats, etc). A lot of this is inefficient or not for pleasure (such as the amount of payroll companies spend to read through feedback emails or the amount of time I spend sifting through Outlook each day). Using Natural Language Processing (NLP), we can reduce the inefficient and not-for-pleasure reading we do so that time can be re-invested into something more productive or fulfilling.

For fun, I scrapily ran the lyrics to Cardi B’s “I Like It” through AWS Comprehend to see what its response would be. I also ran a review of Mission Impossible: Fallout through the same service.  The full output for Cardi B can be viewed here.  The full output for Mission Impossible: Fallout can be viewed here.

While these are low value examples, a more real-world use case for Comprehend could be using AWS Comprehend to detect the language of emails sent to your company to adjust the routing destination in real-time (should they go to you English team or your Spanish?). Another example would be using Comprehend to collect feedback on your new product launch or ad campaign. For example, we could easily capture Twitter mentions for a brand, funnel those into an S3 bucket, and run the contents of that buck to split out negative vs positive vs neutral vs mixed sentiment mentions. From there, we could surface the most frequent adjectives and entities mentioned for each group of each sentiment bucket. It’s a cheap, quick way to customer capture and analyze feedback that would otherwise be ignored.

Initial Setup
In each of the last couple of posts, I’ve outline how to create an IAM user for your project so I won’t repeat that again.  After we have our IAM user created and the credentials added to our /.aws/credentials file, we’ll import the AWS PHP SDK and ComprehendClient.  Next, we’ll create a Comprehend client, define the API version, region, and credentials profile to use:

<?php

require '/home/vendor/autoload.php'; 
use Aws\Comprehend\ComprehendClient;

$client = new Aws\Comprehend\ComprehendClient([
    'version'     => '2017-11-27',
    'region'      => 'us-west-2',
    'profile'     => 'fk-comprehend',
]);
$review="Your Comprehend text";

Exploring the Options
In this example, we’ll detect the language(s), entities (objects, businesses, etc), key phrases, sentiment, and syntax (parts of speech) of our sample texts (Cardi B lyrics and a movie review).  For all of these except DetectDominantLanguage, the language is a required input.  If we use Comprehend to identify that first, then we can simply repeat its output in later functions.  For each output, Comprehend also spits out a confidence score which basically  tells you how confident it is in the output.  This could be used to ignore low-confidence suggestions, thus increasing the accuracy of the models you build using Comprehend.

DetectDominantLanguage Example
This will detect the language and spit out the ISO abbreviation.

//Detecting Dominant Language
$result = $client->detectDominantLanguage([
    "Text" => "$review",
]);

echo "<h1>DetectDominantLanguage</h1><pre>";
print_r($result);
echo "</pre>";

foreach ($result['Languages'] as $phrase) {
    echo "Language ".$phrase['LanguageCode']." has a confidence score of ".round($phrase['Score']*100)."%.<br />";
}

DetectSentiment Example

//Detecting Sentiment
$result = $client->detectSentiment([
    "LanguageCode" => "en",
    "Text" => "$review",
]);

echo "<h1>DetectSentiment</h1><pre>";
print_r($result);
echo "</pre>";

echo "Sentiment: ".$result['Sentiment']."<br />";
echo "Positive: ".round($result['SentimentScore']['Positive']*100)."%<br />";
echo "Negative: ".round($result['SentimentScore']['Negative']*100)."%<br />";
echo "Neutral: ".round($result['SentimentScore']['Neutral']*100)."%<br />";
echo "Mixed: ".round($result['SentimentScore']['Mixed']*100)."%<br />";

DetectKeyPhrases Example

//Detecting KeyPhrases
$result = $client->detectKeyPhrases([
    "LanguageCode" => "en",
    "Text" => "$review",
]);

echo "<h1>DetectKeyPhrases</h1><pre>";
print_r($result);
echo "</pre>";

foreach ($result['KeyPhrases'] as $phrase) {
    echo "Phrase ".$phrase['Text']." has a score of ".round($phrase['Score']*100)."%.<br />";
}

DetectSyntax Example

//Detecting Syntax
$result = $client->detectSyntax([
    "LanguageCode" => "en",
    "Text" => "$review",
]);

echo "<h1>DetectSyntax</h1><pre>";
print_r($result);
echo "</pre>";

foreach ($result['SyntaxTokens'] as $syntax) {
    echo "Phrase ".$syntax['Text']." is as ".$syntax['PartOfSpeech']['Tag']." (with ".round($syntax['PartOfSpeech']['Score']*100)."% confidence).<br />";
}

DetectEntities Example

//Detecting Entities
$result = $client->detectEntities([
    "LanguageCode" => "en",
    "Text" => "$review",
]);

echo "<h1>DetectEntities</h1><pre>";
print_r($result);
echo "</pre>";

foreach ($result['Entities'] as $syntax) {
    echo "Phrase ".$syntax['Text']." is as ".$syntax['Type']." (".round($syntax['Score']*100)."% confidence).<br />";
}

The Results for Cardi B and Tom Cruise

The full output for Cardi B can be viewed here.  This one is the most interesting of the two as “I like it” has a Spanish verse.  You can see how Comprehend dealt with it when it was passed as English.  It also does a good job of determining when “bitch” is a noun vs an adjective except in the line “Where’s my pen? Bitch I’m signin'” — I’m unsure as to why.

The full output for Mission Impossible: Fallout can be viewed here.  The interesting piece here is the sentiment analysis: NEUTRAL (8% positive, 36% negative, 39% neutral, and 17% mixed).  After reading the review, I would say this is pretty in-line with the reviewer and Comprehend did a good job of identifying the overall sentiment of the article.

Automatically Creating Lightsail Instance Snapshots

Given the target audience of Lightsail, I would expect UI-based functionality for automating snapshots and other common tasks; however, this doesn’t exist.  Creating snapshots is an important task – I create snapshots before I make any major changes and every few days.  In the event I screw something up or if something happens to my instance, I can simply spin up a new instance from an old snapshot – no big deal.

In addition to the lack of UI-based functionality, the default IAM policies don’t apply to Lightsail, either.  Given the age of Lightsail, I would think this would be built into IAM default policies by this point.

In the guide below, we’ll:

  1. Create an IAM policy to manage our Lightsail snapshots
  2. Create an IAM user to use that IAM policy
  3. Add our IAM user to our AWS credentials file
  4. Create a Lightsail snapshot using the AWS CLI

Beyond creating snapshots, there AWS CLI offers all commands needed to manage Lightsail – I encourage you to explore: https://docs.aws.amazon.com/cli/latest/reference/lightsail/index.html

Create the needed IAM Policy

  1. From the IAM page of the AWS Console, select Policies.
  2. From there, click “Create Policy” and select the json tab.  We’ll use this policy which will limit actions to just the creation and listing of instance snapshots:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "lightsail:GetInstanceSnapshot",
                    "lightsail:GetInstanceSnapshots",
                    "lightsail:CreateInstanceSnapshot"
                ],
                "Resource": "*"
            }
        ]
    }

    Here’s a screenshot of the inputs.

  3. Click “Review Policy” and then we’ll give it a name (I’ve used “LightsailSnapshotCreate”) and then click “Create Policy”

Create the needed IAM User

  1. Back on the IAM page of the AWS Console, click on Users.
  2. We’ll name the user (I’ve used “FKSnapshotCreate” and check the “Programmatic Access” box.  Screenshot.
  3. On next page, we’ll attach the policy we created (“LightsailSnapshotCreate”) and create the user.  Screenshot.
  4. Lastly, we’ll copy the access ID and key into our credentials file.  Screenshot.

Adding the IAM User Access Key/Access ID to our Credentials file

  1. Open your credentials file and add in another user (as described here).
  2. In my project, I have a user named “fk-createsnapshot” so my credentials file looks something like this.

Creating a snapshot using AWS CLI

  1. Now that we have everything configured, we just need to run the command.  We’ll do this with the following command:
    aws lightsail create-instance-snapshot --instance-name FigarosKingdomWP --instance-snapshot-name FK-2018-09
    -09 --profile fk-createsnapshot --region us-west-2

    “–instance-name FigarosKingdomWP” – this is the name of the instance from the Lightsail console that you’re wanting to snapshot.
    “–instance-snapshot-name FK-2018-09-09” – this is the name of the snapshot.  It can be anything you like.
    “–profile fk-createsnapshot” – this is the IAM User (same one we created above) that we want to use with this command.
    “–region us-west-2” – this is the region of the instance.

  2. Once executed, you should see output similar to this:
  3. Browse to the Lightsail Console and you should see your new snapshot: screenshot.

Creating a shell script to automate snapshots

We’ll create a shell script (I’m using fk-createsnapshot.sh) and add in our command.  I’ve also added variables for the date so that the snapshot name matches the date.  You can find more about this here.  Here’s my fk-createsnapshot.sh:

#!/bin/bash 
aws lightsail create-instance-snapshot --instance-name FigarosKingdomWP --instance-snapshot-name FK-$(date +%Y-%m-%d) --profile fk-createsnapshot --region us-west-2

Adding a cron job to run the shell script

In our crontab (crontab -e), we’ll set it to run at 3am every day (adjust to your script location):

0 3 * * * /home/bitnami/fk-createsnapshot.sh

It may be a good idea to set it to run sooner just to verify that everything is working as intended.  Otherwise, you’ll start seeing new snapshots appear in your Lightsail console every day at 3am!

Using this same approach, you can take pretty much all actions for your Lightsail instances directly through the CLI.  Check out the AWS CLI documentation for more.

 

Using AWS Lambda to Send SNS Topics in CloudWatch

AWS Lambda enables you to run code without managing a server.  You simply plop in your code and it does the rest (no maintenance, scaling concerns, etc).  The cost is only $0.20 per 1 million requests/month and the first million requests are free each month.

In the previous post, I setup an SNS topic. I’m extending this further so that a node.js function will be triggered in AWS Lambda each time my SNS topic is triggered. This Lambda function will feed metrics into AWS CloudWatch which will allow me to chart/monitor/set alarms against events or patterns with my SNS topic.  A practical use case for this could be understanding event patterns or logging SNS messages (and their contents) sent to your customers.

Creating your Lambda Function

From the Lambda page of the AWS console, select “Create Function”.  From here, we’ll author from scratch.  Below are the inputs I’ve used for this example:
Name: SNSPingerToCloudWatch
Runtime: Node.js 8.10
Role: Choose and existing role
Existing role: lambda_basic_execution

On the page after selecting “Create Function”, we’ll click “SNS” from the “Add Triggers” section and then select our SNS topic in the “Configure Triggers” section.  Then click “Add” and “Save”.  Here’s a screenshot of the final state.

Next, click on your function name (SNSPingerToCloudWatch) in the flow chart and scroll to edit the function code.
The JS we’ll use:

exports.handler = async (event, context) => {
    const message = event.Records[0].Sns.Message;
    console.log('Pinger says:', message);
    return message;
};

Under Basic Settings, I’ve set the timeout duration to 5 seconds (because that’s the timeout duration I have set in my SNS topic PHP script) You can add descriptions, throttles, etc but I’m leaving those at the defaults.  Here’s a screenshot of my final config for this Lambda function.

Once complete, click “Save” again and then we’re ready to test. I manually fired my SNS topic and jumped over to the “Monitoring” tab of the console. It took a minute or so but I saw my event appear. From here, you can view the log details in CloudWatch, as well.

Publish to AWS SNS Topic With PHP

Simple Notification Service (SNS) is a handy AWS product which enables programmatic publication and subscription to topics.  They can be as simple as email or SMS or involve more complicated services complicated like Lambda, SQS, HTTP, etc.  The write-up below walks through the process end-to-end from installing the AWS PHP SDK to publishing your first message as an SMS/text message.  With a small amount of additional effort, one could quickly expand this to use cases like weather/emergency notifications for office buildings/schools.

The first two steps are one-time setup, walking through AWS PHP SDK installation and IAM Role creation (something new to me).  The remaining steps are a rinse-and-repeat process so future SNS projects should only take minutes to setup.  For this example, I spun up a LAMP instance in Lightsail so my approach is tailored to this default config.

Step 1: Installing/Prepping the AWS PHP SDK

We’re going to do this by using Composer so let’s install that: curl -sS https://getcomposer.org/installer | sudo php

Next, we’ll create composer.json to add the right dependency for the AWS SDK: sudo nano composer.json

And inside composer.json, we’ll add this requirement:

{
    "require": {
        "aws/aws-sdk-php": "2.*"
    }
}

Lastly, we’ll install the dependencies: php composer.phar install

The end result should be the presence of ./vendor for your project directory.  This is where the needed libraries will live and an autoloader script which you’ll add as a requirement to your project (noted below).

The next thing we’ll do is setup the the credentials file to house the IAM credentials for this project (and any future projects).  We’ll do this by creating a .aws directory and then creating a credentials file within that directory:

sudo mkdir .aws
cd ./.aws
sudo nano credentials

The credentials file structure is outlined in detail here.  Our credentials file will look like:

[sns-project]
aws_access_key_id = ... 
aws_secret_access_key = ...

Here’s a recording of step 1 as I went through it:

Play

Step 2: Creating the IAM credentials for your project

Now, let’s create the IAM credentials we’ll need to plug into the credentials file.

  1. Navigate to the IAM Console and select “Users”.  Click “Add User“.
  2. Name your project (should match what you’ve loaded in your credentials file- ours is “sns-project”) and check the Programmatic Access box.
  3. Use the existing “AmazonSNSFullAccess” policy and click “Review” to verify everything is correct.
  4. Copy your IAM access key ID and secret IAM access key to notepad – you’ll need these in a moment.  These will be what we plug into our credentials file
  5. Lastly, let’s go back to our SSH window and edit the credentials file to add the aws_access_key_id a d aws_secret_access_key from the console in place the “…” we originally put in the credentials file.

Step 3: Setting up your SNS Topic

We’re finished with the one-time setup portion of this project.  The rest can be a rinse-and-repeat process if you want to setup multiple projects.  For this example, I’m just going to setup a topic to send me SMS/text messages but SNS supports a number of different actions.


Play

Step 4: The PHP Script

This example will just publish a static message but it should be simple enough to expand to your use-cases/projects.  I’ve commented what’s happening inline below but a few callouts…

  • Your profile (“sns-project”) should match what you set in your credentials file.  Example screenshot here.
  • Your region has to match the region in which you created your SNS topic.
  • If you’re using something other than SMS messaging for delivery, you can specify additional parameters in the array below.

<?php 
putenv('HOME=/home/bitnami'); //Define the home location so can trigger from other locations (cronjobs, for example)
require '../vendor/autoload.php'; //Load the SDK
use Aws\Sns\SnsClient; //Specify  the SNS client from the SDK 

$client = SnsClient::factory(
  array(
     'sns-project', //This should match your profile name in the credentials file
     'region' => 'us-west-2', //The region you created your SNS topic (is also noted in your AWS SNS console)
     'version' => '2010-03-31', //The API version to use - no need to modify
  )
);

$payload = array(
     'TopicArn' => 'arn:aws:sns:us-west-2:189998:sns-project', //The topic ARN from your AWS console
     'Message' => 'This is the content of your text message', //The content of your message.  If you're using email, you can also add Subject to this array to set subject line
     'MessageStructure' => 'string', ); 
try {
      $client->publish( $payload );
      echo 'Success!';
} catch ( Exception $e ) {
      echo "Failure!\n" . $e->getMessage();
}

From here, you should be good to go!