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:

VoicePerceived genderToneRecommended use
alloyNeutralBalanced, versatileGeneral, demos
echoMaleDeep, calmNarration, podcasts
fableMaleWarm, narrativeStories, education
onyxMaleLow, authoritativeFormal presentations
novaFemaleFriendly, energeticAssistants, tutorials
shimmerFemaleSoft, expressiveMeditation, 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

ModelQualityLatencyCostRecommended use
tts-1GoodLow$15 / 1M charactersReal-time applications, prototyping
tts-1-hdHighHigher$30 / 1M charactersPodcasts, 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:

FormatExtensionCompressionQualityRecommended use
mp3.mp3LossyGoodDefault, web distribution
opus.opusLossyGoodStreaming, low latency
aac.aacLossyGoodYouTube, iOS/Android
flac.flacLosslessMaximumArchiving, later editing
wav.wavUncompressedMaximumProcessing, compatibility
pcm.pcmUncompressedMaximumRaw 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:

SpeedValueUse
Very slow0.25Language learning, accessibility
Slow0.5Step-by-step instructions
Slightly slow0.75Detailed content
Normal1.0Default
Slightly fast1.25Summaries
Fast1.5Skimming content
Very fast2.0Quick review
Maximum4.0Upper 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

ModelCost per 1M charactersCost 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

TextApprox. charactersCost (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:

ActionEffect
Use nova or alloyBetter performance in Spanish
Add correct punctuationImproves intonation
Avoid mixing languagesKeep the text in a single language per request
Use tts-1-hdMore 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) and tts-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, nova and alloy offer the best naturalness.
  • Use tts-1 for development and tts-1-hd for production.

Additional Resources

  1. OpenAI TTS Guide — Official guide
  2. TTS API Reference — Complete reference
  3. Voice Previews — Listen to the voices
  4. pydub — To concatenate generated audio