Skip to content

Commit

Permalink
2018 Revision
Browse files Browse the repository at this point in the history
Includes new transcribe, translate, and speak Step Functions workflow.
  • Loading branch information
brianklaas committed Apr 18, 2018
1 parent 5e2d794 commit b19ef9b
Show file tree
Hide file tree
Showing 23 changed files with 681 additions and 43 deletions.
Binary file added .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.project
.DS_Store
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# The AWS Playbox: Demos for "Level Up Your Web Apps With Amazon Web Services"
# The AWS Playbox: Demos for Using Amazon Web Services from CFML

This repo contains the sample app used during my presentation "Level Up Your Web Apps With Amazon Web Services." It contains sample CFML and Python code for working with a number of AWS services.

There are three requirements for getting these demos working. I know that sounds like a lot, but we're dealing with a series of external services in multiple lanaguages in these demos, and each must be set up correctly.
There are three requirements for getting these demos working:

1. Add the AWS SDK .jar and related files to your CF install.
2. Set up your own AWS credentials and add them to awsCredentials.cfc.
Expand All @@ -12,7 +10,7 @@ There are three requirements for getting these demos working. I know that sounds

The demos in this repo require that you have the following .jar files in your /coldfusion/lib/ directory:

- aws-java-sdk-1.11.120 or later
- aws-java-sdk-1.11.311 or later
- jackson-annotations
- jackson-core
- jackson-databind
Expand All @@ -26,13 +24,16 @@ You have to create your own AWS account and provide both the AccessKey and Secre

The account for which you are providing credentials must also have permissions for the following services:

- S3
- SNS
- Lambda
- CloudWatch (for Lambda logging)
- Rekognition
- S3 (for Rekognition)
- Step Functions
- DynamoDB
- Step Functions
- Rekognition
- Transcribe
- Translate
- Polly

For more infomration about IAM accounts, roles, and permissions, please review the [IAM guide](http://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html).

Expand All @@ -44,7 +45,23 @@ You need to set up the following resources within AWS for these demos to work:
2. Lambda - create a Lambda function using the code in nodejs/lambda/lambda-returnDataToCaller.js. The Lambda runtime should be NodeJS 4.3 or later, and you do not need to configure a trigger for the function, as it will be invoked from this application. The ARN of the function must be added to application.cfc.
3. DynamoDB - create a DynamoDB table with a partition (primary) key of "userID" (String) and a sort key (range key) of "epochTime" (Number). The table name must be added to application.cfc.
4. Rekognition - add photos for Rekognition to analyze. There's a separate list of photos for matching faces, and a list for generating labels (image analysis). These photos and the name of the S3 bucket in which they can be found need to be added to the top of rekognition.cfm.
4. Step Functions - You must first create two Lambda functions using the code in nodejs/lambda/ -- generateRandomNumber.js and detectLabelsForImage.js. The ARNs of those functions must be added to stateMachines/choiceDemoStateMachine.json. Additionally, the name of the S3 bucket and the path to the photos that will be analyzed as part of this step function must be added to stateMachines/choiceDemoStateMachine.json. Once you've added all the required information, use stateMachines/choiceDemoStateMachine.json to create a new Step Function state machine in the AWS Console.
5. Step Functions:
There are two workflows you can set up:
- Describe an Image

a. Create the two Lambda functions used in this workflow using the code in nodejs/lambda/ -- generateRandomNumber.js and detectLabelsForImage.js.
b. Add the ARNs of those functions to stateMachines/choiceDemoStateMachine.json.
c. Add the name of the S3 bucket and the path to the photos that will be analyzed to stateMachines/choiceDemoStateMachine.json.
d. Once you've added all the required information, use stateMachines/choiceDemoStateMachine.json to create a new Step Function state machine in the AWS Console.
e. Add the ARN of the workflow to application.cfc as the application.awsResources.stepFunctionRandomImageARN value.

- Transcribe, Translate, and Speak a Video

a. Create the five Lambda functions used in this workflow using the code in nodejs/lambda/transcribeTranslateExample. You will need to add the name of your S3 bucket where you want the output from the workflow to go to getTranscriptionFile.js, translateText.js, and convertTextToSpeech.js.
b. Add the ARNs of those functions to stateMachines/transcribeTranslateSpeakWorkflow.json.
c. Once you've added all the required information, use stateMachines/transcribeTranslateSpeakWorkflow.json to create a new Step Function state machine in the AWS Console.
d. Add the ARN of the workflow to application.cfc as the application.awsResources.stepFunctionTranscribeTranslateARN value.
e. Modify stepFunctions.cfm (the inputStruct variable) to point to the URL of a MP4 file on S3.

Remember, the AWS docs are pretty great. Use the [Java API Reference](http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/index.html) often, as it'll tell you almost everything you need to know for working with a particular AWS service.

Expand All @@ -58,4 +75,4 @@ There are a number of Python examples in this repo. You can get them running pre
2. Using your own ARNs as noted in the Python code in the /python directory of the repo
3. Setting python/pythonInvoke.sh to run as an executable on your machine

Boto makes it very easy to use AWS from Python and acts as the AWS-approved and supported SDK for Python.
Boto makes it very easy to use AWS from Python and acts as the AWS-approved and supported SDK for Python.
9 changes: 6 additions & 3 deletions application.cfc
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
component {

this.name = 'awsPlaybox';
this.applicationTimeout = CreateTimeSpan(0, 0, 0, 5);
this.applicationTimeout = CreateTimeSpan(0, 0, 5, 0);
this.sessionManagement = false;

function onApplicationStart(){
application.awsServiceFactory = createObject("component", "model.awsServiceFactory").init();

application.currentStepFunctionExecutions = arrayNew(1);

// Put your ARNs for Lambda, and your DynamoDB table name here
application.awsResources = structNew();
application.awsResources.lambdaFunctionARN = "ARN OF THE LAMBDA FUNCTION IN lambda.cfm GOES HERE";
application.awsResources.stepFunctionARN = "ARN OF THE STEP FUNCTION STATE MACHINE IN stepFunctions.cfm GOES HERE";
application.awsResources.stepFunctionRandomImageARN = "ARN OF THE RANDOM IMAGE STEP FUNCTION STATE MACHINE GOES HERE";
application.awsResources.stepFunctionTranscribeTranslateARN = "ARN OF THE TRANSCRIBE, TRANSLATE, SPEAK STEP FUNCTION STATE MACHINE GOES HERE";
application.awsResources.snsTopicARN = "ARN OF THE SNS TOPIC IN sns.cfm GOES HERE";
application.awsResources.dynamoDBTableName = "TABLE NAME OF THE TABLE IN DYNAMODB GOES HERE";
application.awsResources.dynamoDBTableName = "TABLE NAME OF THE TABLE IN DYNAMODB IN dynamodb.cfm GOES HERE";

return true;
}
Expand Down
Binary file added assets/.DS_Store
Binary file not shown.
17 changes: 16 additions & 1 deletion index.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,31 @@
<p><a href="sns.cfm">Simple Notification Service (SNS)</a></p>
<p><a href="rekognition.cfm">Rekognition</a></p>
<p><a href="stepFunctions.cfm">Step Functions</a></p>
<p>&nbsp;</p>
<h3>Python</h3>
<p><a href="python/sns-python.cfm">Simple Notification Service (SNS)</a></p>
<div class="spacer"></div>
<p>&nbsp;</p>
<div class="smallerText">
<h3>Source Code</h3>
<p>Lambda:</p>
<p><a href="showSourceCode.cfm?fileToGrab=jsLargeFile">Lambda (Node): Check for Large File Uploads</a>
<p><a href="showSourceCode.cfm?fileToGrab=jsReturnData">Lambda (Node): Return Data to Caller</a>
<p><a href="showSourceCode.cfm?fileToGrab=jsGenerateRandom">Lambda (Node): Generate Random Number</a>
<p><a href="showSourceCode.cfm?fileToGrab=jsDetectLabels">Lambda (Node): Detect Labels for Image</a>
<p><a href="showSourceCode.cfm?fileToGrab=stateMachineChoiceDemo">Step Function State Machine Source</a></p>
<h5>Transcribe a Video, Translate, and Speak Tranlation Workflow</h5>
<p><a href="showSourceCode.cfm?fileToGrab=vwfStartTranscribe">Lambda (Node): Start Transcribe Job</a>
<p><a href="showSourceCode.cfm?fileToGrab=vwfCheckTranscribeJob">Lambda (Node): Check Transcribe Job Status</a>
<p><a href="showSourceCode.cfm?fileToGrab=vwfGetTranscript">Lambda (Node): Get Transcription File</a>
<p><a href="showSourceCode.cfm?fileToGrab=vwfTranslateText">Lambda (Node): Translate Text</a>
<p><a href="showSourceCode.cfm?fileToGrab=vwfPrepTranlation">Lambda (Node): Prep Translated Text for Speech</a>
<p><a href="showSourceCode.cfm?fileToGrab=vwfTextToSpeech">Lambda (Node): Convert Text to Speech</a>
<p>&nbsp;</p>
<p>Step Functions:</p>
<p><a href="showSourceCode.cfm?fileToGrab=stateMachineChoiceDemo">State Machine: Describe A Random Image</a></p>
<p><a href="showSourceCode.cfm?fileToGrab=transcribeTanslateStepFunc">State Machine: Transcribe a Video, Translate, and Speak Tranlation</a></p>
<p>&nbsp;</p>
<p>Python Examples</p>
<p><a href="showSourceCode.cfm?fileToGrab=pySubscribe">Python: Subscribe to a SNS Topic via Email</a></p>
<p><a href="showSourceCode.cfm?fileToGrab=pySendSNS">Python: Send a Test SNS Notification</a></p>
<p><a href="showSourceCode.cfm?fileToGrab=pyCheckServers">Lambda (Python): Check Server Status</a>
Expand Down
2 changes: 1 addition & 1 deletion model/awsCredentials.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Additionally, the IAM account with which these credentials are assoicated must h
For more information on IAM accounts and permissions, see http://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html
Author: Brian Klaas (bklaas@jhu.edu)
(c) 2017, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
(c) 2018, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
*/

Expand Down
2 changes: 1 addition & 1 deletion model/awsServiceFactory.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ AWS Service Factory
This component creates AWS service objects based on the parameter passed in.
Author: Brian Klaas (bklaas@jhu.edu)
(c) 2017, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
(c) 2018, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
*/

Expand Down
2 changes: 1 addition & 1 deletion model/dynamoItemMaker.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DynamoDB Item Maker
This component creates DynamoDB Item objects with randomized attributes.
Author: Brian Klaas (bklaas@jhu.edu)
(c) 2017, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
(c) 2018, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
*/

Expand Down
2 changes: 1 addition & 1 deletion model/rekognitionLib.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Rekognition Utility Functions
This component contains functions to make requests to AWS Rekognition.
Author: Brian Klaas (bklaas@jhu.edu)
(c) 2017, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
(c) 2018, The Johns Hopkins Bloomberg School of Public Health Center for Teaching and Learning
*/

Expand Down
Binary file added nodejs/.DS_Store
Binary file not shown.
Binary file added nodejs/lambda/.DS_Store
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const util = require('util');
const AWS = require('aws-sdk');
const transcribe = new AWS.TranscribeService;

exports.handler = (event, context, callback) => {
console.log("Reading input from event:\n", util.inspect(event, {depth: 5}));

var jobName = event.jobName;

var params = {
TranscriptionJobName: jobName
}

var request = transcribe.getTranscriptionJob(params, function(err, data) {
if (err) { // an error occurred
console.log(err, err.stack);
callback(err, null);
} else {
console.log(data); // successful response, return job status
var returnData = {
jobName: jobName,
jobStatus: data.TranscriptionJob.TranscriptionJobStatus,
transcriptFileUri: data.TranscriptionJob.Transcript.TranscriptFileUri,
transcriptFileName: jobName
};
callback(null, returnData);
}
});

};
88 changes: 88 additions & 0 deletions nodejs/lambda/transcribeTranslateExample/convertTextToSpeech.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// As speaking jobs can take a few seconds, the default Lambda function timeout will not be enough. Set the timeout to 10 seconds to give Polly time to do the work.

const util = require('util');
const AWS = require('aws-sdk');
const Polly = new AWS.Polly();
const S3 = new AWS.S3();

exports.handler = (event, context, callback) => {
console.log("Reading input from event:\n", util.inspect(event, {depth: 5}));

var textToSpeak = event.textToSpeak;
var languageOfText = event.languageOfText;
var fileNameForOutput = event.transcriptFileName;

// We have to use promises for both steps in the process because S3 operations are async
makeMP3FromText(textToSpeak, languageOfText).then(function(makeMP3Result) {
console.log("Result from speaking transcript:", makeMP3Result);
return writeFileToS3(makeMP3Result.AudioStream, languageOfText, fileNameForOutput);
}).then(function(mp3FileNameOnS3) {
console.log("mp3FileNameOnS3:" + mp3FileNameOnS3);
var returnData = {};
returnData["mp3FileNameOnS3-"+languageOfText] = mp3FileNameOnS3
callback(null, returnData);
}).catch(function(err) {
console.error("Failed to generate MP3!", err);
callback(err, null);
})
};

function makeMP3FromText(textToSpeak, languageOfText) {
return new Promise(function(resolve, reject) {
console.log("Making an MP3 in the language: " + languageOfText);
var voiceToUse = 'Ivy';
// Polly has a current maximum character length of 3000 characters
var maxLength = 2900;
var trimmedText = textToSpeak.substr(0, maxLength);
switch(languageOfText) {
case 'es':
voiceToUse = (Math.random() >= 0.5) ? "Penelope" : "Miguel";
break;
case 'fr':
voiceToUse = (Math.random() >= 0.5) ? "Celine" : "Mathieu";
break;
case 'de':
voiceToUse = (Math.random() >= 0.5) ? "Vicki" : "Hans";
break;
}
var params = {
OutputFormat: "mp3",
SampleRate: "8000",
Text: trimmedText,
VoiceId: voiceToUse
}
var speakPromise = Polly.synthesizeSpeech(params).promise();
speakPromise.then(function(data) {
console.log('Successfully generated MP3 file');
resolve(data);
}).catch(function(err) {
console.log("Error generating MP3 file:\n", err);
reject(Error(err));
});
});
}

function writeFileToS3(mp3AudioStream, languageOfText, fileNameForOutput) {
return new Promise(function(resolve, reject) {
let audioFileName = fileNameForOutput;
// Remove .txt from the file name, if it exists
if (audioFileName.split('.').pop() == 'txt') {
audioFileName = audioFileName.slice(0, -4);
}
let filePathOnS3 = 'audio/' + audioFileName + '-' + languageOfText + '.mp3';
var params = {
Bucket: 'NAME OF YOUR BUCKET WHERE YOU WANT OUTPUT TO GO',
Key: filePathOnS3,
Body: mp3AudioStream,
ContentType: 'audio/mpeg3'
};
var putObjectPromise = S3.putObject(params).promise();
putObjectPromise.then(function(data) {
console.log('Successfully put audio file on S3');
resolve(filePathOnS3);
}).catch(function(err) {
console.log("Error putting file on S3:\n", err);
reject(Error(err));
});
});
}
70 changes: 70 additions & 0 deletions nodejs/lambda/transcribeTranslateExample/getTranscriptionFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const util = require('util');
const AWS = require('aws-sdk');
const https = require('https');
const S3 = new AWS.S3();

exports.handler = (event, context, callback) => {
console.log("Reading input from event:\n", util.inspect(event, {depth: 5}));

// Note: The authenticated URL that Transcribe provides to you only stays valid for a couple minutes
var transcriptFileUri = event.transcriptFileUri;
var transcriptFileName = event.transcriptFileName;

// We have to use promises for both steps in the process because S3 operations are async
getTranscript(transcriptFileUri).then(function(getTranscriptResponse) {
console.log("Retrieved transcript:", getTranscriptResponse);
return writeTranscriptToS3(getTranscriptResponse,transcriptFileName);
}).then(function(filePathOnS3) {
console.log("filePathOnS3 is " + filePathOnS3);
var returnData = {
transcriptFilePathOnS3: filePathOnS3,
transcriptFileName: transcriptFileName
};
callback(null, returnData);
}).catch(function(err) {
console.error("Failed to write transcript file!", err);
callback(err, null);
})
};

function getTranscript(transcriptFileUri) {
return new Promise(function(resolve, reject) {
https.get(transcriptFileUri, res => {
res.setEncoding("utf8");
let body = "";
res.on("data", data => {
body += data;
});
res.on("end", () => {
body = JSON.parse(body);
let transcript = body.results.transcripts[0].transcript;
console.log("Here's the transcript:\n", transcript);
resolve(transcript);
});
res.on("error", (err) => {
console.log("Error getting transcript:\n", err);
reject(Error(err));
});
});
});
}

function writeTranscriptToS3(transcript,transcriptFileName) {
return new Promise(function(resolve, reject) {
console.log("Writing transcript to S3 with the name" + transcriptFileName);
let filePathOnS3 = 'transcripts/' + transcriptFileName + '.txt';
var params = {
Bucket: 'NAME OF YOUR BUCKET WHERE YOU WANT OUTPUT TO GO',
Key: filePathOnS3,
Body: transcript
};
var putObjectPromise = S3.putObject(params).promise();
putObjectPromise.then(function(data) {
console.log('Successfully put transcript file on S3');
resolve(filePathOnS3);
}).catch(function(err) {
console.log("Error putting file on S3:\n", err);
reject(Error(err));
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This function simply translates variable names between the cfdemoTranslateText and cfDemoConvertTextToSpeech functions because Step Functions states language can't do that.

exports.handler = (event, context, callback) => {

var textToSpeak = event.translatedText;
var languageOfText = event.languageOfText;
var transcriptFileName = event.sourceTranscriptFileName;

var returnData = {
textToSpeak: textToSpeak,
languageOfText: languageOfText,
transcriptFileName: transcriptFileName
}

callback(null, returnData);
};
Loading

0 comments on commit b19ef9b

Please sign in to comment.