Image / video / music APIs

/v1/images/generations billed per image · /v1/videos/generations async, billed per second · /v1/music/generations async, billed per generation · /v1/transcripts/extract async video-to-script, billed per call

Alongside the four chat protocol endpoints, the platform offers three families of media endpoints. Authentication is exactly as it is for chat — the same sk-gpushare-* key, in whichever of the four forms you like (x-api-key or x-goog-api-key header, ?key= query, Authorization: Bearer); see Authentication. Everything bills against your account balance (shared by all keys), and an insufficient balance returns 402 quota_exceeded.

EndpointPurposeBilling
POST /v1/images/generationsText-to-image and image-to-image (synchronous)per image
POST /v1/videos/generationsText-to-video and image-to-video (async task)per second
GET /v1/videos/generations/{id}Poll a video taskfree
GET /v1/videos/generationsList this account's video tasks (all statuses by default; ?limit= default 30, max 100; ?status= to filter)free
POST /v1/music/generationsAI music generation (Suno, async task)per generation (2 songs each)
GET /v1/music/generations/{id}Poll a music taskfree
GET /v1/music/generationsList this account's music tasks (same shape)free
POST /v1/audio/speechSpeech synthesis (synchronous, or "async": true for a task)per character
GET /v1/audio/speech/{id}Poll a speech taskfree
GET /v1/audio/speechList this account's speech tasks (same shape)free
POST /v1/transcripts/extractShort-video link → spoken script (synchronous)per call

Every response from these endpoints carries an x-gateway-trace header — include it with the timestamp, the model and the full error body when reporting a problem.


Retries don't double-charge: Idempotency-Key#

Every billed POST endpoint (image, video, music, speech, voice clone, digital-human avatar, transcript) accepts an Idempotency-Key request header. With it, the same request can be sent any number of times and is executed exactly once:

IDEM=$(uuidgen)   # one key per submit intent, reused by every retry of it

curl https://dianqi.zsopc.com/v1/videos/generations \
  -H "Authorization: Bearer $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEM" \
  -d '{"model":"doubao-seedance-2-0-260128","prompt":"sunset over the sea","duration":5}'
  • Same key + same request body → the original response is returned verbatim (same task id), with an extra Idempotency-Replayed: true header. No second task, no second charge.
  • This is also the easiest way to recover a task id: forgot to save the id? Re-run the exact same curl (same key, same body) and you get the original task back.
  • Keys are retained for 7 days; after that the same key counts as a new request.
  • Key format: 1–200 printable ASCII characters (a UUID is the obvious choice). One key per submit intent — do not reuse it across different requests.
  • Scoped to the account, not to a single API key: replaying the same Idempotency-Key from a different sk-gpushare-* key on the same account still hits the replay. (The wallet is shared account-wide, so anything narrower would leak duplicate charges.)
  • Concurrency: when two requests carrying the same key arrive at once, exactly one executes and the other immediately gets 409 idempotency_in_flight — they never both run.
import uuid, requests

idem = str(uuid.uuid4())           # one key per submit intent
body = {
    "model": "doubao-seedance-2-0-260128",
    "content": [{"type": "text", "text": "sunset over the sea, drone shot"}],
    "duration": 5,
}

def submit():
    r = requests.post(
        "https://dianqi.zsopc.com/v1/videos/generations",
        headers={
            "Authorization": f"Bearer {PLATFORM_API_KEY}",
            "Idempotency-Key": idem,          # same key on every retry
        },
        json=body,
        timeout=60,
    )
    r.raise_for_status()
    # "true" on a replay: you got the first call's result, with no second charge
    replayed = r.headers.get("Idempotency-Replayed") == "true"
    return r.json()["id"], replayed

task_id, _ = submit()
task_id_again, replayed = submit()   # lost the id? just call it again
assert task_id == task_id_again and replayed
const idem = crypto.randomUUID();          // one key per submit intent

async function submit() {
  const res = await fetch("https://dianqi.zsopc.com/v1/videos/generations", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${PLATFORM_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idem,             // same key on every retry
    },
    body: JSON.stringify({
      model: "doubao-seedance-2-0-260128",
      content: [{ type: "text", text: "sunset over the sea, drone shot" }],
      duration: 5,
    }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return {
    id: (await res.json()).id,
    replayed: res.headers.get("Idempotency-Replayed") === "true",
  };
}
CaseResponse
Header absentBehaves exactly as before (no deduplication at all)
Malformed key400 invalid_idempotency_key
Same key, different body409 idempotency_key_reuse — use a fresh key
Same key, previous call still running409 idempotency_in_flight — wait and retry with the same key (a new key really would submit again). Usually a few seconds; if the previous call was cut off client-side, the key is held for up to 10 minutes before it frees itself
Same key, original response too large to retain409 idempotency_response_not_cached — only reachable for image calls that explicitly ask for response_format:"b64_json" (the bytes are too large to retain). Use the default response_format:"url" and replay works normally; the call that already happened can only be re-sent under a fresh key (and is billed again)

Only 2xx responses are remembered. Upstream errors and validation failures do not consume the key — retry with the same one.


Recovering a task id#

Async tasks (video, music, speech) return a task id on submit. If you didn't save it, there are three ways back, easiest first:

  1. Re-send the exact same curl (same Idempotency-Key) → you get the original task id straight back. See the section above.
  2. List your tasks: GET /v1/videos/generations, GET /v1/music/generations, GET /v1/audio/speech. All statuses are returned by default (including queued, running and failed), newest first; ?limit= defaults to 30, max 100.
    curl "https://dianqi.zsopc.com/v1/videos/generations?limit=10" \
      -H "Authorization: Bearer $PLATFORM_API_KEY"
    
    ?status= filters, comma-separated for multiple: video accepts queued,running,succeeded,failed,expired,cancelled,all; music processing,succeeded,failed,expired,cancelled,all; speech pending,succeeded,failed,all. Pass ?status=succeeded for the pre-2026-07-31 default.
  3. The call log site logs.dflop.top: the search box matches both task ID and request ID (paste it in — you don't have to know which kind of id you're holding). Async-task records (video, music, speech, avatar, voice) carry a task ID; calls with no task row behind them (chat, image, transcript) carry only a request ID.
    • The request ID is the value of the x-gateway-trace response header.
    • ⚠️ An async task is only recorded here once it reaches a terminal state and settles; for a task still generating, use the list endpoints in point 2. The log defaults to the last 30 days.

POST /v1/images/generations#

OpenAI Images API-compatible shape, returned synchronously.

Available models#

Model IDDisplay namePrice (per image)Notes
doubao-seedream-4-0-250828Seedream 4.011.73size ≥ 960×960
doubao-seedream-4-5-251128Seedream 4.514.96size must be ≥ 1920×1920, or upstream returns 400
doubao-seedream-5-0-260128Seedream 5.012.94size must be ≥ 1920×1920, or upstream returns 400
doubao-seedream-5-0-pro-260628Seedream 5.0 Pro17.79 for output ≤ 2.36 MP, 35.59 above thatsize ≥ 960×960. Omitting size means upstream defaults to 2048×2048 and you pay the 35.59 tier — to stay on the lower tier, pass a size ≤ 2.36 MP explicitly (e.g. 1536x1536). Each image[] reference adds 1.21 of input (on the same billing line)
grok-imagine-imageGrok Imagine (Image)28.31standard tier
grok-imagine-image-qualityGrok Imagine (Quality)28.31high-quality tier

size is passed straight through and never rewritten by the gateway — asking Seedream 4.5/5.0 for less than 1920×1920 gets you the upstream's 400 directly. Seedream 5.0 Pro is tiered on the requested output pixel area (the threshold is 2.36 MP ≈ 1536×1536).

The request#

{
  "model": "doubao-seedream-4-5-251128",
  "prompt": "A panda drinking tea in a bamboo grove, watercolour style",
  "size": "2048x2048",
  "n": 1
}
FieldRequiredNotes
modela Model ID from the table above
promptthe description
size"WxH", passed through upstream (mind each SKU's minimum)
nhow many images, default 1, max 10 (more returns 400 invalid_request). unit price × n is held on submit and settled against the number actually returned
imagearray of reference-image URLs (image-to-image; Seedream takes 1–10)

The response#

{
  "model": "doubao-seedream-4-5-251128",
  "created": 1765432100,
  "data": [{ "url": "https://...", "size": "2048x2048" }],
  "usage": { "generated_images": 1, "output_tokens": 4096, "total_tokens": 4096 }
}

The response is the upstream's own (OpenAI Images shape); the usage fields are whatever upstream actually returns.

curl#

curl https://dianqi.zsopc.com/v1/images/generations \
  -H "Authorization: Bearer $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao-seedream-4-5-251128",
    "prompt": "A panda drinking tea in a bamboo grove, watercolour style",
    "size": "2048x2048"
  }'

Limitations#

  • The returned image URLs are upstream pre-signed links that expire in about 24 hours — download and store them promptly
  • This is a synchronous endpoint. The gateway's per-attempt upstream timeout is 240 seconds (IMAGES_UPSTREAM_TIMEOUT_SECS) and the whole channel ladder is capped at 280 seconds (IMAGES_LADDER_DEADLINE_SECS). Most SKUs finish in 5–20 seconds, but gpt-image-2 measurably takes 60–215 seconds — set your client timeout to ≥ 300 seconds, or your own timeout will cut off a request the gateway is still legitimately waiting on (you are still billed, you just never receive the result)
  • Errors use the OpenAI shape {"error": {"code", "message", "param", "type"}}; upstream 4xx/5xx pass through with their original status and body (and aren't billed)

The Nano Banana family (nano-banana 15.77, nano-banana-pro 54.19, nano-banana-2 16.18 per image) also runs through this endpoint and bills per image. nano-banana-2 returns b64_json (its first hop is our own pool); the others usually return URLs. The older ids (gemini-2.5-flash-image, gemini-3-pro-image-preview, gemini-3.1-flash-image(-preview), tvod-nano-*) remain supported as aliases.


POST /v1/videos/generations#

An async task: submitting returns a task id immediately, and you poll until succeeded.

Available models#

Model IDDisplay namePrice (per second)Notes
doubao-seedance-1-0-pro-fast-251015Seedance 1.0 Pro Fast32.35
doubao-seedance-1-0-pro-250528Seedance 1.0 Pro60.66
doubao-seedance-1-5-pro-251215Seedance 1.5 Pro72.79
doubao-seedance-2-0-fast-260128Seedance 2.0 Fast48.53
doubao-seedance-2-0-260128Seedance 2.088.97
doubao-seedance-2.0Seedance 2.0by resolution: 480p 33.16 / 720p 59.45 / 1080p 147.61 / 2k 291.17 / 4k 355.87supports real-person photos on camera; reference images are moderated and registered automatically
doubao-seedance-2.0-fastSeedance 2.0 Fastby resolution: 480p 23.86 / 720p 47.72 / 1080p 117.28 / 2k 141.54 / 4k 169.85
doubao-seedance-2.0-miniSeedance 2.0 Miniby resolution: 480p 14.96 / 720p 29.93lightweight tier; 480p/720p only, 4–15 seconds
grok-imagine-videoGrok Imagine Video283.08text-to-video and image-to-video
grok-imagine-video-1.5-previewGrok Imagine Video 1.5586.38image-to-video only (upstream 400s without a reference image)
dh-avatarDigital human videoflat per video second (see the in-app catalogue for pricing)requires a reusable avatar first (created from a photo or video); avatar + driving audio (or text + voice) → a talking video
clip-realmanSmart edit · talking head4.04/second (of finished video)a talking-head source video + a template → a finished cut with title, subtitles, name bar and music
clip-mixcutSmart edit · asset mixcut4.04/second (of finished video)narration audio + image/video assets + a template → a subtitled, packaged cut
clip-newsSmart edit · news brief2.43/second (of finished video)a headline + image/video assets + a template → a news-style short, 5–300 seconds

Submitting#

{
  "model": "doubao-seedance-1-0-pro-fast-251015",
  "content": [
    { "type": "text", "text": "Sunset over the sea, aerial drone view --ratio 16:9" }
  ],
  "duration": 5
}
FieldRequiredNotes
modela Model ID from the table above (grok SKUs use the same shape; the gateway translates)
contentan array: {type:"text", text} is mandatory; for image-to-video append {type:"image_url", image_url:{url:"https://..."}}
durationseconds. Omitted, we hold 12 seconds' worth (Seedance's ceiling) and settle on the real length — pass it explicitly
ratio / resolution / watermarkpassed through upstream (Seedance's own semantics). ⚠️ resolution is required on the Lite (-lite) SKUs: the whole card prices on delivered resolution, so omitting it returns 400; the vocabulary is 720p / 1080p only, and anything else 400s too
video_modeonly meaningful on grok SKUs with a reference video: "extend" continues it, anything else (or omitted) rewrites it (see below)

How grok video is translated: grok SKUs run on an xAI upstream, and the gateway converts the Seedance shape above into xAI's. Appending {type:"video_url", video_url:{url:"https://..."}} routes the task to the video-to-video endpoint, which rewrites by default (redrawing the whole clip from your prompt, keeping the source aspect ratio and resolution, ignoring a custom duration); video_mode: "extend" switches to continuation (carrying on from the last frame for duration seconds, 2–10). For image-to-video the gateway deliberately withholds ratio so the source image's native aspect ratio is preserved and nothing gets stretched.

Real people on camera#

doubao-seedance-2.0, doubao-seedance-2.0-fast and doubao-seedance-2.0-mini support uploading a photo of a real person and generating video of them on camera. The compliance path runs entirely inside the gateway and needs nothing special from you — submit the ordinary image-to-video shape and the gateway moderates and registers the reference image (swapping in a compliant asset handle) before generating. If the preferred channel refuses a real-person image, the gateway transparently switches to one that supports registering real-person assets.

{
  "model": "doubao-seedance-2.0",
  "resolution": "720p",
  "duration": 5,
  "content": [
    { "type": "text", "text": "The person in the photo smiles and waves at the camera, background unchanged" },
    { "type": "image_url", "image_url": { "url": "https://your-cdn.com/face.jpg" } }
  ],
  "portrait_auth": true
}
FieldNotes
image_url.urlmust be a publicly fetchable http(s) URL (the upstream pulls it from the open internet). base64 / data: inline images are not supported and are rejected with 400. The image must be reachable from mainland China (our own r2.dflop.top links work; some overseas hosts can't be fetched).
portrait_authOptional boolean asserting "I have the portrait rights for the real person shown", recorded for audit. It does not affect whether video is produced (real-person routing follows the SKU), but passing true explicitly is recommended whenever real people are involved, as a statement of responsibility.
resolutionThe real-person vocabulary: doubao-seedance-2.0 takes 480p/720p/1080p/2k/4k, -fast takes 480p/720p/1080p, -mini only 480p/720p. Omitted, upstream picks its default and the flat rate applies.

Multimodal references (Seedance 2.0 only): beyond a single first-frame image, content[] can carry reference media items tagged with a role{type:"image_url", role:"reference_image", image_url:{url}}, {type:"video_url", role:"reference_video", video_url:{url}} and {type:"audio_url", role:"reference_audio", audio_url:{url}} (up to 10 reference images). Every external image follows the same public-URL and automatic-moderation rules above.

Response:

{ "id": "9f2c...", "status": "queued", "model": "doubao-seedance-1-0-pro-fast-251015", "created_at": 1765432100 }

Submission itself is a synchronous HTTP call (with a 60-second gateway-to-upstream timeout); generation then proceeds in the background and doesn't hold the request open.

Digital-human fields (dh-avatar)#

dh-avatar reuses the same video submission endpoint, adding the fields below at the top level of the body (duration is required — the estimated seconds of the driving audio or script; omitting it returns 400). ⚠️ Digital human is a two-stage process: you need a reusable avatar first, created in the app under "Clone avatar" from a photo or video (the platform's shared avatars have been withdrawn from the UI because upstream stopped returning previews). Calling with an sk-key, just pass an existing avatar id.

The video is as long as its driving audio or script (upstream measures the real length; there's no fixed cap). duration only sizes the billing hold, and settlement uses upstream's actual seconds.

FieldRequiredNotes
avatarthe avatar id (created once under "Clone avatar" in the app, then reusable)
audio_urlone of the twodriving audio (public URL, mp3/wav) — makes the avatar speak it
voice + textone of the twotext-driven: voice is a voice id (shared or cloned) and text the script (≤10,000 characters); upstream synthesises it and drives the avatar in one step
titlea name for the piece (≤20 characters)

Things to note:

  • input media URLs must be publicly reachable (anything you've uploaded to our r2.dflop.top works directly);
  • finished videos carry an automatic "AI generated" mark, as China's AIGC labelling rules require.

Smart-edit fields (clip-realman / clip-mixcut / clip-news)#

The three smart-edit SKUs reuse the same video submission endpoint, adding the fields below at the top level. All three share style_id (the template id), title, language, materials[], bgm and cover_url, each with its own extra requirements. The length of the finished video comes from the source media (realman from the source video, mixcut from the narration audio, news from duration); for realman and mixcut, duration is only a billing hint and is never sent upstream.

{
  "model": "clip-realman",
  "style_id": "tpl_xxx",
  "title": "Today's headlines",
  "source_video_url": "https://your-cdn.com/talk.mp4",
  "materials": [
    { "type": "image", "file_url": "https://your-cdn.com/a.jpg" },
    { "type": "video", "file_url": "https://your-cdn.com/b.mp4", "sound_switch": false }
  ],
  "bgm": { "mode": "auto" }
}
FieldApplies toNotes
style_idall ✓template id (from the platform's smart-edit template library)
source_video_urlrealman ✓the talking-head source video (public URL)
audio_urlmixcut ✓the narration audio (public URL)
materialsmixcut/news ✓, optional for realmanan array of {type:"image"|"video", file_url, sound_switch?}, at most 10
titlenews ✓, optional elsewherethe piece's or story's headline
durationnewstarget length in seconds, 5–300 (clamped if out of range); a billing hint only for realman/mixcut
material_compositionnewsrandom or order; random by default
preprocessrealmanasset pre-processing: roughCut or sliceMerge
bgmall{mode:"auto"|"none"|"custom", url?, volume?}; follows the template by default
cover_urlalla custom first-frame cover (public image URL)
introduce_cardallthe name bar, {name, description}
languageallsubtitle language

Billing: charged on the actual length of the finished video (the real seconds reported by polling). Submissions with an external sk-key hold the clip ceiling (300 seconds) and settle down to the real length on success; passing a longer duration explicitly (for a long source video, say) holds that instead. Insufficient balance returns 402, and failures or expiry are refunded in full.

Template ids: style_id comes from the platform's smart-edit template library. Template discovery currently exists only inside the Digital Human studio in the app, so calling directly with an sk-key means using a template id you already know.

Asset and media requirements (upstream hard limits)#

Every URL must be publicly fetchable. The limits below are the upstream's own, and anything outside them is rejected there (the in-app Digital Human studio validates format, resolution, length and size at upload time).

MediumFormatSizeResolutionLength
Talking-head source video source_video_urlmp4 / mov (h264 or HEVC, 10–60fps, 25 recommended)< 500MB< 2000px per side< 5 minutes
Asset image materials[].file_url (image)static jpg / png / webp< 2000px per sidecounts as 2s each
Asset video materials[].file_url (video)mp4 / mov< 500MB< 2000px per side≤ 60s each
Narration audio audio_url (mixcut)mp3 / wav / m4a≤ 120MB≤ 5 minutes, must be transcribable
Background music bgm.urlmp3 / wav / m4a≤ 120MB≤ 5 minutes
First-frame cover cover_urljpg / jpeg / png≤ 10MB< 2000px per side
  • Total asset length ≤ 5 minutes: images count as 2s each and videos at their real length; upstream rejects anything longer.
  • The talking-head source video's audio must be transcribable (it drives the automatic subtitles); without clear speech the task fails.
  • clip-news output length is set by duration (5–300s); clip-realman and clip-mixcut follow the source video and the narration audio respectively.

Polling#

curl https://dianqi.zsopc.com/v1/videos/generations/$TASK_ID \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

status runs queuedrunningsucceeded / failed / expired / cancelled.

In-flight tasks (queued / running) may carry progress (an integer 0–100 from upstream). It appears only when upstream reports progress — today just the Seedance 2.0 real-person tiers — and its absence means that model has no progress data, not 0%.

On success:

{
  "id": "9f2c...",
  "status": "succeeded",
  "model": "doubao-seedance-1-0-pro-fast-251015",
  "created_at": 1765432100,
  "video_url": "https://...",
  "expires_at": 1765435700
}

Failures carry error: {code, message}. Generation usually takes 1–5 minutes; poll every 5–10 seconds.

Task ids are visible only to their own account — polling a nonexistent task or someone else's returns the same 404 (code: "not_found"), with no distinction drawn.

List tasks#

GET /v1/videos/generations (no id) — this account's video tasks, newest first. Free. Use it to recover a task id you didn't save, or just as a generation history.

Query paramDefaultNotes
limit301–100; anything larger is clamped to 100
status(all)queued / running / succeeded / failed / expired / cancelled / all, comma-separated for multiple (e.g. ?status=queued,running). An unrecognised value returns 400 invalid_request

All statuses are returned by default as of 2026-07-31. The previous default returned only succeeded, which made the list look empty while a task was still running. Pass ?status=succeeded for the old behaviour.

{
  "data": [
    {
      "id": "9a31d5c2-5c13-4caf-ad8b-1ee70ff5887f",
      "status": "running",
      "model": "doubao-seedance-2.0-fast-lite",
      "created_at": 1785495460,
      "progress": 42
    },
    {
      "id": "ed2ab10b-ceb1-4d0c-8c25-84ade94c95ac",
      "status": "succeeded",
      "model": "doubao-seedance-1-0-pro-fast-251015",
      "created_at": 1785490000,
      "video_url": "https://...",
      "expires_at": 1786094800
    }
  ]
}
FieldPresent whenNotes
idalwaysThe task id — the same value GET /v1/videos/generations/{id} takes
statusalwaysSame vocabulary as the polling endpoint
modelalwaysThe submitted Model ID (canonicalised)
created_atalwaysSubmit time, Unix seconds
progressqueued/running, and only when upstream reports itInteger 0–100. An absent field means the model has no progress data — not 0%
video_urlsucceeded onlyA presigned link valid for 7 days; calling this endpoint again re-signs an expired one
expires_atsucceeded onlyWhen video_url expires, Unix seconds
output_filessucceeded, subtitle SKUs onlyOne download link per language
errorfailed/expired/cancelled only{code, message}
import requests
r = requests.get(
    "https://dianqi.zsopc.com/v1/videos/generations",
    headers={"Authorization": f"Bearer {PLATFORM_API_KEY}"},
    params={"limit": 20},                    # in-flight only: {"status": "queued,running"}
    timeout=30,
)
for t in r.json()["data"]:
    print(t["id"], t["status"], t.get("video_url", ""))

How billing works#

  • On submit, unit price × duration is held against the balance (12 seconds if duration is missing); an insufficient balance returns 402
  • Settlement on a final state: success charges the actual length (falling back to the requested seconds when upstream doesn't report one), and failure or expiry is refunded in full; any failure during submission (an upstream error, a failed insert) releases the hold immediately
  • GET /v1/videos/generations (with no id) lists this account's tasks, all statuses by default (?limit= default 30, max 100; ?status=succeeded for successes only) — useful both as a "generation history" and to recover a lost task id (see Recovering a task id)

Limitations#

  • Successful videos are copied into our own object storage, and video_url is a pre-signed link valid for 7 days (expires_at is when it dies); calling the list endpoint again after expiry re-signs a fresh link
  • Upstream moderation can intercept after generation finishes (error codes like OutputVideoSensitiveContentDetected); that counts as a failure and isn't billed

POST /v1/music/generations#

Suno AI music generation, with the same async task shape as video: submit for a task id, then poll to a final state. One generation yields 2 complete songs (with lyrics and cover art).

Available models#

Model IDDisplay namePrice (per generation)
suno-v3.5Suno V3.519.41
suno-v4Suno V419.41
suno-v4.5Suno V4.519.41
suno-v5Suno V519.41
suno-v5.5Suno V5.5 (latest)19.41

The request#

curl https://dianqi.zsopc.com/v1/music/generations \
  -H "Authorization: Bearer $GPUSHARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "suno-v5.5",
    "prompt": "An upbeat Mandarin pop song about a summer walk by the sea"
  }'
FieldTypeNotes
modelstringrequired; any id from the table above
promptstringinspiration mode (≤200 characters): the AI writes the lyrics, picks a title and sings
lyricsstringyour own lyrics (≤3000 characters); passing them switches to custom mode and prompt is ignored
titlestringsong title (custom mode)
tagsstringstyle, e.g. "synthwave, female vocal"
negative_tagsstringstyles to avoid
instrumentalboolinstrumental only (lyrics ignored)

Pass at least one of prompt and lyrics (both may be omitted when instrumental: true). Response: {"id": "<task_id>", "status": "queued", "model": "...", "created_at": ...}.

Polling the task#

curl https://dianqi.zsopc.com/v1/music/generations/$TASK_ID \
  -H "Authorization: Bearer $GPUSHARE_API_KEY"

status is one of processing | succeeded | failed | expired. On success the tracks array gives each song:

{
  "id": "…",
  "status": "succeeded",
  "tracks": [
    {
      "clip_id": "…",
      "title": "Slow Sea Breeze",
      "duration_sec": 192.0,
      "audio_url": "https://…mp3",
      "image_url": "https://…jpeg",
      "lyrics": "[Verse]…"
    }
  ]
}

Generation usually takes 2–4 minutes; poll every 10–20 seconds. Task ids are private to their account, and someone else's or a nonexistent one always returns 404.

List tasks#

GET /v1/music/generations (no id) — this account's music tasks, newest first. Free. Use it to recover a task id you didn't save.

Query paramDefaultNotes
limit301–100
status(all)processing / succeeded / failed / expired / cancelled / all, comma-separated for multiple. ⚠️ The music family has no queued/running — everything between submit and terminal is processing (same vocabulary as the polling endpoint). An unrecognised value returns 400

The response is {"data": [ … ]}, and each item is field-for-field identical to the single-task polling response above (id / model / status / upstream_status / tracks[] / error_code / error_message / created_at / completed_at), so a list item can be consumed exactly like a poll result — no second parser needed.

curl "https://dianqi.zsopc.com/v1/music/generations?limit=10&status=processing" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

How billing works#

  • The flat unit price is held on submit (one generation = 2 songs, already included); an insufficient balance returns 402
  • Settlement: one successful song is enough to charge full price; both failing, or 30 minutes without completion (expiry), is refunded in full, and any failure during submission releases the hold immediately
  • Audio and cover art are copied into our own object storage, with audio_url / image_url as 7-day pre-signed links; if the copy fails we fall back to the upstream's original links

POST /v1/audio/speech#

Speech synthesis: text (≤5000 characters) → MP3, with cloned voices and speed adjustment (speed applies to cloned voices only). Billed per input character (voice-tts-pro, 125.36 per 1,000 characters).

⚠️ Use async mode for long scripts. Synthesis runs on an upstream async queue (seconds for short text, minutes for long), while a synchronous call is bound by the CDN's roughly 100-second ceiling on non-streaming responses — and when that cuts you off, the audio still renders and you're still charged, but you get nothing back. Add "async": true to the body to switch to submit-and-poll.

The request#

{
  "model": "voice-tts-pro",
  "input": "Hello, and welcome to speech synthesis.",
  "voice": "<optional: a platform preset voice id, or a cloned voice id from /v1/audio/voices; omitted means the default voice>",
  "speed": 1.0,
  "async": false
}

The response (synchronous, async omitted or false)#

{
  "model": "voice-tts-pro",
  "audio_url": "https://r2.dflop.top/audio-speech/…/xxx.mp3",
  "characters": 12,
  "cost_usd": "0.0037"
}

audio_url is a permanent public link in our object storage, and can be fed straight into a digital human's (dh-avatar) audio_url.

The response("async": true)#

Returns a task id immediately, without blocking:

{ "id": "3a8e…", "model": "voice-tts-pro", "status": "pending", "characters": 1200, "created_at": "…" }

Then poll GET /v1/audio/speech/{id} (free):

{
  "id": "3a8e…", "model": "voice-tts-pro", "status": "succeeded",
  "characters": 1200, "duration_sec": "86.40",
  "audio_url": "https://r2.dflop.top/audio-speech/…/xxx.mp3", "created_at": "…"
}

status has three states: pending / succeeded / failed. Failures are refunded in full automatically; you're only charged on success, and the audio again lands on a permanent link.

List tasks#

GET /v1/audio/speech (no id) — this account's synthesis tasks, newest first. Free. Use it to recover the task id from an "async": true submit you didn't save.

Query paramDefaultNotes
limit301–100
status(all)pending / succeeded / failed / all, comma-separated for multiple. An unrecognised value returns 400

The response is {"data": [ … ]}, and each item is field-for-field identical to GET /v1/audio/speech/{id} (id / model / status / characters / created_at, plus audio_url / duration_sec on success and error.message on failure).

curl "https://dianqi.zsopc.com/v1/audio/speech?limit=10" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

/v1/audio/voices — voice cloning and management#

MethodPathNotes
POST/v1/audio/voicesClone a voice: {name, audio_url, async?} (a public reference-audio URL, 5 seconds to 3 minutes of clear speech). Billed per call (voice-clone-pro, 40.44 each)
GET/v1/audio/voicesList this account's cloned voices plus the platform presets: {voices:[…], presets:[{id, name}]}
GET/v1/audio/voices/{id}Check one voice's status (pending / ready / failed)
DELETE/v1/audio/voices/{id}Delete a voice (our record of it)

Cloning also supports "async": true: it returns {id, status:"pending"} immediately and you poll GET /v1/audio/voices/{id} until ready. By default it blocks until ready (tens of seconds to minutes) — which, as above, runs into the CDN's ~100-second ceiling, so new integrations should always use async. Failures are refunded in full automatically.

Pass the resulting voice id as voice on /v1/audio/speech to synthesise with it, or as the voice for a text-driven digital human (dh-avatar; see Digital human API). Before cloning a real person's voice, make sure you have their permission.

The platform also offers a set of shared voices (the presets in the GET /v1/audio/voices response) — pass one of their ids as voice with no cloning required.


POST /v1/transcripts/extract#

Short-video link → spoken script: paste a share link or share token from a short video and get back the video's spoken script plus metadata (title, cover, platform, length). The upstream detects the platform itself (Douyin, Kuaishou, Xiaohongshu, Bilibili, WeChat Channels and other major sites), so you don't specify a source.

The server blocks synchronously until extraction finishes (internally: create a task, then poll upstream — usually 5–40 seconds, at most about 55). Set a generous client read timeout (≥ 90 seconds recommended). The upstream's concurrency ceiling is low, so heavy parallel use queues up and slows down.

The request#

{
  "url": "https://v.douyin.com/xxxxxx/   — or just paste the raw share token"
}
FieldNotes
urlRequired. A short-video share link or raw share token (≤ 2000 characters). input is an equivalent alias.

The response#

{
  "model": "video-transcript",
  "content": "the extracted spoken script…",
  "title": "the original video title",
  "cover": "https://… cover image URL",
  "platform": "douyin",
  "duration_sec": 42,
  "origin_link": "https://… the original link echoed by upstream"
}

platform is the platform upstream identified (douyin, kuaishou and so on). content is the script itself; title, cover and duration_sec are supporting metadata and may be empty when the video has no such field.

curl#

curl -X POST https://dianqi.zsopc.com/v1/transcripts/extract \
  -H "Authorization: Bearer $GPUSHARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://v.douyin.com/xxxxxx/"}'

How billing works#

  • A flat per-call charge (video-transcript, 20.22 each) against the account balance (shared by all keys).
  • Only successes are billed: an unparseable link, an unsupported video, an extraction timeout, or a script blocked by content safety are all free; only a clean, successful script costs you one call.
  • Each call is recorded in usage and the call log as unit_type=transcript (the response carries x-gateway-trace; include it with the timestamp when reporting a problem).

Limits and errors#

  • The cover URL may be time-limited (roughly 24 hours) — download and store it yourself if you need it long-term.
  • Input is capped at 2000 characters, and the upstream's low concurrency ceiling means heavy parallel use queues.
  • Errors use the normalised shape shared with every other endpoint: an invalid link or unsupported video → 400, an extraction timeout → 504, service quota temporarily exhausted → 503, an upstream connection failure → 502, insufficient balance → 402.
  • To restrict a key to this capability only, put video-transcript in its allowed_models (no allowed_models means every model the account can use).