New AssemblyAI-TTS-1: Text-to-speech for voice agents. Learn more
Insights & Use Cases

How to generate speech with Fish Audio: API, Python SDK, and self-hosted S2-Pro

A hands-on guide to the Fish Audio API and Python SDK: first clip, voices, emotion tags, cloning and streaming, self-hosting S2-Pro under its license, and how it compares with AssemblyAI-TTS-1.

Abstract green cylinder illustration

Written by

Kelsey Foster

Published on

3 November 2026

Fish Audio does two things that rarely come together: a hosted text-to-speech API, and published weights you can run on your own GPU. That's why "fish audio api" and "fish speech" searches both land in the same place, and why most guides cover only one half.

This one covers both. Like our guide to running OpenAI's Whisper, it walks through the hosted API first, then running the model yourself, then the limits you'll hit in production. At the end, you'll generate the same speech with AssemblyAI-TTS-1 for comparison.

By the end you'll have:

  • Your first Fish Audio clip from cURL and from Python
  • Custom voices, emotion tags, voice cloning and streaming working in the Python SDK
  • S2-Pro running locally behind Fish's API server
  • A clear picture of the license, pricing and concurrency limits before you ship

What is Fish Audio?

Fish Audio is a text-to-speech company that sells a hosted API and publishes some of its model weights. Its open-source project, Fish Speech, is on GitHub. Fish also sells speech-to-text and a voice agent platform, but this guide covers text-to-speech only.

Here are the text-to-speech models you can call today, from Fish's models overview and install guide, as of November 3, 2026:

Model ID Status Languages Weights you can download
s2.1-pro Recommended production model, with TTFA and DPA guarantees 83 No
s2.1-pro-free Same model as s2.1-pro for development and testing, under fair-use limits, with no TTFA or DPA guarantees 83 No
s2-pro Previous S2 generation 80+ Yes, under the non-commercial Fish Audio Research License
drama-3 A model you direct in plain language, built for content creation See Fish's docs Yes, under the Fish Audio Community License
s1 Deprecated; retires December 31, 2026 13 No

A few things in that table matter later. Fish bills text-to-speech per UTF-8 byte, not per character: S2.1 Pro lists at $15 per 1M UTF-8 bytes and Drama 3 at $50 per 1M bytes, so accented characters in Spanish, French, German or Portuguese text count as two. Language detection is automatic on S2 models. And after December 31, 2026, requests that ask for s1 are served by s2.1-pro.

How to generate speech with the Fish Audio API

Prerequisites

  • Python 3.9 or later (for the SDK)
  • A Fish Audio account
  • cURL, if you want to try the raw API first

Step 1: Get a Fish Audio API key

Sign in to Fish Audio, open the developer Get Started page and click "Get your API key". The key is created in one click and shown only once, so copy it straight into an environment variable:

export FISH_API_KEY="replace_me"

Step 2: Install the Fish Audio Python SDK

pip install fish-audio-sdk

The package is fish-audio-sdk, and you import it as fishaudio. If you used an older Fish SDK, note that the version numbering restarted at 1.0.0 (the last old version was 2025.6.3), and the legacy fish_audio_sdk import is no longer updated. Pin fish-audio-sdk>=1.0.0.

Step 3: Generate your first clip

The examples use s2.1-pro-free, the model Fish's quickstart uses so its examples run on a new account before you add API credit.

cURL

curl -X POST https://api.fish.audio/v1/tts \
  -H "Authorization: Bearer $FISH_API_KEY" \
  -H "Content-Type: application/json" \
  -H "model: s2.1-pro-free" \
  -d '{
    "text": "Hello! Welcome to Fish Audio. This is my first AI-generated voice.",
    "format": "mp3"
  }' \
  --output welcome.mp3

The model goes in a model HTTP header, not in the JSON body. The response is the audio itself, saved as welcome.mp3. Play it with afplay welcome.mp3 on macOS or mpg123 welcome.mp3 on Linux.

Python

from fishaudio import FishAudio
from fishaudio.utils import save

client = FishAudio()  # reads FISH_API_KEY from the environment

audio = client.tts.convert(
    text="Hello! Welcome to Fish Audio. This is my first AI-generated voice.",
    model="s2.1-pro-free",
)
save(audio, "welcome.mp3")
print("Saved welcome.mp3")

Run it and you'll see Saved welcome.mp3. convert() returns the whole clip as bytes, MP3 by default.

Two notes on model=. Always pass it: without it, the SDK uses s2-pro, which needs API credit. And if your type checker flags "s2.1-pro-free", that's because the SDK's type hint (version 1.3.0) only lists older model names. The SDK sends whatever string you pass as the model header.

If your first request fails

From Fish's quickstart:

Status Likely cause Fix
401 Invalid or missing API key Check FISH_API_KEY
402 No API credit for the model you called On a new account, use s2.1-pro-free; for other models, add API credit
400 Bad reference_id or parameters Check the voice ID and read the error message
429 Rate limit (concurrency) Retry with exponential backoff; there's no Retry-After header

Fish Audio advanced usage

Pick a voice with reference_id

Every voice in Fish's library has an ID in its URL. Pass it as reference_id:

from fishaudio import FishAudio
from fishaudio.utils import save

client = FishAudio()

audio = client.tts.convert(
    text="Thanks for calling. Your order shipped this morning.",
    reference_id="9a9cf47702da476aa4629e2506d4a857",  # "Energetic Male" from the Fish docs
    model="s2.1-pro-free",
)
save(audio, "custom_voice.mp3")

Reuse settings with TTSConfig and Prosody

When several requests share a voice, format and speed, put them in one TTSConfig:

from fishaudio import FishAudio
from fishaudio.types import Prosody, TTSConfig
from fishaudio.utils import save

client = FishAudio()

config = TTSConfig(
    prosody=Prosody(speed=1.1, volume=0),
    reference_id="9a9cf47702da476aa4629e2506d4a857",
    format="wav",
    latency="balanced",
)

greeting = client.tts.convert(text="Hi, you've reached the help desk.", config=config, model="s2.1-pro-free")
followup = client.tts.convert(text="Can I get your account number?", config=config, model="s2.1-pro-free")
save(greeting, "greeting.wav")
save(followup, "followup.wav")

format takes wav, pcm, mp3 or opus, and the SDK's latency takes normal or balanced. WAV and PCM can be 8, 16, 24, 32 or 44.1 kHz; MP3 is 32 or 44.1 kHz; Opus is 48 kHz.

Add emotion and style with bracket tags

S2 models read free-form cues in square brackets anywhere in the text. Fish's examples include [whisper], [laugh], [sigh], [gasp], [excited] and descriptive ones like [laughing nervously]:

from fishaudio import FishAudio
from fishaudio.utils import save

client = FishAudio()

audio = client.tts.convert(
    text="I can't believe it [gasp] you actually did it [laugh]",
    model="s2.1-pro-free",
)
save(audio, "emotion.mp3")

If you're migrating from S1, its (parenthesis) tags aren't read by S2 models, so convert them to brackets.

Clone a voice

Fish clones voices two ways. Fish's voice cloning guide asks for at least 10 seconds per clip of clean, single-speaker audio, as WAV, MP3, M4A or Opus. Only clone voices you have the speaker's consent to use.

An instant clone sends reference audio and its transcript with each request, so there's no saved model:

from fishaudio import FishAudio
from fishaudio.types import ReferenceAudio
from fishaudio.utils import save

client = FishAudio()

with open("reference.wav", "rb") as f:
    audio = client.tts.convert(
        text="This will sound like the reference voice.",
        references=[
            ReferenceAudio(
                audio=f.read(),
                text="The exact words spoken in the reference clip.",
            )
        ],
        model="s2.1-pro-free",
    )
save(audio, "instant_clone.mp3")

A persistent clone trains once and gives you a reusable ID:

from fishaudio import FishAudio
from fishaudio.utils import save

client = FishAudio()

with open("sample.wav", "rb") as f:
    voice = client.voices.create(
        title="Support line voice",
        voices=[f.read()],
        description="Cloned from a consented studio sample",
        visibility="private",
    )

print(voice.id, voice.state)  # "trained" in most cases with the default fast mode

audio = client.tts.convert(
    text="Now I speak in my cloned voice.",
    reference_id=voice.id,
    model="s2.1-pro-free",  # without model=, the SDK uses s2-pro (402 on a new account)
)
save(audio, "cloned.mp3")

That last model= line matters. Fish's own cloning example leaves it out, and on a new account that returns a 402.

Stream audio over HTTP and WebSocket

For text you already have, tts.stream() yields chunks as they're generated, so playback can start before the clip is finished:

from fishaudio import FishAudio

client = FishAudio()

with open("stream.mp3", "wb") as f:
    for chunk in client.tts.stream(
        text="Streaming keeps time to first audio low.",
        model="s2.1-pro-free",
    ):
        f.write(chunk)  # or forward each chunk to a speaker or socket

For text that's still being written, such as an LLM reply, tts.stream_websocket() takes a generator, sends text as it arrives and yields audio back. This is the worked example most voice apps need:

from fishaudio import FishAudio

client = FishAudio()


def llm_tokens():
    # Replace with your real streaming LLM call
    for token in ["Your ", "table ", "for ", "four ", "is ", "booked ", "for ", "7pm."]:
        yield token


with open("agent_turn.mp3", "wb") as f:
    for chunk in client.tts.stream_websocket(
        llm_tokens(),
        reference_id="9a9cf47702da476aa4629e2506d4a857",
        latency="balanced",
        model="s2.1-pro-free",
    ):
        f.write(chunk)

Here's what each piece does:

  1. llm_tokens() stands in for your LLM's streaming response. Each yield is a token or a few words.
  2. stream_websocket() opens a WebSocket to Fish, forwards tokens as they arrive and buffers them until it has enough context to sound natural. You don't batch tokens yourself.
  3. The loop receives MP3 chunks while the LLM is still generating. In a real app, you'd send each chunk to a speaker or a socket instead of a file.
  4. Ending a turn: when you need a clean break, such as the end of a sentence, Fish's realtime streaming docs show yielding a FlushEvent to force generation right away.

Handle errors

The SDK raises typed exceptions, so you can treat a bad key, a rate limit and a missing voice differently:

from fishaudio import FishAudio
from fishaudio.exceptions import (
    APIError,
    AuthenticationError,
    FishAudioError,
    NotFoundError,
    RateLimitError,
)

client = FishAudio()

try:
    audio = client.tts.convert(text="Hello!", model="s2.1-pro-free")
except AuthenticationError:
    print("Invalid API key")
except RateLimitError:
    print("Rate limit exceeded: retry with exponential backoff")
except NotFoundError:
    print("Voice model not found")
except APIError as e:
    print(f"API error {e.status}: {e.message}")
except FishAudioError as e:
    print(f"SDK error: {e}")

How to run Fish Speech S2-Pro locally

Read the licenses before you download anything. As of November 3, 2026:

  • The S2-Pro weights are non-commercial only. The S2-Pro model card puts them under the Fish Audio Research License; commercial use needs a separate license from Fish.
  • The code is under the Fish Audio Community License v1.0 (LICENSE, updated October 13, 2026). Commercial use is free only while your annual revenue, counting affiliates, is under $500,000.
  • It can't be offered as a hosted service without a separate written license, at any revenue. The license defines that as letting third parties generate speech or clone voices from text or samples they supply, through your API, SDK, app or site.
  • You must also display "Built with Fish Audio" when you ship it, and you can't train other speech or voice models on its outputs.

In practice, self-hosted S2-Pro is for research, evaluation and personal projects. Anything commercial goes through Fish.

Requirements

From Fish's install guide:

Model GPU memory Hardware OS
S2-Pro At least 24 GB NVIDIA GPU or CPU Linux or WSL
Drama 3 At least 48 GB (FP8) NVIDIA GPU only Linux or WSL

Install and download the weights

The install commands are Fish's own, from the install and inference docs. First, the system audio libraries:

apt install portaudio19-dev libsox-dev ffmpeg

Clone the fish-speech repository and move into it:

git clone https://github.com/fishaudio/fish-speech.git
cd fish-speech

Create a Conda environment and install with GPU support:

conda create -n fish-speech python=3.12
conda activate fish-speech
# GPU installation (choose your CUDA version: cu126, cu128, cu129)
# Replace both "cu129" values when using cu126 or cu128.
pip install --extra-index-url https://download.pytorch.org/whl/cu129 -e ".[cu129]"

Then download the S2-Pro weights with the Hugging Face CLI. The model is gated: you accept its non-commercial terms on Hugging Face first:

pip install -U "huggingface_hub[cli]"
hf download fishaudio/s2-pro --local-dir checkpoints/s2-pro

Run the API server

Start S2-Pro behind Fish's local API server:

python tools/api_server.py \
    --listen 0.0.0.0:8080 \
    --llama-checkpoint-path checkpoints/s2-pro \
    --decoder-checkpoint-path checkpoints/s2-pro/codec.pth \
    --decoder-config-name modded_dac_vq

Check that it's up. A response of {"status":"ok"} means it's ready:

curl -X GET http://127.0.0.1:8080/v1/health

Then send a request with the bundled client:

python tools/api_client.py \
    --url http://127.0.0.1:8080/v1/tts \
    --text "Hello from Fish Speech" \
    --output demo

The client plays the audio when it finishes; add --no-play on a machine without an audio device. The local server's /v1/tts takes a MessagePack body, not JSON, and has no per-request model field: it serves whichever weights it loaded at startup. Fish also documents a Docker image and a three-step command-line pipeline in its inference docs.

Fish Audio limitations

Here's what to know before you put Fish Audio into production:

  • License: the S2-Pro weights are non-commercial. The code is free for commercial use only under $500,000 in annual revenue, and never as a hosted TTS or cloning service.
  • Concurrency is tied to prepaid spend: 5 concurrent requests until you've prepaid $100, 15 until $1,000 and 50 after that, with higher limits by enterprise contract. The limit is shared across every API key on the account, and past it you get a 429 without a Retry-After header (pricing and rate limits).
  • Billing is per UTF-8 byte: accented characters in Spanish, French, German or Portuguese text count as two.
  • No telephony encodings: output formats are wav, pcm, mp3 and opus on both the HTTP API and the WebSocket. For phone calls, you convert 8 kHz PCM to μ-law or A-law yourself.
  • Model selection fails silently: Fish's API reference says a missing or unrecognized model header falls back to s2.1-pro instead of returning an error. And the Python SDK defaults to s2-pro when you leave out model=.
  • S1 retires December 31, 2026: after that, s1 requests run on s2.1-pro.
  • Self-hosting costs: a 24 GB GPU per S2-Pro instance, plus engineering time for scaling, monitoring and model updates. The local API server serves whichever weights it loaded at startup, so failover and autoscaling are yours to build.

When to use Fish Audio vs a managed voice-agent TTS API

Here's how the hosted Fish Audio API and AssemblyAI-TTS-1 compare, from Fish Audio vs AssemblyAI, with independent benchmarks as of October 28, 2026:

Fish Audio S2.1 Pro AssemblyAI-TTS-1
Built for Creators Voice applications and production workloads
Streaming time to first audio (Cekura) 321 ms 159 ms
Codes and addresses read correctly (Cekura) 86% 92%
Concurrency 5 to 50 by prepaid tier, custom by contract No hard caps, no commitment
Data residency US by default; inference in US and Tokyo US or EU, same price
Uptime 99% SLA with premium support 99.9%
Voice cloning Yes Instant, at the same per-character price
Emotion control Yes Coming soon
Price $15 per 1M UTF-8 bytes $15 per 1M characters

So the split is fairly clean. If you need emotion control today, Fish has it, and creator content is what it's built for. If you're running a voice agent on phone lines, first audio, getting codes and addresses right, and concurrency without prepaid tiers matter more, and that's where a managed voice-agent TTS API earns its place. For a wider field, see our roundup of text-to-speech APIs.

Hear AssemblyAI-TTS-1 next to Fish

Paste the same lines you just generated with Fish and compare voices in your browser.

Try playground

How to generate the same speech with AssemblyAI-TTS-1

AssemblyAI-TTS-1 is a text-to-speech model built for voice agents on live calls. It's trained on millions of hours of audio, including real conversational data. It runs with unlimited concurrency and no prepaid tiers or contracts, and it costs $15 per 1M characters, with every voice, instant voice cloning, and US or EU processing included (see pricing).

On the independent Cekura text-to-speech benchmark, as of October 28, 2026, AssemblyAI-TTS-1 starts streaming audio in 159 ms against 321 ms for Fish Audio S2.1 Pro, and reads 92% of codes and addresses correctly against 86%.

It covers six languages (English, Spanish, French, German, Italian and Portuguese) with thousands of voices. You clone a voice in the dashboard from ten to thirty seconds of speech, with the speaker's consent, and then use its ID like any other voice.

Install the SDK. Text-to-speech is in the Python SDK from release 1.8.2:

pip install -U assemblyai

Here's the same "first clip" task, saved as WAV:

import os

import assemblyai as aai

tts = aai.TextToSpeech(api_key=os.environ["ASSEMBLYAI_API_KEY"])

response = tts.synthesize(
    "Hi, thanks for calling. How can I help you today?",
    aai.TTSConfig(
        voice_id="49c443a4-8a1d-5f7e-b70b-98bdbd9823c3",  # Audrey
        container=aai.TTSContainer.wav,
    ),
)

with open("aai_hello.wav", "wb") as f:
    f.write(response.audio)

print(f"voice {response.voice_id}, model {response.model}, "
      f"{response.sample_rate} Hz {response.encoding}")

The voice, model, encoding and sample rate it ran with come back on the response. In the Python SDK, TTSContainer is raw or wav.

For a voice agent, open one session per conversation and speak each turn over it:

import os

import assemblyai as aai

config = aai.TTSConfig(
    voice_id="49c443a4-8a1d-5f7e-b70b-98bdbd9823c3",  # Audrey
    language="english",
)

with aai.TTSSession(config, api_key=os.environ["ASSEMBLYAI_API_KEY"]) as session:
    with open("turn.pcm", "wb") as f:
        for chunk in session.speak("Your confirmation code is A B 7 4 2."):  # raw 24 kHz 16-bit mono PCM
            f.write(chunk)

For phone calls, set encoding to pcm_mulaw or pcm_alaw with sample_rate=8000, the 8 kHz μ-law or A-law that telephony integrations ask for. The TTS quickstart covers the HTTP and WebSocket APIs, and our Python text-to-speech guide covers streaming LLM text into a session. To hear the agent and transcribe the caller on one API key, pair it with Universal-3.6 Pro Realtime.

Final words

Fish Audio is easy to start with: one header, a model that runs on a new account and a clean Python SDK. The questions come later. What does your revenue have to do with whether you can self-host? How many calls can speak at once?

Answer those before you build, not after. If your answers point to creator content and expressive delivery, Fish is a reasonable pick. If they point to phone calls, bursty traffic and an agent that has to read confirmation codes right, run the same script through AssemblyAI-TTS-1 and measure both. Our guide to TTS latency explains what to time.

Generate your first AssemblyAI-TTS-1 clip

Get an API key and run the same two snippets from this guide in a few minutes.

Sign up free

Frequently Asked Questions (FAQs)

Does Fish Audio have an API?

Yes. Fish Audio has a REST API at api.fish.audio with an HTTP endpoint for whole clips, a WebSocket for streaming text, and official Python and JavaScript SDKs. You choose the model with a model HTTP header, and Fish's quickstart uses s2.1-pro-free so its examples run on a new account.

Can I use Fish Audio commercially?

The hosted API is Fish's production route: s2.1-pro is its recommended production model. Self-hosting is narrower. The Fish Speech code is under the Fish Audio Community License v1.0, which allows free commercial use only while your annual revenue is under $500,000 and never as a hosted TTS service, and the S2-Pro weights are under the Fish Audio Research License, for non-commercial use only.

How much does the Fish Audio API cost?

Fish Audio S2.1 Pro lists at $15 per 1M UTF-8 bytes, and Drama 3 at $50 per 1M bytes. Because Fish bills per byte, accented characters in Spanish, French, German or Portuguese text count as two. AssemblyAI-TTS-1 is $15 per 1M characters, with every voice, instant voice cloning, and US or EU processing included.

Is Fish Audio open source?

Not under a standard open-source license. The Fish Speech code is under the Fish Audio Community License v1.0, which limits free commercial use to companies under $500,000 in annual revenue and bans hosted services. The S2-Pro weights are under a research license for non-commercial use only.

What GPU do I need to run Fish Speech locally?

S2-Pro needs at least 24 GB of GPU memory on an NVIDIA GPU, or it can run on CPU, on Linux or WSL. Drama 3 needs at least 48 GB (FP8) and runs on NVIDIA GPUs only. Fish's install guide uses Python 3.12 and CUDA 12.6, 12.8 or 12.9 builds of PyTorch.

Why does my Fish Audio request return 402?

A 402 means you called a model your account has no API credit for. It usually happens when you leave out the model: the Python SDK then defaults to s2-pro. Pass model="s2.1-pro-free" (or the model: s2.1-pro-free header in cURL), or add API credit.