PixVerse API 上的图生视频不是一个请求。静帧先上去,响应回一个整数,然后生成调用才存在。这个整数 img_id 是两半之间的接缝,上传侧的任何失败都会在后面以生成错误的形式重新出现。两边的鉴权要求一模一样,都必须在请求头里带上 API-KEY 和一个每个请求都唯一的 Ai-trace-id,而两次调用的职责完全不同。

图生视频的端点

概览页在第一段就说清了形态:The Image-to-Video API transforms static images into dynamic videos with motion and effects. 它给出的端点是 POST https://app-api.pixverse.ai/openapi/v2/video/img/generate

两次调用,只有一次计费

上传调用不产出视频,它只是把字节搬到厂商那里并回一个引用。生成调用才是消耗积分的那一次,也是出现在并发上限里的那一次,因为创建 generating 状态任务的是它。两次调用需要的鉴权头写在 API 指南里。

上传调用生成调用
路径openapi/v2/image/uploadopenapi/v2/video/img/generate
作用把静帧换成引用创建生成任务
返回Resp.img_idResp.img_urlResp.video_id
消耗积分不计费按秒计费
占并发名额不占

为什么图片要先变成一个 id

生成端点收 JSON,并且把 img_id 校验成 integer <uint64>,所以 base64 数据块和本地路径在这里都没有位置。把它变成那个整数的是上传端点,而上传端点既收字节也收 URL,所以已经放在公开存储上的静帧同样要走一遍这个握手。两家主流转售商的做法正好绕开了这一步:一家收首帧和尾帧的 URL,另一家直接收 image_url,两边都不需要先换 id。这也是很多开发者第一次看到 img_id 时会觉得陌生的原因。

上传图片并拿回 img_id

# Step 1: POST the still as multipart/form-data and keep Resp.img_id.
curl --location --request POST 'https://app-api.pixverse.ai/openapi/v2/image/upload' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --form 'image=@"storyboard.png"' \
  > /tmp/pixverse-upload.json

jq -r '.ErrCode, .Resp.img_id, .Resp.img_url' /tmp/pixverse-upload.json

文件或 URL,二选一

上传请求体有两个可选字段 imageimage_url,参考页对两者写了同一条约束:Either image or image_url is required. URL 这一路有个值得提前知道的挑剔之处,application/octet-stream is not supported,所以服务器必须提供能被识别的图片内容类型。

上传会校验什么

上传参考页列了三条规则:maximum dimensions 10000 pixelsfile size less than 20MB,以及 Supported formats: "png", "webp", "jpeg", "jpg",配套 supported mime-type "image/jpeg","image/jpg","image/png","image/webp"。像素尺寸和文件体积是分开判的,所以一张压得很狠的 12000 像素扫描件即使只有四兆也会被拒。

img_id 链路的全貌

四步时序图:本地静帧被 POST 到上传端点,响应返回 img_id 与 img_url,生成调用带着这个 img_id 提交,状态端点被轮询到 status 1 后返回视频 url

提交、留住 video_id、轮询

每一跳只往下带一个值。img_id 从上传响应进入生成请求体;video_id 从生成响应进入状态 URL。

# The whole chain, with every documented field named.
img_id=$(curl -s --request POST \
  'https://app-api.pixverse.ai/openapi/v2/image/upload' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --form 'image=@"storyboard.png"' | jq -r '.Resp.img_id')

video_id=$(curl -s --request POST \
  'https://app-api.pixverse.ai/openapi/v2/video/img/generate' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --header 'Content-Type: application/json' \
  --data-raw "{
    \"img_id\": ${img_id},
    \"model\": \"v6\",
    \"prompt\": \"the camera pushes in slowly, warm light across the rim\",
    \"duration\": 5,
    \"quality\": \"720p\",
    \"motion_mode\": \"normal\",
    \"seed\": 0
  }" | jq -r '.Resp.video_id')

echo "img ${img_id} produced video ${video_id}"

生成调用的字段

有五个请求体参数被标为必填:durationimg_idmodelpromptquality。其余都是可选,参考页逐条注明了每个字段支持哪些模型代次。

# Step 2: the generation call, with optional fields commented out.
curl --location --request POST 'https://app-api.pixverse.ai/openapi/v2/video/img/generate' \
  --header "API-KEY: ${PIXVERSE_API_KEY}" \
  --header "Ai-trace-id: $(uuidgen)" \
  --header 'Content-Type: application/json' \
  --data '{
    "img_id": 98765,
    "model": "v6",
    "prompt": "the camera pushes in slowly, warm light across the rim",
    "duration": 5,
    "quality": "720p",
    "motion_mode": "normal",
    "negative_prompt": "text overlay, warped faces",
    "seed": 0,
    "generate_audio_switch": false
  }'

duration 随模型变

duration 是整数,合法取值跟着模型走,参考页把区间写全了:v.3.5/v4/v4.5 : 5/8 (v3.5 1080p cannot use 8)v5 : 5/8v5.5/v5.6 : 5/8/10 (1080p cannot use 10),以及 v6/c1 : 1~15。写死八秒的请求在好几代模型上都能跑,到了最新这两代却会失败,因为那里接受一到十五之间的任何值。

开关、种子与运动

generate_audio_switch 标注为 Supported in v5.5/5.6/v6/c1 modelsdefault value : falsegenerate_multi_clip_switch 标注为 Supported in v5.5/v6 model,默认同样是 false。seed 的范围是 0 - 2147483647motion_modenegative_prompt 出现在官方自己的 curl 示例里,却不在参数表中;motion_mode 在别处被记为默认 normal,取值 fast 只允许五秒时长,且 1080p 不支持。带 template_id 时可以启用 sound_effect_switch,并用 sound_effect_content 描述音效,留空则自动生成;lip_sync_tts_switchlip_sync_tts_contentlip_sync_tts_speaker_id 只支持 v5 及更早的模型,其中口播文本的上限写的是 ~140 (UTF-8) characters。这些字段在 v6 上不会报错,只是不起作用。

轮询到 status 1

# Step 3: poll the documented status endpoint until the task leaves status 5.
while :; do
  response=$(curl -s \
    "https://app-api.pixverse.ai/openapi/v2/video/result/${video_id}" \
    --header "API-KEY: ${PIXVERSE_API_KEY}" \
    --header "Ai-trace-id: $(uuidgen)")
  status=$(jq -r '.Resp.status' <<<"$response")
  case "$status" in
    1) jq -r '.Resp.url' <<<"$response"; break ;;
    5) sleep 4 ;;
    7) echo 'moderation failure, credits refunded' >&2; break ;;
    8) echo 'generation failure' >&2; break ;;
    *) echo "unexpected status $status" >&2; break ;;
  esac
done

状态码

端点页在示例响应旁边标注了 1: Generation successful, 5: In progress, 7: Moderation failed, 8: Generation failed. 官方索引又补了第六个,6 = deleted,而端点页没有列它,所以只按端点页写的客户端会把它当成未知状态。官方索引还给了轮询节奏,every 3–5s,比这更密的轮询除了给自己增加请求量之外没有别的效果。审核失败会退积分,状态 7 只让人白等一会儿。

出手在上传侧的失败

图片相关的错误码

把路径、文件名或者过期的整数当成 img_id 送进生成请求体时,命中的是 400032 Invalid image ID500030 Image size cannot exceed 20M , 10000px500031500032500033500041500042 都属于上传这一跳,其中好几个只写在错误码表里而不在上传页上,这也是上传页单独不够用的原因。图片被审核拦下时回的是 500054 Content moderation failure.,提示要换一张图再试;500042 则专门指出路径本身无效,和文件内容的对错无关。

# Read the code, then decide whether the fix is upstream.
code=$(jq -r '.ErrCode' /tmp/pixverse-upload.json)
case "$code" in
  0)      echo 'upload accepted, take Resp.img_id' ;;
  500030) echo 'dimensions or weight over the limit' ;;
  500032) echo 'format refused, check the mime type' ;;
  500041) echo 'transfer failed, retry the upload' ;;
  *)      echo "unexpected upload error $code" ;;
esac

官方文档自己打架的一处上限

生成参考页的参数表允许 Prompt <= 5000 characters。排查页对同样这两个字段写的是 =< 2048 characters,而错误码 400018400019 都是 Prompt/negative prompt length exceeds 2048 characters limit.。三处里有两点说 2048、一点说 5000,所以先用小的那个数,等厂商自己对齐。限流页讲的是提交成功之后会撞上的并发上限。

常见问题

生成请求体里能直接放图片 URL 吗? 不能。它收的是 img_id,官方描述为 Image ID from Upload image API。URL 要放进上传端点的 image_url 字段,id 从那里返回。

img_id 的有效期是多久? 官方未公布。上传页和生成页都没有写过期时间,所以按任务现取,不要缓存 id。

哪些字段是必填的? durationimg_idmodelpromptquality。其余包括 seed 和音频开关都是可选。

同一个 img_id 能重复生成几条吗? 该字段只接受单个 id,参考页的描述是 single image or single-image templates。同一个 id 能否提交两次没有公布,多图特效走的是 img_ids

为什么五秒的片子到 1080p 会变贵? 因为计费按秒算,而每秒价格取决于画质和音频。公布的价目表里有每秒数字。