上传素材
先登记素材,等它可用后在生成请求里用 asset:// 引用。
本页内容
素材是登记在平台上的图片、视频或音频。登记一次后,可以在多个生成请求里重复引用。
什么时候用素材
生成请求里的图片、视频和音频有三种来源:直接写 HTTPS 链接、写 base64 data URI,或者引用已登记的素材。同一份文件要反复使用时,登记成素材更省事;只用一次时直接写链接即可。三种来源的规则见「生成视频」。
登记素材
向 POST /v1/assets 发送公开 HTTPS 地址、媒体类型和模型。登记需要 Idempotency-Key,API 密钥需要 service.use 权限。
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"
}'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)// 需要 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;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 是查询路径。
响应202
{
"asset_id": "ast-11111111111111111111111111111111",
"status": "processing"
}查询状态
用 GET /v1/assets/{asset_id} 查询,直到状态不再是 processing:
| 状态 | 含义 |
|---|---|
processing | 正在下载和校验,还不能引用。 |
active | 可以引用;同时返回大小、摘要,以及图片和视频的尺寸、音视频的时长。 |
unavailable | 不可用,failure_reason 说明原因。 |
curl --request GET "${CHENVOYA_BASE_URL}/v1/assets/${ASSET_ID}" \
--header "Authorization: Bearer ${CHENVOYA_API_KEY}"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)// 需要 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;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,稍后重试。