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

视频 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 精准换头或 BFSWan2.1 SCAIL2图片阶段最终结果
视频换人 video-person-swap头部身份图、可选身体图、源视频BFS 换头后 BodySwap 换身体Wan2.1 SCAIL2BodySwap 完成后的结果,不是仅 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×512896×504上下各约 4 像素
1080×1920 竖屏512×896504×896左右各约 4 像素
1080×1080 方形672×672672×672无

方向取视频显示信息,不是只比较编码宽高。最终媒体的实际尺寸、帧率和时长以下载文件为准;状态接口没有承诺 width、height 或完成百分比字段。升级前已在图片阶段运行的任务保留创建时尺寸,不因切换版本改变中间结果合同。

输入限制

模式源媒体与时长帧率 / 音轨尺寸与输出
普通换脸 / 换人MP4/MOV,最多 15 秒;封装容差上限 15.05 秒最多 60 FPS,要求音轨按上述源比例规则;MP4 保留源帧率与音轨
video-undressMP4/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}/contentMP4
支持的普通模式源为 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模板模式必填与源视频二选一;仅用于上表两个模板模型
lorastring姿势模式必填服务已开放的姿势 LoRA 标识,不是任意下载地址
video_promptstring否姿势模式的视频阶段提示词
seconds / resolution_name / ratiostring否姿势模式参数;默认 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":"任务执行失败的具体原因"}}

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

字段类型说明
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、双身份图和模板模式。

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 当作最终视频质量验收。

On this page