AWSTemplateFormatVersion: '2010-09-09' Description: > Amazon Connect IVR Recording Solution: - Create the basic foundation for streaming customer audio from Amazon Connect by deploying: - S3 Bucket for audio files, and the sample contact flow - S3 Bucket for storing combined audio files - Dynamo DB tables: transcriptSegments, transcriptSegmentsToCustomer, and contactDetails - A Lambda that will store the initial contact details in dynamo and trigger Java Lambda to process KVS Stream by passing the stream details to the function - A Java Lambda to consume KVS and stream it to Amazon Transcribe, store the segments in DDB and upload the raw audio to S3 - A Node.js Lambda triggered by S3 once WAV file is uploaded to store the concatenated transcript segments in the contact details table along with the S3 location of the audio file - A Lambda Layer containg FFMPEG - A Lambda function to combine the two audio files FROM_CUSTOMER and TO_CUSTOMER into a single file and place in S3 bucket Mappings: FunctionMap: Configuration: SolutionID: "SOXXXX" Send: AnonymousUsage: Data: "Yes" Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: Amazon DynamoDB Configuration Parameters: - transcriptSegmentsTable - transcriptSegmentsToCustomerTable - contactDetailsTable - Label: default: Existing configuration Parameters: - existingS3BucketName - existingS3Path - Label: default: Amazon S3 Configuration Parameters: - s3BucketName - s3CombinedBucketName - audioFilePrefix - rawAudioUploadPrefix ParameterLabels: existingS3BucketName: default: Existing S3 Bucket existingS3Path: default: Existing S3 path s3BucketName: default: Recorded audio files from the customer and ivr audioFilePrefix: default: Audio File Prefix rawAudioUploadPrefix: default: Test Mono Audio Prefix s3CombinedBucketName: default: Combined recorded audio files Bucket Name transcriptSegmentsTable: default: Transcript Table Name for audio from the customer transcriptSegmentsToCustomerTable: default: Transcript Table Name for audio to the customer contactDetailsTable: default: Contacts Table Name Parameters: existingS3BucketName: Type: String Default: existingS3Bucket Description: The name of the S3 bucket that contains the zipped lambda files existingS3Path: Type: String Default: deployment/ Description: The path to the zipped lambda files in the existingS3BucketName s3BucketName: Type: String Default: "new-audio-bucket-name" Description: > Enter the (globally unique) name you would like to use for the Amazon S3 bucket where we will store the audio files, and the sample contact flow. This template will fail to deploy if the bucket name you chose is currently in use. AllowedPattern: '(?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)' audioFilePrefix: Type: String Default: recordings/ Description: The Amazon S3 prefix where the audio files will be saved (must end in "/") rawAudioUploadPrefix: Type: String Default: audio-file-input/ Description: > The Amazon S3 prefix where raw/wav (audio/L16; mono; 8 kHz) audio recordings may be uploaded in the event you would like process an audio file vs making a phone call and streaming from KVS. Mainly for testing, or for realtime transcription of audio files. This will only work with single channel files (mono). s3CombinedBucketName: Type: String Default: "new-combined-audio-bucket-name" Description: > Enter the (globally unique) name you would like to use for the Amazon S3 bucket where we will store the combined audio files. This template will fail to deploy if the bucket name you chose is currently in use. AllowedPattern: '(?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)' transcriptSegmentsTable: Type: String Default: contactTranscriptSegments Description: The name of the DynamoDB Table where segments (utterances) from the customer for the caller transcript will be saved (Ensure you do not have a table with this name already). transcriptSegmentsToCustomerTable: Type: String Default: contactTranscriptSegmentsToCustomer Description: The name of the DynamoDB Table where segments (utterances) to the customer for the caller transcript will be saved (Ensure you do not have a table with this name already). contactDetailsTable: Type: String Default: contactDetails Description: The name of the DynamoDB Table where contact details will be written (Ensure you do not have a table with this name already). Outputs: createS3BucketOP: Description: Bucket contains individual channel call recordings Value: !Sub 'https://console.aws.amazon.com/s3/home?region=${AWS::Region}&bucket=${s3BucketName}' createCombinedS3BucketOP: Description: Bucket contains the combined call recordings Value: !Sub 'https://console.aws.amazon.com/s3/home?region=${AWS::Region}&bucket=${s3CombinedBucketName}' Resources: allowConnectToKvsConsumerTriggerLambda: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref kvsConsumerTrigger Action: 'lambda:InvokeFunction' Principal: connect.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' allowS3toProcessContactSummaryLambda: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref processContactSummary Action: 'lambda:InvokeFunction' Principal: s3.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' createS3Bucket: Type: 'AWS::S3::Bucket' Properties: BucketName: !Ref s3BucketName NotificationConfiguration: LambdaConfigurations: - Function: !GetAtt processContactSummary.Arn Event: "s3:ObjectCreated:*" Filter: S3Key: Rules: - Name: suffix Value: wav PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 CorsConfiguration: CorsRules: - AllowedOrigins: - '*' AllowedHeaders: - '*' AllowedMethods: - PUT - HEAD MaxAge: '3000' createCombinedS3Bucket: Type: 'AWS::S3::Bucket' Properties: BucketName: !Ref s3CombinedBucketName PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 CorsConfiguration: CorsRules: - AllowedOrigins: - '*' AllowedHeaders: - '*' AllowedMethods: - PUT - HEAD MaxAge: '3000' transcriptSegmentsDDBTable: Type: AWS::DynamoDB::Table Properties: TableName: !Ref transcriptSegmentsTable AttributeDefinitions: - AttributeName: "ContactId" AttributeType: "S" - AttributeName: "StartTime" AttributeType: "N" KeySchema: - AttributeName: "ContactId" KeyType: "HASH" - AttributeName: "StartTime" KeyType: "RANGE" # assuming 5 concurrent calls ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: True SSESpecification: SSEEnabled: True TimeToLiveSpecification: AttributeName: "ExpiresAfter" Enabled: True StreamSpecification: StreamViewType: NEW_AND_OLD_IMAGES transcriptSegmentsToCustomerDDBTable: Type: AWS::DynamoDB::Table Properties: TableName: !Ref transcriptSegmentsToCustomerTable AttributeDefinitions: - AttributeName: "ContactId" AttributeType: "S" - AttributeName: "StartTime" AttributeType: "N" KeySchema: - AttributeName: "ContactId" KeyType: "HASH" - AttributeName: "StartTime" KeyType: "RANGE" # assuming 5 concurrent calls ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: True SSESpecification: SSEEnabled: True TimeToLiveSpecification: AttributeName: "ExpiresAfter" Enabled: True StreamSpecification: StreamViewType: NEW_AND_OLD_IMAGES contactDetailsDDBTable: Type: AWS::DynamoDB::Table Properties: TableName: !Ref contactDetailsTable AttributeDefinitions: - AttributeName: "contactId" AttributeType: "S" KeySchema: - AttributeName: "contactId" KeyType: "HASH" # assuming 5 concurrent calls ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: True SSESpecification: SSEEnabled: True KvsTranscribeRole: Type: "AWS::IAM::Role" Metadata: cfn_nag: rules_to_suppress: - id: F3 reason: transcribe:* do not support resource-level permissions and kinesisvideo streams are dynamically created and therefore cannot be specificed directly - id: W11 reason: transcribe:* do not support resource-level permissions and kinesisvideo streams are dynamically created and therefore cannot be specificed directly Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: kvs-streaming-transcribe-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" - Effect: "Allow" Action: - "dynamodb:Query" - "dynamodb:Scan" - "dynamodb:PutItem" - "dynamodb:UpdateItem" - "dynamodb:GetRecords" - "dynamodb:GetShardIterator" - "dynamodb:DescribeStream" - "dynamodb:ListStreams" Resource: - !Sub ${transcriptSegmentsDDBTable.Arn} - !Sub ${transcriptSegmentsToCustomerDDBTable.Arn} - Effect: "Allow" Action: - "s3:PutObject" - "s3:GetObject" - "s3:PutObjectAcl" Resource: - !Sub ${createS3Bucket.Arn}/* - Effect: "Allow" Action: - "transcribe:DeleteTranscriptionJob" - "transcribe:GetTranscriptionJob" - "transcribe:GetVocabulary" - "transcribe:ListTranscriptionJobs" - "transcribe:ListVocabularies" - "transcribe:StartStreamTranscription" - "transcribe:StartTranscriptionJob" Resource: "*" - Effect: "Allow" Action: - "kinesisvideo:Describe*" - "kinesisvideo:Get*" - "kinesisvideo:List*" Resource: "*" KvsTriggerRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: kvs-streaming-trigger-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" - Effect: "Allow" Action: - "dynamodb:Query" - "dynamodb:Scan" - "dynamodb:PutItem" - "dynamodb:UpdateItem" - "dynamodb:GetRecords" - "dynamodb:GetShardIterator" - "dynamodb:DescribeStream" - "dynamodb:ListStreams" Resource: - !Sub ${contactDetailsDDBTable.Arn} - Effect: "Allow" Action: - "lambda:InvokeFunction" - "lambda:InvokeAsync" Resource: - !GetAtt kvsTranscriber.Arn ProcessContactRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: process-contact-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" - Effect: "Allow" Action: - "dynamodb:Query" Resource: - !Sub ${transcriptSegmentsDDBTable.Arn} - !Sub ${transcriptSegmentsToCustomerDDBTable.Arn} - Effect: "Allow" Action: - "dynamodb:UpdateItem" Resource: - !Sub ${contactDetailsDDBTable.Arn} - Effect: "Allow" Action: - "lambda:InvokeFunction" Resource: - !GetAtt overlayaudio.Arn - Effect: "Allow" Action: - "s3:ListBucket" Resource: - !Sub 'arn:aws:s3:::${s3BucketName}' OverlayAudioRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: overlay-audio-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" - Effect: "Allow" Action: - "s3:PutObject" - "s3:GetObject" - "s3:PutObjectAcl" Resource: - !Sub ${createCombinedS3Bucket.Arn}/* - !Sub 'arn:aws:s3:::${s3BucketName}/*' - Effect: "Allow" Action: - "lambda:ListLayers" - "lambda:ListLayerVersions" Resource: - !Sub ${connectaudioutils}/* - Effect: "Allow" Action: - "dynamodb:Query" - "dynamodb:UpdateItem" - "dynamodb:GetItem" Resource: - !Sub ${contactDetailsDDBTable.Arn} kvsTranscriber: Type: "AWS::Lambda::Function" Properties: Description: > Process audio from Kinesis Video Stream and use Amazon Transcribe to get text for the caller audio. Will be invoked by the kvsConsumerTrigger Lambda, writes results to the transcript DynamoDB tables, and uploads the audio file to S3. Handler: "com.amazonaws.kvstranscribestreaming.KVSTranscribeStreamingLambda::handleRequest" Role: !GetAtt KvsTranscribeRole.Arn Runtime: java8 MemorySize: 512 # maximum timeout is 15 minutes today Timeout: 900 Environment: Variables: # JAVA_TOOL_OPTIONS: "-Djavax.net.ssl.trustStore=lib/InternalAndExternalTrustStore.jks -Djavax.net.ssl.trustStorePassword=amazon" APP_REGION: !Ref "AWS::Region" TRANSCRIBE_REGION: !Ref "AWS::Region" RECORDINGS_BUCKET_NAME: !Ref s3BucketName RECORDINGS_KEY_PREFIX: !Ref audioFilePrefix INPUT_KEY_PREFIX: !Ref rawAudioUploadPrefix TABLE_CALLER_TRANSCRIPT: !Ref transcriptSegmentsTable TABLE_CALLER_TRANSCRIPT_TO_CUSTOMER: !Ref transcriptSegmentsToCustomerTable RECORDINGS_PUBLIC_READ_ACL: "FALSE" CONSOLE_LOG_TRANSCRIPT_FLAG: "TRUE" LOGGING_LEVEL: "FINE" SAVE_PARTIAL_TRANSCRIPTS: "TRUE" START_SELECTOR_TYPE: "FRAGMENT_NUMBER" SEND_ANONYMOUS_DATA: !FindInMap [ "Send", "AnonymousUsage", "Data"] Code: S3Bucket: !Ref existingS3BucketName S3Key: !Join ["", [!Ref existingS3Path, 'kvs_transcribe_streaming_lambda.zip']] kvsConsumerTrigger: Type: "AWS::Lambda::Function" Properties: Description: > AWS Lambda Function to start (asynchronous) streaming transcription; it is expected to be called by the Amazon Connect Contact Flow. Handler: "kvs_trigger.handler" Role: !GetAtt KvsTriggerRole.Arn Runtime: "nodejs12.x" MemorySize: 128 Timeout: 30 Environment: Variables: transcriptionFunction: !Ref kvsTranscriber table_name: !Ref contactDetailsDDBTable Code: S3Bucket: !Ref existingS3BucketName S3Key: !Join ["", [!Ref existingS3Path, 'kvs_trigger.zip']] connectaudioutils: Type: AWS::Lambda::LayerVersion Properties: CompatibleRuntimes: - python3.8 Content: S3Bucket: !Ref existingS3BucketName S3Key: !Join ["", [!Ref existingS3Path, 'layer.zip']] Description: Layer providing ffmpeg library to audio utils lambda LayerName: connectaudioutils overlayaudio: Type: "AWS::Lambda::Function" Properties: Description: > AWS Lambda Function that will merge two audio files from S3 bucket and put them in a different S3 Bucket Handler: "overlay_audio.lambda_handler" Role: !GetAtt OverlayAudioRole.Arn Runtime: "python3.8" Layers: - !Ref connectaudioutils MemorySize: 256 Timeout: 120 Environment: Variables: contactDetailsTableName: !Ref contactDetailsTable Code: S3Bucket: !Ref existingS3BucketName S3Key: !Join ["", [!Ref existingS3Path, 'overlay_audio.zip']] DependsOn: connectaudioutils processContactSummary: Type: "AWS::Lambda::Function" Properties: Description: > AWS Lambda Function that will be triggered when the wav call recording file is placed in S3. This function will collect all the transcript segments, and the audio file location and update the contact db. Handler: "process_contact.handler" Role: !GetAtt ProcessContactRole.Arn Runtime: "nodejs12.x" MemorySize: 256 Timeout: 120 Environment: Variables: contact_table_name: !Ref contactDetailsTable transcript_seg_table_name: !Ref transcriptSegmentsTable transcript_seg_to_customer_table_name: !Ref transcriptSegmentsToCustomerTable combined_audio_bucket: !Ref createCombinedS3Bucket merge_audio_lambda: !Ref overlayaudio METRICS: true Code: S3Bucket: !Ref existingS3BucketName S3Key: !Join ["", [!Ref existingS3Path, 'process_contact.zip']]