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

视频 API

Partner 视频接口的输入顺序、逐模型限制、自动路由、输出尺寸、异步任务、错误与完整调用示例

Partner 视频 API(v3)

历史 v2 文档保留本次切换前的合同;历史 v1 文档保留更早的 SCAIL2 合同。

本页描述 Partner API 合同。XCamShow 工具箱的视频换脸卡片使用 model=video-face-swap 和受控 template_id;见工具箱实际调用对照。

本页面向 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)身份图、源视频;或身份图加受控 template_id无独立图片预编辑阶段服务端仅取源视频前 10 秒,Wan3.0 R2V;按实际处理时长、分辨率和帧率整理输出无
视频换人 video-person-swap头部身份图、可选身体图、源视频BFS 换头后 BodySwap 换身体Wan2.1 SCAIL2BodySwap 完成后的结果
视频编辑 video-undress源视频无独立图片预编辑阶段Wan3.0 视频编辑;按源视频时长、分辨率和帧率整理输出无
Spicy 视频换脸 video-head-swap-spicy身份图、源视频;或身份图加受控 template_id无独立图片预编辑阶段画布内部 /v1/video-head-swap-spicy → 前 10 秒 → Wan3.0 R2V无
视频姿势 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 别名)的公开模型名保持不变;画布 Partner 将它改写为内部 video-head-swap-spicy,底层使用 Wan3.0。工具箱只上传身份图和模板 ID:服务端从美国桶 spicy-cc-us 的受控字典路径取得视频;直接上传源视频时也由服务端先截取前 10 秒。两种模式都按身份图、截取后的源视频顺序送给 Wan3.0;不足 10 秒的有效源视频只处理已有部分,不补帧到 10 秒。video-undress 仍改写为 wan3.0-video,沿用自身的最长 15 秒规则。两条 Wan3 链路固定 duration_mode=match_source_not_longer 和 output_geometry_mode=match_source,完成后按实际处理片段整理时长、编码宽高及帧率。生成式模型仍可能改变画面内容;规格匹配不等于逐帧像素一致或原声逐样本一致。

管理后台 /admin/generations 按实际模型 video-head-swap-spicy 显示换脸任务,视频编辑仍为 wan3.0-video,用户是 Partner 绑定的共享账号;可用任务 ID 或 partner:xcamshow: 操作 ID 查找。held 表示仍在生成或等待结果,不代表记录缺失。旧任务仍按创建时使用的模型显示。

video-person-swap 保持原链路。旧 LTX 2.5 Spicy 画布模型保留为 video-head-swap-spicy-ltx25,原 Scheduler POST /v1/video-head-swap-spicy 未变;历史接口文档记录旧合同。新接口只接受服务端字典列出的 14 个模板 ID,不接受任意 URL 或 OSS Key。

输入限制

模式源媒体与时长帧率 / 音轨尺寸与输出
Wan3 换脸MP4/MOV,源视频最少 2 秒;服务端只处理前 10 秒保持源帧率,输出按请求包含生成音频匹配处理片段的时长、分辨率、帧率(10 秒上限)
Wan3 视频编辑MP4/MOV,源视频最少 2 秒;最多处理前 15 秒保持源帧率,输出按请求包含生成音频匹配处理片段的时长、分辨率、帧率(15 秒上限)
视频换人MP4/MOV,最多 15 秒最多 60 FPS,要求音轨保持现有 SCAIL2 画布合同
当前 Spicy 换脸源视频上传或受控模板 ID;最多处理前 10 秒按 Wan3 视频输入校验Wan3.0,匹配处理片段时长与尺寸
历史 LTX Spicy最长 10 秒;可使用历史模板模式仍受旧工作流校验仅 video-head-swap-spicy-ltx25
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,提示先导出无旋转版本重试;当前解码链路无法保证旋转源在图片阶段与视频阶段的方向一致。
  • 视频换人与历史 LTX Spicy 的音轨要求保持原合同;当前 Spicy 换脸和视频编辑以 Wan3 视频输入校验为准。
  • SCAIL2 的 seconds、fps、ratio、size 不用于任意改写源视频时长、帧率或生成画布。

GIF 输入与输出

video-person-swap 保留 GIF 作为最后一个源媒体 part 的旧合同,MIME 为 image/gif。Proxy 转换为带静音音轨的 MP4 后走原算法,仍须通过时长和帧数校验。当前 Wan3 换脸与视频编辑要求 MP4/MOV 视频文件。

当前 Spicy 换脸直接上传时第二个文件必须是 video/*,不要传 GIF;模板模式只有一张身份图和 template_id,不能同时上传源视频。

下载方式返回
原请求源为 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-face-swap/当前 Spicy 换脸只接受服务端字典中的 ID;传入时只能上传一张身份图,不得再上传源视频
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。

XCamShow 工具箱的模板模式(template_id 使用服务端字典中的 ID)只上传身份图:

curl --fail-with-body "$BASE_URL/videos" \
  -H "Authorization: Bearer $PARTNER_API_KEY" \
  -H "Idempotency-Key: $ORDER_ID" \
  -F "model=video-face-swap" \
  -F "template_id=missionary-15" \
  -F "input_reference[]=@identity.png;type=image/png"

旧 Scheduler 模板模式另见 LTX 2.5 文档。

查询、下载与取消

方法路径说明
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文件数量或顺序不符合模式检查身份图、身体图与源视频顺序
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 和双身份图;历史模板示例见 v2 归档。

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"]
            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 os.environ.get("TEMPLATE_ID"):
                raise ValueError("当前 Partner 版本不支持模板模式")
            if 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