Introduction
Contact centers generate thousands of hours of customer calls every day. Reviewing them manually is not only time-consuming but also inconsistent. Supervisors often struggle to answer questions like:
- How satisfied are customers during calls?
- Are agents following compliance scripts?
- Can we quickly summarize what happened in each call?
In this blog, I’ll walk you through how I built a serverless, AI-powered pipeline on AWS that automates all of this using:
Amazon S3 → for storing audio recordings
Amazon Transcribe → to convert speech to text
Amazon Bedrock (Titan) → to analyze sentiment, compliance, and summaries
Amazon DynamoDB → to store structured insights
Amazon QuickSight → to visualize the results in dashboards
Solution Architecture
Here’s the high-level flow of the system:
- Upload Call Recording → stored in Amazon S3.
- Lambda Function 1 (TranscribeLambda) → triggers Amazon Transcribe to convert speech → text.
- Lambda Function 2 (BedrockAnalysisLambda) → processes the transcript using Amazon Bedrock (Titan).
- Detects Sentiment (Positive/Negative/Neutral).
- Runs a Compliance Check (Pass/Fail).
- Generates a short summary of the call.
- Results are saved in Amazon DynamoDB.
- Supervisors view real-time analytics in Amazon QuickSight dashboards.
This modular design ensures reliability and scalability — transcription and AI analysis scale independently.
Setting Up the Pipeline
1. Create S3 Bucket
aws s3 mb s3://contact-center-demo-bucket
This bucket will store:
- Input audio recordings
- Transcribe-generated transcripts
2. DynamoDB Table
We use DynamoDB to store insights per call:
aws dynamodb create-table \
--table-name CallAnalysis \
--attribute-definitions AttributeName=CallID,AttributeType=S \
--key-schema AttributeName=CallID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
3. Lambda #1 — TranscribeLambda
This function runs when an audio file is uploaded.
import boto3, os, time, re
transcribe = boto3.client('transcribe')
s3_bucket = os.environ['S3_BUCKET']
def lambda_handler(event, context):
file_name = event['Records'][0]['s3']['object']['key']
base_name = file_name.split("/")[-1]
safe_name = re.sub(r'[^0-9a-zA-Z._-]', '_', base_name)
job_name = safe_name + "-" + str(int(time.time()))
transcribe.start_transcription_job(
TranscriptionJobName=job_name,
Media={'MediaFileUri': f"s3://{s3_bucket}/{file_name}"},
MediaFormat='mp3',
LanguageCode='en-US',
OutputBucketName=s3_bucket
)
return {"message": f"Started Transcription Job: {job_name}"}
4. Lambda #2 — BedrockAnalysisLambda (Titan)
This function analyzes transcripts via Amazon Titan and stores results in DynamoDB.
import boto3, json, os, urllib.parse
s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['DDB_TABLE'])
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
def lambda_handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])
if not key.endswith(".json"):
return {"message": "Not a transcript JSON file"}
response = s3.get_object(Bucket=bucket, Key=key)
transcript_json = json.loads(response['Body'].read())
transcript_text = transcript_json['results']['transcripts'][0]['transcript']
prompt = f"""
Analyze the following customer service call transcript:
Transcript: {transcript_text}
Provide the following JSON output:
{{
"Sentiment": "Positive | Negative | Neutral | Mixed",
"ComplianceCheck": "Pass | Fail",
"Summary": "<short summary of the call in 2 sentences>"
}}
"""
response = bedrock.invoke_model(
modelId="amazon.titan-text-express-v1",
contentType="application/json",
accept="application/json",
body=json.dumps({
"inputText": prompt,
"textGenerationConfig": {
"maxTokenCount": 512,
"temperature": 0.7,
"topP": 0.9
}
})
)
result_str = response['body'].read().decode("utf-8")
parsed = json.loads(result_str)
output_text = parsed["results"][0]["outputText"]
try:
analysis = json.loads(output_text)
except:
analysis = {"RawOutput": output_text}
table.put_item(Item={
"CallID": key,
"Transcript": transcript_text,
"Sentiment": analysis.get("Sentiment", "Unknown"),
"ComplianceCheck": analysis.get("ComplianceCheck", "Unknown"),
"Summary": analysis.get("Summary", output_text)
})
return {"message": f"Processed call {key}", "Analysis": analysis}
QuickSight Dashboard
Finally, you can connect QuickSight to DynamoDB to see the data.
- Sentiment Pie Chart → Positive/Negative/Neutral split.
- Compliance KPI → Percentage of compliant calls.
- Summary Table → Quick overview of each call. This gives supervisors real-time visibility into call quality.
Step-by-Step: Add S3 Triggers for Lambda
- Open the S3 Bucket
- Go to AWS Console → S3.
- Find and click your bucket (contact-center-demo-bucket).
- Go to the Properties tab.
Add Event Notification for Audio → Transcribe
- Scroll down to Event notifications → click Create event notification.
- Give it a name: AudioToTranscribe.
- Event types: PUT / All object create events.
- Prefix filter (optional but recommended): audio/
- This keeps recordings in s3://bucket/audio/....
- Suffix filter: .mp3 (or .wav if needed).
- Destination: Lambda function.
- Choose TranscribeLambda.
- Save.
Add Event Notification for Transcript → Bedrock
- Still in the same bucket → Create event notification again.
- Name it: TranscriptToBedrock.
- Event types: PUT / All object create events.
- Prefix filter (optional): transcripts/
- Suffix filter: .json.
- Destination: Lambda function.
- Choose BedrockAnalysisLambda.
- Save.
Sample audio: https://github.com/aws-samples/amazon-transcribe-output-word-document/blob/main/sample-data/example-call.wav
Workflow
Here’s how the demo works in action:
- Upload sample_call.mp3 to S3.
- Amazon Transcribe job runs → transcript JSON created.
- Transcript triggers Bedrock Lambda → Titan generates sentiment, compliance, and summary.
- DynamoDB stores structured results.
- QuickSight dashboard updates → insights appear instantly.
Business Impact
- Faster QA: No need for manual call listening.
- Consistency: AI applies rules the same way every time.
- Scalability: Works for 100 or 100,000 calls.
- Actionable Insights: Supervisors can track compliance and customer satisfaction in real-time.
Challenges & Lessons Learned
Building this system was not without its hiccups! Here are some key issues I faced and how I solved them:
1. S3 Event Trigger Conflicts
Initially, I tried attaching two separate triggers to the same bucket (one for audio, one for transcripts). This caused overlapping event errors.
Fix: Use either one trigger with a dispatcher Lambda, or carefully configure suffix filters (.mp3 for TranscribeLambda and .json for BedrockLambda).
2. Transcribe Job Naming Errors
Amazon Transcribe has strict rules for job names (no slashes, only alphanumeric, _, -, .).
Fix: Cleaned up S3 filenames with regex and appended a timestamp for uniqueness.
3. Bedrock Model Access
Even with the correct IAM policies, I got AccessDenied when calling Bedrock.
Fix: Model access must be explicitly enabled in the Bedrock console. I switched to Amazon Titan Text G1 – Express, which was available in my account.
4. Titan Output Format
Titan wraps responses in a nested JSON format. At first, I was saving raw output, which cluttered DynamoDB.
Fix: Extracted the actual outputText, and parsed it into structured fields (Sentiment, ComplianceCheck, Summary) before saving.
5. Lambda Indentation & Handler Errors
Using inline code in CloudFormation caused handler mismatches (transcribe_handler vs index.lambda_handler) and indentation mistakes.
Fix: Always double-check indentation and remember that inline code defaults to index.lambda_handler.
6. QuickSight Data Modeling
QuickSight doesn’t handle nested DynamoDB JSON gracefully.
Fix: Store flattened fields (Sentiment, ComplianceCheck, Summary) directly in DynamoDB for easy visualization.
Conclusion
This project demonstrates how easily AWS AI services and serverless architecture can be combined to solve real-world business problems.
By leveraging Amazon Transcribe, Amazon Bedrock (Titan), DynamoDB, and QuickSight, we built a scalable solution that automates quality monitoring and empowers contact center supervisors with actionable insights.
Top comments (1)
Insightful post, Looking more integrations along with this 😺