Firefly 的视频生成只有一个端点,契约很短。第一次接入时踩的坑,几乎都落在四件事上:路径、必填头、异步交接,以及片段时长是固定的。这四件事 Adobe 都写在同一页参考文档里,而第三方目录至少会写错一件。
下面按官方文档的顺序给出契约,再给每一步可跑的代码。文中每一条都来自 Adobe 自己的文档;第三方目录把端点写成 /videos/generate-async、把 GA 日期写成 2025 年 6 月,两处都与官方不符。
端点契约一览
| 项目 | 取值 |
|---|---|
| 方法与路径 | post /v3/videos/generate |
| 主机 | firefly-api.adobe.io |
| 必填头 | x-model-version: video1_standard |
| 安全 | X-Api-Key 与 AccessToken |
| 请求字段 | bitRateFactor、image、prompt、seeds、sizes、videoSettings |
| 成功响应 | 202,带 cancelUrl、jobId、statusUrl |
| 固定时长 | 五秒 |
最后一行不是可以覆盖的默认值。官方对这条操作的定义就是生成五秒视频,请求体里根本没有时长字段。要做八秒的剪辑,只能生成两次再剪,或者把这段活挪回网页端。参考页的版本号是 Firefly API (3.0.0),视频条目与图像条目并列在同一页上。
鉴权与 model 头
Firefly APIs 用 IMS 访问令牌,不是长期 API key。官方的入门文档让服务端拿 client ID 与 secret 去 IMS 端点换令牌,并说明每个令牌有效期为 24 小时。令牌拿到后,把它和 client ID 一起放进 x-api-key,后者与换令牌时用的是同一个值。
export FIREFLY_SERVICES_CLIENT_ID='paste-client-id-here'
export FIREFLY_SERVICES_CLIENT_SECRET='paste-client-secret-here'
curl --location 'https://ims-na1.adobelogin.com/ims/token/v3' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_id=$FIREFLY_SERVICES_CLIENT_ID" \
--data-urlencode "client_secret=$FIREFLY_SERVICES_CLIENT_SECRET" \
--data-urlencode 'scope=openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis'
响应里带 access_token、token_type 和 expires_in。
video1_standard 选的是什么
x-model-version 是视频这条操作下唯一的必填 header 参数,而 video1_standard 是它枚举里的唯一取值。把它当成配置里设一次的常量就行。两个头要一起带:Authorization: Bearer 放令牌,x-api-key 放 client ID,少一个都会在鉴权阶段被挡下来。
用 curl 提交任务
下面的请求体只用了官方参考页出现的字段,videoSettings 的四个键与取值也来自 Adobe 自己的示例请求。
export FIREFLY_SERVICES_ACCESS_TOKEN='paste-access-token-here'
curl -s -X POST 'https://firefly-api.adobe.io/v3/videos/generate' \
-H "Authorization: Bearer $FIREFLY_SERVICES_ACCESS_TOKEN" \
-H "x-api-key: $FIREFLY_SERVICES_CLIENT_ID" \
-H 'x-model-version: video1_standard' \
-H 'Content-Type: application/json' \
-d '{
"prompt": "A slow aerial pass over a desert at sunrise, warm light, no text",
"sizes": [{ "height": 1080, "width": 1920 }],
"seeds": [1842533538],
"bitRateFactor": 18,
"videoSettings": {
"cameraMotion": "camera pan left",
"promptStyle": "anime",
"shotAngle": "aerial shot",
"shotSize": "close-up shot"
}
}' | tee job.json
两个字段值得写进客户端。bitRateFactor 是 0 到 63 的整数,默认 18,官方建议待在 17 到 23 之间;0 表示无损,文件最大。seeds 目前只接受一个值,多写的第二个会被静默忽略,而不是报错。image 只用首帧或尾帧引导生成,它是关键帧,不是参考视频。
轮询状态 URL
202 返回了什么
提交调用回的是 202 Accepted,body 里只有三个字符串:cancelUrl、jobId 和 statusUrl。这里没有 links 包装层,所以不要把图像端点的解析器照搬过来。官方的异步 how-to 给的是同样三个键,并说明用 statusUrl 与 cancelUrl 查状态和取消,jobId 用来记日志。轮询频率没有官方标准,官方示例建议每秒一次,但视频任务通常比图像慢,5 秒一次更省请求额度。
STATUS_URL=$(jq -r .statusUrl job.json)
while true; do
POLL=$(curl -s "$STATUS_URL" \
-H "Authorization: Bearer $FIREFLY_SERVICES_ACCESS_TOKEN" \
-H "x-api-key: $FIREFLY_SERVICES_CLIENT_ID")
STATUS=$(echo "$POLL" | jq -r .status)
echo "status: $STATUS"
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && { echo "$POLL"; exit 1; }
sleep 5
done
echo "$POLL" | jq '.result.outputs'
官方自己的轮询循环就是在 status 离开运行态、变成 succeeded 或 failed 时停下,还在排队的任务返回的 status 是 running。成功载荷里有一个 result 对象,outputs 数组放着素材 URL;官方那个例子用的是图像任务,所以先原样打印一次这个数组,再把取值路径写死。

带退避的 Python 客户端
限流按组织计,所以批量任务最先撞上的往往不是别的错误,而是 429。官方给的处置是 retry-after 头或者指数退避,下面这个包装就是照这个写的。
import json
import os
import time
import requests
BASE = "https://firefly-api.adobe.io/v3"
HEADERS = {
"Authorization": f"Bearer {os.environ['FIREFLY_SERVICES_ACCESS_TOKEN']}",
"x-api-key": os.environ["FIREFLY_SERVICES_CLIENT_ID"],
"x-model-version": "video1_standard",
"Content-Type": "application/json",
}
def call(method, url, **kwargs):
for attempt in range(5):
response = requests.request(method, url, headers=HEADERS, timeout=60, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response.json()
wait = int(response.headers.get("retry-after", 2**attempt))
print(f"429 received, retrying in {wait}s")
time.sleep(wait)
raise RuntimeError("rate limit did not clear after five attempts")
job = call(
"POST",
f"{BASE}/videos/generate",
json={
"prompt": "A slow aerial pass over a desert at sunrise, warm light, no text",
"sizes": [{"height": 1080, "width": 1920}],
"seeds": [1842533538],
"bitRateFactor": 18,
},
)
print("submitted", job["jobId"])
while True:
payload = call("GET", job["statusUrl"])
if payload.get("status") in {"succeeded", "failed"}:
break
time.sleep(5)
print(json.dumps(payload, indent=2))
注意查状态时也要带齐两个鉴权头。官方异步示例在轮询时同时发 Authorization 和 x-api-key,少任何一个回来的都是鉴权错误,而不是一个进行中的状态。
画幅、消耗与限流
输出画幅在用法说明页,消耗在另一张操作费率表上。两张表按分辨率档位对得上。
| 画幅 | 尺寸 | 费率档位 | 每秒 Operations |
|---|---|---|---|
| 16:9 | 1920w x 1080h | 1080p | 2 |
| 16:9 | 1280w x 720h | 720p | 1 |
| 16:9 | 960w x 540h | 540p | 0.4 |
| 9:16 | 1080w x 1920h | 1080p | 2 |
| 9:16 | 720w x 1280h | 720p | 1 |
| 9:16 | 540w x 960h | 540p | 0.4 |
| 1:1 | 1080w x 1080h | 1080p | 2 |
| 1:1 | 720w x 720h | 720p | 1 |
| 1:1 | 540w x 540h | 540p | 0.4 |
限流按组织计:每分钟 4 次请求,每天 9,000 次,超过任何一条都返回 429 Too Many Requests。更高的额度要找客户经理谈,不是加个头就能解决。消耗数字是按生成视频的秒数算的,所以按官方那页的算式,五秒 1080p 片段是 10 个 Operations。一个 Operation 折算多少钱,官方没有公布。
这个端点的错误码
视频这条操作自己列了八个状态码和官方名称,其中两个最容易看错。
| 状态码 | 官方名称 |
|---|---|
| 202 | Accepted |
| 400 | Bad Request |
| 403 | Forbidden |
| 408 | Request Timeout |
| 415 | Unsupported Media Type |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
403 通常是凭据挂在了错误的组织下,或者这个 client 没有 Firefly 权限,而不是请求体写坏了。415 指向的是 Content-Type 头,一段在 REST 客户端里跑得通的请求在代码里失败时,先查这里。官方只给了这八个状态码的名称,没有逐条解释触发条件,所以上面这两句判断来自实践,不是官方措辞。
常见问题
端点路径是什么? firefly-api.adobe.io 上的 post /v3/videos/generate。第三方目录写的 /videos/generate-async 在 Adobe 参考页里根本不存在。
哪个头是必填的? x-model-version,且唯一有文档的取值是 video1_standard。
为什么响应里没有视频地址? 因为这次调用是异步的。你拿到的是 cancelUrl、jobId 和 statusUrl,素材要等 status 变成 succeeded 之后,从状态载荷的 result.outputs 数组里取。
能自己设时长吗? 不能。这条操作被定义为生成五秒视频,请求体里没有时长字段。
模型是什么时候正式可用的? Adobe 的新闻稿把 GA 日期定在 2025 年 4 月 24 日。写 2025 年 6 月的目录,引的不是 Adobe。
把 video1_standard 放进配置,轮询 statusUrl 时带齐两个鉴权头,先写退避再写批量循环。相关的 API 说明讲了这一套与 Adobe 音视频处理类 API 的边界,提示词结构指南讲了 prompt 字段该写什么,分步教程从界面侧讲了同一个模型,费用页讲的是网页端积分的行为。