Voicebox API
How to clone a voice and generate speech with it via Voicebox's HTTP API, so an external tool can add Voicebox as a TTS provider (alongside Chatterbox).
Verified 2026-07-07 against a live Voicebox (
puente up voicebox, port 17493): all documented endpoints exist in its OpenAPI spec, and creating a cloned-voice profile viaPOST /profilesworks. Voicebox exposes interactive FastAPI docs at/docs(and the raw schema at/openapi.json) — the authoritative reference if the API evolves.
What Voicebox is (and how it relates to Chatterbox)¶
Voicebox is a self-hosted voice studio with a FastAPI backend. Importantly,
it is multi-backend — its engine parameter selects the TTS model:
qwen, qwen_custom_voice, luxtts, chatterbox, chatterbox_turbo,
tada, kokoro. So Voicebox can even drive Chatterbox internally.
It is not a thin UI over the standalone Chatterbox server — it's an independent stack. As a provider it offers a clean "profile + generate" model.
Endpoint: http://<box>:17493 (default port). Add an nginx-proxy-manager
host (e.g. voice.locopuente.org) for public/TLS access, like the other
services. FastAPI exposes interactive docs at /docs — use it to confirm
schemas live.
The voice-clone model: profiles + samples¶
Voicebox clones a voice as a profile built from one or more samples (reference audio + its transcript). You then generate speech against the profile. Three steps:
1. Create a voice profile¶
POST /profiles
Content-Type: application/json
{
"name": "Lecturer A",
"description": "Course narration voice",
"language": "en",
"voice_type": "cloned"
}
VoiceProfileResponse with an id (the profile_id).
Languages: zh en ja ko de fr ru pt es it he ar da el fi hi ms nl no pl sv sw tr.
2. Add a sample (this is the clone)¶
Upload reference audio + the exact text spoken in it (multipart form):
POST /profiles/{profile_id}/samples
Content-Type: multipart/form-data
file=@reference.wav
reference_text=The exact words spoken in the reference clip.
.wav .mp3 .m4a .ogg .flac .aac .webm .opus.
→ returns a ProfileSampleResponse. Add multiple samples for a better clone.
3. Generate speech with the cloned voice¶
POST /generate
Content-Type: application/json
{
"profile_id": "<id from step 1>",
"text": "Text to speak in the cloned voice.",
"language": "en",
"engine": "chatterbox",
"seed": null,
"normalize": true
}
GenerationResponse with an id and (when finished) audio_path,
duration, status.
engine options: qwen (default) | qwen_custom_voice | luxtts |
chatterbox | chatterbox_turbo | tada | kokoro. For long text, tune
max_chunk_chars (default 800) and crossfade_ms (default 50).
4. Fetch the generated audio¶
Streams the audio file. (AlsoGET /history/{generation_id}/export-audio.)
Async note: /generate may return status: "generating". Poll
GET /generate/{generation_id}/status until complete, or use the streaming
variant POST /generate/stream.
Transcribing a clip¶
To clone from audio alone (no hand-typed transcript), transcribe it first:
POST /transcribe
Content-Type: multipart/form-data
file=@reference.wav
language=en # optional
model=<whisper-size> # optional; server default otherwise
{"text": "...", "duration": <seconds>}, via server-side Whisper.
First-call note: if the Whisper model is not cached yet, this returns
HTTP 202 with {"downloading": true, ...} and fetches the model in the
background. Wait and retry; it is not an error.
Deleting what you created¶
Voicebox exposes three delete endpoints:
DELETE /profiles/{profile_id} # profile + its samples + reference audio
DELETE /profiles/samples/{sample_id} # one sample
DELETE /history/{generation_id} # one generation + its rendered audio
DELETE /profiles/{id} is a deep delete (verified in
backend/services/profiles.py::delete_profile): it removes the sample rows,
the profile row, rmtrees the profile's directory under get_profiles_dir()
— which is where the uploaded reference audio, i.e. the clone itself, lives —
and clears the combined-audio cache. You do not need to delete samples
individually first.
It does not cascade to generations.
Generation.profile_idis a plainForeignKeycolumn with no SQLAlchemyrelationship(..., cascade=...), and SQLite does not enforce FKs unlessPRAGMA foreign_keys=ON. Deleting a profile therefore leaves orphaned history rows whose rendered audio is still on disk and still fetchable viaGET /audio/{generation_id}. To leave nothing behind, delete each generation as well.
Listing generation history¶
To find a profile's generations (e.g. before deleting the profile):
Query params: profile_id (filter to one profile), limit and offset
(pagination). Response shape:
Note: this is what
contrib/voicebox/sweep_ephemeral_profiles.pyassumes. Verify it against the live server's/openapi.json— in particular that/historyhonours theprofile_idfilter. The sweep script does not blindly trust it: it re-checks each item's ownprofile_idfield client-side and never deletes items lacking that field, so a server that ignores the filter cannot cause it to delete the whole history.
Minimal provider flow (pseudocode)¶
B = "http://box:17493"
# one-time per voice: create profile + add sample(s)
pid = post(f"{B}/profiles", json={"name": "Lecturer A", "language": "en",
"voice_type": "cloned"})["id"]
post(f"{B}/profiles/{pid}/samples",
files={"file": open("reference.wav", "rb")},
data={"reference_text": "The exact words in the clip."})
# per generation:
gen = post(f"{B}/generate", json={"profile_id": pid, "text": "Hello world",
"engine": "chatterbox"})
# poll if needed:
while get(f"{B}/generate/{gen['id']}/status")["status"] == "generating":
sleep(1)
audio = get_bytes(f"{B}/audio/{gen['id']}")
Reuse the profile_id across many generations — cloning (steps 1–2) is one-time
per voice; step 3 is per utterance.
Ephemeral clone (what SlideStream's voicebox provider does)¶
When voice_sample is configured instead of profile_id, the provider clones
for the run and leaves nothing behind. Cloning needs a transcript of the clip;
if reference_text is not given, the clip is first transcribed via
POST /transcribe (see "Transcribing a clip" above), so only the audio is
required.
reference_text = reference_text or transcribe(reference.wav) # POST /transcribe
pid = create_profile(name=f"slide-stream-{uuid4()}") # random: no speaker-recognisable label
add_sample(pid, reference.wav, reference_text)
try:
for slide in deck: # per slide
gen_id = generate(pid, slide.text)
try:
audio = get_bytes(f"{B}/audio/{gen_id}")
finally:
delete(f"{B}/history/{gen_id}") # drop the rendered narration
finally:
delete(f"{B}/profiles/{pid}") # drop the clone, even on crash
Both deletes must sit in finally blocks: a run that dies mid-render would
otherwise strand exactly the clone you were trying not to store. Set
delete_generations: false to keep the rendered audio in Voicebox's history.
Backstop: sweeping orphans¶
A finally cannot run if the process is SIGKILLed, OOM-killed, or the box
loses power. contrib/voicebox/sweep_ephemeral_profiles.py reaps whatever such
a run left behind — it deletes each profile's generations, then the profile.
Stdlib only; copy it onto the Voicebox host.
*/15 * * * * /opt/slide-stream/sweep_ephemeral_profiles.py \
--base-url http://localhost:17493 --max-age-minutes 60
The grace period is what stops it deleting the clone out from under a render
still in progress, so keep --max-age-minutes comfortably above your longest
deck. It only ever deletes profiles that are voice_type: cloned and named
exactly slide-stream-<uuid4>; a profile you made by hand cannot match. Use
--dry-run first, and --keep-generations to reap clones but retain history.
It exits non-zero if any deletion failed, so cron will mail you.
Should you use this instead of Chatterbox?¶
Both do voice-clone TTS. For a provider abstraction in the calling tool:
- Chatterbox (:8004) — the currently-integrated, verified TTS. Simplest if
you just need "text → cloned-voice audio." No delete API: uploaded references
are named with a UUID and reaped by the cron in
contrib/chatterbox/. - Voicebox (:17493) — richer: named profiles, multiple engines (incl. Chatterbox), effects, personality rewriting. More setup, still "under evaluation" in Puente. Can delete clones over the API, so the ephemeral flow above needs no server-side cleanup job.
Adding both as selectable TTS providers is reasonable — they don't conflict. See the deployment guide for the full picture.