LLM & Media APIMusic & speech

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:

SurfaceEndpointBilled by
MusicPOST /v1/llm/music/jobs → poll GET /v1/llm/music/jobs/{job_id}per call
Speech & audioPOST /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.

SlugPrice / callNotes
music-v5$0.12Latest stable line — the sensible default
music-v5-5$0.12Newest, experimental
music-v4-5$0.12Previous generation, faster time-to-first-track
generate-music$0.20Legacy slug; needs style + title for vocal tracks
minimax-music-2-6$0.20MiniMax 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, or style + 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

FieldTypeRequiredNotes
modelstringyesSlug from the table above.
promptstringyes1–5000 characters. The track's theme, or the lyric description in inspire mode.
stylestringnoUp to 1000 characters, e.g. "Cinematic, Orchestral, slow build".
titlestringnoUp to 200 characters.
instrumentalboolnotrue for a track with no vocals. Default false.
lyricsstringnoUp to 5000 characters. Sets custom mode; the model sings these words.
model_versionstringnoV4 / 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_weightfloatno0.01.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.

SlugWhat it doesBilledRate
elevenlabs-tts-multilingual-v2High-fidelity multilingual text-to-speechper 1000 characters$0.09
elevenlabs-tts-turbo-2-5Low-latency text-to-speechper 1000 characters$0.045
elevenlabs-v3-dialogueMulti-speaker dialogue with audio tagsper 1000 characters across all turns$0.09
elevenlabs-sound-effect-v2Text-to-sound-effectper second of generated audio$0.0018
elevenlabs-audio-isolationStrip background noise / isolate a voiceper second of source audio$0.00144

Which fields each SKU needs

KindRequiredOptional
TTS (elevenlabs-tts-*)textvoice, stability, similarity_boost, style, speed, language_code
Dialogue (elevenlabs-v3-dialogue)dialogue — a list of {text, voice} turnsstability, language_code
Sound effect (elevenlabs-sound-effect-v2)textduration_seconds, prompt_influence, loop, output_format
Isolation (elevenlabs-audio-isolation)audio_url
FieldTypeNotes
textstring5000 characters. Longer is an HTTP 400 invalid_request with the limit in data.meta.fields — never a truncated read.
voicestringElevenLabs voice id. Omit for a stock voice.
stability, similarity_boost, stylefloat0.01.0 prosody controls.
speedfloat0.71.2.
language_codestringISO 639-1, to force a language the text is ambiguous about.
dialoguearray{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_secondsfloatSound effect length hint, up to 30 s.
prompt_influencefloat0.01.0 — how literally the effect follows the prompt.
loopboolGenerate a seamless loop.
output_formatstringe.g. mp3_44100_128. Sound effects only — the other audio SKUs drop it.
audio_urlstringSource 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.