# 上传素材 先登记素材,等它可用后在生成请求里用 asset:// 引用。 素材是登记在平台上的图片、视频或音频。登记一次后,可以在多个生成请求里重复引用。 ## 什么时候用素材 生成请求里的图片、视频和音频有三种来源:直接写 HTTPS 链接、写 base64 data URI,或者引用已登记的素材。同一份文件要反复使用时,登记成素材更省事;只用一次时直接写链接即可。三种来源的规则见「[生成视频](./api-docs-zh-video-generate.md#sources)」。 ## 登记素材 向 `POST /v1/assets` 发送公开 HTTPS 地址、媒体类型和模型。登记需要 `Idempotency-Key`,API 密钥需要 `service.use` 权限。 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/assets" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://images.example.test/reference.png", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference" }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/assets" body_text = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/assets"; let bodyText = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/assets" bodyText := "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` 接受后平台在后台下载并校验,响应头 `Location` 是查询路径。 ```json { "asset_id": "ast-11111111111111111111111111111111", "status": "processing" } ``` ## 查询状态 用 `GET /v1/assets/{asset_id}` 查询,直到状态不再是 `processing`: | 状态 | 含义 | | --- | --- | | `processing` | 正在下载和校验,还不能引用。 | | `active` | 可以引用;同时返回大小、摘要,以及图片和视频的尺寸、音视频的时长。 | | `unavailable` | 不可用,`failure_reason` 说明原因。 | ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/assets/${ASSET_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") path = "/v1/assets/{asset_id}".replace("{asset_id}", urllib.parse.quote(ASSET_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const ASSET_ID = process.env.ASSET_ID; if (!ASSET_ID) throw new Error("Set ASSET_ID"); if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); const path = "/v1/assets/{asset_id}".replace("{asset_id}", encodeURIComponent(ASSET_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } assetID := os.Getenv("ASSET_ID") if assetID == "" { fail("Set ASSET_ID") } if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } path := strings.Replace("/v1/assets/{asset_id}", "{asset_id}", url.PathEscape(assetID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ## 在请求里引用 在 `image_url`、`video_url` 或 `audio_url` 的 `url` 里写 `asset://` 加素材 ID。为一个模型登记的素材,也可以用在兼容的其他模型的请求里。 - 引用仍在处理中的素材返回 `409 asset_not_ready`,等它变为 `active` 后用同一个 `Idempotency-Key` 重试。 - 素材不属于本团队、不存在或已删除时,统一返回 `asset_not_found`。 - 同一个请求里不能把 `asset://` 引用和 HTTPS 链接或 base64 混用。 ## 地址和额度 - 来源地址必须是公开的 HTTPS 地址,最长 2048 个字节,不能带用户信息;不安全的目标地址返回 `400 source_url_not_allowed`。 - 团队的素材存储额度用完时返回 `429 asset_storage_quota_exceeded`;接入容量受限时返回 `429 asset_intake_rate_limit_exceeded`,稍后重试。 ## 下一步 - [生成视频](./api-docs-zh-video-generate.md):文生视频、首帧、首尾帧、参考图和参考音视频的请求写法。 - [登记素材](./api-docs-zh-api-assets-create.md):需要 service.use 权限。 - [素材对象](./api-docs-zh-api-assets-object.md):素材状态与安全元数据。