Music & speech
Text-to-music with lyrics and style, plus ElevenLabs text-to-speech, multi-speaker dialogue, sound effects and voice isolation — two async job surfaces, billed on delivery.
Two job surfaces sit next to the image and video ones and follow the same kickoff-then-poll contract:
| Surface | Endpoint | Billed by |
|---|---|---|
| Music | POST /v1/llm/music/jobs → poll GET /v1/llm/music/jobs/{job_id} | per call |
| Speech & audio | POST /v1/llm/audio/jobs → poll GET /v1/llm/audio/jobs/{job_id} | per 1000 characters, or per second |
Both return a job_id in under a second, charge only when a poll returns the
finished media, and cost nothing on a failure. Auth is X-API-Key: gpuniq_...
(Authorization: Bearer gpuniq_... and a dashboard JWT work too).
Check exception, not the HTTP status. A refused kickoff — unknown
model, insufficient balance, a missing required field — comes back as
HTTP 200 with a non-zero exception and the reason in
data.error_code. Only a malformed body (wrong type, over a length limit)
is a real HTTP 400. resp.raise_for_status() catches neither the first
kind nor a failed job, so branch on body["exception"] != 0.
Music
One call buys one generation and is billed once, however many tracks come
back. music-v5, music-v5-5, music-v4-5 and generate-music return
two tracks per call — an A/B pair for the price of one render.
| Slug | Price / call | Notes |
|---|---|---|
music-v5 | $0.12 | Latest stable line — the sensible default |
music-v5-5 | $0.12 | Newest, experimental |
music-v4-5 | $0.12 | Previous generation, faster time-to-first-track |
generate-music | $0.20 | Legacy slug; needs style + title for vocal tracks |
minimax-music-2-6 | $0.20 | MiniMax Music 2.6; needs style + title for vocal tracks |
Extending an existing track is not available through the API yet.
Two prompt modes
The fields you send decide how much freedom the model gets:
- Inspire — send only
prompt. The model writes the lyrics, picks a style and titles the track itself. Best for a first pass. - Custom — send
lyrics, orstyle+title. Your words are used as written and the style is followed.
On the legacy generate-music and minimax-music-2-6 slugs the upstream
requires style and title whenever instrumental is false. Omit
them and the job comes back as upstream_validation_error with a hint
rather than a generic failure. The music-v* slugs have no such
requirement — they fall back to inspire mode.
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
model | string | yes | Slug from the table above. |
prompt | string | yes | 1–5000 characters. The track's theme, or the lyric description in inspire mode. |
style | string | no | Up to 1000 characters, e.g. "Cinematic, Orchestral, slow build". |
title | string | no | Up to 200 characters. |
instrumental | bool | no | true for a track with no vocals. Default false. |
lyrics | string | no | Up to 5000 characters. Sets custom mode; the model sings these words. |
model_version | string | no | V4 / V4_5 / V4_5PLUS / V4_5ALL / V5 / V5_5. Default V5. Read by the legacy slugs; the music-v* slugs carry their version in the slug itself. |
style_weight | float | no | 0.0–1.0, two decimals — how strictly the style is followed. Legacy slugs only; accepted and ignored on music-v*. |
Example
import time, requests
BASE = "https://api.gpuniq.com/v1/llm"
HEADERS = {"X-API-Key": "gpuniq_your_key"}
body = requests.post(f"{BASE}/music/jobs", headers=HEADERS, json={
"model": "music-v5",
"prompt": "a slow synthwave track for a night drive through empty streets",
"style": "Synthwave, analog pads, 90 BPM",
"title": "Night Drive",
"instrumental": True,
}).json()
if body["exception"] != 0: # refusals arrive as HTTP 200
raise RuntimeError(body["data"]["error_code"])
start = body["data"]
job_id = start["job_id"]
print("estimated:", start["estimated_cost_usd"]) # 0.12
deadline = time.time() + 600 # music renders run minutes, not seconds
while time.time() < deadline:
time.sleep(4)
d = requests.get(f"{BASE}/music/jobs/{job_id}", headers=HEADERS).json()["data"]
if d["status"] == "completed":
for t in d["tracks"]:
print(t["url"]) # two tracks on the music-v* slugs
print("cost:", d["cost_usd"], "balance:", d["balance_usd"])
break
if d["status"] == "failed":
print("failed:", d.get("error"))
break
Give the poll loop a generous budget. Music renders are slower than
image renders, and abandoning the loop early costs you a render you would
otherwise have received. The job record lives for 60 minutes; after that
a poll returns job_not_found.
Speech, dialogue, sound effects & isolation
One endpoint, four kinds of work. The model slug decides which fields are
read. A missing required field is refused at kickoff with
invalid_request naming it. A field the SKU does not use is silently
dropped, not refused — so check the table below rather than assuming a knob
took effect.
| Slug | What it does | Billed | Rate |
|---|---|---|---|
elevenlabs-tts-multilingual-v2 | High-fidelity multilingual text-to-speech | per 1000 characters | $0.09 |
elevenlabs-tts-turbo-2-5 | Low-latency text-to-speech | per 1000 characters | $0.045 |
elevenlabs-v3-dialogue | Multi-speaker dialogue with audio tags | per 1000 characters across all turns | $0.09 |
elevenlabs-sound-effect-v2 | Text-to-sound-effect | per second of generated audio | $0.0018 |
elevenlabs-audio-isolation | Strip background noise / isolate a voice | per second of source audio | $0.00144 |
Which fields each SKU needs
| Kind | Required | Optional |
|---|---|---|
TTS (elevenlabs-tts-*) | text | voice, stability, similarity_boost, style, speed, language_code |
Dialogue (elevenlabs-v3-dialogue) | dialogue — a list of {text, voice} turns | stability, language_code |
Sound effect (elevenlabs-sound-effect-v2) | text | duration_seconds, prompt_influence, loop, output_format |
Isolation (elevenlabs-audio-isolation) | audio_url | — |
| Field | Type | Notes |
|---|---|---|
text | string | ≤ 5000 characters. Longer is an HTTP 400 invalid_request with the limit in data.meta.fields — never a truncated read. |
voice | string | ElevenLabs voice id. Omit for a stock voice. |
stability, similarity_boost, style | float | 0.0–1.0 prosody controls. |
speed | float | 0.7–1.2. |
language_code | string | ISO 639-1, to force a language the text is ambiguous about. |
dialogue | array | {text, voice} per speaker turn. The 5000-character cap applies to the sum of all turns and is refused with the count. Every turn needs a non-empty text and voice. |
duration_seconds | float | Sound effect length hint, up to 30 s. |
prompt_influence | float | 0.0–1.0 — how literally the effect follows the prompt. |
loop | bool | Generate a seamless loop. |
output_format | string | e.g. mp3_44100_128. Sound effects only — the other audio SKUs drop it. |
audio_url | string | Source file for isolation. |
Example: text-to-speech
import time, requests
BASE = "https://api.gpuniq.com/v1/llm"
HEADERS = {"X-API-Key": "gpuniq_your_key"}
body = requests.post(f"{BASE}/audio/jobs", headers=HEADERS, json={
"model": "elevenlabs-tts-turbo-2-5",
"text": "Your instance is ready. SSH credentials are in the dashboard.",
"speed": 1.0,
}).json()
if body["exception"] != 0: # refusals arrive as HTTP 200
raise RuntimeError(body["data"]["error_code"])
job_id = body["data"]["job_id"]
deadline = time.time() + 300
while time.time() < deadline:
time.sleep(3)
d = requests.get(f"{BASE}/audio/jobs/{job_id}", headers=HEADERS).json()["data"]
if d["status"] == "completed":
print(d["audio"]["url"], d["characters"], "chars", "$", d["cost_usd"])
break
if d["status"] == "failed":
print("failed:", d.get("error"))
break
Example: two-speaker dialogue
curl -X POST https://api.gpuniq.com/v1/llm/audio/jobs \
-H "X-API-Key: gpuniq_your_key" \
-H "Content-Type: application/json" \
-d '{
"model": "elevenlabs-v3-dialogue",
"dialogue": [
{"text": "[cheerfully] The build is green.", "voice": "<voice_id_a>"},
{"text": "[skeptical] On which branch?", "voice": "<voice_id_b>"}
]
}'
How the balance gate works
Character-metered SKUs know the exact charge before the job starts — the
character count is deterministic from your input, so estimated_cost_usd is
the final price.
Per-second SKUs do not: the length of a sound effect or a source file isn't known at submit. Those gate your balance against a conservative assumption (30 s for a sound effect, 600 s for an isolation job) and the precise charge lands at completion, recovered from the upstream's own metering. A job that turns out shorter settles well under its estimate.
Where the delivered media lives
Unlike images and video, music tracks and audio files are not mirrored to GPUniq storage. The completion response carries only the upstream URL, whose lifetime is set by the rendering provider, not by us — and the job record itself expires after 60 minutes. Download the bytes as soon as the job completes.
Errors
Both surfaces use the same typed error codes as the video job API —
insufficient_balance, model_not_found, invalid_request,
content_moderation, upstream_validation_error, provider_unavailable,
and job_not_found (exception: 404, still HTTP 200) for an unknown, expired
or foreign-owned job id.
Full catalog and recovery recipes: Error reference.