Firefly 自定义模型 API 是两次调用,不是一套平台。先用 GET /v3/custom-models 列出模型、读出 assetId,再把这个值当作 customModelId 发给 /v3/images/generate-async,同时用请求头指定 x-model-version

真正让集成卡住的往往不是这两次调用,而是两个前置条件:教程里没写的那个请求头,以及一个从未共享给你的技术账号的模型。

Custom Models API 到底暴露了什么

Adobe 在 Firefly API 总览页给这项能力列了四条:按主体或风格训练模型、用模型生成图片、配合风格预设使用、管理与共享模型。对写代码的人来说是第四条,官方原话是 you can grant applications access so they can list and use your models via the API.,意思是你可以授权应用访问,让它们通过 API 列出并使用你的模型。

概念页补上了这项能力的意图:With custom models, you can capture and replicate distinctive brand aesthetics, characters, objects, or compositional arrangements.

先划一条边界:我们读到的 API reference 只记录了一个自定义模型操作,get /v3/custom-models;而「拿到一个训练好的模型」的官方路径走的是 Firefly 应用和你的 Adobe 客户团队,不是某个训练端点。凭证与平台分层见我们的接入指南

主体模型与风格模型

Adobe 把训练分成两种模式。主体模型 focus on representing specific characters, products, or objects,把特定的角色或产品画准;风格模型 emphasize aesthetic qualities like color palettes, patterns, brush stroke techniques, or illustrative cues,把色板、图案、笔触这类审美特征固定下来。

选哪种看你要固定的是什么。整季物料围绕同一个角色或同一件产品轮廓,用主体模型;贯穿的是跨主体的配色与线条处理,用风格模型。两种模式不会叠在同一个模型里,「都要」等于两个资产、两个 ID。

资产 ID 是干什么用的

自定义模型是由 Adobe 托管的资产,官方描述是 assets hosted securely by Adobe, offering easy organization, versioning, and reuse。列表响应里除了资产 ID,还有版本号、训练时用的基础模型和发布状态,所以 ID 更像一个可以更新的指针,而不是冻结的快照。

先共享模型,再查询模型

Adobe 把依赖关系写得很直白:Once you've trained a custom model, you need to share it with your technical account so that the model is accessible to the List Custom Models API and the Text to Image API. 模型训练完之后必须先共享给你的技术账号,列模型和文生图两个 API 才看得见它。共享是按模型逐个做的,用的是邮箱地址,不是按权限范围做的。

控制台里的四步

  1. 在 Adobe Developer Console 打开你的项目,点进 API 凭证链接。
  2. 复制 Technical Account Email
  3. 登录 Firefly,打开那个自定义模型,点右上角 "...",选 Share
  4. 把技术账号邮箱粘进 Add people or groups 输入框,点 Invite to edit

列表返回空数组,很少是令牌的问题:邀请还在等待状态时权限已经生效,官方原话是 The account may appear in the Pending state for sharing, but access to the custom model is granted.

列出模型并读出资产 ID

端点是 GET https://firefly-api.adobe.io/v3/custom-models,鉴权用惯常的 X-Api-KeyAccessToken,另外还有两个只出现在 API reference 里的请求头。x-user-token 的定义是 A user token referencing the user's individual account,代表某个具体用户的账号,并且 must be preceded by Bearerx-request-id 是可选链路追踪头,你不填服务端会自己生成。

# Requires curl and jq. Exchanges credentials, then lists your custom models.
export CUSTOM_MODELS_CLIENT_ID=<your-client-id>
export CUSTOM_MODELS_CLIENT_SECRET=<your-client-secret>
export CUSTOM_MODEL_ID=<your-asset-id>

TOKEN=$(curl -s -X POST 'https://ims-na1.adobelogin.com/ims/token/v3' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d "client_id=${CUSTOM_MODELS_CLIENT_ID}" \
  -d "client_secret=${CUSTOM_MODELS_CLIENT_SECRET}" \
  -d 'scope=openid,AdobeID,firefly_api,ff_apis' | jq -r .access_token)

curl -s -X GET 'https://firefly-api.adobe.io/v3/custom-models' \
  -H 'Accept: application/json' \
  -H "x-api-key: ${CUSTOM_MODELS_CLIENT_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "x-user-token: Bearer ${TOKEN}" \
  -H 'x-request-id: list-models-1' | jq '.custom_models[] | {assetId, displayName, trainingMode}'

官方给出的响应是一个包装对象,不是裸数组。

{
  "custom_models": [
    {
      "version": "1",
      "assetName": "Warm Custom Model.ffcustommodel",
      "trainingMode": "style",
      "assetId": "urn:aaid:sc:VA6C2:bc1f46cd-be98-4a7b-9ffe-1111111111",
      "mediaType": "application/vnd.adobe.ffmodel+dcx",
      "publishedState": "published",
      "baseModel": { "name": "clio_v2", "version": "2.0.0" },
      "displayName": "Warm Custom Model"
    }
  ],
  "total_count": 1
}

官方页面内部有一处不一致:教程正文说模型 ID 就是响应里 assetId 那个字段,而同一页底部的完整示例读的是 repo:assetId。两者不是同一个键。

分页、排序与 publishedState

三个查询参数决定结果怎么切。sortBy 默认 "modifiedDate",前面加 - 就是倒序;limit 限制在 150start 默认 "0",并且官方注明 Required if a limit is specified

publishedState 默认 "published",列表看起来是空的时候先查它 —— 它的枚举里有 alltrainingfailed,也就是说模型可以存在但暂时不可用。这个端点文档化的状态码从 200 一直到 503,其中 406 通常指向你的 Accept 头,而不是令牌。

用自定义模型生成

生成沿用普通的异步出图端点,差别只是一个请求体字段加一个请求头。API reference 把它写成一条规则:When a custom model is used, a customModelId must also be passed in the request body.

x-model-version 该发哪个值

x-model-version 默认是 image3,枚举包括 "image3" "image3_custom" "image4_standard" "image4_ultra" "image4_custom",所以自定义模型对应的是 image3_customimage4_custom。用着默认值却塞进 customModelId,请求本身是自相矛盾的,不会悄悄帮你回退。

# Send the assetId as customModelId, and select the custom model in the header.
curl -s -X POST 'https://firefly-api.adobe.io/v3/images/generate-async' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'x-accept-mimetype: image/jpeg' \
  -H 'x-model-version: image3_custom' \
  -H "x-api-key: ${CUSTOM_MODELS_CLIENT_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -d "{
    \"numVariations\": 2,
    \"size\": { \"width\": 2048, \"height\": 2048 },
    \"prompt\": \"An almond seed in a warm setting\",
    \"contentClass\": \"photo\",
    \"style\": { \"presets\": [\"painting\"], \"strength\": 50 },
    \"promptBiasingLocaleCode\": \"en-US\",
    \"customModelId\": \"${CUSTOM_MODEL_ID}\"
  }" | jq .
四段式流水线示意图:品牌资产输入自定义模型,模型产出资产 ID,资产 ID 再驱动图片生成

和自定义模型搭配的字段有 numVariationssizecontentClassstyle。有一条限制容易违反:旧一代自定义模型不支持负向提示,官方原话是 Negative prompting is not supported for Firefly Custom Models on Image Model 3 or Firefly Custom Models on Image Model 4.

轮询,以及 Image5 迁移的坑

提交之后返回的是标准异步信封:jobIdstatusUrlcancelUrl。轮询流程不会因为用了自定义模型而改变,状态机与退避重试的细节在异步任务那一篇里。

import os
import time

import requests

CLIENT_ID = os.environ['CUSTOM_MODELS_CLIENT_ID']
TOKEN = os.environ['CUSTOM_MODELS_ACCESS_TOKEN']
HEADERS = {'Accept': 'application/json', 'x-api-key': CLIENT_ID,
           'Authorization': f'Bearer {TOKEN}'}


def pick_model_id():
    """List models and return the first usable asset ID."""
    response = requests.get('https://firefly-api.adobe.io/v3/custom-models',
                            headers={**HEADERS, 'x-user-token': f'Bearer {TOKEN}'},
                            params={'limit': '10'}, timeout=30)
    response.raise_for_status()
    models = response.json().get('custom_models', [])
    if not models:
        raise RuntimeError('no models returned; check that the model is shared')
    first = models[0]
    return first.get('assetId') or first['repo:assetId']


def generate_and_wait(model_id, prompt):
    body = {'prompt': prompt, 'customModelId': model_id, 'numVariations': 1}
    submit = requests.post(
        'https://firefly-api.adobe.io/v3/images/generate-async',
        headers={**HEADERS, 'Content-Type': 'application/json',
                 'x-model-version': 'image3_custom'},
        json=body, timeout=60)
    submit.raise_for_status()
    status_url = submit.json()['statusUrl']

    status = 'running'
    payload = {}
    while status not in ('succeeded', 'failed'):
        time.sleep(5)
        polled = requests.get(status_url, headers=HEADERS, timeout=30)
        polled.raise_for_status()
        payload = polled.json()
        status = payload.get('status', 'unknown')
    if status == 'failed':
        raise RuntimeError(payload)
    return [item['image']['url'] for item in payload['result']['outputs']]


print(generate_and_wait(pick_model_id(), 'An almond seed in a warm setting'))

列表为空时,先把过滤条件放宽,再去排查令牌。

# publishedState defaults to published. Ask for everything to find training or failed models.
curl -s -X GET 'https://firefly-api.adobe.io/v3/custom-models?publishedState=all&limit=50&sortBy=-modifiedDate' \
  -H 'Accept: application/json' \
  -H "x-api-key: ${CUSTOM_MODELS_CLIENT_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "x-user-token: Bearer ${TOKEN}" | jq '.total_count'

Image5 对这个字段的改动不是普通改名。Adobe 的映射表把 modelId 对应到 customModelId,并标注 New semantics; not a direct mapping.,语义变了,不是一一对应。同一张表还撤掉了 aspectRatiomodelVersionmodelSpecificPayloadoutput.cai,新增 negativePromptpromptBiasingLocaleCodevisualIntensity。官方对新旧兼容性的总结很直接:Payloads that do not conform to the new schema will be rejected by the API. 模型本身的能力见 Image 5 上手页

计费口径上,用自定义模型生成和用基础模型生成坐在同一行:官方 rate card 把 Generate Image 定义为 1 Operation = 1 image generated from Firefly foundational model or custom models,换算方式见我们的 API 计费拆解。训练侧则是官方未公布:需要多少张参考图、训练要多久、训练怎么计量,Adobe 的页面上都没有写。

常见问题

列出自定义模型用哪个端点? GET https://firefly-api.adobe.io/v3/custom-models,请求头要带 x-api-keyAuthorization,以及最容易被漏掉的 x-user-token: Bearer <token>

为什么列表是空的? 常见两种原因:模型没有共享给你项目的 Technical Account Email,或者 publishedState 用默认的 published 把它过滤掉了。

到底该发 assetId 还是 repo:assetId Adobe 的教程正文写的是前者,同一页的示例代码读的是后者,两种都备着。

该发哪个 x-model-version image3_customimage4_custom。官方没有公布训练端点,负向提示在 Image Model 3 与 4 的自定义模型上也不可用。