Chatterbox
Clones a voice in 19 languages, using up to 20 s of the clip.
Access: Keys with the media:generate and media:read scopes, on an account with media access granted by Albastar Corp.
Samples
ReferenceNarrator A · synthetic
Transcript Okay, so here's the plan. We land just after sunset, grab the bags, and head straight for the harbour. If the ferry's late again — and honestly, it usually is — we'll get dinner by the water and wait it out. Sound good?
CloneChatterbox in the reference voice
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
- Voice cloning
- From
voice: a saved voicevo_…, an uploadup_…or an https URL, with or withoutreference_text. The model uses at most 20 s of the clip; a longer clip is cut at a pause between 5 and 20 s - Languages
- 19: Arabic, Danish, Dutch, English, Finnish, French, German, Greek, Hindi, Italian, Korean, Malay, Norwegian, Polish, Portuguese, Spanish, Swahili, Swedish, Turkish. The model card lists 23; the API does not serve the other 4 (Chinese, Hebrew, Japanese, Russian)
- Seed
seedon a media job, 0 to 4294967295: the same seed and parameters give the same audio- Text
- Up to 5,000 characters per request
Request
The request that made the sample above, as a media job with seed 7; a sample made again after a failed job or a failed check uses seed 8, then 9, and so on. vo_tBeDCRb2byGFjB6HGyQq6a is Narrator A, a synthetic voice shared with every key that has the media:generate scope. 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": "chatterbox",
"params": {
"text": "$text",
"voice": "vo_tBeDCRb2byGFjB6HGyQq6a",
"seed": 7,
"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": "chatterbox",
"params": {
"text": TEXT,
"voice": "vo_tBeDCRb2byGFjB6HGyQq6a",
"seed": 7,
"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: "chatterbox",
params: {
text,
voice: "vo_tBeDCRb2byGFjB6HGyQq6a",
seed: 7,
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, without seed: Speech reference. Every job parameter, state and error: Media jobs reference.