Skip to content

Text to speech

Speech from text, in a built-in voice or a saved voice.

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

Python

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

NeedModel
Fast narration in a ready-made voiceKokoro
Speech in a cloned voiceOmniVoice
A new voice from a descriptionOmniVoice
Close likeness to a short clipQwen3-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.mp3

Python

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

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

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

The job object, ?wait and queue timeouts: Media jobs reference.

Reference