Module 5: Audio Processing
4. TTS: OpenAI
Description
OpenAI offers a Text-to-Speech (TTS) API that generates natural human speech from text. It supports 6 voices, 2 models (standard and HD), 6 audio formats, speed control, and streaming. In this capsule you'll master the complete API: speech generation, voice and model selection, output formats, handling long text, streaming for real-time applications, and cost optimizations.
Why it matters: TTS is the component that closes the multimodal loop. Without TTS, your pipelines end in text. With TTS, you can generate spoken summaries of meetings, create voice assistants, or build accessibility experiences. OpenAI TTS is the most integrated provider if you already use its ecosystem.
Connection with the project: The Audio Pipeline (capsule 08) uses OpenAI TTS to generate the audio summary. Everything you learn here about voices, formats and long text applies directly to the project.
Available Voices
OpenAI offers 6 voices, each with a distinct character and tone:
| Voice | Perceived gender | Tone | Recommended use |
|---|---|---|---|
alloy | Neutral | Balanced, versatile | General, demos |
echo | Male | Deep, calm | Narration, podcasts |
fable | Male | Warm, narrative | Stories, education |
onyx | Male | Low, authoritative | Formal presentations |
nova | Female | Friendly, energetic | Assistants, tutorials |
shimmer | Female | Soft, expressive | Meditation, calm content |
Preview all voices
from openai import OpenAI
client = OpenAI()
VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
def preview_all_voices(text: str, output_dir: str = "voice_previews") -> list[str]:
from pathlib import Path
Path(output_dir).mkdir(exist_ok=True)
paths = []
for voice in VOICES:
output_path = f"{output_dir}/{voice}.mp3"
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text
)
response.stream_to_file(output_path)
paths.append(output_path)
return paths
Models
| Model | Quality | Latency | Cost | Recommended use |
|---|---|---|---|---|
tts-1 | Good | Low | $15 / 1M characters | Real-time applications, prototyping |
tts-1-hd | High | Higher | $30 / 1M characters | Podcasts, published content, production |
Technical differences:
- tts-1: Optimized for speed. May have audible artifacts on long audio. Ideal for applications where latency matters more than maximum quality.
- tts-1-hd: Optimized for quality. Cleaner and more natural audio. Ideal for content that gets published or distributed.
def text_to_speech(
text: str,
voice: str = "alloy",
model: str = "tts-1",
output_path: str = "output.mp3"
) -> str:
response = client.audio.speech.create(
model=model,
voice=voice,
input=text
)
response.stream_to_file(output_path)
return output_path
tts_standard = text_to_speech("Hello world", model="tts-1")
tts_hd = text_to_speech("Hello world", model="tts-1-hd")
Audio Formats
OpenAI TTS supports 6 output formats:
| Format | Extension | Compression | Quality | Recommended use |
|---|---|---|---|---|
mp3 | .mp3 | Lossy | Good | Default, web distribution |
opus | .opus | Lossy | Good | Streaming, low latency |
aac | .aac | Lossy | Good | YouTube, iOS/Android |
flac | .flac | Lossless | Maximum | Archiving, later editing |
wav | .wav | Uncompressed | Maximum | Processing, compatibility |
pcm | .pcm | Uncompressed | Maximum | Raw audio, DSP processing |
Generate in different formats
def text_to_speech_format(
text: str,
voice: str = "alloy",
response_format: str = "mp3",
output_path: str = None
) -> str:
if output_path is None:
output_path = f"output.{response_format}"
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text,
response_format=response_format
)
response.stream_to_file(output_path)
return output_path
When to use each format
FORMAT_GUIDE = {
"web_general": "mp3",
"streaming_low_latency": "opus",
"mobile_app": "aac",
"high_quality_archive": "flac",
"audio_processing": "wav",
"raw_pipeline": "pcm",
}
Speed Control
The speed parameter controls the playback speed of the generated audio:
| Speed | Value | Use |
|---|---|---|
| Very slow | 0.25 | Language learning, accessibility |
| Slow | 0.5 | Step-by-step instructions |
| Slightly slow | 0.75 | Detailed content |
| Normal | 1.0 | Default |
| Slightly fast | 1.25 | Summaries |
| Fast | 1.5 | Skimming content |
| Very fast | 2.0 | Quick review |
| Maximum | 4.0 | Upper limit |
def text_to_speech_speed(
text: str,
speed: float = 1.0,
voice: str = "nova",
output_path: str = "output.mp3"
) -> str:
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text,
speed=speed
)
response.stream_to_file(output_path)
return output_path
slow = text_to_speech_speed("Detailed instructions...", speed=0.75)
fast = text_to_speech_speed("Executive summary...", speed=1.5)
Streaming
OpenAI TTS supports streaming: the audio is generated and transmitted in real time, without waiting for the entire file to finish. Ideal for applications that need an immediate response.
Streaming to a file
def text_to_speech_streaming(
text: str,
voice: str = "alloy",
output_path: str = "output.mp3"
) -> str:
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text
)
with open(output_path, "wb") as f:
for chunk in response.iter_bytes(chunk_size=1024):
f.write(chunk)
return output_path
Get audio as bytes (without saving a file)
def text_to_speech_bytes(text: str, voice: str = "alloy") -> bytes:
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text
)
return response.content
Streaming with FastAPI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.get("/tts")
async def tts_endpoint(text: str, voice: str = "alloy"):
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text
)
def generate():
for chunk in response.iter_bytes(chunk_size=1024):
yield chunk
return StreamingResponse(
generate(),
media_type="audio/mpeg",
headers={"Content-Disposition": "attachment; filename=speech.mp3"}
)
Handling Long Text
OpenAI TTS has a limit of ~4096 characters per request. For longer text, split into chunks and concatenate the audio:
Strategy: split by sentences and concatenate
from pydub import AudioSegment
import tempfile
from pathlib import Path
def tts_long_text(
text: str,
voice: str = "alloy",
model: str = "tts-1",
max_chars: int = 4000,
output_path: str = "output_long.mp3"
) -> dict:
sentences = text.replace(".", ".\n").replace("!", "!\n").replace("?", "?\n").split("\n")
sentences = [s.strip() for s in sentences if s.strip()]
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) + 1 <= max_chars:
current_chunk = f"{current_chunk} {sentence}".strip()
else:
if current_chunk:
chunks.append(current_chunk)
current_chunk = sentence
if current_chunk:
chunks.append(current_chunk)
segments = []
with tempfile.TemporaryDirectory() as tmp_dir:
for i, chunk in enumerate(chunks):
chunk_path = str(Path(tmp_dir) / f"chunk_{i:03d}.mp3")
response = client.audio.speech.create(
model=model,
voice=voice,
input=chunk
)
response.stream_to_file(chunk_path)
segments.append(AudioSegment.from_mp3(chunk_path))
combined = segments[0]
for seg in segments[1:]:
combined += seg
combined.export(output_path, format="mp3")
return {
"output_path": output_path,
"chunks": len(chunks),
"total_chars": len(text),
"duration_ms": len(combined)
}
Costs
Pricing
| Model | Cost per 1M characters | Cost per 1K characters |
|---|---|---|
tts-1 | $15.00 | $0.015 |
tts-1-hd | $30.00 | $0.030 |
Cost estimation
def estimate_tts_cost(text: str, model: str = "tts-1") -> dict:
char_count = len(text)
cost_per_char = 0.000015 if model == "tts-1" else 0.00003
return {
"characters": char_count,
"model": model,
"estimated_cost_usd": round(char_count * cost_per_char, 6)
}
Reference table
| Text | Approx. characters | Cost (tts-1) | Cost (tts-1-hd) |
|---|---|---|---|
| 1 sentence | ~100 | $0.0015 | $0.003 |
| 1 paragraph | ~500 | $0.0075 | $0.015 |
| 1 page | ~2,000 | $0.03 | $0.06 |
| 1 article | ~5,000 | $0.075 | $0.15 |
| 1 book (200 pages) | ~400,000 | $6.00 | $12.00 |
Cost optimization
def optimize_tts_cost(text: str, max_budget_usd: float = 0.10) -> dict:
chars = len(text)
for model in ["tts-1", "tts-1-hd"]:
cost = estimate_tts_cost(text, model)["estimated_cost_usd"]
if cost <= max_budget_usd:
best_model = model
if estimate_tts_cost(text, "tts-1")["estimated_cost_usd"] > max_budget_usd:
max_chars = int(max_budget_usd / 0.000015)
return {
"warning": f"Text exceeds budget. Maximum {max_chars} characters for ${max_budget_usd}",
"suggestion": "Truncate or summarize the text before generating audio"
}
return {
"model": best_model,
"cost": estimate_tts_cost(text, best_model)["estimated_cost_usd"],
"within_budget": True
}
Complete Function: Configurable TTS
from dataclasses import dataclass
@dataclass
class TTSConfig:
voice: str = "alloy"
model: str = "tts-1"
response_format: str = "mp3"
speed: float = 1.0
def generate_speech(
text: str,
config: TTSConfig = None,
output_path: str = None
) -> dict:
if config is None:
config = TTSConfig()
if output_path is None:
output_path = f"speech.{config.response_format}"
if len(text) > 4000:
return tts_long_text(
text,
voice=config.voice,
model=config.model,
output_path=output_path
)
response = client.audio.speech.create(
model=config.model,
voice=config.voice,
input=text,
response_format=config.response_format,
speed=config.speed
)
response.stream_to_file(output_path)
return {
"output_path": output_path,
"characters": len(text),
"cost_usd": estimate_tts_cost(text, config.model)["estimated_cost_usd"],
"voice": config.voice,
"model": config.model
}
Troubleshooting
Problem 1: Text too long
Symptom: Error or truncation after ~4096 characters.
Solution: Use tts_long_text(), which splits into chunks and concatenates.
Problem 2: Voice doesn't sound natural in Spanish
Symptom: Incorrect pronunciation of Spanish words.
Solution:
| Action | Effect |
|---|---|
Use nova or alloy | Better performance in Spanish |
| Add correct punctuation | Improves intonation |
| Avoid mixing languages | Keep the text in a single language per request |
Use tts-1-hd | More natural pronunciation |
Problem 3: Audio with artifacts
Symptom: Strange sounds, cuts, or distortion.
Solution:
config_hd = TTSConfig(model="tts-1-hd", voice="nova")
result = generate_speech("Test text", config=config_hd)
Problem 4: Inappropriate speed
Symptom: Audio too fast or slow for the use case.
Solution: Adjust speed between 0.25 and 4.0. For narration: 0.85-1.0. For summaries: 1.1-1.25.
Problem 5: Incompatible audio format
Symptom: The player can't open the file.
Solution: Use mp3 (universal compatibility) or wav (uncompressed, maximum compatibility).
Exercises
Exercise 1: Voice preview generator
Create a function that generates the same text with all the voices, in both models (tts-1 and tts-1-hd), and returns a comparison table with paths and costs.
See solution
def generate_voice_matrix(text: str, output_dir: str = "voice_matrix") -> list[dict]:
from pathlib import Path
Path(output_dir).mkdir(exist_ok=True)
results = []
for model in ["tts-1", "tts-1-hd"]:
for voice in VOICES:
output_path = f"{output_dir}/{model}_{voice}.mp3"
response = client.audio.speech.create(
model=model,
voice=voice,
input=text
)
response.stream_to_file(output_path)
cost = estimate_tts_cost(text, model)
results.append({
"model": model,
"voice": voice,
"path": output_path,
"cost_usd": cost["estimated_cost_usd"]
})
return results
Exercise 2: TTS with SSML-like control
Create a function that accepts text with simple markers for pauses (use ... as a pause) and emphasis (use *text* to generate it more slowly).
See solution
import re
def tts_with_markers(
text: str,
voice: str = "nova",
output_path: str = "marked_speech.mp3"
) -> str:
segments_raw = re.split(r'(\.\.\.|(?:\*[^*]+\*))', text)
segments = [s for s in segments_raw if s.strip()]
audio_parts = []
with tempfile.TemporaryDirectory() as tmp_dir:
for i, segment in enumerate(segments):
if segment == "...":
audio_parts.append(AudioSegment.silent(duration=800))
continue
speed = 1.0
clean_text = segment
if segment.startswith("*") and segment.endswith("*"):
clean_text = segment.strip("*")
speed = 0.8
if not clean_text.strip():
continue
chunk_path = str(Path(tmp_dir) / f"seg_{i:03d}.mp3")
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=clean_text,
speed=speed
)
response.stream_to_file(chunk_path)
audio_parts.append(AudioSegment.from_mp3(chunk_path))
combined = audio_parts[0]
for part in audio_parts[1:]:
combined += part
combined.export(output_path, format="mp3")
return output_path
Exercise 3: Cost and duration estimator
Create a function that, given a text and configuration, estimates the cost and approximate duration of the audio without making the API call.
See solution
def estimate_tts_output(
text: str,
model: str = "tts-1",
speed: float = 1.0
) -> dict:
char_count = len(text)
word_count = len(text.split())
words_per_minute = 150
estimated_duration_min = (word_count / words_per_minute) / speed
estimated_duration_s = estimated_duration_min * 60
cost_per_char = 0.000015 if model == "tts-1" else 0.00003
estimated_cost = char_count * cost_per_char
needs_chunking = char_count > 4000
num_chunks = (char_count // 4000) + 1 if needs_chunking else 1
return {
"characters": char_count,
"words": word_count,
"estimated_duration_seconds": round(estimated_duration_s, 1),
"estimated_duration_formatted": f"{int(estimated_duration_min)}:{int(estimated_duration_s % 60):02d}",
"estimated_cost_usd": round(estimated_cost, 6),
"model": model,
"speed": speed,
"needs_chunking": needs_chunking,
"num_chunks": num_chunks
}
Exercise 4: Article-to-podcast converter
Create a function that takes a long article, adds an intro and outro with a different voice, and generates a complete podcast episode.
See solution
def article_to_podcast(
article_text: str,
title: str,
narrator_voice: str = "echo",
host_voice: str = "nova",
output_path: str = "podcast_episode.mp3"
) -> dict:
intro_text = f"Welcome to this episode. Today we cover: {title}. Let's get started."
outro_text = "That's all for today. Thanks for listening. See you next time."
parts = []
with tempfile.TemporaryDirectory() as tmp_dir:
intro_path = str(Path(tmp_dir) / "intro.mp3")
response = client.audio.speech.create(
model="tts-1-hd", voice=host_voice, input=intro_text
)
response.stream_to_file(intro_path)
parts.append(AudioSegment.from_mp3(intro_path))
parts.append(AudioSegment.silent(duration=1000))
body_result = tts_long_text(
article_text,
voice=narrator_voice,
model="tts-1-hd",
output_path=str(Path(tmp_dir) / "body.mp3")
)
parts.append(AudioSegment.from_mp3(body_result["output_path"]))
parts.append(AudioSegment.silent(duration=1500))
outro_path = str(Path(tmp_dir) / "outro.mp3")
response = client.audio.speech.create(
model="tts-1-hd", voice=host_voice, input=outro_text
)
response.stream_to_file(outro_path)
parts.append(AudioSegment.from_mp3(outro_path))
episode = parts[0]
for part in parts[1:]:
episode += part
episode.export(output_path, format="mp3", bitrate="192k")
total_chars = len(intro_text) + len(article_text) + len(outro_text)
return {
"output_path": output_path,
"duration_seconds": len(episode) / 1000,
"total_characters": total_chars,
"estimated_cost_usd": round(total_chars * 0.00003, 4)
}
Summary
- OpenAI TTS offers 6 voices: alloy, echo, fable, onyx, nova, shimmer.
- 2 models:
tts-1($15/1M chars, low latency) andtts-1-hd($30/1M chars, high quality). - 6 output formats: mp3, opus, aac, flac, wav, pcm.
- Speed control with
speed(0.25 to 4.0). - Streaming available for real-time applications.
- 4096-character limit per request — split long text into chunks and concatenate.
- For Spanish,
novaandalloyoffer the best naturalness. - Use
tts-1for development andtts-1-hdfor production.
Additional Resources
- OpenAI TTS Guide — Official guide
- TTS API Reference — Complete reference
- Voice Previews — Listen to the voices
- pydub — To concatenate generated audio