Firefly 的视频生成只有一个端点,契约很短。第一次接入时踩的坑,几乎都落在四件事上:路径、必填头、异步交接,以及片段时长是固定的。这四件事 Adobe 都写在同一页参考文档里,而第三方目录至少会写错一件。

下面按官方文档的顺序给出契约,再给每一步可跑的代码。文中每一条都来自 Adobe 自己的文档;第三方目录把端点写成 /videos/generate-async、把 GA 日期写成 2025 年 6 月,两处都与官方不符。

端点契约一览

项目取值
方法与路径post /v3/videos/generate
主机firefly-api.adobe.io
必填头x-model-version: video1_standard
安全X-Api-KeyAccessToken
请求字段bitRateFactorimagepromptseedssizesvideoSettings
成功响应202,带 cancelUrljobIdstatusUrl
固定时长五秒

最后一行不是可以覆盖的默认值。官方对这条操作的定义就是生成五秒视频,请求体里根本没有时长字段。要做八秒的剪辑,只能生成两次再剪,或者把这段活挪回网页端。参考页的版本号是 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_tokentoken_typeexpires_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 里只有三个字符串:cancelUrljobIdstatusUrl。这里没有 links 包装层,所以不要把图像端点的解析器照搬过来。官方的异步 how-to 给的是同样三个键,并说明用 statusUrlcancelUrl 查状态和取消,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 离开运行态、变成 succeededfailed 时停下,还在排队的任务返回的 statusrunning。成功载荷里有一个 result 对象,outputs 数组放着素材 URL;官方那个例子用的是图像任务,所以先原样打印一次这个数组,再把取值路径写死。

一排里的终端窗口、数据库圆柱体、一叠任务工单、轮询时钟与已完成的视频缩略图
本站自绘示意图,非官方示例:提交、排队、轮询与成品片段连成一条调用链 AI Tool Blog

带退避的 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))

注意查状态时也要带齐两个鉴权头。官方异步示例在轮询时同时发 Authorizationx-api-key,少任何一个回来的都是鉴权错误,而不是一个进行中的状态。

画幅、消耗与限流

输出画幅在用法说明页,消耗在另一张操作费率表上。两张表按分辨率档位对得上。

画幅尺寸费率档位每秒 Operations
16:91920w x 1080h1080p2
16:91280w x 720h720p1
16:9960w x 540h540p0.4
9:161080w x 1920h1080p2
9:16720w x 1280h720p1
9:16540w x 960h540p0.4
1:11080w x 1080h1080p2
1:1720w x 720h720p1
1:1540w x 540h540p0.4

限流按组织计:每分钟 4 次请求,每天 9,000 次,超过任何一条都返回 429 Too Many Requests。更高的额度要找客户经理谈,不是加个头就能解决。消耗数字是按生成视频的秒数算的,所以按官方那页的算式,五秒 1080p 片段是 10 个 Operations。一个 Operation 折算多少钱,官方没有公布。

这个端点的错误码

视频这条操作自己列了八个状态码和官方名称,其中两个最容易看错。

状态码官方名称
202Accepted
400Bad Request
403Forbidden
408Request Timeout
415Unsupported Media Type
422Unprocessable Entity
429Too Many Requests
500Internal 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

为什么响应里没有视频地址? 因为这次调用是异步的。你拿到的是 cancelUrljobIdstatusUrl,素材要等 status 变成 succeeded 之后,从状态载荷的 result.outputs 数组里取。

能自己设时长吗? 不能。这条操作被定义为生成五秒视频,请求体里没有时长字段。

模型是什么时候正式可用的? Adobe 的新闻稿把 GA 日期定在 2025 年 4 月 24 日。写 2025 年 6 月的目录,引的不是 Adobe。

video1_standard 放进配置,轮询 statusUrl 时带齐两个鉴权头,先写退避再写批量循环。相关的 API 说明讲了这一套与 Adobe 音视频处理类 API 的边界,提示词结构指南讲了 prompt 字段该写什么,分步教程从界面侧讲了同一个模型,费用页讲的是网页端积分的行为。