Seedance 视频生成
用火山方舟的 Seedance 系列模型生成视频,支持文生视频、首帧 / 首尾帧生视频、参考图或参考视频生视频,以及视频编辑与延长。视频生成以异步任务的方式提供:提交后立即拿到任务 ID,轮询到完成后再下载视频。接口形状参照 OpenAI 的 Videos API,网关把统一的参数转换成火山方舟的格式;OpenAI 官方 SDK 的 videos.create() 可以直接提交任务,查询和下载建议用 HTTP 直接调用。任务生命周期的通用说明见异步任务。
支持的模型
| 模型 ID | 特点 | 时长与分辨率 | 计费口径 |
|---|---|---|---|
bytedance/doubao-seedance-2.5 |
能力最全:参考图最多 30 张,可输出 1080p(10bit)和 mov,支持用 omni_reference_task_type 声明任务类型 |
4~30 秒;480p、720p、1080p | 按输出 Token;按分辨率(480p / 720p 一档、1080p 一档)和有无视频输入分档 |
bytedance/doubao-seedance-2.0-fast |
2.0 系列,参考图最多 9 张,只输出 mp4 |
4~15 秒;480p、720p | 按输出 Token;按有无视频输入分档 |
bytedance/doubao-seedance-2.0-mini |
与 2.0-fast 能力相同 | 4~15 秒;480p、720p | 按输出 Token;按有无视频输入分档 |
三个模型都支持文生视频、首帧 / 首尾帧生视频、图片参考、视频参考、视频编辑与延长、生成有声视频,输出宽高比可选 21:9、16:9、4:3、1:1、3:4、9:16 或自适应。模型需要在工作空间的模型广场开通后才能调用;具体单价以模型广场显示的为准。
端点
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /v1/videos |
提交视频生成任务,立即返回任务 ID |
| GET | /v1/videos/{id} |
查询任务状态 |
| GET | /v1/videos/{id}/content |
下载视频(302 跳转到视频地址) |
| DELETE | /v1/videos/{id} |
删除已结束的任务;不能取消进行中的任务 |
完整地址为 https://api.lyrouter.com 加上表中的路径。
鉴权
在请求头中携带 API Key:
Authorization: Bearer <API Key>
Bearer 区分大小写。也可以改用 x-api-key: <API Key>,两者同时存在时只读 Authorization。API Key 在工作空间的 API 密钥 页面创建,只用于 LYRouter 鉴权,不会转发给上游服务商。完整说明见接入地址与鉴权。
同一工作空间内的任意 API Key 都能查询、下载该工作空间的任务;其他工作空间的 Key 查询时返回 404。
请求头
| 请求头 | 必填 | 说明 |
|---|---|---|
Content-Type |
提交时必填 | application/json。POST /v1/videos 也接受 multipart/form-data,见下文「用表单提交」 |
X-Gateway-Strict-Parameters |
否 | 设为 true 时,请求中有会被丢弃的参数就直接返回 400,不提交任务、不计费,见网关扩展请求头与响应头 |
提交任务
/v1/videos提交一个视频生成任务。网关校验参数、转换成火山方舟的格式后提交给服务商,立即返回 status 为 queued 的任务对象,视频在后台生成。提交请求本身不产生费用。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
model |
string | 是 | 上表中的模型 ID。必须是具体的模型 ID:虚拟模型 @vmodel/<名称> 和 lyrouter/auto 返回 403,失败时也不会改用工作空间的默认模型 |
prompt |
string | 是 | 提示词。用 @图像1、@视频1 指代参考素材;视频编辑、延长任务靠提示词表达意图,见下文「任务类型与限制」 |
seconds |
integer | 否 | 视频时长(秒),必须是正整数。2.5 为 4~30,2.0 系列为 4~15;不写时用服务商的默认值。视频编辑任务不要传 |
size |
string | 否 | 分辨率,写成 宽x高(如 1280x720)或档位名(480p、720p、1080p),见下文「size 的两种写法」。1080p 仅 2.5 支持 |
first_frame |
string | 否 | 首帧图片的 URL(首帧生视频),1 张。输出宽高比自动跟随首帧图片 |
last_frame |
string | 否 | 尾帧图片的 URL,与 first_frame 一起使用(首尾帧生视频) |
reference_image |
string | string[] | 否 | 参考图的 URL,单张写字符串,多张写数组。2.5 最多 30 张,2.0 系列最多 9 张 |
input_reference |
string | 否 | 参考视频的 URL,用于参考生视频、视频编辑和视频延长。只能传一个地址,写成数组返回 400 |
generate_audio |
boolean | 否 | 是否生成有声视频,三个模型都支持。不传时默认开启,生成的视频带音轨 |
watermark |
boolean | 否 | true 时在视频右下角加「AI生成」标识,默认不加 |
output_format |
string | 否 | 输出格式:mp4(默认)或 mov。mov 仅 2.5 支持 |
omni_reference_task_type |
string | 否 | 为 2.5 设计;2.0 系列传入目前不会报错,任务照常生成并计费。显式声明任务类型:auto(默认)、reference、edit、extend,见下文「任务类型与限制」 |
- 表中没有的字段会被丢弃,响应头
X-Gateway-Unsupported-Parameters列出它们的名字;带X-Gateway-Strict-Parameters: true时改为返回 400。火山方舟原生的duration、resolution、ratio、content也属于这一类,请改用seconds、size和上面的素材字段。 - 本接口不支持
n,每次请求生成一个视频。 reference_audio(参考音频)是例外:目前不会转发给 Seedance,传了不报错,也不会出现在X-Gateway-Unsupported-Parameters中,参考音频被直接忽略;需要参考音频时请改用 MiniMax H3 视频生成。source_video只用于 MiniMax H3 的视频升清,传给 Seedance 返回 400source_video is not supported for this model。- 素材地址要能被服务商访问。大文件请用公网 URL,不要用 Base64,请求体总大小不超过 64 MB。
用表单提交
POST /v1/videos 也接受 multipart/form-data,OpenAI 官方 SDK 的 videos.create() 就用这种格式。表单中网关读取 model、prompt、seconds、size、input_reference、watermark、generate_audio、output_format、omni_reference_task_type 这几个字段:
input_reference只能传 URL,传文件不会报错但会被忽略:请填可公开访问的https://地址;目前上传文件时,任务按没有参考视频的方式生成,并照常计费。watermark、generate_audio只接受字面量true或false。first_frame、last_frame、reference_image请用 JSON 请求体提交。目前表单里的这些字段会被换成占位值转发给服务商,例如带first_frame时服务商返回 400The parameter content[1].image_url specified in the request is not valid,报错不指向真实原因。
请求示例
示例从环境变量 LYROUTER_API_KEY 读取 API Key,设置方法见完成第一次模型调用。
curl https://api.lyrouter.com/v1/videos \
-H "Authorization: Bearer $LYROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "bytedance/doubao-seedance-2.0-mini",
"prompt": "一只柯基在草地上奔跑,阳光穿过树叶",
"size": "1280x720",
"seconds": 5,
"generate_audio": true
}'
import os
import httpx
resp = httpx.post(
"https://api.lyrouter.com/v1/videos",
headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
json={
"model": "bytedance/doubao-seedance-2.0-mini",
"prompt": "一只柯基在草地上奔跑,阳光穿过树叶",
"size": "1280x720",
"seconds": 5,
"generate_audio": True,
},
timeout=60,
)
resp.raise_for_status()
dropped = resp.headers.get("X-Gateway-Unsupported-Parameters")
if dropped:
print("这些参数没有生效:", dropped)
task = resp.json()
print(task["id"], task["status"])
const resp = await fetch("https://api.lyrouter.com/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LYROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "bytedance/doubao-seedance-2.0-mini",
prompt: "一只柯基在草地上奔跑,阳光穿过树叶",
size: "1280x720",
seconds: 5,
generate_audio: true,
}),
});
if (!resp.ok) throw new Error(await resp.text());
const dropped = resp.headers.get("X-Gateway-Unsupported-Parameters");
if (dropped) console.log("这些参数没有生效:", dropped);
const task = await resp.json();
console.log(task.id, task.status);
响应
提交成功返回 HTTP 200 和任务对象,status 为 queued,字段含义见下文「查询任务」。提交响应里没有 usage,费用在任务完成时结算。下面的 400 是 seconds 不是正整数,403 是模型没有在本工作空间开通(或用了虚拟模型、lyrouter/auto),429 是服务商账户的并发任务已满。
{
"id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
"object": "video",
"model": "doubao-seedance-2-0-mini-260615",
"status": "queued",
"progress": 0,
"created_at": 1791950400,
"expires_at": 1792555200,
"seconds": 5,
"size": "1280x720"
}
{
"error": {
"message": "seconds must be a positive whole number of seconds",
"type": "invalid_request_error",
"code": "invalid_parameter"
}
}
{
"error": {
"message": "Model is not available in the model plaza",
"type": "access_denied",
"code": "model_not_available"
}
}
{
"error": {
"message": "upstream concurrency full",
"type": "rate_limit_error",
"code": "upstream_concurrency_full"
}
}
- 保存好返回的
id。查询时网络出错或超时,用同一个 ID 继续查询,不要重新提交:重新提交会生成新任务并再次计费。 - 429
upstream_concurrency_full的并发限制按服务商账户共享,换一个 API Key 也一样,请稍后重新提交。
查询任务
/v1/videos/{id}读取任务的当前状态。网关在后台替你向服务商轮询进度,这个接口只返回网关记录的状态,查得更频繁不会更早拿到结果。
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 提交时返回的任务 ID,以 aigwt_ 开头。服务商自己的任务 ID 查不到,返回 404 |
请求示例
curl https://api.lyrouter.com/v1/videos/$TASK_ID \
-H "Authorization: Bearer $LYROUTER_API_KEY"
import os
import httpx
task_id = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA"
resp = httpx.get(
f"https://api.lyrouter.com/v1/videos/{task_id}",
headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
task = resp.json()
print(task["status"], task.get("usage"), task.get("error"))
const taskId = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA";
const resp = await fetch(`https://api.lyrouter.com/v1/videos/${taskId}`, {
headers: { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` },
});
if (!resp.ok) throw new Error(await resp.text());
const task = await resp.json();
console.log(task.status, task.usage, task.error);
响应
任务存在时总是返回 HTTP 200,任务本身失败也一样,以 status 判断结果。任务 ID 不存在、已超过 7 天或属于其他工作空间时返回 404。
{
"id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
"object": "video",
"model": "doubao-seedance-2-0-mini-260615",
"status": "in_progress",
"progress": 0,
"created_at": 1791950400,
"expires_at": 1792555200,
"seconds": 5,
"size": "1280x720"
}
{
"id": "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA",
"object": "video",
"model": "doubao-seedance-2-0-mini-260615",
"status": "completed",
"progress": 100,
"created_at": 1791950400,
"completed_at": 1791950538,
"expires_at": 1792555200,
"seconds": 5,
"size": "1280x720",
"usage": { "duration_seconds": 5, "completion_tokens": 108900 }
}
{
"id": "aigwt_volcengine_videos_Qw3hTz0bVn1sLk8pYc4dRe",
"object": "video",
"model": "doubao-seedance-2-0-mini-260615",
"status": "failed",
"progress": 0,
"created_at": 1791950400,
"completed_at": 1791950482,
"expires_at": 1792555200,
"seconds": 5,
"error": {
"code": "OutputVideoSensitiveContentDetected",
"message": "The request failed because the output video may contain sensitive information.",
"provider_request_id": "021791950538123abcdef",
"retryable": false
}
}
{
"error": {
"message": "video task not found",
"type": "invalid_request_error",
"code": "not_found"
}
}
status |
含义 | 你需要做的 |
|---|---|---|
queued |
已受理,尚未开始生成 | 继续轮询 |
in_progress |
服务商正在生成 | 继续轮询 |
completed |
已完成 | 调用 /content 下载视频 |
failed |
已失败 | 读取 error,不收费 |
| 字段 | 说明 |
|---|---|
id |
网关的任务 ID,格式为 aigwt_<服务商>_<任务类型>_<随机串>。它不是服务商的任务 ID,向服务商报障时请用 error.provider_request_id |
object |
固定为 video |
model |
实际调用的上游模型名,例如 doubao-seedance-2-0-mini-260615,与请求中的模型 ID 写法不同 |
status |
queued、in_progress、completed、failed 之一 |
progress |
完成前为 0,完成后为 100,不表示中间进度,请以 status 判断 |
created_at / completed_at |
提交时间、结束时间(Unix 秒)。completed_at 只在任务结束后出现 |
expires_at |
任务记录的过期时间,提交后 7 天。过期后查询返回 404 |
seconds / size |
原样回显提交时的值 |
error |
仅 failed 时出现:code、message,以及有值时才出现的 provider_request_id(服务商的请求标识)和 retryable |
usage |
任务完成后出现:duration_seconds 为视频秒数,completion_tokens 为服务商上报的输出 Token 数,Seedance 按它计费 |
失败任务的 error.code 多为服务商的错误码,原样返回:
error.code |
含义与处理 |
|---|---|
InvalidParameter.TaskTypeConstraint |
任务类型与参数冲突,例如视频编辑任务传了 seconds。对照下文「任务类型与限制」修改参数 |
InvalidParameter.TaskTypeMismatch |
用 omni_reference_task_type 声明的类型与模型的判定不一致。按任务类型表中的关键词写提示词,或改用 auto |
OutputVideoSensitiveContentDetected 等 |
内容审核未通过,修改提示词或素材后重新提交 |
upstream_error |
服务商报告失败,但没有给出可识别的错误码 |
error.retryable为false表示原样重试不会成功,需要修改请求;为true表示可以重试;没有这个字段表示网关无法判断,请不要把缺失当成false。完整的取值见异步任务。- 轮询间隔:提交后约 10 秒开始查询,之后逐步放宽到每 30 秒一次。
- 等待上限:提交 24 小时后仍未完成的任务不会再有结果,也不收费,可以停止轮询。这时查询结果可能仍显示
queued或in_progress,任务的实际状态请在控制台工作空间的 多媒体生成 页面按任务 ID 搜索查看(「状态」显示「已过期」)。
下载视频
/v1/videos/{id}/content任务变为 completed 后调用,返回 302,Location 响应头是服务商提供的带签名的视频地址。让客户端跟随跳转即可下载:cURL 加 -L,Python httpx 设 follow_redirects=True,Node.js 的 fetch 默认跟随。
请求示例
# 下载到本地
curl -L https://api.lyrouter.com/v1/videos/$TASK_ID/content \
-H "Authorization: Bearer $LYROUTER_API_KEY" \
-o video.mp4
# 只取视频地址,不下载
curl -s -o /dev/null -w '%{redirect_url}\n' \
https://api.lyrouter.com/v1/videos/$TASK_ID/content \
-H "Authorization: Bearer $LYROUTER_API_KEY"
import os
import httpx
task_id = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA"
with httpx.stream(
"GET",
f"https://api.lyrouter.com/v1/videos/{task_id}/content",
headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
follow_redirects=True,
timeout=600,
) as video:
video.raise_for_status()
with open("video.mp4", "wb") as f:
for chunk in video.iter_bytes():
f.write(chunk)
import { writeFile } from "node:fs/promises";
const taskId = "aigwt_volcengine_videos_MXgno8VlzuzZtJl0kq83jA";
const video = await fetch(`https://api.lyrouter.com/v1/videos/${taskId}/content`, {
headers: { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` },
});
if (!video.ok) throw new Error(`download failed: ${video.status}`);
await writeFile("video.mp4", Buffer.from(await video.arrayBuffer()));
响应
状态码:302
Location: <服务商提供的带签名的视频地址>
{
"error": {
"message": "video is not ready yet",
"type": "invalid_request_error",
"code": "not_ready"
}
}
{
"error": {
"message": "video task not found",
"type": "invalid_request_error",
"code": "not_found"
}
}
- 视频地址在任务完成时确定,之后每次调用都跳转到同一个地址。地址的有效期由服务商决定,通常为 24 小时,过期后无法通过网关重新获取,请拿到后立即下载或转存到自己的存储。
- 任务还没完成(或已失败)时返回 409。这类 4xx 也会计入 API Key 的错误率,频繁调用可能触发熔断,请先查询到
completed再下载。
删除任务
/v1/videos/{id}对已结束(completed 或 failed)的任务返回 HTTP 200 和任务对象,并带 "deleted": true;任务记录不会因此删除,在 expires_at 之前仍可查询和下载。对进行中的任务调用返回 409 cancel_not_supported:服务商不提供取消接口,请等待任务结束。OpenAI 官方 SDK 的 videos.delete() 读取的就是这个 deleted 字段。
Seedance 特有参数与限制
size 的两种写法
| 写法 | 网关怎么处理 |
|---|---|
宽x高(如 1280x720) |
取短边作为分辨率档(1280x720 → 720p);宽高比与服务商支持的某个比例相差在 2% 以内时取该比例,否则按约分后的比例原样发出,由服务商返回 400(如 1000x480 报 the parameter ratio specified in the request is not valid ...) |
档位名(如 720p、1080p) |
原样送出,宽高比由服务商按提示词自适应 |
- 首帧 / 首尾帧生视频时,输出宽高比跟随首帧图片。
- Seedance 2.5 的 1080p 采用 10bit 位深 + H.265/HEVC 编码,少数播放器不兼容;
mov采用 H.264 + yuv444p + PCM。建议用 VLC、mpv、ffplay 或 IINA 播放。
任务类型与限制
Seedance 会根据输入素材和提示词自动判断任务类型,不同类型有不同的限制:
| 任务类型 | 怎么触发 | 限制 |
|---|---|---|
| 文生视频 | 只传 prompt |
无特殊限制 |
| 首帧 / 首尾帧生视频 | 传 first_frame(+ last_frame) |
输出宽高比自动跟随首帧图片 |
| 参考生视频 | 传 reference_image / input_reference |
无特殊限制 |
| 视频编辑 | 传 input_reference,并在提示词中表达编辑意图 |
不要传 seconds;输出宽高比与时长自动跟随源视频;源视频时长须为 4~30 秒 |
| 视频延长 | 传 input_reference,并在提示词中表达延长意图 |
输出宽高比自动跟随源视频 |
视频编辑任务的时长固定跟随源视频,而网关的 seconds 只接受正整数,所以编辑任务请直接不传 seconds;传了会被服务商拒绝(InvalidParameter.TaskTypeConstraint)。
使用 Seedance 2.5 时,可以用 omni_reference_task_type 显式声明任务类型,把服务商的异步失败提前成提交时的同步报错:
| 值 | 含义 | 提示词需包含的关键词 |
|---|---|---|
auto(默认) |
由模型判断 | — |
reference |
参考生视频 | — |
edit |
视频编辑 | 编辑视频 / 增加 / 加上 / 删除 / 去掉 / 修改 / 替换 / 改成 |
extend |
视频延长 | 向前延长 / 向后延长 / 延续 / 续写 |
edit、extend 只适用于带参考素材的场景:用在文生视频(不带参考素材)上,提交时直接返回 400,错误码为 InvalidParameter.TaskTypeConstraint,报错信息包含 This parameter is only supported in the multimodal reference scenario, and is not applicable to text-to-video generation or first-frame / first-last-frame generation。
声明的类型与模型实际判定的不一致时,任务可能异步失败(InvalidParameter.TaskTypeMismatch)。
输入素材限制(Seedance 2.5)
| 素材 | 限制 |
|---|---|
| 图片 | jpeg、png、webp、bmp、tiff、gif、heic、heif;宽高 [300, 6000] px;宽高比 [0.4, 2.5];单张 ≤ 30 MB;首帧 1 张 / 首尾帧 2 张 / 参考图 1~30 张 |
| 视频 | mp4、mov(H.264 或 H.265,音频 AAC / MP3 / PCM);分辨率 480p 或 720p;时长 [2, 30] 秒(编辑任务 [4, 30] 秒);宽高 [300, 6000] px;宽高比 [0.4, 2.5];单个 ≤ 200 MB;帧率 [24, 60] |
| 请求体 | 总大小 ≤ 64 MB,大文件请用公网 URL,不要用 Base64 |
Seedance 不接受含真人人脸的参考图和参考视频,会被输入审核拦截。
服务商的联网搜索、返回尾帧等能力没有对应的网关参数,目前无法通过 LYRouter 使用。
计费
Seedance 按服务商上报的输出 Token 计费(任务对象中的 usage.completion_tokens),分档维度是分辨率和有无视频输入:
| 模型 | 分档 |
|---|---|
bytedance/doubao-seedance-2.5 |
480p / 720p 与 1080p 两档,每档再按有无视频输入分 |
bytedance/doubao-seedance-2.0-fast |
只按有无视频输入分两档 |
bytedance/doubao-seedance-2.0-mini |
只按有无视频输入分两档 |
- 有视频输入时按另一档单价计费,单价更低,但总价更高:传了
input_reference的任务(参考视频、视频编辑、视频延长)计量的 Token 大约是不带参考视频时的两倍(见下文「等效秒」),单价的下降抵不过用量的增加。只传首帧 / 首尾帧的图生视频不算视频输入。 - Token 数 = 固定开销 + 每秒 Token × 等效秒,已经体现了分辨率与时长的差异(实测 480p 4 秒约 40,594 Token,8 秒约 80,770,720p 4 秒约 87,300),不再按时长分档。不带参考视频时,等效秒就是输出秒数。
- 带参考视频时,等效秒不等于输出时长:输出不超过 6 秒时,等效秒 = 输出秒数 + 参考视频秒数;超过 6 秒时,等效秒 = 1.5 × 输出秒数 + 2,与参考视频时长无关。6 秒是拐点,例如参考视频 5 秒时,输出 5 秒按 10 秒计量,输出 10 秒按 17 秒计量。视频编辑、视频延长同样按这条规则计量(实测)。
- 价格在提交时锁定;任务变为
completed时按实际上报的 Token 数结算;failed的任务(包括超时)不收费。在日志中查看任务费用的方法见异步任务。
完整调用示例
提交一个首帧生视频任务,轮询到结束后下载。把 first_frame 换成你自己的公网图片地址。cURL 示例需要 jq,请保存为脚本文件后用 bash 运行(出错时用 exit 退出)。
BASE=https://api.lyrouter.com/v1
AUTH="Authorization: Bearer $LYROUTER_API_KEY"
# 1. 提交任务;没拿到任务 ID 就打印响应并退出,不要继续轮询
RESP=$(curl -s "$BASE/videos" -H "$AUTH" -H "Content-Type: application/json" \
-d '{
"model": "bytedance/doubao-seedance-2.0-mini",
"prompt": "镜头缓慢推近,小猫转头看向窗外",
"first_frame": "https://example.com/cat.jpg",
"size": "720p",
"seconds": 5
}')
TASK_ID=$(echo "$RESP" | jq -r '.id // empty')
if [ -z "$TASK_ID" ]; then echo "提交失败:$RESP"; exit 1; fi
echo "task: $TASK_ID"
# 2. 轮询:从 10 秒开始,逐步放宽到 30 秒,直到结束;最多等 2 小时
DELAY=10
GIVE_UP_AT=$(( $(date +%s) + 2 * 3600 ))
while true; do
if [ "$(date +%s)" -gt "$GIVE_UP_AT" ]; then
echo "$TASK_ID 仍在生成,可稍后用同一个 ID 继续查询"; exit 1
fi
sleep $DELAY
DELAY=$(( DELAY * 3 / 2 > 30 ? 30 : DELAY * 3 / 2 ))
TASK=$(curl -s "$BASE/videos/$TASK_ID" -H "$AUTH")
STATUS=$(echo "$TASK" | jq -r '.status')
echo "status: $STATUS"
case "$STATUS" in
completed|failed) break ;;
queued|in_progress|"") ;; # 空值多为网络抖动:保留任务 ID 继续查
*) echo "查询出错:$TASK"; exit 1 ;;
esac
done
# 3. 完成则下载,失败则打印原因
if [ "$STATUS" = "completed" ]; then
curl -sL "$BASE/videos/$TASK_ID/content" -H "$AUTH" -o video.mp4
else
echo "$TASK" | jq '.error'
fi
import os
import time
import httpx
BASE = "https://api.lyrouter.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"}
with httpx.Client(headers=HEADERS, timeout=60) as client:
# 1. 提交任务
resp = client.post(f"{BASE}/videos", json={
"model": "bytedance/doubao-seedance-2.0-mini",
"prompt": "镜头缓慢推近,小猫转头看向窗外",
"first_frame": "https://example.com/cat.jpg",
"size": "720p",
"seconds": 5,
})
resp.raise_for_status()
task = resp.json()
task_id = task.get("id")
if not task_id:
raise RuntimeError(f"没有拿到任务 ID:{task}")
print("task:", task_id)
# 2. 轮询:从 10 秒开始,逐步放宽到 30 秒;最多等 2 小时
delay, give_up_at = 10, time.time() + 2 * 3600
while task["status"] in ("queued", "in_progress"):
if time.time() > give_up_at:
raise TimeoutError(f"{task_id} 仍在生成,可稍后用同一个 ID 继续查询")
time.sleep(delay)
delay = min(delay * 1.5, 30)
try:
resp = client.get(f"{BASE}/videos/{task_id}")
resp.raise_for_status()
except httpx.TransportError:
continue # 网络抖动:保留任务 ID 继续查,不要重新提交
task = resp.json()
if task["status"] == "failed":
err = task.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
# 3. 下载:接口返回 302,跟随跳转即可
with client.stream("GET", f"{BASE}/videos/{task_id}/content",
follow_redirects=True, timeout=600) as video:
video.raise_for_status()
with open("video.mp4", "wb") as f:
for chunk in video.iter_bytes():
f.write(chunk)
print("已保存 video.mp4,用量:", task.get("usage"))
import { writeFile } from "node:fs/promises";
const BASE = "https://api.lyrouter.com/v1";
const headers = { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 1. 提交任务
const submit = await fetch(`${BASE}/videos`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
model: "bytedance/doubao-seedance-2.0-mini",
prompt: "镜头缓慢推近,小猫转头看向窗外",
first_frame: "https://example.com/cat.jpg",
size: "720p",
seconds: 5,
}),
});
if (!submit.ok) throw new Error(await submit.text());
let task = await submit.json();
if (!task.id) throw new Error(`没有拿到任务 ID:${JSON.stringify(task)}`);
console.log("task:", task.id);
// 2. 轮询:从 10 秒开始,逐步放宽到 30 秒
let delay = 10_000;
while (task.status === "queued" || task.status === "in_progress") {
await sleep(delay);
delay = Math.min(delay * 1.5, 30_000);
let resp;
try {
resp = await fetch(`${BASE}/videos/${task.id}`, { headers });
} catch {
continue; // 网络抖动:保留任务 ID 继续查,不要重新提交
}
if (!resp.ok) throw new Error(await resp.text());
task = await resp.json();
}
if (task.status === "failed") {
throw new Error(`${task.error?.code}: ${task.error?.message}`);
}
// 3. 下载:fetch 默认跟随 302 跳转
const video = await fetch(`${BASE}/videos/${task.id}/content`, { headers });
if (!video.ok) throw new Error(`download failed: ${video.status}`);
await writeFile("video.mp4", Buffer.from(await video.arrayBuffer()));
console.log("saved video.mp4", task.usage);
常见错误
错误响应为 {"error":{"message":"...","type":"...","code":"..."}}。
| 状态码 | error.message |
error.code |
原因与处理 |
|---|---|---|---|
| 400 | seconds must be a positive whole number of seconds |
invalid_parameter |
seconds 不是正整数,例如写成了 0、负数或小数 |
| 400 | input_reference must be a non-empty URL string |
invalid_parameter |
input_reference 为空或写成了数组。Seedance 只收一个参考视频地址 |
| 400 | generate_audio must be a boolean |
invalid_parameter |
JSON 中 generate_audio(或 watermark)不是布尔值 |
| 400 | source_video is not supported for this model |
invalid_parameter |
source_video 只用于 MiniMax H3 的视频升清,从请求中删掉 |
| 400 | unsupported parameters for this model: <参数名> |
unsupported_parameters |
带了 X-Gateway-Strict-Parameters: true,且请求中有会被丢弃的参数。删掉列出的参数或改用网关的参数名 |
| 403 | Model is not available in the model plaza |
model_not_available |
模型没有在本工作空间开通,或使用了虚拟模型、lyrouter/auto。换用已开通的具体模型 ID |
| 404 | video task not found |
not_found |
任务 ID 不存在、已超过 7 天、属于其他工作空间,或传入的是服务商的任务 ID |
| 409 | video is not ready yet |
not_ready |
任务还没完成(或已失败)就调用了 /content。先查询到 completed |
| 409 | cancelling an in-flight video task is not supported: ... |
cancel_not_supported |
对进行中的任务调用了 DELETE。请等待任务结束 |
| 429 | upstream concurrency full |
upstream_concurrency_full |
服务商账户的并发任务已满,换 API Key 无效,稍后重新提交 |
- 用表单提交时
input_reference只能传 URL。目前上传文件不会报错,文件会被忽略,任务按没有参考视频的方式生成并照常计费;只有填了非 URL 的文本(如video.mp4)时才返回 400 纯文本input_reference must be a URL; uploading a file is not supported by this gateway (pass a publicly reachable https:// URL instead)。这类由网关返回的纯文本报错,响应头目前仍是Content-Type: application/json,解析前请先判断正文是不是 JSON。 - 分辨率、时长、素材数量等超出模型范围时,由服务商拒绝,状态码和报错原文由服务商决定;任务提交后才发现的问题以失败任务的形式返回,见上文「查询任务」。完整列表见错误码与请求标识。
常见问题
视频编辑任务失败并返回 InvalidParameter.TaskTypeConstraint,怎么办?
答: 视频编辑任务的时长固定跟随源视频,请从请求中删掉 seconds 再提交。其他任务类型的限制见上文「任务类型与限制」。
任务失败并返回 InvalidParameter.TaskTypeMismatch,怎么办?
答: 用 omni_reference_task_type 声明的任务类型与模型的判定不一致。按任务类型表中的关键词改写提示词(例如编辑任务写明「替换」「去掉」),或改用 auto 让模型自行判断。
请求 1080p 或 mov 被服务商拒绝,为什么?
答: 1080p 和 mov 只有 bytedance/doubao-seedance-2.5 支持,2.0 系列只能输出 480p、720p 的 mp4。换用 2.5,或把 size 改为 720p 并去掉 output_format。
下载的 1080p 视频打不开,怎么办?
答: Seedance 2.5 的 1080p 是 10bit H.265/HEVC 编码,部分播放器不支持。用 VLC、mpv、ffplay 或 IINA 播放,或者自行转码。
可以用 OpenAI 官方 SDK 调用吗?
答: 可以用 client.videos.create() 提交任务,SDK 以表单格式提交,网关会转换成服务商需要的格式。Python SDK 需要 openai 2.x:实测 1.58.1 没有 client.videos,2.48.0 可用。表单中只能传 URL 形式的 input_reference:Python SDK 直接写 input_reference="https://…" 会在本地抛出 RuntimeError: Expected entry at input_reference to be bytes, an io.IOBase instance, PathLike or a tuple …,请求不会发出,请改用 extra_body={"input_reference": "https://…"};Node.js SDK 运行时接受字符串,但 TypeScript 类型不允许,需要绕过类型检查。size 的档位名(如 720p)同样不在 SDK 的类型声明里,TypeScript 中需要绕过类型检查;首帧、尾帧、参考图请用 HTTP 发送 JSON 请求体。查询和下载建议按本页的 HTTP 写法调用。
视频地址过期了,还能重新下载吗?
答: 不能。视频地址在任务完成时确定,再次调用 /content 拿到的仍是同一个地址,过期后需要重新提交任务。请在任务完成后立即下载或转存。
查询结果一直是 in_progress,怎么办?
答: 视频生成通常需要几分钟。提交 24 小时后仍未完成的任务不会再有结果,也不收费,可以停止轮询,并在控制台 多媒体生成 页面按任务 ID 查看任务状态。不要在等待期间重复提交同样的请求,每次提交都会生成新任务并单独计费。