Serverless and event-driven translation bot

2023-12-05
This post cover image
Voice provided by Amazon Polly

In a talk I recently gave at a conference I did some live coding on stage, in that session I created a translation service using AWS and Slack, where you could directly do translations from Slack using a slash command. You also got a audio file where Polly read back the translation for you.

The entire setup was serverless and didn't use much code, instead I used the SDK and Service integrations in StepFunctions (like I always tend to do), and EventBridge to create an event-driven architecture.

In this post I will explain the solution, and I how it was setup end to end. All the source code is available on GitHub as well.

Architecture

First of all, let us do an overview of the architecture and what patterns that I use, before we do a deep dive.

In this solution we will combine the best of two worlds from orchestration and choreography. We have four domain services that each is responsible for a certain task. They will emit domain events so we can orchestrate a Saga pattern. Where services will be invoked in different phases and in response to domain events. Each of the service consists of several steps choreographed by StepFunctions to run in a certain order.

Image showing saga orchestration and choreography.

If we now add some more details to the image above, and start laying out the services we use. We have our hook that Slack will invoke on our slash command this is implemented with API Gateway and Lambda. The translation service that is implemented with a StepFunction and Amazon Translate. The text to voice service, which is also is setup with a StepFunction and Amazon Polly. The final service is a service responsible communicating back to Slack with both the translated text but also the generated voice file.

The services are invoked and communicate in an event-driven way over EventBridge event-buses, both a custom and the default bus. The default bus relay messages from S3 when objects are created.

Image showing architecture overview.

With that short overview, let us dive deep into the different services, events, logic, and infrastructure.

Common infrastructure

In the common infrastructure we will create the custom EventBridge event-bus and we'll create a S3 bucket that we use as intermediate storage of translated text and generated voice.


AWSTemplateFormatVersion: "2010-09-09"
Description: Event-Driven Translation Common Infra
Parameters:
Application:
Type: String
Description: Name of owning application
Default: eventdriven-translation

Resources:
##########################################################################
# INFRASTRUCTURE
##########################################################################
TranslationBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketName: !Sub ${Application}-translation-bucket
NotificationConfiguration:
EventBridgeConfiguration:
EventBridgeEnabled: true
Tags:
- Key: Application
Value: !Ref Application

EventBridgeBus:
Type: AWS::Events::EventBus
Properties:
Name: !Sub ${Application}-eventbus

SlackBotSecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: Slack bot oauth token
Name: /slackbot
Tags:
- Key: Application
Value: !Ref Application

##########################################################################
# Outputs #
##########################################################################
Outputs:
TranslationBucket:
Description: Name of the bucket to store translations in
Value: !Ref TranslationBucket
Export:
Name: !Sub ${AWS::StackName}:TranslationBucket
EventBridgeBus:
Description: The EventBridge EventBus
Value: !Ref EventBridgeBus
Export:
Name: !Sub ${AWS::StackName}:EventBridgeBus
SlackBotSecret:
Description: The Slack Bot Secret
Value: !Ref SlackBotSecret
Export:
Name: !Sub ${AWS::StackName}:SlackBotSecret

With this common infrastructure created we can move on.

Slack Integration

Next let's create the Slack Application and create the API that the application will call. We'll also create the Notification service that will send messages back to out Slack channel.

Slash command hook API

This will create the API that Slack will send the slash commands to. We will create this using API Gateway with a Lambda function integration where we will parse the command, send a response to Slack, and post an event onto our custom event-bus that will be the start of our translations Saga. This is this small part of the architecture.

Image showing architecture overview.


AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Event-driven Slack Bot

Parameters:
Application:
Type: String
Description: Name of the application
CommonInfraStackName:
Type: String
Description: Name of the Common Infra Stack

Globals:
Function:
Runtime: python3.9
Timeout: 30
MemorySize: 1024

Resources:
##########################################################################
# WEBHOOK INFRASTRUCTURE #
##########################################################################

##########################################################################
# WebHook HTTP #
##########################################################################
SlackHookHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowMethods:
- GET
AllowOrigins:
- "*"
AllowHeaders:
- "*"

##########################################################################
# HTTP API Slackhook Lambdas #
##########################################################################
SlackhookFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/SlackhookLambda
Handler: slackhook.handler
Events:
SackhookPost:
Type: HttpApi
Properties:
Path: /slackhook
Method: post
ApiId: !Ref SlackHookHttpApi
Policies:
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Environment:
Variables:
EVENT_BUS_NAME:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus

##########################################################################
# Outputs #
##########################################################################
Outputs:
ApiEndpoint:
Description: HTTP API endpoint URL
Value: !Sub https://${SlackHookHttpApi}.execute-api.${AWS::Region}.amazonaws.com

In the Lambda function we'll decode the slash command, this is an url encoded, base64 encoded, key:value pair string. We create the event that we need, post that on our event-bud and then return a 200 OK with a message to Slack.


import json
import base64
from urllib import parse as urlparse
import boto3
import os
import re

def handler(event, context):

msg_map = dict(
urlparse.parse_qsl(base64.b64decode(str(event["body"])).decode("ascii"))
)
commandString = msg_map.get("command", "err")
text = msg_map.get("text", "err")

translateText = re.findall(r'"(.*?)"', text)[0]

text = text.replace(translateText, "")
text = text.replace('"', "")
index = text.find("to")
text = text.replace("to", "").strip()
languages = text.split(",")

languageArray = []
for language in languages:
language = language.strip()
languageArray.append(
{"Code": language},
)

commandEvent = {
"Languages": languageArray,
"Text": translateText,
"RequestId": event["requestContext"]["requestId"],
}

client = boto3.client("events")
response = client.put_events(
Entries=[
{
"Source": "Translation",
"DetailType": "TranslateText",
"Detail": json.dumps(commandEvent),
"EventBusName": os.environ["EVENT_BUS_NAME"],
},
]
)
return {"statusCode": 200, "body": f"Translating........"}

Create Slack command

Now that we have the hook API up and running we can create the actual slash command in Slack, navigate to Slack API. Click on Create New App to start creating a new Slack App.

Image showing create slack app.

I name my app "Translation", you can name it however you like, also associate it with your workspace.

Image showing create slack app step 2.

When the app is created select it in the drop down menu and navigate to "Slash Commands"

Image showing how to select slash command in the menu.

Here we create a new Slash Command.

Image showing create slash command.

I create the "/translate" command, we need the url for the API that we created previously, the value is in the Output section of the Cloudformation template, copy the value for ApiEndpoint and paste it in Request URL box. A short description of the command is not mandatory, but I still enter a very basic description. After creation the Slash Command should be visible in the menu.

Image showing that the new slash command is visible

Next we need to give our application some permissions. That is done from the OAuth and Permissions menu. Our app need "chat:write", "commands", and "files:write" add these under the Scope section.

Image showing the OAuth scopes

We are almost there now. To get the OAuth token we need, we first need to install the application to our workspace. Navigate to the top and click "Install to workspace".

Image showing the OAuth install to workspace

After a successful installation we should now have the OAuth token that we need.

Image showing the OAuth token

We need to copy this token and store it in the SecretsManager Secret that was created with the common infrastructure previous, so head over to the AWS Console and SecretsManager. Select the "/slackbot" secret and create a key/value pair with the key "OauthToken" and the value set to the token.

Image showing storing the OAuth token in secrets manager

Final step now is to navigate to your workspace, find the Translation app under Apps in the left pane, click it and select "Add this app to a channel" and select the channel of your choice.

Image showing adding the app to a channel

Translation

That was one long section on how to create and setup your Slack app. But with that out of the way we can now create the Translation service. This service looks like this.

Image showing the translation service

It will start on an event from a custom EventBridge event-bus, this will start a StepFunction state-machine. Amazon Translate will use Amazon Comprehend to detect the source language and translate it to the destination. The translated text will be stored in the S3 bucket, that we created in the common infrastructure, and finally post a event back to the event-bus to move to the next step in our saga pattern. We can actually translate to several languages at once, for this we use the Map state in the state-machine to run the translation logic over an array. The StepFunction state-machine looks like this.

Image showing the translation state-machine

I only use the SDK or Optimized integrations, no need for any code or Lambda functions for performing this task, less code to manage.

SAM Tamplate:

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Translate Text State Machine
Parameters:
Application:
Type: String
Description: Name of owning application
CommonInfraStackName:
Type: String
Description: Name of the Common Infra Stack

Resources:
##########################################################################
## TRANSLATE STATEMACHINE
##########################################################################
TranslateStateMachineStandard:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/translate-broken.asl.yaml
Tracing:
Enabled: true
DefinitionSubstitutions:
S3Bucket:
Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
EventBridgeBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Policies:
- Statement:
- Effect: Allow
Action:
- logs:*
Resource: "*"
- Statement:
- Effect: Allow
Action:
- translate:TranslateText
- comprehend:DetectDominantLanguage
Resource: "*"
- S3WritePolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Events:
StateChange:
Type: EventBridgeRule
Properties:
InputPath: $.detail
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Pattern:
source:
- Translation
detail-type:
- TranslateText
Type: STANDARD

StepFunction definition:

Comment: Translation State Machine
StartAt: Debug
States:
Debug:
Type: Pass
Next: Map
Map:
Type: Map
ItemProcessor:
ProcessorConfig:
Mode: INLINE
StartAt: Translate Text
States:
Translate Text:
Type: Task
Parameters:
SourceLanguageCode: auto
TargetLanguageCode.$: $.TargetLanguage
Text.$: $.Text
Resource: arn:aws:states:::aws-sdk:translate:translateText
ResultPath: $.Translation
Next: Store Translated Text
Store Translated Text:
Type: Task
Parameters:
Body.$: $.Translation.TranslatedText
Bucket: ${S3Bucket}
Key.$: States.Format('{}/{}/text.txt',$.RequestId, $.TargetLanguage)
Resource: arn:aws:states:::aws-sdk:s3:putObject
ResultPath: null
Next: Notify
Notify:
Type: Task
Resource: arn:aws:states:::events:putEvents
Parameters:
Entries:
- Source: Translation
DetailType: TextTranslated
Detail:
TextBucket: ${S3Bucket}
TextKey.$: States.Format('{}/{}/text.txt',$.RequestId, $.TargetLanguage)
Language.$: $.TargetLanguage
RequestId.$: $.RequestId
EventBusName: ${EventBridgeBusName}
End: true
End: true
ItemsPath: $.Languages
ItemSelector:
TargetLanguage.$: $$.Map.Item.Value.Code
RequestId.$: $.RequestId
Text.$: $.Text

Text to speech

Next part of the saga is the text to speech service, here we like to use Amazon Polly to read the translated text to us.

Image showing the voice service

This service will be invoked by the translated text being stored in the S3 bucket by the Translation service. This will invoke a StepFunction state-machine that will load the text and start a Polly speech synthesis task. The state-machine will poll and wait for the task to finish, complete or fail. The generated speech mp3 file will be copied to the same place as the translated text. Finally an event is posted onto a custom event-bus that will invoke the last part of our saga.

Image showing the voice state-machine

Once again I only use the SDK or Optimized integrations, no need for any code or Lambda functions for performing this task, less code to manage.

SAM template

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Generate Voice State Machine
Parameters:
Application:
Type: String
Description: Name of owning application
CommonInfraStackName:
Type: String
Description: Name of the Common Infra Stack

Resources:
##########################################################################
## VOICE STATEMACHINE
##########################################################################
VoiceStateMachineStandard:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/voice-broken.asl.yaml
Tracing:
Enabled: true
DefinitionSubstitutions:
S3Bucket:
Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
EventBridgeBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Policies:
- Statement:
- Effect: Allow
Action:
- logs:*
Resource: "*"
- Statement:
- Effect: Allow
Action:
- polly:StartSpeechSynthesisTask
- polly:GetSpeechSynthesisTask
Resource: "*"
- S3CrudPolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Events:
StateChange:
Type: EventBridgeRule
Properties:
EventBusName: default
InputPath: $.detail
Pattern:
source:
- aws.s3
detail-type:
- Object Created
detail:
bucket:
name:
- Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
object:
key:
- suffix: ".txt"
Type: STANDARD

StepFunction definition

Comment: Convert text to voice.
StartAt: Set Source Information
States:
Set Source Information:
Type: Pass
ResultPath: $
Parameters:
TargetBucket.$: $.bucket.name
Targetkey.$: States.Format('{}/{}/voice',States.ArrayGetItem(States.StringSplit($.object.key,'/'),0),States.ArrayGetItem(States.StringSplit($.object.key,'/'),1))
SourceBucket.$: $.bucket.name
SourceKey.$: $.object.key
Langaguge.$: States.Format('{}',States.ArrayGetItem(States.StringSplit($.object.key,'/'),1))
Next: Load Text
Load Text:
Type: Task
Next: Start Speech Synthesis
Parameters:
Bucket.$: $.SourceBucket
Key.$: $.SourceKey
Resource: arn:aws:states:::aws-sdk:s3:getObject
ResultPath: $.Text
ResultSelector:
Body.$: $.Body
Start Speech Synthesis:
Type: Task
Parameters:
Engine: neural
LanguageCode.$: $.Langaguge
OutputFormat: mp3
OutputS3BucketName.$: $.TargetBucket
OutputS3KeyPrefix.$: $.Targetkey
TextType: text
Text.$: $.Text.Body
VoiceId: Joanna
Resource: arn:aws:states:::aws-sdk:polly:startSpeechSynthesisTask
ResultPath: $.Voice
Next: Get Speech Synthesis Status
Get Speech Synthesis Status:
Type: Task
Parameters:
TaskId.$: $.Voice.SynthesisTask.TaskId
Resource: arn:aws:states:::aws-sdk:polly:getSpeechSynthesisTask
ResultPath: $.Voice
Next: Speech Synthesis Done?
Speech Synthesis Done?:
Type: Choice
Choices:
- Variable: $.Voice.SynthesisTask.TaskStatus
StringMatches: completed
Next: Update Voice Object
Comment: Completed!
- Variable: $.Voice.SynthesisTask.TaskStatus
StringMatches: failed
Next: Failed
Comment: Failed!
Default: Wait
Update Voice Object:
Type: Task
Next: Notify
ResultPath: null
Parameters:
Bucket.$: $.TargetBucket
CopySource.$: $.Voice.SynthesisTask.OutputUri
Key.$: States.Format('{}_{}.mp3',$.Targetkey,$.Voice.SynthesisTask.VoiceId)
Resource: arn:aws:states:::aws-sdk:s3:copyObject
Notify:
Type: Task
Resource: arn:aws:states:::events:putEvents
Next: Completed
Parameters:
Entries:
- Source: Translation
DetailType: VoiceGenerated
Detail:
VoiceBucket.$: $.TargetBucket
VoiceKey.$: States.Format('{}_{}.mp3',$.Targetkey,$.Voice.SynthesisTask.VoiceId)
Language.$: $.Langaguge
Voice.$: $.Voice.SynthesisTask.VoiceId
EventBusName: ${EventBridgeBusName}
Completed:
Type: Pass
End: true
Failed:
Type: Pass
End: true
Wait:
Type: Wait
Seconds: 10
Next: Get Speech Synthesis Status

Posting back to Slack

The final service involved in our saga is the notification service, that will post text and audio back to Slack. This service will be invoked by two different domain events, text translated, and audio generated. The state-machine need to handle both and uses a choice state to walk down different paths. In this state-machine we need to use a Lambda function to post to the Slack API. However, with the new HTTPS integration release at re:Invent 2023 we might be able to remove this as well.

Image showing the notification state-machine

SAM Template

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Event-driven Slack Bot Notification Service

Parameters:
Application:
Type: String
Description: Name of the application
CommonInfraStackName:
Type: String
Description: Name of the Common Infra Stack

Globals:
Function:
Runtime: python3.9
Timeout: 30
MemorySize: 1024

Resources:
##########################################################################
# LAMBDA FUNCTIONS #
##########################################################################
PostToChannelFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/SlackPostToChannel
Handler: postchannel.handler
Policies:
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn:
Fn::ImportValue: !Sub ${CommonInfraStackName}:SlackBotSecret
Environment:
Variables:
SLACK_CHANNEL: <your-slack-channel>
SLACK_BOT_TOKEN_ARN:
Fn::ImportValue: !Sub ${CommonInfraStackName}:SlackBotSecret

UploadAudioToChannelFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/UploadAudioToChannel
Handler: uploadchannel.handler
Policies:
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn:
Fn::ImportValue: !Sub ${CommonInfraStackName}:SlackBotSecret
- S3ReadPolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
Environment:
Variables:
SLACK_CHANNEL: <your-slack-channel>
SLACK_BOT_TOKEN_ARN:
Fn::ImportValue: !Sub ${CommonInfraStackName}:SlackBotSecret

##########################################################################
# STEP FUNCTION #
##########################################################################
NotificationLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "${Application}/notificationstatemachine"
RetentionInDays: 5

SlackNotificationStateMachineStandard:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: statemachine/statemachine.asl.yaml
DefinitionSubstitutions:
EventBridgeName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
PostToChannelFunctionArn: !GetAtt PostToChannelFunction.Arn
UploadAudioToChannelFunctionArn: !GetAtt UploadAudioToChannelFunction.Arn
Events:
SlackNotification:
Type: EventBridgeRule
Properties:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
Pattern:
source:
- Translation
detail-type:
- TextTranslated
- VoiceGenerated
RetryPolicy:
MaximumEventAgeInSeconds: 300
MaximumRetryAttempts: 2
Policies:
- Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "cloudwatch:*"
- "logs:*"
Resource: "*"
- EventBridgePutEventsPolicy:
EventBusName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:EventBridgeBus
- LambdaInvokePolicy:
FunctionName: !Ref PostToChannelFunction
- LambdaInvokePolicy:
FunctionName: !Ref UploadAudioToChannelFunction
- S3ReadPolicy:
BucketName:
Fn::ImportValue: !Sub ${CommonInfraStackName}:TranslationBucket
Tracing:
Enabled: true
Logging:
Destinations:
- CloudWatchLogsLogGroup:
LogGroupArn: !GetAtt NotificationLogGroup.Arn
IncludeExecutionData: true
Level: ALL
Type: STANDARD

StepFunction definition

Comment: Translate App Slack Notification service
StartAt: Debug
States:
Debug:
Type: Pass
Next: Event Type ?
Event Type ?:
Type: Choice
Choices:
- Variable: $.detail-type
StringEquals: TextTranslated
Next: Text Translated
- Variable: $.detail-type
StringEquals: VoiceGenerated
Next: Voice Generated
Default: Unknown Event Type
Text Translated:
Type: Pass
Next: GetObject
ResultPath: $
Parameters:
TextBucket.$: $.detail.TextBucket
TextKey.$: $.detail.TextKey
Language.$: $.detail.Language
RequestId.$: $.detail.RequestId
GetObject:
Type: Task
Parameters:
Bucket.$: $.TextBucket
Key.$: $.TextKey
Resource: arn:aws:states:::aws-sdk:s3:getObject
ResultSelector:
Body.$: $.Body
ResultPath: $.Text
Next: Post Text To Channel
Post Text To Channel:
Type: Task
Resource: arn:aws:states:::lambda:invoke
OutputPath: $.Payload
Parameters:
Payload.$: $
FunctionName: ${PostToChannelFunctionArn}
Retry:
- ErrorEquals:
- Lambda.ServiceException
- Lambda.AWSLambdaException
- Lambda.SdkClientException
- Lambda.TooManyRequestsException
IntervalSeconds: 1
MaxAttempts: 3
BackoffRate: 2
Next: Done
Done:
Type: Succeed
Voice Generated:
Type: Pass
ResultPath: $
Parameters:
VoiceBucket.$: $.detail.VoiceBucket
VoiceKey.$: $.detail.VoiceKey
Language.$: $.detail.Language
Voice.$: $.detail.Voice
Next: Upload Audio To Channel
Upload Audio To Channel:
Type: Task
Resource: arn:aws:states:::lambda:invoke
OutputPath: $.Payload
Parameters:
Payload.$: $
FunctionName: ${UploadAudioToChannelFunctionArn}
Retry:
- ErrorEquals:
- Lambda.ServiceException
- Lambda.AWSLambdaException
- Lambda.SdkClientException
- Lambda.TooManyRequestsException
IntervalSeconds: 1
MaxAttempts: 3
BackoffRate: 2
Next: Done
Unknown Event Type:
Type: Fail

Post translated text

import json
import os
import boto3
from symbol import parameters
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

SLACK_CHANNEL = os.environ["SLACK_CHANNEL"]

def handler(event, context):
set_bot_token()

text = f"{event['Language']}:\n{event['Text']['Body']}"

client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
client.chat_postMessage(channel="#" + SLACK_CHANNEL, text=text)

return {"statusCode": 200, "body": "Hello there"}


def set_bot_token():
os.environ["SLACK_BOT_TOKEN"] = get_secret()


def get_secret():
session = boto3.session.Session()
client = session.client(service_name="secretsmanager")

try:
secretValueResponse = client.get_secret_value(
SecretId=os.environ["SLACK_BOT_TOKEN_ARN"]
)
except ClientError as e:
raise e

secret = json.loads(secretValueResponse["SecretString"])["OauthToken"]
return secret

Upload audio file

import json
import os
import boto3
from symbol import parameters
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

SLACK_CHANNEL = os.environ["SLACK_CHANNEL"]

def handler(event, context):
set_bot_token()

path = download_audio_file(
event["VoiceBucket"], event["VoiceKey"], event["Voice"], event["Language"]
)
upload_audio_file(event["Language"], path)

return {"statusCode": 200, "body": "Hello there"}


def download_audio_file(bucket, key, voice, language):
s3 = boto3.client("s3")
path = f"/tmp/{language}_{voice}.mp3"
s3.download_file(bucket, key, path)
return path

def upload_audio_file(language, path):
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
client.files_upload(
channels="#" + SLACK_CHANNEL,
initial_comment=f"Polly Voiced Translation for: {language}",
file=path,
)

return {"statusCode": 200, "body": "Hello there"}

def set_bot_token():
os.environ["SLACK_BOT_TOKEN"] = get_secret()

def get_secret():
session = boto3.session.Session()
client = session.client(service_name="secretsmanager")

try:
secretValueResponse = client.get_secret_value(
SecretId=os.environ["SLACK_BOT_TOKEN_ARN"]
)
except ClientError as e:
raise e
secret = json.loads(secretValueResponse["SecretString"])["OauthToken"]
return secret

Test it

To test the solution we send a slash command with the pattern /translate "text to translate" language_code_1,language_code_2,language_code_n

Image showing the slash command sent in slack

Image showing the notification state-machine

Final Words

In the era of Generative AI it was interesting to build a solution using the more traditional AI services that has been around for several years. The performance on these are really good and the translations and voice files are created very quickly. Building this in a serverless and event-driven way creates a cost effective solution as alway. There are improvements and extensions that can be done to the solution. Stay tuned as I make this changes and update this blog. Also this solution will be powering my new feature turning this blog into multi language.

Don't forget to follow me on LinkedIn and X for more content, and read rest of my Blogs