什麼時候需要素材引用
Seedance 2.0 生成「人物一致性」影片時,不能直接上傳含寫實人臉的參考圖(防深偽攔截)。需要先把人像入庫成可信素材,拿到asset://xxx 素材 ID,再在生成請求裡引用——同一角色跨集、跨鏡頭保持臉部與服裝一致。先對號入座:
| 素材型別 | 例子 | 怎麼用 |
|---|---|---|
| 動漫 / 風格化角色 | 二次元、卡通、3D 卡通角色 | 不含寫實人臉,一般不觸發攔截:直接用公網 URL / Base64 作參考圖,無需入庫,本頁流程從「第 4 步生成」開始即可 |
| 虛擬人(AI 生成寫實人像) | 模型生成、現實中無此人 | 走「虛擬人像入庫」(本頁完整流程),拿 asset:// ID 引用 |
| 真人人臉 | 藝人、模特、使用者本人的照片 | 先走「真人認證」全自動 API 流程拿真人素材組,入庫時帶上該組 groupId,其餘程式碼與本頁完全一致 |
前置條件
兩把鑰匙,不要混用:
- 素材庫 KEY(icover.ai「設定 → 素材庫 KEY」建立,
sk-...):用於上傳 / 入庫 / 查詢素材,建立方法見 素材庫第 0 步。 - APIYI SeeDance2 令牌(api.apiyi.com 建立,
sk-...,須勾選SeeDance2分組):用於影片生成介面。
鏈路總覽
本地圖片 → ①presign 申請直傳地址並 PUT 上傳,拿公網 URL → ② 素材入庫,拿 asset Id → ③ 輪詢到 Active(單圖約 13 秒)→ ④ 生成請求裡引用 asset://<Id>,提示詞用「圖片1」指代 → ⑤ 輪詢任務到 succeeded → ⑥ 下載 content.video_url(簽名直鏈 24 小時過期,立即轉存)。
已有公網圖片 URL 的可跳過 ①;素材 ID 永久有效,入庫一次即可反覆引用。
完整程式碼
import time
from pathlib import Path
import requests
ASSET_KEY = "sk-你的素材庫KEY" # icover.ai「設定 → 素材庫 KEY」
APIYI_KEY = "sk-你的APIYI令牌" # api.apiyi.com,須勾選 SeeDance2 分組
IMAGE_PATH = "portrait.jpg" # 本地參考圖(虛擬人像)
PROMPT = "圖片1中的人物正面微笑,鏡頭緩慢推近,自然光"
ICOVER = "https://icover.ai/api"
SEEDANCE = "https://api.apiyi.com/seedance/api/v3/contents/generations/tasks"
ASSET_HEADERS = {"Authorization": f"Bearer {ASSET_KEY}"}
APIYI_HEADERS = {
"Authorization": f"Bearer {APIYI_KEY}",
"Content-Type": "application/json",
"Accept-Encoding": "identity", # 必加:規避閘道 gzip 頭與實際編碼不符
}
def upload_image(path: str) -> str:
"""① presign + PUT 上傳本地圖片,返回公網 URL(已有公網 URL 可跳過)"""
ext = Path(path).suffix.lstrip(".").lower() or "jpg"
content_type = f"image/{'jpeg' if ext in ('jpg', 'jpeg') else ext}"
data = requests.post(
f"{ICOVER}/storage/presign", headers=ASSET_HEADERS,
json={"ext": ext, "contentType": content_type}, timeout=30,
).json()["data"]
resp = requests.put(
data["uploadUrl"], data=Path(path).read_bytes(),
headers={"Content-Type": content_type}, timeout=120, # 與 presign 一致
)
resp.raise_for_status()
return data["publicUrl"]
def ingest_asset(image_url: str, label: str = "", group_id: str = None) -> str:
"""② 素材入庫,返回 asset Id。真人素材:帶上真人認證拿到的 group_id"""
body = {"imageUrl": image_url, "label": label}
if group_id:
body["groupId"] = group_id
r = requests.post(
f"{ICOVER}/asset-library/assets", headers=ASSET_HEADERS,
json=body, timeout=60,
).json()
return r["Result"]["Id"]
def wait_asset_active(asset_id: str, timeout: int = 90) -> None:
"""③ 輪詢入庫狀態到 Active(單圖約 13 秒,無 SLA)"""
deadline = time.time() + timeout
while time.time() < deadline:
status = requests.get(
f"{ICOVER}/asset-library/assets/{asset_id}",
headers=ASSET_HEADERS, timeout=30,
).json()["Result"]["Status"]
print("asset status:", status)
if status == "Active":
return
if status == "Failed":
raise RuntimeError("素材入庫失敗:檢查格式 / 寬高比 0.4-2.5 / 邊長 300-6000px 後重傳")
time.sleep(3)
raise TimeoutError("90 秒未 Active,請排查後重試")
def create_video_task(asset_id: str) -> str:
"""④ 引用 asset:// 建立生成任務。提示詞用「圖片1」指代,不要寫 asset ID 原文"""
body = {
"model": "doubao-seedance-2-0-260128",
"content": [
{"type": "text", "text": PROMPT},
{"type": "image_url",
"image_url": {"url": f"asset://{asset_id}"},
"role": "reference_image"},
],
"resolution": "720p", "ratio": "16:9", "duration": 5,
}
return requests.post(SEEDANCE, headers=APIYI_HEADERS, json=body, timeout=60).json()["id"]
def wait_video(task_id: str) -> dict:
"""⑤ 輪詢任務到終態(succeeded / failed / expired)"""
while True:
time.sleep(20)
task = requests.get(f"{SEEDANCE}/{task_id}", headers=APIYI_HEADERS, timeout=30).json()
print("task status:", task.get("status"))
if task.get("status") in ("succeeded", "failed", "expired"):
return task
def download(task: dict, out: str) -> None:
"""⑥ 下載影片。直鏈 24 小時過期,立即轉存;下載不要帶 Authorization 頭"""
with requests.get(task["content"]["video_url"], stream=True, timeout=300) as r:
r.raise_for_status()
with open(out, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 20):
f.write(chunk)
print("已儲存", out)
if __name__ == "__main__":
public_url = upload_image(IMAGE_PATH)
asset_id = ingest_asset(public_url, label="角色A-正面")
print("素材 ID:", f"asset://{asset_id}")
wait_asset_active(asset_id)
task_id = create_video_task(asset_id)
print("task_id:", task_id)
task = wait_video(task_id)
if task["status"] == "succeeded":
print("計費 tokens:", task["usage"]["completion_tokens"])
download(task, f"{task_id}.mp4")
else:
print("任務未成功:", task.get("error"))
# ① 申請直傳地址(已有公網圖片 URL 可跳過 ①②)
curl -X POST https://icover.ai/api/storage/presign \
-H "Authorization: Bearer sk-你的素材庫KEY" \
-H "Content-Type: application/json" \
-d '{"ext":"jpg","contentType":"image/jpeg"}'
# → { "code":0, "data": { "uploadUrl":"...", "publicUrl":"https://cdn.icover.ai/..." } }
# ② PUT 上傳檔案(Content-Type 要和申請時一致),成功後 publicUrl 即公網地址
curl -X PUT "剛才返回的uploadUrl" \
-H "Content-Type: image/jpeg" \
--data-binary @portrait.jpg
# ③ 素材入庫(真人素材加 "groupId":"真人認證拿到的組ID")
curl -X POST https://icover.ai/api/asset-library/assets \
-H "Authorization: Bearer sk-你的素材庫KEY" \
-H "Content-Type: application/json" \
-d '{"imageUrl":"https://cdn.icover.ai/uploads/seedance/xxx.jpg","label":"角色A-正面"}'
# → { ..., "Result": { "Id": "asset-2026xxxx-xxxxx" } }
# ④ 每 3 秒查一次,Result.Status == "Active" 即可用
curl https://icover.ai/api/asset-library/assets/asset-2026xxxx-xxxxx \
-H "Authorization: Bearer sk-你的素材庫KEY"
# ⑤ 引用 asset:// 建立生成任務(換成 APIYI 令牌!提示詞用「圖片1」指代素材)
curl -X POST https://api.apiyi.com/seedance/api/v3/contents/generations/tasks \
-H "Authorization: Bearer sk-你的APIYI令牌" \
-H "Content-Type: application/json" \
-d '{
"model": "doubao-seedance-2-0-260128",
"content": [
{"type":"text","text":"圖片1中的人物正面微笑,鏡頭緩慢推近,自然光"},
{"type":"image_url","image_url":{"url":"asset://asset-2026xxxx-xxxxx"},"role":"reference_image"}
],
"resolution":"720p","ratio":"16:9","duration":5
}'
# → {"id":"cgt-2026xxxx-xxxxx"}
# ⑥ 每 20 秒查一次,status=succeeded 後下載 content.video_url(24 小時過期)
curl https://api.apiyi.com/seedance/api/v3/contents/generations/tasks/cgt-2026xxxx-xxxxx \
-H "Authorization: Bearer sk-你的APIYI令牌"
import { readFileSync, writeFileSync } from "node:fs";
const ASSET_KEY = "sk-你的素材庫KEY"; // icover.ai「設定 → 素材庫 KEY」
const APIYI_KEY = "sk-你的APIYI令牌"; // api.apiyi.com,須勾選 SeeDance2 分組
const IMAGE_PATH = "portrait.jpg";
const ICOVER = "https://icover.ai/api";
const SEEDANCE = "https://api.apiyi.com/seedance/api/v3/contents/generations/tasks";
const AH = { Authorization: `Bearer ${ASSET_KEY}`, "Content-Type": "application/json" };
const VH = { Authorization: `Bearer ${APIYI_KEY}`, "Content-Type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// ① presign + PUT 上傳本地圖片,拿公網 URL
const { data } = await fetch(`${ICOVER}/storage/presign`, {
method: "POST", headers: AH,
body: JSON.stringify({ ext: "jpg", contentType: "image/jpeg" }),
}).then((r) => r.json());
await fetch(data.uploadUrl, {
method: "PUT",
headers: { "Content-Type": "image/jpeg" },
body: readFileSync(IMAGE_PATH),
});
// ② 素材入庫(真人素材:body 里加 groupId)
const ingest = await fetch(`${ICOVER}/asset-library/assets`, {
method: "POST", headers: AH,
body: JSON.stringify({ imageUrl: data.publicUrl, label: "角色A-正面" }),
}).then((r) => r.json());
const assetId = ingest.Result.Id;
console.log("素材 ID:", `asset://${assetId}`);
// ③ 輪詢到 Active(約 13 秒)
let assetStatus;
do {
await sleep(3000);
const info = await fetch(`${ICOVER}/asset-library/assets/${assetId}`, { headers: AH })
.then((r) => r.json());
assetStatus = info.Result.Status;
console.log("asset status:", assetStatus);
} while (assetStatus !== "Active" && assetStatus !== "Failed");
if (assetStatus === "Failed") throw new Error("素材入庫失敗,請檢查圖片後重傳");
// ④ 引用 asset:// 建立生成任務(提示詞用「圖片1」指代,不要寫 asset ID 原文)
const { id } = await fetch(SEEDANCE, {
method: "POST", headers: VH,
body: JSON.stringify({
model: "doubao-seedance-2-0-260128",
content: [
{ type: "text", text: "圖片1中的人物正面微笑,鏡頭緩慢推近,自然光" },
{ type: "image_url", image_url: { url: `asset://${assetId}` }, role: "reference_image" },
],
resolution: "720p", ratio: "16:9", duration: 5,
}),
}).then((r) => r.json());
console.log("task_id:", id);
// ⑤ 輪詢任務到終態
let task;
do {
await sleep(20000);
task = await fetch(`${SEEDANCE}/${id}`, { headers: VH }).then((r) => r.json());
console.log("task status:", task.status);
} while (!["succeeded", "failed", "expired"].includes(task.status));
// ⑥ 下載(直鏈 24 小時過期,立即轉存;下載不帶 Authorization 頭)
if (task.status === "succeeded") {
const buf = Buffer.from(await fetch(task.content.video_url).then((r) => r.arrayBuffer()));
writeFileSync(`${id}.mp4`, buf);
console.log(`已儲存 ${id}.mp4`);
} else {
console.log("任務未成功:", task.error);
}
提示詞裡用「圖片1」「圖片2」指代素材(按
content 陣列順序對應),不要寫 asset ID 原文。多素材進階用法
- 多張參考圖:
content裡放多個image_url(0–9 張,role均為reference_image),提示詞用「圖片1」「圖片2」按順序指代。同一人物建議「全身正面 + 人臉正面無表情特寫」兩張一起引用,一致性最好。 - 混用來源:
asset://素材 ID、公網 URL、Base64(data:image/png;base64,...)可以在同一個content裡混用——只有含寫實人臉的圖必須走asset://。 - 參考影片 / 音訊:還可加 0–3 個
video_url(role: "reference_video")與 0–3 個audio_url(role: "reference_audio"),至少要有 1 圖或 1 影片;音訊需與圖片或影片一起傳。 - 真人素材:先按 真人認證全自動流程 讓藝人完成活體認證、拿到專屬真人素材組
GroupId,入庫時帶上groupId即可;生成側程式碼與本頁完全一致。
常見報錯速查
| 報錯 / 現象 | 原因 | 處理 |
|---|---|---|
400 The specified asset ... is not found | asset:// 不在 APIYI 通道對應的賬號下(如在其他渠道商入庫),或 ID 寫錯 | 按本頁流程在 icover.ai 重新入庫;核對素材 ID |
| 「該模型無可用渠道」 | APIYI 令牌未勾選 SeeDance2 分組 | 到 api.apiyi.com 令牌設定勾選後重試 |
| Python 報 gzip 解碼錯誤 / 返回 JSON 缺頭不完整 | 閘道 gzip 響應頭與實際編碼不符 | 請求頭加 "Accept-Encoding": "identity"(指令碼已帶) |
素材狀態 Failed | 圖片格式 / 尺寸不合規 | 寬高比 0.4–2.5、邊長 300–6000px、少於 30MB,修圖後重傳 |
| 直接傳人臉圖被拒 | 寫實人臉不能作直接參考圖(防深偽攔截) | 先入庫拿 asset:// 再引用;真人照片須先完成真人認證 |
PUBLIC_ 字首錯誤 | 上游官方內容稽核攔截(該次不計費) | 調整素材 / 提示詞後直接重試 |
相關頁面
Seedance 2.0 概覽
模型選型、定價、解析度畫素表與 FAQ
影片生成 API
完整參數列、四種生成模式與響應格式
素材庫
素材庫全部介面、網頁零程式碼操作與真人認證