Skip to content

Kokoro

Speaks text in 54 built-in voices in 8 languages; speed sets the rate from 0.5 to 2.0.

Access: Keys with the media:generate and media:read scopes, on an account with media access granted by Albastar Corp.

Built-in voices8 languageskokoro

Samples

SampleVoiceaf_heart

0:00 / 0:11

Transcript I found the photos from the lake house. Do you remember that summer? We stayed up until three, arguing about the stars, and you swore you'd seen a comet. I still think it was a plane.

Capabilities

  • Voices: 54 built-in voices; GET /v1/media/voices?model=kokoro&category=builtin lists every id with its language and gender (List voices)
  • Voice ids: The first letter names the language, the second the gender (f female, m male): af_heart is American English, female. Language letters:
    • a: American English
    • b: British English
    • e: Spanish
    • f: French
    • h: Hindi
    • i: Italian
    • j: Japanese
    • p: Brazilian Portuguese
    • z: Chinese (Mandarin)
  • Languages: 8: English (American and British voices), Spanish, French, Hindi, Italian, Japanese, Brazilian Portuguese, Chinese (voice list); the voice sets the language
  • Speed: speed, 0.5 to 2.0 in steps of 0.05; default 1.0
  • Text: Up to 20,000 characters per request
  • Voice cloning: Not supported; see Voice cloning

Request

The request that made the sample above, as a media job in the built-in voice af_heart. format takes mp3, wav or opus.

curl

text="I found the photos from the lake house. Do you remember that summer?"
text+=" We stayed up until three, arguing about the stars, and you swore"
text+=" you'd seen a comet. I still think it was a plane."

job=$(curl -sS "https://api.albastov.com/v1/media/jobs?wait=60" \
  -H "Authorization: Bearer $ALBASTAR_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "model": "kokoro",
  "params": {
    "text": "$text",
    "voice": "af_heart",
    "format": "mp3"
  }
}
EOF
)

while [[ $(jq -r .state <<<"$job") =~ ^(queued|waiting_device|running)$ ]]; do
  job=$(curl -sS \
    "https://api.albastov.com/v1/media/jobs/$(jq -r .id <<<"$job")?wait=60" \
    -H "Authorization: Bearer $ALBASTAR_API_KEY")
done

curl -sS -o lake-house.mp3 "$(jq -er '.results[0].url' <<<"$job")"

Python

import json
import os
import urllib.request

API = "https://api.albastov.com/v1"
HEADERS = {
    # urllib's default User-Agent gets 403 with error code 1010.
    "User-Agent": "my-app/1.0",
    "Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
    "Content-Type": "application/json",
}
TEXT = (
    "I found the photos from the lake house. Do you remember that summer?"
    " We stayed up until three, arguing about the stars, and you swore"
    " you'd seen a comet. I still think it was a plane."
)


def fetch(url, body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(url, data=data, headers=HEADERS)
    with urllib.request.urlopen(req, timeout=120) as res:
        return res.read()


job = json.loads(fetch(f"{API}/media/jobs?wait=60", {
    "model": "kokoro",
    "params": {
        "text": TEXT,
        "voice": "af_heart",
        "format": "mp3",
    },
}))
while job["state"] in ("queued", "waiting_device", "running"):
    job = json.loads(fetch(f"{API}/media/jobs/{job['id']}?wait=60"))
if job["state"] != "done":
    raise RuntimeError(job["error"])
with open("lake-house.mp3", "wb") as f:
    f.write(fetch(job["results"][0]["url"]))

JavaScript

import { writeFile } from "node:fs/promises";

const api = "https://api.albastov.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}`,
  "Content-Type": "application/json",
};
const text =
  "I found the photos from the lake house. Do you remember that summer?" +
  " We stayed up until three, arguing about the stars, and you swore" +
  " you'd seen a comet. I still think it was a plane.";

async function call(path, body) {
  const res = await fetch(api + path, {
    method: body ? "POST" : "GET",
    headers,
    body: body && JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}

let job = await call("/media/jobs?wait=60", {
  model: "kokoro",
  params: {
    text,
    voice: "af_heart",
    format: "mp3",
  },
});
while (["queued", "waiting_device", "running"].includes(job.state)) {
  job = await call(`/media/jobs/${job.id}?wait=60`);
}
if (job.state !== "done") throw new Error(JSON.stringify(job.error));
const audio = await fetch(job.results[0].url);
if (!audio.ok) throw new Error(`${audio.status} ${await audio.text()}`);
await writeFile("lake-house.mp3", Buffer.from(await audio.arrayBuffer()));

With ?wait=60, the response arrives when the job ends, or after 60 s with the job still queued, waiting_device or running; the loop asks again until the job ends. GET /v1/media/jobs/{id} needs the media:read scope. results[0].url is the MP3 file and needs no key.

POST /v1/audio/speech takes the same text as input and the same voice: Speech reference. Every job parameter, state and error: Media jobs reference.