Course ES
← back to chapter

Text to speech

Type a line and hear it. The model returns audio, rendered in a player. OpenAI

Showcase — Text to speech

Type a line and hear it. The model returns audio, rendered in a player. OpenAI returns MP3 directly; Gemini returns raw PCM samples that the backend wraps in a WAV container (standard library) before sending a data URI to the page.

Run

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Open http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.

What's where

  • backend/ai_openai.pyaudio.speech.create, MP3 → data URI.
  • backend/ai_gemini.py — Gemini TTS PCM → WAV (wave) → data URI.
  • frontend/app/page.tsx — text box; renders the returned audio in an <audio>.

Stop

docker compose down

Run locally

Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.

Download text-to-speech.zip

unzip text-to-speech.zip
cd text-to-speech
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.


  

The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.

backend/ai_openai.py

"""Showcase 2 (OpenAI): text to speech.

Type a line, hear it. The model returns MP3 bytes, which we base64 into a data
URI the page plays in an <audio> element.
"""
import base64

from openai import OpenAI

_client = OpenAI()

_MODEL = "gpt-4o-mini-tts"


def run(text: str) -> str:
    t = text.strip()
    if not t:
        return "Enter some text to speak."
    response = _client.audio.speech.create(model=_MODEL, voice="alloy", input=t)
    b64 = base64.b64encode(response.content).decode()
    return f"data:audio/mpeg;base64,{b64}"

backend/ai_gemini.py

"""Showcase 2 (Gemini): text to speech.

Gemini TTS returns raw PCM samples, so we wrap them in a WAV container (standard
library) before base64-ing into a data URI the page can play.
"""
import base64
import io
import os
import wave

from google import genai
from google.genai import types

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

_MODEL = "gemini-2.5-flash-preview-tts"


def _pcm_to_wav(pcm: bytes, rate: int = 24000) -> bytes:
    buf = io.BytesIO()
    with wave.open(buf, "wb") as w:
        w.setnchannels(1)
        w.setsampwidth(2)  # 16-bit
        w.setframerate(rate)
        w.writeframes(pcm)
    return buf.getvalue()


def run(text: str) -> str:
    t = text.strip()
    if not t:
        return "Enter some text to speak."
    response = _client.models.generate_content(
        model=_MODEL,
        contents=t,
        config=types.GenerateContentConfig(
            response_modalities=["AUDIO"],
            speech_config=types.SpeechConfig(
                voice_config=types.VoiceConfig(
                    prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore"),
                ),
            ),
        ),
    )
    pcm = response.candidates[0].content.parts[0].inline_data.data
    b64 = base64.b64encode(_pcm_to_wav(pcm)).decode()
    return f"data:audio/wav;base64,{b64}"

Project files

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json