Nekochu's picture
keep the download button out of cached examples, gradio crashes on it
3dd1e63
Raw History Blame Contribute Delete
9.86 kB
"""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)