How to Use Amazon Polly to Convert Text to Speech and Store MP3 Files in S3
You have a content pipeline — articles, blog posts, documentation — and you want to ship an audio version without standing up a separate TTS infrastructure. Amazon Polly lets you call a single API, get back an MP3 stream, and pipe it directly to S3. The tricky part is understanding which voice options are available for non-English languages like Korean, and how the neural vs. standard engine selection affects both quality and cost.
TL;DR: Amazon Polly Text-to-Speech Pipeline
| Step | Action | Key Decision |
|---|---|---|
| 1 | Choose engine (Neural vs. Standard) | Neural sounds better; not all voices support it |
| 2 | Select voice ID for target language | Korean: Seoyeon (Neural supported) |
| 3 | Call SynthesizeSpeech or StartSpeechSynthesisTask | Short text → SynthesizeSpeech; long text → async task |
| 4 | Write audio stream to S3 | Use PutObject or let Polly write directly via async task |
How Amazon Polly Text-to-Speech Works
Polly exposes two synthesis paths. The synchronous SynthesizeSpeech API accepts up to 3,000 billable characters per request and returns an audio stream inline. The asynchronous StartSpeechSynthesisTask API handles up to 100,000 billable characters, writes the output directly to an S3 bucket you specify, and returns a task ID you can poll. For article-length content, the async path is almost always the right choice — you avoid managing the stream yourself and you get S3 delivery built in.
The engine parameter controls the underlying synthesis model. Standard engine uses concatenative synthesis. Neural engine uses a deep learning model and produces noticeably more natural prosody. The catch: neural voices are a subset of the full voice catalog, and pricing differs between the two engines. Always check the current AWS pricing page before committing to a volume workload.
(Synchronous)"] Decision -- "Up to 100,000 chars" --> Async["StartSpeechSynthesisTask
(Asynchronous)"] Sync --> PollyEngine["Polly Neural Engine
VoiceId: Seoyeon / ko-KR"] Async --> PollyEngine PollyEngine -- "Audio stream in response" --> AppUpload["App calls S3 PutObject"] PollyEngine -- "Polly writes directly" --> S3Direct["S3 Bucket
(same region)"] AppUpload --> S3Direct S3Direct --> MP3["MP3 Object in S3"]
- Client calls either SynthesizeSpeech (sync) or StartSpeechSynthesisTask (async) with text, voice ID, engine, and output format.
- Polly synthesizes audio using the specified engine and voice model.
- Sync path: audio stream returned directly in the HTTP response body — your application writes it to S3 via PutObject.
- Async path: Polly writes the MP3 directly to the S3 bucket and prefix you specified. You poll GetSpeechSynthesisTask to check completion status.
- S3 stores the final MP3 object, accessible via pre-signed URL or bucket policy.
Korean Voice Options in Amazon Polly
For Korean (ko-KR), Polly currently provides one documented voice: Seoyeon (female). Seoyeon supports both the standard and neural engines. The neural version produces significantly more natural-sounding Korean speech and is the recommended choice for content where listener experience matters.
| Voice ID | Language | Gender | Standard Engine | Neural Engine |
|---|---|---|---|---|
| Seoyeon | ko-KR | Female | Yes | Yes |
Polly's voice catalog evolves — always verify available voices and engine support via the DescribeVoices API or the official AWS documentation before building your pipeline. Do not hardcode assumptions about which voices support neural.
aws polly describe-voices \
--language-code ko-KR \
--engine neural \
--region us-east-1
This returns the current set of neural-capable Korean voices. Run this before wiring a voice ID into your application config.
Calling the SynthesizeSpeech API for Short Text
For content under 3,000 billable characters, the synchronous API is straightforward. The response body is the raw audio stream — you pipe it directly to a file or upload it to S3.
AWS CLI — Sync Synthesis
aws polly synthesize-speech \
--engine neural \
--language-code ko-KR \
--voice-id Seoyeon \
--text-type text \
--output-format mp3 \
--text '안녕하세요. 이것은 테스트 음성입니다.' \
--region us-east-1 \
output.mp3
The final positional argument is the local output file path. The CLI writes the audio stream there automatically. From this point, upload to S3 with aws s3 cp output.mp3 s3://your-bucket/audio/output.mp3.
Python (boto3) — Sync Synthesis + S3 Upload
🔽 Click to expand — boto3 sync synthesis example
import boto3
polly = boto3.client('polly', region_name='us-east-1')
s3 = boto3.client('s3', region_name='us-east-1')
response = polly.synthesize_speech(
Engine='neural',
LanguageCode='ko-KR',
VoiceId='Seoyeon',
OutputFormat='mp3',
Text='안녕하세요. 이것은 테스트 음성입니다.',
TextType='text'
)
audio_stream = response['AudioStream'].read()
s3.put_object(
Bucket='your-bucket-name',
Key='audio/output.mp3',
Body=audio_stream,
ContentType='audio/mpeg'
)
print('Upload complete.')
One thing worth noting: response['AudioStream'] is a streaming body. Call .read() once and hold the bytes — don't try to read it twice. If your article text is close to the 3,000-character limit, measure billable characters carefully. SSML tags don't count toward the limit, but the text they wrap does.
Using StartSpeechSynthesisTask for Article-Length Content
For full articles, the async API is the correct tool. You hand Polly an S3 destination and it handles the write. No stream management, no memory pressure from large audio blobs in your Lambda or EC2 process.
IAM Policy — Minimum Required Permissions
🔽 Click to expand — IAM policy for Polly async task
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PollyAsyncSynthesis",
"Effect": "Allow",
"Action": [
"polly:StartSpeechSynthesisTask",
"polly:GetSpeechSynthesisTask",
"polly:ListSpeechSynthesisTasks"
],
"Resource": "*"
},
{
"Sid": "S3WriteAudioOutput",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/audio/*"
}
]
}
Polly's async task writes to S3 using the caller's identity context — the IAM principal running StartSpeechSynthesisTask must have s3:PutObject on the destination prefix. If the bucket has a restrictive bucket policy or is in a different account, you'll need to account for that explicitly. The S3 bucket must also be in the same region as the Polly API call.
Think of the async task like a print job sent to a network printer — you submit it, get a job ID, and check back later. Polly owns the delivery; you own the destination bucket and the polling logic.
AWS CLI — Async Synthesis Task
aws polly start-speech-synthesis-task \
--engine neural \
--language-code ko-KR \
--voice-id Seoyeon \
--output-format mp3 \
--output-s3-bucket-name your-bucket-name \
--output-s3-key-prefix audio/ \
--text-type text \
--text file://article.txt \
--region us-east-1
The response includes a TaskId. Poll task status with:
aws polly get-speech-synthesis-task \
--task-id <TaskId> \
--region us-east-1
When TaskStatus returns completed, the MP3 is at the S3 path Polly constructed from your prefix plus the task ID. The OutputUri field in the response contains the exact S3 URI.
Python (boto3) — Async Task with Polling
🔽 Click to expand — boto3 async task example
import boto3
import time
polly = boto3.client('polly', region_name='us-east-1')
with open('article.txt', 'r', encoding='utf-8') as f:
article_text = f.read()
task_response = polly.start_speech_synthesis_task(
Engine='neural',
LanguageCode='ko-KR',
VoiceId='Seoyeon',
OutputFormat='mp3',
OutputS3BucketName='your-bucket-name',
OutputS3KeyPrefix='audio/',
TextType='text',
Text=article_text
)
task_id = task_response['SynthesisTask']['TaskId']
print(f'Task submitted: {task_id}')
while True:
status_response = polly.get_speech_synthesis_task(TaskId=task_id)
status = status_response['SynthesisTask']['TaskStatus']
print(f'Status: {status}')
if status in ('completed', 'failed'):
break
time.sleep(5)
if status == 'completed':
output_uri = status_response['SynthesisTask']['OutputUri']
print(f'Audio available at: {output_uri}')
else:
reason = status_response['SynthesisTask'].get('TaskStatusReason', 'unknown')
print(f'Task failed: {reason}')
SSML for Pronunciation and Pacing Control
Plain text synthesis works, but Korean article content often has edge cases — English loanwords, numbers read as digits vs. ordinals, pauses between sections. SSML gives you control over these without changing your source text. Set TextType to ssml and wrap your content in <speak> tags.
aws polly synthesize-speech \
--engine neural \
--language-code ko-KR \
--voice-id Seoyeon \
--text-type ssml \
--output-format mp3 \
--text '<speak>안녕하세요. <break time="500ms"/> 오늘의 주제는 <lang xml:lang="en-US">Amazon Polly</lang>입니다.</speak>' \
--region us-east-1 \
output_ssml.mp3
The <break> tag inserts a pause. The <lang> tag signals a language switch for embedded English terms — without it, Polly will attempt to pronounce English words using Korean phonology, which rarely sounds right. Not all SSML tags are supported by all engines; verify SSML tag support against the Polly SSML reference before using tags in production.
A Real Failure Pattern: Silent Audio or Wrong Language
Here's a failure pattern that shows up when teams first wire this pipeline: the task completes successfully, the MP3 lands in S3, but playback produces garbled audio or the wrong language entirely.
Symptom: TaskStatus: completed, valid MP3 file in S3, but audio sounds like English phonemes applied to Korean characters.
Misdiagnosis: Most engineers first suspect a voice ID problem and try switching voices. There's only one Korean voice, so that goes nowhere. Some then suspect encoding — UTF-8 vs. EUC-KR — and spend time there.
Actual cause: The LanguageCode parameter was omitted. When LanguageCode is not specified, Polly infers the language from the voice ID. For Seoyeon this usually resolves correctly, but if the text contains mixed-language content and the engine selection doesn't align with the voice's supported languages, synthesis behavior can be inconsistent. The more common root cause in practice: the wrong engine was specified. If you request Engine: standard but your text or SSML assumes neural-quality prosody markers, you get degraded output with no error — the API call succeeds.
Fix: Always explicitly pass both Engine and LanguageCode. Don't rely on inference. Run describe-voices to confirm the engine-voice combination is valid before submitting a batch job.
StartSpeechSynthesisTask called"] --> Check1{"LanguageCode
specified?"} Check1 -- "No" --> Infer["Polly infers from VoiceId
— inconsistent with SSML"] Check1 -- "Yes: ko-KR" --> Check2{"Engine specified?"} Infer --> BadOutput["Garbled or wrong-language audio
TaskStatus: completed — no error"] Check2 -- "No / Standard" --> Mismatch["Degraded output
No API error returned"] Check2 -- "Neural" --> Validate["describe-voices confirms
Seoyeon + neural + ko-KR"] Validate --> GoodOutput["Clean Korean neural audio
MP3 written to S3"]
- Missing LanguageCode: Polly infers from VoiceId — usually works for Seoyeon, but not guaranteed with mixed-language SSML.
- Engine mismatch: Requesting standard engine with neural-optimized SSML produces degraded output silently — no API error.
- Correct path: Explicitly set both Engine=neural and LanguageCode=ko-KR. Validate with describe-voices first.
S3 Bucket Configuration for Audio Delivery
The async task requires the destination S3 bucket to be in the same AWS region as the Polly API endpoint you're calling. Cross-region delivery is not supported for StartSpeechSynthesisTask. If your content pipeline runs in ap-northeast-2 (Seoul), your S3 bucket must also be in ap-northeast-2.
For serving audio to end users, generate pre-signed URLs rather than making the bucket public. Pre-signed URLs expire after a configurable duration and require no bucket policy changes.
aws s3 presign s3://your-bucket-name/audio/your-file.mp3 \
--expires-in 3600 \
--region ap-northeast-2
Wrap-Up and Next Steps for Your Polly Text-to-Speech Pipeline
The core pattern is simple: StartSpeechSynthesisTask with Engine=neural, VoiceId=Seoyeon, LanguageCode=ko-KR, and an S3 destination in the same region. Always explicitly set both engine and language code — don't let Polly infer them. Use SSML for any content with English loanwords or section breaks that need controlled pacing.
From here, consider wiring the task completion check into an EventBridge rule or an SQS queue rather than polling in a loop — Polly can send task completion notifications to an SNS topic via the SnsTopicArn parameter on StartSpeechSynthesisTask, which gives you an event-driven alternative to polling.
- AWS Polly SynthesizeSpeech API Reference
- AWS Polly StartSpeechSynthesisTask API Reference
- Amazon Polly Voice List
- SSML Supported Tags in Amazon Polly
Glossary
| Term | Definition |
|---|---|
| SynthesizeSpeech | Synchronous Polly API that returns an audio stream inline; limited to 3,000 billable characters per request. |
| StartSpeechSynthesisTask | Asynchronous Polly API that writes output directly to S3; supports up to 100,000 billable characters. |
| Neural Engine | Deep learning-based synthesis model in Polly that produces more natural prosody than the standard concatenative engine. |
| SSML | Speech Synthesis Markup Language — XML-based markup for controlling pronunciation, pauses, and language switching in TTS output. |
| VoiceId | Polly's identifier for a specific voice-language-gender combination; e.g., 'Seoyeon' for Korean female neural voice. |
Comments
Post a Comment