DEV Community

Harpreet Singh Seehra
Harpreet Singh Seehra

Posted on

Build an AI Audiobook Narrator with Telnyx AI Inference, TTS, and Cloud Storage

Turning a manuscript into a narrated audiobook traditionally means hiring a voice actor, booking studio time, and accepting that revisions cost hours of re-recording. This walkthrough builds a Flask app that takes raw text, uses AI Inference to chunk it into chapters with pacing and emotion cues, narrates each chapter with text-to-speech, and stores the resulting MP3s in Telnyx Cloud Storage with presigned URLs for playback.

The canonical code example lives in the Telnyx code examples repo:

https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-audiobook-narrator-python

What This Example Builds

A single-file Flask app with five endpoints:

Method Path Purpose
POST /books/narrate Submit text for audiobook narration (the main pipeline)
GET /books/<book_id> Retrieve a specific book and its chapter audio URLs
GET /books List all books processed so far
GET /voices List available narrator voices
GET /health Service health check

The pipeline runs in three stages. AI Inference splits the submitted text into chapters and adds narration cues (tone, pacing, emphasis). Text-to-speech renders each chapter as an MP3 using a consistent voice. Cloud Storage holds the final audio and returns a time-limited presigned GET URL for each chapter so callers can stream the result without handling long-lived credentials.

Why This Matters

The audiobook narrator exercises three Telnyx AI products in a single request flow — AI Inference, TTS, and Cloud Storage — and returns a tangible artifact (an actual MP3 file) instead of a text completion. That makes it a useful reference for anyone building content-generation pipelines where the output is media, not text.

Developers use this pattern as the foundation for:

  • Self-publishing tools — Convert manuscripts to audiobooks for distribution platforms that require audio
  • Accessibility services — Turn written content into audio for visually impaired readers
  • Content repurposing — Generate audio versions of blog posts, documentation, or newsletters
  • Podcast pre-production — Draft narration tracks that a human host can later rerecord or remix
  • Multi-language narration — Run the same text through different TTS voices and languages for localized audio

Products Used

telnyx_products: [AI Inference, TTS, Cloud Storage]
Enter fullscreen mode Exit fullscreen mode

Telnyx AI Inference exposes an OpenAI-compatible API for chat completions. Telnyx TTS generates speech from text via POST /v2/ai/generate. Telnyx Cloud Storage is S3-compatible, so the AWS SDK (boto3) talks to it directly — the Telnyx API key is used as both the access key and the secret key, and the endpoint host is region-scoped (https://{region}.telnyxcloudstorage.com).

Architecture

POST /books/narrate
  |
  v
+----------------------+
|  AI Inference         |  chunks text into chapters,
|  /v2/ai/chat/         |  adds tone + pacing cues
|  completions          |
+----------+-----------+
           |
           v
+----------------------+
|  TTS Generate         |  renders each chapter as MP3
|  /v2/ai/generate      |  with a consistent voice
+----------+-----------+
           |
           v
+----------------------+
|  Cloud Storage (S3)   |  PutObject per chapter,
|  boto3 put_object     |  presigned GET URL returned
+----------+-----------+
           |
           +---> JSON response with storage_urls[]
Enter fullscreen mode Exit fullscreen mode

The Code

The full app is 242 lines in a single app.py. The key pieces:

Configuration and storage client. The Telnyx API key is loaded from the environment and used for both the REST API (Inference, TTS) and the S3 client (Cloud Storage). The S3 client is pointed at the region-scoped Telnyx endpoint with s3v4 signing:

import os, boto3
from botocore.config import Config

TELNYX_API_KEY = os.getenv("TELNYX_API_KEY")
REGION = os.getenv("TELNYX_STORAGE_REGION", "us-central-1")

s3 = boto3.client(
    "s3",
    endpoint_url=f"https://{REGION}.telnyxcloudstorage.com",
    aws_access_key_id=TELNYX_API_KEY,
    aws_secret_access_key=TELNYX_API_KEY,
    region_name=REGION,
    config=Config(signature_version="s3v4"),
)
Enter fullscreen mode Exit fullscreen mode

AI Inference call. A system prompt instructs the model to break the text into chapters and return a JSON array with chapter_number, chapter_title, narration_text, tone, and pacing fields. The response is parsed as JSON, with a fallback to paragraph splitting if the model returns malformed JSON:

def inference(messages, max_tokens=4000):
    resp = requests.post(f"{API}/ai/chat/completions", headers=HEADERS, json={
        "model": AI_MODEL, "messages": messages,
        "max_tokens": max_tokens, "temperature": 0.3
    }, timeout=60)
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]
Enter fullscreen mode Exit fullscreen mode

TTS generation. Each chapter's narration text is sent to POST /v2/ai/generate with the chosen voice and output_format: mp3. The response body is the raw MP3 bytes:

def tts_generate(text, voice=None):
    resp = requests.post(f"{API}/ai/generate", headers=HEADERS, json={
        "model": TTS_MODEL, "voice": voice or DEFAULT_VOICE,
        "text": text, "output_format": "mp3"
    }, timeout=60)
    resp.raise_for_status()
    return resp.content
Enter fullscreen mode Exit fullscreen mode

Upload to Cloud Storage. Each chapter MP3 is uploaded with put_object, then a presigned GET URL valid for one hour is returned so the caller can stream the audio without exposing long-lived credentials:

def upload_to_storage(bucket, key, data, content_type="audio/mpeg"):
    s3.put_object(Bucket=bucket, Key=key, Body=data, ContentType=content_type)
    return s3.generate_presigned_url(
        "get_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=3600
    )
Enter fullscreen mode Exit fullscreen mode

The narrate endpoint. Wires the three stages together, with graceful fallback if the AI chunking returns malformed JSON (splits the text by paragraphs instead):

@app.route("/books/narrate", methods=["POST"])
def narrate_book():
    data = request.get_json() or {}
    if not data:
        return jsonify({"error": "invalid request body"}), 400
    title = data.get("title", "Untitled")
    text = data.get("text", "")
    voice = data.get("voice", DEFAULT_VOICE)

    if not text:
        return jsonify({"error": "Provide 'text' to narrate"}), 400

    book_id = f"book-{uuid.uuid4().hex[:8]}"
    # Step 1: AI chunks text into chapters with narration cues
    try:
        chunking_result = inference([
            {"role": "system", "content": "You are an audiobook production assistant..."},
            {"role": "user", "content": text[:15000]}
        ], max_tokens=4000)
        chapters = json.loads(chunking_result)
    except json.JSONDecodeError:
        # Fallback: split by paragraphs
        paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
        chapters = [{
            "chapter_number": i + 1,
            "chapter_title": f"Chapter {i + 1}",
            "narration_text": chunk,
            "tone": "warm", "pacing": "moderate"
        } for i, chunk in enumerate(paragraphs)]

    # Step 2 + 3: TTS per chapter → Cloud Storage
    for chapter in chapters:
        audio = tts_generate(chapter["narration_text"], voice=voice)
        key = f"{book_id}/chapter-{chapter['chapter_number']:02d}.mp3"
        url = upload_to_storage(BUCKET_NAME, key, audio)
        books[book_id]["storage_urls"].append(url)

    return jsonify({
        "book_id": book_id, "title": title, "status": "complete",
        "chapters": len(books[book_id]["chapters"]),
        "storage_urls": books[book_id]["storage_urls"]
    }), 201
Enter fullscreen mode Exit fullscreen mode

Run It

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-audiobook-narrator-python
cp .env.example .env  # add TELNYX_API_KEY, optionally tune AI_MODEL, TTS_MODEL, BUCKET_NAME
pip install -r requirements.txt
python app.py
Enter fullscreen mode Exit fullscreen mode

The server starts on http://localhost:5000. Create a bucket named audiobooks in the Telnyx Portal (or set BUCKET_NAME to an existing bucket) and submit a chunk of text:

curl -X POST http://localhost:5000/books/narrate \
  -H "Content-Type: application/json" \
  -d '{
    "title": "The Future of Infrastructure",
    "text": "Chapter 1: The shift from cloud-edge to carrier-edge compute is changing where latency-sensitive workloads run...",
    "voice": "nova"
  }'
Enter fullscreen mode Exit fullscreen mode

The response includes a book_id and a list of presigned storage_urls, one per chapter:

{
  "book_id": "book-a1b2c3d4",
  "title": "The Future of Infrastructure",
  "status": "complete",
  "chapters": 3,
  "total_audio_mb": 2.1,
  "voice": "nova",
  "storage_urls": [
    "https://us-central-1.telnyxcloudstorage.com/audiobooks/book-a1b2c3d4/chapter-01.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&...",
    "https://us-central-1.telnyxcloudstorage.com/audiobooks/book-a1b2c3d4/chapter-02.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&...",
    "https://us-central-1.telnyxcloudstorage.com/audiobooks/book-a1b2c3d4/chapter-03.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&..."
  ]
}
Enter fullscreen mode Exit fullscreen mode

Open any storage_url in a browser or stream it from a client to hear the narrated chapter.

Available Voices

The app exposes five narrator voices out of the box:

Key Voice Character
warm_female nova Warm, expressive — default narrator
deep_male onyx Deep, authoritative
neutral_male echo Neutral, measured
bright_female shimmer Bright, energetic
calm_neutral alloy Calm, even-tempered

List them at any time:

curl http://localhost:5000/voices
Enter fullscreen mode Exit fullscreen mode

Pass any voice identifier in the voice field of the /books/narrate request.

Going to Production

This example uses an in-memory dict for book metadata, which is fine for local testing but loses state on restart. For production:

  • Persistent storage — Replace the in-memory books dict with PostgreSQL or Redis so book metadata survives restarts
  • Authentication — Add API key validation on every endpoint; the example has none
  • Bucket provisioning — Ensure the BUCKET_NAME exists in your Telnyx Cloud Storage region before the first request, or add a create_bucket call on startup
  • Long-form input — The chunking step truncates input to 15,000 characters. For full manuscripts, split the source text on the client and submit each section as a separate narrate request
  • Presigned URL lifetime — URLs expire after one hour. For longer-lived access, generate new presigned URLs on demand from the stored object keys, or set the bucket to public read for public-domain content
  • Webhook signature verification — If you expose this app via a public URL, validate incoming request signatures (not required for the local-only flow shown here)
  • Monitoring — Add structured logging and alert on failed inference, tts_generate, or upload_to_storage calls so you catch upstream issues early

Frequently Asked Questions

Do I need an existing Cloud Storage bucket before running this? Yes. Create a bucket in the Telnyx Portal and set BUCKET_NAME in .env to match. The app does not create the bucket for you.

What happens if the AI returns malformed JSON for the chapter split? The app catches json.JSONDecodeError and falls back to splitting the text by double-newline paragraphs. Each paragraph becomes a chapter with default tone: warm and pacing: moderate. Narration still completes — you just lose the AI's tone and pacing direction.

Which AI model is used by default? The default is moonshotai/Kimi-K2.6 (set in .env.example and overridable via AI_MODEL). Any model available on Telnyx AI Inference works — see the models list for options.

Can I use a different TTS voice per chapter? Not in this example — the voice is set once per request and used for every chapter. To vary voices per chapter, extend the narrate_book loop to read chapter["voice"] from the AI's response and pass it to tts_generate.

How long do the presigned URLs last? One hour (ExpiresIn=3600). The stored MP3s persist in the bucket; only the URL expires. Generate a fresh presigned URL with s3.generate_presigned_url whenever you need a new one.

Is the uploaded audio public? No. The bucket is private by default. Only the presigned URL grants time-limited read access. To make audio publicly accessible (e.g., for public-domain audiobooks), set a public read policy on the bucket in the Telnyx Portal.

What is the maximum input text length? The chunking step truncates input to 15,000 characters per request. For longer manuscripts, split the source into sections on the client and submit each as a separate /books/narrate request, then stitch the resulting storage_urls together.

Can I narrate in languages other than English? Yes — Telnyx TTS supports multiple languages. The AI Inference prompt is in English, but you can edit the system message in app.py to chunk in another language, and pass a language-appropriate voice to tts_generate.

Resources

Top comments (0)