无限画布无限画布
GPU API工具箱 API 文档Partner API版本归档v2 历史文档

视频 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 SCAIL2BodySwap 完成后的结果
视频编辑 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}/contentMP4
video-person-swap 源为 GIF,GET /videos/{id}/contentGIF
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。

参数类型必填说明
modelstring是使用上表公开模型名
input_reference[]重复 file是上传顺序和数量严格按模式表
promptstring姿势模式必填普通换脸/换人为空时使用保留身份、动作、场景、构图的默认约束;非空原样用于视频阶段
seed十进制整数文本否省略时沿用工作流默认值;显式值会按执行节点的合法范围处理,重试不可改值
template_idstring模板模式必填与源视频二选一;仅用于 video-head-swap-spicy
lorastring姿势模式必填服务已开放的姿势 LoRA 标识,不是任意下载地址
video_promptstring否姿势模式的视频阶段提示词
seconds / resolution_name / ratiostring否姿势模式参数;默认 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":"任务执行失败的具体原因"}}

以上是字段形状示例,不是实际任务结果。

字段类型说明
idstring公开视频任务 ID;不是内部 ComfyUI prompt ID
statusstring当前常见 pending / completed / failed;不要依赖一定出现 running
intermediate_image_availableboolean,可选为 true 后才请求中间 PNG;Spicy/模板通常不提供
error.messagestring,可选异步任务失败原因;保留原任务信息便于排查
computeobject,可选排队/执行遥测,字段可能为空或尚未产生
compute.phasestringqueued 或 assigned,不是完成百分比
compute.queue_wait_ms / run_msnumber,可选毫秒;阶段切换会重新排队,不应冒充端到端耗时
compute.gpu_model / resource_labelstring/null可公开的资源描述,不是 GPU 访问地址
compute.vram_required_mbnumber/null调度预留估算,不是实测显存峰值
compute.vram_total_mbnumber/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_keyKey 无效检查环境及服务端配置,不在日志输出 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 当作最终视频质量验收。

On this page