File size: 5,279 Bytes
49d36c0
 
 
 
 
 
 
 
 
 
 
 
 
 
5c60fe5
49d36c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b34cf7
49d36c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c60fe5
49d36c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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()