Na primeira parte deste artigo confira aqui, vimos uma visão geral da solução. Agora, vamos nos aprofundar no código e explicar como construir a solução do zero. Destacarei os aspectos-chave do código-base que criei para explorar essa ferramenta e construir a solução serverless.
Se você ainda não leu a primeira parte, recomendo fazer isso antes de prosseguir. No entanto, se você está ansioso para colocar as mãos na massa e construir essa solução passo a passo, pode continuar lendo.
Alternativamente, se você simplesmente quer implementar e testar como esta solução funciona, você pode seguir as instruções fornecidas no readme do repositório GitHub
Construir do zero
Para criar este aplicativo, você precisará ter Node.js (>= 16.3.0), AWS CDK e AWS CLI instalados. Você também precisará de uma conta AWS. O AWS CLI deve estar configurado com suas credenciais AWS.
Se você tentar executar o CDK com uma versão do Node abaixo da indicada acima, verá a mensagem de aviso: “v16.2.0 não é suportado e tem conhecidas incompatibilidades com este software.” Atualize sua versão do Node, e tudo deve ficar bem.
Aqui estão os links relevantes para a documentação oficial da AWS que irão ajudá-lo com os pré-requisitos:
AWS CDK: O Guia do Desenvolvedor AWS CDK fornece informações detalhadas sobre como instalar e configurar o AWS CDK. O AWS CDK (Cloud Development Kit) pode ser instalado via npm (Node Package Manager) usando o comando:
npm install -g aws-cdk
AWS CLI: Você pode instalar o AWS CLI (Command Line Interface) seguindo as instruções no Guia do Usuário AWS CLI. Você pode instalar o AWS CLI usando pip, que é um gerenciador de pacotes para Python. O comando para instalar o AWS CLI é:
pip install awscli
Configurando o AWS CLI: Uma vez que o AWS CLI está instalado, você precisará configurá-lo com suas credenciais AWS. Isso inclui seu ID de chave de acesso e chave de acesso secreta. As instruções para isso podem ser encontradas no Guia do Usuário AWS CLI. O comando para configurar o AWS CLI é:
aws configure
Passo 1: Crie um novo projeto CDK
Crie um novo diretório para o projeto:
mkdir text-to-speech-app
cd text-to-speech-app
Inicialize um novo projeto CDK:
cdk init --language=typescript
Passo 2: Instale os pacotes AWS CDK
Você precisará de vários pacotes para esta aplicação. Instale-os da seguinte maneira:
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
Depois disso, seu arquivo package.json deve ficar assim:
"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"
}
Passo 3: Crie os Buckets S3
Abra o arquivo lib/text-to-speech-app-stack.ts
e comece a criar os buckets S3:
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,
});
}
}
Passo 3: Crie os Buckets S3
Abra o arquivo lib/text-to-speech-app-stack.ts
e comece a criar os buckets S3:
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,
});
}
}
Passo 4: Crie uma Tabela DynamoDB
Adicione as seguintes linhas para criar a tabela MetadataTable
no DynamoDB:
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
// dentro do construtor abaixo da criação do audioBucket
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,
});
Passo 5: Crie uma Fila SQS
Em seguida, crie a fila ProcessingQueue
SQS:
import * as sqs from 'aws-cdk-lib/aws-sqs';
// dentro do construtor abaixo da Metadatatable
const processingQueue = new sqs.Queue(this, 'ProcessingQueue', {
visibilityTimeout: cdk.Duration.seconds(300), // 5 minutos
});
Passo 6: Crie uma Notificação de Evento S3 com SQS como alvo
Como já temos nosso textBucket e processingQueue agora podemos criar uma Notificação de Evento S3 para enviar uma mensagem para nossa fila de processamento quando um arquivo é carregado no caminho /upload
.
//
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
// dentro do construtor abaixo da fila de processamento, adicione uma notificação de evento ao textBucket
textBucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SqsDestination(processingQueue), { prefix: 'upload/' });
Criamos a principal infraestrutura a ser utilizada em nossa solução, e o destaque aqui é que escrevemos isso em apenas 38 linhas de código TypeScript.
Você pode executar o comando cdk deploy
para implantar o estado atual de nossa Stack antes de adicionarmos a função como construtores a serem utilizados nesta pilha.
O CDK mostra o progresso do deploy diretamente no seu terminal:
E após este processo ter terminado, você pode ir ao seu console para verificar o status da nossa infraestrutura.
Agora podemos seguir em frente e criar a função lambda que realmente fará a mágica para nós.
Passo 7: Crie o Construct TextToSpeech
Nossa função lambda será criada como um novo construtor em nosso projeto CDK, para isso vamos criar uma nova pasta no /lib chamada TextToSpeech e dentro desta pasta vamos criar dois novos arquivos TextToSpeech.ts e TextToSpeech.lambda.ts, nossa estrutura de projeto será assim:
Agora podemos ir ao arquivo TextToSpeech.ts e criar nosso construtor lambda, aqui adicionaremos todas as configurações da função lambda:
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)
}
}
Este é nosso construtor base onde definiremos todos os recursos lambda, o destaque aqui são as ‘props’, onde declaramos todos os recursos necessários com os quais a lambda interagirá.
Step 8: Agora finalmente podemos criar o código da lambda
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
})
O tempo limite em nossos testes foi suficiente para usar esta API que tem um tempo de resposta acessível.
Passo 9: Conceder Permissões aos Recursos
Agora precisamos conceder as permissões necessárias aos nossos recursos.
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)
)
);
grantRead
e grantReadWriteData
. Seguindo isso, usamos a Declaração de Política IAM para adicionar uma política personalizada à nossa função Lambda, definindo especificamente a ação ‘synthesizeSpeech’.O arquivo completo pode ser encontrado aqui no github
Passo 10: Criar o Código da Função Lambda
Agora vá para o arquivo TextToSpeech.lambda.ts para definir nosso código lambda como a seguir:
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!'),
};
};
Passo 11: Utilize o construtor TextToSpeech
Agora, só precisamos importar nosso construtor recém-criado para o nosso arquivo de stack text-to-speech-app-stack.ts
.
import { TextToSpeech } from './TextToSpeech/TextToSpeech';
// Bellow the addEventNotification
new TextToSpeech(this, 'TextToSpeech', {
textBucket: textBucket,
audioBucket: audioBucket,
metadataTable: metadataTable,
processingQueue: processingQueue
});
Passo 12: Implantar a Aplicação
Finalmente, construa e implante a aplicação:
npm run build
cdk deploy
Isso irá construir sua aplicação e implantá-la em sua conta AWS.
Testando nosso Aplicativo
Uma vez que o processo de implantação esteja completo, acesse sua conta e localize o bucket de texto. Coloque um arquivo de texto no bucket. Após um curto período de tempo, você poderá visualizar seu arquivo de áudio no bucket de áudio.
Para visualizar informações detalhadas sobre cada arquivo processado e o número de caracteres usados para sintetizar os textos que você enviou, consulte a tabela de metadados.
Se preferir, você pode baixar diretamente os arquivos de áudio da plataforma S3. Alternativamente, você tem a opção de explorar o caminho e integrar uma API com o CDK. Eu recomendo muito essa opção, pois é simples de implementar.
Considerações Finais e Exploração Adicional
Depois de explorar essa solução de texto para fala, estou animado com o potencial para criar mais soluções usando CDK e TypeScript. TypeScript é particularmente útil porque é uma linguagem tipada, o que se alinha bem com o desenvolvimento de backend e frontend e agora também com o desenvolvimento de infraestrutura. O CDK também oferece a oportunidade de usar a mesma linguagem para o código de infraestrutura. Além disso, estou interessado em explorar o CDK em outras linguagens de programação. Na minha opinião, o CDK simplifica muitos dos desafios anteriores que enfrentamos com ferramentas como terraform ou cloudFormation, que o CDK utiliza nos bastidores.
Se você tiver alguma ideia ou quiser discutir mais, por favor, me avise. Também agradeço qualquer feedback. Obrigado por se juntar a mim nesta jornada, e fique ligado para possíveis novas postagens em minhas páginas.