Make speech
POST /v1/audio/speech takes a model, a voice and the text, and returns the audio file. The request shape is
OpenAI's, so OpenAI SDKs work with the base URL changed.
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTOV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "kokoro", "voice": "af_heart",
"input": "The river bends past the old mill."}' \
--output river.mp3Python
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.albastov.com/v1", api_key=os.environ["ALBASTOV_API_KEY"])
with client.audio.speech.with_streaming_response.create(
model="kokoro", voice="af_heart", input="The river bends past the old mill."
) as res:
res.stream_to_file("river.mp3")JavaScript
import { writeFile } from "node:fs/promises";
const res = await fetch("https://api.albastov.com/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "kokoro", voice: "af_heart", input: "The river bends past the old mill." }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("river.mp3", Buffer.from(await res.arrayBuffer()));The response body is the whole audio file. The key needs the media:generate scope.
Pick a model
| Need | Model |
|---|---|
| Fast narration in a ready-made voice | Kokoro |
| Speech in a cloned voice | OmniVoice |
| A new voice from a description | OmniVoice |
| Close likeness to a short clip | Qwen3-TTS |
Samples and capabilities of each model: Audio models.
Speak in a saved voice
A saved voice vo_… carries its default model, so model may be left out. Saved voices come from
Voices and Voice cloning.
vo_hVAiUiqrYyvgKKkBvgzHnh is a shared synthetic voice that every account with media may use.
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTOV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice": "vo_hVAiUiqrYyvgKKkBvgzHnh",
"input": "The river bends past the old mill."}' \
--output river.mp3Python
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.albastov.com/v1", api_key=os.environ["ALBASTOV_API_KEY"])
# model is left out: the saved voice's default_model runs. The SDK requires the argument, so pass None.
with client.audio.speech.with_streaming_response.create(
model=None, voice="vo_hVAiUiqrYyvgKKkBvgzHnh", input="The river bends past the old mill."
) as res:
res.stream_to_file("river.mp3")JavaScript
import { writeFile } from "node:fs/promises";
const res = await fetch("https://api.albastov.com/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTOV_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ voice: "vo_hVAiUiqrYyvgKKkBvgzHnh", input: "The river bends past the old mill." }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("river.mp3", Buffer.from(await res.arrayBuffer()));voice also takes an upload up_…, a job result job:<id>/<i> or an https:// URL of a clip: the cloning model
copies the voice in the clip. Every form: Speech reference.
OpenAI voice names
On kokoro, OpenAI's voice names speak as Kokoro voices:
| OpenAI voice | Kokoro voice |
|---|---|
alloy | af_alloy |
ash | am_adam |
ballad | bm_george |
coral | af_heart |
echo | am_echo |
fable | bm_fable |
nova | af_nova |
onyx | am_onyx |
sage | af_sarah |
shimmer | af_bella |
verse | am_michael |
OpenAI's model names run as Albastov models: tts-1 runs kokoro, and omnivoice when voice is a voice reference;
tts-1-hd runs kokoro, and qwen3-tts when voice is a voice reference. Kokoro's 54 built-in voices:
GET /v1/media/voices?model=kokoro.
Output formats
response_format sets the file format.
| Format | Content type | Note |
|---|---|---|
mp3 | audio/mpeg | Default, 128 kb/s |
wav | audio/wav | Uncompressed |
opus | audio/ogg | Opus in Ogg, 64 kb/s, smallest file |
Waiting for a result
Every speech request runs as a media job. The request waits for the job: up to the job's queue timeout while the job
is queued (300 s for speech by default), and at most 85 s from the request's arrival. The X-Job-Id response header
names the job, and X-Queue-Timeout gives the queue timeout in seconds.
A job not done in time answers 503 with code queued and job_id. The job stays in the queue:
GET /v1/media/jobs/{job_id}?wait=30 returns the job, and the job's results[0].url is the audio file once
state is done. code queue-timeout means the job waited past its queue timeout and expired.
{
"error": {
"message": "The job is still queued (the device is busy or asleep). It stays in /v1/media/jobs; poll GET /v1/media/jobs/mj_Y8nW2HfyPWD7ojMvqo2PXC?wait=30 for it.",
"type": "server_error",
"param": null,
"code": "queued",
"job_id": "mj_Y8nW2HfyPWD7ojMvqo2PXC"
}
}A caller that needs an answer within 10 s sends X-Queue-Timeout: 10: the job expires after 10 s in the queue
instead of 300 s. The 503 carries X-Should-Retry: false, so OpenAI SDKs do not repeat the request. An identical
request in the same 10-minute clock window returns the first request's job instead of queueing a second one.
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTOV_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Queue-Timeout: 10" \
-d '{"model": "kokoro", "voice": "af_heart", "input": "The river bends past the old mill."}' \
--output river.mp3The job object, ?wait and queue timeouts: Media jobs reference.
Reference
- Speech reference: every parameter, header and error of
POST /v1/audio/speech. - Media jobs reference: speech jobs,
?waitand the model list with each model's parameters.