OpenMontage HeyGen 数字人视频资产上传实战:upload.heygen.com 直传 API 的完整指南
在 OpenMontage 的 Agent 技能体系中,assets.md 是 avatar-video 技能(基于 HeyGen v2 API 的数字人视频制作)的核心基础参考,它定义了如何向 HeyGen 上传图片、视频、音频三类资产,并将这些资产接入后续的视频生成流程。读完本文,你将掌握 HeyGen 资产上传端点的完整请求/响应规范、多语言(curl / TypeScript / Python)实现方式、流式上传大文件的技巧,以及如何把上传产物(资产 URL、资产 ID、image_key)正确填入 /v2/video/generate 的视频配置中;同时本文结合 OpenMontage 仓库源码,展示该上传机制在工具链中的真实落地方式。
资产上传在 avatar-video 技能中的定位
OpenMontage 通过 .agents/skills/ 目录组织 700 多个 Agent 技能与生产知识文件,让 AI 编码助手具备完整的视频制作能力。其中 avatar-video 技能面向"精确控制"场景:由使用者指定数字人、配音、脚本和背景,通过 HeyGen /v2/video/generate 接口生成视频。技能入口 SKILL.md 规定了默认工作流:列出数字人 → 选择声音 → 撰写脚本 → 生成视频 → 轮询状态,并把各主题拆分到 15 个参考文档中。
在该技能的快速参考表中,"上传资产(图片、音频)"指向 assets.md,与 backgrounds.md(背景配置)、photo-avatars.md(照片数字人)构成一条数据链:
- 先有资产:自定义背景图、动态视频背景、自定义配音都必须是 HeyGen 可访问的资源,本地文件必须先通过上传端点入库;
- 再填配置:上传响应的
data.url、data.id、data.image_key分别服务于背景 URL、talking photo ID、照片数字人创建等下游步骤; - 最后生成:把资产字段填入
video_inputs,调用/v2/video/generate得到video_id。
技能的 frontmatter 声明了运行前提:需要环境变量 HEYGEN_API_KEY(primaryEnv: HEYGEN_API_KEY),且所有请求通过 X-Api-Key 请求头鉴权。仓库中的 PROVIDERS.md 也给出了 HeyGen 的接入说明:注册账号后在设置中生成 API Key,写入 .env 的 HEYGEN_API_KEY 即可解锁对应能力。
上传机制:单步直传原始二进制
HeyGen 的资产上传是单步流程:直接把文件原始二进制 POST 到上传端点,不需要 JSON 包装、不需要表单字段、也不需要预签名 URL 交换。唯一的硬性约束是 Content-Type 请求头必须与文件的真实 MIME 类型一致。
端点: POST https://upload.heygen.com/v1/asset
请求头规范
| Header | 必填 | 说明 |
|---|---|---|
X-Api-Key |
✓ | 你的 HeyGen API Key(即 HEYGEN_API_KEY 环境变量的值) |
Content-Type |
✓ | 文件的 MIME 类型(如 image/jpeg、video/mp4) |
请求体就是文件二进制数据本身。这一点与常见的"先申请上传凭证、再 PUT 到对象存储"的两段式方案不同:调用方少一次交互,但要求 Content-Type 如实声明,否则服务端可能按错误的媒体类型处理资产。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
code |
number | 状态码(100 = 成功) |
data.id |
string | 资产唯一 ID,供视频生成引用 |
data.name |
string | 资产名称 |
data.file_type |
string | image、video 或 audio |
data.url |
string | 上传后可访问的 URL |
data.image_key |
string | null | 创建照片数字人的键(仅图片有值) |
data.folder_id |
string | 所在文件夹 ID(不在文件夹中时为空) |
data.meta |
string | null | 资产元数据 |
data.created_ts |
number | 创建时间的 Unix 时间戳 |
三个产物字段是理解整个资产体系的关键:
data.url:通用引用。作为背景图/视频背景的background.url、作为音频输入的voice.audio_url,都用它;data.id:结构体引用。作为talking_photo数字人的talking_photo_id时用它;data.image_key:仅图片返回,形如image/{id}/original.jpg,是创建照片数字人(photo avatar)的专用输入。photo-avatars.md 中特别强调"要保存image_key而不是id",因为image_key才是创建 avatar group 时 S3 路径语义的键。
用 curl 上传
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./background.jpg'
--data-binary 会原样发送文件内容且不转义特殊字符,配合显式的 Content-Type 头即完成一次标准上传。
TypeScript 实现(缓冲上传)
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
name: string;
file_type: string;
url: string;
image_key: string | null;
folder_id: string;
meta: string | null;
created_ts: number;
};
msg: string | null;
message: string | null;
}
async function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: fileBuffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
// Usage
const asset = await uploadAsset("./background.jpg", "image/jpeg");
console.log(`Uploaded asset: ${asset.id}`);
console.log(`Asset URL: ${asset.url}`);
注意错误判定依据是业务字段 code !== 100,而不是 HTTP 状态码——这与 HeyGen 系列接口的统一包裹风格(code + data + message)一致,客户端必须同时处理两种失败信号。
TypeScript 实现(流式上传大文件)
当文件较大时,把整个文件读进内存不划算。技能文档给出了基于 Node.js 文件流的写法,要点是显式设置 Content-Length 并通过 duplex: "half" 告诉 undici/fetch 这是流式请求体:
import fs from "fs";
import path from "path";
import { stat } from "fs/promises";
async function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileStats = await stat(resolvedPath);
const fileStream = fs.createReadStream(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
"Content-Length": fileStats.size.toString(),
},
body: fileStream as any,
// @ts-ignore - duplex is needed for streaming
duplex: "half",
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
duplex: "half" 是流式上传的易错点:缺少它时,fetch 会拒绝以 ReadableStream 为 body 的请求。Content-Length 则让服务端在接收前就能预判载荷规模,也便于做限流与配额检查。
Python 实现
import requests
import os
def upload_asset(file_path: str, content_type: str) -> dict:
with open(file_path, "rb") as f:
response = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": content_type
},
data=f
)
data = response.json()
if data.get("code") != 100:
raise Exception(data.get("message", "Upload failed"))
return data["data"]
# Usage
asset = upload_asset("./background.jpg", "image/jpeg")
print(f"Uploaded asset: {asset['id']}")
print(f"Asset URL: {asset['url']}")
Python 版用 requests 的 data=f(f 为二进制文件对象)实现与 curl 等效的直传;同样以 code != 100 作为失败判定。
支持的 Content-Type 与典型用途
| 类型 | Content-Type | 用途 |
|---|---|---|
| JPEG | image/jpeg |
背景图、talking photo 源图 |
| PNG | image/png |
背景图、需要透明度的叠加层 |
| MP4 | video/mp4 |
动态(循环)视频背景 |
| WebM | video/webm |
动态(循环)视频背景 |
| MP3 | audio/mpeg |
自定义音频输入 |
| WAV | audio/wav |
自定义音频输入 |
这三类资产恰好覆盖 /v2/video/generate 的三个可定制维度:视觉背景(image/video)、数字人素材(image → talking photo / photo avatar)、语音来源(audio)。选择格式时的原则:照片类素材用 JPEG(体积更小),带透明度的图形用 PNG,配音优先用与预期成片时长匹配的 MP3/WAV。
从 URL 上传:资产已在公网时的两步法
如果资产已经托管在可公开访问的地址上,不需要走对象存储再复制,而是先下载、再直传:
async function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse["data"]> {
// 1. Validate and download the file
const url = new URL(sourceUrl);
if (url.protocol !== "https:") {
throw new Error("Only HTTPS URLs are supported");
}
const sourceResponse = await fetch(sourceUrl);
const buffer = Buffer.from(await sourceResponse.arrayBuffer());
// 2. Upload directly to HeyGen
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: buffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
这里的实现做了一个值得注意的工程决策:只允许 HTTPS 源地址,并在拿到完整 buffer 后重新执行一次标准直传,而不是把 URL 直接交给 HeyGen。这样做的效果是:源站不可达/返回错误时会在本地立刻暴露;同时上传请求的 Content-Type 由调用方显式控制,不受源站响应头的影响。
把上传资产接入视频生成
上传成功后,资产如何被消费取决于用途,三者使用响应中的不同字段,这是最容易混淆的地方。
作为背景图 / 视频背景
background 对象使用 data.url:
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello, this is a video with a custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: asset.url, // 上传响应中的 URL
},
},
],
};
这与 backgrounds.md 中的背景规范一致:background 支持 color / image / video 三种 type,其中 image 和 video 类型都要求携带 url 字段(缺失 url/value 是"背景不生效"的常见原因,该文档还给出了排查示例)。
作为 Talking Photo 源
talking photo 数字人使用 data.id,而非 URL:
const talkingPhotoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: asset.id, // 上传响应中的 ID
},
voice: {
type: "text",
input_text: "Hello from my talking photo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
v2 生成接口的字段表 也印证了这一约定:character.type 为 "talking_photo" 时必须提供 talking_photo_id。而如果要走"照片 → 数字人 avatar group"的完整流程(photo-avatars.md),则使用 image_key 调用 POST /v2/photo_avatar/avatar_group/create。同一次上传,按下游用途取不同字段,这就是资产对象三字段并存的原因。
作为音频输入
voice 对象切换到 type: "audio" 并使用 data.url:
const audioConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: asset.url, // 上传响应中的 URL
},
},
],
};
按 video-generation.md 的字段表,voice 支持 text / audio / silence 三种类型,audio_url 仅在 type 为 audio 时必填。
完整工作流:上传背景并生成视频
把上传与生成串起来的完整链路如下——上传背景图、组装 video_inputs、调用 /v2/video/generate、返回 video_id:
async function createVideoWithCustomBackground(
backgroundPath: string,
script: string
): Promise<string> {
// 1. Upload background
console.log("Uploading background...");
const background = await uploadAsset(backgroundPath, "image/jpeg");
// 2. Create video config
const config = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: background.url,
},
},
],
dimension: { width: 1920, height: 1080 },
};
// 3. Generate video
console.log("Generating video...");
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const { data } = await response.json();
return data.video_id;
}
拿到 video_id 之后,按 video-status.md 的轮询模式调用 GET /v2/videos/{video_id},等待状态变为 completed 后取下载 URL。SKILL.md 的最佳实践还提醒:开发期可设 test: true 进入测试模式(带水印、不消耗额度),且视频生成通常需要 5–15 分钟,客户端应设置充裕的超时。
资产限制与最佳实践
限制
- 文件大小:最大 10MB;
- 图片尺寸:建议与视频尺寸一致(如 1080p 视频配 1920×1080 背景),比例不匹配时可能被裁切或拉伸;
- 音频时长:应与预期成片时长匹配;
- 保留期:资产在长时间不活跃后可能被清理,不应依赖其作为永久存储。
最佳实践
- 上传前优化图片——先缩放到与视频输出一致的尺寸再上传,既省带宽也保证构图不被裁切;
- 选对格式——照片用 JPEG,需要透明通道的图形用 PNG;
- 本地预校验——上传前检查文件类型与大小,避免把 20MB 的文件发给一个 10MB 上限的端点;
- 处理上传失败——对瞬时网络错误实现重试逻辑;
- 缓存资产 ID/URL——同一背景在多条视频生成间复用,避免重复上传消耗时间与流量。
仓库源码印证:OpenMontage 工具链中的资产上传
技能文档描述的是 HeyGen 的直传端点,而 OpenMontage 的 Python 工具链在实现上走了"预签名上传 + 降级"的路线,两者互为补充。
在 tools/video/_shared.py 中,upload_image_heygen 函数的策略是:
- 先请求
POST https://api.heygen.com/v2/assets/upload(v2 预签名上传端点),提交content_type与file_name,从响应中取出upload_url与file_url; - 把文件字节
PUT到upload_url; - 任一步失败则整体
except吞掉,降级调用upload_image_fal把图片放到 fal.ai 存储并返回公网 URL。
从源码结构看,这条链路的产出(一个公网可访问的图片 URL)最终喂给 HeyGen 工作流执行接口 POST https://api.heygen.com/v1/workflows/executions(见同文件 generate_heygen_video),其 reference_image_url 字段接受 URL 而非资产 ID——即工具链选择"URL 优先",与技能文档中"资产对象优先"的 v2 用法是同一能力面的两种封装。该函数的 docstring 也写明其意图:"Upload a local image to HeyGen and return a public URL"。
在 tools/video/heygen_video.py 中,HeyGenVideo 工具声明了与本文主题相关的运行时契约:
get_status以os.environ.get("HEYGEN_API_KEY")是否存在决定工具可用/不可用(L95-L96),与 SKILL.md 的primaryEnv: HEYGEN_API_KEY声明一致;install_instructions提示设置该环境变量后即可解锁;retry_policy配置为最多 2 次重试、10 秒退避,可重试错误含rate_limit、timeout、server_error——正是资产上传"处理上传错误、实现重试"这一最佳实践在工具层的制度化体现;side_effects明确标注了"calls HeyGen API",让 Agent 在执行前知晓外部调用边界。
此外,tools/video/_shared.py 中的 poll_heygen 采用 5 秒起始、1.2 倍递增、上限 30 秒的自适应轮询间隔,并在 completed 时从 output.video 提取 video_url、在 failed/error 时抛错——这套轮询语义与技能文档要求的"轮询直至 completed、设置充裕超时"相互印证。
小结
HeyGen 资产上传的设计非常克制:一个端点、两个请求头、原始二进制,换回一个包含 id / url / image_key 的统一资产对象;三者分别对接背景、talking photo 和照片数字人三条下游链路。对工程实现而言,真正需要写对的地方只有三处——如实声明 Content-Type、以 code == 100 判定业务成败、对失败做重试并缓存已上传资产。OpenMontage 将这套 API 规范固化为 Agent 技能参考文档,并在 tools/video/ 工具链中给出了预签名上传与降级的 Python 实现,二者结合覆盖了从"精确控制 v2 资产"到"工具链自动上传"的完整使用面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00