File size: 9,861 Bytes
a8e08c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548c933
 
 
3dd1e63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548c933
 
a8e08c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548c933
 
 
 
 
 
 
 
 
 
3dd1e63
548c933
 
 
 
 
 
 
 
a8e08c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548c933
 
 
 
 
 
 
 
 
3dd1e63
 
548c933
 
 
 
 
 
 
 
a8e08c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
"""Two CPU text-to-motion models in one Space, one tab each.

Import order matters here and is not cosmetic. matplotlib's ft2font is a
pybind11 extension built against the system libstdc++ and fails to initialise if
a differently linked libstdc++ is already resident, so the drawing stack is
imported before anything native. Kimodo then dlopens libkimodo.so together with
its own bundled libgomp at generate time, while MoMask's torch, onnxruntime and
CLIP are held back behind momask_core._ensure_backends() until that tab is
actually used. A visitor who only generates with Kimodo never loads them.

Each pipeline keeps its own weights. Kimodo runs GGUF checkpoints through the
kimodo.cpp GGML port; MoMask runs its own ONNX graphs. Nothing is shared or
substituted between them.
"""

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402,F401

import gradio as gr  # noqa: E402

import kimodo_core as K  # noqa: E402
import momask_core as M  # noqa: E402

CSS = K.CSS + """
#mm_log textarea { font-family: ui-monospace, monospace; font-size: 0.8em; }
.tabs { gap: 0 !important; }
"""

HEADER = (f"CPU text to motion · **Kimodo** through "
          f"[kimodo.cpp](https://github.com/localai-org/kimodo.cpp) GGML, "
          f"**MoMask** through ONNX · {K.CPUS} threads")


def momask_generate(text, length, seed):
    """Generate a 3D skeletal animation from a text description of a motion.

    Runs MoMask on CPU through ONNX Runtime. Returns a rendered preview and a
    BVH file for Blender. Far cheaper than the Kimodo tab because there is no
    LLM2Vec encode: CLIP text encoding is part of the graph.

    Args:
        text: Plain English description of the motion, for example "a person walks forward".
        length: Clip length in seconds. 0 asks the length estimator to pick one.
        seed: Random seed. The same seed and text reproduce a clip exactly.
    """
    if not (text or "").strip():
        raise gr.Error("Describe a motion first, for example "
                       "\"a person walks forward\".")
    _joints, video_path, bvh_path = M.generate_motion(
        text, float(length or 0), int(seed) if seed is not None else None)
    if video_path is None:
        raise gr.Error("MoMask returned no motion. Try a shorter, plainer "
                       "description, or set Duration above 0.")
    return video_path, gr.update(value=bvh_path, interactive=bvh_path is not None)


def kimodo_example(prompt, model_label, frames, seed):
    """Examples vary the four columns worth varying; the advanced controls keep
    their defaults. Delegating to K.generate rather than duplicating it means a
    cached example is the same code path the Generate button runs.

    The BVH button is dropped from the yielded tuple on purpose. Caching an
    example whose outputs include a gr.DownloadButton crashes inside gradio:
    postprocess_data reaches components/base.py `if self.info:` and
    DownloadButton has no `info` attribute. Video, skeleton and log cache fine,
    and the button still works normally from Generate.
    """
    for video, glb, _bvh, status in K.generate(
            prompt, model_label, int(frames), 20, int(seed), 2.0, True):
        yield video, glb, status


def momask_example(text, length, seed):
    """Same reason as above: cache the preview, leave the download button out."""
    video, _bvh = momask_generate(text, length, seed)
    return video


def kimodo_tab():
    """Kimodo's own layout, unchanged apart from living inside a tab."""
    with gr.Row():
        with gr.Column(scale=4):
            prompt = gr.Textbox(
                value="a person runs forward and jumps over an obstacle",
                label="Describe the motion", lines=1, max_lines=1,
                placeholder="a person performs a spinning back kick")
            run = gr.Button(
                f"Generate ({K._duration(K.ENCODE_S + K.sampling_seconds(120, 20))})",
                variant="primary")
            status = gr.Textbox(lines=8, max_lines=K.LOG_LINES + 2,
                                autoscroll=True, show_label=False,
                                container=False, elem_id="log")
        with gr.Column(scale=3):
            model_label = gr.Dropdown(
                choices=[c.label for c in K.CHECKPOINTS],
                value=K.CHECKPOINTS[0].label, label="Motion model",
                info="SOMA is a 30 joint human rig, G1 a 34 joint Unitree "
                     "robot. RP and SEED are different checkpoints of each.")
            frames = gr.Slider(30, 240, value=120, step=10,
                               label=f"Frames ({120 / K.FPS:.1f} s clip)")
            with gr.Accordion("Advanced", open=False):
                with gr.Row():
                    steps = gr.Slider(4, 50, value=20, step=1, scale=3,
                                      label="Diffusion steps (20)",
                                      info="More steps, cleaner motion.")
                    seed = gr.Number(value=7, precision=0, label="Seed", scale=1)
                with gr.Row():
                    text_cfg = gr.Slider(1.0, 5.0, value=2.0, step=0.5, scale=3,
                                         label="Guidance")
                    deskate = gr.Checkbox(value=True, scale=1,
                                          label="Reduce foot skating")
        with gr.Column(scale=5):
            viewer = gr.Model3D(label="Skeleton", interactive=False, height=260)
            video = gr.Video(label="Preview", autoplay=True, height=180)
            bvh = gr.DownloadButton("Download BVH for Blender", size="sm",
                                    interactive=False)

    # Defined after the row because Examples needs every input component to exist.
    gr.Examples(
        examples=[
            ["a person walks forward at a steady pace", K.CHECKPOINTS[0].label, 120, 42],
            ["a person performs a roundhouse kick", K.CHECKPOINTS[0].label, 120, 123],
            ["a person sits down on a chair", K.CHECKPOINTS[0].label, 90, 456],
            ["a person dances happily in place", K.CHECKPOINTS[0].label, 150, 789],
            ["a person kicks with their right leg", K.CHECKPOINTS[2].label, 120, 101],
        ],
        inputs=[prompt, model_label, frames, seed],
        outputs=[video, viewer, status],
        fn=kimodo_example,
        # Lazy is the only sane mode here. Each distinct prompt pays a ~110 s
        # LLM2Vec encode, so eager would add roughly ten minutes to every build
        # and every wake, and one failure would take the whole Space down with it.
        # Lazy caches a row the first time someone clicks it, instantly after.
        cache_examples=True, cache_mode="lazy",
        label="Examples")

    for control in (prompt, frames, steps):
        control.change(K.control_labels, inputs=[prompt, frames, steps],
                       outputs=[frames, steps, run], queue=False,
                       api_visibility="private")
    run.click(K.generate,
              inputs=[prompt, model_label, frames, steps, seed, text_cfg, deskate],
              outputs=[video, viewer, bvh, status],
              api_name="generate").then(
        K.control_labels, inputs=[prompt, frames, steps],
        outputs=[frames, steps, run], queue=False, api_visibility="private")


def momask_tab():
    """Same shape as the Kimodo tab so switching tabs does not relayout the page:
    inputs left, settings middle, outputs right."""
    with gr.Row():
        with gr.Column(scale=4):
            text = gr.Textbox(value="a person walks forward",
                              label="Describe the motion", lines=1, max_lines=1,
                              placeholder="a person does a salsa dance")
            run = gr.Button("Generate (~10 s)", variant="primary")
        with gr.Column(scale=3):
            length = gr.Slider(0, 10, value=0, step=0.5,
                               label="Duration (0 = estimate it)",
                               info="Seconds. The length estimator picks one "
                                    "from the text when this is 0.")
            seed = gr.Number(value=42, precision=0, label="Seed")
        with gr.Column(scale=5):
            video = gr.Video(label="Preview", autoplay=True, height=260)
            bvh = gr.DownloadButton("Download BVH for Blender", size="sm",
                                    interactive=False)

    # Defined after the row because Examples needs every input component to exist.
    gr.Examples(
        examples=[
            ["A person walks forward", 0, 42],
            ["A person is running on a treadmill", 0, 123],
            ["A person jumps up and then lands", 0, 456],
            ["A person does a salsa dance", 0, 789],
            ["A person kicks with their right leg", 0, 101],
        ],
        inputs=[text, length, seed], outputs=[video],
        fn=momask_example,
        # Lazy, not eager. Eager runs every row before the Space serves its first
        # request, and it would force torch, onnxruntime and CLIP resident at
        # startup for every visitor, which is exactly what _ensure_backends()
        # exists to avoid. Lazy caches each row the first time it is clicked and
        # serves it instantly to everyone after that.
        cache_examples=True, cache_mode="lazy",
        label="Examples")

    run.click(momask_generate, inputs=[text, length, seed],
              outputs=[video, bvh], api_name="momask_generate")


with gr.Blocks(title="Kimodo cpp + MoMask", fill_width=True) as demo:
    gr.Markdown(HEADER, elem_id="foot")
    with gr.Tabs():
        with gr.Tab("Kimodo (GGML)"):
            kimodo_tab()
        with gr.Tab("MoMask (ONNX)"):
            momask_tab()

if __name__ == "__main__":
    demo.queue(max_size=8, default_concurrency_limit=1).launch(
        css=CSS, mcp_server=True)