Generating Spoken Responses from Text

Ma Mahalakshmi V Updated 16 Sep 2026
8 min read ·Lesson 111 of 224

Beyond a Single Synthesis Call

Lesson 1 introduced the basic shape of a text-to-speech call: pass text in, get audio bytes back. This lesson goes deeper into the parameters that shape the quality and character of that output, the practical constraints around input length, streaming for responsiveness, and the design decisions involved in choosing how synthesized speech should sound for a given application.

Generating a single short sentence of speech is straightforward. Generating spoken responses as part of a real application — a voice assistant reply, a narrated article, an automated phone system prompt — involves decisions about voice selection, output format, latency, and how to handle text that does not translate cleanly into natural-sounding speech.

The Full Set of Synthesis Parameters

from pathlib import Path
from openai import OpenAI

client = OpenAI()

response = client.audio.speech.create(
    model="gpt-5.6-terra",
    voice="verse",
    input="Thank you for calling. Your ticket number is 4 4 host and a representative will be with you shortly.",
    response_format="mp3",
    speed=1.0,
)

Path("ticket_confirmation.mp3").write_bytes(response.read())

Each parameter shapes the output in a distinct way:

  • voice selects which pre-built synthetic voice speaks the text. Different voices carry different tonal qualities — some sound warmer and more conversational, others more neutral and formal. This is a real product decision: a meditation app and a fraud-alert notification system should probably not use the same voice.
  • response_format controls the audio container format of the output (common options include MP3, and other formats optimized for different playback or streaming needs). MP3 is broadly compatible and a reasonable default for most applications; formats optimized for low-latency streaming may be preferable for real-time playback scenarios.
  • speed adjusts the playback rate of the generated speech, typically as a multiplier where 1.0 is normal speed. Values below 1.0 slow speech down (useful for accessibility or language-learning contexts), and values above 1.0 speed it up.

Note: The exact list of available voices, supported response_format values, and the valid range for speed are details that can change as the API evolves. Confirm the current set of options against official documentation before hardcoding a specific voice name or speed value into a production system, since a deprecated voice name would cause requests to fail.

Handling Text That Does Not Read Naturally

Raw application text is often not written with speech in mind. Numbers, abbreviations, and symbols that read fine visually can sound awkward or ambiguous when spoken aloud, and a synthesis model can only work with the text it is given — it has no independent knowledge of what a given abbreviation is supposed to mean in your specific context.

import re


def prepare_text_for_speech(raw_text: str) -> str:
    text = raw_text

    # Expand common abbreviations that read poorly as speech.
    replacements = {
        r"\bDr\.": "Doctor",
        r"\bMr\.": "Mister",
        r"\bMrs\.": "Missus",
        r"\be\.g\.": "for example",
        r"\bi\.e\.": "that is",
    }
    for pattern, replacement in replacements.items():
        text = re.sub(pattern, replacement, text)

    # Collapse repeated whitespace left over from formatting.
    text = re.sub(r"\s+", " ", text).strip()

    return text


sample = "Dr. Alvarez will see you shortly, e.g. within 10 minutes."
print(prepare_text_for_speech(sample))

This function applies a small set of regular-expression substitutions to expand common abbreviations into their spoken-word form before the text reaches the synthesis call. Each pattern uses \b (a word boundary) to avoid accidentally matching the abbreviation as part of a longer word, and the replacement dictionary is intentionally small and explicit rather than attempting to handle every possible case — a general-purpose text normalizer is a much larger undertaking, and most applications only need to handle a known, limited set of abbreviations that actually appear in their content. The final whitespace-collapsing step cleans up any formatting artifacts (like double spaces from concatenated strings) that can otherwise cause subtly awkward pauses in the synthesized audio.

This kind of preprocessing is optional for many applications — the synthesis model already handles a great deal of natural text reasonably well — but it becomes valuable for content with a lot of domain-specific abbreviations, or for applications where mispronunciations are especially noticeable or embarrassing, such as a professional voice assistant or automated customer service line.

Handling Long Input Text

The synthesis endpoint has an input length limit, and a long article or document will exceed it in a single call. The correct approach is to split the text into chunks at natural boundaries (sentence or paragraph breaks) and synthesize each chunk separately, then combine the resulting audio if a single continuous file is needed.

def split_text_for_synthesis(text: str, max_chars: int = 3000) -> list[str]:
    """
    Splits text into chunks no longer than max_chars, breaking at sentence
    boundaries where possible to avoid cutting off mid-sentence.
    """
    sentences = re.split(r"(?<=[.!?])\s+", text.strip())
    chunks = []
    current_chunk = ""

    for sentence in sentences:
        candidate = f"{current_chunk} {sentence}".strip() if current_chunk else sentence
        if len(candidate) > max_chars and current_chunk:
            chunks.append(current_chunk)
            current_chunk = sentence
        else:
            current_chunk = candidate

    if current_chunk:
        chunks.append(current_chunk)

    return chunks

re.split(r"(?<=[.!?])\s+", text.strip()) splits the input into sentences using a lookbehind assertion: it breaks on whitespace that immediately follows a sentence-ending punctuation mark (., !, or ?), which keeps the punctuation attached to the sentence it belongs to rather than stripping it out. The function then greedily accumulates sentences into current_chunk until adding the next sentence would exceed max_chars, at which point it closes out the current chunk and starts a new one. This is the same greedy-accumulation pattern used for paragraph grouping in Lesson 5, applied here to text splitting instead of segment grouping — recognizing this kind of reusable pattern across different problems is a useful skill as you write more of these pipelines yourself.

Note: The exact maximum input length accepted by the speech synthesis endpoint should be confirmed against current official documentation; the max_chars value used here is illustrative and conservative, not a guaranteed API limit.

Synthesizing each chunk and concatenating the resulting audio requires either concatenating raw audio bytes (which works cleanly for some formats but can introduce artifacts for others) or using an audio-processing library to properly join clips. For most applications, a simpler and more robust approach is to synthesize and deliver each chunk separately — for example, playing them back-to-back in a client-side player — rather than attempting byte-level concatenation of compressed audio formats.

Streaming Synthesized Audio for Lower Perceived Latency

For interactive applications, waiting for an entire audio file to be generated before playback starts creates a noticeable delay, especially for longer responses. Streaming the response allows playback to begin as soon as the first chunk of audio data arrives, rather than waiting for the complete file.

from openai import OpenAI

client = OpenAI()


def synthesize_to_file_streaming(text: str, output_path: str, voice: str = "alloy") -> None:
    with client.audio.speech.with_streaming_response.create(
        model="gpt-5.6-terra",
        voice=voice,
        input=text,
    ) as response:
        response.stream_to_file(output_path)

with_streaming_response requests a streaming-capable response object rather than one that buffers the entire result before returning. Using it inside a with block ensures the underlying network connection is properly closed once streaming completes, even if an error occurs partway through — this is the same resource-management pattern as using with open(...) for files, applied to a network response instead. response.stream_to_file(output_path) writes the incoming audio data to disk incrementally as it arrives, rather than accumulating it all in memory first. For a real-time playback scenario (rather than writing to a file), the streaming response object typically also supports iterating over the incoming bytes directly, which can be piped to an audio playback device as data arrives.

Note: The exact streaming interface (with_streaming_response, its supported helper methods like stream_to_file, and how to access raw streamed bytes for live playback) is SDK-version-specific. Confirm the current recommended streaming pattern in the official SDK documentation before building latency-sensitive playback features around a specific method name.

When Streaming Matters and When It Does Not

Streaming is valuable when a human is waiting in real time for audio to start playing — a voice assistant response, a live phone system prompt. It is not necessary, and adds unneeded complexity, for batch use cases where the audio is generated ahead of time and stored for later playback, such as pre-generating narration for a video that will not be played until well after synthesis completes. Choosing streaming by default for every use case adds code complexity without a corresponding user-facing benefit in non-interactive contexts.

Common Mistakes

Sending raw, unedited application text directly to synthesis without considering how it will sound spoken aloud, which causes awkward or confusing audio output for text full of abbreviations, symbols, or formatting artifacts intended for visual reading. Light text preprocessing, as shown above, meaningfully improves output quality for such content.

Attempting to synthesize very long text in a single call, which fails once the input exceeds the endpoint's length limit. Split long text into sentence-bounded chunks proactively, rather than discovering the limit through a failed request in production.

Using a blocking, non-streaming call for a latency-sensitive interactive feature, which causes a noticeable and avoidable delay before any audio plays. Use the streaming response pattern for any scenario where a user is waiting in real time.

Best Practices

Pick a voice deliberately based on the application's context and audience, and keep it consistent across the application rather than switching voices between different features, which can feel jarring to users.

Chunk long text at sentence boundaries, not at arbitrary character counts. Splitting mid-sentence produces audio with an unnatural break, while sentence-aware splitting (as shown with the lookbehind-based regular expression) preserves natural speech rhythm across chunk boundaries.

Cache synthesized audio for text that does not change often, such as fixed prompts in an IVR system or standard notification messages. Since synthesis has a real cost and a small amount of latency, regenerating the same audio for identical text repeatedly is wasted work that caching easily eliminates.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Generating Spoken Responses from Text and get answers drawn from it.

Signed-in readers only.