视频 API
Partner 视频接口的输入顺序、逐模型限制、自动路由、输出尺寸、异步任务、错误与完整调用示例
Partner 视频 API
本页面向 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 | 身份图、源视频 | 提取首帧后自动选择 Klein 精准换头或 BFS | Wan2.1 SCAIL2 | 图片阶段最终结果 |
视频换人 video-person-swap | 头部身份图、可选身体图、源视频 | BFS 换头后 BodySwap 换身体 | Wan2.1 SCAIL2 | BodySwap 完成后的结果,不是仅 BFS 结果 |
视频编辑 video-undress | 源视频 | 工作流固定的首帧图片编辑 | Wan2.1 SCAIL2 | 图片编辑结果 |
Spicy 视频换头 video-head-swap-spicy | 身份图、源视频 | 身份图检测;需要时先 Avatar 裁切 | LTX 2.5 双 pass | 无 |
视频姿势 video-pose | 人物图 | Toolsbox 姿势编辑后执行标准 Klein 精准换头 | 20% 百炼 Wan3 下绿网 / 80% 本地 MiniMax H3 DaSiWa I2VA | 换头后的姿势关键帧 |
模板模式 video-face-swap 或 video-head-swap-spicy | 仅身份图,另传 template_id | 服务端解析指定模板 | LTX 2.5 双 pass | 无 |
视频换人传一张身份图时,头部和身体阶段复用该图;两张身份图不能交换顺序。所有组合仍只返回一个视频任务 ID,中间图不是另一个需要调用方提交的任务。
video-pose 的 prompt 与 lora 必填,video_prompt 可选;默认 720p / 9:16 / 5 秒。服务端先生成姿势图,再把姿势图作为目标底图、原始上传图作为头部身份参考,执行标准版 Klein 精准换头(head-swap,不启用 Pro);之后为每个新任务独立选择一次视频后端:20% 使用百炼 Wan3 下绿网,80% 使用本地 MiniMax H3 DaSiWa I2VA。Wan3 明确拒绝提交、明确任务失败或完成后缺少视频地址时,服务端自动使用同一张换头后姿势关键帧降级到本地 MiniMax;调用方始终只维护一个视频任务 ID。该链路不再使用 BFS,普通画布姿势图片编辑及 Partner image-pose 的 Klein 精准换脸合同不变。
若 Wan3 提交时发生网络中断、超时,或上游响应无法确认任务 ID,服务端不会立即降级,以免同一个订单同时产生两个付费视频任务;此时返回 outcome_unknown=true,调用方应保留原任务并人工核对,不得用新的 Idempotency-Key 盲目重提。
普通视频换脸的 Auto 路由
普通 video-face-swap 复用图片 head-swap-bfs 的 resolvePartnerHeadSwap,只分析第一张身份参考图:
- 恰好一个可靠人脸,人脸面积占整图不超过 50%:选择 Klein 精准换头。
- 恰好一个可靠人脸,面积超过 50%:选择 BFS。
- 没有可靠人脸或有多张人脸:返回
422,不盲目提交视频生成。 - 分析服务不可用:返回
503 reference_analyzer_unavailable,不静默切换默认模型。
首次选择持久化;相同幂等键的重试沿用原选择。Partner 接口不接受调用方控制内部 face_swap_model。网站 Proxy 与 Partner 共用自动选择规则,可信 Partner 已产生的结果不会在共享入口重复分析。
图片阶段使用已选工作流的固定换头提示词;公开 prompt 进入 SCAIL2 视频阶段,不代表替换了图片阶段提示词。它是生成式人物替换,不是逐帧只修改脸部像素,场景与身份质量仍需检查最终视频。
横屏、竖屏与方形
以下尺寸规则仅适用于普通上传模式的 video-face-swap 和 video-person-swap,不改变 Spicy、模板、video-undress 或其他模型合同。
服务端读取源视频的显示方向和像素宽高比,在最多 458752 像素(原 512×896 预算)、最长边 896 的范围内计算 32 对齐画布;首帧图片处理与最终 SCAIL2 使用同一组尺寸。内容等比例放入画布,以少量补边吸收尺寸对齐差异,不把横屏强裁成竖屏,也不保证输出原始分辨率。
| 源视频示例 | 处理画布 | 等比例内容 | 对齐补边 |
|---|---|---|---|
1920×1080 横屏 | 896×512 | 896×504 | 上下各约 4 像素 |
1080×1920 竖屏 | 512×896 | 504×896 | 左右各约 4 像素 |
1080×1080 方形 | 672×672 | 672×672 | 无 |
方向取视频显示信息,不是只比较编码宽高。最终媒体的实际尺寸、帧率和时长以下载文件为准;状态接口没有承诺 width、height 或完成百分比字段。升级前已在图片阶段运行的任务保留创建时尺寸,不因切换版本改变中间结果合同。
输入限制
| 模式 | 源媒体与时长 | 帧率 / 音轨 | 尺寸与输出 |
|---|---|---|---|
| 普通换脸 / 换人 | MP4/MOV,最多 15 秒;封装容差上限 15.05 秒 | 最多 60 FPS,要求音轨 | 按上述源比例规则;MP4 保留源帧率与音轨 |
video-undress | MP4/MOV,同上 | 同上 | 现有固定竖版 512×896,本次不改 |
| Spicy 上传模式 | MP4/MOV,最多 10 秒;容差 10.05 秒 | 要求音轨,仍受基础视频安全校验 | LTX 原工作流,不沿用 SCAIL2 尺寸规则 |
| 模板模式 | 不上传源视频;服务端使用模板前 10 秒 | 模板工作流决定 | LTX 原工作流 |
video-pose | 不上传源视频,上传 1 张人物图 | 由所选 H3 工作流生成 | 默认 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,提示先导出无旋转版本重试;当前解码链路无法保证旋转源在图片阶段与视频阶段的方向一致。 - 缺音轨的 MP4/MOV 不会被当作普通成功输入。GIF 转换是下述单独行为,不代表所有无声视频都会自动补音轨。
- SCAIL2 的
seconds、fps、ratio、size不用于任意改写源视频时长、帧率或生成画布。
GIF 输入与输出
Partner 的普通 video-face-swap、video-person-swap、video-undress 可将 GIF 作为最后一个源媒体 part,MIME 为 image/gif。Proxy 先转换为带静音音轨的 MP4,再走原算法;转换后的时长、帧数仍须通过视频限制。不能用 GIF 绕过时长或帧率校验。
Spicy 的 Partner 上传入口要求第二个文件是 video/*,不要给它传 GIF。模板模式不上传源视频,video-pose 也不接收源 GIF。
| 下载方式 | 返回 |
|---|---|
原请求源为 MP4/MOV,GET /videos/{id}/content | MP4 |
支持的普通模式源为 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 | 模板模式必填 | 与源视频二选一;仅用于上表两个模板模型 |
lora | string | 姿势模式必填 | 服务已开放的姿势 LoRA 标识,不是任意下载地址 |
video_prompt | string | 否 | 姿势模式的视频阶段提示词 |
seconds / resolution_name / ratio | string | 否 | 姿势模式参数;默认 5 / 720p / 9:16,不用于普通 SCAIL2 尺寸覆盖 |
业务订单放在 Idempotency-Key 请求头;没有 external_order_id 请求字段。不要传内部工作流路径、face_swap_model、供应商 Key 或 GPU 服务 Token。
创建示例
export BASE_URL="https://dev.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-face-swap 或 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、双身份图和模板模式。
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://dev.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 not in {"video-face-swap", "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 == "video-head-swap-spicy":
raise ValueError("Partner Spicy 上传模式不接收 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 当作最终视频质量验收。