AWS Serverless Text-to-Speech Solution with AWS CDK and TypeScript pt.II
Tags: typescript, aws, serverless, infrastructureascode
Published: 2023-07-12 05:44:44

In the first part of this article [check it out right here](https://dev.to/brenonaraujo/aws-serverless-text-to-speech-solution-with-aws-cdk-and-typescript-pti-379f), we saw an overview of the solution. Now, we will delve into the code and explain how to build the solution from scratch. I will highlight the key aspects of the codebase that I have created for exploring this tool and constructing the serverless solution.

If you haven't read the first part yet, I recommend doing so before proceeding. However, if you are eager to get your hands dirty and build this solution step by step, you can continue reading.

Alternatively, if you simply want to deploy and test how this solution works, you can follow the instructions provided in the readme of the [GitHub repository](https://github.com/brenonaraujo/aws-cdk-serverless-text-to-speech/tree/main)

## Build from scratch

To create this application you'll need to have Node.js (>= 16.3.0), AWS CDK, and AWS CLI installed. You'll also need an AWS account. The AWS CLI should be configured with your AWS credentials.

> If you try to run CDK with a Node version below the one indicated above, you will see the warning message: “v16.2.0 is unsupported and has known compatibility issues with this software.” Update your Node version, and it should be fine.

Here are the relevant links to the official AWS documentation that will help you with the pre-requisites:

1. **AWS CDK**: The [AWS CDK Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html) provides detailed information on how to install and configure AWS CDK. The AWS CDK (Cloud Development Kit) can be installed via npm (Node Package Manager) using the command: `npm install -g aws-cdk`

2. **AWS CLI**: You can install the AWS CLI (Command Line Interface) following the instructions in the [AWS CLI User Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). You can install the AWS CLI using pip, which is a package manager for Python. The command to install the AWS CLI is: `pip install awscli`

3. **Configuring the AWS CLI**: Once the AWS CLI is installed, you will need to configure it with your AWS credentials. This includes your Access Key ID and Secret Access Key. Instructions for this can be found in the [AWS CLI User Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html). The command to configure the AWS CLI is: `aws configure`

---

**Step 1: Create a new CDK project**

Create a new directory for the project:

```bash
mkdir text-to-speech-app
cd text-to-speech-app
```

Initialize a new CDK project:

```bash
cdk init --language=typescript
```

**Step 2: Install AWS CDK packages**

You will need several packages for this application. Install them as follows:

```bash
npm install @aws-cdk/aws-s3 @aws-cdk/aws-dynamodb @aws-cdk/aws-sqs @aws-cdk/aws-lambda @aws-cdk/aws-lambda-event-sources @aws-cdk/aws-iam --save
```

After that your package json has to look like this:

``` "dependencies": {
    "@aws-cdk/aws-dynamodb": "^1.204.0",
    "@aws-cdk/aws-iam": "^1.204.0",
    "@aws-cdk/aws-lambda": "^1.204.0",
    "@aws-cdk/aws-lambda-event-sources": "^1.204.0",
    "@aws-cdk/aws-s3": "^1.204.0",
    "@aws-cdk/aws-s3-notifications": "^1.204.0",
    "@aws-cdk/aws-sqs": "^1.204.0",
    "aws-cdk-lib": "2.87.0",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.21"
  }```

**Step 3: Create S3 Buckets**

Open the `lib/text-to-speech-app-stack.ts` file and start creating the S3 buckets:

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

import * as s3 from 'aws-cdk-lib/aws-s3';

export class TextToSpeechAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const textBucket = new s3.Bucket(this, 'TextBucket', {
      versioned: false,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    const audioBucket = new s3.Bucket(this, 'AudioBucket', {
      versioned: false,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });
  }
}
```
**Step 4: Create a DynamoDB Table**

Add the following lines to create the `MetadataTable` DynamoDB table:

```typescript
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

// inside the constructor bellow audioBucket Creation

const metadataTable = new dynamodb.Table(this, 'MetadataTable', {
      partitionKey: { name: 'uuid', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'submissionTime', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
    });
```

**Step 5: Create an SQS Queue**

Next, create the `ProcessingQueue` SQS queue:

```typescript
import * as sqs from 'aws-cdk-lib/aws-sqs';

// inside the constructor bellow Metadatatable

  const processingQueue = new sqs.Queue(this, 'ProcessingQueue', {
      visibilityTimeout: cdk.Duration.seconds(300), // 5 minutes
    });
```

**Step 6: Create an S3 Event Notification with SQS as target**

As we  had our textBucket and processingQueue now we can create an S3 Event Notification to sends a message to our processing queue when a file is uploaded to the `/upload` path.

```typescript
//
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';

// inside the constructor bellow processingQueue    textBucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SqsDestination(processingQueue), { prefix: 'upload/' });

```

> We've created the main infrastructure to be used in our solution, and the highlight here is that we've written this in only 38 lines of TypeScript code.

You can execute the `cdk deploy` command to deploy the current state of our Stack before we add the function as constructs to be used in this stack.

The CDK shows to you the progress of deploy right on your terminal:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64tyf7r4jr99xkie81jz.png)

And after this process had finished you can go to your console to check the status of our infrastructure.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bgfeujmvclokxm8jy8q.png)

Now we can go forward and create the lambda function that will actually does the magic for us

**Step 7: Create the TextToSpeech Construct**

Our lambda function will be created as a new constructor in our CDK project, for this let's create a new folder on the /lib called TextToSpeech and inside this folder lets creates two new files TextToSpeech.ts and TextToSpeech.lambda.ts, our project structure will be like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9s7hysnjf7apvyvv9b53.png)

Now we can go to the TextToSpeech.ts file and create our lambda constructor, here we'll add all the lambda function configurations:

```typescript
import { Construct } from "constructs";

import { IBucket } from "aws-cdk-lib/aws-s3";
import { ITable } from "aws-cdk-lib/aws-dynamodb";
import { IQueue } from "aws-cdk-lib/aws-sqs";

export class TextToSpeech extends Construct {
    constructor(scope: Construct, id: string, props: {
        textBucket: IBucket,
        audioBucket: IBucket,
        metadataTable: ITable,
        processingQueue: IQueue

    }) {
        super(scope, id)

       }
}
```

This is our base constructor where we'll define all the lambda resources, the highlight here is the ‘props’ where we've declared all the necessary resources the lambda will interact with.

**Step 8: Now we can finally define our lambda funtion**

```typescript
import path = require("path");
import * as cdk from 'aws-cdk-lib';

import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import * as iam from 'aws-cdk-lib/aws-iam';
import { IBucket } from "aws-cdk-lib/aws-s3";
import { ITable } from "aws-cdk-lib/aws-dynamodb";
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { IQueue } from "aws-cdk-lib/aws-sqs";

// Inside the constructor
const textToSpeechFunction = new NodejsFunction(this, "NodejsFunction", {
            entry: path.resolve(__dirname, "TextToSpeech.lambda.ts"),
            bundling: {
                nodeModules: ['aws-sdk'],
            },
            environment: {
                TEXT_BUCKET_NAME: props.textBucket.bucketName,
                AUDIO_BUCKET_NAME: props.audioBucket.bucketName,
                METADATA_TABLE_NAME: props.metadataTable.tableName
            },
            timeout: cdk.Duration.seconds(60) // 1 min
        })
```

The highlight here is that we use path to define where the lambda code is in the **entry** propertie and from now we just pass in the bundling propertie the aws-sdk depedencie to be applied at our lambda bundle.

> The timeout on our tests was enoght to use this API that has an affordable response time.

**Step 9: Grant Permissions to the Resources**

Now we need to grant the necessary permissions to our resources.

```typescript
props.textBucket.grantRead(textToSpeechFunction);
        props.metadataTable.grantReadWriteData(textToSpeechFunction);
        props.audioBucket.grantWrite(textToSpeechFunction);
        textToSpeechFunction.addToRolePolicy(
            new iam.PolicyStatement({
                actions: ['polly:SynthesizeSpeech'],
                resources: ['*'],
            })
        );

        textToSpeechFunction.addEventSource(
            new SqsEventSource(props.processingQueue)
        )
);
```

In this instance, we utilize the resources that we've declared in the constructor props to configure the necessary permissions. This piece of code simplifies the creation of roles and policies by using the `grantRead` and `grantReadWriteData` functions. Following this, we use the **IAM PolicyStatement** to add a custom policy to our Lambda function, specifically defining the 'synthesizeSpeech' action.

The full file can be founded [here at github](https://github.com/brenonaraujo/aws-cdk-serverless-text-to-speech/blob/3cef7d4d4a5623284f46a6929cba2a58333805f4/lib/TextToSpeech/TextToSpeech.ts)

**Step 10: Create the Lambda Function Code**

Now go to the TextToSpeech.lambda.ts file to define our lambda code as bellow:

```typescript
import { S3, DynamoDB, Polly } from 'aws-sdk';
import { SQSEvent } from 'aws-lambda';
import { GenerateUUID } from './utils/UUIDGenerator';

const s3 = new S3();
const dynamodb = new DynamoDB.DocumentClient();
const polly = new Polly();

exports.handler = async (event: SQSEvent) => {
    const textBucketName = process.env.TEXT_BUCKET_NAME || '';
    const audioBucketName = process.env.AUDIO_BUCKET_NAME || '';
    const metadataTableName = process.env.METADATA_TABLE_NAME || '';

    for (const record of event.Records) {
        const body = JSON.parse(record.body);
        const textKey = body.Records[0].s3.object.key;

        // Read the text file from the text bucket
        const textObject = await s3.getObject({
            Bucket: textBucketName,
            Key: textKey
        }).promise();
        if (!textObject.Body) {
            throw new Error(`Failed to get text body from S3 object: ${textKey}`);
        }
        const text = textObject.Body.toString();

        const pollyResponse = await polly.synthesizeSpeech({
            OutputFormat: 'mp3',
            Text: text,
            VoiceId: 'Joanna',
        }).promise();

        if (pollyResponse.AudioStream) {
            const date = new Date();
            const year = date.getFullYear();
            const month = date.getMonth() + 1;
            const day = date.getDate();
            const timestamp = Date.now();

            const uuid = GenerateUUID();

            const audioKey = `synthesized/${uuid}/year=${year}/month=${month}/day=${day}/${timestamp}.mp3`;
            await s3.putObject({
                Bucket: audioBucketName,
                Key: audioKey,
                Body: pollyResponse.AudioStream
            }).promise();

            await dynamodb.put({
                TableName: metadataTableName,
                Item: {
                    'uuid': uuid,
                    'submissionTime': textObject.LastModified?.toISOString(),
                    'textKey': textKey,
                    'audioKey': audioKey,
                    'characters': pollyResponse.RequestCharacters,
                    'status': 'completed'
                }
            }).promise();

            console.log(`Process ${uuid} synthesized file ${textKey} with ${pollyResponse.RequestCharacters}, saved at: ${audioKey}`)
        }
    }

    return {
        statusCode: 200,
        body: JSON.stringify('Text to speech conversion completed successfully!'),
    };
};

```
Not any news right here it's just our implementation to handle the sqs event grab the s3 objectKey and than using the Amazon Polly from aws-sdk to synthesize the text from file into audio. It saves the data into the metadata table when finishes

**Step 11: Uses the TextToSpeech constructor**

Now we just have to import our newly created constructor into our stack file `text-to-speech-app-stack.ts`

```typescript
import { TextToSpeech } from './TextToSpeech/TextToSpeech';

// Bellow the addEventNotification 

    new TextToSpeech(this, 'TextToSpeech', {
      textBucket: textBucket,
      audioBucket: audioBucket,
      metadataTable: metadataTable,
      processingQueue: processingQueue
    });
```

After this setup we can just deploy our Stack and test if its everything working.

**Step 12: Deploy the Application**

Finally, build and deploy the application:

```bash
npm run build
cdk deploy
```

This will build your application and deploy it to your AWS account.

### Testing our App

Once the deployment process is complete, please access your account and locate the text bucket. Place a text file into the bucket. After a short period of time, you will be able to view your audio file in the audio bucket.

To view detailed information about each processed file and the number of characters used to synthesize the texts you have sent, refer to the metadata table.

If you prefer, you can directly download the audio files from the S3 platform. Alternatively, you have the option to explore the journey and integrate an API with CDK. I highly recommend this option as it is simple to implement.

## Final Thoughts and Further Exploration

After exploring this Text-to-speech solution, I am excited about the potential to create more solutions using CDK and TypeScript. TypeScript is particularly useful because it is a typed language, which aligns well with backend and frontend development and now to infrastructure development too. CDK also offers the opportunity to use the same language for infrastructure code. Additionally, I am interested in exploring CDK in other programming languages. In my opinion, CDK simplifies a lot of the previous challenges we faced with tools like terraform or cloudFormation, which the CDK utilizes behind the scenes.

If you have any thoughts or would like to discuss further, please let me know. I also welcome any feedback. Thank you for joining me on this journey, and stay tuned for potential new posts on my pages.