Skip to content

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.mp3

Python

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.mp3

Python

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 voiceKokoro voice
alloyaf_alloy
asham_adam
balladbm_george
coralaf_heart
echoam_echo
fablebm_fable
novaaf_nova
onyxam_onyx
sageaf_sarah
shimmeraf_bella
verseam_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.

FormatContent typeNote
mp3audio/mpegDefault, 128 kbit/s
wavaudio/wavUncompressed
opusaudio/oggOpus 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.

503
{
  "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.ogg

A 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.