cs686's picture
Deploy GEM-X ZeroGPU motion capture
5c60fe5 verified
Raw
History Blame Contribute Delete
5.28 kB
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
import gradio as gr
import spaces
PROJECT_ROOT = Path(__file__).resolve().parent
MAX_SECONDS = 4
def _run(command: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(
command,
cwd=PROJECT_ROOT,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
def _prepare_video(source: str, destination: Path) -> None:
_run(
[
"ffmpeg",
"-y",
"-i",
source,
"-t",
str(MAX_SECONDS),
"-vf",
"fps=30,scale='min(720,iw)':-2:force_original_aspect_ratio=decrease",
"-an",
"-c:v",
"libx264",
"-preset",
"veryfast",
"-pix_fmt",
"yuv420p",
str(destination),
]
)
@spaces.GPU(duration=120)
def capture_motion(
video_path: str | None,
camera_mode: str,
use_ddim: bool,
progress=gr.Progress(track_tqdm=True),
):
if not video_path:
raise gr.Error("请先上传一段视频。")
progress(0.03, desc="准备视频")
job_dir = Path(tempfile.mkdtemp(prefix="gemx_", dir="/tmp"))
input_path = job_dir / "input.mp4"
output_root = job_dir / "outputs"
try:
_prepare_video(video_path, input_path)
progress(0.1, desc="运行 GEM-X")
command = [
sys.executable,
str(PROJECT_ROOT / "space_runner.py"),
"--video",
str(input_path),
"--output-root",
str(output_root),
]
if camera_mode == "固定相机":
command.append("--static-camera")
if use_ddim:
command.append("--ddim")
result = _run(command)
marker = "GEMX_RESULT="
result_line = next(
(line[len(marker) :] for line in result.stdout.splitlines() if line.startswith(marker)),
None,
)
if result_line is None:
raise RuntimeError("推理已结束,但没有返回结果清单。\n" + result.stdout[-4000:])
files = json.loads(result_line)
progress(0.95, desc="整理下载文件")
file_keys = ("preview", "bvh77", "bvh78", "raw", "archive")
missing = [name for name in file_keys if not Path(files[name]).exists()]
if missing:
raise RuntimeError(f"缺少输出文件:{', '.join(missing)}")
summary = (
f"完成:处理 {files['frames']} 帧,{files['fps']:.2f} FPS。"
" Blender 推荐导入 soma77_blender.bvh;"
" soma78_virtual_root.bvh 保留 GEM-X 虚拟 Root。"
)
progress(1.0, desc="完成")
return (
files["preview"],
files["bvh77"],
files["bvh78"],
files["raw"],
files["archive"],
summary,
)
except subprocess.CalledProcessError as exc:
shutil.rmtree(job_dir, ignore_errors=True)
details = (exc.stdout or str(exc))[-6000:]
raise gr.Error("GEM-X 运行失败:\n" + details) from exc
except Exception as exc:
shutil.rmtree(job_dir, ignore_errors=True)
raise gr.Error(str(exc)) from exc
with gr.Blocks(title="GEM-X Motion Capture") as demo:
gr.Markdown(
"""
# GEM-X 视频动作捕捉
上传单人短视频,生成 NVIDIA SOMA 全身动作和 Blender BVH。
为适配 ZeroGPU,视频会自动截取前 4 秒、转为 30 FPS。
"""
)
with gr.Row():
with gr.Column(scale=1):
video = gr.Video(label="输入视频", sources=["upload"])
camera = gr.Radio(
["固定相机", "移动相机"],
value="固定相机",
label="相机类型",
)
ddim = gr.Checkbox(
value=False,
label="DDIM 50 步(更慢,兼容模式下姿态通常更好)",
)
run_button = gr.Button("生成动作", variant="primary")
with gr.Column(scale=1):
preview = gr.Video(label="77 点跟踪预览")
status = gr.Textbox(label="状态", interactive=False)
with gr.Row():
bvh77 = gr.File(label="Blender BVH(77 关节)")
bvh78 = gr.File(label="SOMA BVH(78 节点,含虚拟 Root)")
raw = gr.File(label="GEM-X 原始结果 (.pt)")
archive = gr.File(label="全部结果 (.zip)")
gr.Markdown(
"""
Blender:`文件 → 导入 → Motion Capture (.bvh)`。通常先用 77 关节版本,
再通过 Blender 的 Retarget 工具或插件映射到你的角色骨架。
建议画面中只有一个人,身体和双手尽量完整可见。首次运行需要下载模型,
会明显慢于后续运行。
"""
)
run_button.click(
capture_motion,
inputs=[video, camera, ddim],
outputs=[preview, bvh77, bvh78, raw, archive, status],
)
demo.queue(max_size=8, default_concurrency_limit=1)
if __name__ == "__main__":
demo.launch()