视频 API
Partner 视频接口的输入顺序、逐模型限制、自动路由、输出尺寸、异步任务、错误与完整调用示例
Partner 视频 API(v2)
历史 v1 文档保留切换前的合同。
本页面向 XCamShow/B 端服务端集成。Partner 使用 Bearer Key;网站 /api/proxy/v1/videos 使用登录会话。两种入口共用下游任务系统,但不能互换凭据、账户或幂等请求头。
环境与鉴权
| 环境 | Base URL |
|---|---|
| 正式 | https://ic.xshow.live/api/partner/v1 |
| 开发验收 | https://dev.ic.xshow.live/api/partner/v1 |
Authorization: Bearer <PARTNER_API_KEY>
Idempotency-Key: <业务订单或请求的唯一标识>Key 仅放在调用方服务端,不进入浏览器、URL、日志或代码仓库。使用对应环境发放的 Key;两个环境的任务与业务数据不能混用。创建请求必须有 Idempotency-Key,同一业务请求重试复用原值,新请求才更换。不要把账户 ID、内部路由结果或 GPU 地址当作公开参数。
模型、输入与处理链
媒体通过重复字段 input_reference[] 上传,顺序是合同的一部分。
功能 / model | 按顺序上传 | 图片阶段 | 视频阶段 | 可下载的中间 PNG |
|---|---|---|---|---|
视频换脸 video-face-swap(兼容 faceswap) | 身份图、源视频 | 无独立图片预编辑阶段 | Wan3.0 R2V;按源视频时长、分辨率和帧率整理输出 | 无 |
视频换人 video-person-swap | 头部身份图、可选身体图、源视频 | BFS 换头后 BodySwap 换身体 | Wan2.1 SCAIL2 | BodySwap 完成后的结果 |
视频编辑 video-undress | 源视频 | 无独立图片预编辑阶段 | Wan3.0 视频编辑;按源视频时长、分辨率和帧率整理输出 | 无 |
Spicy 视频换头 video-head-swap-spicy | 身份图、源视频;或身份图 + template_id | 身份图检测;需要时先 Avatar 裁切 | LTX 2.5 双 pass | 无 |
视频姿势 video-pose | 人物图 | Toolsbox 姿势图片编辑 | Wan3.0 I2V;新任务不回退 MiniMax | 姿势编辑后的关键帧 |
视频换人传一张身份图时,头部和身体阶段复用该图;两张身份图不能交换顺序。所有组合仍只返回一个视频任务 ID,中间图不是另一个需要调用方提交的任务。
video-pose 的 prompt 与 lora 必填,video_prompt 可选;默认 720p / 9:16 / 5 秒。服务端先完成姿势图片编辑,再用该结果执行 Wan3.0 I2V。新任务的 Wan3 提交或生成失败会返回原错误,不自动创建 MiniMax 任务。调用方只维护一个视频任务 ID。
视频换脸、视频编辑与源规格
video-face-swap(包括 faceswap 别名)与 video-undress 的公开模型名保持不变;服务端将请求改写为实际计费与执行模型 wan3.0-video,固定 duration_mode=match_source_not_longer 和 output_geometry_mode=match_source。供应商生成长度先向上覆盖源时长,完成后整理为源视频实际时长、编码宽高及帧率,最长处理前 15 秒,不延长。生成式模型仍可能改变画面内容;规格匹配不等于逐帧像素一致或原声逐样本一致。
管理后台 /admin/generations 按实际模型 wan3.0-video 显示这两类任务,用户是 Partner 绑定的共享账号;可用任务 ID 或 partner:xcamshow: 操作 ID 查找。held 表示仍在生成或等待结果,不代表记录缺失。旧任务仍按创建时使用的模型显示。
video-person-swap 与 video-head-swap-spicy 保持独立原有链路;video-face-swap 不再执行旧 Klein/BFS→SCAIL2 链,也不再使用 template_id。需要 Spicy 模板时使用 video-head-swap-spicy。
输入限制
| 模式 | 源媒体与时长 | 帧率 / 音轨 | 尺寸与输出 |
|---|---|---|---|
| Wan3 换脸 / 视频编辑 | MP4/MOV,源视频最少 2 秒;输出最多源视频前 15 秒 | 保持源帧率,输出按请求包含生成音频 | 输出与源片的时长、分辨率、帧率一致(15 秒上限) |
| 视频换人 | MP4/MOV,最多 15 秒 | 最多 60 FPS,要求音轨 | 保持现有 SCAIL2 画布合同 |
| Spicy 上传模式 | MP4/MOV,最多 10 秒 | 要求音轨,仍受基础视频校验 | LTX 原工作流 |
| Spicy 模板模式 | 不上传源视频;服务端使用模板前 10 秒 | 模板工作流决定 | LTX 原工作流 |
video-pose | 不上传源视频,上传 1 张人物图 | Wan3.0 I2V | 默认 720p / 9:16 / 5 秒 |
- 人物图:JPG/PNG/BMP/WEBP,每张最多 30MB;不要上传多帧 GIF 充当身份图。
- 源视频:每个最多 500MB、恰好一个主视频流;最长边不超过 4096、编码面积不超过 16MiP,解码帧/包计数上限 903。
- MIME 必须正确:
image/png、image/jpeg、video/mp4、video/quicktime等;不要把视频声明为application/octet-stream。 - 带旋转元数据(rotate / display matrix)的源视频返回
400,提示先导出无旋转版本重试;当前解码链路无法保证旋转源在图片阶段与视频阶段的方向一致。 - 视频换人和 Spicy 的音轨要求保持原合同;Wan3 换脸与视频编辑以当前 Wan3 视频输入校验为准。
- SCAIL2 的
seconds、fps、ratio、size不用于任意改写源视频时长、帧率或生成画布。
GIF 输入与输出
video-person-swap 保留 GIF 作为最后一个源媒体 part 的旧合同,MIME 为 image/gif。Proxy 转换为带静音音轨的 MP4 后走原算法,仍须通过时长和帧数校验。当前 Wan3 换脸与视频编辑要求 MP4/MOV 视频文件。
Spicy 的 Partner 上传入口要求第二个文件是 video/*,不要给它传 GIF。模板模式不上传源视频,video-pose 也不接收源 GIF。
| 下载方式 | 返回 |
|---|---|
原请求源为 MP4/MOV,GET /videos/{id}/content | MP4 |
video-person-swap 源为 GIF,GET /videos/{id}/content | GIF |
GET /videos/{id}/content?format=gif | 显式 GIF,15 FPS、最大宽度 720px、无音频 |
GET /videos/{id}/content?format=mp4 | 内部生成的 MP4 |
根据响应 Content-Type 保存正确后缀,不要把 GIF 字节保存为 .mp4。
创建参数
POST /videos,请求体为 multipart/form-data;不要自行填写不含 boundary 的 Content-Type。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
model | string | 是 | 使用上表公开模型名 |
input_reference[] | 重复 file | 是 | 上传顺序和数量严格按模式表 |
prompt | string | 姿势模式必填 | 普通换脸/换人为空时使用保留身份、动作、场景、构图的默认约束;非空原样用于视频阶段 |
seed | 十进制整数文本 | 否 | 省略时沿用工作流默认值;显式值会按执行节点的合法范围处理,重试不可改值 |
template_id | string | 模板模式必填 | 与源视频二选一;仅用于 video-head-swap-spicy |
lora | string | 姿势模式必填 | 服务已开放的姿势 LoRA 标识,不是任意下载地址 |
video_prompt | string | 否 | 姿势模式的视频阶段提示词 |
seconds / resolution_name / ratio | string | 否 | 姿势模式参数;默认 5 / 720p / 9:16;Wan3 换脸和视频编辑由服务端固定为源规格 |
业务订单放在 Idempotency-Key 请求头;没有 external_order_id 请求字段。不要传内部工作流路径、face_swap_model、供应商 Key 或 GPU 服务 Token。
创建示例
export BASE_URL="https://ic.xshow.live/api/partner/v1"
curl --fail-with-body "$BASE_URL/videos" \
-H "Authorization: Bearer $PARTNER_API_KEY" \
-H "Idempotency-Key: $ORDER_ID" \
-F "model=video-face-swap" \
-F "input_reference[]=@identity.png;type=image/png" \
-F "input_reference[]=@source.mp4;type=video/mp4" \
-F "seed=77"视频换人:改为 model=video-person-swap,需要独立身体图时在身份图与源视频之间再加一项 input_reference[]=@body.png;type=image/png。
模板模式:使用 model=video-head-swap-spicy,添加已存在的 template_id,删除源视频 part。未知模板返回错误,不会随机选择其它素材。
查询、下载与取消
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /videos | 创建;保留返回 id |
POST | /videos/{id} | 查询;不是 GET |
GET | /videos/{id}/intermediate | 中间 PNG;只有已可用时下载 |
GET | /videos/{id}/content | 最终媒体;可传上述 format |
POST | /videos/{id}/cancel | 请求取消;以回执及后续状态为准 |
全部携带 Bearer Key。一次 HTTP 超时不等于任务失败或未创建;查询原任务,或用原幂等键和完全相同的请求核对,禁止另造订单重发。中间图成功不代表视频已成功。取消请求成功发出也不等于 GPU 已停止或积分已经退回。
响应字段
成功返回任务对象,不统一包裹 code/request_id/data:
{"id":"example-task-id","status":"pending","intermediate_image_available":false,"compute":{"kind":"scheduler","phase":"queued"}}{"id":"example-task-id","status":"completed","intermediate_image_available":true}{"id":"example-task-id","status":"failed","error":{"message":"任务执行失败的具体原因"}}以上是字段形状示例,不是实际任务结果。
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 公开视频任务 ID;不是内部 ComfyUI prompt ID |
status | string | 当前常见 pending / completed / failed;不要依赖一定出现 running |
intermediate_image_available | boolean,可选 | 为 true 后才请求中间 PNG;Spicy/模板通常不提供 |
error.message | string,可选 | 异步任务失败原因;保留原任务信息便于排查 |
compute | object,可选 | 排队/执行遥测,字段可能为空或尚未产生 |
compute.phase | string | queued 或 assigned,不是完成百分比 |
compute.queue_wait_ms / run_ms | number,可选 | 毫秒;阶段切换会重新排队,不应冒充端到端耗时 |
compute.gpu_model / resource_label | string/null | 可公开的资源描述,不是 GPU 访问地址 |
compute.vram_required_mb | number/null | 调度预留估算,不是实测显存峰值 |
compute.vram_total_mb | number/null | 所分配 GPU 的总显存,不是本任务用量 |
部分响应可能包含 url,集成端应使用鉴权的 /content 下载,不依赖内部地址长期可用。接口没有承诺 progress、created_at、updated_at 或尺寸字段。
错误分类
Partner 入参/鉴权错误通常为 {"code":"...","data":null,"message":"..."};共享 Proxy 错误也可能使用 error 字段。按 HTTP 状态和实际 code/message/error 处理,不能只解析一种 envelope。
| HTTP / code | 含义 | 调用方处理 |
|---|---|---|
400 idempotency_key_required | 缺创建幂等键 | 补齐业务标识,不使用每次重试随机生成的新键 |
400 invalid_media_count / invalid_media_order | 文件数量或顺序不符合模式 | 检查身份图、身体图、源视频以及 template 二选一 |
400 媒体校验 | 格式、音轨、时长、帧数或尺寸不合法 | 修正源媒体,勿重复撞同一限制 |
401 invalid_api_key | Key 无效 | 检查环境及服务端配置,不在日志输出 Key |
403 model_not_allowed | 当前账户未开放该模型 | 查询授权,不枚举其他账户资源 |
404 | 路由或任务不存在/不可见 | 核对环境、账户、路径和任务 ID |
409 idempotency_conflict | 同一键的有效请求发生变化 | 恢复原请求,或确属新任务时用新业务键 |
409 结果尚未就绪 | 过早读取产物或任务存在状态冲突 | 查询原任务,不能笼统重建任务 |
422 reference_face_not_found / reference_multiple_faces | 身份图不符合单脸要求 | 换清晰单人身份图 |
503 reference_analyzer_unavailable | 自动路由分析服务未就绪 | 保留原幂等键重试,不自行指定内部模型 |
502/503 其他 | 上游拒绝、媒体校验或调度故障 | 保留原任务/幂等键和真实错误;未知接单结果先核对 |
SCAIL2 上游入参拒绝可能被共享层包装为 502 并附带上游 400 信息;不能仅因 HTTP 不是 400 就认定是可盲重试的网络故障。
完整 Python 示例
安装 requests。创建时必须显式设置 IDEMPOTENCY_KEY;重复执行脚本沿用同值。已取得任务 ID 时设置 TASK_ID,脚本只查询下载,不再次创建。示例支持 MP4/MOV、换人链路允许的 GIF、双身份图和 Spicy 模板模式。
import mimetypes
import os
import time
from contextlib import ExitStack
from pathlib import Path
import requests
def media_type(filename):
value = mimetypes.guess_type(filename)[0]
if value not in {"image/jpeg", "image/png", "image/bmp", "image/webp", "image/gif", "video/mp4", "video/quicktime"}:
raise ValueError("无法识别或不支持的媒体类型")
return value
def checked(response):
response.raise_for_status()
return response
def main():
base = os.environ.get("BASE_URL", "https://ic.xshow.live/api/partner/v1").rstrip("/")
model = os.environ.get("MODEL", "video-face-swap")
models = {"video-face-swap", "video-person-swap", "video-undress", "video-head-swap-spicy", "video-pose"}
if model not in models:
raise ValueError("不支持的公开模型")
task_id = os.environ.get("TASK_ID", "")
with requests.Session() as session:
session.headers["Authorization"] = "Bearer " + os.environ["PARTNER_API_KEY"]
if not task_id:
order_id = os.environ["IDEMPOTENCY_KEY"]
template = os.environ.get("TEMPLATE_ID", "")
data = {"model": model, "prompt": os.environ.get("PROMPT", "")}
if os.environ.get("SEED"):
data["seed"] = os.environ["SEED"]
paths = [] if model == "video-undress" else [os.environ.get("FACE_IMAGE", "identity.png")]
if model == "video-person-swap" and os.environ.get("BODY_IMAGE"):
paths.append(os.environ["BODY_IMAGE"])
if template:
if model != "video-head-swap-spicy":
raise ValueError("该模型不支持模板模式")
data["template_id"] = template
elif model != "video-pose":
source = os.environ.get("SOURCE_VIDEO", "source.mp4")
source_mime = media_type(source)
if source_mime == "image/gif" and model in {"video-head-swap-spicy", "video-face-swap", "video-undress"}:
raise ValueError("此视频模式不接收 GIF")
if source_mime not in {"video/mp4", "video/quicktime", "image/gif"}:
raise ValueError("源媒体必须是 MP4/MOV 或该模式支持的 GIF")
paths.append(source)
if model == "video-pose":
if not data["prompt"].strip():
raise ValueError("姿势模式必须设置 PROMPT")
data.update(lora=os.environ["POSE_LORA"], seconds="5", resolution_name="720p", ratio="9:16")
if os.environ.get("VIDEO_PROMPT"):
data["video_prompt"] = os.environ["VIDEO_PROMPT"]
with ExitStack() as stack:
files = [("input_reference[]", (Path(p).name, stack.enter_context(open(p, "rb")), media_type(p))) for p in paths]
result = checked(session.post(base + "/videos", headers={"Idempotency-Key": order_id}, data=data, files=files, timeout=1800)).json()
task_id = result["id"]
print("TASK_ID=" + task_id, flush=True)
deadline = time.monotonic() + 6 * 60 * 60
intermediate_saved = False
while time.monotonic() < deadline:
state = checked(session.post(f"{base}/videos/{task_id}", timeout=60)).json()
print("status=", state.get("status"), "compute=", state.get("compute"), flush=True)
if state.get("intermediate_image_available") and not intermediate_saved:
image = checked(session.get(f"{base}/videos/{task_id}/intermediate", timeout=600))
Path("intermediate.png").write_bytes(image.content)
intermediate_saved = True
if state.get("status") == "completed":
params = {"format": os.environ["OUTPUT_FORMAT"]} if os.environ.get("OUTPUT_FORMAT") else None
with checked(session.get(f"{base}/videos/{task_id}/content", params=params, timeout=1800, stream=True)) as video:
suffix = ".gif" if video.headers.get("Content-Type", "").split(";")[0] == "image/gif" else ".mp4"
with open("result" + suffix, "wb") as output:
for chunk in video.iter_content(1024 * 1024):
output.write(chunk)
print("saved: result" + suffix)
return
if state.get("status") in {"failed", "cancelled", "canceled", "expired"}:
raise RuntimeError(str(state.get("error") or state.get("status")))
time.sleep(5)
raise TimeoutError("停止客户端轮询;服务端任务不因此取消,请用 TASK_ID 继续查询")
if __name__ == "__main__":
main()正式接入还应保存业务订单与任务 ID、使用有限退避处理查询的临时网络错误,并完成本方超时与取消策略。示例不会因异常自动新建任务,不把中间图或 HTTP 200 当作最终视频质量验收。