AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Amazon Transcribe Live Call Analytics with Agent Assist - Amazon Chime SDK Call Analytics w/Amazon Chime SDK Voice Connector Parameters: LCAStackName: Type: String Description: Name of the LCA stack to prepend to resources. EnableVoiceToneAnalysis: Type: String Default: true AllowedValues: - true - false Description: > Set to true to enable Chime voice tone analysis. This is only used if Chime Call Analytics is enabled. AudioFilePrefix: Type: String Default: lca-audio-recordings/ Description: >- The Amazon S3 prefix where the merged output audio files will be saved (must end in "/") CallAnalyticsPrefix: Type: String Default: lca-call-analytics/ Description: The Amazon S3 prefix where the post-call analytics files will be saved, when using analytics api mode (must end in "/") KinesisDataStreamName: Type: String Description: >- Name of Kinesis Data Stream to publish events to KinesisDataStreamArn: Type: String Description: >- Arn of Kinesis Data Stream to publish events to S3BucketName: Type: String Description: >- S3 Bucket name for recordings TranscribeApiMode: Type: String Default: analytics AllowedValues: - standard - analytics Description: Set the default operational mode for Transcribe IsPartialTranscriptEnabled: Type: String Default: 'true' Description: >- Enable partial transcripts to receive low latency evolving transcriptions for each conversation turn. Set to false to process only the final version of each conversation turn. AllowedValues: - 'true' - 'false' IsContentRedactionEnabled: Type: String Default: 'false' Description: >- Enable content redaction from Amazon Transcribe transcription output. This is only used when the 'en-US' language is selected in the TranscribeLanguageCode parameter. AllowedValues: - 'true' - 'false' TranscribeContentRedactionType: Type: String Default: PII Description: >- Type of content redaction from Amazon Transcribe transcription output AllowedValues: - PII TranscribeLanguageCode: Type: String Description: >- Language code to be used for Amazon Transcribe Default: en-US AllowedValues: - en-US - es-US - en-GB - fr-CA - fr-FR - en-AU - it-IT - de-DE - pt-BR - ja-JP - ko-KR - zh-CN TranscribePiiEntityTypes: Type: String # yamllint disable rule:line-length Default: BANK_ACCOUNT_NUMBER,BANK_ROUTING,CREDIT_DEBIT_NUMBER,CREDIT_DEBIT_CVV,CREDIT_DEBIT_EXPIRY,PIN,EMAIL,ADDRESS,NAME,PHONE,SSN # yamllint enable rule:line-length Description: >- Select the PII entity types you want to identify or redact. Remove the values that you don't want to redact from the default. DO NOT ADD CUSTOM VALUES HERE. CustomVocabularyName: Type: String Default: '' Description: >- The name of the vocabulary to use when processing the transcription job. Leave blank if no custom vocabulary to be used. If yes, the custom vocabulary must pre-exist in your account. CustomLanguageModelName: Type: String Default: '' Description: >- The name of the custom language model to use when processing the transcription job. Leave blank if no custom language model is to be used. If specified, the custom language model must pre-exist in your account, match the Language Code selected above, and use the 'Narrow Band' base model. SiprecLambdaHookFunctionArn: Default: '' Type: String AllowedPattern: '^(|arn:aws:lambda:.*)$' Description: > (Optional) Used only when CallAudioSource is set to 'Chime Voice Connector (SIPREC)'. If present, the specified Lambda function is invoked at the start of each call. The call start event from Amazon Chime SDK Voice Connector (containing SIPREC headers) is provided as input. The function must return a True/False flag to indicate if the call should be processed or ignored, a mapped CallId, an AgentId, and may be extended to support additional features in future. VoiceConnectorId: Type: String Default: '' Description: >- Voice connector Id for setting up EventBridge Rule to restrict events to specific Amazon Chime SDK Voice Connector. Boto3LayerArn: Type: String Description: Arn of the Boto3 Lambda Layer that contains Amazon Chime SDK Call Analytics Resources: ########################################################################## # NodeJS Transcriber Lambda Layer ########################################################################## TranscriberLambdaLayer: Type: AWS::Serverless::LayerVersion Properties: CompatibleRuntimes: - nodejs14.x - nodejs16.x - nodejs18.x Description: > This is a layer with shared nodejs libraries for the LCA call transcriber and call analytics initialization Lambdas. ContentUri: ../lambda_layers/node_transcriber_layer/transcriber-layer.zip ########################################################################## # Media Pipeline configuration ########################################################################## ChimeCallAnalyticsResourceAccessRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - mediapipelines.chime.amazonaws.com Action: - sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Sub ${AWS::AccountId} ArnLike: aws:SourceArn: !Sub arn:aws:chime:*:${AWS::AccountId}:* Path: / Policies: - PolicyName: chime-analytics-passrole PolicyDocument: Version: 2012-10-17 Statement: - Action: - iam:PassRole Effect: Allow Resource: - !GetAtt TcaDataAccessRole.Arn - PolicyName: chime-analytics-kds PolicyDocument: Version: 2012-10-17 Statement: - Action: - kinesis:PutRecord Effect: Allow Resource: - !Ref KinesisDataStreamArn - Action: - kms:GenerateDataKey Effect: Allow Resource: - !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/* Condition: StringLike: aws:ResourceTag/AWSServiceName: ChimeSDK - PolicyName: chime-analytics-kvs-transcribe PolicyDocument: Version: 2012-10-17 Statement: - Action: - transcribe:StartCallAnalyticsStreamTranscription - transcribe:StartTranscriptionJob Effect: Allow Resource: '*' - Action: - kinesisvideo:GetDataEndpoint - kinesisvideo:GetMedia Effect: Allow Resource: - !Sub arn:${AWS::Partition}:kinesisvideo:${AWS::Region}:${AWS::AccountId}:stream/Chime* - Action: - kinesisvideo:GetDataEndpoint - kinesisvideo:GetMedia Effect: Allow Resource: - !Sub arn:${AWS::Partition}:kinesisvideo:${AWS::Region}:${AWS::AccountId}:stream/* Condition: StringLike: aws:ResourceTag/AWSServiceName: ChimeSDK - Action: - kms:Decrypt Effect: Allow Resource: - !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/* Condition: StringLike: aws:ResourceTag/AWSServiceName: ChimeSDK - Action: - lambda:InvokeFunction - lambda:GetPolicy Effect: Allow Resource: - !GetAtt VoiceToneLambda.Arn ########################################################################## # Custom resource to apply config to VoiceConnector # Increment CustomResourceVersion when making changes to CreateMediaPipelineConfigForVCFunction ########################################################################## CreateMediaPipelineConfigForVC: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: Fn::GetAtt: - CreateMediaPipelineConfigForVCFunction - Arn VoiceConnectorId: !Ref VoiceConnectorId EnableVoiceToneAnalysis: !Ref EnableVoiceToneAnalysis KinesisStreamArn: !Ref KinesisDataStreamArn ResourceAccessRoleArn: !GetAtt ChimeCallAnalyticsResourceAccessRole.Arn LambdaSinkArn: !GetAtt VoiceToneLambda.Arn StackName: !Ref LCAStackName TranscribeApiMode: !Ref TranscribeApiMode OutputBucket: !Ref S3BucketName RawFilePrefix: 'lca-audio-raw/' RecordingFilePrefix: !Ref AudioFilePrefix CallAnalyticsFilePrefix: !Ref CallAnalyticsPrefix TcaDataAccessRoleArn: !GetAtt TcaDataAccessRole.Arn PostCallContentRedactionOutput: 'redacted' SavePartialTranscripts: !Ref IsPartialTranscriptEnabled IsContentRedactionEnabled: !If - ShouldEnableContentRedaction - 'true' - 'false' TranscribeLanguageCode: !Ref TranscribeLanguageCode ContentRedactionType: !Ref TranscribeContentRedactionType PiiEntityTypes: !Ref TranscribePiiEntityTypes CustomVocabularyName: !Ref CustomVocabularyName CustomLanguageModelName: !Ref CustomLanguageModelName CustomResourceVersion: '1.1' CreateMediaPipelineConfigForVCFunction: Type: AWS::Serverless::Function Properties: Layers: - !Ref Boto3LayerArn Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - chime:DeleteVoiceConnectorStreamingConfiguration - chime:GetVoiceConnectorStreamingConfiguration - chime:PutVoiceConnectorStreamingConfiguration - chime:CreateMediaInsightsPipelineConfiguration - chime:DeleteMediaInsightsPipelineConfiguration - chime:UpdateMediaInsightsPipelineConfiguration - chime:ListMediaInsightsPipelineConfigurations - chime:GetMediaInsightsPipelineConfiguration Resource: !Sub arn:${AWS::Partition}:chime:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: chime:ListVoiceConnectors Resource: !Sub arn:${AWS::Partition}:chime:${AWS::Region}:${AWS::AccountId}:vc/* - Effect: Allow Action: kinesis:DescribeStream Resource: !Ref KinesisDataStreamArn - Effect: Allow Action: iam:PassRole Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:* Handler: index.lambda_handler Runtime: python3.8 Timeout: 300 InlineCode: | import boto3 import json import cfnresponse import uuid import traceback responseData = {} voiceClient = boto3.client('chime-sdk-voice') mediaPipelineClient = boto3.client('chime-sdk-media-pipelines') cloudformation = boto3.resource("cloudformation") def is_valid_uuid(value): try: uuid.UUID(str(value)) return True except ValueError: return False def delete_pipeline_config(event): id = event.get('PhysicalResourceId','') try: transcribePipelineConfigName = event['ResourceProperties'].get('StackName', '') + '-ts-' + id mediaPipelineClient.delete_media_insights_pipeline_configuration(Identifier=transcribePipelineConfigName) except Exception as e: error = f'Error deleting transcribe media insight pipeline config: {e}.' print(error) try: voiceAnalyticsPipelineConfigName = event['ResourceProperties'].get('StackName', '') + '-' + id mediaPipelineClient.delete_media_insights_pipeline_configuration(Identifier=voiceAnalyticsPipelineConfigName) except Exception as e: error = f'Error deleting voice analytics media insight pipeline config: {e}.' print(error) return {'PhysicalResourceId': id} def generate_voice_analytics_config(event, pipelineConfigName, resourceAccessRoleArn ): elements = [] # configure kds kdsArn = event['ResourceProperties'].get('KinesisStreamArn', '') kds = { "Type": "KinesisDataStreamSink", "KinesisDataStreamSinkConfiguration": { "InsightsTarget": kdsArn } } elements.append(kds) # configure lambda sink lambdaSinkArn = event['ResourceProperties'].get('LambdaSinkArn', '') lambdaSink = { "Type": "LambdaFunctionSink", "LambdaFunctionSinkConfiguration": { "InsightsTarget": lambdaSinkArn } } elements.append(lambdaSink) # configure voice analytics enableVoiceToneAnalysis = event['ResourceProperties'].get('EnableVoiceToneAnalysis', '') enableSpeakerSearch = event['ResourceProperties'].get('EnableSpeakerSearch', '') voiceAnalytics = { "Type": "VoiceAnalyticsProcessor", "VoiceAnalyticsProcessorConfiguration": { "VoiceToneAnalysisStatus": 'Enabled' if enableVoiceToneAnalysis == 'true' else 'Disabled', "SpeakerSearchStatus": 'Enabled' if enableSpeakerSearch == 'true' else 'Disabled' } } elements.append(voiceAnalytics) print("Voice analytics configuration") print(json.dumps(elements)) return elements def generate_transcribe_config(event, pipelineConfigName, resourceAccessRoleArn ): elements = [] # configure kds kdsArn = event['ResourceProperties'].get('KinesisStreamArn', '') kds = { "Type": "KinesisDataStreamSink", "KinesisDataStreamSinkConfiguration": { "InsightsTarget": kdsArn } } elements.append(kds) # configure transcribe transcribeApiMode = event['ResourceProperties'].get('TranscribeApiMode', '') transcribeLanguageCode = event['ResourceProperties'].get('TranscribeLanguageCode', '') callAnalyticsFilePrefix = event['ResourceProperties'].get('CallAnalyticsFilePrefix', '') contentRedactionType = event['ResourceProperties'].get('ContentRedactionType', '') customLanguageModelName = event['ResourceProperties'].get('CustomLanguageModelName', '') customVocabularyName = event['ResourceProperties'].get('CustomVocabularyName', '') isContentRedactionEnabled = event['ResourceProperties'].get('IsContentRedactionEnabled', '') outputBucket = event['ResourceProperties'].get('OutputBucket', '') piiEntityTypes = event['ResourceProperties'].get('PiiEntityTypes', '') postCallContentRedactionOutput = event['ResourceProperties'].get('PostCallContentRedactionOutput', '') rawFilePrefix = event['ResourceProperties'].get('RawFilePrefix', '') recordingFilePrefix = event['ResourceProperties'].get('RecordingFilePrefix', '') tcaDataAccessRoleArn = event['ResourceProperties'].get('TcaDataAccessRoleArn', '') outputLocation = "s3://%s/%s"%(outputBucket,callAnalyticsFilePrefix) if transcribeApiMode == 'analytics': transcribeConfig = "AmazonTranscribeCallAnalyticsProcessorConfiguration" transcribe = { "Type":"AmazonTranscribeCallAnalyticsProcessor", "AmazonTranscribeCallAnalyticsProcessorConfiguration": { "LanguageCode": transcribeLanguageCode, "PostCallAnalyticsSettings": { "DataAccessRoleArn": tcaDataAccessRoleArn, "OutputLocation": outputLocation } } } if postCallContentRedactionOutput and isContentRedactionEnabled == 'true': transcribe[transcribeConfig]["PostCallAnalyticsSettings"]["ContentRedactionOutput"] = postCallContentRedactionOutput else: transcribeConfig = "AmazonTranscribeProcessorConfiguration" transcribe = { "Type":"AmazonTranscribeProcessor", "AmazonTranscribeProcessorConfiguration": { "LanguageCode": transcribeLanguageCode, } } if isContentRedactionEnabled == 'true': transcribe[transcribeConfig]["ContentRedactionType"] = contentRedactionType transcribe[transcribeConfig]["PiiEntityTypes"] = piiEntityTypes if customLanguageModelName: transcribe[transcribeConfig]["LanguageModelName"] = customLanguageModelName if customVocabularyName: transcribe[transcribeConfig]["VocabularyName"] = customVocabularyName elements.append(transcribe) print("Media pipeline configuration") print(json.dumps(elements)) return elements def update_media_pipeline_config(event): id = event.get('PhysicalResourceId') resourceAccessRoleArn = event['ResourceProperties'].get('ResourceAccessRoleArn', '') transcribePipelineConfigName = event['ResourceProperties'].get('StackName', '') + '-ts-' + id transcribeElements = generate_transcribe_config(event, transcribePipelineConfigName, resourceAccessRoleArn) transcribeMediaPipelineConfiguration = get_media_pipeline_config(transcribePipelineConfigName) if transcribeMediaPipelineConfiguration is None: print("Creating transcribe media pipeline config: ", transcribePipelineConfigName) transcribeResponse = mediaPipelineClient.create_media_insights_pipeline_configuration( MediaInsightsPipelineConfigurationName=transcribePipelineConfigName, ResourceAccessRoleArn=resourceAccessRoleArn, RealTimeAlertConfiguration={ 'Disabled': True }, Elements=transcribeElements ) else: print("Updating transcribe media pipeline config: ", transcribePipelineConfigName) transcribeResponse = mediaPipelineClient.update_media_insights_pipeline_configuration( Identifier=transcribePipelineConfigName, ResourceAccessRoleArn=resourceAccessRoleArn, RealTimeAlertConfiguration={ 'Disabled': True }, Elements=transcribeElements ) voiceAnalyticsPipelineConfigName = event['ResourceProperties'].get('StackName', '') + '-' + id voiceAnalyticsElements = generate_voice_analytics_config(event, voiceAnalyticsPipelineConfigName, resourceAccessRoleArn) voiceAnalyticsPipelineConfiguration = get_media_pipeline_config(voiceAnalyticsPipelineConfigName) if voiceAnalyticsPipelineConfiguration is None: print("Creating voice analytics media pipeline config: ", voiceAnalyticsPipelineConfigName) voiceAnalyticsResponse = mediaPipelineClient.create_media_insights_pipeline_configuration( MediaInsightsPipelineConfigurationName=voiceAnalyticsPipelineConfigName, ResourceAccessRoleArn=resourceAccessRoleArn, RealTimeAlertConfiguration={ 'Disabled': True }, Elements=voiceAnalyticsElements ) else: print("Updating voice analytics media pipeline config: ", voiceAnalyticsPipelineConfigName) voiceAnalyticsResponse = mediaPipelineClient.update_media_insights_pipeline_configuration( Identifier=voiceAnalyticsPipelineConfigName, ResourceAccessRoleArn=resourceAccessRoleArn, RealTimeAlertConfiguration={ 'Disabled': True }, Elements=voiceAnalyticsElements ) return { 'VoiceAnalyticsConfigArn': voiceAnalyticsResponse['MediaInsightsPipelineConfiguration']['MediaInsightsPipelineConfigurationArn'], 'ConfigArn': transcribeResponse['MediaInsightsPipelineConfiguration']['MediaInsightsPipelineConfigurationArn'], 'PhysicalResourceId': id } def create_media_pipeline_config(event): print('creating media pipeline configuration') id = str(uuid.uuid4()) resourceAccessRoleArn = event['ResourceProperties'].get('ResourceAccessRoleArn', '') transcribePipelineConfigName = event['ResourceProperties'].get('StackName', '') + '-ts-' + id transcribeElements = generate_transcribe_config(event, transcribePipelineConfigName, resourceAccessRoleArn) print("Creating transcribe media pipeline config: ", transcribePipelineConfigName) transcribeResponse = mediaPipelineClient.create_media_insights_pipeline_configuration( MediaInsightsPipelineConfigurationName=transcribePipelineConfigName, ResourceAccessRoleArn=resourceAccessRoleArn, RealTimeAlertConfiguration={ 'Disabled': True }, Elements=transcribeElements ) voiceAnalyticsPipelineConfigName = event['ResourceProperties'].get('StackName', '') + '-' + id voiceAnalyticsElements = generate_voice_analytics_config(event, voiceAnalyticsPipelineConfigName, resourceAccessRoleArn) print("Creating voice analytics media pipeline config: ", voiceAnalyticsPipelineConfigName) voiceAnalyticsResponse = mediaPipelineClient.create_media_insights_pipeline_configuration( MediaInsightsPipelineConfigurationName=voiceAnalyticsPipelineConfigName, ResourceAccessRoleArn=resourceAccessRoleArn, RealTimeAlertConfiguration={ 'Disabled': True }, Elements=voiceAnalyticsElements ) return { 'VoiceAnalyticsConfigArn': voiceAnalyticsResponse['MediaInsightsPipelineConfiguration']['MediaInsightsPipelineConfigurationArn'], 'ConfigArn': transcribeResponse['MediaInsightsPipelineConfiguration']['MediaInsightsPipelineConfigurationArn'], 'PhysicalResourceId': id } def get_vc_configuration(event): voiceConnectorId = event['ResourceProperties']['VoiceConnectorId'] print(f"Getting existing configuration... {voiceConnectorId}") try: response = voiceClient.get_voice_connector_streaming_configuration(VoiceConnectorId=voiceConnectorId) streamingConfiguration = response["StreamingConfiguration"] print("Voice connector streaming configuration") print(json.dumps(streamingConfiguration)) return streamingConfiguration except Exception as e: error = f'Error getting voice connector streaming config: {e}.' print(error) return None def get_media_pipeline_config(pipeline_identifier): print(f"Getting existing media insight pipeline config... {pipeline_identifier}") try: response = mediaPipelineClient.get_media_insights_pipeline_configuration(Identifier=pipeline_identifier) mediaPipelineConfiguration = response["MediaInsightsPipelineConfiguration"] print("Media insight pipeline configuration already exists") return mediaPipelineConfiguration except mediaPipelineClient.exceptions.NotFoundException as nfe: error = f'Media insight pipeline is not found: {nfe}.' print(error) except Exception as e: error = f'Error getting existing media pipeline config: {e}.' print(error) return None def update_vc_configuration(event, config_arn, delete=False): print("Updating VC configuration") voiceConnectorId = event['ResourceProperties']['VoiceConnectorId'] streamingConfiguration = get_vc_configuration(event) if streamingConfiguration is None: # nothing to do? return None if event['ResourceProperties']['EnableVoiceToneAnalysis'] == 'true' and delete == False: print("Enabling Voice Tone Analysis...") streamingConfiguration['MediaInsightsConfiguration'] = { "ConfigurationArn": config_arn, "Disabled": False } else: print("Disabling Voice Tone Analysis/Removing Streaming Config from VC") if 'MediaInsightsConfiguration' in streamingConfiguration: del streamingConfiguration['MediaInsightsConfiguration'] print(json.dumps(streamingConfiguration)) print("Saving configuration...") response = voiceClient.put_voice_connector_streaming_configuration( VoiceConnectorId=voiceConnectorId, StreamingConfiguration=streamingConfiguration ) print(response) return response def lambda_handler(event, context): print(event) responseData = { "success": True } try: if event['RequestType'] == "Create": responseData = create_media_pipeline_config(event) update_vc_configuration(event, responseData['VoiceAnalyticsConfigArn']) cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, responseData['PhysicalResourceId']) elif event['RequestType'] == "Update": responseData = update_media_pipeline_config(event) update_vc_configuration(event, responseData['VoiceAnalyticsConfigArn']) cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, responseData['PhysicalResourceId']) else: update_vc_configuration(event, None, True) responseData = delete_pipeline_config(event) cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, responseData['PhysicalResourceId']) except Exception as e: tb = traceback.format_exc() print(tb) error = f'Exception thrown: {e}. Please see https://github.com/aws-samples/amazon-transcribe-live-call-analytics/blob/main/TROUBLESHOOTING.md for more information.' print(error) cfnresponse.send(event, context, cfnresponse.FAILED, {}, reason=error ) ########################################################################## # Voice Tone Lambda Function ########################################################################## VoiceToneLambda: Type: AWS::Serverless::Function Properties: Architectures: - x86_64 Description: >- AWS Lambda Function that will accept events from Amazon Chime SDK Call Analytics voice tone analysis, modify them for use for LCA, and save them to the LCA Kinesis Data Streams as a voice tone analysis Runtime: python3.8 Handler: lambda_function.lambda_handler Layers: - !Ref Boto3LayerArn Role: !GetAtt VoiceToneLambdaRole.Arn MemorySize: 128 Timeout: 60 Environment: Variables: KINESIS_STREAM_NAME: !Ref KinesisDataStreamName TRANSCRIBER_CALL_EVENT_TABLE_NAME: !Ref TranscriberCallEventTable CodeUri: ../lambda_functions/voice_tone_processor VoiceToneLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: # CloudWatch Insights Managed Policy - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: lca-voice-tone-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - chime:StartVoiceToneAnalysisTask - chime:StopVoiceToneAnalysisTask - chime:StartSpeakerSearchTask - chime:StopSpeakerSearchTask Resource: '*' - 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:GetItem - dynamodb:PutItem - dynamodb:UpdateItem Resource: - !GetAtt TranscriberCallEventTable.Arn - Action: - kinesis:PutRecord Effect: Allow Resource: - !Ref KinesisDataStreamArn ########################################################################## # Chime Call Initialization ########################################################################## TranscriberCallEventTable: Type: AWS::DynamoDB::Table DeletionPolicy: Delete UpdateReplacePolicy: Delete Properties: AttributeDefinitions: # primary key attributes - AttributeName: PK AttributeType: S - AttributeName: SK AttributeType: S KeySchema: - AttributeName: PK KeyType: HASH - AttributeName: SK KeyType: RANGE BillingMode: PAY_PER_REQUEST PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: true SSESpecification: SSEEnabled: true TimeToLiveSpecification: AttributeName: ExpiresAfter Enabled: true StreamSpecification: StreamViewType: NEW_IMAGE CallAnalyticsInitFunction: Type: AWS::Serverless::Function Properties: Architectures: - arm64 Description: >- AWS Lambda Function that will be triggered when a new call starts. This will initialize the Amazon Chime SDK Call Analytics Media Pipeline. Handler: index.handler Layers: # periodically update the Lambda Insights Layer # https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html - !Sub 'arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension-Arm64:2' - !Ref TranscriberLambdaLayer Role: !GetAtt CallAnalyticsInitFunctionRole.Arn Runtime: nodejs18.x MemorySize: 128 Timeout: 900 Tracing: Active Environment: Variables: ENABLE_VOICETONE: !Ref EnableVoiceToneAnalysis CHIME_MEDIAPIPELINE_CONFIG_ARN: !GetAtt CreateMediaPipelineConfigForVC.ConfigArn LCA_STACK_NAME: !Ref LCAStackName TRANSCRIBE_API_MODE: !Ref TranscribeApiMode BUFFER_SIZE: '128' LAMBDA_INVOKE_TIMEOUT: '720000' KINESIS_STREAM_NAME: !Ref KinesisDataStreamName TRANSCRIBER_CALL_EVENT_TABLE_NAME: !Ref TranscriberCallEventTable REGION: !Ref AWS::Region OUTPUT_BUCKET: !Ref S3BucketName RAW_FILE_PREFIX: 'lca-audio-raw/' RECORDING_FILE_PREFIX: !Ref AudioFilePrefix CALL_ANALYTICS_FILE_PREFIX: !Ref CallAnalyticsPrefix TCA_DATA_ACCESS_ROLE_ARN: !GetAtt TcaDataAccessRole.Arn POST_CALL_CONTENT_REDACTION_OUTPUT: 'redacted' TEMP_FILE_PATH: '/tmp/' SAVE_PARTIAL_TRANSCRIPTS: !Ref IsPartialTranscriptEnabled IS_CONTENT_REDACTION_ENABLED: !If - ShouldEnableContentRedaction - 'true' - 'false' TRANSCRIBE_LANGUAGE_CODE: !Ref TranscribeLanguageCode CONTENT_REDACTION_TYPE: !Ref TranscribeContentRedactionType PII_ENTITY_TYPES: !Ref TranscribePiiEntityTypes CUSTOM_VOCABULARY_NAME: !Ref CustomVocabularyName CUSTOM_LANGUAGE_MODEL_NAME: !Ref CustomLanguageModelName LAMBDA_HOOK_FUNCTION_ARN: !Ref SiprecLambdaHookFunctionArn CodeUri: ../lambda_functions/chime_call_analytics_initialization Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired CallAnalyticsInitFunctionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: # CloudWatch Insights Managed Policy - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: lambda-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - iam:PassRole Resource: - !GetAtt TcaDataAccessRole.Arn - Effect: Allow Action: - chime:CreateMediaInsightsPipeline Resource: - !GetAtt CreateMediaPipelineConfigForVC.ConfigArn - !Sub 'arn:${AWS::Partition}:chime:${AWS::Region}:${AWS::AccountId}:media-pipeline/*' - Effect: Allow Action: - chime:StartVoiceToneAnalysisTask Resource: "*" - Effect: Allow Action: - chime:GetMediaPipeline Resource: - !Sub 'arn:${AWS::Partition}:chime:${AWS::Region}:${AWS::AccountId}:media-pipeline/*' - Effect: Allow Action: - transcribe:DeleteTranscriptionJob - transcribe:GetTranscriptionJob - transcribe:GetVocabulary - transcribe:ListTranscriptionJobs - transcribe:ListVocabularies - transcribe:StartStreamTranscription - transcribe:StartCallAnalyticsStreamTranscription - transcribe:StartTranscriptionJob Resource: '*' - Action: - 'kinesisvideo:Describe*' - 'kinesisvideo:Get*' - 'kinesisvideo:List*' Effect: 'Allow' Resource: '*' - 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:GetItem - dynamodb:PutItem - dynamodb:UpdateItem Resource: - !GetAtt TranscriberCallEventTable.Arn - Action: - kinesis:PutRecord Effect: Allow Resource: - !Ref KinesisDataStreamArn - Effect: Allow Action: - s3:GetObject - s3:ListBucket - s3:PutObject - s3:DeleteObject Resource: - !Sub - 'arn:aws:s3:::${bucket}' - bucket: !Ref S3BucketName - !Sub - 'arn:aws:s3:::${bucket}/*' - bucket: !Ref S3BucketName - !If - ShouldEnableLambdaHook - Effect: Allow Action: - lambda:InvokeFunction Resource: !Sub '${SiprecLambdaHookFunctionArn}' - Ref: AWS::NoValue TcaDataAccessRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - transcribe.streaming.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: tca-post-call-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:GetObject - s3:ListBucket - s3:PutObject - s3:DeleteObject Resource: - !Sub - 'arn:aws:s3:::${bucket}' - bucket: !Ref S3BucketName - !Sub - 'arn:aws:s3:::${bucket}/*' - bucket: !Ref S3BucketName Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: >- Transcribe does not support resource-level permissions and KVS streams are dynamic ########################################################################## # Event Bridge Notifications ########################################################################## AllowEventBridgeToCallAnalyticsInitFunctionLambdaFromChimeVC: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref CallAnalyticsInitFunction Action: 'lambda:InvokeFunction' Principal: events.amazonaws.com SourceArn: !GetAtt EventBridgeRuleToTriggerCallAnalyticsInitLambdaFromChimeVC.Arn SourceAccount: !Ref AWS::AccountId EventBridgeRuleToTriggerCallAnalyticsInitLambdaFromChimeVC: Type: AWS::Events::Rule Properties: Description: 'This rule is triggered when the ChimeVC streaming status changes' EventPattern: detail: voiceConnectorId: - !Ref VoiceConnectorId detail-type: - 'Chime VoiceConnector Streaming Status' source: - aws.chime Targets: - Id: CallAnalyticsInitTarget Arn: !GetAtt CallAnalyticsInitFunction.Arn State: 'ENABLED' AllowEventBridgeToCallAnalyticsInitFunctionLambdFromIVR: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref CallAnalyticsInitFunction Action: 'lambda:InvokeFunction' Principal: events.amazonaws.com SourceArn: !GetAtt EventBridgeRuleToTriggerCallAnalyticsInitLambdaFromIVR.Arn SourceAccount: !Ref AWS::AccountId EventBridgeRuleToTriggerCallAnalyticsInitLambdaFromIVR: Type: AWS::Events::Rule Properties: Description: 'This rule is triggered when a START_CALL_PROCESSING event is sent from IVR' EventPattern: detail-type: - 'START_CALL_PROCESSING' source: - lca-solution Targets: - Id: CallAnalyticsInitTarget Arn: !GetAtt CallAnalyticsInitFunction.Arn State: 'ENABLED' # Permission for Call Transcriber to invoke itself CallAnalyticsInitPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CallAnalyticsInitFunction Principal: !GetAtt CallAnalyticsInitFunctionRole.Arn Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: Amazon S3 Configuration Parameters: - S3BucketName - AudioFilePrefix - MonoAudioFilePrefix ParameterLabels: S3BucketName: default: Call Audio Bucket Name AudioFilePrefix: default: Audio File Prefix IsContentRedactionEnabled: default: Enable Content Redaction TranscribeContentRedactionType: default: Type of Content Redaction TranscribeLanguageCode: default: Transcription Language Code TranscribePiiEntityTypes: default: Transcription PII Redaction Entity Types CustomVocabularyName: default: Transcription Custom Vocabulary Name Conditions: ShouldEnableContentRedaction: !And - !Equals [!Ref IsContentRedactionEnabled, 'true'] - !Equals [!Ref TranscribeLanguageCode, 'en-US'] ShouldEnableLambdaHook: !Not [!Equals [!Ref SiprecLambdaHookFunctionArn, '']] Outputs: CallTranscriberEventTableName: Value: !Ref TranscriberCallEventTable CallTranscriberEventTableArn: Value: !GetAtt TranscriberCallEventTable.Arn IsContentRedactionEnabled: Value: !If - ShouldEnableContentRedaction - 'true' - 'false'