In the previous post I looked at what Fish Audio is as a company and why three of its models are open while the flagship sits behind a paid API. This one is the other half: how you actually wire it into something. Authentication, the endpoint, the Python SDK, both routes to voice cloning, streaming that starts speaking before your language model has finished thinking, and the pricing arithmetic that decides whether any of it is affordable at your volume. Everything below comes from the current documentation rather than from memory.
The short version
- One POST to https://api.fish.audio/v1/tts with a bearer token gets you audio; the model is chosen with a header, not a body field
- The s2.1-pro-free model is the same model as s2.1-pro at zero cost, intended for testing and prototyping, which makes evaluation genuinely free
- Billing is 15 dollars per million UTF-8 bytes, which is roughly 180,000 English words or about 12 hours of speech
- Voice cloning comes in two forms: a persistent reusable voice model, or zero shot references passed inline with a single request
- The websocket mode accepts a generator of text tokens, so you can speak an LLM response as it is produced instead of waiting for it
- Your rate limit is concurrency based and rises with lifetime spend, starting at 5 simultaneous requests
The smallest thing that works
Before installing anything, confirm your key works with a single request. The endpoint is POST https://api.fish.audio/v1/tts, authentication is a standard bearer token, and the response is a chunked audio stream rather than a JSON envelope containing a URL.
# The model is selected with a header, which is unusual enough to trip people up.
# s2.1-pro-free costs nothing, so use it while you are still experimenting.
curl --request POST \
--url https://api.fish.audio/v1/tts \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header 'model: s2.1-pro-free' \
--data '{"text": "Hello! Welcome to Fish Audio."}' \
--output hello.mp3
Two details in that request matter more than they look. The model header is how you pick the engine, so a request that seems to ignore your model choice is usually one where the value was put in the JSON body instead. And the response streams as audio bytes directly, so if you are debugging by printing the body you will get binary noise rather than an error message.
The Python SDK
For anything beyond a smoke test the SDK is less work. One thing to note before you start: the package you install and the module you import are not spelled the same way, which costs people a few confused minutes.
pip install fish-audio-sdk # note the hyphens
export FISH_API_KEY=your_api_key_here
from fishaudio import FishAudio # but the module has no hyphens
from fishaudio.utils import save
client = FishAudio() # reads FISH_API_KEY from the environment
# or be explicit: FishAudio(api_key="...")
# AsyncFishAudio is the asyncio variant
audio = client.tts.convert(text="Hello from Fish Audio!")
save(audio, "out.mp3")
Output format, sample rate and speaking rate are all adjustable. The default format is mp3, which is right for delivery to a browser and wrong for anything you intend to process further, since you want uncompressed frames for that.
from fishaudio.types import TTSConfig
# wav or pcm when the audio feeds another system, mp3 or opus when it feeds a person
audio = client.tts.convert(
text="High quality narration for the archive.",
config=TTSConfig(format="wav", sample_rate=44100),
)
# Speed accepts 0.5 to 2.0. Small adjustments read as natural, large ones do not.
brisk = client.tts.convert(text="Speaking a little faster.", speed=1.2)
# Pick a specific voice by id
branded = client.tts.convert(
text="This uses a specific voice.",
reference_id="9a9cf47702da476aa4629e2506d4a857",
)
For long documents, use stream rather than convert. It yields chunks instead of building the entire clip in memory, which matters once you are narrating something book length.
with open("chapter.mp3", "wb") as f:
for chunk in client.tts.stream(text=very_long_passage):
f.write(chunk)
Which model to use, and the free tier that is not a downgrade
This is the part worth reading carefully, because the naming hides something genuinely useful. The documentation describes s2.1-pro as the recommended production model, with improved quality, latency and throughput over the previous S2-Pro. It describes s2.1-pro-free as the same model at zero cost, intended for testing, prototyping and development.
| Model | Price | Use it for |
|---|---|---|
| s2.1-pro | 15.00 dollars per million UTF-8 bytes | Production, the recommended default |
| s2.1-pro-free | 0.00 dollars | Evaluation, prototyping, development |
| s2-pro | 15.00 dollars per million UTF-8 bytes | Previous generation, superseded |
| s1 | 15.00 dollars per million UTF-8 bytes | Legacy, only for existing integrations |
| transcribe-1 | 0.36 dollars per audio hour | Speech to text, billed to the second |
| voice-design-1 | 0.01 dollars per successful request | Generating a voice from a description |
The practical consequence is that you can evaluate the actual production model against your real content, in your real languages, at no cost, before committing a single rupee or dollar. That removes the usual excuse for benchmarking on a weaker tier and then being surprised in production. Do your quality testing on the free model, and change one header value when you go live.
Tip
Evaluate on the model you will ship
Since s2.1-pro-free is documented as the same model at zero cost, there is no reason to assess quality on anything else. Run your worst text through it, the product names, the numbers, the abbreviations, and only then decide whether the paid tier earns its place.
Voice cloning, both ways
There are two approaches and they suit different products. Create a persistent voice model when the same voice will be reused, since you clone once and then reference an id forever. Use zero shot references when the voice is supplied per request and there is nothing to keep.
# Route 1: a reusable voice model. Clone once, reference the id afterwards.
with open("sample.wav", "rb") as f:
voice = client.voices.create(
title="Narrator",
voices=[f.read()],
description="Cloned from a studio sample",
visibility="private", # private is the default; unlist and public also exist
)
print(voice.id, voice.state)
audio = client.tts.convert(
text="Now I speak in the cloned voice.",
reference_id=voice.id,
)
# Route 2: zero shot. Nothing is stored, the reference travels with the request.
from fishaudio.types import ReferenceAudio
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.",
)],
)
Note the transcript in the zero shot path. You pass both the audio and the words actually spoken in it, and accuracy there measurably affects the result. A transcript that does not match the audio produces a worse clone than no attempt at all.
On source material, the documentation is specific: WAV, MP3, M4A or Opus, a minimum of ten seconds per clip, and one to two minutes of clear single speaker speech as the optimum. That last figure is worth respecting. Ten seconds works, but the difference between the minimum and a proper minute of clean audio is audible, and it is the cheapest quality improvement available. Audio enhancement for noisy recordings is on by default.
Warning
The consent path is your responsibility
Ten seconds of audio is enough to impersonate someone. If users can upload reference audio, you have shipped a voice cloning tool. Verify the uploader has rights to the voice, log which account generated which clip, and disclose synthetic audio to listeners. None of this is provided by the API.
Streaming, and speaking while the model is still thinking
For voice agents and assistants, the HTTP streaming path is still too slow, because it cannot begin until your language model has produced the complete text. The websocket mode removes that wait entirely. It accepts a generator of text tokens and starts emitting audio as the words arrive.
from fishaudio import FishAudio
from fishaudio.utils import play
client = FishAudio()
def llm_tokens():
# In production this yields tokens from your LLM stream, not a fixed list
for token in ["The ", "first ", "move ", "sets ", "everything ", "in ", "motion."]:
yield token
for chunk in client.tts.stream_websocket(llm_tokens(), reference_id="YOUR_VOICE_ID"):
play(chunk)
This is the single most important pattern in the whole integration for anything conversational. Without it, perceived latency is the language model generation time plus the synthesis time. With it, the two overlap, and the user hears the first words while the model is still composing the rest. That difference is usually the gap between an assistant that feels responsive and one that feels broken.
There is a latency parameter with two settings, and the documentation attaches actual numbers to them. normal is described as best quality at roughly 500 milliseconds, and balanced as good quality at roughly 300 milliseconds. For anything conversational that 200 millisecond difference is worth more than the quality increment, and the documentation itself recommends balanced mode when audio is taking too long to start. Reserve normal for pre rendered audio where nobody is waiting. An async client, AsyncFishAudio, takes async generators in the same shape.
| Mode | Stated quality | Stated latency | Use for |
|---|---|---|---|
| balanced | Good quality | about 300ms | Voice agents, assistants, anything interactive |
| normal | Best quality | about 500ms | Narration and pre rendered audio |
Pro tip
Measure time to first audio byte at the client rather than the server. Server side timings routinely miss 100 to 300 milliseconds of buffering and playback startup, which is exactly the range where an interface stops feeling immediate.
What it costs, in numbers you can plan with
Billing is per million UTF-8 bytes rather than per character or per second, which is awkward to reason about until you convert it. The documentation puts one million UTF-8 bytes at roughly 180,000 English words, or about 12 hours of speech. At 15 dollars per million, that gives some useful reference points.
| Workload | Approximate size | Approximate cost |
|---|---|---|
| A 1,500 word article read aloud | about 8,300 bytes | about 0.13 dollars |
| 100 such articles | about 830,000 bytes | about 12.50 dollars |
| 12 hours of continuous speech | about 1,000,000 bytes | about 15 dollars |
| A 30 word notification, 10,000 times | about 1,700,000 bytes | about 25 dollars |
Be careful with the UTF-8 detail if you work in non Latin scripts. Billing counts bytes, not characters, and Sinhala, Tamil, Chinese, Japanese, Arabic and similar scripts use multiple bytes per character. Text that looks the same length as an English sentence can cost two or three times as much, which is worth modelling before you launch a localised product rather than discovering it on an invoice.
The other limit to plan around is concurrency rather than volume. Rate limits are expressed as simultaneous requests and rise with lifetime spend.
| Tier | Threshold | Concurrent requests |
|---|---|---|
| Starter | Under 100 dollars paid | 5 |
| Elevated | 100 dollars or more paid | 15 |
| High Volume | 1,000 dollars or more paid | 50 |
| Enterprise | Custom | Custom |
Five concurrent requests is comfortable for a content pipeline and tight for anything user facing at scale, so queue your work rather than firing requests as they arrive. A simple worker pool sized to your tier, with retries on rejection, prevents the failure mode where a traffic spike turns into a wall of errors.
The production checklist
Four things separate a working prototype from something you can leave running.
1. Cache on a hash of text + voice + parameters.
Applications repeat far more utterances than anyone expects,
and cached audio costs nothing to serve.
2. Normalise text before synthesis.
Expand currency, dates, units and known acronyms into the words
you want spoken. This removes most reported quality complaints.
3. Queue to your concurrency tier.
Five simultaneous requests on the starter tier. Size a worker pool
to match and retry on rejection instead of failing the user.
4. Log voice provenance.
Which voice, which account, which authorisation, retained.
You cannot answer the only question that matters after an incident
without it.
! Common mistakes to avoid
-
✕Putting the model name in the JSON body.
✓The model is selected with a request header. A body field is silently ignored, so you get the default model and conclude your model choice does not work.
-
✕Evaluating quality on a weaker tier and shipping on the flagship.
✓The free model is documented as the same model as s2.1-pro at zero cost. Test on that and change one header when you go live.
-
✕Passing a transcript that does not match the reference audio.
✓Zero shot cloning uses the transcript as conditioning. A mismatched one produces a worse result than a careful, accurate transcript of exactly what was said.
-
✕Waiting for the full LLM response before calling synthesis.
✓Use the websocket mode with a token generator. Generation and synthesis overlap, which removes the model thinking time from perceived latency entirely.
-
✕Budgeting per character when your content is not Latin script.
✓Billing counts UTF-8 bytes. Many scripts use two to four bytes per character, so model your real languages before committing to a price per article.
-
✕Firing requests as they arrive and ignoring concurrency limits.
✓Limits are concurrent requests, starting at five. Queue through a worker pool sized to your tier so a spike degrades gracefully instead of erroring.
? Frequently asked questions
What is the Fish Audio API endpoint? +
Text to speech is POST https://api.fish.audio/v1/tts with an Authorization bearer header. The model is chosen with a separate model header, and the response is a chunked audio stream rather than JSON.
Is there a genuinely free way to test it? +
Yes. The s2.1-pro-free model is documented as the same model as s2.1-pro priced at zero, intended for testing, prototyping and development, so you can evaluate production quality before paying.
How much does Fish Audio cost? +
The paid speech models are 15 dollars per million UTF-8 bytes, which the documentation puts at roughly 180,000 English words or about 12 hours of speech. Transcription is 0.36 dollars per audio hour.
How much reference audio do I need to clone a voice? +
A minimum of ten seconds per clip, with one to two minutes of clear single speaker audio described as optimal. Accepted formats are WAV, MP3, M4A and Opus, and audio enhancement is enabled by default.
What is the difference between reference_id and references? +
reference_id points at a stored voice model you created earlier and want to reuse. references carries the reference audio inline with a single request for zero shot cloning, with nothing persisted.
Can I stream audio from an LLM as it generates? +
Yes, that is what the websocket mode is for. It accepts a generator yielding text tokens and returns audio chunks as they are produced, so synthesis overlaps generation instead of following it.
What is the pip package called? +
Install fish-audio-sdk with hyphens, then import fishaudio without them. The mismatch between the package name and the module name is a common few minutes of confusion when starting out.
Which latency mode should I use? +
Balanced for anything interactive, documented at good quality and roughly 300 milliseconds against normal at best quality and roughly 500. The docs recommend balanced when audio is slow to start playing.
What are the rate limits? +
They are concurrency based and scale with lifetime spend: 5 simultaneous requests under 100 dollars, 15 at 100 dollars or more, 50 at 1,000 dollars or more, and custom limits for enterprise.
Putting it together
The integration itself is genuinely small. A bearer token, one endpoint, a header to pick the model, and a handful of parameters. The work that decides whether the result is any good sits either side of that call: normalising your text so numbers and acronyms are spoken correctly, caching so you are not paying twice for the same sentence, queueing to your concurrency tier, streaming from your language model so the two stages overlap, and keeping an honest record of whose voice you are using and why. Start on the free model with your worst content, and only move to the paid tier once you know exactly what you are buying.
Note
Specs in this post
Endpoint paths, parameter names, default values, prices and rate limits were taken from the official documentation at the time of writing and change without much notice. Check the current docs before depending on any figure here in production.
Comments
0No comments yet. Be the first to share your thoughts.