calesthio/vidu-video
Plan and integrate ShengShu Vidu Open Platform video generation with current model/mode selection, reference consistency, pricing, exact approval, async task handling, and safe artifact custody. Use for Vidu API text-to-video, image-to-video, start/end-frame, multi-subject reference, media-reference, or Q2 multi-frame work; do not conflate the API with vidu.com consumer plans or Vidu-S1 streaming digital humans.
npx skills add https://github.com/calesthio/generative-media-skills --skill vidu-video
Use this skill to design a production request or integration for the Vidu Open Platform API. Default to dry-run planning. Never submit a paid generation, upload private media, cancel a task, or download a result until the operator has approved the exact provider, endpoint, model, inputs, request digest, maximum charge, storage destination, and disclosure plan.
This document was verified against first-party public documentation on 2026-07-10. Models, prices, accepted fields, policies, and availability are volatile; re-open the linked endpoint and pricing pages immediately before execution.
https://api.vidu.com, API keys, organization-level limits, asynchronous tasks, and API credits. This is the surface covered here.https://www.vidu.com and its subscription/plan credits. Do not assume its credits, features, watermark behavior, or terms apply to the API.0.03125 credit unit price, deducts every 6 seconds, rounds to 2-second intervals, and requires at least 45 credits to create a session. Do not revalue those statements using the clip API's $0.005 credit price without written confirmation.api.vidu.cn for China and api.vidu.com internationally. The ordinary clip endpoints documented here use api.vidu.com; do not infer compute residency from the hostname.The endpoint contract is more authoritative than a family name. A model may exist without supporting every mode.
| Need | Endpoint | Production-safe current choices | Important limits |
|---|---|---|---|
| Prompt only | POST /ent/v2/text2video | viduq3-turbo, viduq3-pro; Q2/Q1 only for a documented compatibility need | Q3: 1–16 s, 540p/720p/1080p; aspect 16:9, 9:16, 3:4, 4:3, 1:1 |
| Animate one start image | POST /ent/v2/img2video | Q3 pro-fast/turbo/pro; Q2 pro-fast/pro/turbo; Q1; Vidu 2.0 | Exactly one image; URL or data URL; PNG/JPEG/JPG/WebP; ratio strictly within 1:4–4:1; ≤50 MB; entire POST body ≤20 MB |
| Bridge exact start and end | POST /ent/v2/start-end2video | Q3 turbo/pro; Q2 pro-fast/pro/turbo; Q1/classic; Vidu 2.0 | Exactly two ordered images; their aspect-ratio ratio must be 0.8–1.25; Q3 1–16 s; Q2 endpoint documents 1–8 s |
| Named entities / dialogue | POST /ent/v2/reference2video, subjects form | The page's accepted list is Q3 turbo, Q3, Q2, Q1, Vidu 2.0 | ≤7 total image/text subject items; ≤3 images per subject; reference with @name; Q3 duration 3–16 s |
| Unnamed reference images | Same endpoint, images form | Q3 mix/turbo/Q3, Q2 pro/Q2, Q1, Vidu 2.0 | 1–7 images normally; Q2-pro allows 1–4 images when videos are also supplied |
| Reference video / edit | Same endpoint, videos form | viduq2-pro only | At most one 8 s video or two 5 s videos; MP4/AVI/MOV; ≤100 MB each; decoded base64 <20 MB |
| Multi-keyframe continuity | POST /ent/v2/multiframe | viduq2-pro, viduq2-turbo only | Ordered keyframes; each segment duration 2–7 s; 540p/720p/1080p; verify the current page because its frame/segment counting and pricing prose are ambiguous |
images reference-to-video only; 720p/1080p. The pricing table says 3–16 s while the endpoint prose says 1–16 s. Use the safe intersection, 3–16 s, until Vidu resolves the conflict. It does not currently support the named-entity subjects form or off-peak.The public Model Map lags some endpoint pages: for example, it does not enumerate all newer Q3 reference identifiers. Resolve conflicts in this order: the exact endpoint accepted-values list, the current Pricing page, then the model map/update notice. If those still conflict, stop and confirm in the signed-in console or with Vidu support.
audio: true asks for synchronized generated speech/effects; the reference subjects form also supports audio_type (all, speech_only, sound-effect_only) and per-subject voice_id.bgm; do not set both and assume a mix. voice_id is documented ineffective on Q3 image-to-video.audio explicitly. For a visual-motion test use false; for approved dialogue use named subjects, exact quoted lines, and audio_type.Facts from the endpoint contract:
subjects[].name is invoked as @name in the prompt. auto_subjects is a separate intelligent-entity-library option; do not enable it without knowing which library assets are selected.Production heuristics, not provider guarantees:
@Mara, red linen coat, short black bob; coat and face remain unchanged.Stage inputs before request construction:
PUT, finish) and currently documents an image limit below 10 MB, unlike the generation pages' 50 MB file limit and 20 MB request-body limit. Obtain separate approval, validate bytes before upload, record the returned ETag/URI, and do not infer a retention or deletion SLA from the example's storage headers.Intent and constraints: Keep one actor and one designed object identifiable during a six-second rainy-market dialogue shot. Use the subjects form because the prompt names entities with @name; use Q3 Turbo, normal mode, 720p, and explicit audiovisual controls for a low-cost identity test. This is a schema illustration only; do not run it without exact approval.
{
"model": "viduq3-turbo",
"auto_subjects": false,
"subjects": [
{
"name": "Mara",
"images": [
"https://assets.example.test/mara-front.jpg",
"https://assets.example.test/mara-three-quarter.jpg"
],
"voice_id": "APPROVED_VOICE_ID"
},
{
"name": "OrchidDrone",
"images": ["https://assets.example.test/orchid-drone.png"]
}
],
"prompt": "@Mara walks beside @OrchidDrone through a rainy market. Their designs remain unchanged. One continuous medium tracking shot. @Mara says: 'We leave before sunrise.' Rain and footsteps only after the line.",
"duration": 6,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": true,
"audio_type": "all",
"seed": 18427,
"off_peak": false,
"payload": "job-20260710-shot-014"
}
Expected quote and result: At the current normal Q3 Turbo reference rate, 6 × 10 = 60 API credits, or $0.30 before tax. Expect one continuous tracking shot with the approved line and ambience, not guaranteed identity or verbatim speech.
Review and repair: Reject face/object drift, an unapproved voice, changed wording, extra music, or cuts. First simplify action/camera and retest the same short setting; use speech_only only when the brief does not require rain/footsteps, and obtain a new digest and approval for every changed request.
Intent and constraints: Transform one cleared paper-bird frame into a second cleared flight frame while locking the background and camera. Start/end mode is the correct control because both endpoint compositions matter. Q3 Pro, 5 seconds, 1080p, silent, normal mode is a quality-oriented approved variant.
{
"model": "viduq3-pro",
"images": [
"https://assets.example.test/shot-08-start.png",
"https://assets.example.test/shot-08-end.png"
],
"prompt": "The paper bird unfolds into flight while the locked camera remains still; preserve the cream background and cobalt ink texture.",
"duration": 5,
"resolution": "1080p",
"audio": false,
"seed": 9081,
"off_peak": false,
"payload": "job-20260710-shot-008"
}
Expected quote and result: 5 × 24 = 120 API credits, or $0.60 before tax. Expect interpolation toward the supplied end, not an exact geometric morph.
Review and repair: Reject an end-frame snap, topology break, camera motion, or background redesign. Make the two source frames more compatible before lengthening; a cheaper Q3 Turbo 720p motion test is a meaningful variation but requires a new request and approval.
Intent and constraints: Use three cleared product views to preserve an unnamed lamp across a five-second rotation. Use the direct images form and Q3 Mix because named subjects are not supported. Five seconds is inside the documented 3–16-second pricing range and the endpoint's broader 1–16-second prose.
{
"model": "viduq3-mix",
"images": [
"https://assets.example.test/product-front.png",
"https://assets.example.test/product-side.png",
"https://assets.example.test/product-detail.png"
],
"prompt": "The same brass desk lamp rotates slowly on a dark walnut table, one continuous product shot, no redesign, no text or logos added.",
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"seed": 22109,
"off_peak": false,
"payload": "job-20260710-product-003"
}
Expected quote and result: 5 × 24 = 120 API credits, or $0.60 before tax. Q3 Mix has no listed off-peak rate. The media-list request schema does not currently document an audio request field even though the model description mentions audiovisual output, so do not invent the field or promise silence; inspect the returned streams and confirm with Vidu if audio state is consequential.
Review and repair: Reject product redesign, added text/logo, missing details, multi-shot cuts, or unexpected audio. Improve reference consistency/crops before adding more images. Q3 Turbo reference at 720p is the lower-cost variation; changing models requires a new quote, digest, and approval.
API credits currently cost $0.005 each, before applicable tax. On 2025-11-03 Vidu multiplied the numerical denomination by 10 without changing value; ignore pre-adjustment credit examples.
Selected current normal/off-peak rates per generated second:
| Model/mode | 540p | 720p | 1080p |
|---|---:|---:|---:|
| Q3 pro prompt/image/start-end | 9 / 5 credits | 20 / 10 | 24 / 12 |
| Q3 turbo prompt/image/start-end | 7 / 4 | 11 / 6 | 13 / 7 |
| Q3 pro-fast image | — | 20 / 10 | 25 / 13 |
| Q3 mix reference | — | 24 / no off-peak | 29 / no off-peak |
| Q3 turbo reference | 4 / 2 | 10 / 5 | 13 / 7 |
| Q3 reference | 7 / 4 | 12 / 6 | 15 / 7 |
Example: Q3 turbo text-to-video, 5 s, 720p, normal = 11 × 5 = 55 credits = $0.275 before tax. Q3 pro, 5 s, 1080p, normal = 24 × 5 = 120 credits = $0.60.
off_peak: true is cheaper but may take up to 48 hours; unfinished tasks are automatically cancelled and refunded. Compatibility is conditional: the reference page says Q3 can use off-peak with audio, while Q2/Q1/Vidu 2.0 require audio false; Q3 mix has no off-peak price. Confirm the exact row before approval.
An approval record must include:
provider=Vidu Open Platform API
endpoint=/ent/v2/text2video
model=viduq3-turbo
mode=normal (off_peak=false)
duration=5s resolution=720p aspect=16:9 audio=false
input_manifest_sha256=<hash of canonical prompt/source-byte hashes, byte counts, media facts, and logical IDs; URLs are locators only>
request_sha256=<hash of canonical final JSON body>
execution_manifest_sha256=<hash binding method, URL, non-secret key ID, input/request hashes, pricing date, expected/max charge, call count, retries, and destination>
pricing_verified_on=<UTC date rechecked against pricing page/console>
expected=55 API credits / USD 0.275 before tax
max_authorized_credits=55
max_authorized_usd=<operator ceiling>
allowed_calls=1; retries=0; destination=<approved path/bucket>
The input-manifest digest must cover source bytes and media facts, not merely locator strings. The execution-manifest digest is the approval token: changing an endpoint, API-key identity, body, source, price, ceiling, call count, retry policy, or destination invalidates approval.
If the pricing page or console quote exceeds either ceiling, stop before submission. If the one allowed create returns a different credit charge, persist the task ID and a billing-exception state, make no further generation/cancellation call, and reconcile the receipt with Vidu. Do not reinterpret a general budget as authorization for another model, retry, upscale, prompt recommendation (is_rec currently adds 10 credits), or Q2 audio surcharge.
This standard-library Python example is intentionally fixed to the 55-credit request above. Default mode prints the canonical request and approval envelope and makes no network call and no state write. Rerun dry-run with the real non-secret key ID, destination, ceiling, pricing-verification date, and fresh client request ID before approval. Execution requires the exact execution-manifest digest and claims the request-body digest before the one POST. Because Vidu documents no idempotency key, any exception after transmission becomes UNKNOWN; the script never retries. Use a transactional datastore rather than this local-file illustration when multiple hosts or processes can submit.
import datetime, hashlib, json, os, pathlib, sys, urllib.error, urllib.request
from decimal import Decimal
MODE = os.getenv("VIDU_MODE", "dry-run")
client_id = os.getenv("VIDU_CLIENT_REQUEST_ID", "dryrun-example")
endpoint = "https://api.vidu.com/ent/v2/text2video"
key_id = os.getenv("VIDU_KEY_ID", "UNBOUND")
destination = os.getenv("VIDU_DESTINATION", "UNSET")
pricing_verified_on = os.getenv("VIDU_PRICING_VERIFIED_ON", "UNVERIFIED")
body = {
"model": "viduq3-turbo",
"prompt": "A cobalt paper bird unfolds and glides across a cream studio background, one locked medium shot, preserve its ink texture.",
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": False,
"seed": 18427,
"off_peak": False,
"payload": client_id,
}
wire = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
digest = hashlib.sha256(wire).hexdigest()
input_manifest = {"kind": "text-only", "prompt_sha256": hashlib.sha256(body["prompt"].encode("utf-8")).hexdigest()}
input_digest = hashlib.sha256(json.dumps(input_manifest, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
expected_credits = 55
expected_usd = Decimal("0.275")
max_authorized_credits = int(os.getenv("VIDU_MAX_CREDITS", str(expected_credits)))
max_authorized_usd = Decimal(os.getenv("VIDU_MAX_USD", str(expected_usd)))
approval_manifest = {
"provider": "Vidu Open Platform API", "method": "POST", "url": endpoint,
"key_id": key_id, "input_manifest_sha256": input_digest, "request_sha256": digest,
"pricing_verified_on": pricing_verified_on, "expected_credits": expected_credits,
"expected_usd_before_tax": str(expected_usd), "max_authorized_credits": max_authorized_credits,
"max_authorized_usd": str(max_authorized_usd),
"allowed_calls": 1, "retries": 0, "destination": destination,
}
approval_wire = json.dumps(approval_manifest, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
approval_digest = hashlib.sha256(approval_wire).hexdigest()
print(json.dumps({"mode": MODE, "execution_manifest_sha256": approval_digest,
"approval_manifest": approval_manifest, "input_manifest": input_manifest,
"body": body}, indent=2, ensure_ascii=False))
if MODE == "dry-run":
raise SystemExit(0)
if MODE != "execute":
raise SystemExit("VIDU_MODE must be dry-run or execute")
if client_id == "dryrun-example":
raise SystemExit("Set a fresh VIDU_CLIENT_REQUEST_ID")
if key_id == "UNBOUND" or destination == "UNSET":
raise SystemExit("Bind the non-secret key ID and destination before approval")
try:
verified_date = datetime.date.fromisoformat(pricing_verified_on)
except ValueError:
raise SystemExit("Set VIDU_PRICING_VERIFIED_ON to an ISO date")
if verified_date != datetime.datetime.now(datetime.timezone.utc).date():
raise SystemExit("Pricing must be revalidated on the UTC submission date")
if os.environ.get("VIDU_APPROVED_MANIFEST_SHA256") != approval_digest:
raise SystemExit("Exact execution manifest was not approved")
if max_authorized_credits < expected_credits or max_authorized_usd < expected_usd:
raise SystemExit("Approved ceiling is below expected cost")
api_key = os.environ.get("VIDU_API_KEY", "")
if not api_key:
raise SystemExit("VIDU_API_KEY is required")
state_dir = pathlib.Path(os.getenv("VIDU_STATE_DIR", ".vidu-state")).resolve()
state_dir.mkdir(parents=True, exist_ok=True)
state_path = state_dir / f"{digest}.json"
def replace_state(value):
temp = state_path.with_suffix(f".tmp-{os.getpid()}")
with open(temp, "x", encoding="utf-8") as f:
json.dump(value, f)
f.flush(); os.fsync(f.fileno())
os.replace(temp, state_path)
try:
fd = os.open(state_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
raise SystemExit("Digest already claimed; refusing a duplicate POST")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump({"state": "PREPARED", "request_sha256": digest,
"execution_manifest_sha256": approval_digest, "client_request_id": client_id}, f)
f.flush(); os.fsync(f.fileno())
req = urllib.request.Request(
endpoint, data=wire, method="POST",
headers={"Authorization": f"Token {api_key}", "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=45) as resp:
raw = resp.read(1024 * 1024 + 1)
if len(raw) > 1024 * 1024:
raise RuntimeError("response exceeds 1 MiB")
if resp.status not in (200, 201):
raise RuntimeError(f"unexpected HTTP {resp.status}")
result = json.loads(raw)
task_id = result.get("task_id")
if not isinstance(task_id, str) or not task_id:
raise RuntimeError("successful response lacks task_id")
charged_credits = result.get("credits")
if not isinstance(charged_credits, int) or isinstance(charged_credits, bool):
raise RuntimeError("successful response lacks an integer credits receipt")
except Exception as exc:
replace_state({"state": "UNKNOWN", "request_sha256": digest,
"execution_manifest_sha256": approval_digest,
"client_request_id": client_id, "error_type": type(exc).__name__})
raise SystemExit("POST outcome is UNKNOWN; do not retry. Reconcile in Vidu console/support.") from exc
final_state = "SUBMITTED" if charged_credits == expected_credits and charged_credits <= max_authorized_credits else "SUBMITTED_BILLING_EXCEPTION"
replace_state({"state": final_state, "request_sha256": digest,
"execution_manifest_sha256": approval_digest,
"client_request_id": client_id, "task_id": task_id, "charged_credits": charged_credits})
print(json.dumps({"task_id": task_id, "state": final_state,
"next": f"GET /ent/v2/tasks/{task_id}/creations"}))
if final_state != "SUBMITTED":
raise SystemExit("Task was created with a credit variance; do not submit or cancel anything else")
The local payload is echoed on the task but is not documented as a searchable Task List filter. On an ambiguous create, compare console/task history around the precise submission time, model, prompt, duration, resolution, key, and local client ID; involve Vidu support if needed. Do not issue a replacement until the original outcome is proven non-created or the operator explicitly approves duplicate risk and cost.
task_id, canonical request, digest, API key identifier (not the secret), expected ceiling, and create response.GET /ent/v2/tasks/{id}/creations with bounded exponential backoff and jitter. States are created, queueing, processing, success, failed. Do not treat a client timeout as task failure.429 also covers too-frequent requests/system throttling, so back off reads. Do not flood the queue merely because submissions are accepted.credits and creations[].{id,url,cover_url}. A failed task returns err_code; store the trace ID from structured errors. Policy failures are not prompt bugs to evade./ent/v2/tasks/{id}/cancel; it may fail once a task reaches a non-cancellable state. Obtain task-specific authorization and then confirm terminal state/refund rather than assuming {} proves billing reversal.Callbacks are optional. Vidu retries failed callback delivery three times, so handlers must be idempotent. Verify X-HMAC-ALGORITHM=hmac-sha256, access key vidu, Date freshness, and the exact ordered signed-header list before accepting the body. Reconstruct:
METHOD + "\n" + URI_PATH + "\n" + RAW_QUERY + "\n" + "vidu" + "\n" + DATE + "\n"
+ each "HeaderName:HeaderValue\n" in X-HMAC-SIGNED-HEADERS order
signature = base64(HMAC-SHA256(callback TokenSecret, signing_string))
Use constant-time comparison, retain a short-lived (access_key, nonce) replay cache, reject stale Dates, cap the raw body, parse only after signature verification, and require a known task ID plus a schema-valid state transition before accepting an event. Store the raw-event hash and resulting state durably before acknowledging. Keep polling as recovery because callback delivery is not exactly once. Use the callback credential identified by Vidu as the application's TokenSecret; do not guess that any bearer key is interchangeable.
Creation and cover URLs are documented as valid for only 24 hours. On success, immediately copy them to the approved destination using a downloader that enforces HTTPS, approved host/redirect policy, public-IP resolution on every hop, timeouts, byte ceilings, and streaming hashes. Write to a newly created quarantine file beneath a fixed destination root; reject traversal, symlink/reparse-point escapes, overwrite, and content-type/byte-signature mismatches. Never log signed query strings.
For each clip:
ffprobe to verify container, codec, actual width/height, fps, duration, and audio-stream presence;| Symptom | Check | Safe action |
|---|---|---|
| 401/403 | Header is exactly Authorization: Token …; key/account permissions | Fix auth; never print the key |
| 400 FieldInvalid | Endpoint-specific model, duration, resolution, aspect, body size | Revalidate against that endpoint; dry-run a new digest |
| 400 ModelUnavailable | Stale identifier or temporary availability | Stop; do not silently swap models |
| 429 QuotaExceeded | Five-task organization concurrency or custom quota | Let queued/running work finish; request a limit change if needed |
| 429 TooManyRequests/SystemThrottling | Poll/submission rate | Back off with jitter; respect any server guidance |
| AuditSubmitIllegal / CreationPolicyViolation | Input/output moderation | Stop and review rights/safety; do not obfuscate to bypass review |
| Long queueing | Concurrency or off-peak mode | Report the mode and age; off-peak has a 48-hour window |
| Success but URL fails | 24-hour link expired or unsafe redirect | Check task history; contact support if the artifact was not taken into custody |
| Wrong/no audio | Q3 audio, reference audio_type, voice mapping; Q3 ignores bgm | Correct schema, reprice, obtain new approval before another generation |
Take calesthio/vidu-video from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.