Text to speech
Speech from text with POST /v1/audio/speech, in a built-in voice or a saved voice.
Access: Keys with the media:read or media:generate scope, on an account with media access granted by Albastar Corp.
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. The key needs the media:generate scope.
Make speech
The example speaks one sentence with kokoro in the built-in voice af_heart.
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "kokoro", "voice": "af_heart",
"input": "The next ferry leaves at a quarter past six."}' \
--output ferry.mp3Python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
with client.audio.speech.with_streaming_response.create(
model="kokoro",
voice="af_heart",
input="The next ferry leaves at a quarter past six.",
) as res:
res.stream_to_file("ferry.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.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "kokoro",
voice: "af_heart",
input: "The next ferry leaves at a quarter past six.",
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("ferry.mp3", Buffer.from(await res.arrayBuffer()));The response body is the whole audio file. Every text-to-speech model, with a sample of each: Voice models.
Speak in a saved voice
A saved voice vo_… names its default model, so model may be left out: the request runs on the voice's
default_model. Saved voices come from Voices and
Voice cloning. The example speaks in Narrator A,
vo_tBeDCRb2byGFjB6HGyQq6a, a shared synthetic voice that every key with media:generate may use.
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice": "vo_tBeDCRb2byGFjB6HGyQq6a",
"input": "The next ferry leaves at a quarter past six."}' \
--output ferry.mp3Python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
# model=None sends "model": null: the voice's default_model runs.
with client.audio.speech.with_streaming_response.create(
model=None,
voice="vo_tBeDCRb2byGFjB6HGyQq6a",
input="The next ferry leaves at a quarter past six.",
) as res:
res.stream_to_file("ferry.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.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
voice: "vo_tBeDCRb2byGFjB6HGyQq6a",
input: "The next ferry leaves at a quarter past six.",
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("ferry.mp3", Buffer.from(await res.arrayBuffer()));A model of null and a missing model run the same way. 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.
Instructions
instructions describes the voice or the delivery, such as "calm, low, slow". The speech route passes the text to
the model as its instruct parameter, 500 characters at most. On POST /v1/audio/speech only breeze2 uses the
text: omnivoice ignores instruct whenever a voice is given, and the route always requires voice. Other models
drop the field.
A media job (POST /v1/media/jobs) takes instruct directly. On omnivoice without voice, instruct designs a
new voice from the description; on breeze2, instruct describes the delivery of the cloned voice:
Media jobs 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 Albastar models: tts-1 runs kokoro, or omnivoice when voice is a voice reference;
tts-1-hd runs kokoro, or 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 kbit/s |
wav | audio/wav | Uncompressed |
opus | audio/ogg | Opus in Ogg, 64 kbit/s, the 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), 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 (scope media:read) 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. It stays in /v1/media/jobs; poll …",
"type": "server_error",
"param": null,
"code": "queued",
"job_id": "mj_Y8nW2HfyPWD7ojMvqo2PXC"
}
}The message above is shortened. The whole message: The job is still queued. It stays in /v1/media/jobs; poll GET /v1/media/jobs/mj_Y8nW2HfyPWD7ojMvqo2PXC?wait=30 for it.
To give up if the job hasn't started within 10 s, send X-Queue-Timeout: 10: the job expires after 10 s in the queue
instead of 300 s. A job that has started runs to the end. The 503 carries X-Should-Retry: false, so OpenAI SDKs do
not repeat the request.
curl
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Queue-Timeout: 10" \
-d '{"model": "kokoro", "voice": "af_heart", "response_format": "opus",
"input": "The next ferry leaves at a quarter past six."}' \
--output ferry.oggA request without Idempotency-Key that repeats an earlier request of the same key (the same fields and the same
X-Queue-Timeout) within the same 10-minute window returns the first request's job instead of queueing a second one.
The windows are fixed on the clock: each starts at :00, :10, :20, :30, :40 or :50 past the hour, UTC, so two
identical requests a minute apart fall into two windows when a window boundary lies between them. A first job that
failed, expired or was cancelled runs again as a new job.
Reference
- Speech reference: every parameter, header and error of
POST /v1/audio/speech. - Media jobs reference: the job object,
?wait, queue timeouts and the model list with each model's parameters.