无限画布无限画布
GPU API工具箱 API 文档Proxy Base URL(画布内部)视频类

视频换脸

视频换脸组合 GPU API 的请求、阶段、轮询与错误说明

文档更新时间:2026年09月08日 12时53分18秒(UTC+8)

视频换脸(组合 API)

最后更新:2026-09-07

项目值
modelvideo-face-swap
输入1 个源视频 + 1 张人脸图
内部 SchedulerPOST /v1/video-face-swap
中间输出1 张 BFS 换头后的首帧 PNG,可在任务运行期间读取
最终输出1 个经过容器签名与解码校验的视频

组合流程

首帧提取 → krea2_bfs_v1 换头 → 保存并暴露 BFS 中间 PNG → 将同一张新首帧送入 Wan2.1 SCAIL2 → 最终视频

当前组合工作流不再把 BFS 目标 latent 固定为 1:1。视频内部首帧尺寸由现有整数宽高节点传入;当前 SCAIL2 生产链为 512×896。BFS 参数与独立 krea2_bfs_v1 基线一致:steps=8、ref_boost=1、grounding_px=512,避免过强身份参考造成脸部过度风格化,也避免竖图被重排成方形三联构图。

intermediate_image_available 可能在最终视频仍为 pending/running 时就变为 true。调用方看到该标记后即可读取 /intermediate,不需要等到视频完成;最终完成时应复用已经保存的中间图,不要以同一稳定键重复上传。

默认提示词

提示词留空时,视频换脸和视频换人使用以下人物替换约束;它只替换人物并要求保留源视频场景,不把“人物在跳舞”当作动作或背景重绘指令:

Replace only the person in the source video with the person from the reference image. Preserve the reference person's facial identity, hairstyle, body shape, proportions, and clothing. Preserve the source video's original motion, pose sequence, facial motion, background, environment, objects, lighting, camera movement, framing, composition, timing, and duration. Do not modify or regenerate the scene. Keep the replacement person's identity, body shape, and clothing consistent across all frames.

用户提交非空提示词时仍原样使用用户内容。video-undress 与 scail2-person-swap 的空提示默认值保持 the woman is dancing。

创建任务

curl -X POST "$BASE_URL/api/proxy/v1/videos" \
  -H "Cookie: ic_session=$IC_SESSION" \
  -H "X-IC-Operation-ID: $(uuidgen)" \
  -F "model=video-face-swap" \
  -F "prompt=Replace only the person in the source video with the person from the reference image. Preserve the reference person's facial identity, hairstyle, body shape, proportions, and clothing. Preserve the source video's original motion, pose sequence, facial motion, background, environment, objects, lighting, camera movement, framing, composition, timing, and duration. Do not modify or regenerate the scene. Keep the replacement person's identity, body shape, and clothing consistent across all frames." \
  -F "seed=12345" \
  -F "input_reference[]=@face.png" \
  -F "input_reference[]=@source.mp4"

源视频支持 MP4/MOV、最大 500MB,并须包含可解码音轨;人物图支持 JPG/PNG/BMP/WEBP、单张最大 30MB。Proxy 会按 MIME 分类,不把文件名后缀当作唯一判断。

状态与结果

  1. 创建成功保存返回的 id。
  2. POST /api/proxy/v1/videos/{id} 轮询 pending/running/completed/failed。
  3. 任一轮询响应出现 intermediate_image_available: true 后,可立即调用 GET /api/proxy/v1/videos/{id}/intermediate 下载 BFS 中间 PNG。
  4. 完成后调用 GET /api/proxy/v1/videos/{id}/content 下载视频。
  5. 取消调用 POST /api/proxy/v1/videos/{id}/cancel;只有服务端确认取消后才视为终止。

生产单次验收样本为 13.833 秒、415 帧的竖版源视频,seed 77;排队 5.519s,BFS 中间图约在执行 17.680s 时可读,组合任务总执行 313.228s。中间图解码为 512×896 RGB PNG,最终视频完整返回。该数据是单次生产闭环,不是稳定 SLA 或 P50/P95。

完整 Python 调用

下面代码可直接保存为 call_video_face_swap.py。先执行 pip install requests,再设置 BASE_URL、IC_SESSION 并准备示例媒体。代码会提交任务、轮询、下载最终 MP4;若存在首帧预处理结果,也会下载为 intermediate.png。

import mimetypes
import os
import time
import uuid
from contextlib import ExitStack
from pathlib import Path

import requests

BASE_URL = os.environ.get("BASE_URL", "https://ic.xshow.live").rstrip("/")
IC_SESSION = os.environ["IC_SESSION"]
MODEL = "video-face-swap"
POLL_SECONDS = 5
TIMEOUT_SECONDS = 6 * 60 * 60
DEFAULT_PROMPT = "Replace only the person in the source video with the person from the reference image. Preserve the reference person's facial identity, hairstyle, body shape, proportions, and clothing. Preserve the source video's original motion, pose sequence, facial motion, background, environment, objects, lighting, camera movement, framing, composition, timing, and duration. Do not modify or regenerate the scene. Keep the replacement person's identity, body shape, and clothing consistent across all frames."

session = requests.Session()
session.cookies.set("ic_session", IC_SESSION)


def mime(path: str) -> str:
    value = mimetypes.guess_type(path)[0]
    if not value:
        raise ValueError(f"无法识别媒体 MIME: {path}")
    return value


def checked(response: requests.Response) -> requests.Response:
    if not response.ok:
        raise RuntimeError(f"HTTP {response.status_code}: {response.text[:1000]}")
    return response


with ExitStack() as stack:
    files = [
        ("input_reference[]", (Path("face.png").name, stack.enter_context(open("face.png", "rb")), mime("face.png"))),
        ("input_reference[]", (Path("source.mp4").name, stack.enter_context(open("source.mp4", "rb")), mime("source.mp4")))
    ]
    created = checked(session.post(
        f"{BASE_URL}/api/proxy/v1/videos",
        headers={"X-IC-Operation-ID": str(uuid.uuid4())},
        data={
            "model": MODEL,
            "prompt": DEFAULT_PROMPT,
            "seed": "12345",
        },
        files=files,
        timeout=1800,
    )).json()

task_id = created.get("id") or (created.get("data") or {}).get("id")
if not task_id:
    raise RuntimeError(f"创建响应缺少任务 ID: {created}")
print("task_id:", task_id)

deadline = time.time() + TIMEOUT_SECONDS
while time.time() < deadline:
    state = checked(session.post(
        f"{BASE_URL}/api/proxy/v1/videos/{task_id}",
        timeout=60,
    )).json()
    status = str(state.get("status") or (state.get("data") or {}).get("status") or "pending").lower()
    print("status:", status)
    payload = state.get("data") if isinstance(state.get("data"), dict) else state
    if payload.get("intermediate_image_available") is True and not Path("intermediate.png").exists():
        intermediate = checked(session.get(
            f"{BASE_URL}/api/proxy/v1/videos/{task_id}/intermediate",
            timeout=600,
        ))
        Path("intermediate.png").write_bytes(intermediate.content)
        print("saved: intermediate.png")

    if status == "completed":
        video = checked(session.get(
            f"{BASE_URL}/api/proxy/v1/videos/{task_id}/content",
            timeout=1800,
        ))
        Path("result.mp4").write_bytes(video.content)
        print("saved: result.mp4")

        if payload.get("intermediate_image_available") is True and not Path("intermediate.png").exists():
            intermediate = checked(session.get(
                f"{BASE_URL}/api/proxy/v1/videos/{task_id}/intermediate",
                timeout=600,
            ))
            Path("intermediate.png").write_bytes(intermediate.content)
            print("saved: intermediate.png")
        break
    if status in {"failed", "cancelled", "canceled", "expired"}:
        raise RuntimeError(f"任务失败: {state}")
    time.sleep(POLL_SECONDS)
else:
    raise TimeoutError(f"任务 {task_id} 超过 {TIMEOUT_SECONDS} 秒仍未完成")

阶段失败

错误会保留首帧提取、图片预编辑、SCAIL2 提交、GPU 执行或结果校验阶段的真实原因。400 通常是媒体数量/格式错误,503 是工作流未就绪,502 是上游或结果媒体校验失败。

On this page